#include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; import experiment; using namespace experiment; struct AccuracyBudgetEnergies { double binding{0.0}; double virial{0.0}; }; struct AccuracyBudgetMetrics { double direct_relative_residual{0.0}; double gradient_relative_error{0.0}; double gradient_projection_relative_error{0.0}; double gradient_solution_projection_gap{0.0}; double potential_relative_error{0.0}; double potential_projection_relative_error{0.0}; double potential_solution_projection_gap{0.0}; double binding_relative_error{0.0}; double virial_relative_error{0.0}; double virial_consistency_error{0.0}; }; static double global_norm(const mfem::Vector &vector, MPI_Comm communicator) { const double local_norm_squared = vector * vector; double global_norm_squared = 0.0; MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator); return std::sqrt(global_norm_squared); } static double global_dot(const mfem::Vector &left, const mfem::Vector &right, MPI_Comm communicator) { const double local_dot = left * right; double global_dot_product = 0.0; MPI_Allreduce(&local_dot, &global_dot_product, 1, MPI_DOUBLE, MPI_SUM, communicator); return global_dot_product; } static void zero_vacuum_density(const mean_field::fem::FEM &fem, mfem::GridFunction &density) { using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; const mean_field::field::FieldDofMap density_map = mean_field::field::make_field_dof_map(*fem.densityFes); mfem::Vector density_true; density.GetTrueDofs(density_true); const mfem::Vector supported_density = density_map.gather(density_true); density_map.scatter(supported_density, density_true); density.SetFromTrueDofs(density_true); } static int diagnostic_quadrature_order(const mean_field::fem::FEM &fem) { return 2 * std::max(fem.gravityPotentialFes->GetMaxElementOrder(), fem.gravityFluxFes->GetMaxElementOrder()) + 8; } static mfem::Vector assemble_monopole_projection_rhs( mean_field::fem::FEM &fem, const mfem::GridFunction &displacement, const double mass, const double stellar_radius) { *fem.displacement = displacement; mfem::Vector local_rhs(fem.gravityFluxFes->GetVSize()); local_rhs = 0.0; const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; const int quadrature_order = diagnostic_quadrature_order(fem); mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate); for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id); mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id); mfem::Array gravity_dofs; mfem::DofTransformation *gravity_transform = fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); const int dof_count = gravity_element.GetDof(); const int dimension = transformation->GetSpaceDim(); mfem::Vector element_rhs(dof_count); mfem::Vector physical_position(dimension); mfem::Vector analytic_field(dimension); mfem::Vector pulled_field(dimension); mfem::DenseMatrix vector_shape(dof_count, dimension); element_rhs = 0.0; const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) { const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); mean_field::mapping::MappingPointContext mapping_context; MFEM_VERIFY( mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) == mean_field::mapping::MappingStatus::valid, "Invalid mapping in monopole projection RHS."); physical_position = mapping_context.physical_position; const double radius = physical_position.Norml2(); MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole projection RHS."); analytic_field = physical_position; if (transformation->Attribute == vacuum_attribute) { analytic_field *= mean_field::utils::G * mass / (radius * radius * radius); } else { analytic_field *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius); } mapping_context.mapping_jacobian.MultTranspose(analytic_field, pulled_field); transformation->SetIntPoint(&point); gravity_element.CalcVShape(*transformation, vector_shape); const double reference_weight = point.weight * transformation->Weight(); for (int dof = 0; dof < dof_count; ++dof) { for (int component = 0; component < dimension; ++component) { element_rhs(dof) += reference_weight * vector_shape(dof, component) * pulled_field(component); } } } if (gravity_transform != nullptr) { gravity_transform->TransformDual(element_rhs); } local_rhs.AddElementVector(gravity_dofs, element_rhs); } mfem::Vector true_rhs(fem.gravityFluxFes->GetTrueVSize()); true_rhs = 0.0; const mfem::Operator *prolongation = fem.gravityFluxFes->GetProlongationMatrix(); if (prolongation != nullptr) { prolongation->MultTranspose(local_rhs, true_rhs); } else { true_rhs = local_rhs; } return true_rhs; } static mfem::Vector project_monopole_gradient(mean_field::fem::FEM &fem, const mfem::GridFunction &displacement, const double mass, const double stellar_radius) { mfem::Vector displacement_true; displacement.GetTrueDofs(displacement_true); const mfem::Vector projection_rhs_true = assemble_monopole_projection_rhs(fem, displacement, mass, stellar_radius); mean_field::operators::PreparedMappedHDivMassOperator mass_operator( fem, *fem.domainMapperStateless); mass_operator.Prepare( mass_operator.GetDisplacementMap().gather(displacement_true)); const mfem::Vector projection_rhs = mass_operator.GetFluxMap().gather(projection_rhs_true); mfem::CGSolver solver(fem.gravityFluxFes->GetComm()); solver.SetOperator(mass_operator); solver.SetRelTol(1.0e-11); solver.SetAbsTol(1.0e-13); solver.SetMaxIter(4000); solver.SetPrintLevel(0); mfem::Vector projected_gradient_reduced( mass_operator.GetFluxMap().reduced_size()); projected_gradient_reduced = 0.0; solver.Mult(projection_rhs, projected_gradient_reduced); mfem::Vector residual; mass_operator.Mult(projected_gradient_reduced, residual); residual -= projection_rhs; const double relative_residual = global_norm(residual, fem.gravityFluxFes->GetComm()) / std::max(global_norm(projection_rhs, fem.gravityFluxFes->GetComm()), std::numeric_limits::epsilon()); REQUIRE(std::isfinite(relative_residual)); REQUIRE(relative_residual < 1.0e-8); return mass_operator.GetFluxMap().scatter(projected_gradient_reduced); } static double mapped_hdiv_relative_gap(mean_field::fem::FEM &fem, const mfem::GridFunction &displacement, const mfem::Vector &calculated, const mfem::Vector &reference) { mfem::Vector displacement_true; displacement.GetTrueDofs(displacement_true); mean_field::operators::PreparedMappedHDivMassOperator mass_operator( fem, *fem.domainMapperStateless); mass_operator.Prepare( mass_operator.GetDisplacementMap().gather(displacement_true)); mfem::Vector difference(calculated); difference -= reference; mfem::Vector difference_action; mfem::Vector reference_action; const mfem::Vector reduced_difference = mass_operator.GetFluxMap().gather(difference); const mfem::Vector reduced_reference = mass_operator.GetFluxMap().gather(reference); mass_operator.Mult(reduced_difference, difference_action); mass_operator.Mult(reduced_reference, reference_action); const double difference_energy = global_dot( reduced_difference, difference_action, fem.gravityFluxFes->GetComm()); const double reference_energy = global_dot( reduced_reference, reference_action, fem.gravityFluxFes->GetComm()); MFEM_VERIFY(reference_energy > 0.0, "Projected monopole field has zero mapped H(div) norm."); return std::sqrt(std::max(0.0, difference_energy) / reference_energy); } static AccuracyBudgetEnergies measure_stellar_energies(mean_field::fem::FEM &fem, const mfem::GridFunction &density, const mean_field::physics::GravitySolution &solution) { const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; const int quadrature_order = diagnostic_quadrature_order(fem); mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate); double local_binding = 0.0; double local_virial = 0.0; mfem::Vector physical_position(3); mfem::Vector reference_field(3); mfem::Vector physical_field(3); for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id); if (transformation->Attribute == vacuum_attribute) { continue; } const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) { const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); transformation->SetIntPoint(&point); mean_field::mapping::MappingPointContext mapping_context; MFEM_VERIFY( mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) == mean_field::mapping::MappingStatus::valid, "Invalid mapping in energy diagnostic."); physical_position = mapping_context.physical_position; const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian; const double mapping_determinant = mapping_context.mapping_determinant; MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in energy diagnostic."); solution.gradPhi.GetVectorValue(element_id, point, reference_field); mapping_jacobian.Mult(reference_field, physical_field); physical_field /= mapping_determinant; const double weight = point.weight * transformation->Weight() * mapping_determinant; const double rho = density.GetValue(element_id, point); const double phi = solution.phi.GetValue(element_id, point); local_binding += 0.5 * rho * phi * weight; local_virial -= rho * (physical_position * physical_field) * weight; } } AccuracyBudgetEnergies energies; MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm()); MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm()); return energies; } static double reduced_gravity_relative_residual( mean_field::fem::FEM &fem, const mfem::GridFunction &density, const mfem::GridFunction &displacement, const mean_field::physics::GravitySolution &solution) { using GravityFieldForm = mean_field::utils::blocks::gravity_field_form; constexpr auto gradient_block = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::gravity_field.gradient_term); constexpr auto poisson_block = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::gravity_field.poisson_term); using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; const mean_field::field::FieldDofMap density_map = mean_field::field::make_field_dof_map(*fem.densityFes); const mean_field::field::FieldDofMap displacement_map = mean_field::field::make_field_dof_map(*fem.displacementFes); const mean_field::field::FieldDofMap gravity_flux_map = mean_field::field::make_field_dof_map(*fem.gravityFluxFes); const mean_field::field::FieldDofMap gravity_potential_map = mean_field::field::make_field_dof_map( *fem.gravityPotentialFes); const std::array value_sizes{ density_map.reduced_size(), displacement_map.reduced_size(), gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()}; const std::array residual_sizes{ gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()}; const mean_field::utils::blocks::form_layout layout( value_sizes, residual_sizes); mfem::Vector density_true; mfem::Vector displacement_true; mfem::Vector gradient_true; mfem::Vector potential_true; density.GetTrueDofs(density_true); displacement.GetTrueDofs(displacement_true); solution.gradPhi.GetTrueDofs(gradient_true); solution.phi.GetTrueDofs(potential_true); mean_field::operators::context::gravity_field:: GravityFieldLinearizationContext linearization_context( fem, *fem.domainMapperStateless); mean_field::operators::GravityFieldJacobianOperator jacobian( fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); mean_field::operators::GravityFieldOperator field_operator( fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), jacobian); mean_field::operators::context::gravity_field::GravityFieldGeometryContext geometry_context(fem, *fem.domainMapperStateless); mean_field::operators::ReducedGravityFieldOperator reduced_operator( field_operator, geometry_context, displacement_map.gather(displacement_true)); mfem::Vector right_hand_side; reduced_operator.BuildRightHandSide(density_map.gather(density_true), right_hand_side); mfem::BlockVector state(layout.residual_offsets()); state = 0.0; state.GetBlock(gradient_block) = gravity_flux_map.gather(gradient_true); state.GetBlock(poisson_block) = gravity_potential_map.gather(potential_true); mfem::Vector residual; reduced_operator.Mult(state, residual); residual -= right_hand_side; return global_norm(residual, fem.mesh->GetComm()) / std::max(global_norm(right_hand_side, fem.mesh->GetComm()), std::numeric_limits::epsilon()); } static AccuracyBudgetMetrics measure_monopole_accuracy(mean_field::fem::FEM &fem, const mfem::GridFunction &density, const mfem::GridFunction &displacement, const mean_field::physics::GravitySolution &solution, const mfem::ParGridFunction &projected_potential, const mfem::Vector &projected_gradient, const double mass, const double stellar_radius) { mfem::Vector solution_gradient; solution.gradPhi.GetTrueDofs(solution_gradient); mfem::Vector solution_potential; mfem::Vector projection_potential; solution.phi.GetTrueDofs(solution_potential); projected_potential.GetTrueDofs(projection_potential); mfem::ParGridFunction projected_gradient_grid_function( fem.gravityFluxFes.get()); projected_gradient_grid_function.SetFromTrueDofs(projected_gradient); double local_solution_gradient_error = 0.0; double local_projection_gradient_error = 0.0; double local_gradient_norm = 0.0; double local_solution_potential_error = 0.0; double local_projection_potential_error = 0.0; double local_potential_norm = 0.0; const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; const int quadrature_order = diagnostic_quadrature_order(fem); mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate); mfem::Vector physical_position(3); mfem::Vector analytic_gradient(3); mfem::Vector solution_reference_gradient(3); mfem::Vector projection_reference_gradient(3); mfem::Vector solution_physical_gradient(3); mfem::Vector projection_physical_gradient(3); for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id); const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) { const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); transformation->SetIntPoint(&point); mean_field::mapping::MappingPointContext mapping_context; MFEM_VERIFY( mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) == mean_field::mapping::MappingStatus::valid, "Invalid mapping in accuracy diagnostic."); physical_position = mapping_context.physical_position; const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian; const double mapping_determinant = mapping_context.mapping_determinant; MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in accuracy diagnostic."); const double radius = physical_position.Norml2(); MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole diagnostic."); analytic_gradient = physical_position; double analytic_potential = 0.0; if (transformation->Attribute == vacuum_attribute) { analytic_gradient *= mean_field::utils::G * mass / (radius * radius * radius); analytic_potential = -mean_field::utils::G * mass / radius; } else { analytic_gradient *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius); analytic_potential = -mean_field::utils::G * mass * (3.0 * stellar_radius * stellar_radius - radius * radius) / (2.0 * stellar_radius * stellar_radius * stellar_radius); } solution.gradPhi.GetVectorValue(element_id, point, solution_reference_gradient); mapping_jacobian.Mult(solution_reference_gradient, solution_physical_gradient); solution_physical_gradient /= mapping_determinant; projected_gradient_grid_function.GetVectorValue( element_id, point, projection_reference_gradient); mapping_jacobian.Mult(projection_reference_gradient, projection_physical_gradient); projection_physical_gradient /= mapping_determinant; const double solution_potential_value = solution.phi.GetValue(element_id, point); const double projection_potential_value = projected_potential.GetValue(element_id, point); const double weight = point.weight * transformation->Weight() * mapping_determinant; solution_physical_gradient -= analytic_gradient; projection_physical_gradient -= analytic_gradient; local_solution_gradient_error += weight * (solution_physical_gradient * solution_physical_gradient); local_projection_gradient_error += weight * (projection_physical_gradient * projection_physical_gradient); local_gradient_norm += weight * (analytic_gradient * analytic_gradient); local_solution_potential_error += weight * (solution_potential_value - analytic_potential) * (solution_potential_value - analytic_potential); local_projection_potential_error += weight * (projection_potential_value - analytic_potential) * (projection_potential_value - analytic_potential); local_potential_norm += weight * analytic_potential * analytic_potential; } } const std::array local_values{local_solution_gradient_error, local_projection_gradient_error, local_gradient_norm, local_solution_potential_error, local_projection_potential_error, local_potential_norm}; std::array global_values{}; MPI_Allreduce(local_values.data(), global_values.data(), static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm()); const AccuracyBudgetEnergies energies = measure_stellar_energies(fem, density, solution); const double analytic_energy = -3.0 * mean_field::utils::G * mass * mass / (5.0 * stellar_radius); REQUIRE(global_values[2] > 0.0); REQUIRE(global_values[5] > 0.0); AccuracyBudgetMetrics metrics; metrics.direct_relative_residual = reduced_gravity_relative_residual(fem, density, displacement, solution); metrics.gradient_relative_error = std::sqrt(global_values[0] / global_values[2]); metrics.gradient_projection_relative_error = std::sqrt(global_values[1] / global_values[2]); metrics.gradient_solution_projection_gap = mapped_hdiv_relative_gap( fem, displacement, solution_gradient, projected_gradient); metrics.potential_relative_error = std::sqrt(global_values[3] / global_values[5]); metrics.potential_projection_relative_error = std::sqrt(global_values[4] / global_values[5]); mfem::Vector potential_difference(solution_potential); potential_difference -= projection_potential; const double projection_potential_norm = global_norm(projection_potential, fem.gravityPotentialFes->GetComm()); REQUIRE(projection_potential_norm > 0.0); metrics.potential_solution_projection_gap = global_norm(potential_difference, fem.gravityPotentialFes->GetComm()) / projection_potential_norm; metrics.binding_relative_error = std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy); metrics.virial_relative_error = std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy); metrics.virial_consistency_error = std::abs(energies.binding - energies.virial) / std::max(std::abs(energies.binding), std::numeric_limits::epsilon()); return metrics; } static void run_monopole_case(const std::string &sweep_name, const std::string &case_name, mean_field::utils::Args args, const double solver_tolerance, const int quadrature_boost) { args.p.rtol = solver_tolerance; args.p.atol = std::min(args.p.atol, solver_tolerance * 1.0e-2); args.p.max_iters = std::max(args.p.max_iters, 2000); args.quadrature.global_boost = quadrature_boost; mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(fem.domainMapperStateless != nullptr); const double stellar_radius = mean_field::utils::RADIUS; const double mass = mean_field::utils::MASS; const double density_value = mass / ((4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius); mfem::ParGridFunction displacement(fem.displacementFes.get()); displacement = 0.0; *fem.displacement = 0.0; mfem::GridFunction density(fem.densityFes.get()); density = density_value; zero_vacuum_density(fem, density); mean_field::analysis::conserve_mass(fem, density, mass); fem.com = mean_field::analysis::get_com(fem, density); fem.Q = mean_field::physics::compute_quadrupole_moment_tensor(fem, density, fem.com); const mean_field::physics::GravitySolution solution = mean_field::physics::solve_gravity_field(fem, args, density, displacement); auto analytic_potential = [mass, stellar_radius](const mfem::Vector &position) { const double radius = position.Norml2(); if (radius >= stellar_radius) { return -mean_field::utils::G * mass / radius; } return -mean_field::utils::G * mass * (3.0 * stellar_radius * stellar_radius - radius * radius) / (2.0 * stellar_radius * stellar_radius * stellar_radius); }; mean_field::mapping::PhysicalPositionFunctionCoefficient potential_coefficient(*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, analytic_potential); mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get()); projected_potential.ProjectCoefficient(potential_coefficient); const mfem::Vector projected_gradient = project_monopole_gradient(fem, displacement, mass, stellar_radius); const AccuracyBudgetMetrics metrics = measure_monopole_accuracy( fem, density, displacement, solution, projected_potential, projected_gradient, mass, stellar_radius); REQUIRE(std::isfinite(metrics.direct_relative_residual)); REQUIRE(std::isfinite(metrics.gradient_relative_error)); REQUIRE(std::isfinite(metrics.potential_relative_error)); REQUIRE(std::isfinite(metrics.virial_consistency_error)); record_experiment_result( sweep_name, case_name, {{"solver_rtol", std::to_string(solver_tolerance)}, {"quadrature_global_boost", std::to_string(quadrature_boost)}, {"mesh_file", args.mesh_file}}, {{"direct_relative_residual", metrics.direct_relative_residual}, {"gradient_relative_error", metrics.gradient_relative_error}, {"gradient_projection_relative_error", metrics.gradient_projection_relative_error}, {"gradient_solution_projection_gap", metrics.gradient_solution_projection_gap}, {"potential_relative_error", metrics.potential_relative_error}, {"potential_projection_relative_error", metrics.potential_projection_relative_error}, {"potential_solution_projection_gap", metrics.potential_solution_projection_gap}, {"binding_relative_error", metrics.binding_relative_error}, {"virial_relative_error", metrics.virial_relative_error}, {"virial_consistency_error", metrics.virial_consistency_error}}); } TEST_CASE("Uniform Monopole Accuracy Budget: Solver Tolerance", tags::gravity_analytic_accuracy) { const mean_field::utils::Args args = test_utils::setup_args(); constexpr std::array solver_tolerances{1.0e-8, 1.0e-10, 1.0e-12, 1.0e-14}; for (const double solver_tolerance : solver_tolerances) { run_monopole_case("solver_tolerance", "uniform_monopole", args, solver_tolerance, 0); } } TEST_CASE("Uniform Monopole Accuracy Budget: Quadrature", tags::gravity_analytic_accuracy) { const mean_field::utils::Args args = test_utils::setup_args(); constexpr std::array quadrature_boosts{0, 4, 8}; for (const int quadrature_boost : quadrature_boosts) { run_monopole_case("quadrature", "uniform_monopole", args, 1.0e-13, quadrature_boost); } } TEST_CASE("Uniform Monopole Accuracy Budget: Projection Decomposition", tags::gravity_analytic_accuracy) { run_monopole_case("projection_decomposition", "uniform_monopole", test_utils::setup_args(), 1.0e-13, 0); }