#include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace stellar_equilibrium_test_utils { using Form = mean_field::utils::blocks::barotropic_equilibrium_form; using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; namespace field = mean_field::field; struct FieldMaps final { field::FieldDofMap density; field::FieldDofMap displacement; field::FieldDofMap gravityFlux; field::FieldDofMap gravityPotential; field::FieldDofMap enthalpy; explicit FieldMaps(const mean_field::fem::FEM &f) : density(field::make_field_dof_map( *f.densityFes)), displacement( field::make_field_dof_map( *f.displacementFes)), gravityFlux(field::make_field_dof_map( *f.gravityFluxFes)), gravityPotential( field::make_field_dof_map( *f.gravityPotentialFes)), enthalpy(field::make_field_dof_map( *f.enthalpyFes)) {} }; constexpr auto densityValue = mean_field::utils::blocks::get_value_block
( mean_field::utils::blocks::density_field.mass_term); constexpr auto displacementValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::displacement_field.geometry_term); constexpr auto gravityGradientValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::gravity_field.poisson_term); constexpr auto enthalpyValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::enthalpy_field.specific_term); constexpr auto bernoulliValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::barotropic_constant_field .mass_normalization_term); constexpr auto gravityGradientResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::gravity_field.poisson_term); constexpr auto densityResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::density_field.mass_term); constexpr auto displacementResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::displacement_field.geometry_term); constexpr auto enthalpyResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::enthalpy_field.specific_term); constexpr auto massResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::barotropic_constant_field .mass_normalization_term); template [[nodiscard]] mfem::Vector value_view(mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::value_block block) { return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block)); } template void assign_value_block( mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::value_block block, const mfem::Vector &source) { MFEM_VERIFY(source.Size() == layout.size(block), "Source vector has the wrong size for the coupled value block."); const int offset = layout.offset(block); for (int dof = 0; dof < source.Size(); ++dof) { vector(offset + dof) = source(dof); } } template [[nodiscard]] mfem::Vector const_value_view(const mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::value_block block) { return mfem::Vector(const_cast(vector.GetData()) + layout.offset(block), layout.size(block)); } template [[nodiscard]] mfem::Vector residual_view(mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::residual_block block) { return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block)); } template [[nodiscard]] mfem::Vector const_residual_view( const mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::residual_block block) { return mfem::Vector(const_cast(vector.GetData()) + layout.offset(block), layout.size(block)); } [[nodiscard]] mfem::Vector reduce_density(const mean_field::fem::FEM &f, const mfem::Vector &fullDensity) { const field::FieldDofMap map = field::make_field_dof_map(*f.densityFes); return map.gather(fullDensity); } [[nodiscard]] mfem::Vector reduce_enthalpy(const mean_field::fem::FEM &f, const mfem::Vector &fullEnthalpy) { const field::FieldDofMap map = field::make_field_dof_map(*f.enthalpyFes); return map.gather(fullEnthalpy); } [[nodiscard]] mfem::Vector pack_gravity_state( const mfem::Vector &density, const mfem::Vector &displacement, const mfem::Vector &gravityGradient, const mfem::Vector &gravityPotential) { const std::array blockSizes{density.Size(), displacement.Size(), gravityGradient.Size(), gravityPotential.Size()}; const std::array offsets{ 0, blockSizes[0], blockSizes[0] + blockSizes[1], blockSizes[0] + blockSizes[1] + blockSizes[2], blockSizes[0] + blockSizes[1] + blockSizes[2] + blockSizes[3]}; mfem::Vector packed(offsets[4]); const std::array blocks{ &density, &displacement, &gravityGradient, &gravityPotential}; for (int block = 0; block < 4; ++block) { mfem::Vector destination(packed.GetData() + offsets[block], blockSizes[block]); destination = *blocks[block]; } return packed; } [[nodiscard]] mfem::Vector project_density(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.densityFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.88 + 0.07 * std::sin(0.73 * position(0) + phase) + 0.05 * std::cos(0.59 * position(1) - phase) + 0.025 * position(2) * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_density_direction(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.densityFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.16 * std::sin(0.91 * position(0) + phase) - 0.12 * std::cos(0.77 * position(1) - phase) + 0.06 * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_constant_density(const mean_field::fem::FEM &f, const double value) { mfem::ParGridFunction field(f.densityFes.get()); mfem::ConstantCoefficient coefficient(value); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_displacement(const mean_field::fem::FEM &f, const double scale) { return gravity_prepared_test_utils::make_displacement(f, scale); } [[nodiscard]] mfem::Vector project_displacement_direction(const mean_field::fem::FEM &f, const double scale) { mfem::ParGridFunction field(f.displacementFes.get()); mfem::VectorFunctionCoefficient coefficient( f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = scale * (0.06 * position(0) + 0.014 * position(1) * position(2)); value(1) = scale * (-0.045 * position(1) + 0.011 * position(0) * position(2)); value(2) = scale * (0.035 * position(2) - 0.009 * position(0) * position(1)); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_gravity_gradient(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.gravityFluxFes.get()); mfem::VectorFunctionCoefficient coefficient( f.mesh->Dimension(), [phase](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.27 + 0.07 * position(0) + 0.025 * phase * position(1); value(1) = -0.19 + 0.055 * position(1) - 0.018 * phase * position(2); value(2) = 0.21 - 0.045 * position(2) + 0.021 * phase * position(0); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_gravity_direction(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.gravityFluxFes.get()); mfem::VectorFunctionCoefficient coefficient( f.mesh->Dimension(), [phase](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.13 * std::sin(position(0) + phase) + 0.025 * position(1); value(1) = -0.10 * std::cos(position(1) - phase) + 0.035 * position(2); value(2) = 0.08 * std::sin(position(2) + 0.5 * phase) - 0.018 * position(0); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_gravity_potential(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.gravityPotentialFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.24 + 0.09 * std::sin(0.67 * position(0) + phase) - 0.06 * std::cos(0.53 * position(1) - phase) + 0.035 * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_potential_direction(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.gravityPotentialFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.17 * std::sin(0.81 * position(0) + phase) + 0.11 * std::cos(0.69 * position(1) - phase) - 0.07 * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_enthalpy(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.enthalpyFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.82 + 0.08 * std::sin(0.62 * position(0) + phase) + 0.045 * std::cos(0.57 * position(1) - phase) + 0.02 * position(2) * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_enthalpy_direction(const mean_field::fem::FEM &f, const double phase) { mfem::ParGridFunction field(f.enthalpyFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.21 * std::sin(0.74 * position(0) + phase) - 0.14 * std::cos(0.64 * position(1) - phase) + 0.075 * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector project_constant_scalar(mfem::ParFiniteElementSpace &finiteElementSpace, const double value) { mfem::ParGridFunction field(&finiteElementSpace); mfem::ConstantCoefficient coefficient(value); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mean_field::physics::RigidRotation make_rotation(const double scale) { mfem::Vector angularVelocity(3); angularVelocity(0) = scale * 0.16; angularVelocity(1) = scale * -0.08; angularVelocity(2) = scale * 0.58; mfem::Vector center(3); center(0) = 0.03; center(1) = -0.025; center(2) = 0.015; return mean_field::physics::RigidRotation(angularVelocity, center); } [[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() { return make_rotation(0.0); } [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() { return {.discretization = {.identity = 1009, .revision = 3}, .density = {.identity = 1013, .revision = 5}, .displacement = {.identity = 1019, .revision = 7}, .gravityGradient = {.identity = 1021, .revision = 11}, .gravityPotential = {.identity = 1031, .revision = 13}, .enthalpy = {.identity = 1033, .revision = 17}, .bernoulliConstant = {.identity = 1039, .revision = 19}, .rotation = {.identity = 1049, .revision = 23}, .targetMass = {.identity = 1051, .revision = 29}}; } void increment_all_state_revisions( mean_field::operators::StellarEquilibriumDependencies &dependencies) { ++dependencies.density.revision; ++dependencies.displacement.revision; ++dependencies.gravityGradient.revision; ++dependencies.gravityPotential.revision; ++dependencies.enthalpy.revision; ++dependencies.bernoulliConstant.revision; } [[nodiscard]] mfem::Vector make_state(const mean_field::fem::FEM &f, const mean_field::operators::StellarEquilibriumLayout &layout) { const FieldMaps maps(f); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; { const mfem::Vector fullDensity = project_density(f, 0.31); const mfem::Vector reducedDensity = maps.density.gather(fullDensity); assign_value_block(state, layout, densityValue, reducedDensity); } assign_value_block(state, layout, displacementValue, project_displacement(f, 0.63)); assign_value_block(state, layout, gravityGradientValue, project_gravity_gradient(f, 0.43)); assign_value_block(state, layout, gravityPotentialValue, project_gravity_potential(f, 0.47)); { const mfem::Vector fullEnthalpy = project_enthalpy(f, 0.53); const mfem::Vector reducedEnthalpy = maps.enthalpy.gather(fullEnthalpy); assign_value_block(state, layout, enthalpyValue, reducedEnthalpy); } value_view(state, layout, bernoulliValue)(0) = 1.07; return state; } [[nodiscard]] mfem::Vector make_direction(const mean_field::fem::FEM &f, const mean_field::operators::StellarEquilibriumLayout &layout) { const FieldMaps maps(f); mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; { const mfem::Vector fullDensityDirection = project_density_direction(f, 0.61); const mfem::Vector reducedDensityDirection = maps.density.gather(fullDensityDirection); assign_value_block(direction, layout, densityValue, reducedDensityDirection); } assign_value_block(direction, layout, displacementValue, project_displacement_direction(f, 0.79)); assign_value_block(direction, layout, gravityGradientValue, project_gravity_direction(f, 0.83)); assign_value_block(direction, layout, gravityPotentialValue, project_potential_direction(f, 0.89)); { const mfem::Vector fullEnthalpyDirection = project_enthalpy_direction(f, 0.97); const mfem::Vector reducedEnthalpyDirection = maps.enthalpy.gather(fullEnthalpyDirection); assign_value_block(direction, layout, enthalpyValue, reducedEnthalpyDirection); } value_view(direction, layout, bernoulliValue)(0) = -0.37; return direction; } [[nodiscard]] double global_norm(const mfem::Vector &vector, const MPI_Comm communicator) { return gravity_prepared_test_utils::global_norm(vector, communicator); } [[nodiscard]] double relative_difference(const mfem::Vector &left, const mfem::Vector &right, const MPI_Comm communicator) { mfem::Vector difference(left); difference -= right; const double scale = std::max( {global_norm(left, communicator), global_norm(right, communicator), 100.0 * std::numeric_limits::epsilon()}); return global_norm(difference, communicator) / scale; } [[nodiscard]] mfem::Vector explicit_residual( const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator, const mean_field::fem::FEM &f, const mfem::Vector &state) { const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout(); const mfem::Vector reducedDensity = const_value_view(state, layout, densityValue); const mfem::Vector displacement = const_value_view(state, layout, displacementValue); const mfem::Vector gravityGradient = const_value_view(state, layout, gravityGradientValue); const mfem::Vector gravityPotential = const_value_view(state, layout, gravityPotentialValue); const mfem::Vector gravityState = pack_gravity_state( reducedDensity, displacement, gravityGradient, gravityPotential); mfem::Vector gravity; mfem::Vector closure; mfem::Vector displacementResidualValue; mfem::Vector hydrostatic; mfem::Vector mass; stellarOperator.GetGravityOperator().Mult(gravityState, gravity); stellarOperator.GetBarotropicClosureOperator().BuildResidual(closure); stellarOperator.GetDisplacementOperator().BuildResidual( displacementResidualValue); stellarOperator.GetHydrostaticOperator().BuildResidual(hydrostatic); stellarOperator.GetMassNormalizationOperator().BuildResidual(mass); mfem::Vector result(layout.residual_offsets().Last()); result = 0.0; MFEM_VERIFY(gravity.Size() == layout.size(gravityGradientResidual) + layout.size(gravityPotentialResidual), "Explicit gravity residual has the wrong size."); const mfem::Vector gravityGradientResidualValue( gravity.GetData(), layout.size(gravityGradientResidual)); const mfem::Vector gravityPotentialResidualValue( gravity.GetData() + layout.size(gravityGradientResidual), layout.size(gravityPotentialResidual)); residual_view(result, layout, gravityGradientResidual) = gravityGradientResidualValue; residual_view(result, layout, gravityPotentialResidual) = gravityPotentialResidualValue; residual_view(result, layout, densityResidual) = closure; residual_view(result, layout, displacementResidual) = displacementResidualValue; residual_view(result, layout, enthalpyResidual) = hydrostatic; residual_view(result, layout, massResidual) = mass; return result; } [[nodiscard]] mfem::Vector explicit_jacobian_action( const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator, const mfem::Vector &direction) { const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout(); const mfem::Vector reducedDensityDirection = const_value_view(direction, layout, densityValue); const mfem::Vector displacementDirection = const_value_view(direction, layout, displacementValue); const mfem::Vector gravityGradientDirection = const_value_view(direction, layout, gravityGradientValue); const mfem::Vector gravityPotentialDirection = const_value_view(direction, layout, gravityPotentialValue); const mfem::Vector reducedEnthalpyDirection = const_value_view(direction, layout, enthalpyValue); const mfem::Vector bernoulliDirection = const_value_view(direction, layout, bernoulliValue); const mfem::Vector gravityDirection = pack_gravity_state(reducedDensityDirection, displacementDirection, gravityGradientDirection, gravityPotentialDirection); mfem::Vector gravityAction; mfem::Vector closureAction; mfem::Vector displacementAction; mfem::Vector hydrostaticAction; mfem::Vector massAction; stellarOperator.GetGravityJacobianOperator().Mult(gravityDirection, gravityAction); stellarOperator.GetBarotropicClosureOperator().Mult( reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, closureAction); stellarOperator.GetDisplacementOperator().ApplyCompleteJacobianAction( reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection, displacementAction); stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction( reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection, hydrostaticAction); stellarOperator.GetMassNormalizationOperator().ApplyCompleteJacobianAction( reducedDensityDirection, displacementDirection, massAction); mfem::Vector result(layout.residual_offsets().Last()); result = 0.0; MFEM_VERIFY(gravityAction.Size() == layout.size(gravityGradientResidual) + layout.size(gravityPotentialResidual), "Explicit gravity Jacobian action has the wrong size."); const mfem::Vector gravityGradientAction( gravityAction.GetData(), layout.size(gravityGradientResidual)); const mfem::Vector gravityPotentialAction( gravityAction.GetData() + layout.size(gravityGradientResidual), layout.size(gravityPotentialResidual)); residual_view(result, layout, gravityGradientResidual) = gravityGradientAction; residual_view(result, layout, gravityPotentialResidual) = gravityPotentialAction; residual_view(result, layout, densityResidual) = closureAction; residual_view(result, layout, displacementResidual) = displacementAction; residual_view(result, layout, enthalpyResidual) = hydrostaticAction; residual_view(result, layout, massResidual) = massAction; return result; } [[nodiscard]] long long global_sum(const int localValue, const MPI_Comm communicator) { const long long local = static_cast(localValue); long long global = 0; MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, communicator); return global; } template [[nodiscard]] double block_relative_difference( const mfem::Vector &left, const mfem::Vector &right, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::residual_block block, const MPI_Comm communicator) { return relative_difference(const_residual_view(left, layout, block), const_residual_view(right, layout, block), communicator); } } // namespace stellar_equilibrium_test_utils TEST_CASE( "Prepared Stellar Equilibrium Uses Supported Field DOFs For Solver Blocks", tags::barotrope &tags::prepared &tags::field &tags::unit) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, 1.0); const auto &layout = stellarOperator.GetLayout(); const stellar_equilibrium_test_utils::FieldMaps maps(f); CHECK(layout.size(stellar_equilibrium_test_utils::densityValue) == maps.density.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::displacementValue) == maps.displacement.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::gravityGradientValue) == maps.gravityFlux.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::gravityPotentialValue) == maps.gravityPotential.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::enthalpyValue) == maps.enthalpy.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::bernoulliValue) == 1); CHECK(layout.size(stellar_equilibrium_test_utils::gravityGradientResidual) == maps.gravityFlux.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::gravityPotentialResidual) == maps.gravityPotential.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::densityResidual) == maps.density.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::displacementResidual) == maps.displacement.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::enthalpyResidual) == maps.enthalpy.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::massResidual) == 1); CHECK(maps.displacement.is_identity()); CHECK(maps.gravityFlux.is_identity()); CHECK(maps.gravityPotential.is_identity()); CHECK(stellarOperator.Width() == layout.value_offsets().Last()); CHECK(stellarOperator.Height() == layout.residual_offsets().Last()); const MPI_Comm communicator = f.mesh->GetComm(); const long long globalDensityFull = stellar_equilibrium_test_utils::global_sum(maps.density.full_size(), communicator); const long long globalDensityReduced = stellar_equilibrium_test_utils::global_sum(maps.density.reduced_size(), communicator); const long long globalEnthalpyFull = stellar_equilibrium_test_utils::global_sum(maps.enthalpy.full_size(), communicator); const long long globalEnthalpyReduced = stellar_equilibrium_test_utils::global_sum(maps.enthalpy.reduced_size(), communicator); INFO("Global density full true DOFs = " << globalDensityFull); INFO("Global density solver DOFs = " << globalDensityReduced); INFO("Global enthalpy full true DOFs = " << globalEnthalpyFull); INFO("Global enthalpy solver DOFs = " << globalEnthalpyReduced); REQUIRE(globalDensityFull > 0); REQUIRE(globalEnthalpyFull > 0); CHECK(globalDensityReduced > 0); CHECK(globalDensityReduced < globalDensityFull); CHECK(globalEnthalpyReduced > 0); CHECK(globalEnthalpyReduced < globalEnthalpyFull); } TEST_CASE("Prepared Stellar Equilibrium Jacobian Is The Exact Restricted Full " "Child Jacobian", tags::barotrope_prepared_jacobian_accuracy &tags::field) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, 1.19); const auto &layout = stellarOperator.GetLayout(); const mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout); const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout); const auto dependencies = stellar_equilibrium_test_utils::make_dependencies(); const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.82); stellarOperator.Prepare(state, dependencies, rotation); mfem::Vector rootAction; stellarOperator.Mult(direction, rootAction); const mfem::Vector explicitAction = stellar_equilibrium_test_utils::explicit_jacobian_action(stellarOperator, direction); const double difference = stellar_equilibrium_test_utils::relative_difference( rootAction, explicitAction, f.mesh->GetComm()); INFO("Reduced root versus explicit R J P relative difference = " << difference); CHECK(difference < 2.0e-15); } TEST_CASE( "Prepared Stellar Equilibrium Owns And Composes Every Fixed Residual Row", tags::barotrope &tags::prepared &tags::integration &tags::residuals) { using Operator = mean_field::operators::PreparedStellarEquilibriumOperator; STATIC_REQUIRE_FALSE(std::is_copy_constructible_v); STATIC_REQUIRE_FALSE(std::is_copy_assignable_v); STATIC_REQUIRE_FALSE(std::is_move_constructible_v); STATIC_REQUIRE_FALSE(std::is_move_assignable_v); mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); Operator stellarOperator(f, *f.domainMapperStateless, barotrope, 1.13); const mfem::Vector state = stellar_equilibrium_test_utils::make_state( f, stellarOperator.GetLayout()); const auto dependencies = stellar_equilibrium_test_utils::make_dependencies(); const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.81); const auto report = stellarOperator.Prepare(state, dependencies, rotation); CHECK(report.gravity.DidAnyWork()); CHECK(report.barotropicClosure.DidAnyWork()); CHECK(report.hydrostatic.DidAnyWork()); CHECK(report.displacement.DidAnyWork()); CHECK(report.massNormalization.DidAnyWork()); CHECK(report.assembledResidual); CHECK(stellarOperator.IsPrepared()); CHECK(&stellarOperator.GetGravityOperator().GetLinearizationContext() == &stellarOperator.GetGravityContext()); CHECK(&stellarOperator.GetDisplacementOperator().GetGravityContext() == &stellarOperator.GetGravityContext()); CHECK(&stellarOperator.GetMassNormalizationOperator().GetGravityContext() == &stellarOperator.GetGravityContext()); CHECK(&stellarOperator.GetBarotropicClosureOperator().GetContext() == &stellarOperator.GetBarotropicClosureContext()); mfem::Vector coupledResidual; stellarOperator.BuildResidual(coupledResidual); const mfem::Vector expected = stellar_equilibrium_test_utils::explicit_residual(stellarOperator, f, state); CHECK(stellar_equilibrium_test_utils::relative_difference( coupledResidual, expected, f.mesh->GetComm()) < 2.0e-15); CHECK(stellarOperator.Width() == stellarOperator.GetLayout().value_offsets().Last()); CHECK(stellarOperator.Height() == stellarOperator.GetLayout().residual_offsets().Last()); } TEST_CASE("Prepared Stellar Equilibrium Has Exact Analytic Closure Hydrostatic " "And Mass Rows", tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(1.0, 0.25); constexpr double enthalpy = 0.60; const double density = barotrope.density_from_enthalpy(enthalpy); constexpr double gravityPotential = 0.20; constexpr double bernoulliConstant = enthalpy + gravityPotential; const mean_field::mapping::COORDINATE_SPACE volumeCoordinates = f.has_mapping() ? mean_field::mapping::COORDINATE_SPACE::PHYSICAL : mean_field::mapping::COORDINATE_SPACE::REFERENCE; const double targetMass = density * mean_field::analysis::get_mesh_volume( f, volumeCoordinates, mean_field::utils::DOMAINS::STELLAR); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, targetMass); const auto &layout = stellarOperator.GetLayout(); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; stellar_equilibrium_test_utils::assign_value_block( state, layout, stellar_equilibrium_test_utils::densityValue, stellar_equilibrium_test_utils::reduce_density( f, stellar_equilibrium_test_utils::project_constant_density( f, density))); stellar_equilibrium_test_utils::assign_value_block( state, layout, stellar_equilibrium_test_utils::gravityPotentialValue, stellar_equilibrium_test_utils::project_constant_scalar( *f.gravityPotentialFes, gravityPotential)); stellar_equilibrium_test_utils::assign_value_block( state, layout, stellar_equilibrium_test_utils::enthalpyValue, stellar_equilibrium_test_utils::reduce_enthalpy( f, stellar_equilibrium_test_utils::project_constant_scalar( *f.enthalpyFes, enthalpy))); stellar_equilibrium_test_utils::value_view( state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = bernoulliConstant; stellarOperator.Prepare(state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_zero_rotation()); mfem::Vector residual; stellarOperator.BuildResidual(residual); const double closureNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( residual, layout, stellar_equilibrium_test_utils::densityResidual), f.mesh->GetComm()); const double hydrostaticNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( residual, layout, stellar_equilibrium_test_utils::enthalpyResidual), f.mesh->GetComm()); const double massError = std::abs(stellar_equilibrium_test_utils::const_residual_view( residual, layout, stellar_equilibrium_test_utils::massResidual)(0)); INFO("Exact n=1 closure norm = " << closureNorm); INFO("Exact constant hydrostatic norm = " << hydrostaticNorm); INFO("Independent constant-density mass error = " << massError); CHECK(closureNorm < 2.0e-12); CHECK(hydrostaticNorm < 2.0e-12); CHECK(massError < 2.0e-11 * targetMass); } TEST_CASE( "Prepared Stellar Equilibrium Zero Gravity State Has Analytically Zero " "Gravity Rows", tags::gravity &tags::prepared &tags::analytic_comparison &tags::residuals) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, 1.0); const auto &layout = stellarOperator.GetLayout(); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; stellar_equilibrium_test_utils::assign_value_block( state, layout, stellar_equilibrium_test_utils::displacementValue, stellar_equilibrium_test_utils::project_displacement(f, 0.73)); stellarOperator.Prepare(state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_zero_rotation()); mfem::Vector residual; stellarOperator.BuildResidual(residual); const double gradientNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( residual, layout, stellar_equilibrium_test_utils::gravityGradientResidual), f.mesh->GetComm()); const double poissonNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( residual, layout, stellar_equilibrium_test_utils::gravityPotentialResidual), f.mesh->GetComm()); CHECK(gradientNorm == 0.0); CHECK(poissonNorm == 0.0); } TEST_CASE( "Prepared Stellar Equilibrium Jacobian Has The Declared Six By Six Shape", tags::barotrope &tags::prepared &tags::jacobian &tags::mfem_operators &tags::unit) { using JacobianForm = mean_field::utils::blocks::barotropic_equilibrium_jacobian_form; STATIC_REQUIRE( mean_field::utils::blocks::has_jacobian_coupling_v< mean_field::utils::blocks::barotropic_constant::mass_normalization:: residual, mean_field::utils::blocks::density::mass::value, JacobianForm>); STATIC_REQUIRE(mean_field::utils::blocks::has_jacobian_coupling_v< mean_field::utils::blocks::barotropic_constant:: mass_normalization::residual, mean_field::utils::blocks::displacement::geometry::value, JacobianForm>); STATIC_REQUIRE_FALSE( mean_field::utils::blocks::has_jacobian_coupling_v< mean_field::utils::blocks::barotropic_constant::mass_normalization:: residual, mean_field::utils::blocks::enthalpy::specific::value, JacobianForm>); mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, 1.17); const auto &layout = stellarOperator.GetLayout(); const mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout); stellarOperator.Prepare(state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_rotation(0.77)); const mfem::Vector fullDirection = stellar_equilibrium_test_utils::make_direction(f, layout); struct ShapeCase final { int activeColumn; std::array allowedRows; }; const std::array cases{ ShapeCase{0, {false, true, true, true, false, true}}, ShapeCase{1, {true, true, true, true, true, true}}, ShapeCase{2, {true, true, false, true, false, false}}, ShapeCase{3, {true, false, false, false, true, false}}, ShapeCase{4, {false, false, true, true, true, false}}, ShapeCase{5, {false, false, false, false, true, false}}}; const std::array valueOffsets{ layout.offset(stellar_equilibrium_test_utils::densityValue), layout.offset(stellar_equilibrium_test_utils::displacementValue), layout.offset(stellar_equilibrium_test_utils::gravityGradientValue), layout.offset(stellar_equilibrium_test_utils::gravityPotentialValue), layout.offset(stellar_equilibrium_test_utils::enthalpyValue), layout.offset(stellar_equilibrium_test_utils::bernoulliValue), layout.value_offsets().Last()}; for (const ShapeCase &shapeCase : cases) { CAPTURE(shapeCase.activeColumn); mfem::Vector columnDirection(fullDirection.Size()); columnDirection = 0.0; for (int entry = valueOffsets[shapeCase.activeColumn]; entry < valueOffsets[shapeCase.activeColumn + 1]; ++entry) { columnDirection(entry) = fullDirection(entry); } mfem::Vector action; stellarOperator.Mult(columnDirection, action); const std::array rowActions{ stellar_equilibrium_test_utils::const_residual_view( action, layout, stellar_equilibrium_test_utils::gravityGradientResidual), stellar_equilibrium_test_utils::const_residual_view( action, layout, stellar_equilibrium_test_utils::gravityPotentialResidual), stellar_equilibrium_test_utils::const_residual_view( action, layout, stellar_equilibrium_test_utils::densityResidual), stellar_equilibrium_test_utils::const_residual_view( action, layout, stellar_equilibrium_test_utils::displacementResidual), stellar_equilibrium_test_utils::const_residual_view( action, layout, stellar_equilibrium_test_utils::enthalpyResidual), stellar_equilibrium_test_utils::const_residual_view( action, layout, stellar_equilibrium_test_utils::massResidual)}; double allowedNormSquared = 0.0; for (int row = 0; row < 6; ++row) { CAPTURE(row); const double rowNorm = stellar_equilibrium_test_utils::global_norm( rowActions[row], f.mesh->GetComm()); if (shapeCase.allowedRows[row]) { allowedNormSquared += rowNorm * rowNorm; } else { CHECK(rowNorm == 0.0); } } CHECK(allowedNormSquared > 0.0); } } TEST_CASE("Prepared Stellar Equilibrium Complete Jacobian Matches Every " "Coupled Centered Difference Block", tags::barotrope &tags::prepared &tags::jacobian &tags::accuracy &tags::geometry) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, 1.21); const auto &layout = stellarOperator.GetLayout(); const mfem::Vector baseState = stellar_equilibrium_test_utils::make_state(f, layout); const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout); auto dependencies = stellar_equilibrium_test_utils::make_dependencies(); const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.85); stellarOperator.Prepare(baseState, dependencies, rotation); mfem::Vector analyticAction; stellarOperator.Mult(direction, analyticAction); constexpr double step = 1.0e-5; mfem::Vector plusState(baseState); plusState.Add(step, direction); stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); stellarOperator.Prepare(plusState, dependencies, rotation); mfem::Vector plusResidual; stellarOperator.BuildResidual(plusResidual); mfem::Vector minusState(baseState); minusState.Add(-step, direction); stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); stellarOperator.Prepare(minusState, dependencies, rotation); mfem::Vector minusResidual; stellarOperator.BuildResidual(minusResidual); plusResidual -= minusResidual; plusResidual /= 2.0 * step; const std::array errors{ stellar_equilibrium_test_utils::block_relative_difference( analyticAction, plusResidual, layout, stellar_equilibrium_test_utils::gravityGradientResidual, f.mesh->GetComm()), stellar_equilibrium_test_utils::block_relative_difference( analyticAction, plusResidual, layout, stellar_equilibrium_test_utils::gravityPotentialResidual, f.mesh->GetComm()), stellar_equilibrium_test_utils::block_relative_difference( analyticAction, plusResidual, layout, stellar_equilibrium_test_utils::densityResidual, f.mesh->GetComm()), stellar_equilibrium_test_utils::block_relative_difference( analyticAction, plusResidual, layout, stellar_equilibrium_test_utils::displacementResidual, f.mesh->GetComm()), stellar_equilibrium_test_utils::block_relative_difference( analyticAction, plusResidual, layout, stellar_equilibrium_test_utils::enthalpyResidual, f.mesh->GetComm()), stellar_equilibrium_test_utils::block_relative_difference( analyticAction, plusResidual, layout, stellar_equilibrium_test_utils::massResidual, f.mesh->GetComm())}; INFO("R_g centered-difference error = " << errors[0]); INFO("R_Phi centered-difference error = " << errors[1]); INFO("R_rho centered-difference error = " << errors[2]); INFO("R_d centered-difference error = " << errors[3]); INFO("R_h centered-difference error = " << errors[4]); INFO("R_M centered-difference error = " << errors[5]); CHECK(errors[0] < 2.0e-6); CHECK(errors[1] < 2.0e-6); CHECK(errors[2] < 2.0e-6); CHECK(errors[3] < 2.0e-6); CHECK(errors[4] < 2.0e-6); CHECK(errors[5] < 2.0e-6); } TEST_CASE("Prepared Stellar Equilibrium Bernoulli Newton Step Decreases The " "Residual Exactly", tags::barotrope &tags::prepared &tags::jacobian &tags::convergence) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, 1.09); const auto &layout = stellarOperator.GetLayout(); mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout); auto dependencies = stellar_equilibrium_test_utils::make_dependencies(); const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.69); stellarOperator.Prepare(state, dependencies, rotation); mfem::Vector residualBefore; stellarOperator.BuildResidual(residualBefore); mfem::Vector unitBernoulliDirection(state.Size()); unitBernoulliDirection = 0.0; stellar_equilibrium_test_utils::value_view( unitBernoulliDirection, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = 1.0; mfem::Vector bernoulliAction; stellarOperator.Mult(unitBernoulliDirection, bernoulliAction); const mfem::Vector residualHydrostatic = stellar_equilibrium_test_utils::const_residual_view( residualBefore, layout, stellar_equilibrium_test_utils::enthalpyResidual); const mfem::Vector actionHydrostatic = stellar_equilibrium_test_utils::const_residual_view( bernoulliAction, layout, stellar_equilibrium_test_utils::enthalpyResidual); const double numerator = gravity_prepared_test_utils::global_dot( residualHydrostatic, actionHydrostatic, f.mesh->GetComm()); const double denominator = gravity_prepared_test_utils::global_dot( actionHydrostatic, actionHydrostatic, f.mesh->GetComm()); REQUIRE(denominator > 0.0); const double bernoulliStep = -numerator / denominator; mfem::Vector predictedResidual(residualBefore); predictedResidual.Add(bernoulliStep, bernoulliAction); stellar_equilibrium_test_utils::value_view( state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) += bernoulliStep; ++dependencies.bernoulliConstant.revision; stellarOperator.Prepare(state, dependencies, rotation); mfem::Vector residualAfter; stellarOperator.BuildResidual(residualAfter); const double modelError = stellar_equilibrium_test_utils::relative_difference( residualAfter, predictedResidual, f.mesh->GetComm()); const double hydrostaticNormBefore = stellar_equilibrium_test_utils::global_norm(residualHydrostatic, f.mesh->GetComm()); const double hydrostaticNormAfter = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( residualAfter, layout, stellar_equilibrium_test_utils::enthalpyResidual), f.mesh->GetComm()); const double coupledNormBefore = stellar_equilibrium_test_utils::global_norm( residualBefore, f.mesh->GetComm()); const double coupledNormAfter = stellar_equilibrium_test_utils::global_norm( residualAfter, f.mesh->GetComm()); INFO("Bernoulli least-squares step = " << bernoulliStep); INFO("Exact affine residual-model error = " << modelError); INFO("Hydrostatic norm before = " << hydrostaticNormBefore); INFO("Hydrostatic norm after = " << hydrostaticNormAfter); INFO("Coupled norm before = " << coupledNormBefore); INFO("Coupled norm after = " << coupledNormAfter); CHECK(modelError < 2.0e-13); CHECK(hydrostaticNormAfter < hydrostaticNormBefore); CHECK(coupledNormAfter <= coupledNormBefore); mfem::Vector unchangedDifference(residualAfter); unchangedDifference -= residualBefore; stellar_equilibrium_test_utils::residual_view( unchangedDifference, layout, stellar_equilibrium_test_utils::enthalpyResidual) = 0.0; CHECK(stellar_equilibrium_test_utils::global_norm(unchangedDifference, f.mesh->GetComm()) == 0.0); } TEST_CASE( "Prepared Stellar Equilibrium Selectively Invalidates Rows And Never " "Reprepares In Krylov Mult", tags::barotrope &tags::prepared &tags::contexts &tags::mfem_operators) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mean_field::eos::Polytrope barotrope(3.0, 0.25); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, 1.15); const auto &layout = stellarOperator.GetLayout(); mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout); auto dependencies = stellar_equilibrium_test_utils::make_dependencies(); const auto rotation = stellar_equilibrium_test_utils::make_rotation(0.73); stellarOperator.Prepare(state, dependencies, rotation); const std::uint64_t closurePreparations = stellarOperator.GetBarotropicClosureOperator().GetPreparationCount(); const std::uint64_t hydrostaticPreparations = stellarOperator.GetHydrostaticOperator().GetResidualPreparationCount(); const std::uint64_t displacementPreparations = stellarOperator.GetDisplacementOperator().GetResidualPreparationCount(); const std::uint64_t massPreparations = stellarOperator.GetMassNormalizationOperator().GetPreparationCount(); const std::uint64_t rootAssemblies = stellarOperator.GetStatistics().residualAssemblies; const auto repeated = stellarOperator.Prepare(state, dependencies, rotation); CHECK_FALSE(repeated.DidAnyWork()); CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies); const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout); mfem::Vector action; stellarOperator.Mult(direction, action); stellarOperator.Mult(direction, action); stellarOperator.Mult(direction, action); CHECK(stellarOperator.GetBarotropicClosureOperator().GetPreparationCount() == closurePreparations); CHECK( stellarOperator.GetHydrostaticOperator().GetResidualPreparationCount() == hydrostaticPreparations); CHECK( stellarOperator.GetDisplacementOperator().GetResidualPreparationCount() == displacementPreparations); CHECK(stellarOperator.GetMassNormalizationOperator().GetPreparationCount() == massPreparations); CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies); CHECK(stellarOperator.GetStatistics().jacobianApplications == 3); stellar_equilibrium_test_utils::value_view( state, layout, stellar_equilibrium_test_utils::gravityPotentialValue) .Add(0.03, stellar_equilibrium_test_utils::project_potential_direction( f, 0.41)); ++dependencies.gravityPotential.revision; const auto potentialReport = stellarOperator.Prepare(state, dependencies, rotation); CHECK_FALSE(potentialReport.barotropicClosure.DidAnyWork()); CHECK(potentialReport.hydrostatic.DidAnyWork()); CHECK_FALSE(potentialReport.displacement.DidAnyWork()); CHECK_FALSE(potentialReport.massNormalization.DidAnyWork()); CHECK(potentialReport.assembledResidual); stellar_equilibrium_test_utils::value_view( state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) += 0.09; ++dependencies.bernoulliConstant.revision; const auto bernoulliReport = stellarOperator.Prepare(state, dependencies, rotation); CHECK_FALSE(bernoulliReport.barotropicClosure.DidAnyWork()); CHECK(bernoulliReport.hydrostatic.DidAnyWork()); CHECK_FALSE(bernoulliReport.displacement.DidAnyWork()); CHECK_FALSE(bernoulliReport.massNormalization.DidAnyWork()); CHECK(bernoulliReport.assembledResidual); ++dependencies.targetMass.revision; const auto targetReport = stellarOperator.Prepare(state, dependencies, rotation); CHECK_FALSE(targetReport.barotropicClosure.DidAnyWork()); CHECK_FALSE(targetReport.hydrostatic.DidAnyWork()); CHECK_FALSE(targetReport.displacement.DidAnyWork()); CHECK(targetReport.massNormalization.DidAnyWork()); CHECK(targetReport.assembledResidual); } TEST_CASE("Prepared Stellar Equilibrium Matches The Analytic N1 Lane Emden " "State Up To The Mixed Projection Floor", tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy &tags::gravity &tags::hydro &tags::residuals) { class LaneEmdenGravityGradientCoefficient final : public mfem::VectorCoefficient { public: LaneEmdenGravityGradientCoefficient(const int dimension, const int vacuumAttribute, const double stellarRadius, const double centralDensity, const double targetMass, const double polytropicConstant) : mfem::VectorCoefficient(dimension), m_vacuumAttribute(vacuumAttribute), m_stellarRadius(stellarRadius), m_centralDensity(centralDensity), m_targetMass(targetMass), m_polytropicConstant(polytropicConstant) {} void Eval(mfem::Vector &value, mfem::ElementTransformation &transformation, const mfem::IntegrationPoint &integrationPoint) override { mfem::Vector computationalPosition; transformation.Transform(integrationPoint, computationalPosition); value.SetSize(vdim); value = 0.0; const double radius = computationalPosition.Norml2(); if (!std::isfinite(radius) || radius <= 100.0 * std::numeric_limits::epsilon()) { return; } double radialGradient = 0.0; if (transformation.Attribute == m_vacuumAttribute) { /* * In the compactified exterior, the three-dimensional H(div) * Piola pullback of the inverse-square monopole field reduces * to this finite computational-space expression. */ radialGradient = mean_field::utils::G * m_targetMass / (radius * radius); } else { const double pi = std::acos(-1.0); const double xi = pi * radius / m_stellarRadius; if (std::abs(xi) < 1.0e-5) { /* * sin(xi) - xi cos(xi) = xi^3 / 3 + O(xi^5). */ radialGradient = (4.0 / 3.0) * pi * mean_field::utils::G * m_centralDensity * radius; } else { radialGradient = 2.0 * m_polytropicConstant * m_centralDensity * pi / m_stellarRadius * (std::sin(xi) - xi * std::cos(xi)) / (xi * xi); } } value = computationalPosition; value *= radialGradient / radius; } private: int m_vacuumAttribute; double m_stellarRadius; double m_centralDensity; double m_targetMass; double m_polytropicConstant; }; mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); REQUIRE(f.domainMapperStateless != nullptr); REQUIRE(f.domainMapperStateless != nullptr); *f.displacement = 0.0; const double pi = std::acos(-1.0); const double stellarRadius = mean_field::utils::RADIUS; const double targetMass = mean_field::utils::MASS; /* * For an n = 1 Lane-Emden polytrope, * * R = sqrt(pi K / (2 G)), * * so choosing K this way places the analytic surface exactly at the * stellar boundary of the mesh. */ const double polytropicConstant = 2.0 * mean_field::utils::G * stellarRadius * stellarRadius / pi; /* * The analytic n = 1 mass is * * M = 4 rho_c R^3 / pi. */ const double centralDensity = pi * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius); const double bernoulliConstant = -mean_field::utils::G * targetMass / stellarRadius; const mean_field::eos::Polytrope barotrope(1.0, polytropicConstant); const auto densityFunction = [centralDensity, stellarRadius, pi](const mfem::Vector &position) { const double radius = position.Norml2(); if (radius >= stellarRadius) { return 0.0; } const double xi = pi * radius / stellarRadius; if (std::abs(xi) < 100.0 * std::numeric_limits::epsilon()) { return centralDensity; } return centralDensity * std::sin(xi) / xi; }; const auto enthalpyFunction = [centralDensity, stellarRadius, polytropicConstant, pi](const mfem::Vector &position) { const double radius = position.Norml2(); if (radius >= stellarRadius) { return 0.0; } const double xi = pi * radius / stellarRadius; const double density = std::abs(xi) < 100.0 * std::numeric_limits::epsilon() ? centralDensity : centralDensity * std::sin(xi) / xi; return 2.0 * polytropicConstant * density; }; const auto potentialFunction = [centralDensity, stellarRadius, targetMass, polytropicConstant, bernoulliConstant, pi](const mfem::Vector &physicalPosition) { const double radius = physicalPosition.Norml2(); /* * Phi tends to zero at compactified infinity. */ if (!std::isfinite(radius)) { return 0.0; } if (radius >= stellarRadius) { return radius > 0.0 ? -mean_field::utils::G * targetMass / radius : 0.0; } const double xi = pi * radius / stellarRadius; const double density = std::abs(xi) < 100.0 * std::numeric_limits::epsilon() ? centralDensity : centralDensity * std::sin(xi) / xi; const double enthalpy = 2.0 * polytropicConstant * density; /* * Hydrostatic equilibrium is h + Phi = C. */ return bernoulliConstant - enthalpy; }; mfem::FunctionCoefficient densityCoefficient(densityFunction); mfem::FunctionCoefficient enthalpyCoefficient(enthalpyFunction); mean_field::mapping::PhysicalPositionFunctionCoefficient potentialCoefficient( *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, potentialFunction); LaneEmdenGravityGradientCoefficient gravityGradientCoefficient( f.mesh->Dimension(), field_dof_test_utils::vacuum_material_attribute, stellarRadius, centralDensity, targetMass, polytropicConstant); mfem::ParGridFunction densityField(f.densityFes.get()); mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); mfem::ParGridFunction gravityPotentialField(f.gravityPotentialFes.get()); mfem::ParGridFunction gravityGradientField(f.gravityFluxFes.get()); densityField = 0.0; enthalpyField = 0.0; gravityPotentialField = 0.0; gravityGradientField = 0.0; densityField.ProjectCoefficient(densityCoefficient); enthalpyField.ProjectCoefficient(enthalpyCoefficient); gravityPotentialField.ProjectCoefficient(potentialCoefficient); gravityGradientField.ProjectCoefficient(gravityGradientCoefficient); mfem::Vector densityTrue; mfem::Vector enthalpyTrue; mfem::Vector gravityPotentialTrue; mfem::Vector gravityGradientTrue; densityField.GetTrueDofs(densityTrue); enthalpyField.GetTrueDofs(enthalpyTrue); gravityPotentialField.GetTrueDofs(gravityPotentialTrue); gravityGradientField.GetTrueDofs(gravityGradientTrue); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, targetMass); const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout(); mfem::Vector analyticState(layout.value_offsets().Last()); analyticState = 0.0; stellar_equilibrium_test_utils::assign_value_block( analyticState, layout, stellar_equilibrium_test_utils::densityValue, stellar_equilibrium_test_utils::reduce_density(f, densityTrue)); stellar_equilibrium_test_utils::assign_value_block( analyticState, layout, stellar_equilibrium_test_utils::gravityGradientValue, gravityGradientTrue); stellar_equilibrium_test_utils::assign_value_block( analyticState, layout, stellar_equilibrium_test_utils::gravityPotentialValue, gravityPotentialTrue); stellar_equilibrium_test_utils::assign_value_block( analyticState, layout, stellar_equilibrium_test_utils::enthalpyValue, stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyTrue)); stellar_equilibrium_test_utils::value_view( analyticState, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = bernoulliConstant; mean_field::operators::StellarEquilibriumDependencies dependencies = stellar_equilibrium_test_utils::make_dependencies(); const mean_field::physics::RigidRotation zeroRotation = stellar_equilibrium_test_utils::make_zero_rotation(); stellarOperator.Prepare(analyticState, dependencies, zeroRotation); mfem::Vector analyticResidual; stellarOperator.BuildResidual(analyticResidual); const double analyticGradientNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( analyticResidual, layout, stellar_equilibrium_test_utils::gravityGradientResidual), f.mesh->GetComm()); const double analyticPoissonNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( analyticResidual, layout, stellar_equilibrium_test_utils::gravityPotentialResidual), f.mesh->GetComm()); const double analyticClosureNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( analyticResidual, layout, stellar_equilibrium_test_utils::densityResidual), f.mesh->GetComm()); const double analyticDisplacementNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( analyticResidual, layout, stellar_equilibrium_test_utils::displacementResidual), f.mesh->GetComm()); const double analyticHydrostaticNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( analyticResidual, layout, stellar_equilibrium_test_utils::enthalpyResidual), f.mesh->GetComm()); const double analyticMassError = std::abs(stellar_equilibrium_test_utils::const_residual_view( analyticResidual, layout, stellar_equilibrium_test_utils::massResidual)(0)); /* * Construct a deliberately inconsistent nearby state. The analytic * projection should have a substantially smaller residual in every row. */ mfem::Vector perturbedState(analyticState); { mfem::Vector block = stellar_equilibrium_test_utils::value_view( perturbedState, layout, stellar_equilibrium_test_utils::densityValue); block *= 1.12; } { mfem::Vector block = stellar_equilibrium_test_utils::value_view( perturbedState, layout, stellar_equilibrium_test_utils::gravityGradientValue); block *= 0.87; } { mfem::Vector block = stellar_equilibrium_test_utils::value_view( perturbedState, layout, stellar_equilibrium_test_utils::gravityPotentialValue); block *= 1.08; } { mfem::Vector block = stellar_equilibrium_test_utils::value_view( perturbedState, layout, stellar_equilibrium_test_utils::enthalpyValue); block *= 0.91; } stellar_equilibrium_test_utils::value_view( perturbedState, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) *= 1.04; stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); stellarOperator.Prepare(perturbedState, dependencies, zeroRotation); mfem::Vector perturbedResidual; stellarOperator.BuildResidual(perturbedResidual); const double perturbedGradientNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( perturbedResidual, layout, stellar_equilibrium_test_utils::gravityGradientResidual), f.mesh->GetComm()); const double perturbedPoissonNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( perturbedResidual, layout, stellar_equilibrium_test_utils::gravityPotentialResidual), f.mesh->GetComm()); const double perturbedClosureNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( perturbedResidual, layout, stellar_equilibrium_test_utils::densityResidual), f.mesh->GetComm()); const double perturbedDisplacementNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( perturbedResidual, layout, stellar_equilibrium_test_utils::displacementResidual), f.mesh->GetComm()); const double perturbedHydrostaticNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( perturbedResidual, layout, stellar_equilibrium_test_utils::enthalpyResidual), f.mesh->GetComm()); const double perturbedMassError = std::abs(stellar_equilibrium_test_utils::const_residual_view( perturbedResidual, layout, stellar_equilibrium_test_utils::massResidual)(0)); INFO("Analytic n=1 central density = " << centralDensity); INFO("Analytic n=1 polytropic constant = " << polytropicConstant); INFO("Analytic n=1 target mass = " << targetMass); INFO("Analytic n=1 Bernoulli constant = " << bernoulliConstant); INFO("Gravity-gradient residual: analytic = " << analyticGradientNorm << ", perturbed = " << perturbedGradientNorm); INFO("Poisson residual: analytic = " << analyticPoissonNorm << ", perturbed = " << perturbedPoissonNorm); INFO("Closure residual: analytic = " << analyticClosureNorm << ", perturbed = " << perturbedClosureNorm); INFO("Displacement residual: analytic = " << analyticDisplacementNorm << ", perturbed = " << perturbedDisplacementNorm); INFO("Hydrostatic residual: analytic = " << analyticHydrostaticNorm << ", perturbed = " << perturbedHydrostaticNorm); INFO("Mass error: analytic = " << analyticMassError << ", perturbed = " << perturbedMassError); REQUIRE(std::isfinite(perturbedGradientNorm)); REQUIRE(perturbedPoissonNorm > 0.0); REQUIRE(perturbedClosureNorm > 0.0); REQUIRE(perturbedDisplacementNorm > 0.0); REQUIRE(perturbedHydrostaticNorm > 0.0); REQUIRE(perturbedMassError > 0.0); /* * Closure and hydrostatic balance are algebraically especially favorable * for n = 1 because h = 2 K rho and h + Phi = C are linear relations. */ CHECK(analyticClosureNorm < 0.10 * perturbedClosureNorm); CHECK(analyticHydrostaticNorm < 0.10 * perturbedHydrostaticNorm); /* * Phi_h and g_h are independent L2 and RT projections of the analytic * potential and gradient. They are not a commuting mixed projection and * therefore need not satisfy * * M_g g_h + B^T Phi_h = 0. * * The resulting R_g value is a finite-element projection-compatibility * floor, not a physical equilibrium error. Gravity solver-to-projection * accuracy is tested independently by the dedicated gravity tests. */ CHECK(std::isfinite(analyticGradientNorm)); CHECK(analyticPoissonNorm < 0.35 * perturbedPoissonNorm); CHECK(analyticDisplacementNorm < 0.35 * perturbedDisplacementNorm); CHECK(analyticMassError < 5.0e-5 * targetMass); CHECK(analyticMassError < 0.10 * perturbedMassError); } TEST_CASE( "Prepared Stellar Equilibrium Has A Restoring Jacobian Around An N3 " "Polytrope", tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy &tags::gravity &tags::hydro &tags::jacobian &tags::convergence) { mean_field::utils::Args args = test_utils::setup_args(); args.p.rtol = 1.0e-12; args.p.max_iters = std::max(args.p.max_iters, 1000); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); REQUIRE(f.domainMapperStateless != nullptr); REQUIRE(f.domainMapperStateless != nullptr); const double pi = std::acos(-1.0); const double stellarRadius = mean_field::utils::RADIUS; const double targetMass = mean_field::utils::MASS; /* * Standard n = 3 Lane-Emden constants: * * xi_1 = 6.896848619... * -xi_1^2 theta'(xi_1) = 2.018235951... */ constexpr double surfaceCoordinate = 6.8968486193769603755; constexpr double dimensionlessMass = 2.0182359509662283534; /* * For n = 3, * * M = 4 pi (K / (pi G))^(3/2) mu_1. * * This fixes K for the requested target mass. */ const double polytropicConstant = pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0); /* * The n = 3 radius is * * R = xi_1 sqrt(K / (pi G)) rho_c^(-1/3). * * Choose rho_c so that the Lane-Emden surface coincides with the * stellar boundary of the test mesh. */ const double centralDensity = std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / stellarRadius, 3.0); const mean_field::eos::Polytrope equationOfState(3.0, polytropicConstant); const mean_field::models::structure::PolytropicStructure structurePrescription(equationOfState, targetMass); const mean_field::models::structure::StructureSeed seed = structurePrescription.makeInitialSeed( {.centralDensity = centralDensity, .radialSampleCount = 8192}); INFO("Requested stellar radius = " << stellarRadius); INFO("Seed stellar radius = " << seed.stellarRadius); INFO("Target mass = " << targetMass); INFO("Polytropic constant = " << polytropicConstant); INFO("Central density = " << centralDensity); REQUIRE(seed.radius.Size() == seed.density.Size()); REQUIRE(seed.radius.Size() == seed.enthalpy.Size()); REQUIRE(seed.radius.Size() == 8192); CHECK(std::abs(seed.stellarRadius - stellarRadius) / stellarRadius < 2.0e-4); const auto interpolateProfile = [](const mfem::Vector &radiusSamples, const mfem::Vector &valueSamples, const double radius) { MFEM_VERIFY(radiusSamples.Size() == valueSamples.Size(), "The radial profile has inconsistent sample sizes."); MFEM_VERIFY(radiusSamples.Size() >= 2, "The radial profile requires at least two samples."); if (radius <= radiusSamples(0)) { return valueSamples(0); } const int finalIndex = radiusSamples.Size() - 1; if (radius >= radiusSamples(finalIndex)) { return valueSamples(finalIndex); } int lowerIndex = 0; int upperIndex = finalIndex; while (upperIndex - lowerIndex > 1) { const int middleIndex = lowerIndex + (upperIndex - lowerIndex) / 2; if (radiusSamples(middleIndex) <= radius) { lowerIndex = middleIndex; } else { upperIndex = middleIndex; } } const double radialInterval = radiusSamples(upperIndex) - radiusSamples(lowerIndex); MFEM_VERIFY(radialInterval > 0.0, "The radial profile is not strictly increasing."); const double fraction = (radius - radiusSamples(lowerIndex)) / radialInterval; return (1.0 - fraction) * valueSamples(lowerIndex) + fraction * valueSamples(upperIndex); }; mfem::FunctionCoefficient densityCoefficient( [&seed, &interpolateProfile](const mfem::Vector &position) { const double radius = position.Norml2(); if (radius >= seed.stellarRadius) { return 0.0; } return interpolateProfile(seed.radius, seed.density, radius); }); mfem::FunctionCoefficient enthalpyCoefficient( [&seed, &interpolateProfile](const mfem::Vector &position) { const double radius = position.Norml2(); if (radius >= seed.stellarRadius) { return 0.0; } return interpolateProfile(seed.radius, seed.enthalpy, radius); }); mfem::ParGridFunction densityField(f.densityFes.get()); mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); mfem::ParGridFunction displacementField(f.displacementFes.get()); densityField = 0.0; enthalpyField = 0.0; displacementField = 0.0; densityField.ProjectCoefficient(densityCoefficient); enthalpyField.ProjectCoefficient(enthalpyCoefficient); /* * Gravity initialization and the prepared root operator must see the * same undeformed geometry. */ *f.displacement = displacementField; const mean_field::physics::GravitySolution gravitySolution = mean_field::physics::solve_gravity_field(f, args, densityField, displacementField); mfem::Vector densityTrue; mfem::Vector enthalpyTrue; mfem::Vector displacementTrue; mfem::Vector gravityGradientTrue; mfem::Vector gravityPotentialTrue; densityField.GetTrueDofs(densityTrue); enthalpyField.GetTrueDofs(enthalpyTrue); displacementField.GetTrueDofs(displacementTrue); gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue); gravitySolution.phi.GetTrueDofs(gravityPotentialTrue); const double bernoulliConstant = -mean_field::utils::G * targetMass / stellarRadius; const mean_field::eos::Polytrope barotrope(3.0, polytropicConstant); mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( f, *f.domainMapperStateless, barotrope, targetMass); const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout(); mfem::Vector equilibriumState(layout.value_offsets().Last()); equilibriumState = 0.0; stellar_equilibrium_test_utils::assign_value_block( equilibriumState, layout, stellar_equilibrium_test_utils::densityValue, stellar_equilibrium_test_utils::reduce_density(f, densityTrue)); stellar_equilibrium_test_utils::assign_value_block( equilibriumState, layout, stellar_equilibrium_test_utils::displacementValue, displacementTrue); stellar_equilibrium_test_utils::assign_value_block( equilibriumState, layout, stellar_equilibrium_test_utils::gravityGradientValue, gravityGradientTrue); stellar_equilibrium_test_utils::assign_value_block( equilibriumState, layout, stellar_equilibrium_test_utils::gravityPotentialValue, gravityPotentialTrue); stellar_equilibrium_test_utils::assign_value_block( equilibriumState, layout, stellar_equilibrium_test_utils::enthalpyValue, stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyTrue)); stellar_equilibrium_test_utils::value_view( equilibriumState, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = bernoulliConstant; mean_field::operators::StellarEquilibriumDependencies dependencies = stellar_equilibrium_test_utils::make_dependencies(); const mean_field::physics::RigidRotation zeroRotation = stellar_equilibrium_test_utils::make_zero_rotation(); stellarOperator.Prepare(equilibriumState, dependencies, zeroRotation); mfem::Vector equilibriumResidual; stellarOperator.BuildResidual(equilibriumResidual); /* * Construct a physically safe perturbation direction. Density and * enthalpy perturbations vanish at the surface because they are * proportional to the equilibrium profiles. */ mfem::Vector perturbationDirection(layout.value_offsets().Last()); perturbationDirection = 0.0; mfem::Vector densityDirection(densityTrue); densityDirection *= 0.12; mfem::Vector gravityGradientDirection(gravityGradientTrue); gravityGradientDirection *= -0.09; mfem::Vector gravityPotentialDirection(gravityPotentialTrue); gravityPotentialDirection *= 0.07; mfem::Vector enthalpyDirection(enthalpyTrue); enthalpyDirection *= -0.11; const mfem::Vector displacementDirection = stellar_equilibrium_test_utils::project_displacement_direction(f, 0.15); stellar_equilibrium_test_utils::assign_value_block( perturbationDirection, layout, stellar_equilibrium_test_utils::densityValue, stellar_equilibrium_test_utils::reduce_density(f, densityDirection)); stellar_equilibrium_test_utils::assign_value_block( perturbationDirection, layout, stellar_equilibrium_test_utils::displacementValue, displacementDirection); stellar_equilibrium_test_utils::assign_value_block( perturbationDirection, layout, stellar_equilibrium_test_utils::gravityGradientValue, gravityGradientDirection); stellar_equilibrium_test_utils::assign_value_block( perturbationDirection, layout, stellar_equilibrium_test_utils::gravityPotentialValue, gravityPotentialDirection); stellar_equilibrium_test_utils::assign_value_block( perturbationDirection, layout, stellar_equilibrium_test_utils::enthalpyValue, stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyDirection)); stellar_equilibrium_test_utils::value_view( perturbationDirection, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = 0.05 * bernoulliConstant; /* * Evaluate J delta-x at the equilibrium state before changing the * prepared base point. */ mfem::Vector jacobianAction; stellarOperator.Mult(perturbationDirection, jacobianAction); constexpr double perturbationScale = 2.0e-2; mfem::Vector perturbedState(equilibriumState); perturbedState.Add(perturbationScale, perturbationDirection); stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); stellarOperator.Prepare(perturbedState, dependencies, zeroRotation); mfem::Vector perturbedResidual; stellarOperator.BuildResidual(perturbedResidual); const auto residualBlockNorm = [&layout, &f](const mfem::Vector &residual, const auto block) { return stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view(residual, layout, block), f.mesh->GetComm()); }; const std::array equilibriumRowNorms{ residualBlockNorm( equilibriumResidual, stellar_equilibrium_test_utils::gravityGradientResidual), residualBlockNorm( equilibriumResidual, stellar_equilibrium_test_utils::gravityPotentialResidual), residualBlockNorm(equilibriumResidual, stellar_equilibrium_test_utils::densityResidual), residualBlockNorm(equilibriumResidual, stellar_equilibrium_test_utils::displacementResidual), residualBlockNorm(equilibriumResidual, stellar_equilibrium_test_utils::enthalpyResidual), residualBlockNorm(equilibriumResidual, stellar_equilibrium_test_utils::massResidual)}; const std::array perturbedRowNorms{ residualBlockNorm( perturbedResidual, stellar_equilibrium_test_utils::gravityGradientResidual), residualBlockNorm( perturbedResidual, stellar_equilibrium_test_utils::gravityPotentialResidual), residualBlockNorm(perturbedResidual, stellar_equilibrium_test_utils::densityResidual), residualBlockNorm(perturbedResidual, stellar_equilibrium_test_utils::displacementResidual), residualBlockNorm(perturbedResidual, stellar_equilibrium_test_utils::enthalpyResidual), residualBlockNorm(perturbedResidual, stellar_equilibrium_test_utils::massResidual)}; constexpr std::array rowNames{ "gravity-gradient", "Poisson", "closure", "displacement", "hydrostatic", "mass"}; /* * Most rows are close to exact discrete relations. The displacement row * combines independently projected thermodynamic fields with the discrete * gravity solution and consequently has a larger force-balance projection * floor. */ constexpr std::array maximumEquilibriumFractions{ 0.35, // gravity-gradient 0.35, // Poisson 0.35, // closure 0.60, // displacement-force balance 0.35, // hydrostatic 0.35 // mass }; for (int row = 0; row < 6; ++row) { CAPTURE(row); CAPTURE(rowNames[row]); CAPTURE(equilibriumRowNorms[row]); CAPTURE(perturbedRowNorms[row]); CAPTURE(maximumEquilibriumFractions[row]); REQUIRE(std::isfinite(equilibriumRowNorms[row])); REQUIRE(std::isfinite(perturbedRowNorms[row])); REQUIRE(perturbedRowNorms[row] > 0.0); /* * The Lane-Emden state must be closer to equilibrium than the nearby * perturbed state in every residual row. */ CHECK(equilibriumRowNorms[row] < perturbedRowNorms[row]); /* * Require a substantial separation from the perturbed state while * allowing the larger discrete projection floor in the force row. */ CHECK(equilibriumRowNorms[row] < maximumEquilibriumFractions[row] * perturbedRowNorms[row]); } /* * Record an absolute regression bound for the current coarse-mesh * displacement-force projection floor. */ CHECK(equilibriumRowNorms[3] < 1.0e-3); const double equilibriumMassError = std::abs(stellar_equilibrium_test_utils::const_residual_view( equilibriumResidual, layout, stellar_equilibrium_test_utils::massResidual)(0)); INFO("Equilibrium relative mass error = " << equilibriumMassError / targetMass); CHECK(equilibriumMassError < 5.0e-4 * targetMass); /* * The nonlinear residual departure should be * * R(x + epsilon p) - R(x) * = epsilon J(x) p + O(epsilon^2). */ mfem::Vector residualDeparture(perturbedResidual); residualDeparture -= equilibriumResidual; mfem::Vector linearizedDeparture(jacobianAction); linearizedDeparture *= perturbationScale; mfem::Vector nonlinearRemainder(residualDeparture); nonlinearRemainder -= linearizedDeparture; const double departureNorm = stellar_equilibrium_test_utils::global_norm( residualDeparture, f.mesh->GetComm()); const double nonlinearRemainderNorm = stellar_equilibrium_test_utils::global_norm(nonlinearRemainder, f.mesh->GetComm()); /* * Apply the known restoring correction -epsilon p through the Jacobian. * * This predicts the residual after returning to the equilibrium state: * * R(x + epsilon p) - epsilon J(x)p approximately R(x). */ mfem::Vector restoredResidualPrediction(perturbedResidual); restoredResidualPrediction.Add(-perturbationScale, jacobianAction); restoredResidualPrediction -= equilibriumResidual; const double restoredDistance = stellar_equilibrium_test_utils::global_norm( restoredResidualPrediction, f.mesh->GetComm()); INFO("Residual departure norm = " << departureNorm); INFO("Nonlinear remainder norm = " << nonlinearRemainderNorm); INFO("Distance after the restoring Jacobian correction = " << restoredDistance); INFO("Relative first-order remainder = " << nonlinearRemainderNorm / departureNorm); REQUIRE(std::isfinite(departureNorm)); REQUIRE(std::isfinite(nonlinearRemainderNorm)); REQUIRE(std::isfinite(restoredDistance)); REQUIRE(departureNorm > 0.0); CHECK(nonlinearRemainderNorm < 5.0e-2 * departureNorm); CHECK(restoredDistance < 5.0e-2 * departureNorm); /* * Rotational shape response * * At moderate rotation, the leading deformation is a smooth, axisymmetric, * approximately quadrupolar oblateness. A convenient volume-preserving * affine representative is * * delta d(X) = (X, Y, -2 Z). * * It moves the equator outward, moves the poles inward, and has zero trace. * A cusp is not expected until the nonlinear solution approaches mass * shedding. */ { const double keplerianAngularSpeed = std::sqrt(mean_field::utils::G * targetMass / (stellarRadius * stellarRadius * stellarRadius)); constexpr double rotationFraction = 0.50; const double angularSpeed = rotationFraction * keplerianAngularSpeed; mfem::Vector angularVelocity(3); angularVelocity = 0.0; angularVelocity(2) = angularSpeed; mfem::Vector rotationCenter(3); rotationCenter = 0.0; const mean_field::physics::RigidRotation rotation(angularVelocity, rotationCenter); /* * Return from the perturbed state used by the preceding Jacobian test to * the spherical equilibrium state, while changing the rotation stream. */ stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); ++dependencies.rotation.revision; stellarOperator.Prepare(equilibriumState, dependencies, rotation); mfem::Vector rotatingSphericalResidual; stellarOperator.BuildResidual(rotatingSphericalResidual); mfem::ParGridFunction oblateDisplacementField(f.displacementFes.get()); mfem::VectorFunctionCoefficient oblateDisplacementCoefficient( f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); /* * Positive amplitude: * * equator: d = (x, y, 0), outward * pole: d = (0, 0, -2 z), inward * * The displacement gradient has trace 1 + 1 - 2 = 0, so this is * volume preserving to first order. */ value(0) = position(0); value(1) = position(1); value(2) = -2.0 * position(2); }); oblateDisplacementField = 0.0; oblateDisplacementField.ProjectCoefficient(oblateDisplacementCoefficient); mfem::Vector oblateDisplacement; oblateDisplacementField.GetTrueDofs(oblateDisplacement); mfem::Vector oblateDirection(layout.value_offsets().Last()); oblateDirection = 0.0; stellar_equilibrium_test_utils::assign_value_block( oblateDirection, layout, stellar_equilibrium_test_utils::displacementValue, oblateDisplacement); const mfem::Vector equilibriumDisplacementResidual = stellar_equilibrium_test_utils::const_residual_view( equilibriumResidual, layout, stellar_equilibrium_test_utils::displacementResidual); const mfem::Vector rotatingDisplacementResidual = stellar_equilibrium_test_utils::const_residual_view( rotatingSphericalResidual, layout, stellar_equilibrium_test_utils::displacementResidual); /* * Subtract the nonrotating force-balance projection floor. The remainder * is the displacement residual introduced by rotation. */ mfem::Vector rotationInducedResidual(rotatingDisplacementResidual); rotationInducedResidual -= equilibriumDisplacementResidual; const double rotationInducedWork = gravity_prepared_test_utils::global_dot( rotationInducedResidual, oblateDisplacement, f.mesh->GetComm()); const double rotationInducedNorm = stellar_equilibrium_test_utils::global_norm(rotationInducedResidual, f.mesh->GetComm()); const double oblateDirectionNorm = stellar_equilibrium_test_utils::global_norm(oblateDisplacement, f.mesh->GetComm()); const double workScale = rotationInducedNorm * oblateDirectionNorm; INFO("Keplerian angular speed = " << keplerianAngularSpeed); INFO("Applied angular speed = " << angularSpeed); INFO("Rotation fraction = " << rotationFraction); INFO("Rotation-induced displacement residual norm = " << rotationInducedNorm); INFO("Rotation-induced work against the oblate direction = " << rotationInducedWork); INFO("Normalized oblate work = " << rotationInducedWork / workScale); REQUIRE(std::isfinite(rotationInducedWork)); REQUIRE(std::isfinite(rotationInducedNorm)); REQUIRE(std::isfinite(oblateDirectionNorm)); REQUIRE(rotationInducedNorm > 0.0); REQUIRE(oblateDirectionNorm > 0.0); REQUIRE(workScale > 0.0); /* * The force residual uses the convention R_rot(w) = -integral rho a_c.w. * Therefore negative work against this direction means that -R, the * Newton right-hand side, drives a positive oblate deformation. */ CHECK(rotationInducedWork < 0.0); CHECK(rotationInducedWork < -1.0e-3 * workScale); /* * Evaluate the displacement column of the complete coupled Jacobian at * the rotating spherical state. */ mfem::Vector oblateJacobianAction; stellarOperator.Mult(oblateDirection, oblateJacobianAction); const mfem::Vector oblateDisplacementJacobianAction = stellar_equilibrium_test_utils::const_residual_view( oblateJacobianAction, layout, stellar_equilibrium_test_utils::displacementResidual); const double residualDirectionalDerivative = gravity_prepared_test_utils::global_dot( rotatingDisplacementResidual, oblateDisplacementJacobianAction, f.mesh->GetComm()); const double jacobianDirectionNormSquared = gravity_prepared_test_utils::global_dot( oblateDisplacementJacobianAction, oblateDisplacementJacobianAction, f.mesh->GetComm()); REQUIRE(std::isfinite(residualDirectionalDerivative)); REQUIRE(std::isfinite(jacobianDirectionNormSquared)); REQUIRE(jacobianDirectionNormSquared > 0.0); /* * Minimize the linearized displacement-residual norm along the oblate * direction: * * alpha_* = -(R_d, J_d p) / ||J_d p||^2. * * A positive alpha_* means that the operator selects equatorial expansion * and polar contraction rather than the prolate direction. */ const double optimalLinearizedAmplitude = -residualDirectionalDerivative / jacobianDirectionNormSquared; INFO("Displacement-residual directional derivative = " << residualDirectionalDerivative); INFO( "Optimal linearized oblate amplitude = " << optimalLinearizedAmplitude); REQUIRE(std::isfinite(optimalLinearizedAmplitude)); CHECK(residualDirectionalDerivative < 0.0); REQUIRE(optimalLinearizedAmplitude > 0.0); /* * Take only a fraction of the predicted step and cap it at a two-percent * surface deformation. This keeps the test safely inside the local * linearization regime. */ const double appliedOblateAmplitude = std::min(0.25 * optimalLinearizedAmplitude, 2.0e-2); REQUIRE(appliedOblateAmplitude > 0.0); mfem::Vector predictedDisplacementResidual(rotatingDisplacementResidual); predictedDisplacementResidual.Add(appliedOblateAmplitude, oblateDisplacementJacobianAction); const double rotatingDisplacementNorm = stellar_equilibrium_test_utils::global_norm( rotatingDisplacementResidual, f.mesh->GetComm()); const double predictedDisplacementNorm = stellar_equilibrium_test_utils::global_norm( predictedDisplacementResidual, f.mesh->GetComm()); INFO("Rotating spherical displacement residual norm = " << rotatingDisplacementNorm); INFO("Predicted oblate displacement residual norm = " << predictedDisplacementNorm); CHECK(predictedDisplacementNorm < rotatingDisplacementNorm); /* * Apply the same positive oblate displacement to the nonlinear operator. * Only the displacement row is compared: a complete rotating equilibrium * also requires simultaneous changes in rho, g, Phi, h, and C. */ mfem::Vector oblateState(equilibriumState); { mfem::Vector displacementBlock = stellar_equilibrium_test_utils::value_view( oblateState, layout, stellar_equilibrium_test_utils::displacementValue); displacementBlock.Add(appliedOblateAmplitude, oblateDisplacement); } ++dependencies.displacement.revision; stellarOperator.Prepare(oblateState, dependencies, rotation); mfem::Vector nonlinearOblateResidual; stellarOperator.BuildResidual(nonlinearOblateResidual); const double nonlinearOblateDisplacementNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( nonlinearOblateResidual, layout, stellar_equilibrium_test_utils::displacementResidual), f.mesh->GetComm()); const double equatorialRadiusScale = 1.0 + appliedOblateAmplitude; const double polarRadiusScale = 1.0 - 2.0 * appliedOblateAmplitude; const double equatorialToPolarRadiusRatio = equatorialRadiusScale / polarRadiusScale; INFO("Applied oblate amplitude = " << appliedOblateAmplitude); INFO("Nonlinear oblate displacement residual norm = " << nonlinearOblateDisplacementNorm); INFO("Equatorial radius scale = " << equatorialRadiusScale); INFO("Polar radius scale = " << polarRadiusScale); INFO("Equatorial-to-polar radius ratio = " << equatorialToPolarRadiusRatio); CHECK(equatorialRadiusScale > 1.0); CHECK(polarRadiusScale < 1.0); CHECK(polarRadiusScale > 0.0); CHECK(equatorialToPolarRadiusRatio > 1.0); CHECK(nonlinearOblateDisplacementNorm < rotatingDisplacementNorm); } }