From 36adfa11745cf3eb97d89f836609f1d791a9f8d8 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sat, 29 Aug 2026 08:56:36 -0400 Subject: [PATCH] feat(FieldDofMap): Completed FieldDofMap migration also removed legacy BarotropicPolytrope implementation --- CMakeLists.txt | 7 +- experiments/gravity_accuracy_budget.cpp | 1108 ++-- libmeanfield/impl/analysis/integral.cpp | 107 +- libmeanfield/impl/fem.cpp | 686 +- libmeanfield/impl/integrators/advection.cpp | 17 +- libmeanfield/impl/integrators/centrifugal.cpp | 20 +- libmeanfield/impl/integrators/coriolis.cpp | 15 +- libmeanfield/impl/integrators/gravity.cpp | 20 +- .../impl/integrators/mass_continuity.cpp | 40 +- libmeanfield/impl/integrators/viscosity.cpp | 16 +- libmeanfield/impl/mapping/coefficients.cpp | 85 +- libmeanfield/impl/mapping/domain_mapper.cpp | 1406 ++-- .../impl/mapping/domain_mapper_new.cpp | 770 --- ...rotropic_closure_linearization_context.cpp | 2 +- .../contexts/gravity_field_context.cpp | 63 +- .../hydrostatic_equilibrium_context.cpp | 2 +- .../contexts/pressure_force_context.cpp | 2 +- .../rotation_displacement_force_context.cpp | 2 +- libmeanfield/impl/operators/gravity_field.cpp | 1284 ++-- .../impl/operators/gravity_field_jacobian.cpp | 15 +- .../kernels/barotropic_closure_kernels.cpp | 16 +- .../gravity_displacement_force_kernels.cpp | 1246 ++-- .../operators/kernels/gravity_kernels.cpp | 1374 ++-- .../hydrostatic_equilibrium_kernels.cpp | 1139 ++-- .../kernels/pressure_force_kernels.cpp | 986 ++- .../rotation_displacement_force_kernels.cpp | 1002 ++- .../operators/prepared_barotropic_closure.cpp | 6 +- .../prepared_displacement_operator.cpp | 2 +- .../prepared_gravity_displacement_force.cpp | 2 +- .../operators/prepared_gravity_source.cpp | 864 ++- .../impl/operators/prepared_hdiv_mass.cpp | 648 +- .../prepared_hydrostatic_equilibrium.cpp | 2086 +++--- .../operators/prepared_mass_normalization.cpp | 1233 ++-- .../operators/prepared_pressure_force.cpp | 8 +- .../prepared_rotation_displacement_force.cpp | 2 +- .../prepared_stellar_equilibrium.cpp | 28 +- libmeanfield/impl/physics/gravity.cpp | 382 +- libmeanfield/impl/physics/solid.cpp | 20 +- libmeanfield/impl/utils/domain.cpp | 32 +- libmeanfield/impl/utils/misc.cpp | 129 +- libmeanfield/interface/eos/polytropic.cppm | 8 +- libmeanfield/interface/fem.cppm | 41 +- libmeanfield/interface/field/field_mfem.cppm | 117 + .../interface/integrators/advection.cppm | 10 +- .../interface/integrators/centrifugal.cppm | 8 +- .../interface/integrators/coriolis.cppm | 8 +- .../interface/integrators/gravity.cppm | 8 +- .../integrators/mass_continuity.cppm | 16 +- .../integrators/pressure_gradient.cppm | 20 +- .../interface/integrators/viscosity.cppm | 6 +- .../interface/mapping/coefficients.cppm | 34 +- .../interface/mapping/domain_mapper.cppm | 596 +- libmeanfield/interface/mean_field.cppm | 2 - ...otropic_closure_linearization_context.cppm | 4 +- .../contexts/gravity_field_context.cppm | 13 +- .../hydrostatic_equilibrium_context.cppm | 4 +- .../contexts/pressure_force_context.cppm | 2 +- .../rotation_displacement_force_context.cppm | 2 +- .../interface/operators/gravity_field.cppm | 39 +- .../operators/gravity_field_jacobian.cppm | 4 +- .../kernels/barotropic_closure_kernels.cppm | 8 +- .../gravity_displacement_force_kernels.cppm | 10 +- .../operators/kernels/gravity_kernels.cppm | 8 +- .../hydrostatic_equilibrium_kernels.cppm | 12 +- .../kernels/pressure_force_kernels.cppm | 6 +- .../rotation_displacement_force_kernels.cppm | 8 +- .../prepared_barotropic_closure_operator.cppm | 6 +- .../prepared_displacement_operator.cppm | 4 +- .../prepared_gravity_displacement_force.cppm | 4 +- .../operators/prepared_gravity_source.cppm | 4 +- .../operators/prepared_hdiv_mass.cppm | 10 +- ...ared_hydrostatic_equilibrium_operator.cppm | 4 +- .../prepared_mass_normalization.cppm | 4 +- .../operators/prepared_pressure_force.cppm | 6 +- .../prepared_rotation_displacement_force.cppm | 4 +- .../prepared_stellar_equilibrium.cppm | 6 +- libmeanfield/interface/physics/barotrope.cppm | 162 - libmeanfield/interface/physics/context.cppm | 29 - libmeanfield/interface/physics/gravity.cppm | 17 +- libmeanfield/interface/utils/domain.cppm | 2107 +++--- libmeanfield/interface/utils/misc.cppm | 151 +- libmeanfield/interface/utils/user.cppm | 10 +- tests/field/field_dof_map.cpp | 298 +- tests/integrators/centrifugal.cpp | 105 +- tests/integrators/gravity.cpp | 55 +- tests/mapping/domain_mapper.cpp | 5192 ++++++++------- .../contexts/gravity_field_context.cpp | 9 + .../operators/gravity_displacement_force.cpp | 1142 ++-- ...isplacement_force_analytic_comparisons.cpp | 557 +- tests/operators/gravity_field.cpp | 5705 ++++++++--------- .../hydrostatic_equilibrium_kernels.cpp | 1416 ++-- .../kernels/pressure_force_kernels.cpp | 1140 ++-- tests/operators/prepared_hdiv_mass.cpp | 272 +- ...rostatic_equilibrium_analytic_accuracy.cpp | 633 +- tests/operators/prepared_pressure_force.cpp | 1179 ++-- .../prepared_rotation_displacement_force.cpp | 953 +-- .../prepared_stellar_equilibrium.cpp | 4257 ++++++------ tests/physics/barotrope.cpp | 149 +- tests/physics/barotrope_pressure.cpp | 869 +-- tests/physics/gravity.cpp | 5008 +++++++-------- tests/physics/gravity_monopole_accuracy.cpp | 2049 +++--- tests/quadrature/policy.cpp | 22 +- tests/test_helpers.cppm | 733 ++- tests/utils/domain.cpp | 1746 ++--- 104 files changed, 26967 insertions(+), 26916 deletions(-) delete mode 100644 libmeanfield/impl/mapping/domain_mapper_new.cpp delete mode 100644 libmeanfield/interface/physics/barotrope.cppm delete mode 100644 libmeanfield/interface/physics/context.cppm diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cca9ed..f1f6e9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,6 @@ target_sources(mean_field libmeanfield/impl/analysis/integral.cpp libmeanfield/impl/fem.cpp libmeanfield/impl/mapping/coefficients.cpp - libmeanfield/impl/mapping/domain_mapper.cpp libmeanfield/impl/mapping/compactification/kelvin.cpp libmeanfield/impl/physics/gravity.cpp libmeanfield/impl/physics/solid.cpp @@ -56,7 +55,7 @@ target_sources(mean_field libmeanfield/impl/integrators/gravity.cpp libmeanfield/impl/integrators/mass_continuity.cpp libmeanfield/impl/integrators/viscosity.cpp - libmeanfield/impl/mapping/domain_mapper_new.cpp + libmeanfield/impl/mapping/domain_mapper.cpp libmeanfield/impl/mapping/transformations.cpp libmeanfield/impl/operators/gravity_field.cpp libmeanfield/impl/operators/gravity_field_jacobian.cpp @@ -98,7 +97,6 @@ target_sources(mean_field libmeanfield/interface/mapping/compactification/compactification.cppm libmeanfield/interface/mapping/compactification/kelvin.cppm libmeanfield/interface/mapping/compactification/options.cppm - libmeanfield/interface/physics/context.cppm libmeanfield/interface/physics/gravity.cppm libmeanfield/interface/physics/solid.cppm libmeanfield/interface/utils/domain.cppm @@ -126,7 +124,6 @@ target_sources(mean_field libmeanfield/interface/field/field_base.cppm libmeanfield/interface/field/field_registry.cppm libmeanfield/interface/field/field_mfem.cppm - libmeanfield/interface/physics/barotrope.cppm libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm libmeanfield/interface/operators/contexts/barotropic_closure_linearization_context.cppm libmeanfield/interface/physics/rigid_rotation.cppm @@ -256,4 +253,4 @@ catch_discover_tests( tests experiments WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" -) \ No newline at end of file +) diff --git a/experiments/gravity_accuracy_budget.cpp b/experiments/gravity_accuracy_budget.cpp index 462be98..9e8e7e9 100644 --- a/experiments/gravity_accuracy_budget.cpp +++ b/experiments/gravity_accuracy_budget.cpp @@ -10,7 +10,6 @@ #include #include - import mean_field; import test_helpers; import experiment; @@ -18,599 +17,658 @@ import experiment; using namespace experiment; struct AccuracyBudgetEnergies { - double binding{0.0}; - double virial{0.0}; + 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}; + 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_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 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) { - for (int index = 0; index < fem.vacuum_tdof_rho.Size(); ++index) { - density(fem.vacuum_tdof_rho[index]) = 0.0; - } +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.L2_fes->GetMaxElementOrder(), fem.RT_fes->GetMaxElementOrder()) + 8; +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 -) { - static_cast(displacement); + mean_field::fem::FEM &fem, const mfem::GridFunction &displacement, + const double mass, const double stellar_radius) { + *fem.displacement = displacement; - mfem::Vector local_rhs(fem.RT_fes->GetVSize()); - local_rhs = 0.0; + mfem::Vector local_rhs(fem.gravityFluxFes->GetVSize()); + local_rhs = 0.0; - const int vacuum_attribute = fem.domain_mapper_stateless->GetVacuumElementAttribute(); - const int quadrature_order = diagnostic_quadrature_order(fem); + 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.RT_fes->GetFE(element_id); - mfem::ElementTransformation* transformation = fem.mesh->GetElementTransformation(element_id); + 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.RT_fes->GetElementVDofs(element_id, gravity_dofs); + 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 mapping_jacobian(dimension); - mfem::DenseMatrix vector_shape(dof_count, dimension); - element_rhs = 0.0; + 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 - ); + 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); - fem.mapping->GetPhysicalPoint(*transformation, point, physical_position); + 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."); + 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); - } + 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); + } - fem.mapping->ComputeJacobian(*transformation, mapping_jacobian); - mapping_jacobian.MultTranspose(analytic_field, pulled_field); + 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(); + 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); - } - } + 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.RT_fes->GetTrueVSize()); - true_rhs = 0.0; - const mfem::Operator* prolongation = fem.RT_fes->GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->MultTranspose(local_rhs, true_rhs); - } else { - true_rhs = local_rhs; + 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; } - return true_rhs; -} + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); -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); + 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); - const mfem::Vector projection_rhs = assemble_monopole_projection_rhs( - fem, - displacement, - mass, - stellar_radius - ); + 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."); - mean_field::operators::PreparedMappedHDivMassOperator mass_operator( - fem, - *fem.domain_mapper_stateless - ); - mass_operator.Prepare(displacement_true); + solution.gradPhi.GetVectorValue(element_id, point, reference_field); + mapping_jacobian.Mult(reference_field, physical_field); + physical_field /= mapping_determinant; - mfem::CGSolver solver(fem.RT_fes->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(fem.RT_fes->GetTrueVSize()); - projected_gradient = 0.0; - solver.Mult(projection_rhs, projected_gradient); - - mfem::Vector residual; - mass_operator.Mult(projected_gradient, residual); - residual -= projection_rhs; - - const double relative_residual = global_norm(residual, fem.RT_fes->GetComm()) / - std::max(global_norm(projection_rhs, fem.RT_fes->GetComm()), std::numeric_limits::epsilon()); - - REQUIRE(std::isfinite(relative_residual)); - REQUIRE(relative_residual < 1.0e-8); - return projected_gradient; -} - -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.domain_mapper_stateless - ); - mass_operator.Prepare(displacement_true); - - mfem::Vector difference(calculated); - difference -= reference; - mfem::Vector difference_action; - mfem::Vector reference_action; - mass_operator.Mult(difference, difference_action); - mass_operator.Mult(reference, reference_action); - - const double difference_energy = global_dot(difference, difference_action, fem.RT_fes->GetComm()); - const double reference_energy = global_dot(reference, reference_action, fem.RT_fes->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 = fem.domain_mapper_stateless->GetVacuumElementAttribute(); - const int quadrature_order = diagnostic_quadrature_order(fem); - 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); - mfem::DenseMatrix mapping_jacobian(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); - - fem.mapping->GetPhysicalPoint(*transformation, point, physical_position); - fem.mapping->ComputeJacobian(*transformation, mapping_jacobian); - const double mapping_determinant = mapping_jacobian.Det(); - 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; - } + 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.L2_fes->GetComm()); - MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, fem.L2_fes->GetComm()); - return energies; + 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; + 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 - ); + 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); - const std::array value_sizes{ - fem.L2_fes->GetTrueVSize(), fem.Vec_H1_fes->GetTrueVSize(), - fem.RT_fes->GetTrueVSize(), fem.L2_fes->GetTrueVSize() - }; - const std::array residual_sizes{ - fem.RT_fes->GetTrueVSize(), fem.L2_fes->GetTrueVSize() - }; - const mean_field::utils::blocks::form_layout layout(value_sizes, residual_sizes); + 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); - 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); + 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); - mean_field::operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - fem, - *fem.domain_mapper_stateless - ); - mean_field::operators::GravityFieldJacobianOperator jacobian( - fem, - *fem.domain_mapper_stateless, - linearization_context, - layout.value_offsets(), - layout.residual_offsets() - ); - mean_field::operators::GravityFieldOperator field_operator( - fem, - *fem.domain_mapper_stateless, - linearization_context, - layout.value_offsets(), - jacobian - ); - mean_field::operators::context::gravity_field::GravityFieldGeometryContext geometry_context( - fem, - *fem.domain_mapper_stateless - ); - mean_field::operators::ReducedGravityFieldOperator reduced_operator( - field_operator, - geometry_context, - displacement_true - ); + 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); - mfem::Vector right_hand_side; - reduced_operator.BuildRightHandSide(density_true, right_hand_side); + 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::BlockVector state(layout.residual_offsets()); - state = 0.0; - state.GetBlock(gradient_block) = gradient_true; - state.GetBlock(poisson_block) = potential_true; + mfem::Vector right_hand_side; + reduced_operator.BuildRightHandSide(density_map.gather(density_true), + right_hand_side); - mfem::Vector residual; - reduced_operator.Mult(state, residual); - residual -= 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); - return global_norm(residual, fem.L2_fes->GetComm()) / - std::max(global_norm(right_hand_side, fem.L2_fes->GetComm()), std::numeric_limits::epsilon()); + 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); +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::Vector solution_potential; + mfem::Vector projection_potential; + solution.phi.GetTrueDofs(solution_potential); + projected_potential.GetTrueDofs(projection_potential); - mfem::ParGridFunction projected_gradient_grid_function(fem.RT_fes.get()); - projected_gradient_grid_function.SetFromTrueDofs(projected_gradient); + 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; + 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 = fem.domain_mapper_stateless->GetVacuumElementAttribute(); - const int quadrature_order = diagnostic_quadrature_order(fem); - 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); - mfem::DenseMatrix mapping_jacobian(3); + 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 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); - fem.mapping->GetPhysicalPoint(*transformation, point, physical_position); - fem.mapping->ComputeJacobian(*transformation, mapping_jacobian); - const double mapping_determinant = mapping_jacobian.Det(); - MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in accuracy diagnostic."); + 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."); + 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.L2_fes->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.L2_fes->GetComm()); - REQUIRE(projection_potential_norm > 0.0); - metrics.potential_solution_projection_gap = global_norm(potential_difference, fem.L2_fes->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.mapping != nullptr); - REQUIRE(fem.domain_mapper_stateless != 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.Vec_H1_fes.get()); - displacement = 0.0; - fem.mapping->ResetDisplacement(); - mean_field::physics::update_stiffness_matrix(fem); - - mfem::GridFunction density(fem.L2_fes.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::grav_potential_new(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 * + 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); - }; - mean_field::mapping::PhysicalPositionFunctionCoefficient potential_coefficient( - *fem.mapping, - analytic_potential - ); - mfem::ParGridFunction projected_potential(fem.L2_fes.get()); - projected_potential.ProjectCoefficient(potential_coefficient); + } - const mfem::Vector projected_gradient = project_monopole_gradient( - fem, - displacement, - mass, - 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; - const AccuracyBudgetMetrics metrics = measure_monopole_accuracy( - fem, - density, - displacement, - solution, - projected_potential, - projected_gradient, - mass, - stellar_radius - ); + 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; - 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)); + 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; - 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 & tags::accuracy & tags::integration) { - 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 - ); + 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; } -TEST_CASE("Uniform Monopole Accuracy Budget: Quadrature", tags::gravity & tags::accuracy & tags::integration) { - const mean_field::utils::Args args = test_utils::setup_args(); - constexpr std::array quadrature_boosts{0, 4, 8}; +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; - for (const int quadrature_boost : quadrature_boosts) { - run_monopole_case( - "quadrature", - "uniform_monopole", - args, - 1.0e-13, - 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: Projection Decomposition", tags::gravity & tags::accuracy & tags::integration) { - run_monopole_case( - "projection_decomposition", - "uniform_monopole", - test_utils::setup_args(), - 1.0e-13, - 0 - ); +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); } diff --git a/libmeanfield/impl/analysis/integral.cpp b/libmeanfield/impl/analysis/integral.cpp index 86e31b7..5c1dc71 100644 --- a/libmeanfield/impl/analysis/integral.cpp +++ b/libmeanfield/impl/analysis/integral.cpp @@ -6,6 +6,32 @@ module mean_field; import :mapping.coefficients; namespace { + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + + mfem::Array make_domain_marker( + const mfem::Mesh &mesh, + const mean_field::utils::DOMAINS domain + ) { + switch (domain) { + case mean_field::utils::DOMAINS::CORE: + return mean_field::utils::domain::make_attribute_marker< + mean_field::utils::domain::Core, DomainSchema>(mesh); + case mean_field::utils::DOMAINS::ENVELOPE: + return mean_field::utils::domain::make_attribute_marker< + mean_field::utils::domain::Envelope, DomainSchema>(mesh); + case mean_field::utils::DOMAINS::ALL: + return mean_field::utils::domain::make_attribute_marker< + mean_field::utils::domain::All, DomainSchema>(mesh); + case mean_field::utils::DOMAINS::STELLAR: + return mean_field::utils::domain::make_attribute_marker< + mean_field::utils::domain::Stellar, DomainSchema>(mesh); + case mean_field::utils::DOMAINS::VACUUM: + return mean_field::utils::domain::make_attribute_marker< + mean_field::utils::domain::Vacuum, DomainSchema>(mesh); + } + MFEM_ABORT("Unsupported integration domain."); + } + template const mfem::IntegrationRule &get_density_rule( const mean_field::fem::FEM &fem, @@ -36,14 +62,16 @@ namespace mean_field::analysis { mfem::LinearForm lf(fem.densityFes.get()); mfem::GridFunctionCoefficient gf_c(&gf); double local_integral; - mfem::Array elem_markers; - populate_element_mask(fem.mesh.get(), domain, elem_markers); + mfem::Array elem_markers = make_domain_marker(*fem.mesh, domain); const mfem::ElementTransformation &representative_transformation = *fem.mesh->GetElementTransformation(0); const mfem::IntegrationRule &integration_rule = get_density_rule(fem, representative_transformation, {}, domain); if (fem.has_mapping() && coord_space == mapping::COORDINATE_SPACE::PHYSICAL) { - mapping::MappedScalarCoefficient mapped_gf_c(*fem.mapping, gf_c); + mapping::MappedScalarCoefficient mapped_gf_c( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate, gf_c + ); // ReSharper disable once CppDFAMemoryLeak // Disabled because MFEM // takes ownership so memory is not leaked @@ -78,12 +106,17 @@ namespace mean_field::analysis { const mfem::GridFunction &rho ) { const int dim = fem.mesh->Dimension(); + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate + ); mfem::Vector local_com(dim); local_com = 0.0; double local_mass = 0.0; for (int i = 0; i < fem.mesh->GetNE(); ++i) { - if (fem.mesh->GetAttribute(i) == 3) + if (!DomainSchema::template attribute_belongs_to( + fem.mesh->GetAttribute(i))) continue; mfem::ElementTransformation *trans = fem.mesh->GetElementTransformation(i); const mfem::IntegrationRule &ir = get_density_rule( @@ -94,18 +127,16 @@ namespace mean_field::analysis { const mfem::IntegrationPoint &ip = ir.IntPoint(j); trans->SetIntPoint(&ip); - double weight = trans->Weight() * ip.weight; - if (fem.has_mapping()) { - weight *= fem.mapping->ComputeDetJ(*trans, ip); - } + mapping::VolumeMappingContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluateVolume(*trans, ip, mapping_context) == + mapping::MappingStatus::valid, + "Center-of-mass integration encountered an invalid mapping." + ); + const double weight = mapping_context.quadrature.weight; double rho_val = rho.GetValue(i, ip); - mfem::Vector phys_point(dim); - if (fem.has_mapping()) { - fem.mapping->GetPhysicalPoint(*trans, ip, phys_point); - } else { - trans->Transform(ip, phys_point); - } + const mfem::Vector &phys_point = mapping_context.mapping.physical_position; const double mass_term = rho_val * weight; local_mass += mass_term; @@ -151,7 +182,10 @@ namespace mean_field::analysis { std::unique_ptr s2_coeff; if (fem.has_mapping()) { - s2_coeff = std::make_unique(*fem.mapping, s2_func); + s2_coeff = std::make_unique( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate, s2_func + ); } else { s2_coeff = std::make_unique(s2_func); } @@ -164,12 +198,15 @@ namespace mean_field::analysis { const mfem::IntegrationRule &integration_rule = get_density_rule( fem, representative_transformation, std::array{2}, utils::DOMAINS::STELLAR ); - mfem::Array stellar_markers; - populate_element_mask(fem.mesh.get(), utils::DOMAINS::STELLAR, stellar_markers); + mfem::Array stellar_markers = + utils::domain::make_attribute_marker(*fem.mesh); double local_I = 0.0; if (fem.has_mapping()) { - mapping::MappedScalarCoefficient mapped_integrand(*fem.mapping, I_integrand); + mapping::MappedScalarCoefficient mapped_integrand( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate, I_integrand + ); auto *integrator = new mfem::DomainLFIntegrator(mapped_integrand); integrator->SetIntRule(&integration_rule); I_lf.AddDomainIntegrator(integrator, stellar_markers); @@ -201,23 +238,21 @@ namespace mean_field::analysis { } double local_volume = 0.0; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate + ); for (int e = 0; e < mesh.GetNE(); ++e) { const int attr = mesh.GetAttribute(e); - switch (domain) { - case utils::DOMAINS::ALL: - break; - case utils::DOMAINS::STELLAR: - if (attr == 3) - continue; - break; - case utils::DOMAINS::VACUUM: - if (attr != 3) - continue; - break; - default: - MFEM_ABORT("Unsupported domain type for volume computation."); - } + const bool selected = + domain == utils::DOMAINS::ALL || + (domain == utils::DOMAINS::STELLAR && + DomainSchema::template attribute_belongs_to(attr)) || + (domain == utils::DOMAINS::VACUUM && + DomainSchema::template attribute_belongs_to(attr)); + if (!selected) + continue; mfem::ElementTransformation *T = mesh.GetElementTransformation(e); const mfem::IntegrationRule &ir = get_density_rule(fem, *T, {}, domain); @@ -229,7 +264,13 @@ namespace mean_field::analysis { double dV = ip.weight * T->Weight(); if (physical) { - dV *= std::fabs(fem.mapping->ComputeDetJ(*T, ip)); + mapping::VolumeMappingContext context; + MFEM_VERIFY( + mapping_evaluator.EvaluateVolume(*T, ip, context) == + mapping::MappingStatus::valid, + "Mesh-volume integration encountered an invalid mapping." + ); + dV = context.quadrature.weight; } local_volume += dV; diff --git a/libmeanfield/impl/fem.cpp b/libmeanfield/impl/fem.cpp index 39aff4d..4b2a0ea 100644 --- a/libmeanfield/impl/fem.cpp +++ b/libmeanfield/impl/fem.cpp @@ -21,393 +21,361 @@ import :utils.misc; import :utils.user; namespace mean_field::fem { - FEM setup_fem( - const std::string &filename, - const utils::Args &args, - const int extraRefine - ) { - FEM fem; +FEM setup_fem(const std::string &filename, const utils::Args &args, + const int extraRefine) { + FEM fem; - using GravityPotential = field::Gravity::Potential; - using GravityFlux = field::Gravity::Flux; - using DisplacementVector = field::Displacement::Vector; - using DensityScalar = field::Density::Scalar; - using EnthalpyScalar = field::Enthalpy::Scalar; + using GravityPotential = field::Gravity::Potential; + using GravityFlux = field::Gravity::Flux; + using DisplacementVector = field::Displacement::Vector; + using DensityScalar = field::Density::Scalar; + using EnthalpyScalar = field::Enthalpy::Scalar; + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - // ===================================================================== - // Section 1: Mesh construction - // ===================================================================== + // ===================================================================== + // Section 1: Mesh construction + // ===================================================================== - fem.smesh = stroid::IO::LoadStroidMesh(filename).value(); + fem.smesh = stroid::IO::LoadStroidMesh(filename).value(); - if (extraRefine > 0) { - stroid::refinement::UniformRefinement(fem.smesh, extraRefine); - } + if (extraRefine > 0) { + stroid::refinement::UniformRefinement(fem.smesh, extraRefine); + } - int mpiSize = 1; - MPI_Comm_size(MPI_COMM_WORLD, &mpiSize); + int mpiSize = 1; + MPI_Comm_size(MPI_COMM_WORLD, &mpiSize); - const std::unique_ptr meshPartitioning(fem.smesh.mesh->GeneratePartitioning(mpiSize, 1)); + const std::unique_ptr meshPartitioning( + fem.smesh.mesh->GeneratePartitioning(mpiSize, 1)); - fem.mesh = std::make_unique(MPI_COMM_WORLD, *fem.smesh.mesh, meshPartitioning.get(), 1); + fem.mesh = std::make_unique(MPI_COMM_WORLD, *fem.smesh.mesh, + meshPartitioning.get(), 1); - fem.mesh->EnsureNodes(); + fem.mesh->EnsureNodes(); - // ===================================================================== - // Section 2: Exterior compactification coordinate - // ===================================================================== + // ===================================================================== + // Section 2: Exterior compactification coordinate + // ===================================================================== - if (fem.smesh.exterior_coordinate == nullptr) { - throw std::runtime_error("Exterior coordinate not set."); - } + if (fem.smesh.exterior_coordinate == nullptr) { + throw std::runtime_error("Exterior coordinate not set."); + } - if (fem.smesh.exterior_coordinate->space == nullptr) { - throw std::runtime_error("Space for exterior coordinate not set."); - } + if (fem.smesh.exterior_coordinate->space == nullptr) { + throw std::runtime_error("Space for exterior coordinate not set."); + } - if (fem.smesh.exterior_coordinate->values == nullptr) { - throw std::runtime_error("Values for exterior coordinate not set."); - } + if (fem.smesh.exterior_coordinate->values == nullptr) { + throw std::runtime_error("Values for exterior coordinate not set."); + } - const mfem::FiniteElementSpace &serialCoordinateSpace = *fem.smesh.exterior_coordinate->space; + const mfem::FiniteElementSpace &serialCoordinateSpace = + *fem.smesh.exterior_coordinate->space; - const mfem::GridFunction &serialCoordinate = *fem.smesh.exterior_coordinate->values; + const mfem::GridFunction &serialCoordinate = + *fem.smesh.exterior_coordinate->values; - if (serialCoordinate.FESpace() != &serialCoordinateSpace) { - throw std::runtime_error( - "Exterior coordinate values are not associated with the " - "supplied finite-element space." - ); - } + if (serialCoordinate.FESpace() != &serialCoordinateSpace) { + throw std::runtime_error( + "Exterior coordinate values are not associated with the " + "supplied finite-element space."); + } - if (serialCoordinateSpace.GetMesh() != fem.smesh.mesh.get()) { - throw std::runtime_error( - "Exterior coordinate space is not associated with the " - "loaded STROID mesh." - ); - } + if (serialCoordinateSpace.GetMesh() != fem.smesh.mesh.get()) { + throw std::runtime_error( + "Exterior coordinate space is not associated with the " + "loaded STROID mesh."); + } - if (serialCoordinateSpace.GetVDim() != 1) { - throw std::runtime_error("Exterior coordinate must be a scalar field."); - } + if (serialCoordinateSpace.GetVDim() != 1) { + throw std::runtime_error("Exterior coordinate must be a scalar field."); + } - if (serialCoordinate.Size() != serialCoordinateSpace.GetVSize()) { - throw std::runtime_error( - "Exterior coordinate value count does not match its " - "finite-element space." - ); - } + if (serialCoordinate.Size() != serialCoordinateSpace.GetVSize()) { + throw std::runtime_error( + "Exterior coordinate value count does not match its " + "finite-element space."); + } - const int compactificationOrder = serialCoordinateSpace.GetMaxElementOrder(); + const int compactificationOrder = serialCoordinateSpace.GetMaxElementOrder(); - const int dimension = fem.mesh->Dimension(); + const int dimension = fem.mesh->Dimension(); - fem.compactificationFec = std::make_unique(compactificationOrder, dimension); + fem.compactificationFec = + std::make_unique(compactificationOrder, dimension); - fem.compactificationFes = - std::make_unique(fem.mesh.get(), fem.compactificationFec.get()); + fem.compactificationFes = std::make_unique( + fem.mesh.get(), fem.compactificationFec.get()); - mfem::ParGridFunction distributedCoordinate(fem.mesh.get(), &serialCoordinate, meshPartitioning.get()); + mfem::ParGridFunction distributedCoordinate(fem.mesh.get(), &serialCoordinate, + meshPartitioning.get()); - if (distributedCoordinate.Size() != fem.compactificationFes->GetVSize()) { - throw std::runtime_error( - "Distributed exterior coordinate does not match the " - "constructed parallel finite-element space." - ); - } + if (distributedCoordinate.Size() != fem.compactificationFes->GetVSize()) { + throw std::runtime_error( + "Distributed exterior coordinate does not match the " + "constructed parallel finite-element space."); + } - fem.compactificationCoordinate = std::make_unique(fem.compactificationFes.get()); + fem.compactificationCoordinate = + std::make_unique(fem.compactificationFes.get()); - *fem.compactificationCoordinate = distributedCoordinate; + *fem.compactificationCoordinate = distributedCoordinate; - double localMinimum = std::numeric_limits::infinity(); + double localMinimum = std::numeric_limits::infinity(); - double localMaximum = -std::numeric_limits::infinity(); + double localMaximum = -std::numeric_limits::infinity(); - for (int index = 0; index < fem.compactificationCoordinate->Size(); ++index) { - const double value = (*fem.compactificationCoordinate)(index); + for (int index = 0; index < fem.compactificationCoordinate->Size(); ++index) { + const double value = (*fem.compactificationCoordinate)(index); - if (!std::isfinite(value)) { - throw std::runtime_error("Exterior coordinate contains a non-finite value."); - } - - localMinimum = std::min(localMinimum, value); - - localMaximum = std::max(localMaximum, value); - } - - double globalMinimum = 0.0; - double globalMaximum = 0.0; - - MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, MPI_COMM_WORLD); - - MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD); - - constexpr double coordinateTolerance = 1.0e-12; - - if (globalMinimum < -coordinateTolerance || globalMaximum > 1.0 + coordinateTolerance) { - throw std::runtime_error( - "Exterior coordinate lies outside the expected " - "interval [0, 1]." - ); - } - - // ===================================================================== - // Section 3: Compile-time field realization - // ===================================================================== - - // --------------------------------------------------------------------- - // Gravity potential: scalar L2 - // --------------------------------------------------------------------- - - fem.gravityPotentialFec = GravityField::make_fec(dimension); - - fem.gravityPotentialFes = GravityField::make_fespace(*fem.mesh, *fem.gravityPotentialFec); - - // --------------------------------------------------------------------- - // Gravity flux: H(div)/RT. Basis choices are encoded by field.mfem. - // --------------------------------------------------------------------- - - fem.gravityFluxFec = GravityField::make_fec(dimension); - - fem.gravityFluxFes = GravityField::make_fespace(*fem.mesh, *fem.gravityFluxFec); - - // --------------------------------------------------------------------- - // Displacement: vector H1. Ordering is encoded by field.mfem. - // --------------------------------------------------------------------- - - fem.displacementFec = DisplacementField::make_fec(dimension); - - fem.displacementFes = DisplacementField::make_fespace(*fem.mesh, *fem.displacementFec); - - fem.displacement = std::make_unique(fem.displacementFes.get()); - - *fem.displacement = 0.0; - - // --------------------------------------------------------------------- - // Density: scalar discontinuous L2 - // --------------------------------------------------------------------- - - fem.densityFec = DensityField::make_fec(dimension); - - fem.densityFes = DensityField::make_fespace(*fem.mesh, *fem.densityFec); - - // --------------------------------------------------------------------- - // Specific enthalpy: scalar continuous H1 - // --------------------------------------------------------------------- - - fem.enthalpyFec = EnthalpyField::make_fec(dimension); - - fem.enthalpyFes = EnthalpyField::make_fespace(*fem.mesh, *fem.enthalpyFec); - - // ===================================================================== - // Section 4: Domain mapping - // ===================================================================== - - auto [stellarRadiusReference, infinityRadiusReference] = - utils::discover_bounds(fem.mesh.get(), 3) - .or_else([](const boundary::BoundsError &) -> std::expected { - throw std::runtime_error( - "Unable to determine vacuum-domain reference " - "boundaries." - ); - }) - .value(); - - fem.mapping = - std::make_unique(*fem.displacement, stellarRadiusReference, infinityRadiusReference); - - // ===================================================================== - // Section 5: Block offsets - // - // Legacy layouts only. New coupled operators use :utils.blocks forms. - // - // Main system: [Displacement | Density] - // Gravity system: [Flux | Potential] - // ===================================================================== - - fem.blockTrueOffsets.SetSize(3); - fem.blockTrueOffsets[0] = 0; - - fem.blockTrueOffsets[1] = fem.displacementFes->GetTrueVSize(); - - fem.blockTrueOffsets[2] = fem.blockTrueOffsets[1] + fem.densityFes->GetTrueVSize(); - - fem.gravityBlockTrueOffsets.SetSize(3); - fem.gravityBlockTrueOffsets[0] = 0; - - fem.gravityBlockTrueOffsets[1] = fem.gravityFluxFes->GetTrueVSize(); - - fem.gravityBlockTrueOffsets[2] = fem.gravityBlockTrueOffsets[1] + fem.gravityPotentialFes->GetTrueVSize(); - - // ===================================================================== - // Section 6: Multipole data - // ===================================================================== - - fem.com.SetSize(dimension); - fem.com = 0.0; - - fem.Q.SetSize(dimension, dimension); - fem.Q = 0.0; - - // ===================================================================== - // Section 7: Essential boundaries and domain masks - // ===================================================================== - - fem.essentialDisplacementTdofs.SetSize(0); - - populate_element_mask(fem.mesh.get(), utils::DOMAINS::STELLAR, fem.gravityContext.stellar_mask); - - const int boundaryAttributeCount = fem.mesh->bdr_attributes.Max(); - - fem.boundaryContext.inf_bounds.SetSize(boundaryAttributeCount); - - fem.boundaryContext.stellar_bounds.SetSize(boundaryAttributeCount); - - fem.boundaryContext.inf_bounds = 0; - fem.boundaryContext.stellar_bounds = 0; - - fem.boundaryContext.inf_bounds[static_cast(boundary::Boundaries::INF_SURFACE) - 1] = 1; - - fem.boundaryContext.stellar_bounds[static_cast(boundary::Boundaries::STELLAR_SURFACE) - 1] = 1; - - // ===================================================================== - // Section 8: Gravity solver context - // ===================================================================== - - fem.gravityContext.minres = std::make_unique(fem.mesh->GetComm()); - - fem.gravityContext.minres->SetRelTol(1.0e-12); - fem.gravityContext.minres->SetAbsTol(1.0e-12); - fem.gravityContext.minres->SetMaxIter(1000); - fem.gravityContext.minres->SetPrintLevel(0); - - fem.gravityContext.prec_Phi = std::make_unique(); - - fem.gravityContext.prec_Phi->SetPrintLevel(0); - - fem.gravityContext.block_prec = - std::make_unique(fem.gravityBlockTrueOffsets); - - fem.gravityContext.minres->SetPreconditioner(*fem.gravityContext.block_prec); - - // ===================================================================== - // Section 9: Vacuum true-DOF masks - // ===================================================================== - - { - mfem::Array vacuumMask; - - utils::populate_element_mask(fem.mesh.get(), utils::DOMAINS::VACUUM, vacuumMask); - - utils::populate_domain_tdofs(fem.displacementFes.get(), vacuumMask, fem.vacuumDisplacementTdofs); - - utils::populate_domain_tdofs(fem.densityFes.get(), vacuumMask, fem.vacuumDensityTdofs); - - utils::populate_domain_tdofs(fem.enthalpyFes.get(), vacuumMask, fem.vacuumEnthalpyTdofs); - } - - // ===================================================================== - // Section 10: Quadrature policy - // ===================================================================== - - const quadrature::QuadratureOptions &quadratureOptions = args.quadrature; - - if (quadratureOptions.validation.reject_negative_boosts && quadratureOptions.global_boost < 0) { - throw std::invalid_argument("Global quadrature boost cannot be negative."); - } - - quadrature::RuleSet quadratureRuleSet = - quadrature::make_rule_set(quadratureOptions.mode, quadratureOptions.global_boost); - - if (quadratureOptions.fallback_fixed_order.has_value()) { - if (*quadratureOptions.fallback_fixed_order < 0) { - throw std::invalid_argument("Fallback quadrature order cannot be negative."); - } - - quadratureRuleSet.fallback.fixed_order = quadratureOptions.fallback_fixed_order; - } - - auto apply_quadrature_options = [&quadratureOptions]( - quadrature::RuleControl &ruleControl, - const quadrature::QuadratureTermOptions &termOptions - ) { - if (termOptions.fixed_order.has_value() && *termOptions.fixed_order < 0) { - throw std::invalid_argument("Fixed quadrature order cannot be negative."); - } - - if (quadratureOptions.validation.reject_negative_boosts && termOptions.additional_boost < 0) { - throw std::invalid_argument("Term quadrature boost cannot be negative."); - } - - ruleControl.boost += termOptions.additional_boost; - - if (termOptions.fixed_order.has_value()) { - ruleControl.fixed_order = termOptions.fixed_order; - } - }; - - apply_quadrature_options(quadratureRuleSet.gravity_hdiv_mass, quadratureOptions.gravity_hdiv_mass); - - apply_quadrature_options(quadratureRuleSet.gravity_divergence, quadratureOptions.gravity_divergence); - - apply_quadrature_options(quadratureRuleSet.gravity_source, quadratureOptions.gravity_source); - - apply_quadrature_options(quadratureRuleSet.gravity_force, quadratureOptions.gravity_force); - - apply_quadrature_options(quadratureRuleSet.gravity_boundary, quadratureOptions.gravity_boundary); - - apply_quadrature_options(quadratureRuleSet.centrifugal, quadratureOptions.centrifugal); - - apply_quadrature_options(quadratureRuleSet.density_projection, quadratureOptions.density_projection); - - apply_quadrature_options(quadratureRuleSet.eos_closure, quadratureOptions.eos_closure); - - apply_quadrature_options(quadratureRuleSet.hydrostatic_equilibrium, quadratureOptions.hydrostatic_equilibrium); - - apply_quadrature_options(quadratureRuleSet.isobaric_surface, quadratureOptions.isobaric_surface); - - apply_quadrature_options(quadratureRuleSet.mesh_extension, quadratureOptions.mesh_extension); - - apply_quadrature_options(quadratureRuleSet.mass_conservation, quadratureOptions.mass_conservation); - - apply_quadrature_options(quadratureRuleSet.mass_normalization, quadratureOptions.mass_normalization); - - apply_quadrature_options(quadratureRuleSet.center_of_mass, quadratureOptions.center_of_mass); - - apply_quadrature_options(quadratureRuleSet.quadrupole, quadratureOptions.quadrupole); - - apply_quadrature_options(quadratureRuleSet.gravitational_energy, quadratureOptions.gravitational_energy); - - apply_quadrature_options(quadratureRuleSet.pressure_integral, quadratureOptions.pressure_integral); - - apply_quadrature_options(quadratureRuleSet.pressure_force, quadratureOptions.pressure_force); - - apply_quadrature_options(quadratureRuleSet.virial, quadratureOptions.virial); - - apply_quadrature_options(quadratureRuleSet.error_norm, quadratureOptions.error_norm); - - apply_quadrature_options(quadratureRuleSet.roles.discretization, quadratureOptions.roles.discretization); - - apply_quadrature_options(quadratureRuleSet.roles.preconditioner, quadratureOptions.roles.preconditioner); - - apply_quadrature_options(quadratureRuleSet.roles.diagnostic, quadratureOptions.roles.diagnostic); - - apply_quadrature_options(quadratureRuleSet.roles.projection, quadratureOptions.roles.projection); - - fem.quadratureFactory = - std::make_unique(quadrature::Policy(std::move(quadratureRuleSet))); - - // ===================================================================== - // Section 11: Stateless domain mapper - // ===================================================================== - - auto exteriorDomain = - std::make_unique(args.kelvin_options); - - fem.domainMapperStateless = - std::make_unique(args.domain_mapper_options, std::move(exteriorDomain)); - - return fem; + if (!std::isfinite(value)) { + throw std::runtime_error( + "Exterior coordinate contains a non-finite value."); } -} // namespace mean_field::fem \ No newline at end of file + + localMinimum = std::min(localMinimum, value); + + localMaximum = std::max(localMaximum, value); + } + + double globalMinimum = 0.0; + double globalMaximum = 0.0; + + MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, + MPI_COMM_WORLD); + + MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, + MPI_COMM_WORLD); + + constexpr double coordinateTolerance = 1.0e-12; + + if (globalMinimum < -coordinateTolerance || + globalMaximum > 1.0 + coordinateTolerance) { + throw std::runtime_error("Exterior coordinate lies outside the expected " + "interval [0, 1]."); + } + + // ===================================================================== + // Section 3: Compile-time field realization + // ===================================================================== + + // --------------------------------------------------------------------- + // Gravity potential: scalar L2 + // --------------------------------------------------------------------- + + fem.gravityPotentialFec = GravityField::make_fec(dimension); + + fem.gravityPotentialFes = GravityField::make_fespace( + *fem.mesh, *fem.gravityPotentialFec); + + // --------------------------------------------------------------------- + // Gravity flux: H(div)/RT. Basis choices are encoded by field.mfem. + // --------------------------------------------------------------------- + + fem.gravityFluxFec = GravityField::make_fec(dimension); + + fem.gravityFluxFes = + GravityField::make_fespace(*fem.mesh, *fem.gravityFluxFec); + + // --------------------------------------------------------------------- + // Displacement: vector H1. Ordering is encoded by field.mfem. + // --------------------------------------------------------------------- + + fem.displacementFec = + DisplacementField::make_fec(dimension); + + fem.displacementFes = DisplacementField::make_fespace( + *fem.mesh, *fem.displacementFec); + + fem.displacement = + std::make_unique(fem.displacementFes.get()); + + *fem.displacement = 0.0; + + // --------------------------------------------------------------------- + // Density: scalar discontinuous L2 + // --------------------------------------------------------------------- + + fem.densityFec = DensityField::make_fec(dimension); + + fem.densityFes = + DensityField::make_fespace(*fem.mesh, *fem.densityFec); + + // --------------------------------------------------------------------- + // Specific enthalpy: scalar continuous H1 + // --------------------------------------------------------------------- + + fem.enthalpyFec = EnthalpyField::make_fec(dimension); + + fem.enthalpyFes = + EnthalpyField::make_fespace(*fem.mesh, *fem.enthalpyFec); + + // ===================================================================== + // Section 4: Multipole data + // ===================================================================== + + fem.com.SetSize(dimension); + fem.com = 0.0; + + fem.Q.SetSize(dimension, dimension); + fem.Q = 0.0; + + // ===================================================================== + // Section 5: Boundary markers + // ===================================================================== + + const int boundaryAttributeCount = fem.mesh->bdr_attributes.Max(); + + fem.boundaryContext.inf_bounds.SetSize(boundaryAttributeCount); + + fem.boundaryContext.stellar_bounds.SetSize(boundaryAttributeCount); + + fem.boundaryContext.inf_bounds = 0; + fem.boundaryContext.stellar_bounds = 0; + + fem.boundaryContext + .inf_bounds[static_cast(boundary::Boundaries::INF_SURFACE) - 1] = 1; + + fem.boundaryContext + .stellar_bounds[static_cast(boundary::Boundaries::STELLAR_SURFACE) - + 1] = 1; + + // ===================================================================== + // Section 7: Quadrature policy + // ===================================================================== + + const quadrature::QuadratureOptions &quadratureOptions = args.quadrature; + + if (quadratureOptions.validation.reject_negative_boosts && + quadratureOptions.global_boost < 0) { + throw std::invalid_argument("Global quadrature boost cannot be negative."); + } + + quadrature::RuleSet quadratureRuleSet = quadrature::make_rule_set( + quadratureOptions.mode, quadratureOptions.global_boost); + + if (quadratureOptions.fallback_fixed_order.has_value()) { + if (*quadratureOptions.fallback_fixed_order < 0) { + throw std::invalid_argument( + "Fallback quadrature order cannot be negative."); + } + + quadratureRuleSet.fallback.fixed_order = + quadratureOptions.fallback_fixed_order; + } + + auto apply_quadrature_options = [&quadratureOptions]( + quadrature::RuleControl &ruleControl, + const quadrature::QuadratureTermOptions + &termOptions) { + if (termOptions.fixed_order.has_value() && *termOptions.fixed_order < 0) { + throw std::invalid_argument("Fixed quadrature order cannot be negative."); + } + + if (quadratureOptions.validation.reject_negative_boosts && + termOptions.additional_boost < 0) { + throw std::invalid_argument("Term quadrature boost cannot be negative."); + } + + ruleControl.boost += termOptions.additional_boost; + + if (termOptions.fixed_order.has_value()) { + ruleControl.fixed_order = termOptions.fixed_order; + } + }; + + apply_quadrature_options(quadratureRuleSet.gravity_hdiv_mass, + quadratureOptions.gravity_hdiv_mass); + + apply_quadrature_options(quadratureRuleSet.gravity_divergence, + quadratureOptions.gravity_divergence); + + apply_quadrature_options(quadratureRuleSet.gravity_source, + quadratureOptions.gravity_source); + + apply_quadrature_options(quadratureRuleSet.gravity_force, + quadratureOptions.gravity_force); + + apply_quadrature_options(quadratureRuleSet.gravity_boundary, + quadratureOptions.gravity_boundary); + + apply_quadrature_options(quadratureRuleSet.centrifugal, + quadratureOptions.centrifugal); + + apply_quadrature_options(quadratureRuleSet.density_projection, + quadratureOptions.density_projection); + + apply_quadrature_options(quadratureRuleSet.eos_closure, + quadratureOptions.eos_closure); + + apply_quadrature_options(quadratureRuleSet.hydrostatic_equilibrium, + quadratureOptions.hydrostatic_equilibrium); + + apply_quadrature_options(quadratureRuleSet.isobaric_surface, + quadratureOptions.isobaric_surface); + + apply_quadrature_options(quadratureRuleSet.mesh_extension, + quadratureOptions.mesh_extension); + + apply_quadrature_options(quadratureRuleSet.mass_conservation, + quadratureOptions.mass_conservation); + + apply_quadrature_options(quadratureRuleSet.mass_normalization, + quadratureOptions.mass_normalization); + + apply_quadrature_options(quadratureRuleSet.center_of_mass, + quadratureOptions.center_of_mass); + + apply_quadrature_options(quadratureRuleSet.quadrupole, + quadratureOptions.quadrupole); + + apply_quadrature_options(quadratureRuleSet.gravitational_energy, + quadratureOptions.gravitational_energy); + + apply_quadrature_options(quadratureRuleSet.pressure_integral, + quadratureOptions.pressure_integral); + + apply_quadrature_options(quadratureRuleSet.pressure_force, + quadratureOptions.pressure_force); + + apply_quadrature_options(quadratureRuleSet.virial, quadratureOptions.virial); + + apply_quadrature_options(quadratureRuleSet.error_norm, + quadratureOptions.error_norm); + + apply_quadrature_options(quadratureRuleSet.roles.discretization, + quadratureOptions.roles.discretization); + + apply_quadrature_options(quadratureRuleSet.roles.preconditioner, + quadratureOptions.roles.preconditioner); + + apply_quadrature_options(quadratureRuleSet.roles.diagnostic, + quadratureOptions.roles.diagnostic); + + apply_quadrature_options(quadratureRuleSet.roles.projection, + quadratureOptions.roles.projection); + + fem.quadratureFactory = std::make_unique( + quadrature::Policy(std::move(quadratureRuleSet))); + + // ===================================================================== + // Section 11: Stateless domain mapper + // ===================================================================== + + auto exteriorDomain = + std::make_unique( + args.kelvin_options); + + MFEM_VERIFY( + args.domain_mapper_options.vacuum_element_attribute == + DomainSchema::template material_attribute(), + "The domain-mapper compactification attribute must match the vacuum " + "material registered by the " + "production domain schema."); + + fem.domainMapperStateless = std::make_unique( + args.domain_mapper_options, std::move(exteriorDomain)); + + return fem; +} +} // namespace mean_field::fem diff --git a/libmeanfield/impl/integrators/advection.cpp b/libmeanfield/impl/integrators/advection.cpp index 60c6430..de6024b 100644 --- a/libmeanfield/impl/integrators/advection.cpp +++ b/libmeanfield/impl/integrators/advection.cpp @@ -4,7 +4,12 @@ module; module mean_field; namespace mean_field::integrators { - AdvectionIntegrator::AdvectionIntegrator(const mapping::DomainMapper &map) : m_map(map) { + AdvectionIntegrator::AdvectionIntegrator( + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate + ) + : m_mapping(mapper, displacement, compactification_coordinate) { } void AdvectionIntegrator::AssembleElementVector( @@ -13,6 +18,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvec ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elvec)) { return; } @@ -44,7 +51,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_v->CalcDShape(ip, dshape_v_ref); @@ -93,6 +100,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); + const mfem::FiniteElement *fe_v = el[0]; const mfem::FiniteElement *fe_rho = el[1]; @@ -120,7 +129,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_v->CalcDShape(ip, dshape_v_ref); @@ -208,4 +217,4 @@ namespace mean_field::integrators { } } } -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/impl/integrators/centrifugal.cpp b/libmeanfield/impl/integrators/centrifugal.cpp index 83fffa8..9423c01 100644 --- a/libmeanfield/impl/integrators/centrifugal.cpp +++ b/libmeanfield/impl/integrators/centrifugal.cpp @@ -4,10 +4,12 @@ module mean_field; namespace mean_field::integrators { CentrifugalForceIntegrator::CentrifugalForceIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const mfem::Vector &omega ) - : m_map(map), + : m_mapping(mapper, displacement, compactification_coordinate), m_omega(3) { MFEM_ASSERT(omega.Size() == 3, "Omega vector must be 3D"); m_omega = omega; @@ -28,6 +30,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvec ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elvec)) { return; } @@ -64,12 +68,12 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_rho->CalcShape(ip, shape_rho); - m_map.GetPhysicalPoint(Tr, ip, x_phys); + m_mapping.GetPhysicalPoint(Tr, ip, x_phys); // ω x r a(0) = m_omega(1) * x_phys(2) - m_omega(2) * x_phys(1); @@ -100,6 +104,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elmats)) { return; } @@ -134,12 +140,12 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_rho->CalcShape(ip, shape_rho); - m_map.GetPhysicalPoint(Tr, ip, x_phys); + m_mapping.GetPhysicalPoint(Tr, ip, x_phys); // ω x r a(0) = m_omega(1) * x_phys(2) - m_omega(2) * x_phys(1); @@ -162,4 +168,4 @@ namespace mean_field::integrators { } } } -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/impl/integrators/coriolis.cpp b/libmeanfield/impl/integrators/coriolis.cpp index 579b549..86e3312 100644 --- a/libmeanfield/impl/integrators/coriolis.cpp +++ b/libmeanfield/impl/integrators/coriolis.cpp @@ -5,10 +5,12 @@ module mean_field; namespace mean_field::integrators { CoriolisIntegrator::CoriolisIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const mfem::Vector &omega ) - : m_map(map), + : m_mapping(mapper, displacement, compactification_coordinate), m_omega(omega) { m_omega_mat.SetSize(3, 3); m_omega_mat = 0.0; @@ -26,6 +28,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvec ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elvec)) { return; } @@ -55,7 +59,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_rho->CalcShape(ip, shape_rho); @@ -89,6 +93,7 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); const mfem::FiniteElement *fe_v = el[0]; const mfem::FiniteElement *fe_rho = el[1]; @@ -115,7 +120,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_rho->CalcShape(ip, shape_rho); @@ -163,4 +168,4 @@ namespace mean_field::integrators { } } } -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/impl/integrators/gravity.cpp b/libmeanfield/impl/integrators/gravity.cpp index 29e9622..cb99244 100644 --- a/libmeanfield/impl/integrators/gravity.cpp +++ b/libmeanfield/impl/integrators/gravity.cpp @@ -14,10 +14,12 @@ namespace { namespace mean_field::integrators { GravityMomentumIntegrator::GravityMomentumIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const GravityForceJacobianMode jacobian_mode ) - : m_map(map), + : m_mapping(mapper, displacement, compactification_coordinate), m_jacobian_mode(jacobian_mode) { } @@ -39,6 +41,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvec ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elvec)) { return; } @@ -123,7 +127,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); Tr.SetIntPoint(&integration_point); - const mapping::VolumeQuadratureContext context = m_map.GetQuadratureContext(Tr, integration_point); + const mapping::VolumeQuadratureContext context = m_mapping.GetQuadratureContext(Tr, integration_point); velocity_element->CalcShape(integration_point, velocity_shape); density_element->CalcShape(integration_point, density_shape); @@ -152,6 +156,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elmats)) { return; } @@ -189,8 +195,8 @@ namespace mean_field::integrators { MFEM_ABORT( "Exact GravityForceIntegrator geometry Jacobian is unavailable " "until " - "DomainMapper linearization is " - "implemented." + "the stateless mapping variation is wired into this legacy " + "integrator." ); } @@ -240,7 +246,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); Tr.SetIntPoint(&integration_point); - const mapping::VolumeQuadratureContext context = m_map.GetQuadratureContext(Tr, integration_point); + const mapping::VolumeQuadratureContext context = m_mapping.GetQuadratureContext(Tr, integration_point); velocity_element->CalcShape(integration_point, velocity_shape); density_element->CalcShape(integration_point, density_shape); @@ -286,4 +292,4 @@ namespace mean_field::integrators { } } } -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/impl/integrators/mass_continuity.cpp b/libmeanfield/impl/integrators/mass_continuity.cpp index 2bbdb8e..d132d93 100644 --- a/libmeanfield/impl/integrators/mass_continuity.cpp +++ b/libmeanfield/impl/integrators/mass_continuity.cpp @@ -4,7 +4,12 @@ module; module mean_field; namespace mean_field::integrators { - ContinuityVolumeIntegrator::ContinuityVolumeIntegrator(const mapping::DomainMapper &map) : m_map(map) { }; + ContinuityVolumeIntegrator::ContinuityVolumeIntegrator( + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate + ) + : m_mapping(mapper, displacement, compactification_coordinate) { }; void ContinuityVolumeIntegrator::AssembleElementVector( const mfem::Array &el, @@ -12,6 +17,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvec ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elvec)) { return; } @@ -46,7 +53,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_rho->CalcShape(ip, shape_rho); @@ -82,6 +89,7 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); const mfem::FiniteElement *fe_v = el[0]; const mfem::FiniteElement *fe_rho = el[1]; @@ -115,7 +123,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcShape(ip, shape_v); fe_rho->CalcShape(ip, shape_rho); @@ -161,7 +169,12 @@ namespace mean_field::integrators { } } - ContinuityFaceIntegrator::ContinuityFaceIntegrator(const mapping::DomainMapper &map) : m_map(map) { + ContinuityFaceIntegrator::ContinuityFaceIntegrator( + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate + ) + : m_mapping(mapper, displacement, compactification_coordinate) { } void ContinuityFaceIntegrator::AssembleFaceVector( @@ -171,6 +184,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvect ) { + m_mapping.InvalidateCache(); + const mfem::FiniteElement *fe_v_minus = el1[0]; const mfem::FiniteElement *fe_v_plus = el2[0]; @@ -195,9 +210,9 @@ namespace mean_field::integrators { const int attr_minus = Tr.Elem1->Attribute; const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1; - constexpr int VACUUM_ATTR = 3; - - if (attr_minus == VACUUM_ATTR || attr_plus == VACUUM_ATTR) { + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + if (DomainSchema::template attribute_belongs_to(attr_minus) || + DomainSchema::template attribute_belongs_to(attr_plus)) { return; // No flux contribution for vacuum faces } @@ -228,7 +243,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip_minus = Tr.GetElement1IntPoint(); const mfem::IntegrationPoint &ip_plus = Tr.GetElement2IntPoint(); - auto [n_unit, ds, v_dot_n_scale] = m_map.GetFaceQuadratureContext(Tr, face_ip); + auto [n_unit, ds, v_dot_n_scale] = m_mapping.GetFaceQuadratureContext(Tr, face_ip); fe_v_minus->CalcShape(ip_minus, shape_v_minus); fe_rho_minus->CalcShape(ip_minus, shape_rho_minus); @@ -281,6 +296,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); + const mfem::FiniteElement *fe_v_minus = el1[0]; const mfem::FiniteElement *fe_v_plus = el2[0]; const mfem::FiniteElement *fe_rho_minus = el1[1]; @@ -330,7 +347,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip_minus = Tr.GetElement1IntPoint(); const mfem::IntegrationPoint &ip_plus = Tr.GetElement2IntPoint(); - auto [n_unit, ds, v_dot_n_scale] = m_map.GetFaceQuadratureContext(Tr, face_ip); + auto [n_unit, ds, v_dot_n_scale] = m_mapping.GetFaceQuadratureContext(Tr, face_ip); fe_v_minus->CalcShape(ip_minus, shape_v_minus); fe_rho_minus->CalcShape(ip_minus, shape_rho_minus); @@ -399,10 +416,11 @@ namespace mean_field::integrators { } bool ContinuityFaceIntegrator::skip_face(const mfem::FaceElementTransformations &Tr) { - constexpr int VACUUM_ATTR = 3; const int attr_minus = Tr.Elem1->Attribute; const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1; - if (attr_minus == VACUUM_ATTR || attr_plus == VACUUM_ATTR) { + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + if (DomainSchema::template attribute_belongs_to(attr_minus) || + DomainSchema::template attribute_belongs_to(attr_plus)) { return true; // No flux contribution for vacuum faces } if (Tr.Elem2 == nullptr) { diff --git a/libmeanfield/impl/integrators/viscosity.cpp b/libmeanfield/impl/integrators/viscosity.cpp index a21d472..e6ba869 100644 --- a/libmeanfield/impl/integrators/viscosity.cpp +++ b/libmeanfield/impl/integrators/viscosity.cpp @@ -4,11 +4,13 @@ module mean_field; namespace mean_field::integrators { ViscosityIntegrator::ViscosityIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const double mu, const int quad_boost ) - : m_map(map), + : m_mapping(mapper, displacement, compactification_coordinate), m_mu(mu), m_quad_boost(quad_boost) { } @@ -23,6 +25,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvec ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elvec)) { return; } @@ -56,7 +60,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcDShape(ip, dshape_v_ref); mfem::Mult(dshape_v_ref, J_inv, dshape_v_phys); @@ -102,6 +106,8 @@ namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); + const mfem::FiniteElement *fe_v = el[0]; const mfem::FiniteElement *fe_rho = el[1]; @@ -130,7 +136,7 @@ namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcDShape(ip, dshape_v_ref); mfem::Mult(dshape_v_ref, J_inv, dshape_v_phys); @@ -164,4 +170,4 @@ namespace mean_field::integrators { } } -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/impl/mapping/coefficients.cpp b/libmeanfield/impl/mapping/coefficients.cpp index a609584..7ba93fd 100644 --- a/libmeanfield/impl/mapping/coefficients.cpp +++ b/libmeanfield/impl/mapping/coefficients.cpp @@ -9,11 +9,13 @@ namespace mean_field::mapping { /// MappedScalarCoefficient /// ////////////////////////////// MappedScalarCoefficient::MappedScalarCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, Coefficient &coeff, const COORDINATE_SPACE coord_space ) - : m_map(map), + : m_mapping(mapper, displacement, compactification_coordinate), m_coeff(coeff), m_coord_space(coord_space) { }; @@ -27,8 +29,12 @@ namespace mean_field::mapping { switch (m_coord_space) { case COORDINATE_SPACE::PHYSICAL: { f_val = eval_at_point(m_coeff, T, ip); - const double detJ = m_map.ComputeDetJ(T, ip); - return f_val * fabs(detJ); + VolumeMappingContext context; + MFEM_VERIFY( + m_mapping.EvaluateVolume(T, ip, context) == MappingStatus::valid, + "Mapped scalar coefficient encountered an invalid mapping." + ); + return f_val * std::abs(context.mapping.mapping_determinant); } case COORDINATE_SPACE::REFERENCE: { f_val = m_coeff.Eval(T, ip); @@ -50,21 +56,25 @@ namespace mean_field::mapping { ////////////////////////////////// MappedDiffusionCoefficient::MappedDiffusionCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, mfem::Coefficient &sigma, const int dim ) : MatrixCoefficient(dim), - m_map(map), + m_mapping(mapper, displacement, compactification_coordinate), m_scalar(&sigma), m_tensor(nullptr) { }; MappedDiffusionCoefficient::MappedDiffusionCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, MatrixCoefficient &sigma ) : MatrixCoefficient(sigma.GetHeight()), - m_map(map), + m_mapping(mapper, displacement, compactification_coordinate), m_scalar(nullptr), m_tensor(&sigma) { }; @@ -76,10 +86,13 @@ namespace mean_field::mapping { const int dim = height; T.SetIntPoint(&ip); - mfem::DenseMatrix J(dim, dim), JInv(dim, dim); - m_map.ComputeJacobian(T, J); - const double detJ = J.Det(); - mfem::CalcInverse(J, JInv); + VolumeMappingContext context; + MFEM_VERIFY( + m_mapping.EvaluateVolume(T, ip, context) == MappingStatus::valid, + "Mapped diffusion coefficient encountered an invalid mapping." + ); + const mfem::DenseMatrix &JInv = context.mapping.inverse_mapping_jacobian; + const double detJ = context.mapping.mapping_determinant; if (m_scalar) { const double sig_val = m_scalar->Eval(T, ip); @@ -101,11 +114,13 @@ namespace mean_field::mapping { /// MappedVectorCoefficient /// /////////////////////////////// MappedVectorCoefficient::MappedVectorCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, VectorCoefficient &coeff ) : VectorCoefficient(coeff.GetVDim()), - m_map(map), + m_mapping(mapper, displacement, compactification_coordinate), m_coeff(coeff) { }; void MappedVectorCoefficient::Eval( @@ -116,9 +131,13 @@ namespace mean_field::mapping { const int dim = vdim; T.SetIntPoint(&ip); - mfem::DenseMatrix JInv(dim, dim); - m_map.ComputeInverseJacobian(T, JInv); - double detJ = m_map.ComputeDetJ(T, ip); + VolumeMappingContext context; + MFEM_VERIFY( + m_mapping.EvaluateVolume(T, ip, context) == MappingStatus::valid, + "Mapped vector coefficient encountered an invalid mapping." + ); + const mfem::DenseMatrix &JInv = context.mapping.inverse_mapping_jacobian; + const double detJ = context.mapping.mapping_determinant; mfem::Vector C_phys(dim); m_coeff.Eval(C_phys, T, ip); @@ -132,28 +151,35 @@ namespace mean_field::mapping { /// PhysicalPositionFunctionCoefficient /// /////////////////////////////////////////// PhysicalPositionFunctionCoefficient::PhysicalPositionFunctionCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, Func f // std::function ) : m_f(std::move(f)), - m_map(map) { }; + m_mapping(mapper, displacement, compactification_coordinate) { }; double PhysicalPositionFunctionCoefficient::Eval( mfem::ElementTransformation &T, const mfem::IntegrationPoint &ip ) { T.SetIntPoint(&ip); - mfem::Vector x; - m_map.GetPhysicalPoint(T, ip, x); - return m_f(x); + MappingPointContext context; + MFEM_VERIFY( + m_mapping.EvaluatePoint(T, ip, context) == MappingStatus::valid, + "Physical-position coefficient encountered an invalid mapping." + ); + return m_f(context.physical_position); } MappedHDivMassCoefficient::MappedHDivMassCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const int dim ) : MatrixCoefficient(dim), - m_map(map) { + m_mapping(mapper, displacement, compactification_coordinate) { } void MappedHDivMassCoefficient::Eval( @@ -163,10 +189,13 @@ namespace mean_field::mapping { ) { transformation.SetIntPoint(&integration_point); - mfem::DenseMatrix map_jacobian(height, height); - m_map.ComputeJacobian(transformation, map_jacobian); - - const double map_determinant = map_jacobian.Det(); + VolumeMappingContext context; + MFEM_VERIFY( + m_mapping.EvaluateVolume(transformation, integration_point, context) == MappingStatus::valid, + "Mapped H(div) coefficient encountered an invalid mapping." + ); + const mfem::DenseMatrix &map_jacobian = context.mapping.mapping_jacobian; + const double map_determinant = context.mapping.mapping_determinant; MFEM_VERIFY(map_determinant > 0.0, "Domain mapping has a non-positive Jacobian determinant."); diff --git a/libmeanfield/impl/mapping/domain_mapper.cpp b/libmeanfield/impl/mapping/domain_mapper.cpp index a85bec1..b1ccc6a 100644 --- a/libmeanfield/impl/mapping/domain_mapper.cpp +++ b/libmeanfield/impl/mapping/domain_mapper.cpp @@ -1,447 +1,989 @@ module; + +#include +#include #include +#include +#include module mean_field; import :mapping.types; +import :mapping.compactification; +import :utils.user; namespace { - double get_positive_map_jacobian( - const mean_field::mapping::DomainMapper &domain_mapper, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - mfem::DenseMatrix &map_jacobian - ) { - transformation.SetIntPoint(&integration_point); - domain_mapper.ComputeJacobian(transformation, map_jacobian); +bool vector_is_finite(const mfem::Vector &vector) { + for (int i = 0; i < vector.Size(); ++i) { + if (!std::isfinite(vector(i))) + return false; + } + return true; +} - const double map_determinant = map_jacobian.Det(); - MFEM_VERIFY(map_determinant > 0.0, "Domain mapping has a non-positive Jacobian determinant."); - return map_determinant; +bool matrix_is_finite(const mfem::DenseMatrix &matrix) { + for (int i = 0; i < matrix.Height(); ++i) { + for (int j = 0; j < matrix.Width(); ++j) { + if (!std::isfinite(matrix(i, j))) + return false; } + } + return true; +} } // namespace namespace mean_field::mapping { - DomainMapper::DomainMapper( - const double r_star_ref, - const double r_inf_ref - ) - : m_d(nullptr), - m_r_star_ref(r_star_ref), - m_r_inf_ref(r_inf_ref) { - InitAllScratchSpaces(); - CalcIsIdentity() ? m_displacement_is_identity = true : m_displacement_is_identity = false; +ElementCompactificationData::ElementCompactificationData( + const mfem::FiniteElement &element, const mfem::Vector &dofs) + : m_element(&element), m_dofs(dofs) { + if (element.GetRangeType() != mfem::FiniteElement::SCALAR) { + throw std::invalid_argument( + "Compactification coordinate requires a scalar finite element."); + } + + if (element.GetMapType() != mfem::FiniteElement::VALUE) { + throw std::invalid_argument( + "Compactification coordinate requires a value-mapped scalar " + "finite " + "element."); + } + + if (element.GetDerivType() != mfem::FiniteElement::GRAD) { + throw std::invalid_argument( + "Compactification coordinate finite element must provide a " + "gradient."); + } + + if (element.GetDof() <= 0) { + throw std::invalid_argument( + "Compactification coordinate finite element has no degrees of " + "freedom."); + } + + if (dofs.Size() != element.GetDof()) { + throw std::invalid_argument( + "Compactification coordinate DOF count does not match its " + "finite " + "element."); + } +} + +const mfem::FiniteElement & +ElementCompactificationData::GetElement() const noexcept { + return *m_element; +} + +const mfem::Vector &ElementCompactificationData::GetDofs() const noexcept { + return m_dofs; +} + +int ElementCompactificationData::GetDofCount() const noexcept { + return m_dofs.Size(); +} + +ElementDisplacementData::ElementDisplacementData( + const mfem::FiniteElement &element, const mfem::Vector &displacement_dofs, + const mfem::Ordering::Type ordering) + : m_element(&element), m_dimension(0), m_ordering(ordering) { + const int dof_count = element.GetDof(); + if (dof_count <= 0) + throw std::invalid_argument( + "The displacement element must have at least one degree of " + "freedom."); + if (displacement_dofs.Size() <= 0 || + displacement_dofs.Size() % dof_count != 0) { + throw std::invalid_argument( + "The displacement vector size must be a positive multiple of " + "the " + "element degree-of-freedom count."); + } + + m_dimension = displacement_dofs.Size() / dof_count; + m_dof_matrix.SetSize(dof_count, m_dimension); + + if (ordering == mfem::Ordering::byNODES) { + for (int component = 0; component < m_dimension; ++component) { + for (int i = 0; i < dof_count; ++i) { + m_dof_matrix(i, component) = + displacement_dofs(i + component * dof_count); + } + } + } else if (ordering == mfem::Ordering::byVDIM) { + for (int i = 0; i < dof_count; ++i) { + for (int component = 0; component < m_dimension; ++component) { + m_dof_matrix(i, component) = + displacement_dofs(component + i * m_dimension); + } + } + } else { + throw std::invalid_argument("Unsupported MFEM displacement ordering."); + } +} + +const mfem::FiniteElement & +ElementDisplacementData::GetElement() const noexcept { + return *m_element; +} + +const mfem::DenseMatrix & +ElementDisplacementData::GetDofMatrix() const noexcept { + return m_dof_matrix; +} + +int ElementDisplacementData::GetDimension() const noexcept { + return m_dimension; +} + +int ElementDisplacementData::GetDofCount() const noexcept { + return m_element->GetDof(); +} + +mfem::Ordering::Type ElementDisplacementData::GetOrdering() const noexcept { + return m_ordering; +} + +ElementDisplacementData +ElementDisplacementDataFromElementVDofs(const mfem::FiniteElement &element, + const mfem::Vector &displacement_dofs) { + return ElementDisplacementData(element, displacement_dofs, + mfem::Ordering::byNODES); +} + +DomainMapper::Workspace::Workspace(const int dimension) { + SetDimension(dimension); +} + +void DomainMapper::Workspace::SetDimension(const int dimension) { + if (dimension <= 0) { + throw std::invalid_argument( + "Domain mapping workspace dimension must be positive."); + } + + m_dimension = dimension; + + m_field_value.SetSize(dimension); + m_field_jacobian.SetSize(dimension, dimension); + + m_compactification_point.coordinate = 0.0; + m_compactification_point.coordinate_gradient.SetSize(dimension); + + m_reference_normal.SetSize(dimension); + m_mapped_normal.SetSize(dimension); + m_full_element_jacobian.SetSize(dimension, dimension); + + m_vector_temp.SetSize(dimension); + m_matrix_temp_1.SetSize(dimension, dimension); + m_matrix_temp_2.SetSize(dimension, dimension); + + m_exterior_result.physical_position.SetSize(dimension); + m_exterior_result.mapping_jacobian.SetSize(dimension, dimension); + + m_exterior_variation.physical_position_variation.SetSize(dimension); + m_exterior_variation.mapping_jacobian_variation.SetSize(dimension, dimension); +} + +int DomainMapper::Workspace::GetDimension() const noexcept { + return m_dimension; +} + +DomainMapper::DomainMapper( + const utils::DomainMapperOptions options, + std::unique_ptr exterior_map) + : m_options(options), m_exterior_map(std::move(exterior_map)) { + if (m_options.dimension <= 0) + throw std::invalid_argument( + "The domain-mapping dimension must be positive."); + if (m_options.vacuum_element_attribute <= 0) + throw std::invalid_argument( + "The vacuum element attribute must be positive."); + if (!m_exterior_map) + throw std::invalid_argument( + "DomainMapper requires an exterior-domain mapping."); +} + +bool DomainMapper::IsCompactifiedElement( + const mfem::ElementTransformation &transformation) const noexcept { + return transformation.Attribute == m_options.vacuum_element_attribute; +} + +int DomainMapper::GetDimension() const noexcept { + return m_options.dimension; +} + +const compactification::ExteriorDomainMap & +DomainMapper::GetExteriorMap() const noexcept { + return *m_exterior_map; +} + +GridFunctionMappingEvaluator::GridFunctionMappingEvaluator( + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate) + : m_mapper(mapper), m_displacement(displacement), + m_compactification_coordinate(compactification_coordinate), + m_displacement_space(displacement.FESpace()), + m_compactification_space(compactification_coordinate.FESpace()), + m_displacement_space_sequence( + m_displacement_space != nullptr ? m_displacement_space->GetSequence() + : -1), + m_compactification_space_sequence( + m_compactification_space != nullptr + ? m_compactification_space->GetSequence() + : -1), + m_workspace(mapper.GetDimension()) { + if (m_displacement_space == nullptr) { + throw std::invalid_argument( + "Grid-function mapping requires a displacement finite-element space."); + } + if (m_compactification_space == nullptr) { + throw std::invalid_argument( + "Grid-function mapping requires a compactification finite-element " + "space."); + } + if (m_displacement_space->GetMesh() != + m_compactification_space->GetMesh()) { + throw std::invalid_argument( + "Grid-function mapping fields must use the same mesh."); + } + if (m_displacement.VectorDim() != mapper.GetDimension()) { + throw std::invalid_argument( + "The displacement dimension does not match the domain mapper."); + } + if (m_compactification_coordinate.VectorDim() != 1) { + throw std::invalid_argument( + "The compactification coordinate must be a scalar grid function."); + } + if (m_displacement_space->GetMesh()->SpaceDimension() != + mapper.GetDimension()) { + throw std::invalid_argument( + "The mapping dimension does not match the mesh space dimension."); + } +} + +void GridFunctionMappingEvaluator::InvalidateCache() noexcept { + m_displacement_data.reset(); + m_compactification_data.reset(); + m_cached_element_id = -1; +} + +void GridFunctionMappingEvaluator::ValidateFieldBindings() const { + if (m_displacement.FESpace() != m_displacement_space) { + throw std::invalid_argument( + "The displacement grid function was rebound after construction of " + "its mapping evaluator."); + } + if (m_compactification_coordinate.FESpace() != m_compactification_space) { + throw std::invalid_argument( + "The compactification grid function was rebound after construction " + "of its mapping evaluator."); + } +} + +bool GridFunctionMappingEvaluator::InvalidateForChangedSpaces() { + const long displacement_sequence = m_displacement_space->GetSequence(); + const long compactification_sequence = + m_compactification_space->GetSequence(); + + if (displacement_sequence == m_displacement_space_sequence && + compactification_sequence == m_compactification_space_sequence) { + return false; + } + + InvalidateCache(); + m_displacement_space_sequence = displacement_sequence; + m_compactification_space_sequence = compactification_sequence; + return true; +} + +void GridFunctionMappingEvaluator::Refresh() { + ValidateFieldBindings(); + + if (InvalidateForChangedSpaces()) { + return; + } + + const int element_id = m_cached_element_id; + InvalidateCache(); + + if (element_id >= 0) { + LoadElement(element_id); + } +} + +void GridFunctionMappingEvaluator::LoadElement(const int element_id) { + ValidateFieldBindings(); + (void)InvalidateForChangedSpaces(); + + if (element_id == m_cached_element_id) { + return; + } + + const mfem::FiniteElementSpace &displacement_space = *m_displacement_space; + const mfem::FiniteElementSpace &compactification_space = + *m_compactification_space; + + MFEM_VERIFY(element_id >= 0 && + element_id < displacement_space.GetMesh()->GetNE(), + "Grid-function mapping received an invalid element ID."); + + mfem::DofTransformation *displacement_transformation = + displacement_space.GetElementVDofs(element_id, m_displacement_dofs); + compactification_space.GetElementDofs(element_id, m_compactification_dofs); + + m_displacement.GetSubVector(m_displacement_dofs, m_element_displacement); + m_compactification_coordinate.GetSubVector( + m_compactification_dofs, m_element_compactification); + + if (displacement_transformation != nullptr) { + displacement_transformation->InvTransformPrimal(m_element_displacement); + } + + const mfem::FiniteElement &displacement_element = + *displacement_space.GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *compactification_space.GetFE(element_id); + + m_displacement_data = std::make_unique( + ElementDisplacementDataFromElementVDofs(displacement_element, + m_element_displacement)); + m_compactification_data = + std::make_unique( + compactification_element, m_element_compactification); + m_cached_element_id = element_id; +} + +MappingStatus GridFunctionMappingEvaluator::EvaluatePoint( + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + MappingPointContext &context) { + LoadElement(transformation.ElementNo); + const ElementMappingData data{.displacement = *m_displacement_data, + .compactification = + *m_compactification_data}; + return m_mapper.EvaluatePoint(data, transformation, integration_point, + m_workspace, context); +} + +MappingStatus GridFunctionMappingEvaluator::EvaluateVolume( + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + VolumeMappingContext &context) { + LoadElement(transformation.ElementNo); + const ElementMappingData data{.displacement = *m_displacement_data, + .compactification = + *m_compactification_data}; + return m_mapper.EvaluateVolume(data, transformation, integration_point, + m_workspace, context); +} + +MappingStatus GridFunctionMappingEvaluator::EvaluateFace( + mfem::FaceElementTransformations &transformation, + const FaceElementSide side, + const mfem::IntegrationPoint &integration_point, + FaceMappingContext &context) { + mfem::ElementTransformation *element_transformation = + side == FaceElementSide::element_1 ? transformation.Elem1 + : transformation.Elem2; + MFEM_VERIFY(element_transformation != nullptr, + "Grid-function face mapping requires the requested element."); + LoadElement(element_transformation->ElementNo); + const ElementMappingData data{.displacement = *m_displacement_data, + .compactification = + *m_compactification_data}; + return m_mapper.EvaluateFace(data, transformation, side, integration_point, + m_workspace, context); +} + +VolumeQuadratureContext GridFunctionMappingEvaluator::GetQuadratureContext( + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) { + VolumeMappingContext context; + MFEM_VERIFY(EvaluateVolume(transformation, integration_point, context) == + MappingStatus::valid, + "Volume quadrature encountered an invalid domain mapping."); + return context.quadrature; +} + +FaceQuadratureContext GridFunctionMappingEvaluator::GetFaceQuadratureContext( + mfem::FaceElementTransformations &transformation, + const mfem::IntegrationPoint &integration_point, + const FaceElementSide side) { + FaceMappingContext context; + MFEM_VERIFY(EvaluateFace(transformation, side, integration_point, context) == + MappingStatus::valid, + "Face quadrature encountered an invalid domain mapping."); + return context.quadrature; +} + +void GridFunctionMappingEvaluator::GetPhysicalPoint( + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + mfem::Vector &physical_position) { + MappingPointContext context; + MFEM_VERIFY(EvaluatePoint(transformation, integration_point, context) == + MappingStatus::valid, + "Physical-point evaluation encountered an invalid domain " + "mapping."); + physical_position = context.physical_position; +} + +void DomainMapper::ValidateElementData( + const ElementMappingData &element_data) const { + const ElementDisplacementData &displacement = element_data.displacement; + const ElementCompactificationData &compactification = + element_data.compactification; + + if (displacement.GetDimension() != m_options.dimension) { + throw std::invalid_argument( + "Displacement field dimension does not match the domain mapper " + "dimension."); + } + + if (displacement.GetElement().GetDim() != m_options.dimension) { + throw std::invalid_argument( + "Displacement finite element dimension does not match the " + "domain " + "mapper dimension."); + } + + if (compactification.GetElement().GetDim() != m_options.dimension) { + throw std::invalid_argument( + "Compactification finite element dimension does not match the " + "domain " + "mapper dimension."); + } + + if (displacement.GetElement().GetGeomType() != + compactification.GetElement().GetGeomType()) { + throw std::invalid_argument( + "Displacement and compactification finite elements have " + "different " + "geometries."); + } + + if (compactification.GetElement().GetRangeType() != + mfem::FiniteElement::SCALAR) { + throw std::invalid_argument( + "Compactification coordinate requires a scalar finite element."); + } + + if (compactification.GetElement().GetMapType() != + mfem::FiniteElement::VALUE) { + throw std::invalid_argument( + "Compactification coordinate requires a value-mapped finite " + "element."); + } + + if (compactification.GetElement().GetDerivType() != + mfem::FiniteElement::GRAD) { + throw std::invalid_argument( + "Compactification coordinate finite element does not provide a " + "gradient."); + } + + if (compactification.GetDofCount() != + compactification.GetElement().GetDof()) { + throw std::invalid_argument( + "Compactification coordinate DOF count does not match its " + "finite " + "element."); + } +} + +MappingStatus DomainMapper::EvaluateCompactificationCoordinate( + const ElementCompactificationData &compactification, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, Workspace &workspace, + CompactificationPointData &point_data) const { + const mfem::FiniteElement &element = compactification.GetElement(); + const mfem::Vector &dofs = compactification.GetDofs(); + const int dof_count = element.GetDof(); + + if (workspace.GetDimension() != m_options.dimension || + transformation.GetSpaceDim() != m_options.dimension || + element.GetDim() != m_options.dimension) { + return MappingStatus::invalid_dimension; + } + + if (dofs.Size() != dof_count) { + return MappingStatus::invalid_dimension; + } + + for (int i = 0; i < dofs.Size(); ++i) { + if (!std::isfinite(dofs(i))) + return MappingStatus::non_finite_input; + } + + transformation.SetIntPoint(&integration_point); + + workspace.m_compactification_shape.SetSize(dof_count); + workspace.m_compactification_dshape.SetSize(dof_count, m_options.dimension); + + element.CalcShape(integration_point, workspace.m_compactification_shape); + element.CalcPhysDShape(transformation, workspace.m_compactification_dshape); + + point_data.coordinate = dofs * workspace.m_compactification_shape; + point_data.coordinate_gradient.SetSize(m_options.dimension); + workspace.m_compactification_dshape.MultTranspose( + dofs, point_data.coordinate_gradient); + + if (!std::isfinite(point_data.coordinate)) { + return MappingStatus::non_finite_result; + } + + for (int d = 0; d < point_data.coordinate_gradient.Size(); ++d) { + if (!std::isfinite(point_data.coordinate_gradient(d))) + return MappingStatus::non_finite_result; + } + + return MappingStatus::valid; +} + +void DomainMapper::EvaluateField( + const ElementDisplacementData &field, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, Workspace &workspace, + mfem::Vector &value, mfem::DenseMatrix &jacobian) const { + transformation.SetIntPoint(&integration_point); + + const mfem::FiniteElement &element = field.GetElement(); + const mfem::DenseMatrix &dof_matrix = field.GetDofMatrix(); + + workspace.m_shape.SetSize(element.GetDof()); + workspace.m_mesh_dshape.SetSize(element.GetDof(), m_options.dimension); + + element.CalcShape(integration_point, workspace.m_shape); + element.CalcPhysDShape(transformation, workspace.m_mesh_dshape); + + value.SetSize(m_options.dimension); + dof_matrix.MultTranspose(workspace.m_shape, value); + + jacobian.SetSize(m_options.dimension, m_options.dimension); + mfem::MultAtB(dof_matrix, workspace.m_mesh_dshape, jacobian); +} + +MappingStatus DomainMapper::EvaluatePoint( + const ElementMappingData &element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, Workspace &workspace, + MappingPointContext &context) const { + ValidateElementData(element_data); + + if (workspace.GetDimension() != m_options.dimension) + throw std::invalid_argument( + "The mapping workspace has the wrong dimension."); + if (transformation.GetSpaceDim() != m_options.dimension) + throw std::invalid_argument( + "The element transformation has the wrong spatial dimension."); + if (transformation.GetGeometryType() != + element_data.displacement.GetElement().GetGeomType()) + throw std::invalid_argument( + "The element transformation geometry does not match the " + "supplied " + "element data."); + + transformation.SetIntPoint(&integration_point); + + context.reference_position.SetSize(m_options.dimension); + transformation.Transform(integration_point, context.reference_position); + + EvaluateField(element_data.displacement, transformation, integration_point, + workspace, workspace.m_field_value, workspace.m_field_jacobian); + + if (!vector_is_finite(context.reference_position) || + !vector_is_finite(workspace.m_field_value) || + !matrix_is_finite(workspace.m_field_jacobian)) { + return MappingStatus::non_finite_input; + } + + context.displaced_position.SetSize(m_options.dimension); + context.displaced_position = context.reference_position; + context.displaced_position += workspace.m_field_value; + + context.displacement_jacobian.SetSize(m_options.dimension, + m_options.dimension); + context.displacement_jacobian = workspace.m_field_jacobian; + for (int i = 0; i < m_options.dimension; ++i) + context.displacement_jacobian(i, i) += 1.0; + + context.compactified = IsCompactifiedElement(transformation); + + if (context.compactified) { + const MappingStatus coordinate_status = EvaluateCompactificationCoordinate( + element_data.compactification, transformation, integration_point, + workspace, workspace.m_compactification_point); + + if (coordinate_status != MappingStatus::valid) + return coordinate_status; + + const compactification::ExteriorMapInput exterior_input{ + .reference_position = context.reference_position, + .displaced_position = context.displaced_position, + .displacement_jacobian = context.displacement_jacobian, + .compactification_coordinate = + workspace.m_compactification_point.coordinate, + .compactification_coordinate_gradient = + workspace.m_compactification_point.coordinate_gradient}; + + const MappingStatus exterior_status = + m_exterior_map->Evaluate(exterior_input, workspace.m_exterior_result); + if (exterior_status != MappingStatus::valid) + return exterior_status; + + context.physical_position = workspace.m_exterior_result.physical_position; + context.mapping_jacobian = workspace.m_exterior_result.mapping_jacobian; + } else { + context.physical_position = context.displaced_position; + context.mapping_jacobian = context.displacement_jacobian; + } + + if (!vector_is_finite(context.physical_position) || + !matrix_is_finite(context.mapping_jacobian)) + return MappingStatus::non_finite_result; + + context.mapping_determinant = context.mapping_jacobian.Det(); + if (!std::isfinite(context.mapping_determinant)) + return MappingStatus::non_finite_result; + if (context.mapping_determinant <= 0.0) + return MappingStatus::non_positive_determinant; + + context.inverse_mapping_jacobian.SetSize(m_options.dimension, + m_options.dimension); + mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian); + + if (!matrix_is_finite(context.inverse_mapping_jacobian)) + return MappingStatus::non_finite_result; + + return MappingStatus::valid; +} + +MappingStatus DomainMapper::EvaluateVolume( + const ElementMappingData &element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, Workspace &workspace, + VolumeMappingContext &context) const { + const MappingStatus point_status = + EvaluatePoint(element_data, transformation, integration_point, workspace, + context.mapping); + if (point_status != MappingStatus::valid) + return point_status; + + transformation.SetIntPoint(&integration_point); + mfem::Mult(context.mapping.mapping_jacobian, transformation.Jacobian(), + workspace.m_full_element_jacobian); + + context.quadrature.J_inv.SetSize(m_options.dimension, m_options.dimension); + mfem::CalcInverse(workspace.m_full_element_jacobian, + context.quadrature.J_inv); + + context.quadrature.detJ = context.mapping.mapping_determinant; + context.quadrature.weight = integration_point.weight * + transformation.Weight() * + context.mapping.mapping_determinant; + + if (!matrix_is_finite(context.quadrature.J_inv) || + !std::isfinite(context.quadrature.weight)) + return MappingStatus::non_finite_result; + if (context.quadrature.weight <= 0.0) + return MappingStatus::non_positive_determinant; + + return MappingStatus::valid; +} + +mfem::ElementTransformation & +DomainMapper::SelectFaceElementTransformation( + mfem::FaceElementTransformations &transformation, + const FaceElementSide side) { + if (side == FaceElementSide::element_1) { + MFEM_VERIFY(transformation.Elem1 != nullptr, + "The face does not have an element-1 transformation."); + return *transformation.Elem1; + } + + MFEM_VERIFY(transformation.Elem2 != nullptr, + "The face does not have an element-2 transformation."); + return *transformation.Elem2; +} + +const mfem::IntegrationPoint & +DomainMapper::SelectFaceElementIntegrationPoint( + mfem::FaceElementTransformations &transformation, + const FaceElementSide side) { + mfem::ElementTransformation &element_transformation = + SelectFaceElementTransformation(transformation, side); + return element_transformation.GetIntPoint(); +} + +MappingStatus DomainMapper::EvaluateFace( + const ElementMappingData &element_data, + mfem::FaceElementTransformations &transformation, + const FaceElementSide side, const mfem::IntegrationPoint &integration_point, + Workspace &workspace, FaceMappingContext &context) const { + transformation.SetAllIntPoints(&integration_point); + mfem::ElementTransformation &element_transformation = + SelectFaceElementTransformation(transformation, side); + const mfem::IntegrationPoint &element_integration_point = + SelectFaceElementIntegrationPoint(transformation, side); + + const MappingStatus point_status = + EvaluatePoint(element_data, element_transformation, + element_integration_point, workspace, context.mapping); + if (point_status != MappingStatus::valid) + return point_status; + + workspace.m_reference_normal.SetSize(m_options.dimension); + mfem::CalcOrtho(transformation.Jacobian(), workspace.m_reference_normal); + if (side == FaceElementSide::element_2) + workspace.m_reference_normal *= -1.0; + + const double reference_normal_magnitude = + workspace.m_reference_normal.Norml2(); + if (!std::isfinite(reference_normal_magnitude) || + reference_normal_magnitude <= 0.0) + return MappingStatus::non_finite_result; + + context.reference_normal.SetSize(m_options.dimension); + context.reference_normal = workspace.m_reference_normal; + context.reference_normal /= reference_normal_magnitude; + + context.mapping.inverse_mapping_jacobian.MultTranspose( + workspace.m_reference_normal, workspace.m_mapped_normal); + workspace.m_mapped_normal *= context.mapping.mapping_determinant; + + const double mapped_normal_magnitude = workspace.m_mapped_normal.Norml2(); + if (!std::isfinite(mapped_normal_magnitude) || mapped_normal_magnitude <= 0.0) + return MappingStatus::non_finite_result; + + context.quadrature.normal.SetSize(m_options.dimension); + context.quadrature.normal = workspace.m_mapped_normal; + context.quadrature.normal /= mapped_normal_magnitude; + + context.reference_surface_weight = + integration_point.weight * reference_normal_magnitude; + context.physical_surface_weight = + integration_point.weight * mapped_normal_magnitude; + + context.quadrature.ds = context.reference_surface_weight; + context.quadrature.v_dot_n_scale = + mapped_normal_magnitude / reference_normal_magnitude; + + if (!vector_is_finite(context.quadrature.normal) || + !std::isfinite(context.reference_surface_weight) || + !std::isfinite(context.physical_surface_weight) || + !std::isfinite(context.quadrature.v_dot_n_scale)) { + return MappingStatus::non_finite_result; + } + + return MappingStatus::valid; +} + +MappingStatus DomainMapper::EvaluatePointVariation( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + const MappingPointContext &base_context, Workspace &workspace, + MappingPointVariation &variation) const { + ValidateElementData(element_data); + const ElementMappingData direction_data{.displacement = direction, + .compactification = + element_data.compactification}; + ValidateElementData(direction_data); + + if (element_data.displacement.GetDofCount() != direction.GetDofCount()) + throw std::invalid_argument( + "The displacement and direction elements have different " + "degree-of-freedom counts."); + if (workspace.GetDimension() != m_options.dimension) + throw std::invalid_argument( + "The mapping workspace has the wrong dimension."); + if (base_context.compactified != IsCompactifiedElement(transformation)) + throw std::invalid_argument( + "The base mapping context does not match the current element " + "domain."); + + EvaluateField(direction, transformation, integration_point, workspace, + workspace.m_field_value, workspace.m_field_jacobian); + + if (!vector_is_finite(workspace.m_field_value) || + !matrix_is_finite(workspace.m_field_jacobian)) + return MappingStatus::non_finite_input; + + variation.displacement_variation = workspace.m_field_value; + variation.displacement_jacobian_variation = workspace.m_field_jacobian; + + if (base_context.compactified) { + const MappingStatus coordinate_status = EvaluateCompactificationCoordinate( + element_data.compactification, transformation, integration_point, + workspace, workspace.m_compactification_point); + + if (coordinate_status != MappingStatus::valid) + return coordinate_status; + + const compactification::ExteriorMapInput exterior_input{ + .reference_position = base_context.reference_position, + .displaced_position = base_context.displaced_position, + .displacement_jacobian = base_context.displacement_jacobian, + .compactification_coordinate = + workspace.m_compactification_point.coordinate, + .compactification_coordinate_gradient = + workspace.m_compactification_point.coordinate_gradient}; + + workspace.m_exterior_result.physical_position = + base_context.physical_position; + workspace.m_exterior_result.mapping_jacobian = + base_context.mapping_jacobian; + + const compactification::ExteriorMapDirection exterior_direction{ + .displaced_position_variation = variation.displacement_variation, + .displacement_jacobian_variation = + variation.displacement_jacobian_variation}; + + // ReSharper disable once CppTooWideScopeInitStatement + const MappingStatus exterior_status = m_exterior_map->EvaluateVariation( + exterior_input, workspace.m_exterior_result, exterior_direction, + workspace.m_exterior_variation); + + if (exterior_status != MappingStatus::valid) { + return exterior_status; } - DomainMapper::DomainMapper( - const mfem::GridFunction &d, - const double r_star_ref, - const double r_inf_ref - ) - : m_d(&d), - m_dim(d.FESpace()->GetMesh()->Dimension()), - m_r_star_ref(r_star_ref), - m_r_inf_ref(r_inf_ref) { - InitAllScratchSpaces(); - CalcIsIdentity() ? m_displacement_is_identity = true : m_displacement_is_identity = false; - } - - bool DomainMapper::is_vacuum(const mfem::ElementTransformation &T) const { - if (T.ElementType == mfem::ElementTransformation::ELEMENT) { - return T.Attribute == m_vacuum_attr; - } else if (T.ElementType == mfem::ElementTransformation::BDR_ELEMENT) { - return T.Attribute == m_vacuum_attr - 1; - // TODO: In a more robust code this should really be read from the - // stroid API to ensure that the vacuum boundary is really 1 - the - // vacuum material attribute - } - return false; - } - - void DomainMapper::SetDisplacement(const mfem::GridFunction &d) { - if (m_dim != d.FESpace()->GetMesh()->Dimension()) { - const std::string err_msg = std::format( - "Dimension mismatch: DomainMapper is initialized for dimension " - "{}, " - "but provided displacement field has " - "dimension {}.", - m_dim, d.FESpace()->GetMesh()->Dimension() - ); - throw std::invalid_argument(err_msg); - } - m_d = &d; - InvalidateCache(); - - CalcIsIdentity() ? m_displacement_is_identity = true : m_displacement_is_identity = false; - } - - bool DomainMapper::HasCompactification() const noexcept { - return std::isfinite(m_r_star_ref) && std::isfinite(m_r_inf_ref) && m_r_star_ref > 0.0 && - m_r_inf_ref > m_r_star_ref && m_xi_clamp > 0.0 && m_xi_clamp < 1.0; - } - - bool DomainMapper::HasDisplacementField() const noexcept { - return m_d != nullptr; - } - - bool DomainMapper::CalcIsIdentity() const { - if (m_d == nullptr) { - return true; - } - - const int local_identity = m_d->Normlinf() == 0.0 ? 1 : 0; - - const auto *parallel_displacement = dynamic_cast(m_d); - - if (parallel_displacement == nullptr) { - return local_identity == 1; - } - - int global_identity = 0; - MPI_Allreduce( - &local_identity, &global_identity, 1, MPI_INT, MPI_MIN, parallel_displacement->ParFESpace()->GetComm() - ); - - return global_identity == 1; - } - - void DomainMapper::ResetDisplacement() { - m_d = nullptr; - InvalidateCache(); - CalcIsIdentity() ? m_displacement_is_identity = true : m_displacement_is_identity = false; - } - - void DomainMapper::ComputeJacobian( - mfem::ElementTransformation &T, - mfem::DenseMatrix &J - ) const { - J.SetSize(m_dim, m_dim); - J = 0.0; - m_J_D = 0.0; - if (!HasDisplacementField()) { - for (int i = 0; i < m_dim; ++i) { - m_J_D(i, i) = 1.0; // Identity mapping - } - } else { - UpdateElementCache(T); - m_dshape.SetSize(m_fe->GetDof(), m_dim); - m_fe->CalcPhysDShape(T, m_dshape); - mfem::MultAtB(m_dof_mat, m_dshape, m_J_D); - - for (int i = 0; i < m_dim; ++i) { - m_J_D(i, i) += 1.0; - } - } - - if (is_vacuum(T)) { - T.Transform(T.GetIntPoint(), m_x_ref); - - if (!HasDisplacementField()) { - m_x_disp = m_x_ref; - } else { - m_shape.SetSize(m_fe->GetDof()); - m_fe->CalcShape(T.GetIntPoint(), m_shape); - m_dof_mat.MultTranspose(m_shape, m_d_val); - add(m_x_ref, m_d_val, m_x_disp); - } - - ComputeKelvinJacobian(m_x_ref, m_x_disp, m_J_D, J); - } else { - J = m_J_D; - } - } - - double DomainMapper::ComputeDetJ( - mfem::ElementTransformation &T, - const mfem::IntegrationPoint &ip - ) const { - if (!HasDisplacementField() && !is_vacuum(T)) - return 1.0; // If no mapping, the determinant of the Jacobian is 1 - T.SetIntPoint(&ip); - mfem::DenseMatrix J; - ComputeJacobian(T, J); - return J.Det(); - } - - void DomainMapper::ComputeMappedDiffusionTensor( - mfem::ElementTransformation &T, - mfem::DenseMatrix &D - ) const { - ComputeJacobian(T, m_J_temp); - const double detJ = m_J_temp.Det(); - mfem::CalcInverse(m_J_temp, m_JInv_temp); - D.SetSize(m_dim, m_dim); - mfem::MultABt(m_JInv_temp, m_JInv_temp, D); - D *= fabs(detJ); - } - - void DomainMapper::ComputeInverseJacobian( - mfem::ElementTransformation &T, - mfem::DenseMatrix &JInv - ) const { - ComputeJacobian(T, m_J_temp); - JInv.SetSize(m_dim, m_dim); - mfem::CalcInverse(m_J_temp, JInv); - } - - VolumeQuadratureContext DomainMapper::GetQuadratureContext( - mfem::ElementTransformation &T, - const mfem::IntegrationPoint &ip - ) const { - const int dim = T.GetSpaceDim(); - mfem::DenseMatrix J_map(dim, dim), J_inv(dim, dim); - ComputeJacobian(T, J_map); - mfem::DenseMatrix J_full(dim, dim); - mfem::Mult(J_map, T.Jacobian(), J_full); - mfem::CalcInverse(J_full, J_inv); - const double detJ = std::fabs(ComputeDetJ(T, ip)); - const double weight = ip.weight * T.Weight() * detJ; - return {.J_inv = J_inv, .detJ = detJ, .weight = weight}; - } - - FaceQuadratureContext DomainMapper::GetFaceQuadratureContext( - mfem::FaceElementTransformations &T, - const mfem::IntegrationPoint &ip - ) const { - const int dim = T.GetSpaceDim(); - T.SetAllIntPoints(&ip); - - mfem::Vector n_raw(dim); - mfem::CalcOrtho(T.Jacobian(), n_raw); - - if (!HasDisplacementField() && !is_vacuum(T)) { - const double n_raw_mag = n_raw.Norml2(); - mfem::Vector n_unit(dim); - n_unit = n_raw; - n_unit /= n_raw_mag; - return FaceQuadratureContext{.normal = n_unit, .ds = ip.weight * n_raw_mag, .v_dot_n_scale = 1.0}; - } - - // Nanson's Formula - // (https://en.wikiversity.org/wiki/Continuum_mechanics/Volume_change_and_area_change) - // Since the displacement field lives in H1 it should be irrelevant if - // we pick Elem1 or Elem2 - mfem::DenseMatrix J_map(dim, dim); - ComputeJacobian(*T.Elem1, J_map); - const double detJ_map = J_map.Det(); - - mfem::DenseMatrix J_map_inv(dim, dim); - mfem::CalcInverse(J_map, J_map_inv); - - mfem::Vector n_phys(dim); - J_map_inv.MultTranspose(n_raw, n_phys); - n_phys *= detJ_map; - - const double n_phys_mag = n_phys.Norml2(); - mfem::Vector n_unit(dim); - n_unit = n_phys; - n_unit /= n_phys_mag; - - const double n_raw_mag = n_raw.Norml2(); - - return FaceQuadratureContext{ - .normal = n_unit, .ds = ip.weight * n_raw_mag, .v_dot_n_scale = n_phys_mag / n_raw_mag - }; - } - - void DomainMapper::GetPhysicalPoint( - mfem::ElementTransformation &T, - const mfem::IntegrationPoint &ip, - mfem::Vector &x_phys - ) const { - x_phys.SetSize(m_dim); - T.Transform(ip, m_x_ref); - - if (!HasDisplacementField()) { - x_phys = m_x_ref; - } else { - UpdateElementCache(T); - - m_shape.SetSize(m_fe->GetDof()); - m_fe->CalcShape(ip, m_shape); - - m_dof_mat.MultTranspose(m_shape, m_d_val); - add(m_x_ref, m_d_val, x_phys); - } - if (is_vacuum(T)) { - ApplyKelvinMapping(m_x_ref, x_phys); - } - } - - void DomainMapper::GetVectorValue( - const int i, - const mfem::IntegrationPoint &ip, - mfem::Vector &val - ) const { - m_d->GetVectorValue(i, ip, val); - } - - void DomainMapper::MapHDivFluxToPhysical( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const mfem::Vector &reference_flux, - mfem::Vector &physical_flux - ) const { - MFEM_VERIFY(reference_flux.Size() == m_dim, "The reference H(div) flux has the wrong dimension."); - - mfem::DenseMatrix map_jacobian(m_dim, m_dim); - const double map_determinant = - get_positive_map_jacobian(*this, transformation, integration_point, map_jacobian); - - mfem::Vector mapped_flux(m_dim); - map_jacobian.Mult(reference_flux, mapped_flux); - mapped_flux /= map_determinant; - - physical_flux = mapped_flux; - } - - void DomainMapper::MapPhysicalFluxToHDivReference( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const mfem::Vector &physical_flux, - mfem::Vector &reference_flux - ) const { - MFEM_VERIFY(physical_flux.Size() == m_dim, "The physical flux has the wrong dimension."); - - mfem::DenseMatrix map_jacobian(m_dim, m_dim); - const double map_determinant = - get_positive_map_jacobian(*this, transformation, integration_point, map_jacobian); - - mfem::DenseMatrix inverse_map_jacobian(m_dim, m_dim); - mfem::CalcInverse(map_jacobian, inverse_map_jacobian); - - mfem::Vector mapped_flux(m_dim); - inverse_map_jacobian.Mult(physical_flux, mapped_flux); - mapped_flux *= map_determinant; - - reference_flux = mapped_flux; - } - - void DomainMapper::MapReferenceGradientToPhysical( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const mfem::Vector &reference_gradient, - mfem::Vector &physical_gradient - ) const { - MFEM_VERIFY(reference_gradient.Size() == m_dim, "The reference gradient has the wrong dimension."); - - mfem::DenseMatrix map_jacobian(m_dim, m_dim); - get_positive_map_jacobian(*this, transformation, integration_point, map_jacobian); - - mfem::DenseMatrix inverse_map_jacobian(m_dim, m_dim); - mfem::CalcInverse(map_jacobian, inverse_map_jacobian); - - mfem::Vector mapped_gradient(m_dim); - inverse_map_jacobian.MultTranspose(reference_gradient, mapped_gradient); - - physical_gradient = mapped_gradient; - } - - const mfem::GridFunction *DomainMapper::GetDisplacement() const { - return m_d; - } - - double DomainMapper::GetPhysInfRadius() const { - return 1.0 - m_xi_clamp; - } - - size_t DomainMapper::GetCacheHits() const { - return m_cache_hits; - } - - size_t DomainMapper::GetCacheMisses() const { - return m_cache_misses; - } - - double DomainMapper::GetCacheHitRate() const { - return (static_cast(m_cache_hits)) / static_cast(m_cache_misses + m_cache_hits); - } - - void DomainMapper::ResetCacheStats() const { - m_cache_hits = 0; - m_cache_misses = 0; - } - - void DomainMapper::InitAllScratchSpaces() const { - m_J_D.SetSize(m_dim, m_dim); - m_J_temp.SetSize(m_dim, m_dim); - m_JInv_temp.SetSize(m_dim, m_dim); - m_x_ref.SetSize(m_dim); - m_x_disp.SetSize(m_dim); - m_d_val.SetSize(m_dim); - } - - void DomainMapper::ApplyKelvinMapping( - const mfem::Vector &x_ref, - mfem::Vector &x_phys - ) const { - const double r_ref = x_ref.Norml2(); - double xi = (r_ref - m_r_star_ref) / (m_r_inf_ref - m_r_star_ref); - xi = std::clamp(xi, 0.0, m_xi_clamp); - const double factor = m_r_star_ref / (r_ref * (1 - xi)); - x_phys *= factor; - } - - void DomainMapper::ComputeKelvinJacobian( - const mfem::Vector &x_ref, - const mfem::Vector &x_disp, - const mfem::DenseMatrix &J_D, - mfem::DenseMatrix &J - ) const { - const double r_ref = x_ref.Norml2(); - const double delta_R = m_r_inf_ref - m_r_star_ref; - - double xi = (r_ref - m_r_star_ref) / delta_R; - xi = std::clamp(xi, 0.0, m_xi_clamp); - - const double denom = 1.0 - xi; - - const double k = m_r_star_ref / (r_ref * denom); - - const double dk_dr = - m_r_star_ref * ((1.0 / (delta_R * r_ref * denom * denom)) - (1.0 / (r_ref * r_ref * denom))); - - J.SetSize(m_dim, m_dim); - const double outer_factor = dk_dr / r_ref; - - for (int i = 0; i < m_dim; ++i) { - for (int j = 0; j < m_dim; ++j) { - J(i, j) = outer_factor * x_disp(i) * x_ref(j) + k * J_D(i, j); - } - } - } - - void DomainMapper::InvalidateCache() const { - m_cached_elem_id = -1; - } - - void DomainMapper::UpdateElementCache(const mfem::ElementTransformation &T) const { - if (!HasDisplacementField()) - return; - - if (T.ElementNo != m_cached_elem_id || T.ElementType != m_cached_elem_type) { - m_cache_misses++; - m_cached_elem_id = T.ElementNo; - m_cached_elem_type = T.ElementType; - - const mfem::FiniteElementSpace *fes = m_d->FESpace(); - mfem::Array vdofs; - - if (T.ElementType == mfem::ElementTransformation::ELEMENT) { - m_fe = fes->GetFE(m_cached_elem_id); - fes->GetElementVDofs(m_cached_elem_id, vdofs); - } else { - m_fe = fes->GetBE(m_cached_elem_id); - fes->GetBdrElementVDofs(m_cached_elem_id, vdofs); - } - - m_d->GetSubVector(vdofs, m_elem_dofs); - - const int nd = m_fe->GetDof(); - const int vd = fes->GetVDim(); - - m_dof_mat.UseExternalData(m_elem_dofs.GetData(), nd, vd); - } else { - m_cache_hits++; - } - } + variation.physical_position_variation = + workspace.m_exterior_variation.physical_position_variation; + variation.mapping_jacobian_variation = + workspace.m_exterior_variation.mapping_jacobian_variation; + } else { + variation.physical_position_variation = variation.displacement_variation; + variation.mapping_jacobian_variation = + variation.displacement_jacobian_variation; + } + + mfem::Mult(base_context.inverse_mapping_jacobian, + variation.mapping_jacobian_variation, workspace.m_matrix_temp_1); + + double trace = 0.0; + for (int i = 0; i < m_options.dimension; ++i) + trace += workspace.m_matrix_temp_1(i, i); + variation.mapping_determinant_variation = + base_context.mapping_determinant * trace; + + variation.inverse_mapping_jacobian_variation.SetSize(m_options.dimension, + m_options.dimension); + mfem::Mult(workspace.m_matrix_temp_1, base_context.inverse_mapping_jacobian, + variation.inverse_mapping_jacobian_variation); + variation.inverse_mapping_jacobian_variation *= -1.0; + + if (!vector_is_finite(variation.physical_position_variation) || + !matrix_is_finite(variation.mapping_jacobian_variation) || + !matrix_is_finite(variation.inverse_mapping_jacobian_variation) || + !std::isfinite(variation.mapping_determinant_variation)) { + return MappingStatus::non_finite_result; + } + + return MappingStatus::valid; +} + +MappingStatus DomainMapper::EvaluateVolumeVariation( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + const VolumeMappingContext &base_context, Workspace &workspace, + VolumeMappingVariation &variation) const { + const MappingStatus point_status = EvaluatePointVariation( + element_data, direction, transformation, integration_point, + base_context.mapping, workspace, variation.mapping); + if (point_status != MappingStatus::valid) + return point_status; + + transformation.SetIntPoint(&integration_point); + mfem::Mult(variation.mapping.mapping_jacobian_variation, + transformation.Jacobian(), workspace.m_full_element_jacobian); + mfem::Mult(base_context.quadrature.J_inv, workspace.m_full_element_jacobian, + workspace.m_matrix_temp_1); + + variation.inverse_element_jacobian_variation.SetSize(m_options.dimension, + m_options.dimension); + mfem::Mult(workspace.m_matrix_temp_1, base_context.quadrature.J_inv, + variation.inverse_element_jacobian_variation); + variation.inverse_element_jacobian_variation *= -1.0; + + variation.weight_variation = integration_point.weight * + transformation.Weight() * + variation.mapping.mapping_determinant_variation; + + if (!matrix_is_finite(variation.inverse_element_jacobian_variation) || + !std::isfinite(variation.weight_variation)) + return MappingStatus::non_finite_result; + + return MappingStatus::valid; +} + +MappingStatus DomainMapper::EvaluateFaceVariation( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::FaceElementTransformations &transformation, + const FaceElementSide side, const mfem::IntegrationPoint &integration_point, + const FaceMappingContext &base_context, Workspace &workspace, + FaceMappingVariation &variation) const { + transformation.SetAllIntPoints(&integration_point); + mfem::ElementTransformation &element_transformation = + SelectFaceElementTransformation(transformation, side); + const mfem::IntegrationPoint &element_integration_point = + SelectFaceElementIntegrationPoint(transformation, side); + + const MappingStatus point_status = + EvaluatePointVariation(element_data, direction, element_transformation, + element_integration_point, base_context.mapping, + workspace, variation.mapping); + if (point_status != MappingStatus::valid) + return point_status; + + workspace.m_reference_normal.SetSize(m_options.dimension); + mfem::CalcOrtho(transformation.Jacobian(), workspace.m_reference_normal); + if (side == FaceElementSide::element_2) + workspace.m_reference_normal *= -1.0; + + const double reference_normal_magnitude = + workspace.m_reference_normal.Norml2(); + if (!std::isfinite(reference_normal_magnitude) || + reference_normal_magnitude <= 0.0) + return MappingStatus::non_finite_result; + + base_context.mapping.inverse_mapping_jacobian.MultTranspose( + workspace.m_reference_normal, workspace.m_vector_temp); + workspace.m_mapped_normal = workspace.m_vector_temp; + workspace.m_mapped_normal *= base_context.mapping.mapping_determinant; + + variation.physical_normal_variation.SetSize(m_options.dimension); + variation.mapping.inverse_mapping_jacobian_variation.MultTranspose( + workspace.m_reference_normal, variation.physical_normal_variation); + variation.physical_normal_variation *= + base_context.mapping.mapping_determinant; + variation.physical_normal_variation.Add( + variation.mapping.mapping_determinant_variation, workspace.m_vector_temp); + + const double mapped_normal_magnitude = workspace.m_mapped_normal.Norml2(); + if (!std::isfinite(mapped_normal_magnitude) || mapped_normal_magnitude <= 0.0) + return MappingStatus::non_finite_result; + + const double mapped_normal_magnitude_variation = + base_context.quadrature.normal * variation.physical_normal_variation; + + variation.physical_normal_variation.Add(-mapped_normal_magnitude_variation, + base_context.quadrature.normal); + variation.physical_normal_variation /= mapped_normal_magnitude; + + variation.physical_surface_weight_variation = + integration_point.weight * mapped_normal_magnitude_variation; + variation.normal_flux_scale_variation = + mapped_normal_magnitude_variation / reference_normal_magnitude; + + if (!vector_is_finite(variation.physical_normal_variation) || + !std::isfinite(variation.physical_surface_weight_variation) || + !std::isfinite(variation.normal_flux_scale_variation)) { + return MappingStatus::non_finite_result; + } + + return MappingStatus::valid; +} } // namespace mean_field::mapping diff --git a/libmeanfield/impl/mapping/domain_mapper_new.cpp b/libmeanfield/impl/mapping/domain_mapper_new.cpp deleted file mode 100644 index 60c312b..0000000 --- a/libmeanfield/impl/mapping/domain_mapper_new.cpp +++ /dev/null @@ -1,770 +0,0 @@ -module; - -#include -#include -#include -#include -#include - -module mean_field; -import :mapping.types; -import :mapping.compactification; -import :utils.user; - -namespace { - bool vector_is_finite(const mfem::Vector &vector) { - for (int i = 0; i < vector.Size(); ++i) { - if (!std::isfinite(vector(i))) - return false; - } - return true; - } - - bool matrix_is_finite(const mfem::DenseMatrix &matrix) { - for (int i = 0; i < matrix.Height(); ++i) { - for (int j = 0; j < matrix.Width(); ++j) { - if (!std::isfinite(matrix(i, j))) - return false; - } - } - return true; - } -} // namespace - -namespace mean_field::mapping { - ElementCompactificationData::ElementCompactificationData( - const mfem::FiniteElement &element, - const mfem::Vector &dofs - ) - : m_element(&element), - m_dofs(dofs) { - if (element.GetRangeType() != mfem::FiniteElement::SCALAR) { - throw std::invalid_argument("Compactification coordinate requires a scalar finite element."); - } - - if (element.GetMapType() != mfem::FiniteElement::VALUE) { - throw std::invalid_argument( - "Compactification coordinate requires a value-mapped scalar " - "finite " - "element." - ); - } - - if (element.GetDerivType() != mfem::FiniteElement::GRAD) { - throw std::invalid_argument( - "Compactification coordinate finite element must provide a " - "gradient." - ); - } - - if (element.GetDof() <= 0) { - throw std::invalid_argument( - "Compactification coordinate finite element has no degrees of " - "freedom." - ); - } - - if (dofs.Size() != element.GetDof()) { - throw std::invalid_argument( - "Compactification coordinate DOF count does not match its " - "finite " - "element." - ); - } - } - - const mfem::FiniteElement &ElementCompactificationData::GetElement() const noexcept { - return *m_element; - } - - const mfem::Vector &ElementCompactificationData::GetDofs() const noexcept { - return m_dofs; - } - - int ElementCompactificationData::GetDofCount() const noexcept { - return m_dofs.Size(); - } - - ElementDisplacementData::ElementDisplacementData( - const mfem::FiniteElement &element, - const mfem::Vector &displacement_dofs, - const mfem::Ordering::Type ordering - ) - : m_element(&element), - m_dimension(0), - m_ordering(ordering) { - const int dof_count = element.GetDof(); - if (dof_count <= 0) - throw std::invalid_argument( - "The displacement element must have at least one degree of " - "freedom." - ); - if (displacement_dofs.Size() <= 0 || displacement_dofs.Size() % dof_count != 0) { - throw std::invalid_argument( - "The displacement vector size must be a positive multiple of " - "the " - "element degree-of-freedom count." - ); - } - - m_dimension = displacement_dofs.Size() / dof_count; - m_dof_matrix.SetSize(dof_count, m_dimension); - - if (ordering == mfem::Ordering::byNODES) { - for (int component = 0; component < m_dimension; ++component) { - for (int i = 0; i < dof_count; ++i) { - m_dof_matrix(i, component) = displacement_dofs(i + component * dof_count); - } - } - } else if (ordering == mfem::Ordering::byVDIM) { - for (int i = 0; i < dof_count; ++i) { - for (int component = 0; component < m_dimension; ++component) { - m_dof_matrix(i, component) = displacement_dofs(component + i * m_dimension); - } - } - } else { - throw std::invalid_argument("Unsupported MFEM displacement ordering."); - } - } - - const mfem::FiniteElement &ElementDisplacementData::GetElement() const noexcept { - return *m_element; - } - - const mfem::DenseMatrix &ElementDisplacementData::GetDofMatrix() const noexcept { - return m_dof_matrix; - } - - int ElementDisplacementData::GetDimension() const noexcept { - return m_dimension; - } - - int ElementDisplacementData::GetDofCount() const noexcept { - return m_element->GetDof(); - } - - mfem::Ordering::Type ElementDisplacementData::GetOrdering() const noexcept { - return m_ordering; - } - - ElementDisplacementData ElementDisplacementDataFromElementVDofs( - const mfem::FiniteElement &element, - const mfem::Vector &displacement_dofs - ) { - return ElementDisplacementData(element, displacement_dofs, mfem::Ordering::byNODES); - } - - DomainMapperStateless::Workspace::Workspace(const int dimension) { - SetDimension(dimension); - } - - void DomainMapperStateless::Workspace::SetDimension(const int dimension) { - if (dimension <= 0) { - throw std::invalid_argument("Domain mapping workspace dimension must be positive."); - } - - m_dimension = dimension; - - m_field_value.SetSize(dimension); - m_field_jacobian.SetSize(dimension, dimension); - - m_compactification_point.coordinate = 0.0; - m_compactification_point.coordinate_gradient.SetSize(dimension); - - m_reference_normal.SetSize(dimension); - m_mapped_normal.SetSize(dimension); - m_full_element_jacobian.SetSize(dimension, dimension); - - m_vector_temp.SetSize(dimension); - m_matrix_temp_1.SetSize(dimension, dimension); - m_matrix_temp_2.SetSize(dimension, dimension); - - m_exterior_result.physical_position.SetSize(dimension); - m_exterior_result.mapping_jacobian.SetSize(dimension, dimension); - - m_exterior_variation.physical_position_variation.SetSize(dimension); - m_exterior_variation.mapping_jacobian_variation.SetSize(dimension, dimension); - } - - int DomainMapperStateless::Workspace::GetDimension() const noexcept { - return m_dimension; - } - - DomainMapperStateless::DomainMapperStateless( - const utils::DomainMapperStatelessOptions options, - std::unique_ptr exterior_map - ) - : m_options(options), - m_exterior_map(std::move(exterior_map)) { - if (m_options.dimension <= 0) - throw std::invalid_argument("The domain-mapping dimension must be positive."); - if (m_options.vacuum_element_attribute <= 0) - throw std::invalid_argument("The vacuum element attribute must be positive."); - if (!m_exterior_map) - throw std::invalid_argument("DomainMapperStateless requires an exterior-domain mapping."); - } - - bool - DomainMapperStateless::IsCompactifiedElement(const mfem::ElementTransformation &transformation) const noexcept { - return transformation.Attribute == m_options.vacuum_element_attribute; - } - - int DomainMapperStateless::GetDimension() const noexcept { - return m_options.dimension; - } - - int DomainMapperStateless::GetVacuumElementAttribute() const noexcept { - return m_options.vacuum_element_attribute; - } - - const compactification::ExteriorDomainMap &DomainMapperStateless::GetExteriorMap() const noexcept { - return *m_exterior_map; - } - - void DomainMapperStateless::ValidateElementData(const ElementMappingData &element_data) const { - const ElementDisplacementData &displacement = element_data.displacement; - const ElementCompactificationData &compactification = element_data.compactification; - - if (displacement.GetDimension() != m_options.dimension) { - throw std::invalid_argument( - "Displacement field dimension does not match the domain mapper " - "dimension." - ); - } - - if (displacement.GetElement().GetDim() != m_options.dimension) { - throw std::invalid_argument( - "Displacement finite element dimension does not match the " - "domain " - "mapper dimension." - ); - } - - if (compactification.GetElement().GetDim() != m_options.dimension) { - throw std::invalid_argument( - "Compactification finite element dimension does not match the " - "domain " - "mapper dimension." - ); - } - - if (displacement.GetElement().GetGeomType() != compactification.GetElement().GetGeomType()) { - throw std::invalid_argument( - "Displacement and compactification finite elements have " - "different " - "geometries." - ); - } - - if (compactification.GetElement().GetRangeType() != mfem::FiniteElement::SCALAR) { - throw std::invalid_argument("Compactification coordinate requires a scalar finite element."); - } - - if (compactification.GetElement().GetMapType() != mfem::FiniteElement::VALUE) { - throw std::invalid_argument( - "Compactification coordinate requires a value-mapped finite " - "element." - ); - } - - if (compactification.GetElement().GetDerivType() != mfem::FiniteElement::GRAD) { - throw std::invalid_argument( - "Compactification coordinate finite element does not provide a " - "gradient." - ); - } - - if (compactification.GetDofCount() != compactification.GetElement().GetDof()) { - throw std::invalid_argument( - "Compactification coordinate DOF count does not match its " - "finite " - "element." - ); - } - } - - MappingStatus DomainMapperStateless::EvaluateCompactificationCoordinate( - const ElementCompactificationData &compactification, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - CompactificationPointData &point_data - ) const { - const mfem::FiniteElement &element = compactification.GetElement(); - const mfem::Vector &dofs = compactification.GetDofs(); - const int dof_count = element.GetDof(); - - if (workspace.GetDimension() != m_options.dimension || transformation.GetSpaceDim() != m_options.dimension || - element.GetDim() != m_options.dimension) { - return MappingStatus::invalid_dimension; - } - - if (dofs.Size() != dof_count) { - return MappingStatus::invalid_dimension; - } - - for (int i = 0; i < dofs.Size(); ++i) { - if (!std::isfinite(dofs(i))) - return MappingStatus::non_finite_input; - } - - transformation.SetIntPoint(&integration_point); - - workspace.m_compactification_shape.SetSize(dof_count); - workspace.m_compactification_dshape.SetSize(dof_count, m_options.dimension); - - element.CalcShape(integration_point, workspace.m_compactification_shape); - element.CalcPhysDShape(transformation, workspace.m_compactification_dshape); - - point_data.coordinate = dofs * workspace.m_compactification_shape; - point_data.coordinate_gradient.SetSize(m_options.dimension); - workspace.m_compactification_dshape.MultTranspose(dofs, point_data.coordinate_gradient); - - if (!std::isfinite(point_data.coordinate)) { - return MappingStatus::non_finite_result; - } - - for (int d = 0; d < point_data.coordinate_gradient.Size(); ++d) { - if (!std::isfinite(point_data.coordinate_gradient(d))) - return MappingStatus::non_finite_result; - } - - return MappingStatus::valid; - } - - void DomainMapperStateless::EvaluateField( - const ElementDisplacementData &field, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - mfem::Vector &value, - mfem::DenseMatrix &jacobian - ) const { - transformation.SetIntPoint(&integration_point); - - const mfem::FiniteElement &element = field.GetElement(); - const mfem::DenseMatrix &dof_matrix = field.GetDofMatrix(); - - workspace.m_shape.SetSize(element.GetDof()); - workspace.m_mesh_dshape.SetSize(element.GetDof(), m_options.dimension); - - element.CalcShape(integration_point, workspace.m_shape); - element.CalcPhysDShape(transformation, workspace.m_mesh_dshape); - - value.SetSize(m_options.dimension); - dof_matrix.MultTranspose(workspace.m_shape, value); - - jacobian.SetSize(m_options.dimension, m_options.dimension); - mfem::MultAtB(dof_matrix, workspace.m_mesh_dshape, jacobian); - } - - MappingStatus DomainMapperStateless::EvaluatePoint( - const ElementMappingData &element_data, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - MappingPointContext &context - ) const { - ValidateElementData(element_data); - - if (workspace.GetDimension() != m_options.dimension) - throw std::invalid_argument("The mapping workspace has the wrong dimension."); - if (transformation.GetSpaceDim() != m_options.dimension) - throw std::invalid_argument("The element transformation has the wrong spatial dimension."); - if (transformation.GetGeometryType() != element_data.displacement.GetElement().GetGeomType()) - throw std::invalid_argument( - "The element transformation geometry does not match the " - "supplied " - "element data." - ); - - transformation.SetIntPoint(&integration_point); - - context.reference_position.SetSize(m_options.dimension); - transformation.Transform(integration_point, context.reference_position); - - EvaluateField( - element_data.displacement, transformation, integration_point, workspace, workspace.m_field_value, - workspace.m_field_jacobian - ); - - if (!vector_is_finite(context.reference_position) || !vector_is_finite(workspace.m_field_value) || - !matrix_is_finite(workspace.m_field_jacobian)) { - return MappingStatus::non_finite_input; - } - - context.displaced_position.SetSize(m_options.dimension); - context.displaced_position = context.reference_position; - context.displaced_position += workspace.m_field_value; - - context.displacement_jacobian.SetSize(m_options.dimension, m_options.dimension); - context.displacement_jacobian = workspace.m_field_jacobian; - for (int i = 0; i < m_options.dimension; ++i) - context.displacement_jacobian(i, i) += 1.0; - - context.compactified = IsCompactifiedElement(transformation); - - if (context.compactified) { - const MappingStatus coordinate_status = EvaluateCompactificationCoordinate( - element_data.compactification, transformation, integration_point, workspace, - workspace.m_compactification_point - ); - - if (coordinate_status != MappingStatus::valid) - return coordinate_status; - - const compactification::ExteriorMapInput exterior_input{ - .reference_position = context.reference_position, - .displaced_position = context.displaced_position, - .displacement_jacobian = context.displacement_jacobian, - .compactification_coordinate = workspace.m_compactification_point.coordinate, - .compactification_coordinate_gradient = workspace.m_compactification_point.coordinate_gradient - }; - - const MappingStatus exterior_status = m_exterior_map->Evaluate(exterior_input, workspace.m_exterior_result); - if (exterior_status != MappingStatus::valid) - return exterior_status; - - context.physical_position = workspace.m_exterior_result.physical_position; - context.mapping_jacobian = workspace.m_exterior_result.mapping_jacobian; - } else { - context.physical_position = context.displaced_position; - context.mapping_jacobian = context.displacement_jacobian; - } - - if (!vector_is_finite(context.physical_position) || !matrix_is_finite(context.mapping_jacobian)) - return MappingStatus::non_finite_result; - - context.mapping_determinant = context.mapping_jacobian.Det(); - if (!std::isfinite(context.mapping_determinant)) - return MappingStatus::non_finite_result; - if (context.mapping_determinant <= 0.0) - return MappingStatus::non_positive_determinant; - - context.inverse_mapping_jacobian.SetSize(m_options.dimension, m_options.dimension); - mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian); - - if (!matrix_is_finite(context.inverse_mapping_jacobian)) - return MappingStatus::non_finite_result; - - return MappingStatus::valid; - } - - MappingStatus DomainMapperStateless::EvaluateVolume( - const ElementMappingData &element_data, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - VolumeMappingContext &context - ) const { - const MappingStatus point_status = - EvaluatePoint(element_data, transformation, integration_point, workspace, context.mapping); - if (point_status != MappingStatus::valid) - return point_status; - - transformation.SetIntPoint(&integration_point); - mfem::Mult(context.mapping.mapping_jacobian, transformation.Jacobian(), workspace.m_full_element_jacobian); - - context.quadrature.J_inv.SetSize(m_options.dimension, m_options.dimension); - mfem::CalcInverse(workspace.m_full_element_jacobian, context.quadrature.J_inv); - - context.quadrature.detJ = context.mapping.mapping_determinant; - context.quadrature.weight = - integration_point.weight * transformation.Weight() * context.mapping.mapping_determinant; - - if (!matrix_is_finite(context.quadrature.J_inv) || !std::isfinite(context.quadrature.weight)) - return MappingStatus::non_finite_result; - if (context.quadrature.weight <= 0.0) - return MappingStatus::non_positive_determinant; - - return MappingStatus::valid; - } - - mfem::ElementTransformation &DomainMapperStateless::SelectFaceElementTransformation( - mfem::FaceElementTransformations &transformation, - const FaceElementSide side - ) { - if (side == FaceElementSide::element_1) { - MFEM_VERIFY(transformation.Elem1 != nullptr, "The face does not have an element-1 transformation."); - return *transformation.Elem1; - } - - MFEM_VERIFY(transformation.Elem2 != nullptr, "The face does not have an element-2 transformation."); - return *transformation.Elem2; - } - - const mfem::IntegrationPoint &DomainMapperStateless::SelectFaceElementIntegrationPoint( - mfem::FaceElementTransformations &transformation, - const FaceElementSide side - ) { - mfem::ElementTransformation &element_transformation = SelectFaceElementTransformation(transformation, side); - return element_transformation.GetIntPoint(); - } - - MappingStatus DomainMapperStateless::EvaluateFace( - const ElementMappingData &element_data, - mfem::FaceElementTransformations &transformation, - const FaceElementSide side, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - FaceMappingContext &context - ) const { - transformation.SetAllIntPoints(&integration_point); - mfem::ElementTransformation &element_transformation = SelectFaceElementTransformation(transformation, side); - const mfem::IntegrationPoint &element_integration_point = - SelectFaceElementIntegrationPoint(transformation, side); - - const MappingStatus point_status = - EvaluatePoint(element_data, element_transformation, element_integration_point, workspace, context.mapping); - if (point_status != MappingStatus::valid) - return point_status; - - workspace.m_reference_normal.SetSize(m_options.dimension); - mfem::CalcOrtho(transformation.Jacobian(), workspace.m_reference_normal); - if (side == FaceElementSide::element_2) - workspace.m_reference_normal *= -1.0; - - const double reference_normal_magnitude = workspace.m_reference_normal.Norml2(); - if (!std::isfinite(reference_normal_magnitude) || reference_normal_magnitude <= 0.0) - return MappingStatus::non_finite_result; - - context.reference_normal.SetSize(m_options.dimension); - context.reference_normal = workspace.m_reference_normal; - context.reference_normal /= reference_normal_magnitude; - - context.mapping.inverse_mapping_jacobian.MultTranspose(workspace.m_reference_normal, workspace.m_mapped_normal); - workspace.m_mapped_normal *= context.mapping.mapping_determinant; - - const double mapped_normal_magnitude = workspace.m_mapped_normal.Norml2(); - if (!std::isfinite(mapped_normal_magnitude) || mapped_normal_magnitude <= 0.0) - return MappingStatus::non_finite_result; - - context.quadrature.normal.SetSize(m_options.dimension); - context.quadrature.normal = workspace.m_mapped_normal; - context.quadrature.normal /= mapped_normal_magnitude; - - context.reference_surface_weight = integration_point.weight * reference_normal_magnitude; - context.physical_surface_weight = integration_point.weight * mapped_normal_magnitude; - - context.quadrature.ds = context.reference_surface_weight; - context.quadrature.v_dot_n_scale = mapped_normal_magnitude / reference_normal_magnitude; - - if (!vector_is_finite(context.quadrature.normal) || !std::isfinite(context.reference_surface_weight) || - !std::isfinite(context.physical_surface_weight) || !std::isfinite(context.quadrature.v_dot_n_scale)) { - return MappingStatus::non_finite_result; - } - - return MappingStatus::valid; - } - - MappingStatus DomainMapperStateless::EvaluatePointVariation( - const ElementMappingData &element_data, - const ElementDisplacementData &direction, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const MappingPointContext &base_context, - Workspace &workspace, - MappingPointVariation &variation - ) const { - ValidateElementData(element_data); - const ElementMappingData direction_data{ - .displacement = direction, .compactification = element_data.compactification - }; - ValidateElementData(direction_data); - - if (element_data.displacement.GetDofCount() != direction.GetDofCount()) - throw std::invalid_argument( - "The displacement and direction elements have different " - "degree-of-freedom counts." - ); - if (workspace.GetDimension() != m_options.dimension) - throw std::invalid_argument("The mapping workspace has the wrong dimension."); - if (base_context.compactified != IsCompactifiedElement(transformation)) - throw std::invalid_argument( - "The base mapping context does not match the current element " - "domain." - ); - - EvaluateField( - direction, transformation, integration_point, workspace, workspace.m_field_value, workspace.m_field_jacobian - ); - - if (!vector_is_finite(workspace.m_field_value) || !matrix_is_finite(workspace.m_field_jacobian)) - return MappingStatus::non_finite_input; - - variation.displacement_variation = workspace.m_field_value; - variation.displacement_jacobian_variation = workspace.m_field_jacobian; - - if (base_context.compactified) { - const MappingStatus coordinate_status = EvaluateCompactificationCoordinate( - element_data.compactification, transformation, integration_point, workspace, - workspace.m_compactification_point - ); - - if (coordinate_status != MappingStatus::valid) - return coordinate_status; - - const compactification::ExteriorMapInput exterior_input{ - .reference_position = base_context.reference_position, - .displaced_position = base_context.displaced_position, - .displacement_jacobian = base_context.displacement_jacobian, - .compactification_coordinate = workspace.m_compactification_point.coordinate, - .compactification_coordinate_gradient = workspace.m_compactification_point.coordinate_gradient - }; - - workspace.m_exterior_result.physical_position = base_context.physical_position; - workspace.m_exterior_result.mapping_jacobian = base_context.mapping_jacobian; - - const compactification::ExteriorMapDirection exterior_direction{ - .displaced_position_variation = variation.displacement_variation, - .displacement_jacobian_variation = variation.displacement_jacobian_variation - }; - - // ReSharper disable once CppTooWideScopeInitStatement - const MappingStatus exterior_status = m_exterior_map->EvaluateVariation( - exterior_input, workspace.m_exterior_result, exterior_direction, workspace.m_exterior_variation - ); - - if (exterior_status != MappingStatus::valid) { - return exterior_status; - } - - variation.physical_position_variation = workspace.m_exterior_variation.physical_position_variation; - variation.mapping_jacobian_variation = workspace.m_exterior_variation.mapping_jacobian_variation; - } else { - variation.physical_position_variation = variation.displacement_variation; - variation.mapping_jacobian_variation = variation.displacement_jacobian_variation; - } - - mfem::Mult( - base_context.inverse_mapping_jacobian, variation.mapping_jacobian_variation, workspace.m_matrix_temp_1 - ); - - double trace = 0.0; - for (int i = 0; i < m_options.dimension; ++i) - trace += workspace.m_matrix_temp_1(i, i); - variation.mapping_determinant_variation = base_context.mapping_determinant * trace; - - variation.inverse_mapping_jacobian_variation.SetSize(m_options.dimension, m_options.dimension); - mfem::Mult( - workspace.m_matrix_temp_1, base_context.inverse_mapping_jacobian, - variation.inverse_mapping_jacobian_variation - ); - variation.inverse_mapping_jacobian_variation *= -1.0; - - if (!vector_is_finite(variation.physical_position_variation) || - !matrix_is_finite(variation.mapping_jacobian_variation) || - !matrix_is_finite(variation.inverse_mapping_jacobian_variation) || - !std::isfinite(variation.mapping_determinant_variation)) { - return MappingStatus::non_finite_result; - } - - return MappingStatus::valid; - } - - MappingStatus DomainMapperStateless::EvaluateVolumeVariation( - const ElementMappingData &element_data, - const ElementDisplacementData &direction, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const VolumeMappingContext &base_context, - Workspace &workspace, - VolumeMappingVariation &variation - ) const { - const MappingStatus point_status = EvaluatePointVariation( - element_data, direction, transformation, integration_point, base_context.mapping, workspace, - variation.mapping - ); - if (point_status != MappingStatus::valid) - return point_status; - - transformation.SetIntPoint(&integration_point); - mfem::Mult( - variation.mapping.mapping_jacobian_variation, transformation.Jacobian(), workspace.m_full_element_jacobian - ); - mfem::Mult(base_context.quadrature.J_inv, workspace.m_full_element_jacobian, workspace.m_matrix_temp_1); - - variation.inverse_element_jacobian_variation.SetSize(m_options.dimension, m_options.dimension); - mfem::Mult( - workspace.m_matrix_temp_1, base_context.quadrature.J_inv, variation.inverse_element_jacobian_variation - ); - variation.inverse_element_jacobian_variation *= -1.0; - - variation.weight_variation = - integration_point.weight * transformation.Weight() * variation.mapping.mapping_determinant_variation; - - if (!matrix_is_finite(variation.inverse_element_jacobian_variation) || - !std::isfinite(variation.weight_variation)) - return MappingStatus::non_finite_result; - - return MappingStatus::valid; - } - - MappingStatus DomainMapperStateless::EvaluateFaceVariation( - const ElementMappingData &element_data, - const ElementDisplacementData &direction, - mfem::FaceElementTransformations &transformation, - const FaceElementSide side, - const mfem::IntegrationPoint &integration_point, - const FaceMappingContext &base_context, - Workspace &workspace, - FaceMappingVariation &variation - ) const { - transformation.SetAllIntPoints(&integration_point); - mfem::ElementTransformation &element_transformation = SelectFaceElementTransformation(transformation, side); - const mfem::IntegrationPoint &element_integration_point = - SelectFaceElementIntegrationPoint(transformation, side); - - const MappingStatus point_status = EvaluatePointVariation( - element_data, direction, element_transformation, element_integration_point, base_context.mapping, workspace, - variation.mapping - ); - if (point_status != MappingStatus::valid) - return point_status; - - workspace.m_reference_normal.SetSize(m_options.dimension); - mfem::CalcOrtho(transformation.Jacobian(), workspace.m_reference_normal); - if (side == FaceElementSide::element_2) - workspace.m_reference_normal *= -1.0; - - const double reference_normal_magnitude = workspace.m_reference_normal.Norml2(); - if (!std::isfinite(reference_normal_magnitude) || reference_normal_magnitude <= 0.0) - return MappingStatus::non_finite_result; - - base_context.mapping.inverse_mapping_jacobian.MultTranspose( - workspace.m_reference_normal, workspace.m_vector_temp - ); - workspace.m_mapped_normal = workspace.m_vector_temp; - workspace.m_mapped_normal *= base_context.mapping.mapping_determinant; - - variation.physical_normal_variation.SetSize(m_options.dimension); - variation.mapping.inverse_mapping_jacobian_variation.MultTranspose( - workspace.m_reference_normal, variation.physical_normal_variation - ); - variation.physical_normal_variation *= base_context.mapping.mapping_determinant; - variation.physical_normal_variation.Add( - variation.mapping.mapping_determinant_variation, workspace.m_vector_temp - ); - - const double mapped_normal_magnitude = workspace.m_mapped_normal.Norml2(); - if (!std::isfinite(mapped_normal_magnitude) || mapped_normal_magnitude <= 0.0) - return MappingStatus::non_finite_result; - - const double mapped_normal_magnitude_variation = - base_context.quadrature.normal * variation.physical_normal_variation; - - variation.physical_normal_variation.Add(-mapped_normal_magnitude_variation, base_context.quadrature.normal); - variation.physical_normal_variation /= mapped_normal_magnitude; - - variation.physical_surface_weight_variation = integration_point.weight * mapped_normal_magnitude_variation; - variation.normal_flux_scale_variation = mapped_normal_magnitude_variation / reference_normal_magnitude; - - if (!vector_is_finite(variation.physical_normal_variation) || - !std::isfinite(variation.physical_surface_weight_variation) || - !std::isfinite(variation.normal_flux_scale_variation)) { - return MappingStatus::non_finite_result; - } - - return MappingStatus::valid; - } -} // namespace mean_field::mapping \ No newline at end of file diff --git a/libmeanfield/impl/operators/contexts/barotropic_closure_linearization_context.cpp b/libmeanfield/impl/operators/contexts/barotropic_closure_linearization_context.cpp index 29d8802..1ada617 100644 --- a/libmeanfield/impl/operators/contexts/barotropic_closure_linearization_context.cpp +++ b/libmeanfield/impl/operators/contexts/barotropic_closure_linearization_context.cpp @@ -35,7 +35,7 @@ namespace { namespace mean_field::operators::context::barotropic { BarotropicClosureLinearizationContext::BarotropicClosureLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const field::FieldDofMap &densityMap, const field::FieldDofMap &enthalpyMap, const field::FieldDofMap &displacementMap diff --git a/libmeanfield/impl/operators/contexts/gravity_field_context.cpp b/libmeanfield/impl/operators/contexts/gravity_field_context.cpp index 0ec2e9d..5e29068 100644 --- a/libmeanfield/impl/operators/contexts/gravity_field_context.cpp +++ b/libmeanfield/impl/operators/contexts/gravity_field_context.cpp @@ -9,6 +9,29 @@ import :operators.context.gravity_field; namespace { using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + [[nodiscard]] std::unique_ptr make_divergence_operator(const mean_field::fem::FEM &f) { + auto divergence = + std::make_unique(f.gravityFluxFes.get(), f.gravityPotentialFes.get()); + + divergence->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL); + + auto integrator = std::make_unique(); + + const mfem::FiniteElement &trialElement = *f.gravityFluxFes->GetTypicalFE(); + const mfem::FiniteElement &testElement = *f.gravityPotentialFes->GetTypicalFE(); + const mfem::ElementTransformation &transformation = *f.mesh->GetElementTransformation(0); + + f.quadratureFactory->configure_gravity_divergence( + *integrator, mean_field::quadrature::QuadratureRole::discretization, trialElement, testElement, + transformation, mean_field::utils::DOMAINS::ALL, mean_field::quadrature::MappingKind::none + ); + + divergence->AddDomainIntegrator(integrator.release()); + divergence->Assemble(); + + return divergence; + } + void validate_displacement( const mean_field::field::FieldDofMap &displacement_map, const mfem::Vector &displacement @@ -96,7 +119,7 @@ namespace { namespace mean_field::operators::context::gravity_field { GravityFieldGeometryContext::GravityFieldGeometryContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper + const mapping::DomainMapper &domain_mapper ) : m_fem(f), m_domain_mapper(domain_mapper), @@ -170,18 +193,23 @@ namespace mean_field::operators::context::gravity_field { } if (discretization_changed) { - auto mass_operator = std::make_unique(m_fem, m_domain_mapper); - auto source_operator = std::make_unique(m_fem, m_domain_mapper); + auto mass_operator = std::make_unique(m_fem, m_domain_mapper); + auto source_operator = std::make_unique(m_fem, m_domain_mapper); + auto divergence_operator = make_divergence_operator(m_fem); + auto transpose_divergence_operator = std::make_unique(divergence_operator.get()); mass_operator->Prepare(displacement); source_operator->Prepare(displacement); - m_mass_operator = std::move(mass_operator); - m_source_operator = std::move(source_operator); + m_mass_operator = std::move(mass_operator); + m_source_operator = std::move(source_operator); + m_divergence_operator = std::move(divergence_operator); + m_transpose_divergence_operator = std::move(transpose_divergence_operator); - preparation.reconstructed_operators = true; - preparation.rebuilt_mass_operator = true; - preparation.rebuilt_source_operator = true; + preparation.reconstructed_operators = true; + preparation.rebuilt_mass_operator = true; + preparation.rebuilt_source_operator = true; + preparation.rebuilt_divergence_operator = true; } else { MFEM_VERIFY( m_mass_operator != nullptr, "GravityFieldGeometryContext has " @@ -231,6 +259,23 @@ namespace mean_field::operators::context::gravity_field { return *m_source_operator; } + const mfem::Operator &GravityFieldGeometryContext::GetDivergenceOperator() const { + MFEM_VERIFY(m_is_prepared, "GravityFieldGeometryContext must be prepared before accessing divergence."); + MFEM_VERIFY(m_divergence_operator != nullptr, "GravityFieldGeometryContext has no divergence operator."); + return *m_divergence_operator; + } + + const mfem::Operator &GravityFieldGeometryContext::GetTransposeDivergenceOperator() const { + MFEM_VERIFY( + m_is_prepared, "GravityFieldGeometryContext must be prepared before accessing transpose divergence." + ); + MFEM_VERIFY( + m_transpose_divergence_operator != nullptr, + "GravityFieldGeometryContext has no transpose-divergence operator." + ); + return *m_transpose_divergence_operator; + } + const mfem::Vector &GravityFieldGeometryContext::GetDisplacementTrue() const { MFEM_VERIFY( m_is_prepared, "GravityFieldGeometryContext must be prepared before " @@ -257,7 +302,7 @@ namespace mean_field::operators::context::gravity_field { GravityFieldLinearizationContext::GravityFieldLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper + const mapping::DomainMapper &domain_mapper ) : m_fem(f), m_geometry_context( diff --git a/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp b/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp index 0ebca61..4a6b429 100644 --- a/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp +++ b/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp @@ -75,7 +75,7 @@ namespace { namespace mean_field::operators::context::hydrostatic { HydrostaticEquilibriumContext::HydrostaticEquilibriumContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper + const mapping::DomainMapper &domainMapper ) : m_f(f), m_domainMapper(domainMapper), diff --git a/libmeanfield/impl/operators/contexts/pressure_force_context.cpp b/libmeanfield/impl/operators/contexts/pressure_force_context.cpp index f5f1212..f76bd67 100644 --- a/libmeanfield/impl/operators/contexts/pressure_force_context.cpp +++ b/libmeanfield/impl/operators/contexts/pressure_force_context.cpp @@ -37,7 +37,7 @@ namespace { namespace mean_field::operators::context::pressure_force { PressureForceLinearizationContext::PressureForceLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const field::FieldDofMap &enthalpyMap, const field::FieldDofMap &displacementMap ) diff --git a/libmeanfield/impl/operators/contexts/rotation_displacement_force_context.cpp b/libmeanfield/impl/operators/contexts/rotation_displacement_force_context.cpp index 47a3e86..0363e2f 100644 --- a/libmeanfield/impl/operators/contexts/rotation_displacement_force_context.cpp +++ b/libmeanfield/impl/operators/contexts/rotation_displacement_force_context.cpp @@ -33,7 +33,7 @@ namespace { namespace mean_field::operators::context::rotational_displacement_force { RotationalDisplacementForceLinearizationContext::RotationalDisplacementForceLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper + const mapping::DomainMapper &domainMapper ) : m_f(f), m_densityMap( diff --git a/libmeanfield/impl/operators/gravity_field.cpp b/libmeanfield/impl/operators/gravity_field.cpp index e2b7141..9ce066d 100644 --- a/libmeanfield/impl/operators/gravity_field.cpp +++ b/libmeanfield/impl/operators/gravity_field.cpp @@ -11,596 +11,750 @@ import :solver.fields; import :operators.kernels.gravity_field; namespace { - using namespace mean_field; - int get_state_width(const mfem::Array &state_offsets) { - MFEM_VERIFY(state_offsets.Size() >= 2, "The coupled state requires at least one block."); - MFEM_VERIFY(state_offsets[0] == 0, "The coupled state offsets must begin at zero."); +using namespace mean_field; +using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; +int get_state_width(const mfem::Array &state_offsets) { + MFEM_VERIFY(state_offsets.Size() >= 2, + "The coupled state requires at least one block."); + MFEM_VERIFY(state_offsets[0] == 0, + "The coupled state offsets must begin at zero."); - for (int i = 0; i < state_offsets.Size() - 1; ++i) { - MFEM_VERIFY(state_offsets[i + 1] >= state_offsets[i], "The coupled state offsets must be nondecreasing."); - } + for (int i = 0; i < state_offsets.Size() - 1; ++i) { + MFEM_VERIFY(state_offsets[i + 1] >= state_offsets[i], + "The coupled state offsets must be nondecreasing."); + } - MFEM_VERIFY(state_offsets.Last() > 0, "The coupled state cannot be empty."); - return state_offsets.Last(); - } + MFEM_VERIFY(state_offsets.Last() > 0, "The coupled state cannot be empty."); + return state_offsets.Last(); +} - int get_gravity_residual_height(const fem::FEM &f) { - MFEM_VERIFY( - f.gravityFluxFes != nullptr, "GravityFieldOperator requires the gravity-gradient finite-element " - "space (RT: Raviart-Thomas)." - ); - MFEM_VERIFY( - f.gravityPotentialFes != nullptr, "GravityFieldOperator requires the gravity-potential " - "finite-element " - "space (L2: Lebesgue " - "space of square-integrable functions)." - ); - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - return field::make_field_dof_map(*f.gravityFluxFes).reduced_size() + - field::make_field_dof_map(*f.gravityPotentialFes).reduced_size(); - } +int get_gravity_residual_height(const fem::FEM &f) { + MFEM_VERIFY( + f.gravityFluxFes != nullptr, + "GravityFieldOperator requires the gravity-gradient finite-element " + "space (RT: Raviart-Thomas)."); + MFEM_VERIFY(f.gravityPotentialFes != nullptr, + "GravityFieldOperator requires the gravity-potential " + "finite-element " + "space (L2: Lebesgue " + "space of square-integrable functions)."); + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + return field::make_field_dof_map( + *f.gravityFluxFes) + .reduced_size() + + field::make_field_dof_map( + *f.gravityPotentialFes) + .reduced_size(); +} - mfem::Array make_gravity_residual_offsets(const fem::FEM &f) { - mfem::Array offsets(utils::blocks::gravity_field_form::residual_block_count + 1); - offsets[0] = 0; - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - offsets[1] = field::make_field_dof_map(*f.gravityFluxFes).reduced_size(); - offsets[2] = - offsets[1] + field::make_field_dof_map(*f.gravityPotentialFes).reduced_size(); - return offsets; - } +mfem::Array make_gravity_residual_offsets(const fem::FEM &f) { + mfem::Array offsets( + utils::blocks::gravity_field_form::residual_block_count + 1); + offsets[0] = 0; + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + offsets[1] = + field::make_field_dof_map(*f.gravityFluxFes) + .reduced_size(); + offsets[2] = + offsets[1] + field::make_field_dof_map( + *f.gravityPotentialFes) + .reduced_size(); + return offsets; +} - template - int get_state_block_size( - const mfem::Array &state_offsets, - const utils::blocks::value_block - ) { - MFEM_VERIFY(index + 1 < state_offsets.Size(), "Value block is not present in the state offsets."); - return state_offsets[index + 1] - state_offsets[index]; - } +template +int get_state_block_size(const mfem::Array &state_offsets, + const utils::blocks::value_block) { + MFEM_VERIFY(index + 1 < state_offsets.Size(), + "Value block is not present in the state offsets."); + return state_offsets[index + 1] - state_offsets[index]; +} - void validate_state_offsets( - const fem::FEM &f, - const mfem::Array &state_offsets - ) { - MFEM_VERIFY(f.densityFes != nullptr, "GravityFieldOperator requires the density finite-element space."); - MFEM_VERIFY( - f.displacementFes != nullptr, "GravityFieldOperator requires the " - "displacement finite-element space." - ); +void validate_state_offsets(const fem::FEM &f, + const mfem::Array &state_offsets) { + MFEM_VERIFY( + f.densityFes != nullptr, + "GravityFieldOperator requires the density finite-element space."); + MFEM_VERIFY(f.displacementFes != nullptr, + "GravityFieldOperator requires the " + "displacement finite-element space."); - using form = utils::blocks::gravity_field_form; + using form = utils::blocks::gravity_field_form; - constexpr auto density_block = utils::blocks::get_value_block
(utils::blocks::density_field.mass_term); - constexpr auto displacement_block = - utils::blocks::get_value_block(utils::blocks::displacement_field.geometry_term); - constexpr auto gravity_gradient_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravity_potential_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.poisson_term); + constexpr auto density_block = utils::blocks::get_value_block( + utils::blocks::density_field.mass_term); + constexpr auto displacement_block = utils::blocks::get_value_block( + utils::blocks::displacement_field.geometry_term); + constexpr auto gravity_gradient_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravity_potential_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.poisson_term); - MFEM_VERIFY( - state_offsets.Size() == form::value_block_count + 1, - "The gravity state offsets do not match gravity_field_form." - ); - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - const auto density_map = field::make_field_dof_map(*f.densityFes); - const auto displacement_map = field::make_field_dof_map(*f.displacementFes); - const auto flux_map = field::make_field_dof_map(*f.gravityFluxFes); - const auto potential_map = field::make_field_dof_map(*f.gravityPotentialFes); + MFEM_VERIFY(state_offsets.Size() == form::value_block_count + 1, + "The gravity state offsets do not match gravity_field_form."); + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + const auto density_map = + field::make_field_dof_map(*f.densityFes); + const auto displacement_map = + field::make_field_dof_map( + *f.displacementFes); + const auto flux_map = field::make_field_dof_map( + *f.gravityFluxFes); + const auto potential_map = + field::make_field_dof_map( + *f.gravityPotentialFes); - MFEM_VERIFY( - get_state_block_size(state_offsets, density_block) == density_map.reduced_size(), - "The density block does not match the density finite-element space." - ); - MFEM_VERIFY( - get_state_block_size(state_offsets, displacement_block) == displacement_map.reduced_size(), - "The displacement block does not match the displacement " - "finite-element " - "space." - ); - MFEM_VERIFY( - get_state_block_size(state_offsets, gravity_gradient_block) == flux_map.reduced_size(), - "The gravity-gradient block does not match the RT finite-element " - "space." - ); - MFEM_VERIFY( - get_state_block_size(state_offsets, gravity_potential_block) == potential_map.reduced_size(), - "The gravity-potential block does not match the potential " - "finite-element space." - ); - } + MFEM_VERIFY( + get_state_block_size(state_offsets, density_block) == + density_map.reduced_size(), + "The density block does not match the density finite-element space."); + MFEM_VERIFY(get_state_block_size(state_offsets, displacement_block) == + displacement_map.reduced_size(), + "The displacement block does not match the displacement " + "finite-element " + "space."); + MFEM_VERIFY(get_state_block_size(state_offsets, gravity_gradient_block) == + flux_map.reduced_size(), + "The gravity-gradient block does not match the RT finite-element " + "space."); + MFEM_VERIFY(get_state_block_size(state_offsets, gravity_potential_block) == + potential_map.reduced_size(), + "The gravity-potential block does not match the potential " + "finite-element space."); +} - void validate_gravity_context(const fem::FEM &f) { - MFEM_VERIFY(f.gravityContext.b_form != nullptr, "GravityFieldOperator requires the divergence operator."); - MFEM_VERIFY(f.gravityContext.BT != nullptr, "GravityFieldOperator requires the transpose divergence operator."); - MFEM_VERIFY(f.quadratureFactory != nullptr, "GravityFieldOperator requires the quadrature-rule factory."); - } +void validate_gravity_context(const fem::FEM &f) { + MFEM_VERIFY(f.quadratureFactory != nullptr, + "GravityFieldOperator requires the quadrature-rule factory."); +} - template - mfem::Vector make_read_only_value_view( - const mfem::Vector &vector, - const mfem::Array &offsets, - const utils::blocks::value_block - ) { - MFEM_VERIFY(index + 1 < offsets.Size(), "Value block is not present in the supplied offset array."); +[[nodiscard]] std::unique_ptr +make_gravity_schur_preconditioner(const fem::FEM &f, + const mfem::Vector &mass_diagonal) { + MFEM_VERIFY(mass_diagonal.Size() == f.gravityFluxFes->GetTrueVSize(), + "The gravity Schur preconditioner requires a full " + "gravity-gradient mass diagonal."); - const int begin = offsets[index]; - const int size = offsets[index + 1] - begin; + mfem::Vector inverse_mass_diagonal(mass_diagonal); - MFEM_VERIFY(vector.Size() == offsets.Last(), "Vector size does not match the value-block offsets."); - return mfem::Vector(const_cast(vector.GetData()) + begin, size); - } + for (int i = 0; i < inverse_mass_diagonal.Size(); ++i) { + MFEM_VERIFY(std::isfinite(inverse_mass_diagonal(i)) && + inverse_mass_diagonal(i) > 0.0, + "The gravity Schur preconditioner encountered a non-positive " + "or non-finite mass diagonal."); + inverse_mass_diagonal(i) = 1.0 / inverse_mass_diagonal(i); + } - template - mfem::Vector make_read_only_residual_view( - const mfem::Vector &vector, - const mfem::Array &offsets, - const utils::blocks::residual_block block - ) { - const int block_id = block; - const int begin = offsets[block_id]; - const int size = offsets[block_id + 1] - begin; + mfem::ParMixedBilinearForm divergence(f.gravityFluxFes.get(), + f.gravityPotentialFes.get()); + auto integrator = std::make_unique(); - MFEM_VERIFY(vector.Size() == offsets.Last(), "The vector does not match the residual-block layout."); + const mfem::FiniteElement &trial_element = *f.gravityFluxFes->GetTypicalFE(); + const mfem::FiniteElement &test_element = + *f.gravityPotentialFes->GetTypicalFE(); + const mfem::ElementTransformation &transformation = + *f.mesh->GetElementTransformation(0); - mfem::Vector view; - view.MakeRef(const_cast(vector), begin, size); - return view; - } + f.quadratureFactory->configure_gravity_divergence( + *integrator, quadrature::QuadratureRole::preconditioner, trial_element, + test_element, transformation, utils::DOMAINS::ALL, + quadrature::MappingKind::none); - template - mfem::Vector make_residual_view( - mfem::Vector &vector, - const mfem::Array &offsets, - const utils::blocks::residual_block - ) { - MFEM_VERIFY(index + 1 < offsets.Size(), "Residual block is not present in the supplied offset array."); + divergence.AddDomainIntegrator(integrator.release()); + divergence.Assemble(); + divergence.Finalize(); - const int begin = offsets[index]; - const int size = offsets[index + 1] - begin; + std::unique_ptr divergence_matrix( + divergence.ParallelAssemble()); + std::unique_ptr inverse_mass_divergence_transpose( + divergence_matrix->Transpose()); + inverse_mass_divergence_transpose->ScaleRows(inverse_mass_diagonal); - MFEM_VERIFY(vector.Size() == offsets.Last(), "Vector size does not match the residual-block offsets."); - return mfem::Vector(vector.GetData() + begin, size); - } + return std::unique_ptr(mfem::ParMult( + divergence_matrix.get(), inverse_mass_divergence_transpose.get())); +} + +template +mfem::Vector +make_read_only_value_view(const mfem::Vector &vector, + const mfem::Array &offsets, + const utils::blocks::value_block) { + MFEM_VERIFY(index + 1 < offsets.Size(), + "Value block is not present in the supplied offset array."); + + const int begin = offsets[index]; + const int size = offsets[index + 1] - begin; + + MFEM_VERIFY(vector.Size() == offsets.Last(), + "Vector size does not match the value-block offsets."); + return mfem::Vector(const_cast(vector.GetData()) + begin, + size); +} + +template +mfem::Vector +make_read_only_residual_view(const mfem::Vector &vector, + const mfem::Array &offsets, + const utils::blocks::residual_block block) { + const int block_id = block; + const int begin = offsets[block_id]; + const int size = offsets[block_id + 1] - begin; + + MFEM_VERIFY(vector.Size() == offsets.Last(), + "The vector does not match the residual-block layout."); + + mfem::Vector view; + view.MakeRef(const_cast(vector), begin, size); + return view; +} + +template +mfem::Vector make_residual_view(mfem::Vector &vector, + const mfem::Array &offsets, + const utils::blocks::residual_block) { + MFEM_VERIFY(index + 1 < offsets.Size(), + "Residual block is not present in the supplied offset array."); + + const int begin = offsets[index]; + const int size = offsets[index + 1] - begin; + + MFEM_VERIFY(vector.Size() == offsets.Last(), + "Vector size does not match the residual-block offsets."); + return mfem::Vector(vector.GetData() + begin, size); +} } // namespace namespace mean_field::operators { - GravityFieldOperator::GravityFieldOperator( - fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - context::gravity_field::GravityFieldLinearizationContext &linearization_context, - const mfem::Array &state_offsets, - GravityFieldJacobianOperator &jacobian - ) - : Operator( - get_gravity_residual_height(f), - get_state_width(state_offsets) - ), - m_fem(f), - m_domain_mapper(domain_mapper), - m_linearization_context(linearization_context), - m_state_offsets(state_offsets), - m_residual_offsets(make_gravity_residual_offsets(f)), - m_jacobian(jacobian) { - MFEM_VERIFY(f.mesh != nullptr, "GravityFieldOperator requires a mesh."); - MFEM_VERIFY( - f.displacementFes != nullptr, "GravityFieldOperator requires the " - "displacement finite-element space." - ); - MFEM_VERIFY( - f.smesh.exterior_coordinate != nullptr, "GravityFieldOperator requires the STROID exterior coordinate." - ); - MFEM_VERIFY( - f.smesh.exterior_coordinate->space != nullptr, "GravityFieldOperator requires the exterior-coordinate " - "finite-element " - "space." - ); - MFEM_VERIFY( - f.smesh.exterior_coordinate->values != nullptr, - "GravityFieldOperator requires the exterior-coordinate values." - ); - MFEM_VERIFY( - domain_mapper.GetDimension() == f.mesh->Dimension(), - "GravityFieldOperator received a domain mapper with the wrong " - "dimension." - ); +GravityFieldOperator::GravityFieldOperator( + fem::FEM &f, const mapping::DomainMapper &domain_mapper, + context::gravity_field::GravityFieldLinearizationContext + &linearization_context, + const mfem::Array &state_offsets, + GravityFieldJacobianOperator &jacobian) + : Operator(get_gravity_residual_height(f), get_state_width(state_offsets)), + m_fem(f), m_domain_mapper(domain_mapper), + m_linearization_context(linearization_context), + m_state_offsets(state_offsets), + m_residual_offsets(make_gravity_residual_offsets(f)), + m_jacobian(jacobian) { + MFEM_VERIFY(f.mesh != nullptr, "GravityFieldOperator requires a mesh."); + MFEM_VERIFY(f.displacementFes != nullptr, + "GravityFieldOperator requires the " + "displacement finite-element space."); + MFEM_VERIFY(f.smesh.exterior_coordinate != nullptr, + "GravityFieldOperator requires the STROID exterior coordinate."); + MFEM_VERIFY(f.smesh.exterior_coordinate->space != nullptr, + "GravityFieldOperator requires the exterior-coordinate " + "finite-element " + "space."); + MFEM_VERIFY(f.smesh.exterior_coordinate->values != nullptr, + "GravityFieldOperator requires the exterior-coordinate values."); + MFEM_VERIFY(domain_mapper.GetDimension() == f.mesh->Dimension(), + "GravityFieldOperator received a domain mapper with the wrong " + "dimension."); - validate_state_offsets(f, m_state_offsets); - validate_gravity_context(f); + validate_state_offsets(f, m_state_offsets); + validate_gravity_context(f); - bool has_vacuum_domain = false; + bool has_vacuum_domain = false; - for (int i = 0; i < f.mesh->attributes.Size(); ++i) { - if (f.mesh->attributes[i] == domain_mapper.GetVacuumElementAttribute()) { - has_vacuum_domain = true; - break; - } - } - - MFEM_VERIFY(has_vacuum_domain, "GravityFieldOperator requires a compactified vacuum domain."); - MFEM_VERIFY( - m_residual_offsets.Last() == Height(), "The gravity residual offsets do not match the operator height." - ); - MFEM_VERIFY(m_state_offsets.Last() == Width(), "The coupled state offsets do not match the operator width."); - } - - context::gravity_field::GravityFieldPreparationReport GravityFieldOperator::Prepare( - const mfem::Vector &state, - const context::gravity_field::GravityFieldRevisions &revisions - ) { - using form = utils::blocks::gravity_field_form; - - constexpr auto density_block = utils::blocks::get_value_block(utils::blocks::density_field.mass_term); - constexpr auto displacement_block = - utils::blocks::get_value_block(utils::blocks::displacement_field.geometry_term); - constexpr auto gravity_gradient_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravity_potential_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.poisson_term); - - MFEM_VERIFY( - state.Size() == Width(), "GravityFieldOperator received a " - "preparation state with the wrong size." - ); - - const mfem::Vector density = make_read_only_value_view(state, m_state_offsets, density_block); - const mfem::Vector displacement = make_read_only_value_view(state, m_state_offsets, displacement_block); - const mfem::Vector gravity_gradient = make_read_only_value_view(state, m_state_offsets, gravity_gradient_block); - const mfem::Vector gravity_potential = - make_read_only_value_view(state, m_state_offsets, gravity_potential_block); - - return m_linearization_context.Prepare( - {.density = density, - .displacement = displacement, - .gravity_gradient = gravity_gradient, - .gravity_potential = gravity_potential}, - revisions - ); - } - - const mfem::Array &GravityFieldOperator::GetStateOffsets() const noexcept { - return m_state_offsets; - } - - const mfem::Array &GravityFieldOperator::GetResidualOffsets() const noexcept { - return m_residual_offsets; - } - - void GravityFieldOperator::ApplyGravityUnknowns( - const mfem::Vector &gravity_gradient, - const mfem::Vector &gravity_potential, - const context::gravity_field::GravityFieldGeometryContext &geometry_context, - mfem::Vector &action - ) const { - using form = utils::blocks::gravity_field_form; - - constexpr auto gravity_gradient_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravity_poisson_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - - MFEM_VERIFY(geometry_context.IsPrepared(), "GravityFieldOperator received an unprepared geometry context."); - MFEM_VERIFY( - gravity_gradient.Size() == geometry_context.GetMassOperator().GetFluxMap().reduced_size(), - "GravityFieldOperator received a gravity-gradient vector with the " - "wrong size." - ); - MFEM_VERIFY( - gravity_potential.Size() == geometry_context.GetSourceOperator().GetPotentialMap().reduced_size(), - "GravityFieldOperator received a gravity-potential vector with the " - "wrong size." - ); - - action.SetSize(Height()); - action = 0.0; - - mfem::Vector gravity_gradient_action = - make_residual_view(action, m_residual_offsets, gravity_gradient_residual_block); - mfem::Vector gravity_poisson_action = - make_residual_view(action, m_residual_offsets, gravity_poisson_residual_block); - const field::FieldDofMap &flux_map = geometry_context.GetMassOperator().GetFluxMap(); - const field::FieldDofMap &potential_map = geometry_context.GetSourceOperator().GetPotentialMap(); - mfem::Vector potential_true(potential_map.full_size()); - mfem::Vector transpose_divergence_action_true(flux_map.full_size()); - mfem::Vector transpose_divergence_action(flux_map.reduced_size()); - mfem::Vector gradient_true(flux_map.full_size()); - mfem::Vector divergence_action_true(potential_map.full_size()); - - geometry_context.GetMassOperator().Mult(gravity_gradient, gravity_gradient_action); - potential_map.scatter(gravity_potential, potential_true); - m_fem.gravityContext.BT->Mult(potential_true, transpose_divergence_action_true); - flux_map.gather(transpose_divergence_action_true, transpose_divergence_action); - gravity_gradient_action += transpose_divergence_action; - flux_map.scatter(gravity_gradient, gradient_true); - m_fem.gravityContext.b_form->Mult(gradient_true, divergence_action_true); - potential_map.gather(divergence_action_true, gravity_poisson_action); - } - - void GravityFieldOperator::ApplyDensitySource( - const mfem::Vector &density, - const context::gravity_field::GravityFieldGeometryContext &geometry_context, - mfem::Vector &action - ) const { - using form = utils::blocks::gravity_field_form; - - constexpr auto gravity_poisson_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - - MFEM_VERIFY(geometry_context.IsPrepared(), "GravityFieldOperator received an unprepared geometry context."); - MFEM_VERIFY( - density.Size() == geometry_context.GetSourceOperator().GetDensityMap().reduced_size(), - "GravityFieldOperator received a density vector with the wrong " - "size." - ); - - action.SetSize(Height()); - action = 0.0; - - mfem::Vector gravity_poisson_action = - make_residual_view(action, m_residual_offsets, gravity_poisson_residual_block); - geometry_context.GetSourceOperator().Mult(density, gravity_poisson_action); - } - - void GravityFieldOperator::Mult( - const mfem::Vector &state, - mfem::Vector &residual - ) const { - MEAN_FIELD_PROFILE_SCOPE("GravityFieldOperator::Mult"); - - using form = utils::blocks::gravity_field_form; - - constexpr auto density_block = utils::blocks::get_value_block(utils::blocks::density_field.mass_term); - constexpr auto gravity_gradient_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravity_potential_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.poisson_term); - - MFEM_VERIFY(state.Size() == Width(), "GravityFieldOperator received a state with the wrong size."); - MFEM_VERIFY( - m_linearization_context.IsPrepared(), "GravityFieldOperator must be prepared before Mult is called." - ); - - const mfem::Vector density = make_read_only_value_view(state, m_state_offsets, density_block); - const mfem::Vector gravity_gradient = make_read_only_value_view(state, m_state_offsets, gravity_gradient_block); - const mfem::Vector gravity_potential = - make_read_only_value_view(state, m_state_offsets, gravity_potential_block); - const context::gravity_field::GravityFieldGeometryContext &geometry_context = - m_linearization_context.GetGeometryContext(); - - mfem::Vector source; - - ApplyGravityUnknowns(gravity_gradient, gravity_potential, geometry_context, residual); - ApplyDensitySource(density, geometry_context, source); - - residual -= source; - } - - context::gravity_field::GravityFieldLinearizationContext &GravityFieldOperator::GetLinearizationContext() noexcept { - return m_linearization_context; - } - - const context::gravity_field::GravityFieldLinearizationContext & - GravityFieldOperator::GetLinearizationContext() const noexcept { - return m_linearization_context; - } - - mfem::Operator &GravityFieldOperator::GetGradient(const mfem::Vector &state) const { - MFEM_VERIFY( - state.Size() == Width(), "GravityFieldOperator received a " - "linearization state with the wrong size." - ); - MFEM_VERIFY( - m_linearization_context.IsPrepared(), "GravityFieldOperator must be prepared before GetGradient is " - "called." - ); - - return m_jacobian; - } - ReducedGravityFieldOperator::ReducedGravityFieldOperator( - GravityFieldOperator &gravity_field_operator, - context::gravity_field::GravityFieldGeometryContext &gravity_field_geometry_context, - const mfem::Vector &displacement - ) - : Operator( - gravity_field_operator.Height(), - gravity_field_operator.Height() - ), - m_gravity_field_operator(gravity_field_operator), - m_gravity_offsets(gravity_field_operator.GetResidualOffsets()), - m_gravity_field_geometry_context(gravity_field_geometry_context) { - using form = utils::blocks::gravity_field_form; - - constexpr auto gravity_gradient_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravity_potential_block = - utils::blocks::get_value_block(utils::blocks::gravity_field.poisson_term); - constexpr auto gravity_gradient_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravity_poisson_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - - const mfem::Array &state_offsets = m_gravity_field_operator.GetStateOffsets(); - - MFEM_VERIFY( - state_offsets.Size() == form::value_block_count + 1, - "ReducedGravityFieldOperator received an invalid coupled-state layout." - ); - MFEM_VERIFY( - m_gravity_offsets.Size() == form::residual_block_count + 1, - "ReducedGravityFieldOperator received an invalid gravity-residual " - "layout." - ); - MFEM_VERIFY(state_offsets[0] == 0, "The coupled-state offsets must begin at zero."); - MFEM_VERIFY(m_gravity_offsets[0] == 0, "The reduced gravity offsets must begin at zero."); - MFEM_VERIFY( - state_offsets.Last() == m_gravity_field_operator.Width(), - "The coupled-state offsets do not match the gravity-field operator " - "width." - ); - MFEM_VERIFY( - m_gravity_offsets.Last() == m_gravity_field_operator.Height(), - "The reduced gravity offsets do not match the gravity-field " - "operator " - "height." - ); - MFEM_VERIFY(Width() == Height(), "ReducedGravityFieldOperator must be square."); - - const int state_gradient_size = - state_offsets[static_cast(gravity_gradient_block) + 1] - state_offsets[gravity_gradient_block]; - const int state_potential_size = - state_offsets[static_cast(gravity_potential_block) + 1] - state_offsets[gravity_potential_block]; - const int gravity_gradient_size = m_gravity_offsets[static_cast(gravity_gradient_residual_block) + 1] - - m_gravity_offsets[gravity_gradient_residual_block]; - const int gravity_potential_size = m_gravity_offsets[static_cast(gravity_poisson_residual_block) + 1] - - m_gravity_offsets[gravity_poisson_residual_block]; - - MFEM_VERIFY( - state_gradient_size == gravity_gradient_size, "The gravity-gradient block does not match the coupled-state " - "gravity-gradient block." - ); - MFEM_VERIFY( - state_potential_size == gravity_potential_size, "The gravity-potential block does not match the Poisson " - "residual block." - ); - - SetDisplacement(displacement); - } - - void ReducedGravityFieldOperator::SetDisplacement(const mfem::Vector &displacement) { - ValidateDisplacement(displacement); - - context::gravity_field::DiscretizationRevision discretization_revision; - context::gravity_field::DisplacementRevision displacement_revision; - - if (m_gravity_field_geometry_context.IsPrepared()) { - discretization_revision = m_gravity_field_geometry_context.GetDiscretizationRevision(); - displacement_revision = m_gravity_field_geometry_context.GetDisplacementRevision(); - - MFEM_VERIFY( - displacement_revision.value < std::numeric_limits::max(), - "The reduced gravity displacement revision has overflowed." - ); - ++displacement_revision.value; - } - - m_gravity_field_geometry_context.Prepare(displacement, discretization_revision, displacement_revision); - m_displacement = displacement; - } - - const mfem::Vector &ReducedGravityFieldOperator::GetDisplacement() const { - return m_displacement; - } - - void ReducedGravityFieldOperator::BuildRightHandSide( - const mfem::Vector &density, - mfem::Vector &right_hand_side - ) const { - ValidateDensity(density); - - m_gravity_field_operator.ApplyDensitySource(density, m_gravity_field_geometry_context, right_hand_side); - - MFEM_VERIFY( - right_hand_side.Size() == Height(), "ReducedGravityFieldOperator produced a right-hand side with the " - "wrong " - "size." - ); - } - - void ReducedGravityFieldOperator::Mult( - const mfem::Vector &gravity_state, - mfem::Vector &action - ) const { - MEAN_FIELD_PROFILE_SCOPE("ReducedGravityFieldOperator::Mult"); - - using form = utils::blocks::gravity_field_form; - - constexpr auto gravity_gradient_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravity_poisson_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - - ValidateGravityState(gravity_state); - - const mfem::Vector gravity_gradient = - make_read_only_residual_view(gravity_state, m_gravity_offsets, gravity_gradient_residual_block); - const mfem::Vector gravity_potential = - make_read_only_residual_view(gravity_state, m_gravity_offsets, gravity_poisson_residual_block); - - m_gravity_field_operator.ApplyGravityUnknowns( - gravity_gradient, gravity_potential, m_gravity_field_geometry_context, action - ); - - MFEM_VERIFY( - action.Size() == Height(), "ReducedGravityFieldOperator produced " - "an action with the wrong size." - ); - } - - GravityFieldOperator &ReducedGravityFieldOperator::GetGravityFieldOperator() noexcept { - return m_gravity_field_operator; - } - - const GravityFieldOperator &ReducedGravityFieldOperator::GetGravityFieldOperator() const noexcept { - return m_gravity_field_operator; - } - - context::gravity_field::GravityFieldGeometryContext &ReducedGravityFieldOperator::GetGeometryContext() noexcept { - return m_gravity_field_geometry_context; - } - - const context::gravity_field::GravityFieldGeometryContext & - ReducedGravityFieldOperator::GetGeometryContext() const noexcept { - return m_gravity_field_geometry_context; - } - - const mfem::Array &ReducedGravityFieldOperator::GetGravityOffsets() const noexcept { - return m_gravity_offsets; - } - - void ReducedGravityFieldOperator::ValidateDisplacement(const mfem::Vector &displacement) const { - using form = utils::blocks::gravity_field_form; - - constexpr auto displacement_block = - utils::blocks::get_value_block(utils::blocks::displacement_field.geometry_term); - - const mfem::Array &state_offsets = m_gravity_field_operator.GetStateOffsets(); - const int expected_size = - state_offsets[static_cast(displacement_block) + 1] - state_offsets[displacement_block]; - - MFEM_VERIFY( - displacement.Size() == expected_size, "ReducedGravityFieldOperator received a displacement with the " - "wrong " - "size." - ); - - for (int i = 0; i < displacement.Size(); ++i) { - MFEM_VERIFY( - std::isfinite(displacement(i)), "ReducedGravityFieldOperator received a non-finite " - "displacement " - "value." - ); - } - } - - void ReducedGravityFieldOperator::ValidateDensity(const mfem::Vector &density) const { - using form = utils::blocks::gravity_field_form; - - constexpr auto density_block = utils::blocks::get_value_block(utils::blocks::density_field.mass_term); - - const mfem::Array &state_offsets = m_gravity_field_operator.GetStateOffsets(); - const int expected_size = state_offsets[static_cast(density_block) + 1] - state_offsets[density_block]; - - MFEM_VERIFY( - density.Size() == expected_size, "ReducedGravityFieldOperator received a density with the wrong " - "size." - ); - } - - void ReducedGravityFieldOperator::ValidateGravityState(const mfem::Vector &gravity_state) const { - MFEM_VERIFY( - gravity_state.Size() == Width(), "ReducedGravityFieldOperator received " - "a gravity state with the wrong size." - ); + for (int i = 0; i < f.mesh->attributes.Size(); ++i) { + if (DomainSchema::template attribute_belongs_to( + f.mesh->attributes[i])) { + has_vacuum_domain = true; + break; } + } + + MFEM_VERIFY(has_vacuum_domain, + "GravityFieldOperator requires a compactified vacuum domain."); + MFEM_VERIFY(m_residual_offsets.Last() == Height(), + "The gravity residual offsets do not match the operator height."); + MFEM_VERIFY(m_state_offsets.Last() == Width(), + "The coupled state offsets do not match the operator width."); +} + +context::gravity_field::GravityFieldPreparationReport +GravityFieldOperator::Prepare( + const mfem::Vector &state, + const context::gravity_field::GravityFieldRevisions &revisions) { + using form = utils::blocks::gravity_field_form; + + constexpr auto density_block = utils::blocks::get_value_block( + utils::blocks::density_field.mass_term); + constexpr auto displacement_block = utils::blocks::get_value_block( + utils::blocks::displacement_field.geometry_term); + constexpr auto gravity_gradient_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravity_potential_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.poisson_term); + + MFEM_VERIFY(state.Size() == Width(), + "GravityFieldOperator received a " + "preparation state with the wrong size."); + + const mfem::Vector density = + make_read_only_value_view(state, m_state_offsets, density_block); + const mfem::Vector displacement = + make_read_only_value_view(state, m_state_offsets, displacement_block); + const mfem::Vector gravity_gradient = + make_read_only_value_view(state, m_state_offsets, gravity_gradient_block); + const mfem::Vector gravity_potential = make_read_only_value_view( + state, m_state_offsets, gravity_potential_block); + + return m_linearization_context.Prepare( + {.density = density, + .displacement = displacement, + .gravity_gradient = gravity_gradient, + .gravity_potential = gravity_potential}, + revisions); +} + +const mfem::Array &GravityFieldOperator::GetStateOffsets() const noexcept { + return m_state_offsets; +} + +const mfem::Array & +GravityFieldOperator::GetResidualOffsets() const noexcept { + return m_residual_offsets; +} + +void GravityFieldOperator::ApplyGravityUnknowns( + const mfem::Vector &gravity_gradient, const mfem::Vector &gravity_potential, + const context::gravity_field::GravityFieldGeometryContext &geometry_context, + mfem::Vector &action) const { + using form = utils::blocks::gravity_field_form; + + constexpr auto gravity_gradient_residual_block = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravity_poisson_residual_block = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.poisson_term); + + MFEM_VERIFY(geometry_context.IsPrepared(), + "GravityFieldOperator received an unprepared geometry context."); + MFEM_VERIFY( + gravity_gradient.Size() == + geometry_context.GetMassOperator().GetFluxMap().reduced_size(), + "GravityFieldOperator received a gravity-gradient vector with the " + "wrong size."); + MFEM_VERIFY( + gravity_potential.Size() == + geometry_context.GetSourceOperator().GetPotentialMap().reduced_size(), + "GravityFieldOperator received a gravity-potential vector with the " + "wrong size."); + + action.SetSize(Height()); + action = 0.0; + + mfem::Vector gravity_gradient_action = make_residual_view( + action, m_residual_offsets, gravity_gradient_residual_block); + mfem::Vector gravity_poisson_action = make_residual_view( + action, m_residual_offsets, gravity_poisson_residual_block); + const field::FieldDofMap &flux_map = + geometry_context.GetMassOperator().GetFluxMap(); + const field::FieldDofMap &potential_map = + geometry_context.GetSourceOperator().GetPotentialMap(); + mfem::Vector potential_true(potential_map.full_size()); + mfem::Vector transpose_divergence_action_true(flux_map.full_size()); + mfem::Vector transpose_divergence_action(flux_map.reduced_size()); + mfem::Vector gradient_true(flux_map.full_size()); + mfem::Vector divergence_action_true(potential_map.full_size()); + + geometry_context.GetMassOperator().Mult(gravity_gradient, + gravity_gradient_action); + potential_map.scatter(gravity_potential, potential_true); + geometry_context.GetTransposeDivergenceOperator().Mult( + potential_true, transpose_divergence_action_true); + flux_map.gather(transpose_divergence_action_true, + transpose_divergence_action); + gravity_gradient_action += transpose_divergence_action; + flux_map.scatter(gravity_gradient, gradient_true); + geometry_context.GetDivergenceOperator().Mult(gradient_true, + divergence_action_true); + potential_map.gather(divergence_action_true, gravity_poisson_action); +} + +void GravityFieldOperator::ApplyDensitySource( + const mfem::Vector &density, + const context::gravity_field::GravityFieldGeometryContext &geometry_context, + mfem::Vector &action) const { + using form = utils::blocks::gravity_field_form; + + constexpr auto gravity_poisson_residual_block = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.poisson_term); + + MFEM_VERIFY(geometry_context.IsPrepared(), + "GravityFieldOperator received an unprepared geometry context."); + MFEM_VERIFY( + density.Size() == + geometry_context.GetSourceOperator().GetDensityMap().reduced_size(), + "GravityFieldOperator received a density vector with the wrong " + "size."); + + action.SetSize(Height()); + action = 0.0; + + mfem::Vector gravity_poisson_action = make_residual_view( + action, m_residual_offsets, gravity_poisson_residual_block); + geometry_context.GetSourceOperator().Mult(density, gravity_poisson_action); +} + +void GravityFieldOperator::Mult(const mfem::Vector &state, + mfem::Vector &residual) const { + MEAN_FIELD_PROFILE_SCOPE("GravityFieldOperator::Mult"); + + using form = utils::blocks::gravity_field_form; + + constexpr auto density_block = utils::blocks::get_value_block( + utils::blocks::density_field.mass_term); + constexpr auto gravity_gradient_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravity_potential_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.poisson_term); + + MFEM_VERIFY(state.Size() == Width(), + "GravityFieldOperator received a state with the wrong size."); + MFEM_VERIFY(m_linearization_context.IsPrepared(), + "GravityFieldOperator must be prepared before Mult is called."); + + const mfem::Vector density = + make_read_only_value_view(state, m_state_offsets, density_block); + const mfem::Vector gravity_gradient = + make_read_only_value_view(state, m_state_offsets, gravity_gradient_block); + const mfem::Vector gravity_potential = make_read_only_value_view( + state, m_state_offsets, gravity_potential_block); + const context::gravity_field::GravityFieldGeometryContext &geometry_context = + m_linearization_context.GetGeometryContext(); + + mfem::Vector source; + + ApplyGravityUnknowns(gravity_gradient, gravity_potential, geometry_context, + residual); + ApplyDensitySource(density, geometry_context, source); + + residual -= source; +} + +context::gravity_field::GravityFieldLinearizationContext & +GravityFieldOperator::GetLinearizationContext() noexcept { + return m_linearization_context; +} + +const context::gravity_field::GravityFieldLinearizationContext & +GravityFieldOperator::GetLinearizationContext() const noexcept { + return m_linearization_context; +} + +mfem::Operator & +GravityFieldOperator::GetGradient(const mfem::Vector &state) const { + MFEM_VERIFY(state.Size() == Width(), + "GravityFieldOperator received a " + "linearization state with the wrong size."); + MFEM_VERIFY(m_linearization_context.IsPrepared(), + "GravityFieldOperator must be prepared before GetGradient is " + "called."); + + return m_jacobian; +} +ReducedGravityFieldOperator::ReducedGravityFieldOperator( + GravityFieldOperator &gravity_field_operator, + context::gravity_field::GravityFieldGeometryContext + &gravity_field_geometry_context, + const mfem::Vector &displacement) + : Operator(gravity_field_operator.Height(), + gravity_field_operator.Height()), + m_gravity_field_operator(gravity_field_operator), + m_gravity_offsets(gravity_field_operator.GetResidualOffsets()), + m_gravity_field_geometry_context(gravity_field_geometry_context) { + using form = utils::blocks::gravity_field_form; + + constexpr auto gravity_gradient_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravity_potential_block = utils::blocks::get_value_block( + utils::blocks::gravity_field.poisson_term); + constexpr auto gravity_gradient_residual_block = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravity_poisson_residual_block = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.poisson_term); + + const mfem::Array &state_offsets = + m_gravity_field_operator.GetStateOffsets(); + + MFEM_VERIFY( + state_offsets.Size() == form::value_block_count + 1, + "ReducedGravityFieldOperator received an invalid coupled-state layout."); + MFEM_VERIFY( + m_gravity_offsets.Size() == form::residual_block_count + 1, + "ReducedGravityFieldOperator received an invalid gravity-residual " + "layout."); + MFEM_VERIFY(state_offsets[0] == 0, + "The coupled-state offsets must begin at zero."); + MFEM_VERIFY(m_gravity_offsets[0] == 0, + "The reduced gravity offsets must begin at zero."); + MFEM_VERIFY( + state_offsets.Last() == m_gravity_field_operator.Width(), + "The coupled-state offsets do not match the gravity-field operator " + "width."); + MFEM_VERIFY(m_gravity_offsets.Last() == m_gravity_field_operator.Height(), + "The reduced gravity offsets do not match the gravity-field " + "operator " + "height."); + MFEM_VERIFY(Width() == Height(), + "ReducedGravityFieldOperator must be square."); + + const int state_gradient_size = + state_offsets[static_cast(gravity_gradient_block) + 1] - + state_offsets[gravity_gradient_block]; + const int state_potential_size = + state_offsets[static_cast(gravity_potential_block) + 1] - + state_offsets[gravity_potential_block]; + const int gravity_gradient_size = + m_gravity_offsets[static_cast(gravity_gradient_residual_block) + 1] - + m_gravity_offsets[gravity_gradient_residual_block]; + const int gravity_potential_size = + m_gravity_offsets[static_cast(gravity_poisson_residual_block) + 1] - + m_gravity_offsets[gravity_poisson_residual_block]; + + MFEM_VERIFY(state_gradient_size == gravity_gradient_size, + "The gravity-gradient block does not match the coupled-state " + "gravity-gradient block."); + MFEM_VERIFY(state_potential_size == gravity_potential_size, + "The gravity-potential block does not match the Poisson " + "residual block."); + + SetDisplacement(displacement); +} + +void ReducedGravityFieldOperator::SetDisplacement( + const mfem::Vector &displacement) { + ValidateDisplacement(displacement); + + context::gravity_field::DiscretizationRevision discretization_revision; + context::gravity_field::DisplacementRevision displacement_revision; + + if (m_gravity_field_geometry_context.IsPrepared()) { + discretization_revision = + m_gravity_field_geometry_context.GetDiscretizationRevision(); + displacement_revision = + m_gravity_field_geometry_context.GetDisplacementRevision(); + + MFEM_VERIFY(displacement_revision.value < + std::numeric_limits::max(), + "The reduced gravity displacement revision has overflowed."); + ++displacement_revision.value; + } + + m_gravity_field_geometry_context.Prepare( + displacement, discretization_revision, displacement_revision); + m_displacement = displacement; +} + +const mfem::Vector &ReducedGravityFieldOperator::GetDisplacement() const { + return m_displacement; +} + +void ReducedGravityFieldOperator::BuildRightHandSide( + const mfem::Vector &density, mfem::Vector &right_hand_side) const { + ValidateDensity(density); + + m_gravity_field_operator.ApplyDensitySource( + density, m_gravity_field_geometry_context, right_hand_side); + + MFEM_VERIFY(right_hand_side.Size() == Height(), + "ReducedGravityFieldOperator produced a right-hand side with the " + "wrong " + "size."); +} + +void ReducedGravityFieldOperator::Mult(const mfem::Vector &gravity_state, + mfem::Vector &action) const { + MEAN_FIELD_PROFILE_SCOPE("ReducedGravityFieldOperator::Mult"); + + using form = utils::blocks::gravity_field_form; + + constexpr auto gravity_gradient_residual_block = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravity_poisson_residual_block = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.poisson_term); + + ValidateGravityState(gravity_state); + + const mfem::Vector gravity_gradient = make_read_only_residual_view( + gravity_state, m_gravity_offsets, gravity_gradient_residual_block); + const mfem::Vector gravity_potential = make_read_only_residual_view( + gravity_state, m_gravity_offsets, gravity_poisson_residual_block); + + m_gravity_field_operator.ApplyGravityUnknowns( + gravity_gradient, gravity_potential, m_gravity_field_geometry_context, + action); + + MFEM_VERIFY(action.Size() == Height(), "ReducedGravityFieldOperator produced " + "an action with the wrong size."); +} + +GravityFieldOperator & +ReducedGravityFieldOperator::GetGravityFieldOperator() noexcept { + return m_gravity_field_operator; +} + +const GravityFieldOperator & +ReducedGravityFieldOperator::GetGravityFieldOperator() const noexcept { + return m_gravity_field_operator; +} + +context::gravity_field::GravityFieldGeometryContext & +ReducedGravityFieldOperator::GetGeometryContext() noexcept { + return m_gravity_field_geometry_context; +} + +const context::gravity_field::GravityFieldGeometryContext & +ReducedGravityFieldOperator::GetGeometryContext() const noexcept { + return m_gravity_field_geometry_context; +} + +const mfem::Array & +ReducedGravityFieldOperator::GetGravityOffsets() const noexcept { + return m_gravity_offsets; +} + +void ReducedGravityFieldOperator::ValidateDisplacement( + const mfem::Vector &displacement) const { + using form = utils::blocks::gravity_field_form; + + constexpr auto displacement_block = utils::blocks::get_value_block( + utils::blocks::displacement_field.geometry_term); + + const mfem::Array &state_offsets = + m_gravity_field_operator.GetStateOffsets(); + const int expected_size = + state_offsets[static_cast(displacement_block) + 1] - + state_offsets[displacement_block]; + + MFEM_VERIFY(displacement.Size() == expected_size, + "ReducedGravityFieldOperator received a displacement with the " + "wrong " + "size."); + + for (int i = 0; i < displacement.Size(); ++i) { + MFEM_VERIFY(std::isfinite(displacement(i)), + "ReducedGravityFieldOperator received a non-finite " + "displacement " + "value."); + } +} + +void ReducedGravityFieldOperator::ValidateDensity( + const mfem::Vector &density) const { + using form = utils::blocks::gravity_field_form; + + constexpr auto density_block = utils::blocks::get_value_block( + utils::blocks::density_field.mass_term); + + const mfem::Array &state_offsets = + m_gravity_field_operator.GetStateOffsets(); + const int expected_size = state_offsets[static_cast(density_block) + 1] - + state_offsets[density_block]; + + MFEM_VERIFY(density.Size() == expected_size, + "ReducedGravityFieldOperator received a density with the wrong " + "size."); +} + +void ReducedGravityFieldOperator::ValidateGravityState( + const mfem::Vector &gravity_state) const { + MFEM_VERIFY(gravity_state.Size() == Width(), + "ReducedGravityFieldOperator received " + "a gravity state with the wrong size."); +} + +ReducedGravityFieldPreconditioner::ReducedGravityFieldPreconditioner( + const fem::FEM &f, + const context::gravity_field::GravityFieldGeometryContext &geometry_context) + : Solver(geometry_context.GetMassOperator().GetFluxMap().reduced_size() + + geometry_context.GetSourceOperator() + .GetPotentialMap() + .reduced_size()), + m_flux_map(geometry_context.GetMassOperator().GetFluxMap()), + m_potential_map(geometry_context.GetSourceOperator().GetPotentialMap()), + m_offsets(3) { + MFEM_VERIFY(geometry_context.IsPrepared(), + "The reduced gravity preconditioner requires prepared geometry."); + MFEM_VERIFY(f.mesh != nullptr, + "The reduced gravity preconditioner requires a parallel mesh."); + MFEM_VERIFY(f.gravityFluxFes != nullptr && f.gravityPotentialFes != nullptr, + "The reduced gravity preconditioner requires both gravity " + "finite-element spaces."); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "The reduced gravity preconditioner requires the quadrature-rule " + "factory."); + + m_offsets[0] = 0; + m_offsets[1] = m_flux_map.reduced_size(); + m_offsets[2] = m_offsets[1] + m_potential_map.reduced_size(); + + MFEM_VERIFY(m_offsets.Last() == Height(), + "The reduced gravity preconditioner has inconsistent offsets."); + + mfem::Vector reduced_mass_diagonal; + geometry_context.GetMassOperator().AssembleDiagonal(reduced_mass_diagonal); + m_mass_preconditioner = std::make_unique( + reduced_mass_diagonal, m_empty_tdofs); + + mfem::Vector true_mass_diagonal; + geometry_context.GetMassOperator().AssembleTrueDiagonal(true_mass_diagonal); + m_schur = make_gravity_schur_preconditioner(f, true_mass_diagonal); + + m_potential_preconditioner = std::make_unique(); + m_potential_preconditioner->SetPrintLevel(0); + m_potential_preconditioner->SetOperator(*m_schur); +} + +void ReducedGravityFieldPreconditioner::SetOperator( + const mfem::Operator &gravity_operator) { + MFEM_VERIFY(gravity_operator.Width() == Width() && + gravity_operator.Height() == Height(), + "The reduced gravity preconditioner received an operator with " + "incompatible dimensions."); +} + +void ReducedGravityFieldPreconditioner::Mult( + const mfem::Vector &right_hand_side, mfem::Vector &action) const { + MFEM_VERIFY(right_hand_side.Size() == Width(), + "The reduced gravity preconditioner received a right-hand side " + "with the wrong size."); + + mfem::Vector gradient_rhs; + gradient_rhs.MakeRef(const_cast(right_hand_side), + m_offsets[0], m_offsets[1] - m_offsets[0]); + mfem::Vector potential_rhs; + potential_rhs.MakeRef(const_cast(right_hand_side), + m_offsets[1], m_offsets[2] - m_offsets[1]); + + action.SetSize(Height()); + mfem::Vector gradient_action; + gradient_action.MakeRef(action, m_offsets[0], m_offsets[1] - m_offsets[0]); + mfem::Vector potential_action; + potential_action.MakeRef(action, m_offsets[1], m_offsets[2] - m_offsets[1]); + + m_mass_preconditioner->Mult(gradient_rhs, gradient_action); + + m_potential_rhs_true.SetSize(m_potential_map.full_size()); + m_potential_action_true.SetSize(m_potential_map.full_size()); + m_potential_map.scatter(potential_rhs, m_potential_rhs_true); + m_potential_preconditioner->Mult(m_potential_rhs_true, + m_potential_action_true); + m_potential_map.gather(m_potential_action_true, potential_action); +} + +const mfem::Array & +ReducedGravityFieldPreconditioner::GetOffsets() const noexcept { + return m_offsets; +} } // namespace mean_field::operators diff --git a/libmeanfield/impl/operators/gravity_field_jacobian.cpp b/libmeanfield/impl/operators/gravity_field_jacobian.cpp index cc166f7..5d09a92 100644 --- a/libmeanfield/impl/operators/gravity_field_jacobian.cpp +++ b/libmeanfield/impl/operators/gravity_field_jacobian.cpp @@ -129,7 +129,7 @@ namespace { namespace mean_field::operators { GravityFieldJacobianOperator::GravityFieldJacobianOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, + const mapping::DomainMapper &domain_mapper, const context::gravity_field::GravityFieldLinearizationContext &linearization_context, const mfem::Array &state_offsets, const mfem::Array &residual_offsets @@ -159,13 +159,6 @@ namespace mean_field::operators { f.displacementFes != nullptr, "GravityFieldJacobianOperator requires the " "displacement finite-element space." ); - MFEM_VERIFY( - f.gravityContext.b_form != nullptr, "GravityFieldJacobianOperator requires the divergence operator." - ); - MFEM_VERIFY( - f.gravityContext.BT != nullptr, "GravityFieldJacobianOperator requires the transpose divergence " - "operator." - ); MFEM_VERIFY( f.quadratureFactory != nullptr, "GravityFieldJacobianOperator requires the quadrature-rule factory." ); @@ -263,14 +256,16 @@ namespace mean_field::operators { potential_map.gather(source_variation_action_true, source_variation_action); transpose_divergence_action_true.SetSize(flux_map.full_size()); - m_fem.gravityContext.BT->Mult(gravity_potential_direction_true, transpose_divergence_action_true); + geometry_context.GetTransposeDivergenceOperator().Mult( + gravity_potential_direction_true, transpose_divergence_action_true + ); flux_map.gather(transpose_divergence_action_true, transpose_divergence_action); gravity_gradient_action += transpose_divergence_action; gravity_gradient_action += mass_variation_action; divergence_action_true.SetSize(potential_map.full_size()); - m_fem.gravityContext.b_form->Mult(gravity_gradient_direction_true, divergence_action_true); + geometry_context.GetDivergenceOperator().Mult(gravity_gradient_direction_true, divergence_action_true); potential_map.gather(divergence_action_true, gravity_poisson_action); gravity_poisson_action -= source_action; diff --git a/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp b/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp index 8ea2c21..618c929 100644 --- a/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp @@ -111,7 +111,7 @@ namespace { void validate_common_inputs( const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, + const mean_field::mapping::DomainMapper &domainMapper, const mfem::Vector &displacementTrue ) { MFEM_VERIFY(f.mesh != nullptr, "The EOS closure kernel requires a mesh."); @@ -150,7 +150,7 @@ namespace { void apply_closure_action( const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, + const mean_field::mapping::DomainMapper &domainMapper, const mean_field::eos::Polytrope &barotrope, const ClosureAction closureAction, const mfem::Vector *densityInputTrue, @@ -204,7 +204,7 @@ namespace { mfem::Vector localAction(f.densityFes->GetVSize()); localAction = 0.0; - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); mfem::Array densityDofs; mfem::Array enthalpyDofs; @@ -365,7 +365,7 @@ namespace { namespace mean_field::operators::kernels { void apply_barotropic_closure( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const mfem::Vector &densityTrue, const mfem::Vector &enthalpyTrue, @@ -380,7 +380,7 @@ namespace mean_field::operators::kernels { void apply_barotropic_closure_density_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const mfem::Vector &densityVariationTrue, const mfem::Vector &displacementTrue, @@ -394,7 +394,7 @@ namespace mean_field::operators::kernels { void apply_barotropic_closure_enthalpy_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &enthalpyVariationTrue, @@ -409,7 +409,7 @@ namespace mean_field::operators::kernels { void apply_barotropic_closure_displacement_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const mfem::Vector &baseDensityTrue, const mfem::Vector &baseEnthalpyTrue, @@ -490,7 +490,7 @@ namespace mean_field::operators::kernels { mfem::Vector localAction(f.densityFes->GetVSize()); localAction = 0.0; - mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); mfem::Array densityDofs; mfem::Array enthalpyDofs; diff --git a/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp b/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp index 61f8eca..a7db7ee 100644 --- a/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp @@ -11,708 +11,688 @@ module mean_field; import :operators.kernels.gravity_displacement_force; namespace { - enum class GravityDisplacementForceAction { residual, density, gravityGradient, displacement, complete }; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - void true_to_local( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &trueVector, - mfem::Vector &localVector - ) { - MFEM_VERIFY( - trueVector.Size() == finiteElementSpace.GetTrueVSize(), - "The gravity-displacement-force true vector has the wrong size." - ); +[[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(attribute); +} - localVector.SetSize(finiteElementSpace.GetVSize()); +enum class GravityDisplacementForceAction { + residual, + density, + gravityGradient, + displacement, + complete +}; - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void true_to_local(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, mfem::Vector &localVector) { + MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), + "The gravity-displacement-force true vector has the wrong size."); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } - } + localVector.SetSize(finiteElementSpace.GetVSize()); - void local_to_true( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &localVector, - mfem::Vector &trueVector - ) { - MFEM_VERIFY( - localVector.Size() == finiteElementSpace.GetVSize(), - "The gravity-displacement-force local vector has the wrong size." - ); + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - trueVector.SetSize(finiteElementSpace.GetTrueVSize()); - trueVector = 0.0; + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } +} - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void local_to_true(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, mfem::Vector &trueVector) { + MFEM_VERIFY( + localVector.Size() == finiteElementSpace.GetVSize(), + "The gravity-displacement-force local vector has the wrong size."); - if (prolongation != nullptr) { - prolongation->MultTranspose(localVector, trueVector); - } else { - trueVector = localVector; - } - } + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; - [[nodiscard]] int vector_dof_index( - const mfem::Ordering::Type ordering, - const int scalarDof, - const int component, - const int scalarDofCount, - const int dimension - ) { - if (ordering == mfem::Ordering::byNODES) { - return scalarDof + component * scalarDofCount; - } + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - if (ordering == mfem::Ordering::byVDIM) { - return scalarDof * dimension + component; - } + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } +} - MFEM_ABORT( - "The gravity-displacement-force test space uses an unsupported " - "ordering." - ); +[[nodiscard]] int vector_dof_index(const mfem::Ordering::Type ordering, + const int scalarDof, const int component, + const int scalarDofCount, + const int dimension) { + if (ordering == mfem::Ordering::byNODES) { + return scalarDof + component * scalarDofCount; + } - return -1; - } + if (ordering == mfem::Ordering::byVDIM) { + return scalarDof * dimension + component; + } - [[nodiscard]] const mfem::IntegrationRule &get_gravity_force_rule( - const mean_field::fem::FEM &f, - const mfem::FiniteElement &densityElement, - const mfem::FiniteElement &gravityGradientElement, - const mfem::FiniteElement &displacementElement, - const mfem::ElementTransformation &transformation - ) { - using DisplacementField = mean_field::field::Field; + MFEM_ABORT("The gravity-displacement-force test space uses an unsupported " + "ordering."); - MFEM_VERIFY( - densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder, - "The gravity-displacement-force density element does not match " - "the registered density field." - ); + return -1; +} - MFEM_VERIFY( - gravityGradientElement.GetOrder() == mean_field::field::Gravity::Flux::familyOrder + 1, - "The gravity-displacement-force RT element does not match the " - "registered gravity-gradient field." - ); +[[nodiscard]] const mfem::IntegrationRule & +get_gravity_force_rule(const mean_field::fem::FEM &f, + const mfem::FiniteElement &densityElement, + const mfem::FiniteElement &gravityGradientElement, + const mfem::FiniteElement &displacementElement, + const mfem::ElementTransformation &transformation) { + using DisplacementField = + mean_field::field::Field; - MFEM_VERIFY( - displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder, - "The gravity-displacement-force test element does not match the " - "registered displacement field." - ); + MFEM_VERIFY(densityElement.GetOrder() == + mean_field::field::Density::Scalar::familyOrder, + "The gravity-displacement-force density element does not match " + "the registered density field."); - const mean_field::quadrature::Query query = - DisplacementField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + MFEM_VERIFY(gravityGradientElement.GetOrder() == + mean_field::field::Gravity::Flux::familyOrder + 1, + "The gravity-displacement-force RT element does not match the " + "registered gravity-gradient field."); - const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY(displacementElement.GetOrder() == + mean_field::field::Displacement::Vector::familyOrder, + "The gravity-displacement-force test element does not match the " + "registered displacement field."); - MFEM_VERIFY( - rule.integration_rule != nullptr, "The quadrature policy did not return a gravity-displacement-" - "force integration rule." - ); + const mean_field::quadrature::Query query = DisplacementField::make_query< + mean_field::field::Displacement::Form::GravityForce>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - return *rule.integration_rule; - } + const mean_field::quadrature::MfemRule rule = + f.quadratureFactory->get(query, transformation.GetGeometryType()); - void validate_finite_vector( - const mfem::Vector &vector, - const char *message - ) { - for (int index = 0; index < vector.Size(); ++index) { - MFEM_VERIFY(std::isfinite(vector(index)), message); - } - } + MFEM_VERIFY(rule.integration_rule != nullptr, + "The quadrature policy did not return a gravity-displacement-" + "force integration rule."); - void validate_common_inputs( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &displacementTrue - ) { - MFEM_VERIFY(f.mesh != nullptr, "The gravity-displacement-force kernel requires a mesh."); + return *rule.integration_rule; +} - MFEM_VERIFY( - f.densityFes != nullptr, "The gravity-displacement-force kernel requires the density " - "finite-element space." - ); +void validate_finite_vector(const mfem::Vector &vector, const char *message) { + for (int index = 0; index < vector.Size(); ++index) { + MFEM_VERIFY(std::isfinite(vector(index)), message); + } +} - MFEM_VERIFY( - f.gravityFluxFes != nullptr, "The gravity-displacement-force kernel requires the gravity-" - "gradient finite-element space." - ); +void validate_common_inputs( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mfem::Vector &displacementTrue) { + MFEM_VERIFY(f.mesh != nullptr, + "The gravity-displacement-force kernel requires a mesh."); - MFEM_VERIFY( - f.displacementFes != nullptr, "The gravity-displacement-force kernel requires the displacement " - "finite-element space." - ); + MFEM_VERIFY(f.densityFes != nullptr, + "The gravity-displacement-force kernel requires the density " + "finite-element space."); - MFEM_VERIFY( - f.compactificationFes != nullptr && f.compactificationCoordinate != nullptr, - "The gravity-displacement-force kernel requires the " - "compactification coordinate." - ); + MFEM_VERIFY(f.gravityFluxFes != nullptr, + "The gravity-displacement-force kernel requires the gravity-" + "gradient finite-element space."); - MFEM_VERIFY( - f.quadratureFactory != nullptr, "The gravity-displacement-force kernel requires the quadrature " - "rule factory." - ); + MFEM_VERIFY(f.displacementFes != nullptr, + "The gravity-displacement-force kernel requires the displacement " + "finite-element space."); - MFEM_VERIFY( - displacementTrue.Size() == f.displacementFes->GetTrueVSize(), - "The gravity-displacement-force displacement vector has the " - "wrong size." - ); + MFEM_VERIFY(f.compactificationFes != nullptr && + f.compactificationCoordinate != nullptr, + "The gravity-displacement-force kernel requires the " + "compactification coordinate."); - MFEM_VERIFY( - domainMapper.GetDimension() == f.mesh->Dimension(), - "The gravity-displacement-force mapper dimension does not match " - "the mesh dimension." - ); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "The gravity-displacement-force kernel requires the quadrature " + "rule factory."); - MFEM_VERIFY( - f.displacementFes->GetVDim() == f.mesh->Dimension(), - "The gravity-displacement-force displacement dimension does not " - "match the mesh dimension." - ); + MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The gravity-displacement-force displacement vector has the " + "wrong size."); - validate_finite_vector( - displacementTrue, "The gravity-displacement-force displacement contains a " - "non-finite value." - ); - } + MFEM_VERIFY(domainMapper.GetDimension() == f.mesh->Dimension(), + "The gravity-displacement-force mapper dimension does not match " + "the mesh dimension."); - void validate_density( - const mean_field::fem::FEM &f, - const mfem::Vector &density, - const char *message - ) { - MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message); - validate_finite_vector(density, message); - } + MFEM_VERIFY(f.displacementFes->GetVDim() == f.mesh->Dimension(), + "The gravity-displacement-force displacement dimension does not " + "match the mesh dimension."); - void validate_gravity_gradient( - const mean_field::fem::FEM &f, - const mfem::Vector &gravityGradient, - const char *message - ) { - MFEM_VERIFY(gravityGradient.Size() == f.gravityFluxFes->GetTrueVSize(), message); + validate_finite_vector( + displacementTrue, + "The gravity-displacement-force displacement contains a " + "non-finite value."); +} - validate_finite_vector(gravityGradient, message); - } +void validate_density(const mean_field::fem::FEM &f, + const mfem::Vector &density, const char *message) { + MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message); + validate_finite_vector(density, message); +} - void apply_gravity_displacement_force_action( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const GravityDisplacementForceAction requestedAction, - const mfem::Vector *baseDensityTrue, - const mfem::Vector *densityVariationTrue, - const mfem::Vector *baseGravityGradientTrue, - const mfem::Vector *gravityGradientVariationTrue, - const mfem::Vector *displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - validate_common_inputs(f, domainMapper, displacementTrue); +void validate_gravity_gradient(const mean_field::fem::FEM &f, + const mfem::Vector &gravityGradient, + const char *message) { + MFEM_VERIFY(gravityGradient.Size() == f.gravityFluxFes->GetTrueVSize(), + message); - const bool needsBaseDensity = requestedAction == GravityDisplacementForceAction::residual || - requestedAction == GravityDisplacementForceAction::gravityGradient || - requestedAction == GravityDisplacementForceAction::displacement || - requestedAction == GravityDisplacementForceAction::complete; + validate_finite_vector(gravityGradient, message); +} - const bool needsDensityVariation = requestedAction == GravityDisplacementForceAction::density || - requestedAction == GravityDisplacementForceAction::complete; +void apply_gravity_displacement_force_action( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const GravityDisplacementForceAction requestedAction, + const mfem::Vector *baseDensityTrue, + const mfem::Vector *densityVariationTrue, + const mfem::Vector *baseGravityGradientTrue, + const mfem::Vector *gravityGradientVariationTrue, + const mfem::Vector *displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + validate_common_inputs(f, domainMapper, displacementTrue); - const bool needsBaseGravityGradient = requestedAction == GravityDisplacementForceAction::residual || - requestedAction == GravityDisplacementForceAction::density || - requestedAction == GravityDisplacementForceAction::displacement || - requestedAction == GravityDisplacementForceAction::complete; + const bool needsBaseDensity = + requestedAction == GravityDisplacementForceAction::residual || + requestedAction == GravityDisplacementForceAction::gravityGradient || + requestedAction == GravityDisplacementForceAction::displacement || + requestedAction == GravityDisplacementForceAction::complete; - const bool needsGravityGradientVariation = requestedAction == GravityDisplacementForceAction::gravityGradient || - requestedAction == GravityDisplacementForceAction::complete; + const bool needsDensityVariation = + requestedAction == GravityDisplacementForceAction::density || + requestedAction == GravityDisplacementForceAction::complete; - const bool needsDisplacementVariation = requestedAction == GravityDisplacementForceAction::displacement || - requestedAction == GravityDisplacementForceAction::complete; + const bool needsBaseGravityGradient = + requestedAction == GravityDisplacementForceAction::residual || + requestedAction == GravityDisplacementForceAction::density || + requestedAction == GravityDisplacementForceAction::displacement || + requestedAction == GravityDisplacementForceAction::complete; - if (needsBaseDensity) { - MFEM_VERIFY( - baseDensityTrue != nullptr, "The gravity-displacement-force action requires a base " - "density." - ); + const bool needsGravityGradientVariation = + requestedAction == GravityDisplacementForceAction::gravityGradient || + requestedAction == GravityDisplacementForceAction::complete; - validate_density(f, *baseDensityTrue, "The gravity-displacement-force base density is invalid."); - } + const bool needsDisplacementVariation = + requestedAction == GravityDisplacementForceAction::displacement || + requestedAction == GravityDisplacementForceAction::complete; - if (needsDensityVariation) { - MFEM_VERIFY( - densityVariationTrue != nullptr, "The gravity-displacement-force action requires a density " - "variation." - ); + if (needsBaseDensity) { + MFEM_VERIFY(baseDensityTrue != nullptr, + "The gravity-displacement-force action requires a base " + "density."); - validate_density( - f, *densityVariationTrue, - "The gravity-displacement-force density variation is " - "invalid." - ); - } + validate_density(f, *baseDensityTrue, + "The gravity-displacement-force base density is invalid."); + } - if (needsBaseGravityGradient) { - MFEM_VERIFY( - baseGravityGradientTrue != nullptr, "The gravity-displacement-force action requires a base " - "gravity gradient." - ); + if (needsDensityVariation) { + MFEM_VERIFY(densityVariationTrue != nullptr, + "The gravity-displacement-force action requires a density " + "variation."); - validate_gravity_gradient( - f, *baseGravityGradientTrue, - "The gravity-displacement-force base gravity gradient is " - "invalid." - ); - } + validate_density(f, *densityVariationTrue, + "The gravity-displacement-force density variation is " + "invalid."); + } - if (needsGravityGradientVariation) { - MFEM_VERIFY( - gravityGradientVariationTrue != nullptr, "The gravity-displacement-force action requires a gravity-" - "gradient variation." - ); + if (needsBaseGravityGradient) { + MFEM_VERIFY(baseGravityGradientTrue != nullptr, + "The gravity-displacement-force action requires a base " + "gravity gradient."); - validate_gravity_gradient( - f, *gravityGradientVariationTrue, - "The gravity-displacement-force gravity-gradient variation " - "is invalid." - ); - } + validate_gravity_gradient( + f, *baseGravityGradientTrue, + "The gravity-displacement-force base gravity gradient is " + "invalid."); + } - if (needsDisplacementVariation) { - MFEM_VERIFY( - displacementVariationTrue != nullptr && - displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(), + if (needsGravityGradientVariation) { + MFEM_VERIFY(gravityGradientVariationTrue != nullptr, + "The gravity-displacement-force action requires a gravity-" + "gradient variation."); + + validate_gravity_gradient( + f, *gravityGradientVariationTrue, + "The gravity-displacement-force gravity-gradient variation " + "is invalid."); + } + + if (needsDisplacementVariation) { + MFEM_VERIFY(displacementVariationTrue != nullptr && + displacementVariationTrue->Size() == + f.displacementFes->GetTrueVSize(), "The gravity-displacement-force displacement variation is " - "invalid." - ); + "invalid."); - validate_finite_vector( - *displacementVariationTrue, "The gravity-displacement-force displacement variation " - "contains a non-finite value." - ); - } + validate_finite_vector( + *displacementVariationTrue, + "The gravity-displacement-force displacement variation " + "contains a non-finite value."); + } - mfem::Vector baseDensityLocal; - mfem::Vector densityVariationLocal; - mfem::Vector baseGravityGradientLocal; - mfem::Vector gravityGradientVariationLocal; - mfem::Vector displacementLocal; - mfem::Vector displacementVariationLocal; + mfem::Vector baseDensityLocal; + mfem::Vector densityVariationLocal; + mfem::Vector baseGravityGradientLocal; + mfem::Vector gravityGradientVariationLocal; + mfem::Vector displacementLocal; + mfem::Vector displacementVariationLocal; - if (needsBaseDensity) { - true_to_local(*f.densityFes, *baseDensityTrue, baseDensityLocal); - } + if (needsBaseDensity) { + true_to_local(*f.densityFes, *baseDensityTrue, baseDensityLocal); + } + if (needsDensityVariation) { + true_to_local(*f.densityFes, *densityVariationTrue, densityVariationLocal); + } + + if (needsBaseGravityGradient) { + true_to_local(*f.gravityFluxFes, *baseGravityGradientTrue, + baseGravityGradientLocal); + } + + if (needsGravityGradientVariation) { + true_to_local(*f.gravityFluxFes, *gravityGradientVariationTrue, + gravityGradientVariationLocal); + } + + true_to_local(*f.displacementFes, displacementTrue, displacementLocal); + + if (needsDisplacementVariation) { + true_to_local(*f.displacementFes, *displacementVariationTrue, + displacementVariationLocal); + } + + mfem::Vector localAction(f.displacementFes->GetVSize()); + localAction = 0.0; + + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); + + mfem::Array densityDofs; + mfem::Array gravityGradientDofs; + mfem::Array displacementDofs; + mfem::Array compactificationDofs; + + mfem::Vector elementBaseDensity; + mfem::Vector elementDensityVariation; + mfem::Vector elementBaseGravityGradient; + mfem::Vector elementGravityGradientVariation; + mfem::Vector elementDisplacement; + mfem::Vector elementDisplacementVariation; + mfem::Vector elementCompactification; + mfem::Vector elementAction; + + mfem::Vector densityShape; + mfem::Vector displacementShape; + mfem::DenseMatrix gravityGradientShape; + + mfem::Vector baseGravityReferenceValue; + mfem::Vector gravityVariationReferenceValue; + mfem::Vector mappedBaseGravity; + mfem::Vector mappedGravityVariation; + mfem::Vector mappedGeometryVariation; + mfem::Vector forceValue; + + mean_field::mapping::VolumeMappingContext mappingContext; + mean_field::mapping::VolumeMappingVariation mappingVariation; + + const int dimension = f.mesh->Dimension(); + + const mfem::Ordering::Type displacementOrdering = + f.displacementFes->GetOrdering(); + + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elementId); + + MFEM_VERIFY(transformation != nullptr, + "The gravity-displacement-force kernel received a null " + "element transformation."); + + if (is_vacuum_attribute(transformation->Attribute)) { + continue; + } + + const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId); + + const mfem::FiniteElement &gravityGradientElement = + *f.gravityFluxFes->GetFE(elementId); + + const mfem::FiniteElement &displacementElement = + *f.displacementFes->GetFE(elementId); + + const mfem::FiniteElement &compactificationElement = + *f.compactificationFes->GetFE(elementId); + + mfem::DofTransformation *densityDofTransformation = + f.densityFes->GetElementDofs(elementId, densityDofs); + + mfem::DofTransformation *gravityGradientDofTransformation = + f.gravityFluxFes->GetElementVDofs(elementId, gravityGradientDofs); + + mfem::DofTransformation *displacementDofTransformation = + f.displacementFes->GetElementVDofs(elementId, displacementDofs); + + mfem::DofTransformation *compactificationDofTransformation = + f.compactificationFes->GetElementDofs(elementId, compactificationDofs); + + if (needsBaseDensity) { + baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity); + } + + if (needsDensityVariation) { + densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation); + } + + if (needsBaseGravityGradient) { + baseGravityGradientLocal.GetSubVector(gravityGradientDofs, + elementBaseGravityGradient); + } + + if (needsGravityGradientVariation) { + gravityGradientVariationLocal.GetSubVector( + gravityGradientDofs, elementGravityGradientVariation); + } + + displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + + if (needsDisplacementVariation) { + displacementVariationLocal.GetSubVector(displacementDofs, + elementDisplacementVariation); + } + + f.compactificationCoordinate->GetSubVector(compactificationDofs, + elementCompactification); + + if (densityDofTransformation != nullptr) { + if (needsBaseDensity) { + densityDofTransformation->InvTransformPrimal(elementBaseDensity); + } + + if (needsDensityVariation) { + densityDofTransformation->InvTransformPrimal(elementDensityVariation); + } + } + + if (gravityGradientDofTransformation != nullptr) { + if (needsBaseGravityGradient) { + gravityGradientDofTransformation->InvTransformPrimal( + elementBaseGravityGradient); + } + + if (needsGravityGradientVariation) { + gravityGradientDofTransformation->InvTransformPrimal( + elementGravityGradientVariation); + } + } + + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->InvTransformPrimal(elementDisplacement); + + if (needsDisplacementVariation) { + displacementDofTransformation->InvTransformPrimal( + elementDisplacementVariation); + } + } + + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal( + elementCompactification); + } + + const mean_field::mapping::ElementDisplacementData displacementData = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacement); + + const mean_field::mapping::ElementCompactificationData compactificationData( + compactificationElement, elementCompactification); + + const mean_field::mapping::ElementMappingData mappingData{ + .displacement = displacementData, + .compactification = compactificationData}; + + std::optional + displacementVariationData; + + if (needsDisplacementVariation) { + displacementVariationData.emplace( + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacementVariation)); + } + + const int scalarDisplacementDofCount = displacementElement.GetDof(); + + MFEM_VERIFY(displacementDofs.Size() == + scalarDisplacementDofCount * dimension, + "The gravity-displacement-force element displacement vector " + "has the wrong size."); + + densityShape.SetSize(densityElement.GetDof()); + displacementShape.SetSize(scalarDisplacementDofCount); + gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension); + + baseGravityReferenceValue.SetSize(dimension); + gravityVariationReferenceValue.SetSize(dimension); + mappedBaseGravity.SetSize(dimension); + mappedGravityVariation.SetSize(dimension); + mappedGeometryVariation.SetSize(dimension); + forceValue.SetSize(dimension); + + elementAction.SetSize(displacementDofs.Size()); + elementAction = 0.0; + + const mfem::IntegrationRule &integrationRule = + get_gravity_force_rule(f, densityElement, gravityGradientElement, + displacementElement, *transformation); + + for (int quadratureIndex = 0; + quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) { + const mfem::IntegrationPoint &integrationPoint = + integrationRule.IntPoint(quadratureIndex); + + transformation->SetIntPoint(&integrationPoint); + + const mean_field::mapping::MappingStatus mappingStatus = + domainMapper.EvaluateVolume(mappingData, *transformation, + integrationPoint, workspace, + mappingContext); + + MFEM_VERIFY(mappingStatus == mean_field::mapping::MappingStatus::valid, + "Stateless mapping failed in the gravity-displacement-" + "force kernel. Element: " + << elementId + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadratureIndex + << ", status: " << static_cast(mappingStatus)); + + if (needsDisplacementVariation) { + const mean_field::mapping::MappingStatus variationStatus = + domainMapper.EvaluateVolumeVariation( + mappingData, *displacementVariationData, *transformation, + integrationPoint, mappingContext, workspace, mappingVariation); + + MFEM_VERIFY( + variationStatus == mean_field::mapping::MappingStatus::valid, + "Stateless mapping variation failed in the gravity-" + "displacement-force kernel. Element: " + << elementId << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadratureIndex + << ", status: " << static_cast(variationStatus)); + } + + densityElement.CalcShape(integrationPoint, densityShape); + + displacementElement.CalcShape(integrationPoint, displacementShape); + + gravityGradientElement.CalcVShape(*transformation, gravityGradientShape); + + double baseDensityValue = 0.0; + double densityVariationValue = 0.0; + + if (needsBaseDensity) { + baseDensityValue = elementBaseDensity * densityShape; + } + + if (needsDensityVariation) { + densityVariationValue = elementDensityVariation * densityShape; + } + + if (needsBaseGravityGradient) { + gravityGradientShape.MultTranspose(elementBaseGravityGradient, + baseGravityReferenceValue); + + mappingContext.mapping.mapping_jacobian.Mult(baseGravityReferenceValue, + mappedBaseGravity); + } else { + mappedBaseGravity = 0.0; + } + + if (needsGravityGradientVariation) { + gravityGradientShape.MultTranspose(elementGravityGradientVariation, + gravityVariationReferenceValue); + + mappingContext.mapping.mapping_jacobian.Mult( + gravityVariationReferenceValue, mappedGravityVariation); + } else { + mappedGravityVariation = 0.0; + } + + if (needsDisplacementVariation) { + mappingVariation.mapping.mapping_jacobian_variation.Mult( + baseGravityReferenceValue, mappedGeometryVariation); + } else { + mappedGeometryVariation = 0.0; + } + + forceValue = 0.0; + + if (requestedAction == GravityDisplacementForceAction::residual) { + forceValue.Add(baseDensityValue, mappedBaseGravity); + } else { if (needsDensityVariation) { - true_to_local(*f.densityFes, *densityVariationTrue, densityVariationLocal); - } - - if (needsBaseGravityGradient) { - true_to_local(*f.gravityFluxFes, *baseGravityGradientTrue, baseGravityGradientLocal); + forceValue.Add(densityVariationValue, mappedBaseGravity); } if (needsGravityGradientVariation) { - true_to_local(*f.gravityFluxFes, *gravityGradientVariationTrue, gravityGradientVariationLocal); + forceValue.Add(baseDensityValue, mappedGravityVariation); } - true_to_local(*f.displacementFes, displacementTrue, displacementLocal); - if (needsDisplacementVariation) { - true_to_local(*f.displacementFes, *displacementVariationTrue, displacementVariationLocal); + forceValue.Add(baseDensityValue, mappedGeometryVariation); } - - mfem::Vector localAction(f.displacementFes->GetVSize()); - localAction = 0.0; - - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); - - mfem::Array densityDofs; - mfem::Array gravityGradientDofs; - mfem::Array displacementDofs; - mfem::Array compactificationDofs; - - mfem::Vector elementBaseDensity; - mfem::Vector elementDensityVariation; - mfem::Vector elementBaseGravityGradient; - mfem::Vector elementGravityGradientVariation; - mfem::Vector elementDisplacement; - mfem::Vector elementDisplacementVariation; - mfem::Vector elementCompactification; - mfem::Vector elementAction; - - mfem::Vector densityShape; - mfem::Vector displacementShape; - mfem::DenseMatrix gravityGradientShape; - - mfem::Vector baseGravityReferenceValue; - mfem::Vector gravityVariationReferenceValue; - mfem::Vector mappedBaseGravity; - mfem::Vector mappedGravityVariation; - mfem::Vector mappedGeometryVariation; - mfem::Vector forceValue; - - mean_field::mapping::VolumeMappingContext mappingContext; - mean_field::mapping::VolumeMappingVariation mappingVariation; - - const int dimension = f.mesh->Dimension(); - const int vacuumAttribute = domainMapper.GetVacuumElementAttribute(); - - const mfem::Ordering::Type displacementOrdering = f.displacementFes->GetOrdering(); - - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); - - MFEM_VERIFY( - transformation != nullptr, "The gravity-displacement-force kernel received a null " - "element transformation." - ); - - if (transformation->Attribute == vacuumAttribute) { - continue; - } - - const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId); - - const mfem::FiniteElement &gravityGradientElement = *f.gravityFluxFes->GetFE(elementId); - - const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId); - - const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId); - - mfem::DofTransformation *densityDofTransformation = f.densityFes->GetElementDofs(elementId, densityDofs); - - mfem::DofTransformation *gravityGradientDofTransformation = - f.gravityFluxFes->GetElementVDofs(elementId, gravityGradientDofs); - - mfem::DofTransformation *displacementDofTransformation = - f.displacementFes->GetElementVDofs(elementId, displacementDofs); - - mfem::DofTransformation *compactificationDofTransformation = - f.compactificationFes->GetElementDofs(elementId, compactificationDofs); - - if (needsBaseDensity) { - baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity); - } - - if (needsDensityVariation) { - densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation); - } - - if (needsBaseGravityGradient) { - baseGravityGradientLocal.GetSubVector(gravityGradientDofs, elementBaseGravityGradient); - } - - if (needsGravityGradientVariation) { - gravityGradientVariationLocal.GetSubVector(gravityGradientDofs, elementGravityGradientVariation); - } - - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); - - if (needsDisplacementVariation) { - displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation); - } - - f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); - - if (densityDofTransformation != nullptr) { - if (needsBaseDensity) { - densityDofTransformation->InvTransformPrimal(elementBaseDensity); - } - - if (needsDensityVariation) { - densityDofTransformation->InvTransformPrimal(elementDensityVariation); - } - } - - if (gravityGradientDofTransformation != nullptr) { - if (needsBaseGravityGradient) { - gravityGradientDofTransformation->InvTransformPrimal(elementBaseGravityGradient); - } - - if (needsGravityGradientVariation) { - gravityGradientDofTransformation->InvTransformPrimal(elementGravityGradientVariation); - } - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacement); - - if (needsDisplacementVariation) { - displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation); - } - } - - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal(elementCompactification); - } - - const mean_field::mapping::ElementDisplacementData displacementData = - mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement); - - const mean_field::mapping::ElementCompactificationData compactificationData( - compactificationElement, elementCompactification - ); - - const mean_field::mapping::ElementMappingData mappingData{ - .displacement = displacementData, .compactification = compactificationData - }; - - std::optional displacementVariationData; - - if (needsDisplacementVariation) { - displacementVariationData.emplace( - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacementElement, elementDisplacementVariation - ) - ); - } - - const int scalarDisplacementDofCount = displacementElement.GetDof(); - - MFEM_VERIFY( - displacementDofs.Size() == scalarDisplacementDofCount * dimension, - "The gravity-displacement-force element displacement vector " - "has the wrong size." - ); - - densityShape.SetSize(densityElement.GetDof()); - displacementShape.SetSize(scalarDisplacementDofCount); - gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension); - - baseGravityReferenceValue.SetSize(dimension); - gravityVariationReferenceValue.SetSize(dimension); - mappedBaseGravity.SetSize(dimension); - mappedGravityVariation.SetSize(dimension); - mappedGeometryVariation.SetSize(dimension); - forceValue.SetSize(dimension); - - elementAction.SetSize(displacementDofs.Size()); - elementAction = 0.0; - - const mfem::IntegrationRule &integrationRule = - get_gravity_force_rule(f, densityElement, gravityGradientElement, displacementElement, *transformation); - - for (int quadratureIndex = 0; quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) { - const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadratureIndex); - - transformation->SetIntPoint(&integrationPoint); - - const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume( - mappingData, *transformation, integrationPoint, workspace, mappingContext - ); - - MFEM_VERIFY( - mappingStatus == mean_field::mapping::MappingStatus::valid, - "Stateless mapping failed in the gravity-displacement-" - "force kernel. Element: " - << elementId << ", attribute: " << transformation->Attribute - << ", quadrature point: " << quadratureIndex << ", status: " << static_cast(mappingStatus) - ); - - if (needsDisplacementVariation) { - const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation( - mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext, - workspace, mappingVariation - ); - - MFEM_VERIFY( - variationStatus == mean_field::mapping::MappingStatus::valid, - "Stateless mapping variation failed in the gravity-" - "displacement-force kernel. Element: " - << elementId << ", attribute: " << transformation->Attribute << ", quadrature point: " - << quadratureIndex << ", status: " << static_cast(variationStatus) - ); - } - - densityElement.CalcShape(integrationPoint, densityShape); - - displacementElement.CalcShape(integrationPoint, displacementShape); - - gravityGradientElement.CalcVShape(*transformation, gravityGradientShape); - - double baseDensityValue = 0.0; - double densityVariationValue = 0.0; - - if (needsBaseDensity) { - baseDensityValue = elementBaseDensity * densityShape; - } - - if (needsDensityVariation) { - densityVariationValue = elementDensityVariation * densityShape; - } - - if (needsBaseGravityGradient) { - gravityGradientShape.MultTranspose(elementBaseGravityGradient, baseGravityReferenceValue); - - mappingContext.mapping.mapping_jacobian.Mult(baseGravityReferenceValue, mappedBaseGravity); - } else { - mappedBaseGravity = 0.0; - } - - if (needsGravityGradientVariation) { - gravityGradientShape.MultTranspose(elementGravityGradientVariation, gravityVariationReferenceValue); - - mappingContext.mapping.mapping_jacobian.Mult( - gravityVariationReferenceValue, mappedGravityVariation - ); - } else { - mappedGravityVariation = 0.0; - } - - if (needsDisplacementVariation) { - mappingVariation.mapping.mapping_jacobian_variation.Mult( - baseGravityReferenceValue, mappedGeometryVariation - ); - } else { - mappedGeometryVariation = 0.0; - } - - forceValue = 0.0; - - if (requestedAction == GravityDisplacementForceAction::residual) { - forceValue.Add(baseDensityValue, mappedBaseGravity); - } else { - if (needsDensityVariation) { - forceValue.Add(densityVariationValue, mappedBaseGravity); - } - - if (needsGravityGradientVariation) { - forceValue.Add(baseDensityValue, mappedGravityVariation); - } - - if (needsDisplacementVariation) { - forceValue.Add(baseDensityValue, mappedGeometryVariation); - } - } - - /* - * If g_ref is the RT pullback, then - * - * g_phys = J_map g_ref / det(J_map), - * dV_phys = det(J_map) dV_ref. - * - * The determinant cancels exactly. Consequently the base - * integrand uses J_map g_ref and its geometry derivative uses - * delta(J_map) g_ref. This is algebraically identical to - * differentiating the Piola map and physical volume weight, - * but avoids a numerically pointless cancellation. - */ - const double referenceWeight = integrationPoint.weight * transformation->Weight(); - - forceValue *= referenceWeight; - - for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) { - for (int component = 0; component < dimension; ++component) { - const int vectorDof = vector_dof_index( - displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension - ); - - const double contribution = displacementShape(scalarDof) * forceValue(component); - - MFEM_VERIFY( - std::isfinite(contribution), "The gravity-displacement-force kernel " - "encountered a non-finite contribution." - ); - - elementAction(vectorDof) += contribution; - } - } - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(displacementDofs, elementAction); + } + + /* + * If g_ref is the RT pullback, then + * + * g_phys = J_map g_ref / det(J_map), + * dV_phys = det(J_map) dV_ref. + * + * The determinant cancels exactly. Consequently the base + * integrand uses J_map g_ref and its geometry derivative uses + * delta(J_map) g_ref. This is algebraically identical to + * differentiating the Piola map and physical volume weight, + * but avoids a numerically pointless cancellation. + */ + const double referenceWeight = + integrationPoint.weight * transformation->Weight(); + + forceValue *= referenceWeight; + + for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; + ++scalarDof) { + for (int component = 0; component < dimension; ++component) { + const int vectorDof = + vector_dof_index(displacementOrdering, scalarDof, component, + scalarDisplacementDofCount, dimension); + + const double contribution = + displacementShape(scalarDof) * forceValue(component); + + MFEM_VERIFY(std::isfinite(contribution), + "The gravity-displacement-force kernel " + "encountered a non-finite contribution."); + + elementAction(vectorDof) += contribution; } - - local_to_true(*f.displacementFes, localAction, actionTrue); + } } + + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->TransformDual(elementAction); + } + + localAction.AddElementVector(displacementDofs, elementAction); + } + + local_to_true(*f.displacementFes, localAction, actionTrue); +} } // namespace namespace mean_field::operators::kernels { - void apply_gravity_displacement_force_residual( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &densityTrue, - const mfem::Vector &gravityGradientTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &residualTrue - ) { - apply_gravity_displacement_force_action( - f, domainMapper, GravityDisplacementForceAction::residual, &densityTrue, nullptr, &gravityGradientTrue, - nullptr, nullptr, displacementTrue, residualTrue - ); - } +void apply_gravity_displacement_force_residual( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const mfem::Vector &densityTrue, const mfem::Vector &gravityGradientTrue, + const mfem::Vector &displacementTrue, mfem::Vector &residualTrue) { + apply_gravity_displacement_force_action( + f, domainMapper, GravityDisplacementForceAction::residual, &densityTrue, + nullptr, &gravityGradientTrue, nullptr, nullptr, displacementTrue, + residualTrue); +} - void apply_gravity_displacement_force_density_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &densityVariationTrue, - const mfem::Vector &baseGravityGradientTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_gravity_displacement_force_action( - f, domainMapper, GravityDisplacementForceAction::density, nullptr, &densityVariationTrue, - &baseGravityGradientTrue, nullptr, nullptr, displacementTrue, actionTrue - ); - } +void apply_gravity_displacement_force_density_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const mfem::Vector &densityVariationTrue, + const mfem::Vector &baseGravityGradientTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_gravity_displacement_force_action( + f, domainMapper, GravityDisplacementForceAction::density, nullptr, + &densityVariationTrue, &baseGravityGradientTrue, nullptr, nullptr, + displacementTrue, actionTrue); +} - void apply_gravity_displacement_force_gradient_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &baseDensityTrue, - const mfem::Vector &gravityGradientVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_gravity_displacement_force_action( - f, domainMapper, GravityDisplacementForceAction::gravityGradient, &baseDensityTrue, nullptr, nullptr, - &gravityGradientVariationTrue, nullptr, displacementTrue, actionTrue - ); - } +void apply_gravity_displacement_force_gradient_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const mfem::Vector &baseDensityTrue, + const mfem::Vector &gravityGradientVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_gravity_displacement_force_action( + f, domainMapper, GravityDisplacementForceAction::gravityGradient, + &baseDensityTrue, nullptr, nullptr, &gravityGradientVariationTrue, + nullptr, displacementTrue, actionTrue); +} - void apply_gravity_displacement_force_displacement_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &baseDensityTrue, - const mfem::Vector &baseGravityGradientTrue, - const mfem::Vector &displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_gravity_displacement_force_action( - f, domainMapper, GravityDisplacementForceAction::displacement, &baseDensityTrue, nullptr, - &baseGravityGradientTrue, nullptr, &displacementVariationTrue, displacementTrue, actionTrue - ); - } +void apply_gravity_displacement_force_displacement_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const mfem::Vector &baseDensityTrue, + const mfem::Vector &baseGravityGradientTrue, + const mfem::Vector &displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_gravity_displacement_force_action( + f, domainMapper, GravityDisplacementForceAction::displacement, + &baseDensityTrue, nullptr, &baseGravityGradientTrue, nullptr, + &displacementVariationTrue, displacementTrue, actionTrue); +} - void apply_gravity_displacement_force_complete_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &baseDensityTrue, - const mfem::Vector &densityVariationTrue, - const mfem::Vector &baseGravityGradientTrue, - const mfem::Vector &gravityGradientVariationTrue, - const mfem::Vector &displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_gravity_displacement_force_action( - f, domainMapper, GravityDisplacementForceAction::complete, &baseDensityTrue, &densityVariationTrue, - &baseGravityGradientTrue, &gravityGradientVariationTrue, &displacementVariationTrue, displacementTrue, - actionTrue - ); - } +void apply_gravity_displacement_force_complete_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const mfem::Vector &baseDensityTrue, + const mfem::Vector &densityVariationTrue, + const mfem::Vector &baseGravityGradientTrue, + const mfem::Vector &gravityGradientVariationTrue, + const mfem::Vector &displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_gravity_displacement_force_action( + f, domainMapper, GravityDisplacementForceAction::complete, + &baseDensityTrue, &densityVariationTrue, &baseGravityGradientTrue, + &gravityGradientVariationTrue, &displacementVariationTrue, + displacementTrue, actionTrue); +} } // namespace mean_field::operators::kernels diff --git a/libmeanfield/impl/operators/kernels/gravity_kernels.cpp b/libmeanfield/impl/operators/kernels/gravity_kernels.cpp index 6fecbaa..54f52e8 100644 --- a/libmeanfield/impl/operators/kernels/gravity_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/gravity_kernels.cpp @@ -6,778 +6,788 @@ module mean_field; import :operators.kernels.gravity_field; namespace { - void true_to_local( - const mfem::ParFiniteElementSpace &finite_element_space, - const mfem::Vector &true_vector, - mfem::Vector &local_vector - ) { - MFEM_VERIFY(true_vector.Size() == finite_element_space.GetTrueVSize(), "True vector has the wrong size."); +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - local_vector.SetSize(finite_element_space.GetVSize()); +[[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(attribute); +} - const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->Mult(true_vector, local_vector); - } else { - local_vector = true_vector; - } - } +void true_to_local(const mfem::ParFiniteElementSpace &finite_element_space, + const mfem::Vector &true_vector, + mfem::Vector &local_vector) { + MFEM_VERIFY(true_vector.Size() == finite_element_space.GetTrueVSize(), + "True vector has the wrong size."); - void local_to_true( - const mfem::ParFiniteElementSpace &finite_element_space, - const mfem::Vector &local_vector, - mfem::Vector &true_vector - ) { - MFEM_VERIFY(local_vector.Size() == finite_element_space.GetVSize(), "Local vector has the wrong size."); + local_vector.SetSize(finite_element_space.GetVSize()); - true_vector.SetSize(finite_element_space.GetTrueVSize()); - true_vector = 0.0; + const mfem::Operator *prolongation = + finite_element_space.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->Mult(true_vector, local_vector); + } else { + local_vector = true_vector; + } +} - const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->MultTranspose(local_vector, true_vector); - } else { - true_vector = local_vector; - } - } +void local_to_true(const mfem::ParFiniteElementSpace &finite_element_space, + const mfem::Vector &local_vector, + mfem::Vector &true_vector) { + MFEM_VERIFY(local_vector.Size() == finite_element_space.GetVSize(), + "Local vector has the wrong size."); - void add_local_to_true( - const mfem::ParFiniteElementSpace &fes, - const mfem::Vector &local_vector, - mfem::Vector &true_vector - ) { - const mfem::Operator *prolongation = fes.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->AddMultTranspose(local_vector, true_vector); - } else { - true_vector += local_vector; - } - } + true_vector.SetSize(finite_element_space.GetTrueVSize()); + true_vector = 0.0; - mean_field::quadrature::MappingKind get_mapping_kind( - const mean_field::mapping::DomainMapperStateless &domain_mapper, - const mfem::ElementTransformation &transformation - ) { - return transformation.Attribute == domain_mapper.GetVacuumElementAttribute() - ? mean_field::quadrature::MappingKind::kelvin - : mean_field::quadrature::MappingKind::general; - } + const mfem::Operator *prolongation = + finite_element_space.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->MultTranspose(local_vector, true_vector); + } else { + true_vector = local_vector; + } +} - const mfem::IntegrationRule &get_hdiv_mass_rule( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domain_mapper, - const mfem::FiniteElement &element, - const mfem::ElementTransformation &transformation - ) { - using GravityField = mean_field::field::Field; - MFEM_VERIFY( - element.GetOrder() == mean_field::field::Gravity::Flux::familyOrder + 1, - "The H(div) kernel element does not match the registered gravity " - "flux." - ); - const mean_field::quadrature::Query query = - GravityField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::ALL, get_mapping_kind(domain_mapper, transformation) - ); +void add_local_to_true(const mfem::ParFiniteElementSpace &fes, + const mfem::Vector &local_vector, + mfem::Vector &true_vector) { + const mfem::Operator *prolongation = fes.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->AddMultTranspose(local_vector, true_vector); + } else { + true_vector += local_vector; + } +} - const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType()); - MFEM_VERIFY( - resolution.integration_rule != nullptr, "The quadrature policy did not return an H(div) mass integration " - "rule." - ); - return *resolution.integration_rule; - } +mean_field::quadrature::MappingKind get_mapping_kind( + const mean_field::mapping::DomainMapper &domain_mapper, + const mfem::ElementTransformation &transformation) { + return domain_mapper.IsCompactifiedElement(transformation) + ? mean_field::quadrature::MappingKind::kelvin + : mean_field::quadrature::MappingKind::general; +} - const mfem::IntegrationRule &get_source_rule( - const mean_field::fem::FEM &f, - const mfem::FiniteElement &density_element, - const mfem::FiniteElement &potential_element, - const mfem::ElementTransformation &transformation - ) { - using GravityField = mean_field::field::Field; - MFEM_VERIFY( - density_element.GetOrder() == mean_field::field::Density::Scalar::familyOrder, - "The source-kernel trial element does not match the registered " - "density " - "field." - ); - MFEM_VERIFY( - potential_element.GetOrder() == mean_field::field::Gravity::Potential::familyOrder, - "The source-kernel test element does not match the registered " - "gravity " - "potential." - ); - const mean_field::quadrature::Query query = - GravityField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); +const mfem::IntegrationRule &get_hdiv_mass_rule( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domain_mapper, + const mfem::FiniteElement &element, + const mfem::ElementTransformation &transformation) { + using GravityField = mean_field::field::Field; + MFEM_VERIFY(element.GetOrder() == + mean_field::field::Gravity::Flux::familyOrder + 1, + "The H(div) kernel element does not match the registered gravity " + "flux."); + const mean_field::quadrature::Query query = + GravityField::make_query( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::ALL, + get_mapping_kind(domain_mapper, transformation)); - const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType()); - MFEM_VERIFY( - resolution.integration_rule != nullptr, "The quadrature policy did not return a gravity-source integration " - "rule." - ); - return *resolution.integration_rule; - } + const auto resolution = + f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY(resolution.integration_rule != nullptr, + "The quadrature policy did not return an H(div) mass integration " + "rule."); + return *resolution.integration_rule; +} + +const mfem::IntegrationRule & +get_source_rule(const mean_field::fem::FEM &f, + const mfem::FiniteElement &density_element, + const mfem::FiniteElement &potential_element, + const mfem::ElementTransformation &transformation) { + using GravityField = mean_field::field::Field; + MFEM_VERIFY(density_element.GetOrder() == + mean_field::field::Density::Scalar::familyOrder, + "The source-kernel trial element does not match the registered " + "density " + "field."); + MFEM_VERIFY(potential_element.GetOrder() == + mean_field::field::Gravity::Potential::familyOrder, + "The source-kernel test element does not match the registered " + "gravity " + "potential."); + const mean_field::quadrature::Query query = GravityField::make_query< + mean_field::field::Gravity::Form::SourceProjection>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); + + const auto resolution = + f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY( + resolution.integration_rule != nullptr, + "The quadrature policy did not return a gravity-source integration " + "rule."); + return *resolution.integration_rule; +} } // namespace namespace mean_field::operators::kernels { - void apply_mapped_hdiv_mass( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &gravity_gradient_true, - const mfem::Vector &displacement_true, - mfem::Vector &action - ) { - MFEM_VERIFY( - f.gravityFluxFes != nullptr, "The H(div) mass kernel requires the " - "gravity-gradient finite-element space." - ); - MFEM_VERIFY( - f.displacementFes != nullptr, "The H(div) mass kernel requires the " - "displacement finite-element space." - ); - MFEM_VERIFY( - f.compactificationFes != nullptr, "The H(div) mass kernel requires the compactification " - "finite-element " - "space." - ); - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, "The H(div) mass kernel requires the compactification field." - ); - MFEM_VERIFY(f.quadratureFactory != nullptr, "The H(div) mass kernel requires the quadrature rule factory."); - MFEM_VERIFY( - gravity_gradient_true.Size() == f.gravityFluxFes->GetTrueVSize(), - "The gravity-gradient vector has the wrong size." - ); - MFEM_VERIFY( - displacement_true.Size() == f.displacementFes->GetTrueVSize(), "The displacement vector has the wrong size." - ); +void apply_mapped_hdiv_mass(const fem::FEM &f, + const mapping::DomainMapper &domain_mapper, + const mfem::Vector &gravity_gradient_true, + const mfem::Vector &displacement_true, + mfem::Vector &action) { + MFEM_VERIFY(f.gravityFluxFes != nullptr, + "The H(div) mass kernel requires the " + "gravity-gradient finite-element space."); + MFEM_VERIFY(f.displacementFes != nullptr, + "The H(div) mass kernel requires the " + "displacement finite-element space."); + MFEM_VERIFY(f.compactificationFes != nullptr, + "The H(div) mass kernel requires the compactification " + "finite-element " + "space."); + MFEM_VERIFY(f.compactificationCoordinate != nullptr, + "The H(div) mass kernel requires the compactification field."); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "The H(div) mass kernel requires the quadrature rule factory."); + MFEM_VERIFY(gravity_gradient_true.Size() == f.gravityFluxFes->GetTrueVSize(), + "The gravity-gradient vector has the wrong size."); + MFEM_VERIFY(displacement_true.Size() == f.displacementFes->GetTrueVSize(), + "The displacement vector has the wrong size."); - mfem::Vector gravity_gradient_local; - mfem::Vector displacement_local; - true_to_local(*f.gravityFluxFes, gravity_gradient_true, gravity_gradient_local); - true_to_local(*f.displacementFes, displacement_true, displacement_local); + mfem::Vector gravity_gradient_local; + mfem::Vector displacement_local; + true_to_local(*f.gravityFluxFes, gravity_gradient_true, + gravity_gradient_local); + true_to_local(*f.displacementFes, displacement_true, displacement_local); - mfem::Vector local_action(f.gravityFluxFes->GetVSize()); - local_action = 0.0; + mfem::Vector local_action(f.gravityFluxFes->GetVSize()); + local_action = 0.0; - mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - mfem::Array gravity_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + mfem::Array gravity_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - mfem::Vector element_gravity_gradient; - mfem::Vector element_displacement; - mfem::Vector element_compactification; - mfem::Vector element_action; - mfem::Vector gravity_gradient_value; - mfem::Vector mapped_gravity_gradient_value; + mfem::Vector element_gravity_gradient; + mfem::Vector element_displacement; + mfem::Vector element_compactification; + mfem::Vector element_action; + mfem::Vector gravity_gradient_value; + mfem::Vector mapped_gravity_gradient_value; - mfem::DenseMatrix vector_shape; - mfem::DenseMatrix mapped_mass_tensor; + mfem::DenseMatrix vector_shape; + mfem::DenseMatrix mapped_mass_tensor; - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - const mfem::FiniteElement &gravity_element = *f.gravityFluxFes->GetFE(element_id); - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + const mfem::FiniteElement &gravity_element = + *f.gravityFluxFes->GetFE(element_id); + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); - mfem::DofTransformation *gravity_dof_transformation = - f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); - mfem::DofTransformation *displacement_dof_transformation = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_dof_transformation = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); + mfem::DofTransformation *gravity_dof_transformation = + f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); + mfem::DofTransformation *displacement_dof_transformation = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + mfem::DofTransformation *compactification_dof_transformation = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); - gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); - displacement_local.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); + gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); + displacement_local.GetSubVector(displacement_dofs, element_displacement); + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); - if (gravity_dof_transformation != nullptr) - gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); - if (displacement_dof_transformation != nullptr) - displacement_dof_transformation->InvTransformPrimal(element_displacement); - if (compactification_dof_transformation != nullptr) - compactification_dof_transformation->InvTransformPrimal(element_compactification); + if (gravity_dof_transformation != nullptr) + gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); + if (displacement_dof_transformation != nullptr) + displacement_dof_transformation->InvTransformPrimal(element_displacement); + if (compactification_dof_transformation != nullptr) + compactification_dof_transformation->InvTransformPrimal( + element_compactification); - // const mapping::ElementDisplacementData - // displacement_data(displacement_element, element_displacement, - // mfem::Ordering::byVDIM); + // const mapping::ElementDisplacementData + // displacement_data(displacement_element, element_displacement, + // mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); + const mapping::ElementDisplacementData displacement_data = + mapping::ElementDisplacementDataFromElementVDofs(displacement_element, + element_displacement); - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification); + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; - const int gravity_dof_count = gravity_element.GetDof(); - const int dimension = transformation->GetSpaceDim(); + const int gravity_dof_count = gravity_element.GetDof(); + const int dimension = transformation->GetSpaceDim(); - element_action.SetSize(gravity_dof_count); - gravity_gradient_value.SetSize(dimension); - mapped_gravity_gradient_value.SetSize(dimension); - vector_shape.SetSize(gravity_dof_count, dimension); - mapped_mass_tensor.SetSize(dimension); - element_action = 0.0; + element_action.SetSize(gravity_dof_count); + gravity_gradient_value.SetSize(dimension); + mapped_gravity_gradient_value.SetSize(dimension); + vector_shape.SetSize(gravity_dof_count, dimension); + mapped_mass_tensor.SetSize(dimension); + element_action = 0.0; - const mfem::IntegrationRule &integration_rule = - get_hdiv_mass_rule(f, domain_mapper, gravity_element, *transformation); + const mfem::IntegrationRule &integration_rule = + get_hdiv_mass_rule(f, domain_mapper, gravity_element, *transformation); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); - mapping::VolumeMappingContext mapping_context; - const mapping::MappingStatus status = domain_mapper.EvaluateVolume( - mapping_data, *transformation, integration_point, workspace, mapping_context - ); - MFEM_VERIFY( - status == mapping::MappingStatus::valid, - "Stateless mapping failed in the matrix-free H(div) mass " - "kernel. " - "Element: " - << element_id << ", attribute: " << transformation->Attribute << ", quadrature point: " << q - << ", status: " << static_cast(status) - ); + mapping::VolumeMappingContext mapping_context; + const mapping::MappingStatus status = domain_mapper.EvaluateVolume( + mapping_data, *transformation, integration_point, workspace, + mapping_context); + MFEM_VERIFY(status == mapping::MappingStatus::valid, + "Stateless mapping failed in the matrix-free H(div) mass " + "kernel. " + "Element: " + << element_id + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << q + << ", status: " << static_cast(status)); - gravity_element.CalcVShape(*transformation, vector_shape); - mapping::ComputeHDivMassTensor(mapping_context.mapping, mapped_mass_tensor); + gravity_element.CalcVShape(*transformation, vector_shape); + mapping::ComputeHDivMassTensor(mapping_context.mapping, + mapped_mass_tensor); - vector_shape.MultTranspose(element_gravity_gradient, gravity_gradient_value); - mapped_mass_tensor.Mult(gravity_gradient_value, mapped_gravity_gradient_value); + vector_shape.MultTranspose(element_gravity_gradient, + gravity_gradient_value); + mapped_mass_tensor.Mult(gravity_gradient_value, + mapped_gravity_gradient_value); - const double weight = integration_point.weight * transformation->Weight(); + const double weight = integration_point.weight * transformation->Weight(); - for (int i = 0; i < gravity_dof_count; ++i) { - double value = 0.0; - for (int component = 0; component < dimension; ++component) - value += vector_shape(i, component) * mapped_gravity_gradient_value(component); - element_action(i) += weight * value; - } - } - - if (gravity_dof_transformation != nullptr) - gravity_dof_transformation->TransformDual(element_action); - local_action.AddElementVector(gravity_dofs, element_action); - } - - local_to_true(*f.gravityFluxFes, local_action, action); + for (int i = 0; i < gravity_dof_count; ++i) { + double value = 0.0; + for (int component = 0; component < dimension; ++component) + value += vector_shape(i, component) * + mapped_gravity_gradient_value(component); + element_action(i) += weight * value; + } } - void apply_mapped_source( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &density_true, - const mfem::Vector &displacement_true, - mfem::Vector &action - ) { - MFEM_VERIFY( - f.densityFes != nullptr, "The gravity-source kernel requires the " - "density finite-element space." - ); - MFEM_VERIFY( - f.gravityPotentialFes != nullptr, "The gravity-source kernel requires the gravity-potential " - "finite-element space." - ); - MFEM_VERIFY( - f.displacementFes != nullptr, "The gravity-source kernel requires the " - "displacement finite-element space." - ); - MFEM_VERIFY( - f.compactificationFes != nullptr, "The gravity-source kernel requires the compactification " - "finite-element space." - ); - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, "The gravity-source kernel requires the compactification field." - ); - MFEM_VERIFY(f.quadratureFactory != nullptr, "The gravity-source kernel requires the quadrature rule factory."); - MFEM_VERIFY(density_true.Size() == f.densityFes->GetTrueVSize(), "The density vector has the wrong size."); - MFEM_VERIFY( - displacement_true.Size() == f.displacementFes->GetTrueVSize(), "The displacement vector has the wrong size." - ); + if (gravity_dof_transformation != nullptr) + gravity_dof_transformation->TransformDual(element_action); + local_action.AddElementVector(gravity_dofs, element_action); + } - mfem::Vector density_local; - mfem::Vector displacement_local; - true_to_local(*f.densityFes, density_true, density_local); - true_to_local(*f.displacementFes, displacement_true, displacement_local); + local_to_true(*f.gravityFluxFes, local_action, action); +} - mfem::Vector local_action(f.gravityPotentialFes->GetVSize()); - local_action = 0.0; +void apply_mapped_source(const fem::FEM &f, + const mapping::DomainMapper &domain_mapper, + const mfem::Vector &density_true, + const mfem::Vector &displacement_true, + mfem::Vector &action) { + MFEM_VERIFY(f.densityFes != nullptr, "The gravity-source kernel requires the " + "density finite-element space."); + MFEM_VERIFY(f.gravityPotentialFes != nullptr, + "The gravity-source kernel requires the gravity-potential " + "finite-element space."); + MFEM_VERIFY(f.displacementFes != nullptr, + "The gravity-source kernel requires the " + "displacement finite-element space."); + MFEM_VERIFY(f.compactificationFes != nullptr, + "The gravity-source kernel requires the compactification " + "finite-element space."); + MFEM_VERIFY(f.compactificationCoordinate != nullptr, + "The gravity-source kernel requires the compactification field."); + MFEM_VERIFY( + f.quadratureFactory != nullptr, + "The gravity-source kernel requires the quadrature rule factory."); + MFEM_VERIFY(density_true.Size() == f.densityFes->GetTrueVSize(), + "The density vector has the wrong size."); + MFEM_VERIFY(displacement_true.Size() == f.displacementFes->GetTrueVSize(), + "The displacement vector has the wrong size."); - mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + mfem::Vector density_local; + mfem::Vector displacement_local; + true_to_local(*f.densityFes, density_true, density_local); + true_to_local(*f.displacementFes, displacement_true, displacement_local); - mfem::Array density_dofs; - mfem::Array potential_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + mfem::Vector local_action(f.gravityPotentialFes->GetVSize()); + local_action = 0.0; - mfem::Vector element_density; - mfem::Vector element_displacement; - mfem::Vector element_compactification; - mfem::Vector element_action; - mfem::Vector density_shape; - mfem::Vector potential_shape; + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - const int vacuum_attribute = domain_mapper.GetVacuumElementAttribute(); - constexpr double source_scale = 4.0 * M_PI * utils::G; + mfem::Array density_dofs; + mfem::Array potential_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - if (transformation->Attribute == vacuum_attribute) - continue; + mfem::Vector element_density; + mfem::Vector element_displacement; + mfem::Vector element_compactification; + mfem::Vector element_action; + mfem::Vector density_shape; + mfem::Vector potential_shape; - const mfem::FiniteElement &density_element = *f.densityFes->GetFE(element_id); - const mfem::FiniteElement &potential_element = *f.gravityPotentialFes->GetFE(element_id); - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); + constexpr double source_scale = 4.0 * M_PI * utils::G; - mfem::DofTransformation *density_dof_transformation = - f.densityFes->GetElementDofs(element_id, density_dofs); - mfem::DofTransformation *potential_dof_transformation = - f.gravityPotentialFes->GetElementDofs(element_id, potential_dofs); - mfem::DofTransformation *displacement_dof_transformation = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_dof_transformation = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); + if (is_vacuum_attribute(transformation->Attribute)) + continue; - density_local.GetSubVector(density_dofs, element_density); - displacement_local.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); + const mfem::FiniteElement &density_element = + *f.densityFes->GetFE(element_id); + const mfem::FiniteElement &potential_element = + *f.gravityPotentialFes->GetFE(element_id); + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); - if (density_dof_transformation != nullptr) - density_dof_transformation->InvTransformPrimal(element_density); - if (displacement_dof_transformation != nullptr) - displacement_dof_transformation->InvTransformPrimal(element_displacement); - if (compactification_dof_transformation != nullptr) - compactification_dof_transformation->InvTransformPrimal(element_compactification); + mfem::DofTransformation *density_dof_transformation = + f.densityFes->GetElementDofs(element_id, density_dofs); + mfem::DofTransformation *potential_dof_transformation = + f.gravityPotentialFes->GetElementDofs(element_id, potential_dofs); + mfem::DofTransformation *displacement_dof_transformation = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + mfem::DofTransformation *compactification_dof_transformation = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); - const mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; + density_local.GetSubVector(density_dofs, element_density); + displacement_local.GetSubVector(displacement_dofs, element_displacement); + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); - const int density_dof_count = density_element.GetDof(); - const int potential_dof_count = potential_element.GetDof(); + if (density_dof_transformation != nullptr) + density_dof_transformation->InvTransformPrimal(element_density); + if (displacement_dof_transformation != nullptr) + displacement_dof_transformation->InvTransformPrimal(element_displacement); + if (compactification_dof_transformation != nullptr) + compactification_dof_transformation->InvTransformPrimal( + element_compactification); - density_shape.SetSize(density_dof_count); - potential_shape.SetSize(potential_dof_count); - element_action.SetSize(potential_dof_count); - element_action = 0.0; + const mapping::ElementDisplacementData displacement_data = + mapping::ElementDisplacementDataFromElementVDofs(displacement_element, + element_displacement); + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification); + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; - const mfem::IntegrationRule &integration_rule = - get_source_rule(f, density_element, potential_element, *transformation); + const int density_dof_count = density_element.GetDof(); + const int potential_dof_count = potential_element.GetDof(); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); + density_shape.SetSize(density_dof_count); + potential_shape.SetSize(potential_dof_count); + element_action.SetSize(potential_dof_count); + element_action = 0.0; - mapping::VolumeMappingContext mapping_context; - const mapping::MappingStatus status = domain_mapper.EvaluateVolume( - mapping_data, *transformation, integration_point, workspace, mapping_context - ); - MFEM_VERIFY( - status == mapping::MappingStatus::valid, - "Stateless mapping failed in the matrix-free " - "gravity-source " - "kernel. Element: " - << element_id << ", attribute: " << transformation->Attribute << ", quadrature point: " << q - << ", status: " << static_cast(status) - ); + const mfem::IntegrationRule &integration_rule = + get_source_rule(f, density_element, potential_element, *transformation); - density_element.CalcShape(integration_point, density_shape); - potential_element.CalcShape(integration_point, potential_shape); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); - const double density_value = element_density * density_shape; - const double weight = source_scale * density_value * mapping_context.quadrature.weight; + mapping::VolumeMappingContext mapping_context; + const mapping::MappingStatus status = domain_mapper.EvaluateVolume( + mapping_data, *transformation, integration_point, workspace, + mapping_context); + MFEM_VERIFY(status == mapping::MappingStatus::valid, + "Stateless mapping failed in the matrix-free " + "gravity-source " + "kernel. Element: " + << element_id + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << q + << ", status: " << static_cast(status)); - for (int i = 0; i < potential_dof_count; ++i) - element_action(i) += weight * potential_shape(i); - } + density_element.CalcShape(integration_point, density_shape); + potential_element.CalcShape(integration_point, potential_shape); - if (potential_dof_transformation != nullptr) - potential_dof_transformation->TransformDual(element_action); - local_action.AddElementVector(potential_dofs, element_action); - } + const double density_value = element_density * density_shape; + const double weight = + source_scale * density_value * mapping_context.quadrature.weight; - local_to_true(*f.gravityPotentialFes, local_action, action); + for (int i = 0; i < potential_dof_count; ++i) + element_action(i) += weight * potential_shape(i); } - void apply_mapped_hdiv_mass_variation( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &gravity_gradient_true, - const mfem::Vector &displacement_true, - const mfem::Vector &displacement_variation_true, - mfem::Vector &action_variation - ) { - MFEM_VERIFY(f.mesh != nullptr, "The H(div) mass-variation kernel requires a mesh."); - MFEM_VERIFY( - f.gravityFluxFes != nullptr, "The H(div) mass-variation kernel requires the " - "gravity-gradient finite-element space." - ); - MFEM_VERIFY( - f.displacementFes != nullptr, "The H(div) mass-variation kernel requires " - "the displacement finite-element space." - ); - MFEM_VERIFY( - f.compactificationFes != nullptr, "The H(div) mass-variation kernel requires the compactification " - "finite-element space." - ); - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, "The H(div) mass-variation kernel requires the compactification " - "field." - ); - MFEM_VERIFY( - f.quadratureFactory != nullptr, "The H(div) mass-variation kernel requires the quadrature rule " - "factory." - ); - MFEM_VERIFY( - gravity_gradient_true.Size() == f.gravityFluxFes->GetTrueVSize(), - "The gravity-gradient vector has the wrong size." - ); - MFEM_VERIFY( - displacement_true.Size() == f.displacementFes->GetTrueVSize(), "The displacement vector has the wrong size." - ); - MFEM_VERIFY( - displacement_variation_true.Size() == f.displacementFes->GetTrueVSize(), - "The displacement-variation vector has the wrong size." - ); - MFEM_VERIFY( - domain_mapper.GetDimension() == f.mesh->Dimension(), - "The domain-mapper dimension does not match the mesh dimension." - ); + if (potential_dof_transformation != nullptr) + potential_dof_transformation->TransformDual(element_action); + local_action.AddElementVector(potential_dofs, element_action); + } - mfem::Vector gravity_gradient_local; - mfem::Vector displacement_local; - mfem::Vector displacement_variation_local; + local_to_true(*f.gravityPotentialFes, local_action, action); +} - true_to_local(*f.gravityFluxFes, gravity_gradient_true, gravity_gradient_local); - true_to_local(*f.displacementFes, displacement_true, displacement_local); - true_to_local(*f.displacementFes, displacement_variation_true, displacement_variation_local); +void apply_mapped_hdiv_mass_variation( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper, + const mfem::Vector &gravity_gradient_true, + const mfem::Vector &displacement_true, + const mfem::Vector &displacement_variation_true, + mfem::Vector &action_variation) { + MFEM_VERIFY(f.mesh != nullptr, + "The H(div) mass-variation kernel requires a mesh."); + MFEM_VERIFY(f.gravityFluxFes != nullptr, + "The H(div) mass-variation kernel requires the " + "gravity-gradient finite-element space."); + MFEM_VERIFY(f.displacementFes != nullptr, + "The H(div) mass-variation kernel requires " + "the displacement finite-element space."); + MFEM_VERIFY(f.compactificationFes != nullptr, + "The H(div) mass-variation kernel requires the compactification " + "finite-element space."); + MFEM_VERIFY(f.compactificationCoordinate != nullptr, + "The H(div) mass-variation kernel requires the compactification " + "field."); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "The H(div) mass-variation kernel requires the quadrature rule " + "factory."); + MFEM_VERIFY(gravity_gradient_true.Size() == f.gravityFluxFes->GetTrueVSize(), + "The gravity-gradient vector has the wrong size."); + MFEM_VERIFY(displacement_true.Size() == f.displacementFes->GetTrueVSize(), + "The displacement vector has the wrong size."); + MFEM_VERIFY(displacement_variation_true.Size() == + f.displacementFes->GetTrueVSize(), + "The displacement-variation vector has the wrong size."); + MFEM_VERIFY(domain_mapper.GetDimension() == f.mesh->Dimension(), + "The domain-mapper dimension does not match the mesh dimension."); - mfem::Vector local_action(f.gravityFluxFes->GetVSize()); - local_action = 0.0; + mfem::Vector gravity_gradient_local; + mfem::Vector displacement_local; + mfem::Vector displacement_variation_local; - mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + true_to_local(*f.gravityFluxFes, gravity_gradient_true, + gravity_gradient_local); + true_to_local(*f.displacementFes, displacement_true, displacement_local); + true_to_local(*f.displacementFes, displacement_variation_true, + displacement_variation_local); - mfem::Array gravity_gradient_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + mfem::Vector local_action(f.gravityFluxFes->GetVSize()); + local_action = 0.0; - mfem::Vector element_gravity_gradient; - mfem::Vector element_displacement; - mfem::Vector element_displacement_variation; - mfem::Vector element_compactification; - mfem::Vector element_action; - mfem::Vector gravity_gradient_value; - mfem::Vector mass_tensor_variation_action; + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - mfem::DenseMatrix gravity_gradient_shape; - mfem::DenseMatrix mass_tensor_variation; + mfem::Array gravity_gradient_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - const int vacuum_attribute = domain_mapper.GetVacuumElementAttribute(); + mfem::Vector element_gravity_gradient; + mfem::Vector element_displacement; + mfem::Vector element_displacement_variation; + mfem::Vector element_compactification; + mfem::Vector element_action; + mfem::Vector gravity_gradient_value; + mfem::Vector mass_tensor_variation_action; - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - const mfem::FiniteElement &gravity_gradient_element = *f.gravityFluxFes->GetFE(element_id); - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); + mfem::DenseMatrix gravity_gradient_shape; + mfem::DenseMatrix mass_tensor_variation; - MFEM_VERIFY( - transformation != nullptr, "The H(div) mass-variation kernel " - "received a null element transformation." - ); + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + const mfem::FiniteElement &gravity_gradient_element = + *f.gravityFluxFes->GetFE(element_id); + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); - mfem::DofTransformation *gravity_dof_transformation = - f.gravityFluxFes->GetElementVDofs(element_id, gravity_gradient_dofs); - mfem::DofTransformation *displacement_dof_transformation = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_dof_transformation = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); + MFEM_VERIFY(transformation != nullptr, + "The H(div) mass-variation kernel " + "received a null element transformation."); - gravity_gradient_local.GetSubVector(gravity_gradient_dofs, element_gravity_gradient); - displacement_local.GetSubVector(displacement_dofs, element_displacement); - displacement_variation_local.GetSubVector(displacement_dofs, element_displacement_variation); - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); + mfem::DofTransformation *gravity_dof_transformation = + f.gravityFluxFes->GetElementVDofs(element_id, gravity_gradient_dofs); + mfem::DofTransformation *displacement_dof_transformation = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + mfem::DofTransformation *compactification_dof_transformation = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); - if (gravity_dof_transformation != nullptr) - gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); + gravity_gradient_local.GetSubVector(gravity_gradient_dofs, + element_gravity_gradient); + displacement_local.GetSubVector(displacement_dofs, element_displacement); + displacement_variation_local.GetSubVector(displacement_dofs, + element_displacement_variation); + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(element_displacement); - displacement_dof_transformation->InvTransformPrimal(element_displacement_variation); - } + if (gravity_dof_transformation != nullptr) + gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); - if (compactification_dof_transformation != nullptr) - compactification_dof_transformation->InvTransformPrimal(element_compactification); - - const mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); - const mapping::ElementDisplacementData displacement_variation_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement_variation); - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - const int gravity_gradient_dof_count = gravity_gradient_element.GetDof(); - const int dimension = transformation->GetSpaceDim(); - - element_action.SetSize(gravity_gradient_dof_count); - element_action = 0.0; - - gravity_gradient_value.SetSize(dimension); - mass_tensor_variation_action.SetSize(dimension); - - gravity_gradient_shape.SetSize(gravity_gradient_dof_count, dimension); - mass_tensor_variation.SetSize(dimension, dimension); - - const mfem::IntegrationRule &integration_rule = - get_hdiv_mass_rule(f, domain_mapper, gravity_gradient_element, *transformation); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); - - mapping::VolumeMappingContext mapping_context; - const mapping::MappingStatus status = domain_mapper.EvaluateVolume( - mapping_data, *transformation, integration_point, workspace, mapping_context - ); - MFEM_VERIFY( - status == mapping::MappingStatus::valid, - "Stateless mapping failed in the matrix-free H(div) mass " - "kernel. " - "Element: " - << element_id << ", attribute: " << transformation->Attribute << ", quadrature point: " << q - << ", status: " << static_cast(status) - ); - - mapping::VolumeMappingVariation mapping_variation; - const mapping::MappingStatus variation_status = domain_mapper.EvaluateVolumeVariation( - mapping_data, displacement_variation_data, *transformation, integration_point, mapping_context, - workspace, mapping_variation - ); - MFEM_VERIFY( - variation_status == mapping::MappingStatus::valid, - "The mapping variation is invalid while applying the " - "H(div) mass " - "variation." - ); - - mapping::ComputeHDivMassTensorVariation( - mapping_context.mapping, mapping_variation.mapping, mass_tensor_variation - ); - - gravity_gradient_element.CalcVShape(*transformation, gravity_gradient_shape); - gravity_gradient_shape.MultTranspose(element_gravity_gradient, gravity_gradient_value); - mass_tensor_variation.Mult(gravity_gradient_value, mass_tensor_variation_action); - const double reference_weight = integration_point.weight * transformation->Weight(); - - gravity_gradient_shape.AddMult(mass_tensor_variation_action, element_action, reference_weight); - } - - if (gravity_dof_transformation != nullptr) - gravity_dof_transformation->TransformDual(element_action); - local_action.AddElementVector(gravity_gradient_dofs, element_action); - } - - action_variation.SetSize(f.gravityFluxFes->GetTrueVSize()); - action_variation = 0.0; - add_local_to_true(*f.gravityFluxFes, local_action, action_variation); + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal(element_displacement); + displacement_dof_transformation->InvTransformPrimal( + element_displacement_variation); } - void apply_mapped_source_variation( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &density_true, - const mfem::Vector &displacement_true, - const mfem::Vector &displacement_variation_true, - mfem::Vector &action_variation - ) { - MFEM_VERIFY(f.mesh != nullptr, "The source-variation kernel requires a mesh."); - MFEM_VERIFY( - f.densityFes != nullptr, "The source-variation kernel requires the density finite-element " - "space." - ); - MFEM_VERIFY( - f.gravityPotentialFes != nullptr, "The source-variation kernel requires the gravity-potential " - "finite-element space." - ); - MFEM_VERIFY( - f.displacementFes != nullptr, "The source-variation kernel requires the " - "displacement finite-element space." - ); - MFEM_VERIFY( - f.compactificationFes != nullptr, "The source-variation kernel requires the compactification " - "finite-element space." - ); - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, "The source-variation kernel requires the compactification field." - ); - MFEM_VERIFY( - f.quadratureFactory != nullptr, "The source-variation kernel requires the quadrature rule factory." - ); - MFEM_VERIFY(density_true.Size() == f.densityFes->GetTrueVSize(), "The density vector has the wrong size."); - MFEM_VERIFY( - displacement_true.Size() == f.displacementFes->GetTrueVSize(), "The displacement vector has the wrong size." - ); - MFEM_VERIFY( - displacement_variation_true.Size() == f.displacementFes->GetTrueVSize(), - "The displacement-variation vector has the wrong size." - ); - MFEM_VERIFY( - domain_mapper.GetDimension() == f.mesh->Dimension(), - "The domain-mapper dimension does not match the mesh dimension." - ); + if (compactification_dof_transformation != nullptr) + compactification_dof_transformation->InvTransformPrimal( + element_compactification); - mfem::Vector density_local; - mfem::Vector displacement_local; - mfem::Vector displacement_variation_local; + const mapping::ElementDisplacementData displacement_data = + mapping::ElementDisplacementDataFromElementVDofs(displacement_element, + element_displacement); + const mapping::ElementDisplacementData displacement_variation_data = + mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement_variation); + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification); + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; - true_to_local(*f.densityFes, density_true, density_local); - true_to_local(*f.displacementFes, displacement_true, displacement_local); - true_to_local(*f.displacementFes, displacement_variation_true, displacement_variation_local); + const int gravity_gradient_dof_count = gravity_gradient_element.GetDof(); + const int dimension = transformation->GetSpaceDim(); - mfem::Vector local_action(f.gravityPotentialFes->GetVSize()); - local_action = 0.0; + element_action.SetSize(gravity_gradient_dof_count); + element_action = 0.0; - mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + gravity_gradient_value.SetSize(dimension); + mass_tensor_variation_action.SetSize(dimension); - mfem::Array density_dofs; - mfem::Array potential_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + gravity_gradient_shape.SetSize(gravity_gradient_dof_count, dimension); + mass_tensor_variation.SetSize(dimension, dimension); - mfem::Vector element_density; - mfem::Vector element_displacement; - mfem::Vector element_displacement_variation; - mfem::Vector element_compactification; - mfem::Vector element_action; - mfem::Vector density_shape; - mfem::Vector potential_shape; + const mfem::IntegrationRule &integration_rule = get_hdiv_mass_rule( + f, domain_mapper, gravity_gradient_element, *transformation); - mapping::VolumeMappingContext mapping_context; - mapping::VolumeMappingVariation mapping_variation; + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); - const int vacuum_attribute = domain_mapper.GetVacuumElementAttribute(); - constexpr double gravitational_source_scale = 4.0 * M_PI * utils::G; + mapping::VolumeMappingContext mapping_context; + const mapping::MappingStatus status = domain_mapper.EvaluateVolume( + mapping_data, *transformation, integration_point, workspace, + mapping_context); + MFEM_VERIFY(status == mapping::MappingStatus::valid, + "Stateless mapping failed in the matrix-free H(div) mass " + "kernel. " + "Element: " + << element_id + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << q + << ", status: " << static_cast(status)); - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - MFEM_VERIFY( - transformation != nullptr, "The source-variation kernel received a null element " - "transformation." - ); + mapping::VolumeMappingVariation mapping_variation; + const mapping::MappingStatus variation_status = + domain_mapper.EvaluateVolumeVariation( + mapping_data, displacement_variation_data, *transformation, + integration_point, mapping_context, workspace, mapping_variation); + MFEM_VERIFY(variation_status == mapping::MappingStatus::valid, + "The mapping variation is invalid while applying the " + "H(div) mass " + "variation."); - if (transformation->Attribute == vacuum_attribute) - continue; + mapping::ComputeHDivMassTensorVariation(mapping_context.mapping, + mapping_variation.mapping, + mass_tensor_variation); - const mfem::FiniteElement &density_element = *f.densityFes->GetFE(element_id); - const mfem::FiniteElement &potential_element = *f.gravityPotentialFes->GetFE(element_id); - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); + gravity_gradient_element.CalcVShape(*transformation, + gravity_gradient_shape); + gravity_gradient_shape.MultTranspose(element_gravity_gradient, + gravity_gradient_value); + mass_tensor_variation.Mult(gravity_gradient_value, + mass_tensor_variation_action); + const double reference_weight = + integration_point.weight * transformation->Weight(); - mfem::DofTransformation *density_dof_transformation = - f.densityFes->GetElementDofs(element_id, density_dofs); - mfem::DofTransformation *potential_dof_transformation = - f.gravityPotentialFes->GetElementDofs(element_id, potential_dofs); - mfem::DofTransformation *displacement_dof_transformation = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_dof_transformation = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); - - density_local.GetSubVector(density_dofs, element_density); - displacement_local.GetSubVector(displacement_dofs, element_displacement); - displacement_variation_local.GetSubVector(displacement_dofs, element_displacement_variation); - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - - if (density_dof_transformation != nullptr) - density_dof_transformation->InvTransformPrimal(element_density); - - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(element_displacement); - displacement_dof_transformation->InvTransformPrimal(element_displacement_variation); - } - - if (compactification_dof_transformation != nullptr) - compactification_dof_transformation->InvTransformPrimal(element_compactification); - - const mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); - const mapping::ElementDisplacementData displacement_variation_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement_variation); - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - element_action.SetSize(potential_element.GetDof()); - element_action = 0.0; - - density_shape.SetSize(density_element.GetDof()); - potential_shape.SetSize(potential_element.GetDof()); - - const mfem::IntegrationRule &integration_rule = - get_source_rule(f, density_element, potential_element, *transformation); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); - - const mapping::MappingStatus mapping_status = domain_mapper.EvaluateVolume( - mapping_data, *transformation, integration_point, workspace, mapping_context - ); - MFEM_VERIFY( - mapping_status == mapping::MappingStatus::valid, - "The base mapping is invalid while applying the source " - "variation." - ); - - const mapping::MappingStatus variation_status = domain_mapper.EvaluateVolumeVariation( - mapping_data, displacement_variation_data, *transformation, integration_point, mapping_context, - workspace, mapping_variation - ); - - MFEM_VERIFY( - variation_status == mapping::MappingStatus::valid, - "The mapping variation is invalid while applying the " - "source " - "variation." - ); - - density_element.CalcShape(integration_point, density_shape); - potential_element.CalcShape(integration_point, potential_shape); - - const double density_value = density_shape * element_density; - const double source_variation_value = - gravitational_source_scale * density_value * mapping_variation.weight_variation; - - element_action.Add(source_variation_value, potential_shape); - } - - if (potential_dof_transformation != nullptr) - potential_dof_transformation->TransformDual(element_action); - local_action.AddElementVector(potential_dofs, element_action); - } - - action_variation.SetSize(f.gravityPotentialFes->GetTrueVSize()); - action_variation = 0.0; - add_local_to_true(*f.gravityPotentialFes, local_action, action_variation); + gravity_gradient_shape.AddMult(mass_tensor_variation_action, + element_action, reference_weight); } + + if (gravity_dof_transformation != nullptr) + gravity_dof_transformation->TransformDual(element_action); + local_action.AddElementVector(gravity_gradient_dofs, element_action); + } + + action_variation.SetSize(f.gravityFluxFes->GetTrueVSize()); + action_variation = 0.0; + add_local_to_true(*f.gravityFluxFes, local_action, action_variation); +} + +void apply_mapped_source_variation( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper, + const mfem::Vector &density_true, const mfem::Vector &displacement_true, + const mfem::Vector &displacement_variation_true, + mfem::Vector &action_variation) { + MFEM_VERIFY(f.mesh != nullptr, + "The source-variation kernel requires a mesh."); + MFEM_VERIFY(f.densityFes != nullptr, + "The source-variation kernel requires the density finite-element " + "space."); + MFEM_VERIFY(f.gravityPotentialFes != nullptr, + "The source-variation kernel requires the gravity-potential " + "finite-element space."); + MFEM_VERIFY(f.displacementFes != nullptr, + "The source-variation kernel requires the " + "displacement finite-element space."); + MFEM_VERIFY(f.compactificationFes != nullptr, + "The source-variation kernel requires the compactification " + "finite-element space."); + MFEM_VERIFY( + f.compactificationCoordinate != nullptr, + "The source-variation kernel requires the compactification field."); + MFEM_VERIFY( + f.quadratureFactory != nullptr, + "The source-variation kernel requires the quadrature rule factory."); + MFEM_VERIFY(density_true.Size() == f.densityFes->GetTrueVSize(), + "The density vector has the wrong size."); + MFEM_VERIFY(displacement_true.Size() == f.displacementFes->GetTrueVSize(), + "The displacement vector has the wrong size."); + MFEM_VERIFY(displacement_variation_true.Size() == + f.displacementFes->GetTrueVSize(), + "The displacement-variation vector has the wrong size."); + MFEM_VERIFY(domain_mapper.GetDimension() == f.mesh->Dimension(), + "The domain-mapper dimension does not match the mesh dimension."); + + mfem::Vector density_local; + mfem::Vector displacement_local; + mfem::Vector displacement_variation_local; + + true_to_local(*f.densityFes, density_true, density_local); + true_to_local(*f.displacementFes, displacement_true, displacement_local); + true_to_local(*f.displacementFes, displacement_variation_true, + displacement_variation_local); + + mfem::Vector local_action(f.gravityPotentialFes->GetVSize()); + local_action = 0.0; + + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); + + mfem::Array density_dofs; + mfem::Array potential_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; + + mfem::Vector element_density; + mfem::Vector element_displacement; + mfem::Vector element_displacement_variation; + mfem::Vector element_compactification; + mfem::Vector element_action; + mfem::Vector density_shape; + mfem::Vector potential_shape; + + mapping::VolumeMappingContext mapping_context; + mapping::VolumeMappingVariation mapping_variation; + + constexpr double gravitational_source_scale = 4.0 * M_PI * utils::G; + + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); + MFEM_VERIFY(transformation != nullptr, + "The source-variation kernel received a null element " + "transformation."); + + if (is_vacuum_attribute(transformation->Attribute)) + continue; + + const mfem::FiniteElement &density_element = + *f.densityFes->GetFE(element_id); + const mfem::FiniteElement &potential_element = + *f.gravityPotentialFes->GetFE(element_id); + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); + + mfem::DofTransformation *density_dof_transformation = + f.densityFes->GetElementDofs(element_id, density_dofs); + mfem::DofTransformation *potential_dof_transformation = + f.gravityPotentialFes->GetElementDofs(element_id, potential_dofs); + mfem::DofTransformation *displacement_dof_transformation = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + mfem::DofTransformation *compactification_dof_transformation = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); + + density_local.GetSubVector(density_dofs, element_density); + displacement_local.GetSubVector(displacement_dofs, element_displacement); + displacement_variation_local.GetSubVector(displacement_dofs, + element_displacement_variation); + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); + + if (density_dof_transformation != nullptr) + density_dof_transformation->InvTransformPrimal(element_density); + + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal(element_displacement); + displacement_dof_transformation->InvTransformPrimal( + element_displacement_variation); + } + + if (compactification_dof_transformation != nullptr) + compactification_dof_transformation->InvTransformPrimal( + element_compactification); + + const mapping::ElementDisplacementData displacement_data = + mapping::ElementDisplacementDataFromElementVDofs(displacement_element, + element_displacement); + const mapping::ElementDisplacementData displacement_variation_data = + mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement_variation); + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification); + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; + + element_action.SetSize(potential_element.GetDof()); + element_action = 0.0; + + density_shape.SetSize(density_element.GetDof()); + potential_shape.SetSize(potential_element.GetDof()); + + const mfem::IntegrationRule &integration_rule = + get_source_rule(f, density_element, potential_element, *transformation); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); + + const mapping::MappingStatus mapping_status = + domain_mapper.EvaluateVolume(mapping_data, *transformation, + integration_point, workspace, + mapping_context); + MFEM_VERIFY(mapping_status == mapping::MappingStatus::valid, + "The base mapping is invalid while applying the source " + "variation."); + + const mapping::MappingStatus variation_status = + domain_mapper.EvaluateVolumeVariation( + mapping_data, displacement_variation_data, *transformation, + integration_point, mapping_context, workspace, mapping_variation); + + MFEM_VERIFY(variation_status == mapping::MappingStatus::valid, + "The mapping variation is invalid while applying the " + "source " + "variation."); + + density_element.CalcShape(integration_point, density_shape); + potential_element.CalcShape(integration_point, potential_shape); + + const double density_value = density_shape * element_density; + const double source_variation_value = gravitational_source_scale * + density_value * + mapping_variation.weight_variation; + + element_action.Add(source_variation_value, potential_shape); + } + + if (potential_dof_transformation != nullptr) + potential_dof_transformation->TransformDual(element_action); + local_action.AddElementVector(potential_dofs, element_action); + } + + action_variation.SetSize(f.gravityPotentialFes->GetTrueVSize()); + action_variation = 0.0; + add_local_to_true(*f.gravityPotentialFes, local_action, action_variation); +} } // namespace mean_field::operators::kernels diff --git a/libmeanfield/impl/operators/kernels/hydrostatic_equilibrium_kernels.cpp b/libmeanfield/impl/operators/kernels/hydrostatic_equilibrium_kernels.cpp index 531e508..993e4d7 100644 --- a/libmeanfield/impl/operators/kernels/hydrostatic_equilibrium_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/hydrostatic_equilibrium_kernels.cpp @@ -11,614 +11,603 @@ module mean_field; import :operators.kernels.hydrostatic_equilibrium; namespace { - void true_to_local( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &trueVector, - mfem::Vector &localVector - ) { - MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), "True vector has the wrong size."); +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - localVector.SetSize(finiteElementSpace.GetVSize()); +[[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(attribute); +} - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void true_to_local(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, mfem::Vector &localVector) { + MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), + "True vector has the wrong size."); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } + localVector.SetSize(finiteElementSpace.GetVSize()); + + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); + + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } +} + +void local_to_true(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, mfem::Vector &trueVector) { + MFEM_VERIFY(localVector.Size() == finiteElementSpace.GetVSize(), + "Local vector has the wrong size."); + + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + + trueVector = 0.0; + + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); + + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } +} + +void validate_fem( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper) { + MFEM_VERIFY(f.mesh != nullptr, "The hydrostatic kernel requires a mesh."); + + MFEM_VERIFY(f.enthalpyFes != nullptr, "The hydrostatic kernel requires the " + "enthalpy finite-element space."); + + MFEM_VERIFY(f.gravityPotentialFes != nullptr, + "The hydrostatic kernel requires the " + "gravity-potential finite-element space."); + + MFEM_VERIFY(f.displacementFes != nullptr, + "The hydrostatic kernel requires the " + "displacement finite-element space."); + + MFEM_VERIFY(f.compactificationFes != nullptr, + "The hydrostatic kernel requires the " + "compactification finite-element space."); + + MFEM_VERIFY(f.compactificationCoordinate != nullptr, + "The hydrostatic kernel requires the " + "compactification coordinate."); + + MFEM_VERIFY(f.quadratureFactory != nullptr, + "The hydrostatic kernel requires the " + "quadrature-rule factory."); + + MFEM_VERIFY(f.mesh->Dimension() == 3, + "The rigid-rotation hydrostatic kernel " + "currently requires a three-dimensional mesh."); + + MFEM_VERIFY(domainMapper.GetDimension() == f.mesh->Dimension(), + "The domain-mapper dimension does not match " + "the mesh dimension."); +} + +const mfem::IntegrationRule & +get_hydrostatic_rule(const mean_field::fem::FEM &f, + const mfem::FiniteElement &enthalpyElement, + const mfem::FiniteElement &potentialElement, + const mfem::ElementTransformation &transformation) { + using EnthalpyField = mean_field::field::Field; + + MFEM_VERIFY(enthalpyElement.GetOrder() == + mean_field::field::Enthalpy::Scalar::familyOrder, + "The hydrostatic test element does not match " + "the registered enthalpy field."); + + MFEM_VERIFY(potentialElement.GetOrder() == + mean_field::field::Gravity::Potential::familyOrder, + "The hydrostatic potential element does not " + "match the registered gravity-potential field."); + + const auto enthalpyQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumEnthalpy>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); + + const auto gravityQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumGravity>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); + + const auto rotationQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumRotation>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), std::array{2}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); + + const auto constantQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumConstant>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); + + int integrationOrder = 0; + + const auto update_order = [&f, &transformation, &integrationOrder]( + const mean_field::quadrature::Query &query) { + const auto rule = + f.quadratureFactory->get(query, transformation.GetGeometryType()); + + MFEM_VERIFY(rule.integration_rule != nullptr, + "The quadrature policy did not return " + "a hydrostatic-equilibrium rule."); + + integrationOrder = std::max(integrationOrder, rule.resolution.order); + }; + + update_order(enthalpyQuery); + update_order(gravityQuery); + update_order(rotationQuery); + update_order(constantQuery); + + return mfem::IntRules.Get(transformation.GetGeometryType(), integrationOrder); +} + +struct HydrostaticAssemblyRequest { + const mean_field::physics::RigidRotation *rotation{nullptr}; + + const mfem::Vector *baseEnthalpyTrue{nullptr}; + const mfem::Vector *basePotentialTrue{nullptr}; + + const mfem::Vector *enthalpyVariationTrue{nullptr}; + const mfem::Vector *potentialVariationTrue{nullptr}; + const mfem::Vector *displacementVariationTrue{nullptr}; + + double bernoulliConstant{0.0}; + double constantVariation{0.0}; + + bool buildResidual{false}; +}; + +void assemble_hydrostatic_form( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mfem::Vector &displacementTrue, + const HydrostaticAssemblyRequest &request, mfem::Vector &result) { + validate_fem(f, domainMapper); + + MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The hydrostatic displacement vector has " + "the wrong size."); + + MFEM_VERIFY(std::isfinite(request.bernoulliConstant), + "The Bernoulli constant is non-finite."); + + MFEM_VERIFY(std::isfinite(request.constantVariation), + "The Bernoulli-constant variation is non-finite."); + + const bool requiresBaseState = + request.buildResidual || request.displacementVariationTrue != nullptr; + + if (requiresBaseState) { + MFEM_VERIFY(request.rotation != nullptr, + "The hydrostatic residual or geometry " + "action requires the rotation model."); + + MFEM_VERIFY(request.baseEnthalpyTrue != nullptr, + "The hydrostatic residual or geometry " + "action requires the base enthalpy."); + + MFEM_VERIFY(request.basePotentialTrue != nullptr, + "The hydrostatic residual or geometry " + "action requires the base potential."); + } + + if (request.baseEnthalpyTrue != nullptr) { + MFEM_VERIFY(request.baseEnthalpyTrue->Size() == + f.enthalpyFes->GetTrueVSize(), + "The base enthalpy vector has the wrong size."); + } + + if (request.basePotentialTrue != nullptr) { + MFEM_VERIFY(request.basePotentialTrue->Size() == + f.gravityPotentialFes->GetTrueVSize(), + "The base potential vector has the wrong size."); + } + + if (request.enthalpyVariationTrue != nullptr) { + MFEM_VERIFY(request.enthalpyVariationTrue->Size() == + f.enthalpyFes->GetTrueVSize(), + "The enthalpy variation has the wrong size."); + } + + if (request.potentialVariationTrue != nullptr) { + MFEM_VERIFY(request.potentialVariationTrue->Size() == + f.gravityPotentialFes->GetTrueVSize(), + "The potential variation has the wrong size."); + } + + if (request.displacementVariationTrue != nullptr) { + MFEM_VERIFY(request.displacementVariationTrue->Size() == + f.displacementFes->GetTrueVSize(), + "The displacement variation has the wrong size."); + } + + mfem::Vector displacementLocal; + true_to_local(*f.displacementFes, displacementTrue, displacementLocal); + + mfem::Vector baseEnthalpyLocal; + mfem::Vector basePotentialLocal; + mfem::Vector enthalpyVariationLocal; + mfem::Vector potentialVariationLocal; + mfem::Vector displacementVariationLocal; + + if (request.baseEnthalpyTrue != nullptr) { + true_to_local(*f.enthalpyFes, *request.baseEnthalpyTrue, baseEnthalpyLocal); + } + + if (request.basePotentialTrue != nullptr) { + true_to_local(*f.gravityPotentialFes, *request.basePotentialTrue, + basePotentialLocal); + } + + if (request.enthalpyVariationTrue != nullptr) { + true_to_local(*f.enthalpyFes, *request.enthalpyVariationTrue, + enthalpyVariationLocal); + } + + if (request.potentialVariationTrue != nullptr) { + true_to_local(*f.gravityPotentialFes, *request.potentialVariationTrue, + potentialVariationLocal); + } + + if (request.displacementVariationTrue != nullptr) { + true_to_local(*f.displacementFes, *request.displacementVariationTrue, + displacementVariationLocal); + } + + mfem::Vector localResult(f.enthalpyFes->GetVSize()); + + localResult = 0.0; + + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); + + mfem::Array enthalpyDofs; + mfem::Array potentialDofs; + mfem::Array displacementDofs; + mfem::Array compactificationDofs; + + mfem::Vector elementBaseEnthalpy; + mfem::Vector elementBasePotential; + mfem::Vector elementEnthalpyVariation; + mfem::Vector elementPotentialVariation; + mfem::Vector elementDisplacement; + mfem::Vector elementDisplacementVariation; + mfem::Vector elementCompactification; + mfem::Vector elementResult; + + mfem::Vector enthalpyShape; + mfem::Vector potentialShape; + + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elementId); + + MFEM_VERIFY(transformation != nullptr, + "The hydrostatic kernel received a null " + "element transformation."); + + if (is_vacuum_attribute(transformation->Attribute)) { + continue; } - void local_to_true( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &localVector, - mfem::Vector &trueVector - ) { - MFEM_VERIFY(localVector.Size() == finiteElementSpace.GetVSize(), "Local vector has the wrong size."); + const mfem::FiniteElement &enthalpyElement = + *f.enthalpyFes->GetFE(elementId); - trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + const mfem::FiniteElement &potentialElement = + *f.gravityPotentialFes->GetFE(elementId); - trueVector = 0.0; + const mfem::FiniteElement &displacementElement = + *f.displacementFes->GetFE(elementId); - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + const mfem::FiniteElement &compactificationElement = + *f.compactificationFes->GetFE(elementId); - if (prolongation != nullptr) { - prolongation->MultTranspose(localVector, trueVector); - } else { - trueVector = localVector; - } + mfem::DofTransformation *enthalpyDofTransformation = + f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); + + mfem::DofTransformation *potentialDofTransformation = + f.gravityPotentialFes->GetElementDofs(elementId, potentialDofs); + + mfem::DofTransformation *displacementDofTransformation = + f.displacementFes->GetElementVDofs(elementId, displacementDofs); + + mfem::DofTransformation *compactificationDofTransformation = + f.compactificationFes->GetElementDofs(elementId, compactificationDofs); + + displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + + f.compactificationCoordinate->GetSubVector(compactificationDofs, + elementCompactification); + + if (request.baseEnthalpyTrue != nullptr) { + baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy); } - void validate_fem( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper - ) { - MFEM_VERIFY(f.mesh != nullptr, "The hydrostatic kernel requires a mesh."); - - MFEM_VERIFY( - f.enthalpyFes != nullptr, "The hydrostatic kernel requires the " - "enthalpy finite-element space." - ); - - MFEM_VERIFY( - f.gravityPotentialFes != nullptr, "The hydrostatic kernel requires the " - "gravity-potential finite-element space." - ); - - MFEM_VERIFY( - f.displacementFes != nullptr, "The hydrostatic kernel requires the " - "displacement finite-element space." - ); - - MFEM_VERIFY( - f.compactificationFes != nullptr, "The hydrostatic kernel requires the " - "compactification finite-element space." - ); - - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, "The hydrostatic kernel requires the " - "compactification coordinate." - ); - - MFEM_VERIFY( - f.quadratureFactory != nullptr, "The hydrostatic kernel requires the " - "quadrature-rule factory." - ); - - MFEM_VERIFY( - f.mesh->Dimension() == 3, "The rigid-rotation hydrostatic kernel " - "currently requires a three-dimensional mesh." - ); - - MFEM_VERIFY( - domainMapper.GetDimension() == f.mesh->Dimension(), "The domain-mapper dimension does not match " - "the mesh dimension." - ); + if (request.basePotentialTrue != nullptr) { + basePotentialLocal.GetSubVector(potentialDofs, elementBasePotential); } - const mfem::IntegrationRule &get_hydrostatic_rule( - const mean_field::fem::FEM &f, - const mfem::FiniteElement &enthalpyElement, - const mfem::FiniteElement &potentialElement, - const mfem::ElementTransformation &transformation - ) { - using EnthalpyField = mean_field::field::Field; - - MFEM_VERIFY( - enthalpyElement.GetOrder() == mean_field::field::Enthalpy::Scalar::familyOrder, - "The hydrostatic test element does not match " - "the registered enthalpy field." - ); - - MFEM_VERIFY( - potentialElement.GetOrder() == mean_field::field::Gravity::Potential::familyOrder, - "The hydrostatic potential element does not " - "match the registered gravity-potential field." - ); - - const auto enthalpyQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); - - const auto gravityQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); - - const auto rotationQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array{2}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); - - const auto constantQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); - - int integrationOrder = 0; - - const auto update_order = [&f, &transformation, &integrationOrder](const mean_field::quadrature::Query &query) { - const auto rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); - - MFEM_VERIFY( - rule.integration_rule != nullptr, "The quadrature policy did not return " - "a hydrostatic-equilibrium rule." - ); - - integrationOrder = std::max(integrationOrder, rule.resolution.order); - }; - - update_order(enthalpyQuery); - update_order(gravityQuery); - update_order(rotationQuery); - update_order(constantQuery); - - return mfem::IntRules.Get(transformation.GetGeometryType(), integrationOrder); + if (request.enthalpyVariationTrue != nullptr) { + enthalpyVariationLocal.GetSubVector(enthalpyDofs, + elementEnthalpyVariation); } - struct HydrostaticAssemblyRequest { - const mean_field::physics::RigidRotation *rotation{nullptr}; - - const mfem::Vector *baseEnthalpyTrue{nullptr}; - const mfem::Vector *basePotentialTrue{nullptr}; - - const mfem::Vector *enthalpyVariationTrue{nullptr}; - const mfem::Vector *potentialVariationTrue{nullptr}; - const mfem::Vector *displacementVariationTrue{nullptr}; - - double bernoulliConstant{0.0}; - double constantVariation{0.0}; - - bool buildResidual{false}; - }; - - void assemble_hydrostatic_form( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &displacementTrue, - const HydrostaticAssemblyRequest &request, - mfem::Vector &result - ) { - validate_fem(f, domainMapper); - - MFEM_VERIFY( - displacementTrue.Size() == f.displacementFes->GetTrueVSize(), "The hydrostatic displacement vector has " - "the wrong size." - ); - - MFEM_VERIFY(std::isfinite(request.bernoulliConstant), "The Bernoulli constant is non-finite."); - - MFEM_VERIFY(std::isfinite(request.constantVariation), "The Bernoulli-constant variation is non-finite."); - - const bool requiresBaseState = request.buildResidual || request.displacementVariationTrue != nullptr; - - if (requiresBaseState) { - MFEM_VERIFY( - request.rotation != nullptr, "The hydrostatic residual or geometry " - "action requires the rotation model." - ); - - MFEM_VERIFY( - request.baseEnthalpyTrue != nullptr, "The hydrostatic residual or geometry " - "action requires the base enthalpy." - ); - - MFEM_VERIFY( - request.basePotentialTrue != nullptr, "The hydrostatic residual or geometry " - "action requires the base potential." - ); - } - - if (request.baseEnthalpyTrue != nullptr) { - MFEM_VERIFY( - request.baseEnthalpyTrue->Size() == f.enthalpyFes->GetTrueVSize(), - "The base enthalpy vector has the wrong size." - ); - } - - if (request.basePotentialTrue != nullptr) { - MFEM_VERIFY( - request.basePotentialTrue->Size() == f.gravityPotentialFes->GetTrueVSize(), - "The base potential vector has the wrong size." - ); - } - - if (request.enthalpyVariationTrue != nullptr) { - MFEM_VERIFY( - request.enthalpyVariationTrue->Size() == f.enthalpyFes->GetTrueVSize(), - "The enthalpy variation has the wrong size." - ); - } - - if (request.potentialVariationTrue != nullptr) { - MFEM_VERIFY( - request.potentialVariationTrue->Size() == f.gravityPotentialFes->GetTrueVSize(), - "The potential variation has the wrong size." - ); - } - - if (request.displacementVariationTrue != nullptr) { - MFEM_VERIFY( - request.displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(), - "The displacement variation has the wrong size." - ); - } - - mfem::Vector displacementLocal; - true_to_local(*f.displacementFes, displacementTrue, displacementLocal); - - mfem::Vector baseEnthalpyLocal; - mfem::Vector basePotentialLocal; - mfem::Vector enthalpyVariationLocal; - mfem::Vector potentialVariationLocal; - mfem::Vector displacementVariationLocal; - - if (request.baseEnthalpyTrue != nullptr) { - true_to_local(*f.enthalpyFes, *request.baseEnthalpyTrue, baseEnthalpyLocal); - } - - if (request.basePotentialTrue != nullptr) { - true_to_local(*f.gravityPotentialFes, *request.basePotentialTrue, basePotentialLocal); - } - - if (request.enthalpyVariationTrue != nullptr) { - true_to_local(*f.enthalpyFes, *request.enthalpyVariationTrue, enthalpyVariationLocal); - } - - if (request.potentialVariationTrue != nullptr) { - true_to_local(*f.gravityPotentialFes, *request.potentialVariationTrue, potentialVariationLocal); - } - - if (request.displacementVariationTrue != nullptr) { - true_to_local(*f.displacementFes, *request.displacementVariationTrue, displacementVariationLocal); - } - - mfem::Vector localResult(f.enthalpyFes->GetVSize()); - - localResult = 0.0; - - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); - - mfem::Array enthalpyDofs; - mfem::Array potentialDofs; - mfem::Array displacementDofs; - mfem::Array compactificationDofs; - - mfem::Vector elementBaseEnthalpy; - mfem::Vector elementBasePotential; - mfem::Vector elementEnthalpyVariation; - mfem::Vector elementPotentialVariation; - mfem::Vector elementDisplacement; - mfem::Vector elementDisplacementVariation; - mfem::Vector elementCompactification; - mfem::Vector elementResult; - - mfem::Vector enthalpyShape; - mfem::Vector potentialShape; - - const int vacuumAttribute = domainMapper.GetVacuumElementAttribute(); - - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); - - MFEM_VERIFY( - transformation != nullptr, "The hydrostatic kernel received a null " - "element transformation." - ); - - if (transformation->Attribute == vacuumAttribute) { - continue; - } - - const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId); - - const mfem::FiniteElement &potentialElement = *f.gravityPotentialFes->GetFE(elementId); - - const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId); - - const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId); - - mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); - - mfem::DofTransformation *potentialDofTransformation = - f.gravityPotentialFes->GetElementDofs(elementId, potentialDofs); - - mfem::DofTransformation *displacementDofTransformation = - f.displacementFes->GetElementVDofs(elementId, displacementDofs); - - mfem::DofTransformation *compactificationDofTransformation = - f.compactificationFes->GetElementDofs(elementId, compactificationDofs); - - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); - - f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); - - if (request.baseEnthalpyTrue != nullptr) { - baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy); - } - - if (request.basePotentialTrue != nullptr) { - basePotentialLocal.GetSubVector(potentialDofs, elementBasePotential); - } - - if (request.enthalpyVariationTrue != nullptr) { - enthalpyVariationLocal.GetSubVector(enthalpyDofs, elementEnthalpyVariation); - } - - if (request.potentialVariationTrue != nullptr) { - potentialVariationLocal.GetSubVector(potentialDofs, elementPotentialVariation); - } - - if (request.displacementVariationTrue != nullptr) { - displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation); - } - - if (enthalpyDofTransformation != nullptr) { - if (request.baseEnthalpyTrue != nullptr) { - enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy); - } - - if (request.enthalpyVariationTrue != nullptr) { - enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation); - } - } - - if (potentialDofTransformation != nullptr) { - if (request.basePotentialTrue != nullptr) { - potentialDofTransformation->InvTransformPrimal(elementBasePotential); - } - - if (request.potentialVariationTrue != nullptr) { - potentialDofTransformation->InvTransformPrimal(elementPotentialVariation); - } - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacement); - - if (request.displacementVariationTrue != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation); - } - } - - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal(elementCompactification); - } - - const mean_field::mapping::ElementDisplacementData displacementData = - mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement); - - const mean_field::mapping::ElementCompactificationData compactificationData( - compactificationElement, elementCompactification - ); - - const mean_field::mapping::ElementMappingData mappingData{ - .displacement = displacementData, .compactification = compactificationData - }; - - std::optional displacementVariationData; - - if (request.displacementVariationTrue != nullptr) { - displacementVariationData.emplace( - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacementElement, elementDisplacementVariation - ) - ); - } - - elementResult.SetSize(enthalpyElement.GetDof()); - - elementResult = 0.0; - - enthalpyShape.SetSize(enthalpyElement.GetDof()); - - potentialShape.SetSize(potentialElement.GetDof()); - - const mfem::IntegrationRule &integrationRule = - get_hydrostatic_rule(f, enthalpyElement, potentialElement, *transformation); - - for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) { - const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadraturePoint); - - transformation->SetIntPoint(&integrationPoint); - - mean_field::mapping::VolumeMappingContext mappingContext; - - const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume( - mappingData, *transformation, integrationPoint, workspace, mappingContext - ); - - MFEM_VERIFY( - mappingStatus == mean_field::mapping::MappingStatus::valid, - "The base mapping is invalid in the " - "hydrostatic kernel. Element: " - << elementId << ", quadrature point: " << quadraturePoint - << ", status: " << static_cast(mappingStatus) - ); - - enthalpyElement.CalcShape(integrationPoint, enthalpyShape); - - potentialElement.CalcShape(integrationPoint, potentialShape); - - double baseIntegrand = 0.0; - - if (requiresBaseState) { - const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; - - const double potentialValue = elementBasePotential * potentialShape; - - const double rotationPotential = - request.rotation->potential(mappingContext.mapping.physical_position); - - baseIntegrand = enthalpyValue + potentialValue - rotationPotential - request.bernoulliConstant; - } - - if (request.buildResidual) { - elementResult.Add(mappingContext.quadrature.weight * baseIntegrand, enthalpyShape); - - continue; - } - - double materialVariation = -request.constantVariation; - - if (request.enthalpyVariationTrue != nullptr) { - materialVariation += elementEnthalpyVariation * enthalpyShape; - } - - if (request.potentialVariationTrue != nullptr) { - materialVariation += elementPotentialVariation * potentialShape; - } - - double weightedVariation = mappingContext.quadrature.weight * materialVariation; - - if (request.displacementVariationTrue != nullptr) { - mean_field::mapping::VolumeMappingVariation mappingVariation; - - const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation( - mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext, - workspace, mappingVariation - ); - - MFEM_VERIFY( - variationStatus == mean_field::mapping::MappingStatus::valid, - "The mapping variation is invalid " - "in the hydrostatic kernel." - ); - - const double rotationVariation = request.rotation->potential_directional_derivative( - mappingContext.mapping.physical_position, mappingVariation.mapping.physical_position_variation - ); - - weightedVariation += baseIntegrand * mappingVariation.weight_variation - - rotationVariation * mappingContext.quadrature.weight; - } - - elementResult.Add(weightedVariation, enthalpyShape); - } - - if (enthalpyDofTransformation != nullptr) { - enthalpyDofTransformation->TransformDual(elementResult); - } - - localResult.AddElementVector(enthalpyDofs, elementResult); - } - - local_to_true(*f.enthalpyFes, localResult, result); + if (request.potentialVariationTrue != nullptr) { + potentialVariationLocal.GetSubVector(potentialDofs, + elementPotentialVariation); } + + if (request.displacementVariationTrue != nullptr) { + displacementVariationLocal.GetSubVector(displacementDofs, + elementDisplacementVariation); + } + + if (enthalpyDofTransformation != nullptr) { + if (request.baseEnthalpyTrue != nullptr) { + enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy); + } + + if (request.enthalpyVariationTrue != nullptr) { + enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation); + } + } + + if (potentialDofTransformation != nullptr) { + if (request.basePotentialTrue != nullptr) { + potentialDofTransformation->InvTransformPrimal(elementBasePotential); + } + + if (request.potentialVariationTrue != nullptr) { + potentialDofTransformation->InvTransformPrimal( + elementPotentialVariation); + } + } + + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->InvTransformPrimal(elementDisplacement); + + if (request.displacementVariationTrue != nullptr) { + displacementDofTransformation->InvTransformPrimal( + elementDisplacementVariation); + } + } + + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal( + elementCompactification); + } + + const mean_field::mapping::ElementDisplacementData displacementData = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacement); + + const mean_field::mapping::ElementCompactificationData compactificationData( + compactificationElement, elementCompactification); + + const mean_field::mapping::ElementMappingData mappingData{ + .displacement = displacementData, + .compactification = compactificationData}; + + std::optional + displacementVariationData; + + if (request.displacementVariationTrue != nullptr) { + displacementVariationData.emplace( + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacementVariation)); + } + + elementResult.SetSize(enthalpyElement.GetDof()); + + elementResult = 0.0; + + enthalpyShape.SetSize(enthalpyElement.GetDof()); + + potentialShape.SetSize(potentialElement.GetDof()); + + const mfem::IntegrationRule &integrationRule = get_hydrostatic_rule( + f, enthalpyElement, potentialElement, *transformation); + + for (int quadraturePoint = 0; + quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = + integrationRule.IntPoint(quadraturePoint); + + transformation->SetIntPoint(&integrationPoint); + + mean_field::mapping::VolumeMappingContext mappingContext; + + const mean_field::mapping::MappingStatus mappingStatus = + domainMapper.EvaluateVolume(mappingData, *transformation, + integrationPoint, workspace, + mappingContext); + + MFEM_VERIFY(mappingStatus == mean_field::mapping::MappingStatus::valid, + "The base mapping is invalid in the " + "hydrostatic kernel. Element: " + << elementId << ", quadrature point: " << quadraturePoint + << ", status: " << static_cast(mappingStatus)); + + enthalpyElement.CalcShape(integrationPoint, enthalpyShape); + + potentialElement.CalcShape(integrationPoint, potentialShape); + + double baseIntegrand = 0.0; + + if (requiresBaseState) { + const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; + + const double potentialValue = elementBasePotential * potentialShape; + + const double rotationPotential = request.rotation->potential( + mappingContext.mapping.physical_position); + + baseIntegrand = enthalpyValue + potentialValue - rotationPotential - + request.bernoulliConstant; + } + + if (request.buildResidual) { + elementResult.Add(mappingContext.quadrature.weight * baseIntegrand, + enthalpyShape); + + continue; + } + + double materialVariation = -request.constantVariation; + + if (request.enthalpyVariationTrue != nullptr) { + materialVariation += elementEnthalpyVariation * enthalpyShape; + } + + if (request.potentialVariationTrue != nullptr) { + materialVariation += elementPotentialVariation * potentialShape; + } + + double weightedVariation = + mappingContext.quadrature.weight * materialVariation; + + if (request.displacementVariationTrue != nullptr) { + mean_field::mapping::VolumeMappingVariation mappingVariation; + + const mean_field::mapping::MappingStatus variationStatus = + domainMapper.EvaluateVolumeVariation( + mappingData, *displacementVariationData, *transformation, + integrationPoint, mappingContext, workspace, mappingVariation); + + MFEM_VERIFY(variationStatus == + mean_field::mapping::MappingStatus::valid, + "The mapping variation is invalid " + "in the hydrostatic kernel."); + + const double rotationVariation = + request.rotation->potential_directional_derivative( + mappingContext.mapping.physical_position, + mappingVariation.mapping.physical_position_variation); + + weightedVariation += + baseIntegrand * mappingVariation.weight_variation - + rotationVariation * mappingContext.quadrature.weight; + } + + elementResult.Add(weightedVariation, enthalpyShape); + } + + if (enthalpyDofTransformation != nullptr) { + enthalpyDofTransformation->TransformDual(elementResult); + } + + localResult.AddElementVector(enthalpyDofs, elementResult); + } + + local_to_true(*f.enthalpyFes, localResult, result); +} } // namespace namespace mean_field::operators::kernels { - void apply_hydrostatic_equilibrium( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const physics::RigidRotation &rotation, - const mfem::Vector &enthalpyTrue, - const mfem::Vector &potentialTrue, - const mfem::Vector &displacementTrue, - const double bernoulliConstant, - mfem::Vector &residual - ) { - HydrostaticAssemblyRequest request; +void apply_hydrostatic_equilibrium( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const physics::RigidRotation &rotation, const mfem::Vector &enthalpyTrue, + const mfem::Vector &potentialTrue, const mfem::Vector &displacementTrue, + const double bernoulliConstant, mfem::Vector &residual) { + HydrostaticAssemblyRequest request; - request.rotation = &rotation; - request.baseEnthalpyTrue = &enthalpyTrue; - request.basePotentialTrue = &potentialTrue; - request.bernoulliConstant = bernoulliConstant; - request.buildResidual = true; + request.rotation = &rotation; + request.baseEnthalpyTrue = &enthalpyTrue; + request.basePotentialTrue = &potentialTrue; + request.bernoulliConstant = bernoulliConstant; + request.buildResidual = true; - assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, residual); - } + assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, + residual); +} - void apply_hydrostatic_equilibrium_enthalpy_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &enthalpyVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &action - ) { - HydrostaticAssemblyRequest request; +void apply_hydrostatic_equilibrium_enthalpy_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const mfem::Vector &enthalpyVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &action) { + HydrostaticAssemblyRequest request; - request.enthalpyVariationTrue = &enthalpyVariationTrue; + request.enthalpyVariationTrue = &enthalpyVariationTrue; - assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action); - } + assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action); +} - void apply_hydrostatic_equilibrium_potential_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &potentialVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &action - ) { - HydrostaticAssemblyRequest request; +void apply_hydrostatic_equilibrium_potential_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const mfem::Vector &potentialVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &action) { + HydrostaticAssemblyRequest request; - request.potentialVariationTrue = &potentialVariationTrue; + request.potentialVariationTrue = &potentialVariationTrue; - assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action); - } + assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action); +} - void apply_hydrostatic_equilibrium_constant_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const double constantVariation, - const mfem::Vector &displacementTrue, - mfem::Vector &action - ) { - HydrostaticAssemblyRequest request; +void apply_hydrostatic_equilibrium_constant_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const double constantVariation, const mfem::Vector &displacementTrue, + mfem::Vector &action) { + HydrostaticAssemblyRequest request; - request.constantVariation = constantVariation; + request.constantVariation = constantVariation; - assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action); - } + assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action); +} - void apply_hydrostatic_equilibrium_displacement_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const physics::RigidRotation &rotation, - const mfem::Vector &baseEnthalpyTrue, - const mfem::Vector &basePotentialTrue, - const mfem::Vector &baseDisplacementTrue, - const double baseBernoulliConstant, - const mfem::Vector &displacementVariationTrue, - mfem::Vector &action - ) { - HydrostaticAssemblyRequest request; +void apply_hydrostatic_equilibrium_displacement_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const physics::RigidRotation &rotation, + const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &basePotentialTrue, + const mfem::Vector &baseDisplacementTrue, + const double baseBernoulliConstant, + const mfem::Vector &displacementVariationTrue, mfem::Vector &action) { + HydrostaticAssemblyRequest request; - request.rotation = &rotation; - request.baseEnthalpyTrue = &baseEnthalpyTrue; - request.basePotentialTrue = &basePotentialTrue; - request.displacementVariationTrue = &displacementVariationTrue; - request.bernoulliConstant = baseBernoulliConstant; + request.rotation = &rotation; + request.baseEnthalpyTrue = &baseEnthalpyTrue; + request.basePotentialTrue = &basePotentialTrue; + request.displacementVariationTrue = &displacementVariationTrue; + request.bernoulliConstant = baseBernoulliConstant; - assemble_hydrostatic_form(f, domainMapper, baseDisplacementTrue, request, action); - } + assemble_hydrostatic_form(f, domainMapper, baseDisplacementTrue, request, + action); +} - void apply_hydrostatic_equilibrium_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const physics::RigidRotation &rotation, - const mfem::Vector &baseEnthalpyTrue, - const mfem::Vector &basePotentialTrue, - const mfem::Vector &baseDisplacementTrue, - const double baseBernoulliConstant, - const mfem::Vector &enthalpyVariationTrue, - const mfem::Vector &potentialVariationTrue, - const double constantVariation, - const mfem::Vector &displacementVariationTrue, - mfem::Vector &action - ) { - HydrostaticAssemblyRequest request; +void apply_hydrostatic_equilibrium_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const physics::RigidRotation &rotation, + const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &basePotentialTrue, + const mfem::Vector &baseDisplacementTrue, + const double baseBernoulliConstant, + const mfem::Vector &enthalpyVariationTrue, + const mfem::Vector &potentialVariationTrue, const double constantVariation, + const mfem::Vector &displacementVariationTrue, mfem::Vector &action) { + HydrostaticAssemblyRequest request; - request.rotation = &rotation; - request.baseEnthalpyTrue = &baseEnthalpyTrue; - request.basePotentialTrue = &basePotentialTrue; - request.enthalpyVariationTrue = &enthalpyVariationTrue; - request.potentialVariationTrue = &potentialVariationTrue; - request.displacementVariationTrue = &displacementVariationTrue; - request.bernoulliConstant = baseBernoulliConstant; - request.constantVariation = constantVariation; + request.rotation = &rotation; + request.baseEnthalpyTrue = &baseEnthalpyTrue; + request.basePotentialTrue = &basePotentialTrue; + request.enthalpyVariationTrue = &enthalpyVariationTrue; + request.potentialVariationTrue = &potentialVariationTrue; + request.displacementVariationTrue = &displacementVariationTrue; + request.bernoulliConstant = baseBernoulliConstant; + request.constantVariation = constantVariation; - assemble_hydrostatic_form(f, domainMapper, baseDisplacementTrue, request, action); - } -} // namespace mean_field::operators::kernels \ No newline at end of file + assemble_hydrostatic_form(f, domainMapper, baseDisplacementTrue, request, + action); +} +} // namespace mean_field::operators::kernels diff --git a/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp b/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp index f82a654..4813298 100644 --- a/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp @@ -12,563 +12,549 @@ module mean_field; import :operators.kernels.pressure_force; namespace { - enum class PressureForceAction { residual, enthalpy, displacement }; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - void true_to_local( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &trueVector, - mfem::Vector &localVector - ) { - MFEM_VERIFY( - trueVector.Size() == finiteElementSpace.GetTrueVSize(), "The pressure-force true vector has the wrong size." - ); +[[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(attribute); +} - localVector.SetSize(finiteElementSpace.GetVSize()); +enum class PressureForceAction { residual, enthalpy, displacement }; - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void true_to_local(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, mfem::Vector &localVector) { + MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), + "The pressure-force true vector has the wrong size."); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } - } + localVector.SetSize(finiteElementSpace.GetVSize()); - void local_to_true( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &localVector, - mfem::Vector &trueVector - ) { - MFEM_VERIFY( - localVector.Size() == finiteElementSpace.GetVSize(), "The pressure-force local vector has the wrong size." - ); + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - trueVector.SetSize(finiteElementSpace.GetTrueVSize()); - trueVector = 0.0; + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } +} - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void local_to_true(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, mfem::Vector &trueVector) { + MFEM_VERIFY(localVector.Size() == finiteElementSpace.GetVSize(), + "The pressure-force local vector has the wrong size."); - if (prolongation != nullptr) { - prolongation->MultTranspose(localVector, trueVector); - } else { - trueVector = localVector; - } - } + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; - [[nodiscard]] int vector_dof_index( - const mfem::Ordering::Type ordering, - const int scalarDof, - const int component, - const int scalarDofCount, - const int dimension - ) { - if (ordering == mfem::Ordering::byNODES) { - return scalarDof + component * scalarDofCount; - } + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - if (ordering == mfem::Ordering::byVDIM) { - return scalarDof * dimension + component; - } + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } +} - MFEM_ABORT("The displacement space uses an unsupported ordering."); - return -1; - } +[[nodiscard]] int vector_dof_index(const mfem::Ordering::Type ordering, + const int scalarDof, const int component, + const int scalarDofCount, + const int dimension) { + if (ordering == mfem::Ordering::byNODES) { + return scalarDof + component * scalarDofCount; + } - [[nodiscard]] int get_pressure_extra_order(const mean_field::eos::Polytrope &barotrope) { - /* - * Pressure has the enthalpy dependence - * - * P(h) proportional to h^(n + 1). - * - * The registered enthalpy operand already contributes one factor - * of the enthalpy polynomial order. The remaining dynamic - * contribution is therefore n times that order. - */ - const double extraOrder = - barotrope.polytropic_index() * static_cast(mean_field::field::Enthalpy::Scalar::familyOrder); + if (ordering == mfem::Ordering::byVDIM) { + return scalarDof * dimension + component; + } - MFEM_VERIFY( - std::isfinite(extraOrder) && extraOrder >= 0.0 && - extraOrder <= static_cast(std::numeric_limits::max()), - "The pressure EOS effective polynomial order is invalid." - ); + MFEM_ABORT("The displacement space uses an unsupported ordering."); + return -1; +} - return static_cast(std::ceil(extraOrder)); - } +[[nodiscard]] int +get_pressure_extra_order(const mean_field::eos::Polytrope &barotrope) { + /* + * Pressure has the enthalpy dependence + * + * P(h) proportional to h^(n + 1). + * + * The registered enthalpy operand already contributes one factor + * of the enthalpy polynomial order. The remaining dynamic + * contribution is therefore n times that order. + */ + const double extraOrder = + barotrope.polytropic_index() * + static_cast(mean_field::field::Enthalpy::Scalar::familyOrder); - [[nodiscard]] const mfem::IntegrationRule &get_pressure_force_rule( - const mean_field::fem::FEM &f, - const mean_field::eos::Polytrope &barotrope, - const mfem::FiniteElement &enthalpyElement, - const mfem::FiniteElement &displacementElement, - const mfem::ElementTransformation &transformation - ) { - using EnthalpyField = mean_field::field::Field; + MFEM_VERIFY(std::isfinite(extraOrder) && extraOrder >= 0.0 && + extraOrder <= + static_cast(std::numeric_limits::max()), + "The pressure EOS effective polynomial order is invalid."); - MFEM_VERIFY( - enthalpyElement.GetOrder() == mean_field::field::Enthalpy::Scalar::familyOrder, - "The pressure-force enthalpy element does not match the " - "registered enthalpy field." - ); + return static_cast(std::ceil(extraOrder)); +} - MFEM_VERIFY( - displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder, - "The pressure-force test element does not match the " - "registered displacement field." - ); +[[nodiscard]] const mfem::IntegrationRule & +get_pressure_force_rule(const mean_field::fem::FEM &f, + const mean_field::eos::Polytrope &barotrope, + const mfem::FiniteElement &enthalpyElement, + const mfem::FiniteElement &displacementElement, + const mfem::ElementTransformation &transformation) { + using EnthalpyField = mean_field::field::Field; - const mean_field::quadrature::Query query = - EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), - std::array{get_pressure_extra_order(barotrope)}, mean_field::utils::DOMAINS::STELLAR, - mean_field::quadrature::MappingKind::general - ); + MFEM_VERIFY(enthalpyElement.GetOrder() == + mean_field::field::Enthalpy::Scalar::familyOrder, + "The pressure-force enthalpy element does not match the " + "registered enthalpy field."); - const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY(displacementElement.GetOrder() == + mean_field::field::Displacement::Vector::familyOrder, + "The pressure-force test element does not match the " + "registered displacement field."); - MFEM_VERIFY( - rule.integration_rule != nullptr, "The quadrature policy did not return a pressure-force " - "integration rule." - ); + const mean_field::quadrature::Query query = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::PressureForce>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), + std::array{get_pressure_extra_order(barotrope)}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - return *rule.integration_rule; - } + const mean_field::quadrature::MfemRule rule = + f.quadratureFactory->get(query, transformation.GetGeometryType()); - void validate_inputs( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &enthalpyTrue, - const mfem::Vector &displacementTrue - ) { - MFEM_VERIFY(f.mesh != nullptr, "The pressure-force kernel requires a mesh."); + MFEM_VERIFY(rule.integration_rule != nullptr, + "The quadrature policy did not return a pressure-force " + "integration rule."); - MFEM_VERIFY( - f.enthalpyFes != nullptr, "The pressure-force kernel requires the enthalpy " - "finite-element space." - ); + return *rule.integration_rule; +} - MFEM_VERIFY( - f.displacementFes != nullptr, "The pressure-force kernel requires the displacement " - "finite-element space." - ); +void validate_inputs( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mfem::Vector &enthalpyTrue, const mfem::Vector &displacementTrue) { + MFEM_VERIFY(f.mesh != nullptr, "The pressure-force kernel requires a mesh."); - MFEM_VERIFY( - f.compactificationFes != nullptr, "The pressure-force kernel requires the compactification " - "finite-element space." - ); + MFEM_VERIFY(f.enthalpyFes != nullptr, + "The pressure-force kernel requires the enthalpy " + "finite-element space."); - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, "The pressure-force kernel requires the compactification " - "coordinate." - ); + MFEM_VERIFY(f.displacementFes != nullptr, + "The pressure-force kernel requires the displacement " + "finite-element space."); - MFEM_VERIFY( - f.quadratureFactory != nullptr, "The pressure-force kernel requires the quadrature " - "rule factory." - ); + MFEM_VERIFY(f.compactificationFes != nullptr, + "The pressure-force kernel requires the compactification " + "finite-element space."); - MFEM_VERIFY( - enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), - "The pressure-force enthalpy vector has the wrong size." - ); + MFEM_VERIFY(f.compactificationCoordinate != nullptr, + "The pressure-force kernel requires the compactification " + "coordinate."); - MFEM_VERIFY( - displacementTrue.Size() == f.displacementFes->GetTrueVSize(), - "The pressure-force displacement vector has the wrong size." - ); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "The pressure-force kernel requires the quadrature " + "rule factory."); - MFEM_VERIFY( - domainMapper.GetDimension() == f.mesh->Dimension(), - "The pressure-force domain-mapper dimension does not match " - "the mesh dimension." - ); + MFEM_VERIFY(enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), + "The pressure-force enthalpy vector has the wrong size."); - MFEM_VERIFY( - f.displacementFes->GetVDim() == f.mesh->Dimension(), "The displacement vector dimension does not match the " - "mesh dimension." - ); + MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The pressure-force displacement vector has the wrong size."); - /* - * ElementDisplacementDataFromElementVDofs currently consumes the - * registered byNODES layout. Keep this explicit so a future - * registry change fails immediately rather than silently - * corrupting the geometry. - */ - MFEM_VERIFY( - f.displacementFes->GetOrdering() == mfem::Ordering::byNODES, - "The pressure-force kernel requires the registered byNODES " - "displacement ordering." - ); - } + MFEM_VERIFY(domainMapper.GetDimension() == f.mesh->Dimension(), + "The pressure-force domain-mapper dimension does not match " + "the mesh dimension."); - void apply_pressure_force_action( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mean_field::eos::Polytrope &barotrope, - const PressureForceAction pressureForceAction, - const mfem::Vector &baseEnthalpyTrue, - const mfem::Vector *enthalpyVariationTrue, - const mfem::Vector *displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - validate_inputs(f, domainMapper, baseEnthalpyTrue, displacementTrue); + MFEM_VERIFY(f.displacementFes->GetVDim() == f.mesh->Dimension(), + "The displacement vector dimension does not match the " + "mesh dimension."); - if (pressureForceAction == PressureForceAction::enthalpy) { - MFEM_VERIFY( - enthalpyVariationTrue != nullptr && enthalpyVariationTrue->Size() == f.enthalpyFes->GetTrueVSize(), - "The pressure-force enthalpy variation has the wrong size." - ); - } + /* + * ElementDisplacementDataFromElementVDofs currently consumes the + * registered byNODES layout. Keep this explicit so a future + * registry change fails immediately rather than silently + * corrupting the geometry. + */ + MFEM_VERIFY(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES, + "The pressure-force kernel requires the registered byNODES " + "displacement ordering."); +} - if (pressureForceAction == PressureForceAction::displacement) { - MFEM_VERIFY( - displacementVariationTrue != nullptr && - displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(), +void apply_pressure_force_action( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mean_field::eos::Polytrope &barotrope, + const PressureForceAction pressureForceAction, + const mfem::Vector &baseEnthalpyTrue, + const mfem::Vector *enthalpyVariationTrue, + const mfem::Vector *displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + validate_inputs(f, domainMapper, baseEnthalpyTrue, displacementTrue); + + if (pressureForceAction == PressureForceAction::enthalpy) { + MFEM_VERIFY(enthalpyVariationTrue != nullptr && + enthalpyVariationTrue->Size() == + f.enthalpyFes->GetTrueVSize(), + "The pressure-force enthalpy variation has the wrong size."); + } + + if (pressureForceAction == PressureForceAction::displacement) { + MFEM_VERIFY(displacementVariationTrue != nullptr && + displacementVariationTrue->Size() == + f.displacementFes->GetTrueVSize(), "The pressure-force displacement variation has the wrong " - "size." - ); - } + "size."); + } - mfem::Vector baseEnthalpyLocal; - mfem::Vector enthalpyVariationLocal; - mfem::Vector displacementLocal; - mfem::Vector displacementVariationLocal; + mfem::Vector baseEnthalpyLocal; + mfem::Vector enthalpyVariationLocal; + mfem::Vector displacementLocal; + mfem::Vector displacementVariationLocal; - true_to_local(*f.enthalpyFes, baseEnthalpyTrue, baseEnthalpyLocal); + true_to_local(*f.enthalpyFes, baseEnthalpyTrue, baseEnthalpyLocal); - if (enthalpyVariationTrue != nullptr) { - true_to_local(*f.enthalpyFes, *enthalpyVariationTrue, enthalpyVariationLocal); - } + if (enthalpyVariationTrue != nullptr) { + true_to_local(*f.enthalpyFes, *enthalpyVariationTrue, + enthalpyVariationLocal); + } - true_to_local(*f.displacementFes, displacementTrue, displacementLocal); + true_to_local(*f.displacementFes, displacementTrue, displacementLocal); - if (displacementVariationTrue != nullptr) { - true_to_local(*f.displacementFes, *displacementVariationTrue, displacementVariationLocal); - } + if (displacementVariationTrue != nullptr) { + true_to_local(*f.displacementFes, *displacementVariationTrue, + displacementVariationLocal); + } - mfem::Vector localAction(f.displacementFes->GetVSize()); - localAction = 0.0; + mfem::Vector localAction(f.displacementFes->GetVSize()); + localAction = 0.0; - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); - mfem::Array enthalpyDofsofs; - mfem::Array displacementDofs; - mfem::Array compactificationDofs; + mfem::Array enthalpyDofsofs; + mfem::Array displacementDofs; + mfem::Array compactificationDofs; - mfem::Vector elementBaseEnthalpy; - mfem::Vector elementEnthalpyVariation; - mfem::Vector elementDisplacement; - mfem::Vector elementDisplacementVariation; - mfem::Vector elementCompactification; - mfem::Vector elementAction; - mfem::Vector enthalpyShape; + mfem::Vector elementBaseEnthalpy; + mfem::Vector elementEnthalpyVariation; + mfem::Vector elementDisplacement; + mfem::Vector elementDisplacementVariation; + mfem::Vector elementCompactification; + mfem::Vector elementAction; + mfem::Vector enthalpyShape; - mfem::Array enthalpyDofs; + mfem::Array enthalpyDofs; - mfem::DenseMatrix displacementDShapeReference; - mfem::DenseMatrix displacementDShapePhysical; - mfem::DenseMatrix displacementDShapePhysicalVariation; + mfem::DenseMatrix displacementDShapeReference; + mfem::DenseMatrix displacementDShapePhysical; + mfem::DenseMatrix displacementDShapePhysicalVariation; - mean_field::mapping::VolumeMappingContext mappingContext; + mean_field::mapping::VolumeMappingContext mappingContext; - const int dimension = f.mesh->Dimension(); - const int vacuumAttribute = domainMapper.GetVacuumElementAttribute(); + const int dimension = f.mesh->Dimension(); - const mfem::Ordering::Type displacementOrdering = f.displacementFes->GetOrdering(); + const mfem::Ordering::Type displacementOrdering = + f.displacementFes->GetOrdering(); - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elementId); - MFEM_VERIFY( - transformation != nullptr, "The pressure-force kernel received a null element " - "transformation." - ); + MFEM_VERIFY(transformation != nullptr, + "The pressure-force kernel received a null element " + "transformation."); - /* - * Skip vacuum before constructing or evaluating any mapping - * data for the element. - */ - if (transformation->Attribute == vacuumAttribute) { - continue; - } - - const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId); - - const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId); - - const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId); - - mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); - - mfem::DofTransformation *displacementDofTransformation = - f.displacementFes->GetElementVDofs(elementId, displacementDofs); - - mfem::DofTransformation *compactificationDofTransformation = - f.compactificationFes->GetElementDofs(elementId, compactificationDofs); - - baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy); - - if (enthalpyVariationTrue != nullptr) { - enthalpyVariationLocal.GetSubVector(enthalpyDofs, elementEnthalpyVariation); - } - - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); - - if (displacementVariationTrue != nullptr) { - displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation); - } - - f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); - - if (enthalpyDofTransformation != nullptr) { - enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy); - - if (enthalpyVariationTrue != nullptr) { - enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation); - } - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacement); - - if (displacementVariationTrue != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation); - } - } - - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal(elementCompactification); - } - - const mean_field::mapping::ElementDisplacementData displacementData = - mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement); - - const mean_field::mapping::ElementCompactificationData compactificationData( - compactificationElement, elementCompactification - ); - - const mean_field::mapping::ElementMappingData mappingData{ - .displacement = displacementData, .compactification = compactificationData - }; - - std::optional displacementVariationData; - - if (displacementVariationTrue != nullptr) { - displacementVariationData.emplace( - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacementElement, elementDisplacementVariation - ) - ); - } - - const int scalarDisplacementDofCount = displacementElement.GetDof(); - - MFEM_VERIFY( - displacementDofs.Size() == scalarDisplacementDofCount * dimension, - "The pressure-force element displacement vector has " - "the wrong size." - ); - - enthalpyShape.SetSize(enthalpyElement.GetDof()); - - displacementDShapeReference.SetSize(scalarDisplacementDofCount, dimension); - - displacementDShapePhysical.SetSize(scalarDisplacementDofCount, dimension); - - displacementDShapePhysicalVariation.SetSize(scalarDisplacementDofCount, dimension); - - elementAction.SetSize(displacementDofs.Size()); - elementAction = 0.0; - - const mfem::IntegrationRule &integrationRule = - get_pressure_force_rule(f, barotrope, enthalpyElement, displacementElement, *transformation); - - for (int quadratureIndex = 0; quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) { - const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadratureIndex); - - transformation->SetIntPoint(&integrationPoint); - - const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume( - mappingData, *transformation, integrationPoint, workspace, mappingContext - ); - - MFEM_VERIFY( - mappingStatus == mean_field::mapping::MappingStatus::valid, - "Stateless mapping failed in the pressure-force " - "kernel. Element: " - << elementId << ", attribute: " << transformation->Attribute - << ", quadrature point: " << quadratureIndex << ", status: " << static_cast(mappingStatus) - ); - - enthalpyElement.CalcShape(integrationPoint, enthalpyShape); - - const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; - - double pressureFactor = 0.0; - - if (pressureForceAction == PressureForceAction::residual || - pressureForceAction == PressureForceAction::displacement) { - pressureFactor = barotrope.pressure_from_enthalpy(enthalpyValue); - } else { - const double enthalpyVariationValue = elementEnthalpyVariation * enthalpyShape; - - pressureFactor = - barotrope.pressure_derivative_from_enthalpy(enthalpyValue) * enthalpyVariationValue; - } - - displacementElement.CalcDShape(integrationPoint, displacementDShapeReference); - - /* - * Row i of DShape is grad_reference(N_i). Multiplication - * by the complete inverse element Jacobian gives - * - * grad_physical(N_i) - * = grad_reference(N_i) J^{-1}. - */ - mfem::Mult(displacementDShapeReference, mappingContext.quadrature.J_inv, displacementDShapePhysical); - - std::optional mappingVariation; - - if (pressureForceAction == PressureForceAction::displacement) { - mappingVariation.emplace(); - - const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation( - mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext, - workspace, *mappingVariation - ); - - MFEM_VERIFY( - variationStatus == mean_field::mapping::MappingStatus::valid, - "Stateless mapping variation failed in the " - "pressure-force kernel. Element: " - << elementId << ", attribute: " << transformation->Attribute << ", quadrature point: " - << quadratureIndex << ", status: " << static_cast(variationStatus) - ); - - /* - * Differentiating - * - * grad_x(N_i) = grad_reference(N_i) J^{-1} - * - * at the frozen base geometry gives the physical - * test-gradient variation used by the geometric - * pressure block. - */ - mfem::Mult( - displacementDShapeReference, mappingVariation->inverse_element_jacobian_variation, - displacementDShapePhysicalVariation - ); - } - - const double weightedPressureFactor = pressureFactor * mappingContext.quadrature.weight; - - MFEM_VERIFY( - std::isfinite(pressureFactor) && std::isfinite(weightedPressureFactor), - "The pressure-force kernel encountered a non-finite " - "quadrature value." - ); - - /* - * For the vector basis N_i e_c, - * - * div(N_i e_c) = partial_c N_i. - * - * Therefore - * - * R_(i,c) - * = -integral P partial_c N_i dV. - */ - for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) { - for (int component = 0; component < dimension; ++component) { - const int vectorDof = vector_dof_index( - displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension - ); - - if (pressureForceAction == PressureForceAction::displacement) { - /* - * Differentiate the complete discrete factor - * - * grad_x(N_i) dV_x. - * - * The enthalpy DOFs, and therefore P(h), are - * frozen in this Jacobian column. - */ - const double gradientWeightVariation = - mappingContext.quadrature.weight * - displacementDShapePhysicalVariation(scalarDof, component) + - mappingVariation->weight_variation * displacementDShapePhysical(scalarDof, component); - - const double contribution = pressureFactor * gradientWeightVariation; - - MFEM_VERIFY( - std::isfinite(gradientWeightVariation) && std::isfinite(contribution), - "The pressure-force geometry action " - "encountered a non-finite contribution." - ); - - elementAction(vectorDof) -= contribution; - } else { - elementAction(vectorDof) -= - weightedPressureFactor * displacementDShapePhysical(scalarDof, component); - } - } - } - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(displacementDofs, elementAction); - } - - local_to_true(*f.displacementFes, localAction, actionTrue); + /* + * Skip vacuum before constructing or evaluating any mapping + * data for the element. + */ + if (is_vacuum_attribute(transformation->Attribute)) { + continue; } + + const mfem::FiniteElement &enthalpyElement = + *f.enthalpyFes->GetFE(elementId); + + const mfem::FiniteElement &displacementElement = + *f.displacementFes->GetFE(elementId); + + const mfem::FiniteElement &compactificationElement = + *f.compactificationFes->GetFE(elementId); + + mfem::DofTransformation *enthalpyDofTransformation = + f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); + + mfem::DofTransformation *displacementDofTransformation = + f.displacementFes->GetElementVDofs(elementId, displacementDofs); + + mfem::DofTransformation *compactificationDofTransformation = + f.compactificationFes->GetElementDofs(elementId, compactificationDofs); + + baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy); + + if (enthalpyVariationTrue != nullptr) { + enthalpyVariationLocal.GetSubVector(enthalpyDofs, + elementEnthalpyVariation); + } + + displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + + if (displacementVariationTrue != nullptr) { + displacementVariationLocal.GetSubVector(displacementDofs, + elementDisplacementVariation); + } + + f.compactificationCoordinate->GetSubVector(compactificationDofs, + elementCompactification); + + if (enthalpyDofTransformation != nullptr) { + enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy); + + if (enthalpyVariationTrue != nullptr) { + enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation); + } + } + + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->InvTransformPrimal(elementDisplacement); + + if (displacementVariationTrue != nullptr) { + displacementDofTransformation->InvTransformPrimal( + elementDisplacementVariation); + } + } + + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal( + elementCompactification); + } + + const mean_field::mapping::ElementDisplacementData displacementData = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacement); + + const mean_field::mapping::ElementCompactificationData compactificationData( + compactificationElement, elementCompactification); + + const mean_field::mapping::ElementMappingData mappingData{ + .displacement = displacementData, + .compactification = compactificationData}; + + std::optional + displacementVariationData; + + if (displacementVariationTrue != nullptr) { + displacementVariationData.emplace( + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacementVariation)); + } + + const int scalarDisplacementDofCount = displacementElement.GetDof(); + + MFEM_VERIFY(displacementDofs.Size() == + scalarDisplacementDofCount * dimension, + "The pressure-force element displacement vector has " + "the wrong size."); + + enthalpyShape.SetSize(enthalpyElement.GetDof()); + + displacementDShapeReference.SetSize(scalarDisplacementDofCount, dimension); + + displacementDShapePhysical.SetSize(scalarDisplacementDofCount, dimension); + + displacementDShapePhysicalVariation.SetSize(scalarDisplacementDofCount, + dimension); + + elementAction.SetSize(displacementDofs.Size()); + elementAction = 0.0; + + const mfem::IntegrationRule &integrationRule = get_pressure_force_rule( + f, barotrope, enthalpyElement, displacementElement, *transformation); + + for (int quadratureIndex = 0; + quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) { + const mfem::IntegrationPoint &integrationPoint = + integrationRule.IntPoint(quadratureIndex); + + transformation->SetIntPoint(&integrationPoint); + + const mean_field::mapping::MappingStatus mappingStatus = + domainMapper.EvaluateVolume(mappingData, *transformation, + integrationPoint, workspace, + mappingContext); + + MFEM_VERIFY(mappingStatus == mean_field::mapping::MappingStatus::valid, + "Stateless mapping failed in the pressure-force " + "kernel. Element: " + << elementId + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadratureIndex + << ", status: " << static_cast(mappingStatus)); + + enthalpyElement.CalcShape(integrationPoint, enthalpyShape); + + const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; + + double pressureFactor = 0.0; + + if (pressureForceAction == PressureForceAction::residual || + pressureForceAction == PressureForceAction::displacement) { + pressureFactor = barotrope.pressure_from_enthalpy(enthalpyValue); + } else { + const double enthalpyVariationValue = + elementEnthalpyVariation * enthalpyShape; + + pressureFactor = + barotrope.pressure_derivative_from_enthalpy(enthalpyValue) * + enthalpyVariationValue; + } + + displacementElement.CalcDShape(integrationPoint, + displacementDShapeReference); + + /* + * Row i of DShape is grad_reference(N_i). Multiplication + * by the complete inverse element Jacobian gives + * + * grad_physical(N_i) + * = grad_reference(N_i) J^{-1}. + */ + mfem::Mult(displacementDShapeReference, mappingContext.quadrature.J_inv, + displacementDShapePhysical); + + std::optional + mappingVariation; + + if (pressureForceAction == PressureForceAction::displacement) { + mappingVariation.emplace(); + + const mean_field::mapping::MappingStatus variationStatus = + domainMapper.EvaluateVolumeVariation( + mappingData, *displacementVariationData, *transformation, + integrationPoint, mappingContext, workspace, *mappingVariation); + + MFEM_VERIFY( + variationStatus == mean_field::mapping::MappingStatus::valid, + "Stateless mapping variation failed in the " + "pressure-force kernel. Element: " + << elementId << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadratureIndex + << ", status: " << static_cast(variationStatus)); + + /* + * Differentiating + * + * grad_x(N_i) = grad_reference(N_i) J^{-1} + * + * at the frozen base geometry gives the physical + * test-gradient variation used by the geometric + * pressure block. + */ + mfem::Mult(displacementDShapeReference, + mappingVariation->inverse_element_jacobian_variation, + displacementDShapePhysicalVariation); + } + + const double weightedPressureFactor = + pressureFactor * mappingContext.quadrature.weight; + + MFEM_VERIFY(std::isfinite(pressureFactor) && + std::isfinite(weightedPressureFactor), + "The pressure-force kernel encountered a non-finite " + "quadrature value."); + + /* + * For the vector basis N_i e_c, + * + * div(N_i e_c) = partial_c N_i. + * + * Therefore + * + * R_(i,c) + * = -integral P partial_c N_i dV. + */ + for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; + ++scalarDof) { + for (int component = 0; component < dimension; ++component) { + const int vectorDof = + vector_dof_index(displacementOrdering, scalarDof, component, + scalarDisplacementDofCount, dimension); + + if (pressureForceAction == PressureForceAction::displacement) { + /* + * Differentiate the complete discrete factor + * + * grad_x(N_i) dV_x. + * + * The enthalpy DOFs, and therefore P(h), are + * frozen in this Jacobian column. + */ + const double gradientWeightVariation = + mappingContext.quadrature.weight * + displacementDShapePhysicalVariation(scalarDof, component) + + mappingVariation->weight_variation * + displacementDShapePhysical(scalarDof, component); + + const double contribution = + pressureFactor * gradientWeightVariation; + + MFEM_VERIFY(std::isfinite(gradientWeightVariation) && + std::isfinite(contribution), + "The pressure-force geometry action " + "encountered a non-finite contribution."); + + elementAction(vectorDof) -= contribution; + } else { + elementAction(vectorDof) -= + weightedPressureFactor * + displacementDShapePhysical(scalarDof, component); + } + } + } + } + + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->TransformDual(elementAction); + } + + localAction.AddElementVector(displacementDofs, elementAction); + } + + local_to_true(*f.displacementFes, localAction, actionTrue); +} } // namespace namespace mean_field::operators::kernels { - void apply_pressure_force_residual( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const eos::Polytrope &barotrope, - const mfem::Vector &enthalpyTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &residualTrue - ) { - apply_pressure_force_action( - f, domainMapper, barotrope, PressureForceAction::residual, enthalpyTrue, nullptr, nullptr, displacementTrue, - residualTrue - ); - } +void apply_pressure_force_residual( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const eos::Polytrope &barotrope, const mfem::Vector &enthalpyTrue, + const mfem::Vector &displacementTrue, mfem::Vector &residualTrue) { + apply_pressure_force_action(f, domainMapper, barotrope, + PressureForceAction::residual, enthalpyTrue, + nullptr, nullptr, displacementTrue, residualTrue); +} - void apply_pressure_force_enthalpy_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const eos::Polytrope &barotrope, - const mfem::Vector &baseEnthalpyTrue, - const mfem::Vector &enthalpyVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_pressure_force_action( - f, domainMapper, barotrope, PressureForceAction::enthalpy, baseEnthalpyTrue, &enthalpyVariationTrue, - nullptr, displacementTrue, actionTrue - ); - } +void apply_pressure_force_enthalpy_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const eos::Polytrope &barotrope, const mfem::Vector &baseEnthalpyTrue, + const mfem::Vector &enthalpyVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_pressure_force_action(f, domainMapper, barotrope, + PressureForceAction::enthalpy, baseEnthalpyTrue, + &enthalpyVariationTrue, nullptr, displacementTrue, + actionTrue); +} - void apply_pressure_force_displacement_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const eos::Polytrope &barotrope, - const mfem::Vector &baseEnthalpyTrue, - const mfem::Vector &displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_pressure_force_action( - f, domainMapper, barotrope, PressureForceAction::displacement, baseEnthalpyTrue, nullptr, - &displacementVariationTrue, displacementTrue, actionTrue - ); - } +void apply_pressure_force_displacement_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const eos::Polytrope &barotrope, const mfem::Vector &baseEnthalpyTrue, + const mfem::Vector &displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_pressure_force_action( + f, domainMapper, barotrope, PressureForceAction::displacement, + baseEnthalpyTrue, nullptr, &displacementVariationTrue, displacementTrue, + actionTrue); +} } // namespace mean_field::operators::kernels diff --git a/libmeanfield/impl/operators/kernels/rotation_displacement_force_kernels.cpp b/libmeanfield/impl/operators/kernels/rotation_displacement_force_kernels.cpp index f21aff1..0ecc65e 100644 --- a/libmeanfield/impl/operators/kernels/rotation_displacement_force_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/rotation_displacement_force_kernels.cpp @@ -11,580 +11,554 @@ module mean_field; import :operators.kernels.rotational_displacement_force; namespace { - enum class RotationalDisplacementForceAction { residual, density, displacement, complete }; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - void true_to_local( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &trueVector, - mfem::Vector &localVector - ) { - MFEM_VERIFY( - trueVector.Size() == finiteElementSpace.GetTrueVSize(), - "The rotational-displacement-force true vector has the wrong " - "size." - ); +[[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(attribute); +} - localVector.SetSize(finiteElementSpace.GetVSize()); +enum class RotationalDisplacementForceAction { + residual, + density, + displacement, + complete +}; - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void true_to_local(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, mfem::Vector &localVector) { + MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), + "The rotational-displacement-force true vector has the wrong " + "size."); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } - } + localVector.SetSize(finiteElementSpace.GetVSize()); - void local_to_true( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &localVector, - mfem::Vector &trueVector - ) { - MFEM_VERIFY( - localVector.Size() == finiteElementSpace.GetVSize(), - "The rotational-displacement-force local vector has the wrong " - "size." - ); + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - trueVector.SetSize(finiteElementSpace.GetTrueVSize()); - trueVector = 0.0; + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } +} - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void local_to_true(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, mfem::Vector &trueVector) { + MFEM_VERIFY(localVector.Size() == finiteElementSpace.GetVSize(), + "The rotational-displacement-force local vector has the wrong " + "size."); - if (prolongation != nullptr) { - prolongation->MultTranspose(localVector, trueVector); - } else { - trueVector = localVector; - } - } + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; - [[nodiscard]] int vector_dof_index( - const mfem::Ordering::Type ordering, - const int scalarDof, - const int component, - const int scalarDofCount, - const int dimension - ) { - if (ordering == mfem::Ordering::byNODES) { - return scalarDof + component * scalarDofCount; - } + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - if (ordering == mfem::Ordering::byVDIM) { - return scalarDof * dimension + component; - } + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } +} - MFEM_ABORT( - "The rotational-displacement-force test space uses an " - "unsupported ordering." - ); +[[nodiscard]] int vector_dof_index(const mfem::Ordering::Type ordering, + const int scalarDof, const int component, + const int scalarDofCount, + const int dimension) { + if (ordering == mfem::Ordering::byNODES) { + return scalarDof + component * scalarDofCount; + } - return -1; - } + if (ordering == mfem::Ordering::byVDIM) { + return scalarDof * dimension + component; + } - [[nodiscard]] const mfem::IntegrationRule &get_rotation_force_rule( - const mean_field::fem::FEM &f, - const mfem::FiniteElement &densityElement, - const mfem::FiniteElement &displacementElement, - const mfem::ElementTransformation &transformation - ) { - using DisplacementField = mean_field::field::Field; + MFEM_ABORT("The rotational-displacement-force test space uses an " + "unsupported ordering."); - MFEM_VERIFY( - densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder, - "The rotational-displacement-force density element does not " - "match the registered density field." - ); + return -1; +} - MFEM_VERIFY( - displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder, - "The rotational-displacement-force test element does not match " - "the registered displacement field." - ); +[[nodiscard]] const mfem::IntegrationRule & +get_rotation_force_rule(const mean_field::fem::FEM &f, + const mfem::FiniteElement &densityElement, + const mfem::FiniteElement &displacementElement, + const mfem::ElementTransformation &transformation) { + using DisplacementField = + mean_field::field::Field; - /* - * grad(Psi_rotation) is linear in physical position, so it adds one - * dynamic polynomial-order contribution. - */ - const mean_field::quadrature::Query query = - DisplacementField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array{1}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + MFEM_VERIFY(densityElement.GetOrder() == + mean_field::field::Density::Scalar::familyOrder, + "The rotational-displacement-force density element does not " + "match the registered density field."); - const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY(displacementElement.GetOrder() == + mean_field::field::Displacement::Vector::familyOrder, + "The rotational-displacement-force test element does not match " + "the registered displacement field."); - MFEM_VERIFY( - rule.integration_rule != nullptr, "The quadrature policy did not return a rotational-" - "displacement-force integration rule." - ); + /* + * grad(Psi_rotation) is linear in physical position, so it adds one + * dynamic polynomial-order contribution. + */ + const mean_field::quadrature::Query query = DisplacementField::make_query< + mean_field::field::Displacement::Form::CentrifugalForce>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), std::array{1}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - return *rule.integration_rule; - } + const mean_field::quadrature::MfemRule rule = + f.quadratureFactory->get(query, transformation.GetGeometryType()); - void validate_finite_vector( - const mfem::Vector &vector, - const char *message - ) { - for (int index = 0; index < vector.Size(); ++index) { - MFEM_VERIFY(std::isfinite(vector(index)), message); - } - } + MFEM_VERIFY(rule.integration_rule != nullptr, + "The quadrature policy did not return a rotational-" + "displacement-force integration rule."); - void validate_common_inputs( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &displacementTrue - ) { - MFEM_VERIFY(f.mesh != nullptr, "The rotational-displacement-force kernel requires a mesh."); + return *rule.integration_rule; +} - MFEM_VERIFY( - f.mesh->Dimension() == 3, "The rotational-displacement-force kernel requires a " - "three-dimensional mesh." - ); +void validate_finite_vector(const mfem::Vector &vector, const char *message) { + for (int index = 0; index < vector.Size(); ++index) { + MFEM_VERIFY(std::isfinite(vector(index)), message); + } +} - MFEM_VERIFY( - f.densityFes != nullptr, "The rotational-displacement-force kernel requires the density " - "finite-element space." - ); +void validate_common_inputs( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mfem::Vector &displacementTrue) { + MFEM_VERIFY(f.mesh != nullptr, + "The rotational-displacement-force kernel requires a mesh."); - MFEM_VERIFY( - f.displacementFes != nullptr, "The rotational-displacement-force kernel requires the " - "displacement finite-element space." - ); + MFEM_VERIFY(f.mesh->Dimension() == 3, + "The rotational-displacement-force kernel requires a " + "three-dimensional mesh."); - MFEM_VERIFY( - f.compactificationFes != nullptr && f.compactificationCoordinate != nullptr, - "The rotational-displacement-force kernel requires the " - "compactification coordinate." - ); + MFEM_VERIFY(f.densityFes != nullptr, + "The rotational-displacement-force kernel requires the density " + "finite-element space."); - MFEM_VERIFY( - f.quadratureFactory != nullptr, "The rotational-displacement-force kernel requires the " - "quadrature-rule factory." - ); + MFEM_VERIFY(f.displacementFes != nullptr, + "The rotational-displacement-force kernel requires the " + "displacement finite-element space."); - MFEM_VERIFY( - displacementTrue.Size() == f.displacementFes->GetTrueVSize(), - "The rotational-displacement-force displacement vector has the " - "wrong size." - ); + MFEM_VERIFY(f.compactificationFes != nullptr && + f.compactificationCoordinate != nullptr, + "The rotational-displacement-force kernel requires the " + "compactification coordinate."); - MFEM_VERIFY( - domainMapper.GetDimension() == f.mesh->Dimension(), - "The rotational-displacement-force mapper dimension does not " - "match the mesh dimension." - ); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "The rotational-displacement-force kernel requires the " + "quadrature-rule factory."); - MFEM_VERIFY( - f.displacementFes->GetVDim() == f.mesh->Dimension(), - "The rotational-displacement-force displacement dimension does " - "not match the mesh dimension." - ); + MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The rotational-displacement-force displacement vector has the " + "wrong size."); - validate_finite_vector( - displacementTrue, "The rotational-displacement-force displacement contains a " - "non-finite value." - ); - } + MFEM_VERIFY(domainMapper.GetDimension() == f.mesh->Dimension(), + "The rotational-displacement-force mapper dimension does not " + "match the mesh dimension."); - void validate_density( - const mean_field::fem::FEM &f, - const mfem::Vector &density, - const char *message - ) { - MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message); - validate_finite_vector(density, message); - } + MFEM_VERIFY(f.displacementFes->GetVDim() == f.mesh->Dimension(), + "The rotational-displacement-force displacement dimension does " + "not match the mesh dimension."); - void apply_rotational_displacement_force_action( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mean_field::physics::RigidRotation &rotation, - const RotationalDisplacementForceAction requestedAction, - const mfem::Vector *baseDensityTrue, - const mfem::Vector *densityVariationTrue, - const mfem::Vector *displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - validate_common_inputs(f, domainMapper, displacementTrue); + validate_finite_vector( + displacementTrue, + "The rotational-displacement-force displacement contains a " + "non-finite value."); +} - const bool needsBaseDensity = requestedAction == RotationalDisplacementForceAction::residual || - requestedAction == RotationalDisplacementForceAction::displacement || - requestedAction == RotationalDisplacementForceAction::complete; +void validate_density(const mean_field::fem::FEM &f, + const mfem::Vector &density, const char *message) { + MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message); + validate_finite_vector(density, message); +} - const bool needsDensityVariation = requestedAction == RotationalDisplacementForceAction::density || - requestedAction == RotationalDisplacementForceAction::complete; +void apply_rotational_displacement_force_action( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mean_field::physics::RigidRotation &rotation, + const RotationalDisplacementForceAction requestedAction, + const mfem::Vector *baseDensityTrue, + const mfem::Vector *densityVariationTrue, + const mfem::Vector *displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + validate_common_inputs(f, domainMapper, displacementTrue); - const bool needsDisplacementVariation = requestedAction == RotationalDisplacementForceAction::displacement || - requestedAction == RotationalDisplacementForceAction::complete; + const bool needsBaseDensity = + requestedAction == RotationalDisplacementForceAction::residual || + requestedAction == RotationalDisplacementForceAction::displacement || + requestedAction == RotationalDisplacementForceAction::complete; - if (needsBaseDensity) { - MFEM_VERIFY( - baseDensityTrue != nullptr, "The rotational-displacement-force action requires a base " - "density." - ); + const bool needsDensityVariation = + requestedAction == RotationalDisplacementForceAction::density || + requestedAction == RotationalDisplacementForceAction::complete; - validate_density(f, *baseDensityTrue, "The rotational-displacement-force base density is invalid."); - } + const bool needsDisplacementVariation = + requestedAction == RotationalDisplacementForceAction::displacement || + requestedAction == RotationalDisplacementForceAction::complete; - if (needsDensityVariation) { - MFEM_VERIFY( - densityVariationTrue != nullptr, "The rotational-displacement-force action requires a " - "density variation." - ); + if (needsBaseDensity) { + MFEM_VERIFY(baseDensityTrue != nullptr, + "The rotational-displacement-force action requires a base " + "density."); - validate_density( - f, *densityVariationTrue, - "The rotational-displacement-force density variation is " - "invalid." - ); - } + validate_density( + f, *baseDensityTrue, + "The rotational-displacement-force base density is invalid."); + } - if (needsDisplacementVariation) { - MFEM_VERIFY( - displacementVariationTrue != nullptr && - displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(), + if (needsDensityVariation) { + MFEM_VERIFY(densityVariationTrue != nullptr, + "The rotational-displacement-force action requires a " + "density variation."); + + validate_density(f, *densityVariationTrue, + "The rotational-displacement-force density variation is " + "invalid."); + } + + if (needsDisplacementVariation) { + MFEM_VERIFY(displacementVariationTrue != nullptr && + displacementVariationTrue->Size() == + f.displacementFes->GetTrueVSize(), "The rotational-displacement-force displacement variation " - "is invalid." - ); + "is invalid."); - validate_finite_vector( - *displacementVariationTrue, "The rotational-displacement-force displacement variation " - "contains a non-finite value." - ); - } + validate_finite_vector( + *displacementVariationTrue, + "The rotational-displacement-force displacement variation " + "contains a non-finite value."); + } - mfem::Vector baseDensityLocal; - mfem::Vector densityVariationLocal; - mfem::Vector displacementLocal; - mfem::Vector displacementVariationLocal; + mfem::Vector baseDensityLocal; + mfem::Vector densityVariationLocal; + mfem::Vector displacementLocal; + mfem::Vector displacementVariationLocal; - if (needsBaseDensity) { - true_to_local(*f.densityFes, *baseDensityTrue, baseDensityLocal); - } + if (needsBaseDensity) { + true_to_local(*f.densityFes, *baseDensityTrue, baseDensityLocal); + } + if (needsDensityVariation) { + true_to_local(*f.densityFes, *densityVariationTrue, densityVariationLocal); + } + + true_to_local(*f.displacementFes, displacementTrue, displacementLocal); + + if (needsDisplacementVariation) { + true_to_local(*f.displacementFes, *displacementVariationTrue, + displacementVariationLocal); + } + + mfem::Vector localAction(f.displacementFes->GetVSize()); + localAction = 0.0; + + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); + + mfem::Array densityDofs; + mfem::Array displacementDofs; + mfem::Array compactificationDofs; + + mfem::Vector elementBaseDensity; + mfem::Vector elementDensityVariation; + mfem::Vector elementDisplacement; + mfem::Vector elementDisplacementVariation; + mfem::Vector elementCompactification; + mfem::Vector elementAction; + + mfem::Vector densityShape; + mfem::Vector displacementShape; + mfem::Vector potentialGradient; + mfem::Vector potentialGradientVariation; + mfem::Vector centrifugalAcceleration; + mfem::Vector centrifugalAccelerationVariation; + mfem::Vector weightedForce; + + mean_field::mapping::VolumeMappingContext mappingContext; + mean_field::mapping::VolumeMappingVariation mappingVariation; + + const int dimension = f.mesh->Dimension(); + + const mfem::Ordering::Type displacementOrdering = + f.displacementFes->GetOrdering(); + + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elementId); + + MFEM_VERIFY(transformation != nullptr, + "The rotational-displacement-force kernel received a null " + "element transformation."); + + if (is_vacuum_attribute(transformation->Attribute)) { + continue; + } + + const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId); + + const mfem::FiniteElement &displacementElement = + *f.displacementFes->GetFE(elementId); + + const mfem::FiniteElement &compactificationElement = + *f.compactificationFes->GetFE(elementId); + + mfem::DofTransformation *densityDofTransformation = + f.densityFes->GetElementDofs(elementId, densityDofs); + + mfem::DofTransformation *displacementDofTransformation = + f.displacementFes->GetElementVDofs(elementId, displacementDofs); + + mfem::DofTransformation *compactificationDofTransformation = + f.compactificationFes->GetElementDofs(elementId, compactificationDofs); + + if (needsBaseDensity) { + baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity); + } + + if (needsDensityVariation) { + densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation); + } + + displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + + if (needsDisplacementVariation) { + displacementVariationLocal.GetSubVector(displacementDofs, + elementDisplacementVariation); + } + + f.compactificationCoordinate->GetSubVector(compactificationDofs, + elementCompactification); + + if (densityDofTransformation != nullptr) { + if (needsBaseDensity) { + densityDofTransformation->InvTransformPrimal(elementBaseDensity); + } + + if (needsDensityVariation) { + densityDofTransformation->InvTransformPrimal(elementDensityVariation); + } + } + + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->InvTransformPrimal(elementDisplacement); + + if (needsDisplacementVariation) { + displacementDofTransformation->InvTransformPrimal( + elementDisplacementVariation); + } + } + + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal( + elementCompactification); + } + + const mean_field::mapping::ElementDisplacementData displacementData = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacement); + + const mean_field::mapping::ElementCompactificationData compactificationData( + compactificationElement, elementCompactification); + + const mean_field::mapping::ElementMappingData mappingData{ + .displacement = displacementData, + .compactification = compactificationData}; + + std::optional + displacementVariationData; + + if (needsDisplacementVariation) { + displacementVariationData.emplace( + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacementVariation)); + } + + const int scalarDisplacementDofCount = displacementElement.GetDof(); + + MFEM_VERIFY(displacementDofs.Size() == + scalarDisplacementDofCount * dimension, + "The rotational-displacement-force element displacement " + "vector has the wrong size."); + + densityShape.SetSize(densityElement.GetDof()); + displacementShape.SetSize(scalarDisplacementDofCount); + potentialGradient.SetSize(dimension); + potentialGradientVariation.SetSize(dimension); + centrifugalAcceleration.SetSize(dimension); + centrifugalAccelerationVariation.SetSize(dimension); + weightedForce.SetSize(dimension); + + elementAction.SetSize(displacementDofs.Size()); + elementAction = 0.0; + + const mfem::IntegrationRule &integrationRule = get_rotation_force_rule( + f, densityElement, displacementElement, *transformation); + + for (int quadratureIndex = 0; + quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) { + const mfem::IntegrationPoint &integrationPoint = + integrationRule.IntPoint(quadratureIndex); + + transformation->SetIntPoint(&integrationPoint); + + const mean_field::mapping::MappingStatus mappingStatus = + domainMapper.EvaluateVolume(mappingData, *transformation, + integrationPoint, workspace, + mappingContext); + + MFEM_VERIFY(mappingStatus == mean_field::mapping::MappingStatus::valid, + "Stateless mapping failed in the rotational-" + "displacement-force kernel. Element: " + << elementId + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadratureIndex + << ", status: " << static_cast(mappingStatus)); + + if (needsDisplacementVariation) { + const mean_field::mapping::MappingStatus variationStatus = + domainMapper.EvaluateVolumeVariation( + mappingData, *displacementVariationData, *transformation, + integrationPoint, mappingContext, workspace, mappingVariation); + + MFEM_VERIFY( + variationStatus == mean_field::mapping::MappingStatus::valid, + "Stateless mapping variation failed in the " + "rotational-displacement-force kernel. Element: " + << elementId << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadratureIndex + << ", status: " << static_cast(variationStatus)); + } + + densityElement.CalcShape(integrationPoint, densityShape); + + displacementElement.CalcShape(integrationPoint, displacementShape); + + double baseDensityValue = 0.0; + double densityVariationValue = 0.0; + + if (needsBaseDensity) { + baseDensityValue = elementBaseDensity * densityShape; + } + + if (needsDensityVariation) { + densityVariationValue = elementDensityVariation * densityShape; + } + + rotation.potential_gradient(mappingContext.mapping.physical_position, + potentialGradient); + + centrifugalAcceleration = potentialGradient; + centrifugalAcceleration *= -1.0; + + if (needsDisplacementVariation) { + rotation.potential_gradient_directional_derivative( + mappingVariation.mapping.physical_position_variation, + potentialGradientVariation); + + centrifugalAccelerationVariation = potentialGradientVariation; + + centrifugalAccelerationVariation *= -1.0; + } else { + centrifugalAccelerationVariation = 0.0; + } + + weightedForce = 0.0; + + if (requestedAction == RotationalDisplacementForceAction::residual) { + weightedForce.Add(baseDensityValue * mappingContext.quadrature.weight, + centrifugalAcceleration); + } else { if (needsDensityVariation) { - true_to_local(*f.densityFes, *densityVariationTrue, densityVariationLocal); + weightedForce.Add(densityVariationValue * + mappingContext.quadrature.weight, + centrifugalAcceleration); } - true_to_local(*f.displacementFes, displacementTrue, displacementLocal); - if (needsDisplacementVariation) { - true_to_local(*f.displacementFes, *displacementVariationTrue, displacementVariationLocal); + weightedForce.Add(baseDensityValue * mappingContext.quadrature.weight, + centrifugalAccelerationVariation); + + weightedForce.Add(baseDensityValue * + mappingVariation.weight_variation, + centrifugalAcceleration); } + } - mfem::Vector localAction(f.displacementFes->GetVSize()); - localAction = 0.0; + for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; + ++scalarDof) { + for (int component = 0; component < dimension; ++component) { + const int vectorDof = + vector_dof_index(displacementOrdering, scalarDof, component, + scalarDisplacementDofCount, dimension); - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + const double contribution = + displacementShape(scalarDof) * weightedForce(component); - mfem::Array densityDofs; - mfem::Array displacementDofs; - mfem::Array compactificationDofs; + MFEM_VERIFY(std::isfinite(contribution), + "The rotational-displacement-force kernel " + "encountered a non-finite contribution."); - mfem::Vector elementBaseDensity; - mfem::Vector elementDensityVariation; - mfem::Vector elementDisplacement; - mfem::Vector elementDisplacementVariation; - mfem::Vector elementCompactification; - mfem::Vector elementAction; - - mfem::Vector densityShape; - mfem::Vector displacementShape; - mfem::Vector potentialGradient; - mfem::Vector potentialGradientVariation; - mfem::Vector centrifugalAcceleration; - mfem::Vector centrifugalAccelerationVariation; - mfem::Vector weightedForce; - - mean_field::mapping::VolumeMappingContext mappingContext; - mean_field::mapping::VolumeMappingVariation mappingVariation; - - const int dimension = f.mesh->Dimension(); - const int vacuumAttribute = domainMapper.GetVacuumElementAttribute(); - - const mfem::Ordering::Type displacementOrdering = f.displacementFes->GetOrdering(); - - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); - - MFEM_VERIFY( - transformation != nullptr, "The rotational-displacement-force kernel received a null " - "element transformation." - ); - - if (transformation->Attribute == vacuumAttribute) { - continue; - } - - const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId); - - const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId); - - const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId); - - mfem::DofTransformation *densityDofTransformation = f.densityFes->GetElementDofs(elementId, densityDofs); - - mfem::DofTransformation *displacementDofTransformation = - f.displacementFes->GetElementVDofs(elementId, displacementDofs); - - mfem::DofTransformation *compactificationDofTransformation = - f.compactificationFes->GetElementDofs(elementId, compactificationDofs); - - if (needsBaseDensity) { - baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity); - } - - if (needsDensityVariation) { - densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation); - } - - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); - - if (needsDisplacementVariation) { - displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation); - } - - f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); - - if (densityDofTransformation != nullptr) { - if (needsBaseDensity) { - densityDofTransformation->InvTransformPrimal(elementBaseDensity); - } - - if (needsDensityVariation) { - densityDofTransformation->InvTransformPrimal(elementDensityVariation); - } - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacement); - - if (needsDisplacementVariation) { - displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation); - } - } - - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal(elementCompactification); - } - - const mean_field::mapping::ElementDisplacementData displacementData = - mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement); - - const mean_field::mapping::ElementCompactificationData compactificationData( - compactificationElement, elementCompactification - ); - - const mean_field::mapping::ElementMappingData mappingData{ - .displacement = displacementData, .compactification = compactificationData - }; - - std::optional displacementVariationData; - - if (needsDisplacementVariation) { - displacementVariationData.emplace( - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacementElement, elementDisplacementVariation - ) - ); - } - - const int scalarDisplacementDofCount = displacementElement.GetDof(); - - MFEM_VERIFY( - displacementDofs.Size() == scalarDisplacementDofCount * dimension, - "The rotational-displacement-force element displacement " - "vector has the wrong size." - ); - - densityShape.SetSize(densityElement.GetDof()); - displacementShape.SetSize(scalarDisplacementDofCount); - potentialGradient.SetSize(dimension); - potentialGradientVariation.SetSize(dimension); - centrifugalAcceleration.SetSize(dimension); - centrifugalAccelerationVariation.SetSize(dimension); - weightedForce.SetSize(dimension); - - elementAction.SetSize(displacementDofs.Size()); - elementAction = 0.0; - - const mfem::IntegrationRule &integrationRule = - get_rotation_force_rule(f, densityElement, displacementElement, *transformation); - - for (int quadratureIndex = 0; quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) { - const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadratureIndex); - - transformation->SetIntPoint(&integrationPoint); - - const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume( - mappingData, *transformation, integrationPoint, workspace, mappingContext - ); - - MFEM_VERIFY( - mappingStatus == mean_field::mapping::MappingStatus::valid, - "Stateless mapping failed in the rotational-" - "displacement-force kernel. Element: " - << elementId << ", attribute: " << transformation->Attribute - << ", quadrature point: " << quadratureIndex << ", status: " << static_cast(mappingStatus) - ); - - if (needsDisplacementVariation) { - const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation( - mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext, - workspace, mappingVariation - ); - - MFEM_VERIFY( - variationStatus == mean_field::mapping::MappingStatus::valid, - "Stateless mapping variation failed in the " - "rotational-displacement-force kernel. Element: " - << elementId << ", attribute: " << transformation->Attribute << ", quadrature point: " - << quadratureIndex << ", status: " << static_cast(variationStatus) - ); - } - - densityElement.CalcShape(integrationPoint, densityShape); - - displacementElement.CalcShape(integrationPoint, displacementShape); - - double baseDensityValue = 0.0; - double densityVariationValue = 0.0; - - if (needsBaseDensity) { - baseDensityValue = elementBaseDensity * densityShape; - } - - if (needsDensityVariation) { - densityVariationValue = elementDensityVariation * densityShape; - } - - rotation.potential_gradient(mappingContext.mapping.physical_position, potentialGradient); - - centrifugalAcceleration = potentialGradient; - centrifugalAcceleration *= -1.0; - - if (needsDisplacementVariation) { - rotation.potential_gradient_directional_derivative( - mappingVariation.mapping.physical_position_variation, potentialGradientVariation - ); - - centrifugalAccelerationVariation = potentialGradientVariation; - - centrifugalAccelerationVariation *= -1.0; - } else { - centrifugalAccelerationVariation = 0.0; - } - - weightedForce = 0.0; - - if (requestedAction == RotationalDisplacementForceAction::residual) { - weightedForce.Add(baseDensityValue * mappingContext.quadrature.weight, centrifugalAcceleration); - } else { - if (needsDensityVariation) { - weightedForce.Add( - densityVariationValue * mappingContext.quadrature.weight, centrifugalAcceleration - ); - } - - if (needsDisplacementVariation) { - weightedForce.Add( - baseDensityValue * mappingContext.quadrature.weight, centrifugalAccelerationVariation - ); - - weightedForce.Add( - baseDensityValue * mappingVariation.weight_variation, centrifugalAcceleration - ); - } - } - - for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) { - for (int component = 0; component < dimension; ++component) { - const int vectorDof = vector_dof_index( - displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension - ); - - const double contribution = displacementShape(scalarDof) * weightedForce(component); - - MFEM_VERIFY( - std::isfinite(contribution), "The rotational-displacement-force kernel " - "encountered a non-finite contribution." - ); - - elementAction(vectorDof) += contribution; - } - } - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(displacementDofs, elementAction); + elementAction(vectorDof) += contribution; } - - local_to_true(*f.displacementFes, localAction, actionTrue); + } } + + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->TransformDual(elementAction); + } + + localAction.AddElementVector(displacementDofs, elementAction); + } + + local_to_true(*f.displacementFes, localAction, actionTrue); +} } // namespace namespace mean_field::operators::kernels { - void apply_rotational_displacement_force_residual( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const physics::RigidRotation &rotation, - const mfem::Vector &densityTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &residualTrue - ) { - apply_rotational_displacement_force_action( - f, domainMapper, rotation, RotationalDisplacementForceAction::residual, &densityTrue, nullptr, nullptr, - displacementTrue, residualTrue - ); - } +void apply_rotational_displacement_force_residual( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const physics::RigidRotation &rotation, const mfem::Vector &densityTrue, + const mfem::Vector &displacementTrue, mfem::Vector &residualTrue) { + apply_rotational_displacement_force_action( + f, domainMapper, rotation, RotationalDisplacementForceAction::residual, + &densityTrue, nullptr, nullptr, displacementTrue, residualTrue); +} - void apply_rotational_displacement_force_density_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const physics::RigidRotation &rotation, - const mfem::Vector &densityVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_rotational_displacement_force_action( - f, domainMapper, rotation, RotationalDisplacementForceAction::density, nullptr, &densityVariationTrue, - nullptr, displacementTrue, actionTrue - ); - } +void apply_rotational_displacement_force_density_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const physics::RigidRotation &rotation, + const mfem::Vector &densityVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_rotational_displacement_force_action( + f, domainMapper, rotation, RotationalDisplacementForceAction::density, + nullptr, &densityVariationTrue, nullptr, displacementTrue, actionTrue); +} - void apply_rotational_displacement_force_displacement_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const physics::RigidRotation &rotation, - const mfem::Vector &baseDensityTrue, - const mfem::Vector &displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_rotational_displacement_force_action( - f, domainMapper, rotation, RotationalDisplacementForceAction::displacement, &baseDensityTrue, nullptr, - &displacementVariationTrue, displacementTrue, actionTrue - ); - } +void apply_rotational_displacement_force_displacement_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const physics::RigidRotation &rotation, const mfem::Vector &baseDensityTrue, + const mfem::Vector &displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_rotational_displacement_force_action( + f, domainMapper, rotation, + RotationalDisplacementForceAction::displacement, &baseDensityTrue, + nullptr, &displacementVariationTrue, displacementTrue, actionTrue); +} - void apply_rotational_displacement_force_complete_action( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const physics::RigidRotation &rotation, - const mfem::Vector &baseDensityTrue, - const mfem::Vector &densityVariationTrue, - const mfem::Vector &displacementVariationTrue, - const mfem::Vector &displacementTrue, - mfem::Vector &actionTrue - ) { - apply_rotational_displacement_force_action( - f, domainMapper, rotation, RotationalDisplacementForceAction::complete, &baseDensityTrue, - &densityVariationTrue, &displacementVariationTrue, displacementTrue, actionTrue - ); - } +void apply_rotational_displacement_force_complete_action( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const physics::RigidRotation &rotation, const mfem::Vector &baseDensityTrue, + const mfem::Vector &densityVariationTrue, + const mfem::Vector &displacementVariationTrue, + const mfem::Vector &displacementTrue, mfem::Vector &actionTrue) { + apply_rotational_displacement_force_action( + f, domainMapper, rotation, RotationalDisplacementForceAction::complete, + &baseDensityTrue, &densityVariationTrue, &displacementVariationTrue, + displacementTrue, actionTrue); +} } // namespace mean_field::operators::kernels diff --git a/libmeanfield/impl/operators/prepared_barotropic_closure.cpp b/libmeanfield/impl/operators/prepared_barotropic_closure.cpp index 5da77ca..053c1b0 100644 --- a/libmeanfield/impl/operators/prepared_barotropic_closure.cpp +++ b/libmeanfield/impl/operators/prepared_barotropic_closure.cpp @@ -183,7 +183,7 @@ namespace mean_field::operators { PreparedBarotropicClosureOperator::PreparedBarotropicClosureOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState ) : PreparedBarotropicClosureOperator( @@ -196,7 +196,7 @@ namespace mean_field::operators { PreparedBarotropicClosureOperator::PreparedBarotropicClosureOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, ConstructionData constructionData ) @@ -285,7 +285,7 @@ namespace mean_field::operators { true_to_local(*m_fem.enthalpyFes, m_baseEnthalpyTrue, baseEnthalpyLocal); true_to_local(*m_fem.displacementFes, m_baseDisplacementTrue, displacementLocal); - mapping::DomainMapperStateless::Workspace workspace(m_fem.mesh->Dimension()); + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); mfem::Array displacementDofs; mfem::Array compactificationDofs; diff --git a/libmeanfield/impl/operators/prepared_displacement_operator.cpp b/libmeanfield/impl/operators/prepared_displacement_operator.cpp index 8e5078c..21d17cf 100644 --- a/libmeanfield/impl/operators/prepared_displacement_operator.cpp +++ b/libmeanfield/impl/operators/prepared_displacement_operator.cpp @@ -76,7 +76,7 @@ namespace { namespace mean_field::operators { PreparedDisplacementResidualOperator::PreparedDisplacementResidualOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const context::gravity_field::GravityFieldLinearizationContext &gravityContext ) diff --git a/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp b/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp index 024921f..8d0bdd6 100644 --- a/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp +++ b/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp @@ -20,7 +20,7 @@ namespace { namespace mean_field::operators { PreparedGravityDisplacementForceOperator::PreparedGravityDisplacementForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const context::gravity_field::GravityFieldLinearizationContext &gravityContext ) : m_fem(f), diff --git a/libmeanfield/impl/operators/prepared_gravity_source.cpp b/libmeanfield/impl/operators/prepared_gravity_source.cpp index 554b645..11c13ef 100644 --- a/libmeanfield/impl/operators/prepared_gravity_source.cpp +++ b/libmeanfield/impl/operators/prepared_gravity_source.cpp @@ -9,533 +9,531 @@ module mean_field; import :operators.prepared_gravity_source; namespace { - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - int get_operator_height(const mean_field::fem::FEM &f) { - MFEM_VERIFY( - f.gravityPotentialFes != nullptr, "PreparedMappedGravitySourceOperator requires the " - "gravity-potential " - "finite-element space." - ); - return mean_field::field::make_field_dof_map(*f.gravityPotentialFes) - .reduced_size(); - } +int get_operator_height(const mean_field::fem::FEM &f) { + MFEM_VERIFY(f.gravityPotentialFes != nullptr, + "PreparedMappedGravitySourceOperator requires the " + "gravity-potential " + "finite-element space."); + return mean_field::field::make_field_dof_map( + *f.gravityPotentialFes) + .reduced_size(); +} - int get_operator_width(const mean_field::fem::FEM &f) { - MFEM_VERIFY( - f.densityFes != nullptr, "PreparedMappedGravitySourceOperator requires the density " - "finite-element space." - ); - return mean_field::field::make_field_dof_map(*f.densityFes) - .reduced_size(); - } +int get_operator_width(const mean_field::fem::FEM &f) { + MFEM_VERIFY(f.densityFes != nullptr, + "PreparedMappedGravitySourceOperator requires the density " + "finite-element space."); + return mean_field::field::make_field_dof_map(*f.densityFes) + .reduced_size(); +} - void true_to_local( - const mfem::ParFiniteElementSpace &finite_element_space, - const mfem::Vector &true_vector, - mfem::Vector &local_vector - ) { - local_vector.SetSize(finite_element_space.GetVSize()); +void true_to_local(const mfem::ParFiniteElementSpace &finite_element_space, + const mfem::Vector &true_vector, + mfem::Vector &local_vector) { + local_vector.SetSize(finite_element_space.GetVSize()); - const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); + const mfem::Operator *prolongation = + finite_element_space.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->Mult(true_vector, local_vector); - } else { - local_vector = true_vector; - } - } + if (prolongation != nullptr) { + prolongation->Mult(true_vector, local_vector); + } else { + local_vector = true_vector; + } +} - void local_to_true( - const mfem::ParFiniteElementSpace &finite_element_space, - const mfem::Vector &local_vector, - mfem::Vector &true_vector - ) { - MFEM_VERIFY(local_vector.Size() == finite_element_space.GetVSize(), "Local vector has the wrong size."); +void local_to_true(const mfem::ParFiniteElementSpace &finite_element_space, + const mfem::Vector &local_vector, + mfem::Vector &true_vector) { + MFEM_VERIFY(local_vector.Size() == finite_element_space.GetVSize(), + "Local vector has the wrong size."); - true_vector.SetSize(finite_element_space.GetTrueVSize()); - true_vector = 0.0; + true_vector.SetSize(finite_element_space.GetTrueVSize()); + true_vector = 0.0; - const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); + const mfem::Operator *prolongation = + finite_element_space.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->MultTranspose(local_vector, true_vector); - } else { - true_vector = local_vector; - } - } + if (prolongation != nullptr) { + prolongation->MultTranspose(local_vector, true_vector); + } else { + true_vector = local_vector; + } +} - const mfem::IntegrationRule &get_source_rule( - const mean_field::fem::FEM &f, - const mfem::FiniteElement &density_element, - const mfem::FiniteElement &potential_element, - const mfem::ElementTransformation &transformation - ) { - using GravityField = mean_field::field::Field; - MFEM_VERIFY( - density_element.GetOrder() == mean_field::field::Density::Scalar::familyOrder, - "The prepared source trial element does not match the registered " - "density field." - ); - MFEM_VERIFY( - potential_element.GetOrder() == mean_field::field::Gravity::Potential::familyOrder, - "The prepared source test element does not match the registered " - "gravity potential." - ); - const mean_field::quadrature::Query query = - GravityField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); +const mfem::IntegrationRule & +get_source_rule(const mean_field::fem::FEM &f, + const mfem::FiniteElement &density_element, + const mfem::FiniteElement &potential_element, + const mfem::ElementTransformation &transformation) { + using GravityField = mean_field::field::Field; + MFEM_VERIFY(density_element.GetOrder() == + mean_field::field::Density::Scalar::familyOrder, + "The prepared source trial element does not match the registered " + "density field."); + MFEM_VERIFY(potential_element.GetOrder() == + mean_field::field::Gravity::Potential::familyOrder, + "The prepared source test element does not match the registered " + "gravity potential."); + const mean_field::quadrature::Query query = GravityField::make_query< + mean_field::field::Gravity::Form::SourceProjection>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - return *f.quadratureFactory->get(query, transformation.GetGeometryType()).integration_rule; - } + return *f.quadratureFactory->get(query, transformation.GetGeometryType()) + .integration_rule; +} - class FrozenMappedGravitySourceCoefficient final : public mfem::Coefficient { - public: - FrozenMappedGravitySourceCoefficient( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &displacement_true - ) - : m_fem(f), - m_domain_mapper(domain_mapper), - m_workspace(domain_mapper.GetDimension()) { - true_to_local(*m_fem.displacementFes, displacement_true, m_displacement_local); - } +class FrozenMappedGravitySourceCoefficient final : public mfem::Coefficient { +public: + FrozenMappedGravitySourceCoefficient( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domain_mapper, + const mfem::Vector &displacement_true) + : m_fem(f), m_domain_mapper(domain_mapper), + m_workspace(domain_mapper.GetDimension()) { + true_to_local(*m_fem.displacementFes, displacement_true, + m_displacement_local); + } - double Eval( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point - ) override { - transformation.SetIntPoint(&integration_point); + double Eval(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) override { + transformation.SetIntPoint(&integration_point); - const int element_id = transformation.ElementNo; - MFEM_VERIFY( - element_id >= 0 && element_id < m_fem.mesh->GetNE(), + const int element_id = transformation.ElementNo; + MFEM_VERIFY(element_id >= 0 && element_id < m_fem.mesh->GetNE(), "Mapped gravity source coefficient received an invalid element " - "ID." - ); - if (transformation.Attribute == m_domain_mapper.GetVacuumElementAttribute()) { - return 0.0; - } + "ID."); + if (DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(transformation.Attribute)) { + return 0.0; + } - LoadElement(element_id); - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = *m_displacement_data, .compactification = *m_compactification_data - }; + LoadElement(element_id); + const mean_field::mapping::ElementMappingData mapping_data{ + .displacement = *m_displacement_data, + .compactification = *m_compactification_data}; - mean_field::mapping::VolumeMappingContext mapping_context; + mean_field::mapping::VolumeMappingContext mapping_context; - const mean_field::mapping::MappingStatus status = m_domain_mapper.EvaluateVolume( - mapping_data, transformation, integration_point, m_workspace, mapping_context - ); + const mean_field::mapping::MappingStatus status = + m_domain_mapper.EvaluateVolume(mapping_data, transformation, + integration_point, m_workspace, + mapping_context); - if (status != mean_field::mapping::MappingStatus::valid) { - const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id); + if (status != mean_field::mapping::MappingStatus::valid) { + const mfem::FiniteElement &displacement_element = + *m_fem.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *m_fem.compactificationFes->GetFE(element_id); - mfem::Vector displacement_shape(displacement_element.GetDof()); - mfem::Vector compactification_shape(compactification_element.GetDof()); - mfem::Vector reference_position(m_domain_mapper.GetDimension()); - mfem::Vector displacement_value(m_domain_mapper.GetDimension()); + mfem::Vector displacement_shape(displacement_element.GetDof()); + mfem::Vector compactification_shape(compactification_element.GetDof()); + mfem::Vector reference_position(m_domain_mapper.GetDimension()); + mfem::Vector displacement_value(m_domain_mapper.GetDimension()); - displacement_element.CalcShape(integration_point, displacement_shape); - compactification_element.CalcShape(integration_point, compactification_shape); - transformation.Transform(integration_point, reference_position); - m_displacement_data->GetDofMatrix().MultTranspose(displacement_shape, displacement_value); + displacement_element.CalcShape(integration_point, displacement_shape); + compactification_element.CalcShape(integration_point, + compactification_shape); + transformation.Transform(integration_point, reference_position); + m_displacement_data->GetDofMatrix().MultTranspose(displacement_shape, + displacement_value); - const double compactification_coordinate = m_compactification_data->GetDofs() * compactification_shape; + const double compactification_coordinate = + m_compactification_data->GetDofs() * compactification_shape; - MFEM_ABORT( - "Stateless domain mapping failed while preparing the " - "gravity " - "source operator." - << "\nMapping status = " << static_cast(status) << "\nElement ID = " << element_id - << "\nElement attribute = " << transformation.Attribute - << "\nIntegration-point index = " << integration_point.index << "\nIntegration point = <" - << integration_point.x << ", " << integration_point.y << ", " << integration_point.z << ">" - << "\nReference position = <" << reference_position(0) << ", " << reference_position(1) << ", " - << reference_position(2) << ">" - << "\nReference radius = " << reference_position.Norml2() << "\nDisplacement value = <" - << displacement_value(0) << ", " << displacement_value(1) << ", " << displacement_value(2) << ">" - << "\nDisplacement magnitude = " << displacement_value.Norml2() - << "\nCompactification coordinate = " << compactification_coordinate - << "\nDisplacement ordering = " << static_cast(m_fem.displacementFes->GetOrdering()) - ); - } - const double mapping_determinant = mapping_context.mapping.mapping_determinant; - MFEM_VERIFY( - std::isfinite(mapping_determinant) && mapping_determinant > 0.0, + MFEM_ABORT( + "Stateless domain mapping failed while preparing the " + "gravity " + "source operator." + << "\nMapping status = " << static_cast(status) + << "\nElement ID = " << element_id + << "\nElement attribute = " << transformation.Attribute + << "\nIntegration-point index = " << integration_point.index + << "\nIntegration point = <" << integration_point.x << ", " + << integration_point.y << ", " << integration_point.z << ">" + << "\nReference position = <" << reference_position(0) << ", " + << reference_position(1) << ", " << reference_position(2) << ">" + << "\nReference radius = " << reference_position.Norml2() + << "\nDisplacement value = <" << displacement_value(0) << ", " + << displacement_value(1) << ", " << displacement_value(2) << ">" + << "\nDisplacement magnitude = " << displacement_value.Norml2() + << "\nCompactification coordinate = " << compactification_coordinate + << "\nDisplacement ordering = " + << static_cast(m_fem.displacementFes->GetOrdering())); + } + const double mapping_determinant = + mapping_context.mapping.mapping_determinant; + MFEM_VERIFY(std::isfinite(mapping_determinant) && mapping_determinant > 0.0, "Prepared gravity source operator encountered a non-positive " "or " - "non-finite mapping determinant." - ); + "non-finite mapping determinant."); - return 4.0 * std::numbers::pi * mean_field::utils::G * mapping_determinant; - } + return 4.0 * std::numbers::pi * mean_field::utils::G * mapping_determinant; + } - private: - void LoadElement(const int element_id) { - if (element_id == m_cached_element_id) { - return; - } +private: + void LoadElement(const int element_id) { + if (element_id == m_cached_element_id) { + return; + } - const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id); + const mfem::FiniteElement &displacement_element = + *m_fem.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *m_fem.compactificationFes->GetFE(element_id); - mfem::DofTransformation *displacement_dof_transformation = - m_fem.displacementFes->GetElementVDofs(element_id, m_displacement_dofs); - mfem::DofTransformation *compactification_dof_transformation = - m_fem.compactificationFes->GetElementDofs(element_id, m_compactification_dofs); + mfem::DofTransformation *displacement_dof_transformation = + m_fem.displacementFes->GetElementVDofs(element_id, m_displacement_dofs); + mfem::DofTransformation *compactification_dof_transformation = + m_fem.compactificationFes->GetElementDofs(element_id, + m_compactification_dofs); - m_displacement_local.GetSubVector(m_displacement_dofs, m_element_displacement); - m_fem.compactificationCoordinate->GetSubVector(m_compactification_dofs, m_element_compactification); + m_displacement_local.GetSubVector(m_displacement_dofs, + m_element_displacement); + m_fem.compactificationCoordinate->GetSubVector(m_compactification_dofs, + m_element_compactification); - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(m_element_displacement); - } + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal( + m_element_displacement); + } - if (compactification_dof_transformation != nullptr) { - compactification_dof_transformation->InvTransformPrimal(m_element_compactification); - } + if (compactification_dof_transformation != nullptr) { + compactification_dof_transformation->InvTransformPrimal( + m_element_compactification); + } - m_displacement_data = std::make_unique( - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, m_element_displacement - ) - ); + m_displacement_data = + std::make_unique( + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, m_element_displacement)); - m_compactification_data = std::make_unique( - compactification_element, m_element_compactification - ); + m_compactification_data = + std::make_unique( + compactification_element, m_element_compactification); - m_cached_element_id = element_id; - } + m_cached_element_id = element_id; + } - const mean_field::fem::FEM &m_fem; - const mean_field::mapping::DomainMapperStateless &m_domain_mapper; + const mean_field::fem::FEM &m_fem; + const mean_field::mapping::DomainMapper &m_domain_mapper; - mfem::Vector m_displacement_local; + mfem::Vector m_displacement_local; - mfem::Array m_displacement_dofs; - mfem::Array m_compactification_dofs; + mfem::Array m_displacement_dofs; + mfem::Array m_compactification_dofs; - mfem::Vector m_element_displacement; - mfem::Vector m_element_compactification; + mfem::Vector m_element_displacement; + mfem::Vector m_element_compactification; - std::unique_ptr m_displacement_data; - std::unique_ptr m_compactification_data; + std::unique_ptr + m_displacement_data; + std::unique_ptr + m_compactification_data; - mean_field::mapping::DomainMapperStateless::Workspace m_workspace; - int m_cached_element_id{-1}; - }; + mean_field::mapping::DomainMapper::Workspace m_workspace; + int m_cached_element_id{-1}; +}; } // namespace namespace mean_field::operators { - PreparedMappedGravitySourceOperator::PreparedMappedGravitySourceOperator( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper - ) - : Operator( - get_operator_height(f), - get_operator_width(f) - ), - m_fem(f), - m_domain_mapper(domain_mapper), - m_density_map( - field::make_field_dof_map< - field::Density, - DomainSchema>(*f.densityFes) - ), - m_potential_map( - field::make_field_dof_map< - field::Gravity, - DomainSchema>(*f.gravityPotentialFes) - ), - m_displacement_map( - field::make_field_dof_map< - field::Displacement, - DomainSchema>(*f.displacementFes) - ) { - MFEM_VERIFY(f.mesh != nullptr, "PreparedMappedGravitySourceOperator requires a mesh."); - MFEM_VERIFY( - f.densityFes != nullptr, "PreparedMappedGravitySourceOperator requires the density " - "finite-element space." - ); - MFEM_VERIFY( - f.gravityPotentialFes != nullptr, "PreparedMappedGravitySourceOperator requires the " - "gravity-potential " - "finite-element space." - ); - MFEM_VERIFY( - f.displacementFes != nullptr, "PreparedMappedGravitySourceOperator requires " - "the displacement finite-element space." - ); - MFEM_VERIFY( - f.compactificationFes != nullptr, "PreparedMappedGravitySourceOperator requires the compactification " - "finite-element space." - ); - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, - "PreparedMappedGravitySourceOperator requires the compactification " - "coordinate." - ); - MFEM_VERIFY( - f.quadratureFactory != nullptr, "PreparedMappedGravitySourceOperator " - "requires the quadrature-rule factory." - ); - MFEM_VERIFY( - domain_mapper.GetDimension() == f.mesh->Dimension(), - "The stateless domain-mapper dimension does not match the mesh " - "dimension." - ); +PreparedMappedGravitySourceOperator::PreparedMappedGravitySourceOperator( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper) + : Operator(get_operator_height(f), get_operator_width(f)), m_fem(f), + m_domain_mapper(domain_mapper), + m_density_map(field::make_field_dof_map( + *f.densityFes)), + m_potential_map(field::make_field_dof_map( + *f.gravityPotentialFes)), + m_displacement_map( + field::make_field_dof_map( + *f.displacementFes)) { + MFEM_VERIFY(f.mesh != nullptr, + "PreparedMappedGravitySourceOperator requires a mesh."); + MFEM_VERIFY(f.densityFes != nullptr, + "PreparedMappedGravitySourceOperator requires the density " + "finite-element space."); + MFEM_VERIFY(f.gravityPotentialFes != nullptr, + "PreparedMappedGravitySourceOperator requires the " + "gravity-potential " + "finite-element space."); + MFEM_VERIFY(f.displacementFes != nullptr, + "PreparedMappedGravitySourceOperator requires " + "the displacement finite-element space."); + MFEM_VERIFY( + f.compactificationFes != nullptr, + "PreparedMappedGravitySourceOperator requires the compactification " + "finite-element space."); + MFEM_VERIFY( + f.compactificationCoordinate != nullptr, + "PreparedMappedGravitySourceOperator requires the compactification " + "coordinate."); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "PreparedMappedGravitySourceOperator " + "requires the quadrature-rule factory."); + MFEM_VERIFY(domain_mapper.GetDimension() == f.mesh->Dimension(), + "The stateless domain-mapper dimension does not match the mesh " + "dimension."); - utils::populate_element_mask(f.mesh.get(), utils::DOMAINS::STELLAR, m_stellar_marker); + m_stellar_marker = + utils::domain::make_attribute_marker(*f.mesh); +} + +void PreparedMappedGravitySourceOperator::Prepare( + const mfem::Vector &displacement) { + MFEM_VERIFY(displacement.Size() == m_displacement_map.reduced_size(), + "PreparedMappedGravitySourceOperator received a displacement " + "vector " + "with the wrong size."); + + for (int i = 0; i < displacement.Size(); ++i) { + MFEM_VERIFY(std::isfinite(displacement(i)), + "PreparedMappedGravitySourceOperator received a non-finite " + "displacement value."); + } + + m_is_prepared = false; + m_displacement_true.SetSize(m_displacement_map.full_size()); + m_displacement_map.scatter(displacement, m_displacement_true); + m_elements.clear(); + m_elements.reserve(m_fem.mesh->GetNE()); + + FrozenMappedGravitySourceCoefficient source_coefficient( + m_fem, m_domain_mapper, m_displacement_true); + + for (int element_id = 0; element_id < m_fem.mesh->GetNE(); ++element_id) { + const int attribute = m_fem.mesh->GetAttribute(element_id); + + if (attribute <= 0 || attribute > m_stellar_marker.Size() || + m_stellar_marker[attribute - 1] == 0) { + continue; } - void PreparedMappedGravitySourceOperator::Prepare(const mfem::Vector &displacement) { - MFEM_VERIFY( - displacement.Size() == m_displacement_map.reduced_size(), - "PreparedMappedGravitySourceOperator received a displacement " - "vector " - "with the wrong size." - ); + m_elements.emplace_back(); + ElementPAData &data = m_elements.back(); - for (int i = 0; i < displacement.Size(); ++i) { - MFEM_VERIFY( - std::isfinite(displacement(i)), "PreparedMappedGravitySourceOperator received a non-finite " - "displacement value." - ); - } + data.element_id = element_id; - m_is_prepared = false; - m_displacement_true.SetSize(m_displacement_map.full_size()); - m_displacement_map.scatter(displacement, m_displacement_true); - m_elements.clear(); - m_elements.reserve(m_fem.mesh->GetNE()); + data.density_dof_transformation = + m_fem.densityFes->GetElementDofs(element_id, data.density_dofs); - FrozenMappedGravitySourceCoefficient source_coefficient(m_fem, m_domain_mapper, m_displacement_true); + data.potential_dof_transformation = + m_fem.gravityPotentialFes->GetElementDofs(element_id, + data.potential_dofs); - for (int element_id = 0; element_id < m_fem.mesh->GetNE(); ++element_id) { - const int attribute = m_fem.mesh->GetAttribute(element_id); + const mfem::FiniteElement &density_element = + *m_fem.densityFes->GetFE(element_id); - if (attribute <= 0 || attribute > m_stellar_marker.Size() || m_stellar_marker[attribute - 1] == 0) { - continue; - } + const mfem::FiniteElement &potential_element = + *m_fem.gravityPotentialFes->GetFE(element_id); - m_elements.emplace_back(); - ElementPAData &data = m_elements.back(); + mfem::ElementTransformation &transformation = + *m_fem.mesh->GetElementTransformation(element_id); - data.element_id = element_id; + const mfem::IntegrationRule &integration_rule = get_source_rule( + m_fem, density_element, potential_element, transformation); - data.density_dof_transformation = m_fem.densityFes->GetElementDofs(element_id, data.density_dofs); + const int quadrature_point_count = integration_rule.GetNPoints(); - data.potential_dof_transformation = - m_fem.gravityPotentialFes->GetElementDofs(element_id, data.potential_dofs); + const int density_dof_count = density_element.GetDof(); - const mfem::FiniteElement &density_element = *m_fem.densityFes->GetFE(element_id); + const int potential_dof_count = potential_element.GetDof(); - const mfem::FiniteElement &potential_element = *m_fem.gravityPotentialFes->GetFE(element_id); + data.density_basis.SetSize(quadrature_point_count, density_dof_count); - mfem::ElementTransformation &transformation = *m_fem.mesh->GetElementTransformation(element_id); + data.potential_basis.SetSize(quadrature_point_count, potential_dof_count); - const mfem::IntegrationRule &integration_rule = - get_source_rule(m_fem, density_element, potential_element, transformation); + data.quadrature_data.SetSize(quadrature_point_count); - const int quadrature_point_count = integration_rule.GetNPoints(); + mfem::Vector density_shape(density_dof_count); + mfem::Vector potential_shape(potential_dof_count); - const int density_dof_count = density_element.GetDof(); + for (int quadrature_point = 0; quadrature_point < quadrature_point_count; + ++quadrature_point) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(quadrature_point); - const int potential_dof_count = potential_element.GetDof(); + transformation.SetIntPoint(&integration_point); - data.density_basis.SetSize(quadrature_point_count, density_dof_count); + // CalcPhysShape matches the scalar mixed-mass discretization, + // including the finite-element map type. + density_element.CalcPhysShape(transformation, density_shape); - data.potential_basis.SetSize(quadrature_point_count, potential_dof_count); + potential_element.CalcPhysShape(transformation, potential_shape); - data.quadrature_data.SetSize(quadrature_point_count); + for (int i = 0; i < density_dof_count; ++i) { + data.density_basis(quadrature_point, i) = density_shape(i); + } - mfem::Vector density_shape(density_dof_count); - mfem::Vector potential_shape(potential_dof_count); + for (int i = 0; i < potential_dof_count; ++i) { + data.potential_basis(quadrature_point, i) = potential_shape(i); + } - for (int quadrature_point = 0; quadrature_point < quadrature_point_count; ++quadrature_point) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(quadrature_point); + const double coefficient_value = + source_coefficient.Eval(transformation, integration_point); - transformation.SetIntPoint(&integration_point); + transformation.SetIntPoint(&integration_point); - // CalcPhysShape matches the scalar mixed-mass discretization, - // including the finite-element map type. - density_element.CalcPhysShape(transformation, density_shape); + const double quadrature_value = integration_point.weight * + transformation.Weight() * + coefficient_value; - potential_element.CalcPhysShape(transformation, potential_shape); + MFEM_VERIFY(std::isfinite(quadrature_value) && quadrature_value > 0.0, + "Prepared gravity source operator encountered invalid " + "quadrature data on element " + << element_id << ", quadrature point " << quadrature_point + << "."); - for (int i = 0; i < density_dof_count; ++i) { - data.density_basis(quadrature_point, i) = density_shape(i); - } - - for (int i = 0; i < potential_dof_count; ++i) { - data.potential_basis(quadrature_point, i) = potential_shape(i); - } - - const double coefficient_value = source_coefficient.Eval(transformation, integration_point); - - transformation.SetIntPoint(&integration_point); - - const double quadrature_value = integration_point.weight * transformation.Weight() * coefficient_value; - - MFEM_VERIFY( - std::isfinite(quadrature_value) && quadrature_value > 0.0, - "Prepared gravity source operator encountered invalid " - "quadrature data on element " - << element_id << ", quadrature point " << quadrature_point << "." - ); - - data.quadrature_data(quadrature_point) = quadrature_value; - } - } - - MFEM_VERIFY(!m_elements.empty(), "PreparedMappedGravitySourceOperator found no stellar elements."); - - m_is_prepared = true; - ++m_preparation_count; + data.quadrature_data(quadrature_point) = quadrature_value; } - void PreparedMappedGravitySourceOperator::Mult( - const mfem::Vector &density, - mfem::Vector &action - ) const { - MFEM_VERIFY( - m_is_prepared, "PreparedMappedGravitySourceOperator must be prepared before " - "Mult is called." - ); + } - MFEM_VERIFY( - density.Size() == Width(), "PreparedMappedGravitySourceOperator received a density vector " - "with the wrong size." - ); + MFEM_VERIFY(!m_elements.empty(), + "PreparedMappedGravitySourceOperator found no stellar elements."); - m_density_true.SetSize(m_density_map.full_size()); - m_density_map.scatter(density, m_density_true); + m_is_prepared = true; + ++m_preparation_count; +} +void PreparedMappedGravitySourceOperator::Mult(const mfem::Vector &density, + mfem::Vector &action) const { + MFEM_VERIFY(m_is_prepared, + "PreparedMappedGravitySourceOperator must be prepared before " + "Mult is called."); - mfem::Vector density_local; + MFEM_VERIFY(density.Size() == Width(), + "PreparedMappedGravitySourceOperator received a density vector " + "with the wrong size."); - true_to_local(*m_fem.densityFes, m_density_true, density_local); + m_density_true.SetSize(m_density_map.full_size()); + m_density_map.scatter(density, m_density_true); - mfem::Vector local_action(m_fem.gravityPotentialFes->GetVSize()); - local_action = 0.0; + mfem::Vector density_local; - mfem::Vector element_density; - mfem::Vector quadrature_density; - mfem::Vector element_action; + true_to_local(*m_fem.densityFes, m_density_true, density_local); - for (const ElementPAData &data : m_elements) { - density_local.GetSubVector(data.density_dofs, element_density); + mfem::Vector local_action(m_fem.gravityPotentialFes->GetVSize()); + local_action = 0.0; - if (data.density_dof_transformation != nullptr) { - data.density_dof_transformation->InvTransformPrimal(element_density); - } + mfem::Vector element_density; + mfem::Vector quadrature_density; + mfem::Vector element_action; - quadrature_density.SetSize(data.quadrature_data.Size()); + for (const ElementPAData &data : m_elements) { + density_local.GetSubVector(data.density_dofs, element_density); - // B_density * x_e - data.density_basis.Mult(element_density, quadrature_density); - - // D * B_density * x_e - for (int q = 0; q < quadrature_density.Size(); ++q) { - quadrature_density(q) *= data.quadrature_data(q); - } - - element_action.SetSize(data.potential_dofs.Size()); - - // B_potential^T * D * B_density * x_e - data.potential_basis.MultTranspose(quadrature_density, element_action); - - if (data.potential_dof_transformation != nullptr) { - data.potential_dof_transformation->TransformDual(element_action); - } - - local_action.AddElementVector(data.potential_dofs, element_action); - } - - local_to_true(*m_fem.gravityPotentialFes, local_action, m_action_true); - action.SetSize(Height()); - m_potential_map.gather(m_action_true, action); + if (data.density_dof_transformation != nullptr) { + data.density_dof_transformation->InvTransformPrimal(element_density); } - void PreparedMappedGravitySourceOperator::MultTranspose( - const mfem::Vector &potential, - mfem::Vector &action - ) const { - MFEM_VERIFY( - m_is_prepared, "PreparedMappedGravitySourceOperator must be prepared before " - "MultTranspose is called." - ); + quadrature_density.SetSize(data.quadrature_data.Size()); - MFEM_VERIFY( - potential.Size() == Height(), "PreparedMappedGravitySourceOperator received a potential vector " - "with the wrong size." - ); + // B_density * x_e + data.density_basis.Mult(element_density, quadrature_density); - m_potential_true.SetSize(m_potential_map.full_size()); - m_potential_map.scatter(potential, m_potential_true); - - mfem::Vector potential_local; - - true_to_local(*m_fem.gravityPotentialFes, m_potential_true, potential_local); - - mfem::Vector local_action(m_fem.densityFes->GetVSize()); - local_action = 0.0; - - mfem::Vector element_potential; - mfem::Vector quadrature_potential; - mfem::Vector element_action; - - for (const ElementPAData &data : m_elements) { - potential_local.GetSubVector(data.potential_dofs, element_potential); - - if (data.potential_dof_transformation != nullptr) { - data.potential_dof_transformation->InvTransformPrimal(element_potential); - } - - quadrature_potential.SetSize(data.quadrature_data.Size()); - - data.potential_basis.Mult(element_potential, quadrature_potential); - - for (int q = 0; q < quadrature_potential.Size(); ++q) { - quadrature_potential(q) *= data.quadrature_data(q); - } - - element_action.SetSize(data.density_dofs.Size()); - - data.density_basis.MultTranspose(quadrature_potential, element_action); - - if (data.density_dof_transformation != nullptr) { - data.density_dof_transformation->TransformDual(element_action); - } - - local_action.AddElementVector(data.density_dofs, element_action); - } - - local_to_true(*m_fem.densityFes, local_action, m_action_true); - action.SetSize(Width()); - m_density_map.gather(m_action_true, action); - } - bool PreparedMappedGravitySourceOperator::IsPrepared() const noexcept { - return m_is_prepared; + // D * B_density * x_e + for (int q = 0; q < quadrature_density.Size(); ++q) { + quadrature_density(q) *= data.quadrature_data(q); } - std::uint64_t PreparedMappedGravitySourceOperator::GetPreparationCount() const noexcept { - return m_preparation_count; + element_action.SetSize(data.potential_dofs.Size()); + + // B_potential^T * D * B_density * x_e + data.potential_basis.MultTranspose(quadrature_density, element_action); + + if (data.potential_dof_transformation != nullptr) { + data.potential_dof_transformation->TransformDual(element_action); } - const field::FieldDofMap &PreparedMappedGravitySourceOperator::GetDensityMap() const noexcept { - return m_density_map; + local_action.AddElementVector(data.potential_dofs, element_action); + } + + local_to_true(*m_fem.gravityPotentialFes, local_action, m_action_true); + action.SetSize(Height()); + m_potential_map.gather(m_action_true, action); +} + +void PreparedMappedGravitySourceOperator::MultTranspose( + const mfem::Vector &potential, mfem::Vector &action) const { + MFEM_VERIFY(m_is_prepared, + "PreparedMappedGravitySourceOperator must be prepared before " + "MultTranspose is called."); + + MFEM_VERIFY(potential.Size() == Height(), + "PreparedMappedGravitySourceOperator received a potential vector " + "with the wrong size."); + + m_potential_true.SetSize(m_potential_map.full_size()); + m_potential_map.scatter(potential, m_potential_true); + + mfem::Vector potential_local; + + true_to_local(*m_fem.gravityPotentialFes, m_potential_true, potential_local); + + mfem::Vector local_action(m_fem.densityFes->GetVSize()); + local_action = 0.0; + + mfem::Vector element_potential; + mfem::Vector quadrature_potential; + mfem::Vector element_action; + + for (const ElementPAData &data : m_elements) { + potential_local.GetSubVector(data.potential_dofs, element_potential); + + if (data.potential_dof_transformation != nullptr) { + data.potential_dof_transformation->InvTransformPrimal(element_potential); } - const field::FieldDofMap &PreparedMappedGravitySourceOperator::GetPotentialMap() const noexcept { - return m_potential_map; + quadrature_potential.SetSize(data.quadrature_data.Size()); + + data.potential_basis.Mult(element_potential, quadrature_potential); + + for (int q = 0; q < quadrature_potential.Size(); ++q) { + quadrature_potential(q) *= data.quadrature_data(q); } - const field::FieldDofMap &PreparedMappedGravitySourceOperator::GetDisplacementMap() const noexcept { - return m_displacement_map; + element_action.SetSize(data.density_dofs.Size()); + + data.density_basis.MultTranspose(quadrature_potential, element_action); + + if (data.density_dof_transformation != nullptr) { + data.density_dof_transformation->TransformDual(element_action); } + + local_action.AddElementVector(data.density_dofs, element_action); + } + + local_to_true(*m_fem.densityFes, local_action, m_action_true); + action.SetSize(Width()); + m_density_map.gather(m_action_true, action); +} +bool PreparedMappedGravitySourceOperator::IsPrepared() const noexcept { + return m_is_prepared; +} + +std::uint64_t +PreparedMappedGravitySourceOperator::GetPreparationCount() const noexcept { + return m_preparation_count; +} + +const field::FieldDofMap & +PreparedMappedGravitySourceOperator::GetDensityMap() const noexcept { + return m_density_map; +} + +const field::FieldDofMap & +PreparedMappedGravitySourceOperator::GetPotentialMap() const noexcept { + return m_potential_map; +} + +const field::FieldDofMap & +PreparedMappedGravitySourceOperator::GetDisplacementMap() const noexcept { + return m_displacement_map; +} } // namespace mean_field::operators diff --git a/libmeanfield/impl/operators/prepared_hdiv_mass.cpp b/libmeanfield/impl/operators/prepared_hdiv_mass.cpp index e84e022..444d923 100644 --- a/libmeanfield/impl/operators/prepared_hdiv_mass.cpp +++ b/libmeanfield/impl/operators/prepared_hdiv_mass.cpp @@ -8,379 +8,405 @@ module mean_field; import :operators.prepared_hdiv_mass; namespace { - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - int get_operator_size(const mean_field::fem::FEM &f) { - MFEM_VERIFY( - f.gravityFluxFes != nullptr, "PreparedMappedHDivMassOperator requires the " - "gravity-gradient finite-element space." - ); - return mean_field::field::make_field_dof_map(*f.gravityFluxFes) - .reduced_size(); +int get_operator_size(const mean_field::fem::FEM &f) { + MFEM_VERIFY(f.gravityFluxFes != nullptr, + "PreparedMappedHDivMassOperator requires the " + "gravity-gradient finite-element space."); + return mean_field::field::make_field_dof_map(*f.gravityFluxFes) + .reduced_size(); +} + +void true_to_local(const mfem::ParFiniteElementSpace &finite_element_space, + const mfem::Vector &true_vector, + mfem::Vector &local_vector) { + local_vector.SetSize(finite_element_space.GetVSize()); + + const mfem::Operator *prolongation = + finite_element_space.GetProlongationMatrix(); + + if (prolongation != nullptr) { + prolongation->Mult(true_vector, local_vector); + } else { + local_vector = true_vector; + } +} + +int find_representative_element(const mean_field::fem::FEM &f, + const mfem::Array &marker) { + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + const int attribute = f.mesh->GetAttribute(element_id); + + if (attribute > 0 && attribute <= marker.Size() && + marker[attribute - 1] != 0) { + return element_id; + } + } + + return -1; +} + +void validate_uniform_domain_discretization( + const mean_field::fem::FEM &f, const mfem::Array &marker, + const int representative_element_id) { + const mfem::FiniteElement &representative_element = + *f.gravityFluxFes->GetFE(representative_element_id); + const mfem::ElementTransformation &representative_transformation = + *f.mesh->GetElementTransformation(representative_element_id); + + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + const int attribute = f.mesh->GetAttribute(element_id); + + if (attribute <= 0 || attribute > marker.Size() || + marker[attribute - 1] == 0) { + continue; } - void true_to_local( - const mfem::ParFiniteElementSpace &finite_element_space, - const mfem::Vector &true_vector, - mfem::Vector &local_vector - ) { - local_vector.SetSize(finite_element_space.GetVSize()); + const mfem::FiniteElement &element = *f.gravityFluxFes->GetFE(element_id); + const mfem::ElementTransformation &transformation = + *f.mesh->GetElementTransformation(element_id); - const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); - - if (prolongation != nullptr) { - prolongation->Mult(true_vector, local_vector); - } else { - local_vector = true_vector; - } - } - - int find_representative_element( - const mean_field::fem::FEM &f, - const mfem::Array &marker - ) { - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - const int attribute = f.mesh->GetAttribute(element_id); - - if (attribute > 0 && attribute <= marker.Size() && marker[attribute - 1] != 0) { - return element_id; - } - } - - return -1; - } - - void validate_uniform_domain_discretization( - const mean_field::fem::FEM &f, - const mfem::Array &marker, - const int representative_element_id - ) { - const mfem::FiniteElement &representative_element = *f.gravityFluxFes->GetFE(representative_element_id); - const mfem::ElementTransformation &representative_transformation = - *f.mesh->GetElementTransformation(representative_element_id); - - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - const int attribute = f.mesh->GetAttribute(element_id); - - if (attribute <= 0 || attribute > marker.Size() || marker[attribute - 1] == 0) { - continue; - } - - const mfem::FiniteElement &element = *f.gravityFluxFes->GetFE(element_id); - const mfem::ElementTransformation &transformation = *f.mesh->GetElementTransformation(element_id); - - MFEM_VERIFY( - element.GetGeomType() == representative_element.GetGeomType(), + MFEM_VERIFY(element.GetGeomType() == representative_element.GetGeomType(), "Prepared H(div) mass domains currently require a uniform " "element " - "geometry." - ); - MFEM_VERIFY( - element.GetOrder() == representative_element.GetOrder(), + "geometry."); + MFEM_VERIFY(element.GetOrder() == representative_element.GetOrder(), "Prepared H(div) mass domains currently require a uniform " - "finite-element order." - ); - MFEM_VERIFY( - transformation.OrderW() == representative_transformation.OrderW(), + "finite-element order."); + MFEM_VERIFY(transformation.OrderW() == + representative_transformation.OrderW(), "Prepared H(div) mass domains currently require a uniform " - "geometry-weight order." - ); - } + "geometry-weight order."); + } +} + +class FrozenMappedHDivMassCoefficient final : public mfem::MatrixCoefficient { +public: + FrozenMappedHDivMassCoefficient( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domain_mapper, + const mfem::Vector &displacement_true, bool elevates_vacuum) + : MatrixCoefficient(domain_mapper.GetDimension()), m_fem(f), + m_domain_mapper(domain_mapper), + m_workspace(domain_mapper.GetDimension()), + m_elevates_vacuum(elevates_vacuum) { + true_to_local(*m_fem.displacementFes, displacement_true, + m_displacement_local); + } + + void Eval(mfem::DenseMatrix &mass_tensor, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) override { + transformation.SetIntPoint(&integration_point); + + const int element_id = transformation.ElementNo; + MFEM_VERIFY( + element_id >= 0 && element_id < m_fem.mesh->GetNE(), + "Mapped H(div) mass coefficient received an invalid element ID."); + + const bool element_is_vacuum = DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(transformation.Attribute); + + if (element_is_vacuum != m_elevates_vacuum) { + mass_tensor.SetSize(m_domain_mapper.GetDimension()); + mass_tensor = 0.0; + return; } - class FrozenMappedHDivMassCoefficient final : public mfem::MatrixCoefficient { - public: - FrozenMappedHDivMassCoefficient( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &displacement_true, - bool elevates_vacuum - ) - : MatrixCoefficient(domain_mapper.GetDimension()), - m_fem(f), - m_domain_mapper(domain_mapper), - m_workspace(domain_mapper.GetDimension()), - m_elevates_vacuum(elevates_vacuum) { - true_to_local(*m_fem.displacementFes, displacement_true, m_displacement_local); - } + LoadElement(element_id); - void Eval( - mfem::DenseMatrix &mass_tensor, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point - ) override { - transformation.SetIntPoint(&integration_point); + const mean_field::mapping::ElementMappingData mapping_data{ + .displacement = *m_displacement_data, + .compactification = *m_compactification_data}; - const int element_id = transformation.ElementNo; - MFEM_VERIFY( - element_id >= 0 && element_id < m_fem.mesh->GetNE(), - "Mapped H(div) mass coefficient received an invalid element ID." - ); + mean_field::mapping::VolumeMappingContext mapping_context; - const bool element_is_vacuum = transformation.Attribute == m_domain_mapper.GetVacuumElementAttribute(); + const mean_field::mapping::MappingStatus status = + m_domain_mapper.EvaluateVolume(mapping_data, transformation, + integration_point, m_workspace, + mapping_context); - if (element_is_vacuum != m_elevates_vacuum) { - mass_tensor.SetSize(m_domain_mapper.GetDimension()); - mass_tensor = 0.0; - return; - } - - LoadElement(element_id); - - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = *m_displacement_data, .compactification = *m_compactification_data - }; - - mean_field::mapping::VolumeMappingContext mapping_context; - - const mean_field::mapping::MappingStatus status = m_domain_mapper.EvaluateVolume( - mapping_data, transformation, integration_point, m_workspace, mapping_context - ); - - MFEM_VERIFY( - status == mean_field::mapping::MappingStatus::valid, + MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, "Stateless domain mapping failed while preparing the H(div) " "mass " "operator. Mapping status = " - << static_cast(status) << ", element ID = " << element_id + << static_cast(status) + << ", element ID = " << element_id << ", element attribute = " << transformation.Attribute - << ", coefficient domain = " << (m_elevates_vacuum ? "vacuum" : "stellar") - ); + << ", coefficient domain = " + << (m_elevates_vacuum ? "vacuum" : "stellar")); - const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping.mapping_jacobian; - const double mapping_determinant = mapping_context.mapping.mapping_determinant; + const mfem::DenseMatrix &mapping_jacobian = + mapping_context.mapping.mapping_jacobian; + const double mapping_determinant = + mapping_context.mapping.mapping_determinant; - MFEM_VERIFY( - std::isfinite(mapping_determinant) && mapping_determinant > 0.0, + MFEM_VERIFY(std::isfinite(mapping_determinant) && mapping_determinant > 0.0, "Prepared H(div) mass operator encountered a non-positive or " - "non-finite mapping determinant." - ); + "non-finite mapping determinant."); - mfem::MultAtB(mapping_jacobian, mapping_jacobian, mass_tensor); - mass_tensor *= 1.0 / mapping_determinant; - } + mfem::MultAtB(mapping_jacobian, mapping_jacobian, mass_tensor); + mass_tensor *= 1.0 / mapping_determinant; + } - private: - void LoadElement(const int element_id) { - if (element_id == m_cached_element_id) { - return; - } +private: + void LoadElement(const int element_id) { + if (element_id == m_cached_element_id) { + return; + } - const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id); + const mfem::FiniteElement &displacement_element = + *m_fem.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *m_fem.compactificationFes->GetFE(element_id); - mfem::DofTransformation *displacement_dof_transformation = - m_fem.displacementFes->GetElementVDofs(element_id, m_displacement_dofs); - mfem::DofTransformation *compactification_dof_transformation = - m_fem.compactificationFes->GetElementDofs(element_id, m_compactification_dofs); + mfem::DofTransformation *displacement_dof_transformation = + m_fem.displacementFes->GetElementVDofs(element_id, m_displacement_dofs); + mfem::DofTransformation *compactification_dof_transformation = + m_fem.compactificationFes->GetElementDofs(element_id, + m_compactification_dofs); - m_displacement_local.GetSubVector(m_displacement_dofs, m_element_displacement); - m_fem.compactificationCoordinate->GetSubVector(m_compactification_dofs, m_element_compactification); + m_displacement_local.GetSubVector(m_displacement_dofs, + m_element_displacement); + m_fem.compactificationCoordinate->GetSubVector(m_compactification_dofs, + m_element_compactification); - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(m_element_displacement); - } + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal( + m_element_displacement); + } - if (compactification_dof_transformation != nullptr) { - compactification_dof_transformation->InvTransformPrimal(m_element_compactification); - } + if (compactification_dof_transformation != nullptr) { + compactification_dof_transformation->InvTransformPrimal( + m_element_compactification); + } - m_displacement_data = std::make_unique( - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, m_element_displacement - ) - ); + m_displacement_data = + std::make_unique( + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, m_element_displacement)); - m_compactification_data = std::make_unique( - compactification_element, m_element_compactification - ); + m_compactification_data = + std::make_unique( + compactification_element, m_element_compactification); - m_cached_element_id = element_id; - } + m_cached_element_id = element_id; + } - const mean_field::fem::FEM &m_fem; - const mean_field::mapping::DomainMapperStateless &m_domain_mapper; + const mean_field::fem::FEM &m_fem; + const mean_field::mapping::DomainMapper &m_domain_mapper; - mfem::Vector m_displacement_local; + mfem::Vector m_displacement_local; - mfem::Array m_displacement_dofs; - mfem::Array m_compactification_dofs; + mfem::Array m_displacement_dofs; + mfem::Array m_compactification_dofs; - mfem::Vector m_element_displacement; - mfem::Vector m_element_compactification; + mfem::Vector m_element_displacement; + mfem::Vector m_element_compactification; - std::unique_ptr m_displacement_data; - std::unique_ptr m_compactification_data; + std::unique_ptr + m_displacement_data; + std::unique_ptr + m_compactification_data; - mean_field::mapping::DomainMapperStateless::Workspace m_workspace; - int m_cached_element_id{-1}; - bool m_elevates_vacuum; - }; + mean_field::mapping::DomainMapper::Workspace m_workspace; + int m_cached_element_id{-1}; + bool m_elevates_vacuum; +}; } // namespace namespace mean_field::operators { - PreparedMappedHDivMassOperator::PreparedMappedHDivMassOperator( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper - ) - : Operator(get_operator_size(f)), - m_fem(f), - m_domain_mapper(domain_mapper), - m_flux_map( - field::make_field_dof_map< - field::Gravity, - DomainSchema>(*f.gravityFluxFes) - ), - m_displacement_map( - field::make_field_dof_map< - field::Displacement, - DomainSchema>(*f.displacementFes) - ) { - MFEM_VERIFY(f.mesh != nullptr, "PreparedMappedHDivMassOperator requires a mesh."); - MFEM_VERIFY( - f.gravityFluxFes != nullptr, "PreparedMappedHDivMassOperator requires the " - "gravity-gradient finite-element space." - ); - MFEM_VERIFY( - f.displacementFes != nullptr, "PreparedMappedHDivMassOperator requires the " - "displacement finite-element space." - ); - MFEM_VERIFY( - f.compactificationFes != nullptr, "PreparedMappedHDivMassOperator requires the compactification " - "finite-element space." - ); - MFEM_VERIFY( - f.compactificationCoordinate != nullptr, "PreparedMappedHDivMassOperator requires the compactification " - "coordinate." - ); - MFEM_VERIFY( - f.quadratureFactory != nullptr, "PreparedMappedHDivMassOperator requires the quadrature-rule " - "factory." - ); - MFEM_VERIFY( - domain_mapper.GetDimension() == f.mesh->Dimension(), - "The stateless domain-mapper dimension does not match the mesh " - "dimension." - ); +PreparedMappedHDivMassOperator::PreparedMappedHDivMassOperator( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper) + : Operator(get_operator_size(f)), m_fem(f), m_domain_mapper(domain_mapper), + m_flux_map(field::make_field_dof_map( + *f.gravityFluxFes)), + m_displacement_map( + field::make_field_dof_map( + *f.displacementFes)) { + MFEM_VERIFY(f.mesh != nullptr, + "PreparedMappedHDivMassOperator requires a mesh."); + MFEM_VERIFY(f.gravityFluxFes != nullptr, + "PreparedMappedHDivMassOperator requires the " + "gravity-gradient finite-element space."); + MFEM_VERIFY(f.displacementFes != nullptr, + "PreparedMappedHDivMassOperator requires the " + "displacement finite-element space."); + MFEM_VERIFY(f.compactificationFes != nullptr, + "PreparedMappedHDivMassOperator requires the compactification " + "finite-element space."); + MFEM_VERIFY(f.compactificationCoordinate != nullptr, + "PreparedMappedHDivMassOperator requires the compactification " + "coordinate."); + MFEM_VERIFY(f.quadratureFactory != nullptr, + "PreparedMappedHDivMassOperator requires the quadrature-rule " + "factory."); + MFEM_VERIFY(domain_mapper.GetDimension() == f.mesh->Dimension(), + "The stateless domain-mapper dimension does not match the mesh " + "dimension."); - utils::populate_element_mask(f.mesh.get(), utils::DOMAINS::STELLAR, m_stellar_marker); - utils::populate_element_mask(f.mesh.get(), utils::DOMAINS::VACUUM, m_vacuum_marker); + m_stellar_marker = + utils::domain::make_attribute_marker(*f.mesh); + m_vacuum_marker = + utils::domain::make_attribute_marker( + *f.mesh); - const int stellar_element_id = find_representative_element(f, m_stellar_marker); - const int vacuum_element_id = find_representative_element(f, m_vacuum_marker); + const int stellar_element_id = + find_representative_element(f, m_stellar_marker); + const int vacuum_element_id = find_representative_element(f, m_vacuum_marker); - MFEM_VERIFY( - stellar_element_id >= 0, "PreparedMappedHDivMassOperator requires " - "at least one stellar element." - ); - MFEM_VERIFY( - vacuum_element_id >= 0, "PreparedMappedHDivMassOperator requires at " - "least one compactified vacuum element." - ); + MFEM_VERIFY(stellar_element_id >= 0, + "PreparedMappedHDivMassOperator requires " + "at least one stellar element."); + MFEM_VERIFY(vacuum_element_id >= 0, + "PreparedMappedHDivMassOperator requires at " + "least one compactified vacuum element."); - validate_uniform_domain_discretization(f, m_stellar_marker, stellar_element_id); - validate_uniform_domain_discretization(f, m_vacuum_marker, vacuum_element_id); - } + validate_uniform_domain_discretization(f, m_stellar_marker, + stellar_element_id); + validate_uniform_domain_discretization(f, m_vacuum_marker, vacuum_element_id); +} - void PreparedMappedHDivMassOperator::Prepare(const mfem::Vector &displacement) { - MFEM_VERIFY( - displacement.Size() == m_displacement_map.reduced_size(), - "PreparedMappedHDivMassOperator received a displacement vector " - "with " - "the wrong size." - ); +void PreparedMappedHDivMassOperator::Prepare(const mfem::Vector &displacement) { + MFEM_VERIFY(displacement.Size() == m_displacement_map.reduced_size(), + "PreparedMappedHDivMassOperator received a displacement vector " + "with " + "the wrong size."); - for (int i = 0; i < displacement.Size(); ++i) { - MFEM_VERIFY( - std::isfinite(displacement(i)), "PreparedMappedHDivMassOperator received a non-finite " - "displacement " - "value." - ); - } + for (int i = 0; i < displacement.Size(); ++i) { + MFEM_VERIFY(std::isfinite(displacement(i)), + "PreparedMappedHDivMassOperator received a non-finite " + "displacement " + "value."); + } - m_displacement_true.SetSize(m_displacement_map.full_size()); - m_displacement_map.scatter(displacement, m_displacement_true); + m_displacement_true.SetSize(m_displacement_map.full_size()); + m_displacement_map.scatter(displacement, m_displacement_true); - const int stellar_element_id = find_representative_element(m_fem, m_stellar_marker); - const int vacuum_element_id = find_representative_element(m_fem, m_vacuum_marker); + const int stellar_element_id = + find_representative_element(m_fem, m_stellar_marker); + const int vacuum_element_id = + find_representative_element(m_fem, m_vacuum_marker); - const mfem::FiniteElement &stellar_element = *m_fem.gravityFluxFes->GetFE(stellar_element_id); - const mfem::FiniteElement &vacuum_element = *m_fem.gravityFluxFes->GetFE(vacuum_element_id); + const mfem::FiniteElement &stellar_element = + *m_fem.gravityFluxFes->GetFE(stellar_element_id); + const mfem::FiniteElement &vacuum_element = + *m_fem.gravityFluxFes->GetFE(vacuum_element_id); - mfem::ElementTransformation &stellar_transformation = *m_fem.mesh->GetElementTransformation(stellar_element_id); - mfem::ElementTransformation &vacuum_transformation = *m_fem.mesh->GetElementTransformation(vacuum_element_id); + mfem::ElementTransformation &stellar_transformation = + *m_fem.mesh->GetElementTransformation(stellar_element_id); + mfem::ElementTransformation &vacuum_transformation = + *m_fem.mesh->GetElementTransformation(vacuum_element_id); - m_mass_form.reset(); - m_stellar_mass_coefficient.reset(); - m_vacuum_mass_coefficient.reset(); + m_stellar_mass_form.reset(); + m_vacuum_mass_form.reset(); + m_stellar_mass_coefficient.reset(); + m_vacuum_mass_coefficient.reset(); - m_stellar_mass_coefficient = - std::make_unique(m_fem, m_domain_mapper, m_displacement_true, false); - m_vacuum_mass_coefficient = - std::make_unique(m_fem, m_domain_mapper, m_displacement_true, true); + m_stellar_mass_coefficient = + std::make_unique( + m_fem, m_domain_mapper, m_displacement_true, false); + m_vacuum_mass_coefficient = std::make_unique( + m_fem, m_domain_mapper, m_displacement_true, true); - m_mass_form = std::make_unique(m_fem.gravityFluxFes.get()); - m_mass_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL); + m_stellar_mass_form = + std::make_unique(m_fem.gravityFluxFes.get()); + m_vacuum_mass_form = + std::make_unique(m_fem.gravityFluxFes.get()); + m_stellar_mass_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL); + m_vacuum_mass_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL); - auto stellar_integrator = std::make_unique(*m_stellar_mass_coefficient); - auto vacuum_integrator = std::make_unique(*m_vacuum_mass_coefficient); + auto stellar_integrator = std::make_unique( + *m_stellar_mass_coefficient); + auto vacuum_integrator = std::make_unique( + *m_vacuum_mass_coefficient); - m_fem.quadratureFactory->configure_gravity_hdiv_mass( - *stellar_integrator, quadrature::QuadratureRole::discretization, stellar_element, stellar_transformation, - utils::DOMAINS::STELLAR, quadrature::MappingKind::general - ); + m_fem.quadratureFactory->configure_gravity_hdiv_mass( + *stellar_integrator, quadrature::QuadratureRole::discretization, + stellar_element, stellar_transformation, utils::DOMAINS::STELLAR, + quadrature::MappingKind::general); - m_fem.quadratureFactory->configure_gravity_hdiv_mass( - *vacuum_integrator, quadrature::QuadratureRole::discretization, vacuum_element, vacuum_transformation, - utils::DOMAINS::VACUUM, quadrature::MappingKind::kelvin - ); + m_fem.quadratureFactory->configure_gravity_hdiv_mass( + *vacuum_integrator, quadrature::QuadratureRole::discretization, + vacuum_element, vacuum_transformation, utils::DOMAINS::VACUUM, + quadrature::MappingKind::kelvin); - m_mass_form->AddDomainIntegrator(stellar_integrator.release(), m_stellar_marker); - m_mass_form->AddDomainIntegrator(vacuum_integrator.release(), m_vacuum_marker); - m_mass_form->Assemble(); + m_stellar_mass_form->AddDomainIntegrator(stellar_integrator.release(), + m_stellar_marker); + m_vacuum_mass_form->AddDomainIntegrator(vacuum_integrator.release(), + m_vacuum_marker); + m_stellar_mass_form->Assemble(); + m_vacuum_mass_form->Assemble(); - m_is_prepared = true; - ++m_preparation_count; - } + m_is_prepared = true; + ++m_preparation_count; +} - void PreparedMappedHDivMassOperator::Mult( - const mfem::Vector &gravity_gradient, - mfem::Vector &action - ) const { - MFEM_VERIFY( - m_is_prepared, "PreparedMappedHDivMassOperator must be prepared " - "before Mult is called." - ); - MFEM_VERIFY( - m_mass_form != nullptr, "PreparedMappedHDivMassOperator has no " - "assembled partial-assembly form." - ); - MFEM_VERIFY( - gravity_gradient.Size() == Width(), "PreparedMappedHDivMassOperator received a gravity-gradient vector " - "with the wrong size." - ); +void PreparedMappedHDivMassOperator::Mult(const mfem::Vector &gravity_gradient, + mfem::Vector &action) const { + MFEM_VERIFY(m_is_prepared, "PreparedMappedHDivMassOperator must be prepared " + "before Mult is called."); + MFEM_VERIFY( + m_stellar_mass_form != nullptr && m_vacuum_mass_form != nullptr, + "PreparedMappedHDivMassOperator has incomplete domain mass forms."); + MFEM_VERIFY( + gravity_gradient.Size() == Width(), + "PreparedMappedHDivMassOperator received a gravity-gradient vector " + "with the wrong size."); - m_flux_true.SetSize(m_flux_map.full_size()); - m_action_true.SetSize(m_flux_map.full_size()); - m_flux_map.scatter(gravity_gradient, m_flux_true); - m_mass_form->Mult(m_flux_true, m_action_true); - action.SetSize(Height()); - m_flux_map.gather(m_action_true, action); - } + m_flux_true.SetSize(m_flux_map.full_size()); + m_action_true.SetSize(m_flux_map.full_size()); + m_domain_action_true.SetSize(m_flux_map.full_size()); + m_flux_map.scatter(gravity_gradient, m_flux_true); + m_stellar_mass_form->Mult(m_flux_true, m_action_true); + m_vacuum_mass_form->Mult(m_flux_true, m_domain_action_true); + m_action_true += m_domain_action_true; + action.SetSize(Height()); + m_flux_map.gather(m_action_true, action); +} - bool PreparedMappedHDivMassOperator::IsPrepared() const noexcept { - return m_is_prepared; - } +void PreparedMappedHDivMassOperator::AssembleDiagonal( + mfem::Vector &diagonal) const { + mfem::Vector true_diagonal; + AssembleTrueDiagonal(true_diagonal); + diagonal.SetSize(Height()); + m_flux_map.gather(true_diagonal, diagonal); +} - std::uint64_t PreparedMappedHDivMassOperator::GetPreparationCount() const noexcept { - return m_preparation_count; - } +void PreparedMappedHDivMassOperator::AssembleTrueDiagonal( + mfem::Vector &diagonal) const { + MFEM_VERIFY(m_is_prepared, "PreparedMappedHDivMassOperator must be prepared " + "before assembling its diagonal."); + MFEM_VERIFY( + m_stellar_mass_form != nullptr && m_vacuum_mass_form != nullptr, + "PreparedMappedHDivMassOperator has incomplete domain mass forms."); - const field::FieldDofMap &PreparedMappedHDivMassOperator::GetFluxMap() const noexcept { - return m_flux_map; - } + diagonal.SetSize(m_flux_map.full_size()); + mfem::Vector domain_diagonal(m_flux_map.full_size()); + m_stellar_mass_form->AssembleDiagonal(diagonal); + m_vacuum_mass_form->AssembleDiagonal(domain_diagonal); + diagonal += domain_diagonal; +} - const field::FieldDofMap &PreparedMappedHDivMassOperator::GetDisplacementMap() const noexcept { - return m_displacement_map; - } +bool PreparedMappedHDivMassOperator::IsPrepared() const noexcept { + return m_is_prepared; +} + +std::uint64_t +PreparedMappedHDivMassOperator::GetPreparationCount() const noexcept { + return m_preparation_count; +} + +const field::FieldDofMap & +PreparedMappedHDivMassOperator::GetFluxMap() const noexcept { + return m_flux_map; +} + +const field::FieldDofMap & +PreparedMappedHDivMassOperator::GetDisplacementMap() const noexcept { + return m_displacement_map; +} } // namespace mean_field::operators diff --git a/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp b/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp index cc76503..2a92d09 100644 --- a/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp @@ -12,1236 +12,1268 @@ module mean_field; import :operators.prepared_hydrostatic_equilibrium; namespace { - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - void true_to_local( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &trueVector, - mfem::Vector &localVector - ) { - MFEM_VERIFY( - trueVector.Size() == finiteElementSpace.GetTrueVSize(), "Hydrostatic true vector has the wrong size." - ); +[[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(attribute); +} - localVector.SetSize(finiteElementSpace.GetVSize()); +void true_to_local(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, mfem::Vector &localVector) { + MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), + "Hydrostatic true vector has the wrong size."); - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + localVector.SetSize(finiteElementSpace.GetVSize()); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } - } + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - void local_to_true( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &localVector, - mfem::Vector &trueVector - ) { - MFEM_VERIFY( - localVector.Size() == finiteElementSpace.GetVSize(), "Hydrostatic local vector has the wrong size." - ); + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } +} - trueVector.SetSize(finiteElementSpace.GetTrueVSize()); - trueVector = 0.0; +void local_to_true(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, mfem::Vector &trueVector) { + MFEM_VERIFY(localVector.Size() == finiteElementSpace.GetVSize(), + "Hydrostatic local vector has the wrong size."); - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; - if (prolongation != nullptr) { - prolongation->MultTranspose(localVector, trueVector); - } else { - trueVector = localVector; - } - } + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - void copy_vector_block( - const mfem::Vector &source, - const int offset, - const int size, - mfem::Vector &block - ) { - MFEM_VERIFY( - offset >= 0 && size >= 0 && offset + size <= source.Size(), "Hydrostatic Jacobian block lies outside the " - "input vector." - ); + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } +} - block.SetSize(size); +void copy_vector_block(const mfem::Vector &source, const int offset, + const int size, mfem::Vector &block) { + MFEM_VERIFY(offset >= 0 && size >= 0 && offset + size <= source.Size(), + "Hydrostatic Jacobian block lies outside the " + "input vector."); - for (int entry = 0; entry < size; ++entry) { - block(entry) = source(offset + entry); - } - } + block.SetSize(size); - const mfem::IntegrationRule &get_hydrostatic_rule( - const mean_field::fem::FEM &f, - const mfem::FiniteElement &enthalpyElement, - const mfem::FiniteElement &gravityPotentialElement, - const mfem::ElementTransformation &transformation - ) { - using EnthalpyField = mean_field::field::Field; + for (int entry = 0; entry < size; ++entry) { + block(entry) = source(offset + entry); + } +} - MFEM_VERIFY( - enthalpyElement.GetOrder() == mean_field::field::Enthalpy::Scalar::familyOrder, - "The prepared hydrostatic enthalpy element does " - "not match the registered field." - ); +const mfem::IntegrationRule & +get_hydrostatic_rule(const mean_field::fem::FEM &f, + const mfem::FiniteElement &enthalpyElement, + const mfem::FiniteElement &gravityPotentialElement, + const mfem::ElementTransformation &transformation) { + using EnthalpyField = mean_field::field::Field; - MFEM_VERIFY( - gravityPotentialElement.GetOrder() == mean_field::field::Gravity::Potential::familyOrder, - "The prepared hydrostatic potential element does " - "not match the registered field." - ); + MFEM_VERIFY(enthalpyElement.GetOrder() == + mean_field::field::Enthalpy::Scalar::familyOrder, + "The prepared hydrostatic enthalpy element does " + "not match the registered field."); - const auto enthalpyQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + MFEM_VERIFY(gravityPotentialElement.GetOrder() == + mean_field::field::Gravity::Potential::familyOrder, + "The prepared hydrostatic potential element does " + "not match the registered field."); - const auto gravityQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + const auto enthalpyQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumEnthalpy>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - // A rigid-rotation potential is quadratic in physical position. - const auto rotationQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array{2}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + const auto gravityQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumGravity>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - const auto constantQuery = EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + // A rigid-rotation potential is quadratic in physical position. + const auto rotationQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumRotation>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), std::array{2}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - const std::array candidateRules{ - f.quadratureFactory->get(enthalpyQuery, transformation.GetGeometryType()), - f.quadratureFactory->get(gravityQuery, transformation.GetGeometryType()), - f.quadratureFactory->get(rotationQuery, transformation.GetGeometryType()), - f.quadratureFactory->get(constantQuery, transformation.GetGeometryType()) - }; + const auto constantQuery = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::EquilibriumConstant>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - const auto selectedRule = - std::max_element(candidateRules.begin(), candidateRules.end(), [](const auto &left, const auto &right) { - return left.resolution.order < right.resolution.order; - }); + const std::array candidateRules{ + f.quadratureFactory->get(enthalpyQuery, transformation.GetGeometryType()), + f.quadratureFactory->get(gravityQuery, transformation.GetGeometryType()), + f.quadratureFactory->get(rotationQuery, transformation.GetGeometryType()), + f.quadratureFactory->get(constantQuery, + transformation.GetGeometryType())}; - MFEM_VERIFY( - selectedRule != candidateRules.end() && selectedRule->integration_rule != nullptr, - "The quadrature policy did not return a valid " - "hydrostatic-equilibrium integration rule." - ); + const auto selectedRule = + std::max_element(candidateRules.begin(), candidateRules.end(), + [](const auto &left, const auto &right) { + return left.resolution.order < right.resolution.order; + }); - return *selectedRule->integration_rule; - } + MFEM_VERIFY(selectedRule != candidateRules.end() && + selectedRule->integration_rule != nullptr, + "The quadrature policy did not return a valid " + "hydrostatic-equilibrium integration rule."); + + return *selectedRule->integration_rule; +} } // namespace namespace mean_field::operators { - HydrostaticJacobianBlockLayout::HydrostaticJacobianBlockLayout(const fem::FEM &f) { - MFEM_VERIFY( - f.enthalpyFes != nullptr, "HydrostaticJacobianBlockLayout requires the " - "enthalpy finite-element space." - ); +HydrostaticJacobianBlockLayout::HydrostaticJacobianBlockLayout( + const fem::FEM &f) { + MFEM_VERIFY(f.enthalpyFes != nullptr, + "HydrostaticJacobianBlockLayout requires the " + "enthalpy finite-element space."); - MFEM_VERIFY( - f.gravityPotentialFes != nullptr, "HydrostaticJacobianBlockLayout requires the " - "gravity-potential finite-element space." - ); + MFEM_VERIFY(f.gravityPotentialFes != nullptr, + "HydrostaticJacobianBlockLayout requires the " + "gravity-potential finite-element space."); - MFEM_VERIFY( - f.displacementFes != nullptr, "HydrostaticJacobianBlockLayout requires the " - "displacement finite-element space." - ); + MFEM_VERIFY(f.displacementFes != nullptr, + "HydrostaticJacobianBlockLayout requires the " + "displacement finite-element space."); - const field::FieldDofMap enthalpyMap = - field::make_field_dof_map(*f.enthalpyFes); + const field::FieldDofMap enthalpyMap = + field::make_field_dof_map(*f.enthalpyFes); - const field::FieldDofMap gravityPotentialMap = - field::make_field_dof_map(*f.gravityPotentialFes); + const field::FieldDofMap gravityPotentialMap = + field::make_field_dof_map( + *f.gravityPotentialFes); - const field::FieldDofMap displacementMap = - field::make_field_dof_map(*f.displacementFes); + const field::FieldDofMap displacementMap = + field::make_field_dof_map( + *f.displacementFes); - m_enthalpySize = enthalpyMap.reduced_size(); - m_gravityPotentialSize = gravityPotentialMap.reduced_size(); - m_displacementSize = displacementMap.reduced_size(); - m_residualSize = m_enthalpySize; + m_enthalpySize = enthalpyMap.reduced_size(); + m_gravityPotentialSize = gravityPotentialMap.reduced_size(); + m_displacementSize = displacementMap.reduced_size(); + m_residualSize = m_enthalpySize; - m_totalSize = m_enthalpySize + m_gravityPotentialSize + 1 + m_displacementSize; + m_totalSize = + m_enthalpySize + m_gravityPotentialSize + 1 + m_displacementSize; - MFEM_VERIFY( - m_enthalpySize > 0 && m_gravityPotentialSize > 0 && m_displacementSize > 0, - "HydrostaticJacobianBlockLayout received an empty " - "finite-element space." - ); + MFEM_VERIFY(m_enthalpySize > 0 && m_gravityPotentialSize > 0 && + m_displacementSize > 0, + "HydrostaticJacobianBlockLayout received an empty " + "finite-element space."); +} + +int HydrostaticJacobianBlockLayout::Offset( + const HydrostaticJacobianInputBlock block) const { + switch (block) { + case HydrostaticJacobianInputBlock::enthalpy: + return 0; + + case HydrostaticJacobianInputBlock::gravityPotential: + return m_enthalpySize; + + case HydrostaticJacobianInputBlock::bernoulliConstant: + return m_enthalpySize + m_gravityPotentialSize; + + case HydrostaticJacobianInputBlock::displacement: + return m_enthalpySize + m_gravityPotentialSize + 1; + } + + MFEM_ABORT("HydrostaticJacobianBlockLayout received an " + "unknown input block."); + + return 0; +} + +int HydrostaticJacobianBlockLayout::Size( + const HydrostaticJacobianInputBlock block) const { + switch (block) { + case HydrostaticJacobianInputBlock::enthalpy: + return m_enthalpySize; + + case HydrostaticJacobianInputBlock::gravityPotential: + return m_gravityPotentialSize; + + case HydrostaticJacobianInputBlock::bernoulliConstant: + return 1; + + case HydrostaticJacobianInputBlock::displacement: + return m_displacementSize; + } + + MFEM_ABORT("HydrostaticJacobianBlockLayout received an " + "unknown input block."); + + return 0; +} + +int HydrostaticJacobianBlockLayout::GetTotalSize() const noexcept { + return m_totalSize; +} + +int HydrostaticJacobianBlockLayout::GetResidualSize() const noexcept { + return m_residualSize; +} + +PreparedHydrostaticEquilibriumOperator::PreparedHydrostaticEquilibriumOperator( + const fem::FEM &f, const mapping::DomainMapper &domainMapper) + : m_fem(f), m_domainMapper(domainMapper), m_context(f, domainMapper) { + MFEM_VERIFY(m_fem.mesh != nullptr, + "PreparedHydrostaticEquilibriumOperator requires a mesh."); + + MFEM_VERIFY(m_fem.enthalpyFes != nullptr, + "PreparedHydrostaticEquilibriumOperator requires " + "the enthalpy finite-element space."); + + MFEM_VERIFY(m_fem.gravityPotentialFes != nullptr, + "PreparedHydrostaticEquilibriumOperator requires " + "the gravity-potential finite-element space."); + + MFEM_VERIFY(m_fem.displacementFes != nullptr, + "PreparedHydrostaticEquilibriumOperator requires " + "the displacement finite-element space."); + + MFEM_VERIFY(m_fem.compactificationFes != nullptr, + "PreparedHydrostaticEquilibriumOperator requires " + "the compactification finite-element space."); + + MFEM_VERIFY(m_fem.compactificationCoordinate != nullptr, + "PreparedHydrostaticEquilibriumOperator requires " + "the compactification coordinate."); + + MFEM_VERIFY(m_fem.quadratureFactory != nullptr, + "PreparedHydrostaticEquilibriumOperator requires " + "the quadrature-rule factory."); + + MFEM_VERIFY(m_domainMapper.GetDimension() == m_fem.mesh->Dimension(), + "The hydrostatic operator's stateless mapper " + "dimension does not match the mesh dimension."); + + m_enthalpyVariationTrue.SetSize(m_context.GetEnthalpyMap().full_size()); + m_gravityPotentialVariationTrue.SetSize( + m_context.GetGravityPotentialMap().full_size()); + m_displacementVariationTrue.SetSize( + m_context.GetDisplacementMap().full_size()); + m_fullEnthalpyAction.SetSize(m_context.GetEnthalpyMap().full_size()); +} + +PreparedHydrostaticEquilibriumReport +PreparedHydrostaticEquilibriumOperator::Prepare( + const context::hydrostatic::HydrostaticEquilibriumStateView &state, + const context::hydrostatic::HydrostaticEquilibriumDependencies + &dependencies, + const physics::RigidRotation &rotation) { + const bool rotationObjectChanged = + !m_context.IsPrepared() || + dependencies.rotation != m_context.GetDependencies().rotation; + + PreparedHydrostaticEquilibriumReport report; + + report.contextReport = m_context.Prepare(state, dependencies); + + if (rotationObjectChanged) { + m_rotation = rotation; + report.updatedRotation = true; + } + + MFEM_VERIFY(m_rotation.has_value(), + "The prepared hydrostatic operator has no frozen " + "rotation state."); + + m_isPrepared = false; + + if (report.contextReport.preparedStaticDependencies) { + PrepareStaticPlan(); + } + + if (report.contextReport.preparedGeometryState) { + PrepareGeometry(); + PrepareAlgebraicJacobianBlocks(); + report.preparedAlgebraicJacobianBlocks = true; + } + + if (report.contextReport.preparedRotationDependencies) { + PrepareRotation(); + } + + if (report.contextReport.preparedBaseState) { + PrepareBaseState(); + FinalizeDisplacementJacobianPreparation(); + AssembleCachedResidual(); + ++m_residualPreparationCount; + report.preparedDisplacementJacobianData = true; + report.preparedResidual = true; + } + + MFEM_VERIFY(!m_elements.empty(), + "PreparedHydrostaticEquilibriumOperator found no " + "stellar elements."); + + MFEM_VERIFY( + m_cachedResidual.Size() == m_context.GetEnthalpyMap().reduced_size(), + "The prepared hydrostatic residual has the wrong supported size."); + + m_isPrepared = true; + return report; +} + +void PreparedHydrostaticEquilibriumOperator::PrepareStaticPlan() { + m_elements.clear(); + m_elements.reserve(m_fem.mesh->GetNE()); + + mfem::Vector enthalpyShape; + mfem::Vector gravityPotentialShape; + + for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + m_fem.mesh->GetElementTransformation(elementId); + + MFEM_VERIFY(transformation != nullptr, + "Prepared hydrostatic static planning received " + "a null element transformation."); + + if (is_vacuum_attribute(transformation->Attribute)) { + continue; } - int HydrostaticJacobianBlockLayout::Offset(const HydrostaticJacobianInputBlock block) const { - switch (block) { - case HydrostaticJacobianInputBlock::enthalpy: - return 0; + const mfem::FiniteElement &enthalpyElement = + *m_fem.enthalpyFes->GetFE(elementId); - case HydrostaticJacobianInputBlock::gravityPotential: - return m_enthalpySize; + const mfem::FiniteElement &gravityPotentialElement = + *m_fem.gravityPotentialFes->GetFE(elementId); - case HydrostaticJacobianInputBlock::bernoulliConstant: - return m_enthalpySize + m_gravityPotentialSize; + MFEM_VERIFY(enthalpyElement.GetGeomType() == + gravityPotentialElement.GetGeomType() && + enthalpyElement.GetGeomType() == + transformation->GetGeometryType(), + "Hydrostatic element geometries do not agree."); - case HydrostaticJacobianInputBlock::displacement: - return m_enthalpySize + m_gravityPotentialSize + 1; - } + m_elements.emplace_back(); + ElementPAData &data = m_elements.back(); + data.elementId = elementId; - MFEM_ABORT( - "HydrostaticJacobianBlockLayout received an " - "unknown input block." - ); + data.enthalpyDofTransformation = + m_fem.enthalpyFes->GetElementDofs(elementId, data.enthalpyDofs); - return 0; + data.gravityPotentialDofTransformation = + m_fem.gravityPotentialFes->GetElementDofs(elementId, + data.gravityPotentialDofs); + + data.displacementDofTransformation = m_fem.displacementFes->GetElementVDofs( + elementId, data.displacementDofs); + + data.integrationRule = &get_hydrostatic_rule( + m_fem, enthalpyElement, gravityPotentialElement, *transformation); + + const int quadraturePointCount = data.integrationRule->GetNPoints(); + + const int enthalpyDofCount = enthalpyElement.GetDof(); + + const int gravityPotentialDofCount = gravityPotentialElement.GetDof(); + + data.enthalpyBasis.SetSize(quadraturePointCount, enthalpyDofCount); + + data.gravityPotentialBasis.SetSize(quadraturePointCount, + gravityPotentialDofCount); + + enthalpyShape.SetSize(enthalpyDofCount); + gravityPotentialShape.SetSize(gravityPotentialDofCount); + + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; + ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = + data.integrationRule->IntPoint(quadraturePoint); + + enthalpyElement.CalcShape(integrationPoint, enthalpyShape); + + gravityPotentialElement.CalcShape(integrationPoint, + gravityPotentialShape); + + for (int dof = 0; dof < enthalpyDofCount; ++dof) { + data.enthalpyBasis(quadraturePoint, dof) = enthalpyShape(dof); + } + + for (int dof = 0; dof < gravityPotentialDofCount; ++dof) { + data.gravityPotentialBasis(quadraturePoint, dof) = + gravityPotentialShape(dof); + } } + } +} - int HydrostaticJacobianBlockLayout::Size(const HydrostaticJacobianInputBlock block) const { - switch (block) { - case HydrostaticJacobianInputBlock::enthalpy: - return m_enthalpySize; +void PreparedHydrostaticEquilibriumOperator::PrepareGeometry() { + mfem::Vector displacementLocal; - case HydrostaticJacobianInputBlock::gravityPotential: - return m_gravityPotentialSize; + true_to_local(*m_fem.displacementFes, m_context.GetDisplacementTrue(), + displacementLocal); - case HydrostaticJacobianInputBlock::bernoulliConstant: - return 1; + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); - case HydrostaticJacobianInputBlock::displacement: - return m_displacementSize; - } + mfem::Array compactificationDofs; - MFEM_ABORT( - "HydrostaticJacobianBlockLayout received an " - "unknown input block." - ); + mfem::Vector elementDisplacement; + mfem::Vector elementCompactification; - return 0; - } + for (ElementPAData &data : m_elements) { + mfem::ElementTransformation *transformation = + m_fem.mesh->GetElementTransformation(data.elementId); - int HydrostaticJacobianBlockLayout::GetTotalSize() const noexcept { - return m_totalSize; - } - - int HydrostaticJacobianBlockLayout::GetResidualSize() const noexcept { - return m_residualSize; - } - - PreparedHydrostaticEquilibriumOperator::PreparedHydrostaticEquilibriumOperator( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper - ) - : m_fem(f), - m_domainMapper(domainMapper), - m_context( - f, - domainMapper - ) { - MFEM_VERIFY(m_fem.mesh != nullptr, "PreparedHydrostaticEquilibriumOperator requires a mesh."); - - MFEM_VERIFY( - m_fem.enthalpyFes != nullptr, "PreparedHydrostaticEquilibriumOperator requires " - "the enthalpy finite-element space." - ); - - MFEM_VERIFY( - m_fem.gravityPotentialFes != nullptr, "PreparedHydrostaticEquilibriumOperator requires " - "the gravity-potential finite-element space." - ); - - MFEM_VERIFY( - m_fem.displacementFes != nullptr, "PreparedHydrostaticEquilibriumOperator requires " - "the displacement finite-element space." - ); - - MFEM_VERIFY( - m_fem.compactificationFes != nullptr, "PreparedHydrostaticEquilibriumOperator requires " - "the compactification finite-element space." - ); - - MFEM_VERIFY( - m_fem.compactificationCoordinate != nullptr, "PreparedHydrostaticEquilibriumOperator requires " - "the compactification coordinate." - ); - - MFEM_VERIFY( - m_fem.quadratureFactory != nullptr, "PreparedHydrostaticEquilibriumOperator requires " - "the quadrature-rule factory." - ); - - MFEM_VERIFY( - m_domainMapper.GetDimension() == m_fem.mesh->Dimension(), "The hydrostatic operator's stateless mapper " - "dimension does not match the mesh dimension." - ); - - m_enthalpyVariationTrue.SetSize(m_context.GetEnthalpyMap().full_size()); - m_gravityPotentialVariationTrue.SetSize(m_context.GetGravityPotentialMap().full_size()); - m_displacementVariationTrue.SetSize(m_context.GetDisplacementMap().full_size()); - m_fullEnthalpyAction.SetSize(m_context.GetEnthalpyMap().full_size()); - } - - PreparedHydrostaticEquilibriumReport PreparedHydrostaticEquilibriumOperator::Prepare( - const context::hydrostatic::HydrostaticEquilibriumStateView &state, - const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies, - const physics::RigidRotation &rotation - ) { - const bool rotationObjectChanged = - !m_context.IsPrepared() || dependencies.rotation != m_context.GetDependencies().rotation; - - PreparedHydrostaticEquilibriumReport report; - - report.contextReport = m_context.Prepare(state, dependencies); - - if (rotationObjectChanged) { - m_rotation = rotation; - report.updatedRotation = true; - } - - MFEM_VERIFY( - m_rotation.has_value(), "The prepared hydrostatic operator has no frozen " - "rotation state." - ); - - m_isPrepared = false; - - if (report.contextReport.preparedStaticDependencies) { - PrepareStaticPlan(); - } - - if (report.contextReport.preparedGeometryState) { - PrepareGeometry(); - PrepareAlgebraicJacobianBlocks(); - report.preparedAlgebraicJacobianBlocks = true; - } - - if (report.contextReport.preparedRotationDependencies) { - PrepareRotation(); - } - - if (report.contextReport.preparedBaseState) { - PrepareBaseState(); - FinalizeDisplacementJacobianPreparation(); - AssembleCachedResidual(); - ++m_residualPreparationCount; - report.preparedDisplacementJacobianData = true; - report.preparedResidual = true; - } - - MFEM_VERIFY( - !m_elements.empty(), "PreparedHydrostaticEquilibriumOperator found no " - "stellar elements." - ); - - MFEM_VERIFY( - m_cachedResidual.Size() == m_context.GetEnthalpyMap().reduced_size(), - "The prepared hydrostatic residual has the wrong supported size." - ); - - m_isPrepared = true; - return report; - } - - void PreparedHydrostaticEquilibriumOperator::PrepareStaticPlan() { - m_elements.clear(); - m_elements.reserve(m_fem.mesh->GetNE()); - - const int vacuumAttribute = m_domainMapper.GetVacuumElementAttribute(); - - mfem::Vector enthalpyShape; - mfem::Vector gravityPotentialShape; - - for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId); - - MFEM_VERIFY( - transformation != nullptr, "Prepared hydrostatic static planning received " - "a null element transformation." - ); - - if (transformation->Attribute == vacuumAttribute) { - continue; - } - - const mfem::FiniteElement &enthalpyElement = *m_fem.enthalpyFes->GetFE(elementId); - - const mfem::FiniteElement &gravityPotentialElement = *m_fem.gravityPotentialFes->GetFE(elementId); - - MFEM_VERIFY( - enthalpyElement.GetGeomType() == gravityPotentialElement.GetGeomType() && - enthalpyElement.GetGeomType() == transformation->GetGeometryType(), - "Hydrostatic element geometries do not agree." - ); - - m_elements.emplace_back(); - ElementPAData &data = m_elements.back(); - data.elementId = elementId; - - data.enthalpyDofTransformation = m_fem.enthalpyFes->GetElementDofs(elementId, data.enthalpyDofs); - - data.gravityPotentialDofTransformation = - m_fem.gravityPotentialFes->GetElementDofs(elementId, data.gravityPotentialDofs); - - data.displacementDofTransformation = - m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs); - - data.integrationRule = - &get_hydrostatic_rule(m_fem, enthalpyElement, gravityPotentialElement, *transformation); - - const int quadraturePointCount = data.integrationRule->GetNPoints(); - - const int enthalpyDofCount = enthalpyElement.GetDof(); - - const int gravityPotentialDofCount = gravityPotentialElement.GetDof(); - - data.enthalpyBasis.SetSize(quadraturePointCount, enthalpyDofCount); - - data.gravityPotentialBasis.SetSize(quadraturePointCount, gravityPotentialDofCount); - - enthalpyShape.SetSize(enthalpyDofCount); - gravityPotentialShape.SetSize(gravityPotentialDofCount); - - for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { - const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); - - enthalpyElement.CalcShape(integrationPoint, enthalpyShape); - - gravityPotentialElement.CalcShape(integrationPoint, gravityPotentialShape); - - for (int dof = 0; dof < enthalpyDofCount; ++dof) { - data.enthalpyBasis(quadraturePoint, dof) = enthalpyShape(dof); - } - - for (int dof = 0; dof < gravityPotentialDofCount; ++dof) { - data.gravityPotentialBasis(quadraturePoint, dof) = gravityPotentialShape(dof); - } - } - } - } - - void PreparedHydrostaticEquilibriumOperator::PrepareGeometry() { - mfem::Vector displacementLocal; - - true_to_local(*m_fem.displacementFes, m_context.GetDisplacementTrue(), displacementLocal); - - mapping::DomainMapperStateless::Workspace workspace(m_fem.mesh->Dimension()); - - mfem::Array compactificationDofs; - - mfem::Vector elementDisplacement; - mfem::Vector elementCompactification; - - for (ElementPAData &data : m_elements) { - mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); - - MFEM_VERIFY( - transformation != nullptr && data.integrationRule != nullptr, + MFEM_VERIFY(transformation != nullptr && data.integrationRule != nullptr, "Prepared hydrostatic geometry has invalid " - "static element data." - ); + "static element data."); - mfem::DofTransformation *compactificationDofTransformation = - m_fem.compactificationFes->GetElementDofs(data.elementId, compactificationDofs); + mfem::DofTransformation *compactificationDofTransformation = + m_fem.compactificationFes->GetElementDofs(data.elementId, + compactificationDofs); - displacementLocal.GetSubVector(data.displacementDofs, elementDisplacement); + displacementLocal.GetSubVector(data.displacementDofs, elementDisplacement); - m_fem.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); + m_fem.compactificationCoordinate->GetSubVector(compactificationDofs, + elementCompactification); - if (data.displacementDofTransformation != nullptr) { - data.displacementDofTransformation->InvTransformPrimal(elementDisplacement); - } - - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal(elementCompactification); - } - - const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); - - const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); - - data.baseDisplacementData.emplace( - mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement) - ); - - data.compactificationData.emplace(compactificationElement, elementCompactification); - - const mapping::ElementMappingData mappingData{ - .displacement = *data.baseDisplacementData, .compactification = *data.compactificationData - }; - - const int quadraturePointCount = data.integrationRule->GetNPoints(); - - data.physicalPositions.SetSize(quadraturePointCount, m_fem.mesh->Dimension()); - - data.quadratureWeights.SetSize(quadraturePointCount); - - data.baseMappingContexts.resize(quadraturePointCount); - - for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { - const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); - - transformation->SetIntPoint(&integrationPoint); - - mapping::VolumeMappingContext &mappingContext = data.baseMappingContexts[quadraturePoint]; - - const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume( - mappingData, *transformation, integrationPoint, workspace, mappingContext - ); - - MFEM_VERIFY( - mappingStatus == mapping::MappingStatus::valid, - "Stateless mapping failed while preparing " - "hydrostatic geometry. Element: " - << data.elementId << ", attribute: " << transformation->Attribute - << ", quadrature point: " << quadraturePoint << ", status: " << static_cast(mappingStatus) - ); - - const double quadratureWeight = mappingContext.quadrature.weight; - - MFEM_VERIFY( - std::isfinite(quadratureWeight) && quadratureWeight > 0.0, - "Prepared hydrostatic geometry encountered " - "an invalid quadrature weight." - ); - - data.quadratureWeights(quadraturePoint) = quadratureWeight; - - for (int component = 0; component < m_fem.mesh->Dimension(); ++component) { - const double position = mappingContext.mapping.physical_position(component); - - MFEM_VERIFY( - std::isfinite(position), "Prepared hydrostatic geometry encountered " - "a non-finite physical position." - ); - - data.physicalPositions(quadraturePoint, component) = position; - } - } - } + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal( + elementDisplacement); } - void PreparedHydrostaticEquilibriumOperator::PrepareAlgebraicJacobianBlocks() { - for (ElementPAData &data : m_elements) { - const int quadraturePointCount = data.quadratureWeights.Size(); + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal( + elementCompactification); + } - const int enthalpyDofCount = data.enthalpyBasis.Width(); + const mfem::FiniteElement &displacementElement = + *m_fem.displacementFes->GetFE(data.elementId); - const int gravityPotentialDofCount = data.gravityPotentialBasis.Width(); + const mfem::FiniteElement &compactificationElement = + *m_fem.compactificationFes->GetFE(data.elementId); - MFEM_VERIFY( - data.enthalpyBasis.Height() == quadraturePointCount && + data.baseDisplacementData.emplace( + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, + elementDisplacement)); + + data.compactificationData.emplace(compactificationElement, + elementCompactification); + + const mapping::ElementMappingData mappingData{ + .displacement = *data.baseDisplacementData, + .compactification = *data.compactificationData}; + + const int quadraturePointCount = data.integrationRule->GetNPoints(); + + data.physicalPositions.SetSize(quadraturePointCount, + m_fem.mesh->Dimension()); + + data.quadratureWeights.SetSize(quadraturePointCount); + + data.baseMappingContexts.resize(quadraturePointCount); + + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; + ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = + data.integrationRule->IntPoint(quadraturePoint); + + transformation->SetIntPoint(&integrationPoint); + + mapping::VolumeMappingContext &mappingContext = + data.baseMappingContexts[quadraturePoint]; + + const mapping::MappingStatus mappingStatus = + m_domainMapper.EvaluateVolume(mappingData, *transformation, + integrationPoint, workspace, + mappingContext); + + MFEM_VERIFY(mappingStatus == mapping::MappingStatus::valid, + "Stateless mapping failed while preparing " + "hydrostatic geometry. Element: " + << data.elementId + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadraturePoint + << ", status: " << static_cast(mappingStatus)); + + const double quadratureWeight = mappingContext.quadrature.weight; + + MFEM_VERIFY(std::isfinite(quadratureWeight) && quadratureWeight > 0.0, + "Prepared hydrostatic geometry encountered " + "an invalid quadrature weight."); + + data.quadratureWeights(quadraturePoint) = quadratureWeight; + + for (int component = 0; component < m_fem.mesh->Dimension(); + ++component) { + const double position = + mappingContext.mapping.physical_position(component); + + MFEM_VERIFY(std::isfinite(position), + "Prepared hydrostatic geometry encountered " + "a non-finite physical position."); + + data.physicalPositions(quadraturePoint, component) = position; + } + } + } +} + +void PreparedHydrostaticEquilibriumOperator::PrepareAlgebraicJacobianBlocks() { + for (ElementPAData &data : m_elements) { + const int quadraturePointCount = data.quadratureWeights.Size(); + + const int enthalpyDofCount = data.enthalpyBasis.Width(); + + const int gravityPotentialDofCount = data.gravityPotentialBasis.Width(); + + MFEM_VERIFY(data.enthalpyBasis.Height() == quadraturePointCount && data.gravityPotentialBasis.Height() == quadraturePointCount, "Prepared hydrostatic algebraic Jacobian has " - "inconsistent quadrature data." - ); + "inconsistent quadrature data."); - data.enthalpyJacobian.SetSize(enthalpyDofCount, enthalpyDofCount); + data.enthalpyJacobian.SetSize(enthalpyDofCount, enthalpyDofCount); - data.gravityPotentialJacobian.SetSize(enthalpyDofCount, gravityPotentialDofCount); + data.gravityPotentialJacobian.SetSize(enthalpyDofCount, + gravityPotentialDofCount); - data.bernoulliConstantJacobian.SetSize(enthalpyDofCount); + data.bernoulliConstantJacobian.SetSize(enthalpyDofCount); - data.enthalpyJacobian = 0.0; - data.gravityPotentialJacobian = 0.0; - data.bernoulliConstantJacobian = 0.0; + data.enthalpyJacobian = 0.0; + data.gravityPotentialJacobian = 0.0; + data.bernoulliConstantJacobian = 0.0; - for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { - const double quadratureWeight = data.quadratureWeights(quadraturePoint); + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; + ++quadraturePoint) { + const double quadratureWeight = data.quadratureWeights(quadraturePoint); - for (int testDof = 0; testDof < enthalpyDofCount; ++testDof) { - const double weightedTestBasis = quadratureWeight * data.enthalpyBasis(quadraturePoint, testDof); + for (int testDof = 0; testDof < enthalpyDofCount; ++testDof) { + const double weightedTestBasis = + quadratureWeight * data.enthalpyBasis(quadraturePoint, testDof); - data.bernoulliConstantJacobian(testDof) -= weightedTestBasis; + data.bernoulliConstantJacobian(testDof) -= weightedTestBasis; - for (int trialDof = 0; trialDof < enthalpyDofCount; ++trialDof) { - data.enthalpyJacobian(testDof, trialDof) += - weightedTestBasis * data.enthalpyBasis(quadraturePoint, trialDof); - } - - for (int trialDof = 0; trialDof < gravityPotentialDofCount; ++trialDof) { - data.gravityPotentialJacobian(testDof, trialDof) += - weightedTestBasis * data.gravityPotentialBasis(quadraturePoint, trialDof); - } - } - } + for (int trialDof = 0; trialDof < enthalpyDofCount; ++trialDof) { + data.enthalpyJacobian(testDof, trialDof) += + weightedTestBasis * data.enthalpyBasis(quadraturePoint, trialDof); } - ++m_algebraicJacobianStatistics.preparations; - } - - void PreparedHydrostaticEquilibriumOperator::PrepareRotation() { - MFEM_VERIFY(m_rotation.has_value(), "Prepared hydrostatic rotation has no frozen state."); - - mfem::Vector physicalPosition(m_fem.mesh->Dimension()); - - mfem::Vector coordinateDirection(m_fem.mesh->Dimension()); - - for (ElementPAData &data : m_elements) { - const int quadraturePointCount = data.physicalPositions.Height(); - - MFEM_VERIFY( - data.physicalPositions.Width() == m_fem.mesh->Dimension(), "Prepared hydrostatic rotation has invalid " - "geometry data." - ); - - data.rotationPotential.SetSize(quadraturePointCount); - - data.rotationGradient.SetSize(quadraturePointCount, m_fem.mesh->Dimension()); - - for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { - for (int component = 0; component < physicalPosition.Size(); ++component) { - physicalPosition(component) = data.physicalPositions(quadraturePoint, component); - } - - const double rotationPotential = m_rotation->potential(physicalPosition); - - MFEM_VERIFY( - std::isfinite(rotationPotential), "Prepared hydrostatic rotation encountered " - "a non-finite potential." - ); - - data.rotationPotential(quadraturePoint) = rotationPotential; - - for (int component = 0; component < physicalPosition.Size(); ++component) { - coordinateDirection = 0.0; - coordinateDirection(component) = 1.0; - - const double gradientComponent = - m_rotation->potential_directional_derivative(physicalPosition, coordinateDirection); - - MFEM_VERIFY( - std::isfinite(gradientComponent), "Prepared hydrostatic rotation encountered " - "a non-finite potential gradient." - ); - - data.rotationGradient(quadraturePoint, component) = gradientComponent; - } - } + for (int trialDof = 0; trialDof < gravityPotentialDofCount; + ++trialDof) { + data.gravityPotentialJacobian(testDof, trialDof) += + weightedTestBasis * + data.gravityPotentialBasis(quadraturePoint, trialDof); } + } + } + } + + ++m_algebraicJacobianStatistics.preparations; +} + +void PreparedHydrostaticEquilibriumOperator::PrepareRotation() { + MFEM_VERIFY(m_rotation.has_value(), + "Prepared hydrostatic rotation has no frozen state."); + + mfem::Vector physicalPosition(m_fem.mesh->Dimension()); + + mfem::Vector coordinateDirection(m_fem.mesh->Dimension()); + + for (ElementPAData &data : m_elements) { + const int quadraturePointCount = data.physicalPositions.Height(); + + MFEM_VERIFY(data.physicalPositions.Width() == m_fem.mesh->Dimension(), + "Prepared hydrostatic rotation has invalid " + "geometry data."); + + data.rotationPotential.SetSize(quadraturePointCount); + + data.rotationGradient.SetSize(quadraturePointCount, + m_fem.mesh->Dimension()); + + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; + ++quadraturePoint) { + for (int component = 0; component < physicalPosition.Size(); + ++component) { + physicalPosition(component) = + data.physicalPositions(quadraturePoint, component); + } + + const double rotationPotential = m_rotation->potential(physicalPosition); + + MFEM_VERIFY(std::isfinite(rotationPotential), + "Prepared hydrostatic rotation encountered " + "a non-finite potential."); + + data.rotationPotential(quadraturePoint) = rotationPotential; + + for (int component = 0; component < physicalPosition.Size(); + ++component) { + coordinateDirection = 0.0; + coordinateDirection(component) = 1.0; + + const double gradientComponent = + m_rotation->potential_directional_derivative(physicalPosition, + coordinateDirection); + + MFEM_VERIFY(std::isfinite(gradientComponent), + "Prepared hydrostatic rotation encountered " + "a non-finite potential gradient."); + + data.rotationGradient(quadraturePoint, component) = gradientComponent; + } + } + } +} + +void PreparedHydrostaticEquilibriumOperator::PrepareBaseState() { + mfem::Vector enthalpyLocal; + mfem::Vector gravityPotentialLocal; + + true_to_local(*m_fem.enthalpyFes, m_context.GetBaseEnthalpyTrue(), + enthalpyLocal); + + true_to_local(*m_fem.gravityPotentialFes, + m_context.GetBaseGravityPotentialTrue(), gravityPotentialLocal); + + mfem::Vector elementEnthalpy; + mfem::Vector elementGravityPotential; + mfem::Vector quadratureEnthalpy; + mfem::Vector quadratureGravityPotential; + + for (ElementPAData &data : m_elements) { + enthalpyLocal.GetSubVector(data.enthalpyDofs, elementEnthalpy); + + gravityPotentialLocal.GetSubVector(data.gravityPotentialDofs, + elementGravityPotential); + + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy); } - void PreparedHydrostaticEquilibriumOperator::PrepareBaseState() { - mfem::Vector enthalpyLocal; - mfem::Vector gravityPotentialLocal; - - true_to_local(*m_fem.enthalpyFes, m_context.GetBaseEnthalpyTrue(), enthalpyLocal); - - true_to_local(*m_fem.gravityPotentialFes, m_context.GetBaseGravityPotentialTrue(), gravityPotentialLocal); - - mfem::Vector elementEnthalpy; - mfem::Vector elementGravityPotential; - mfem::Vector quadratureEnthalpy; - mfem::Vector quadratureGravityPotential; - - for (ElementPAData &data : m_elements) { - enthalpyLocal.GetSubVector(data.enthalpyDofs, elementEnthalpy); - - gravityPotentialLocal.GetSubVector(data.gravityPotentialDofs, elementGravityPotential); - - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy); - } - - if (data.gravityPotentialDofTransformation != nullptr) { - data.gravityPotentialDofTransformation->InvTransformPrimal(elementGravityPotential); - } - - const int quadraturePointCount = data.quadratureWeights.Size(); - - quadratureEnthalpy.SetSize(quadraturePointCount); - - quadratureGravityPotential.SetSize(quadraturePointCount); - - data.enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy); - - data.gravityPotentialBasis.Mult(elementGravityPotential, quadratureGravityPotential); - - MFEM_VERIFY( - data.rotationPotential.Size() == quadraturePointCount, "Prepared hydrostatic base state has stale " - "rotation data." - ); - - data.weightedResidual.SetSize(quadraturePointCount); - - data.hydrostaticImbalance.SetSize(quadraturePointCount); - - for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { - const double imbalance = quadratureEnthalpy(quadraturePoint) + - quadratureGravityPotential(quadraturePoint) - - data.rotationPotential(quadraturePoint) - m_context.GetBernoulliConstant(); - - const double weightedResidual = data.quadratureWeights(quadraturePoint) * imbalance; - - MFEM_VERIFY( - std::isfinite(weightedResidual), "Prepared hydrostatic base state encountered " - "a non-finite residual value." - ); - - data.weightedResidual(quadraturePoint) = weightedResidual; - - data.hydrostaticImbalance(quadraturePoint) = imbalance; - } - } + if (data.gravityPotentialDofTransformation != nullptr) { + data.gravityPotentialDofTransformation->InvTransformPrimal( + elementGravityPotential); } - void PreparedHydrostaticEquilibriumOperator::FinalizeDisplacementJacobianPreparation() { - const int dimension = m_fem.mesh->Dimension(); + const int quadraturePointCount = data.quadratureWeights.Size(); - for (const ElementPAData &data : m_elements) { - const int quadraturePointCount = data.quadratureWeights.Size(); + quadratureEnthalpy.SetSize(quadraturePointCount); - MFEM_VERIFY( - data.baseDisplacementData.has_value() && data.compactificationData.has_value() && - static_cast(data.baseMappingContexts.size()) == quadraturePointCount && + quadratureGravityPotential.SetSize(quadraturePointCount); + + data.enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy); + + data.gravityPotentialBasis.Mult(elementGravityPotential, + quadratureGravityPotential); + + MFEM_VERIFY(data.rotationPotential.Size() == quadraturePointCount, + "Prepared hydrostatic base state has stale " + "rotation data."); + + data.weightedResidual.SetSize(quadraturePointCount); + + data.hydrostaticImbalance.SetSize(quadraturePointCount); + + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; + ++quadraturePoint) { + const double imbalance = quadratureEnthalpy(quadraturePoint) + + quadratureGravityPotential(quadraturePoint) - + data.rotationPotential(quadraturePoint) - + m_context.GetBernoulliConstant(); + + const double weightedResidual = + data.quadratureWeights(quadraturePoint) * imbalance; + + MFEM_VERIFY(std::isfinite(weightedResidual), + "Prepared hydrostatic base state encountered " + "a non-finite residual value."); + + data.weightedResidual(quadraturePoint) = weightedResidual; + + data.hydrostaticImbalance(quadraturePoint) = imbalance; + } + } +} + +void PreparedHydrostaticEquilibriumOperator:: + FinalizeDisplacementJacobianPreparation() { + const int dimension = m_fem.mesh->Dimension(); + + for (const ElementPAData &data : m_elements) { + const int quadraturePointCount = data.quadratureWeights.Size(); + + MFEM_VERIFY(data.baseDisplacementData.has_value() && + data.compactificationData.has_value() && + static_cast(data.baseMappingContexts.size()) == + quadraturePointCount && data.rotationGradient.Height() == quadraturePointCount && data.rotationGradient.Width() == dimension && data.hydrostaticImbalance.Size() == quadraturePointCount, "Prepared hydrostatic displacement Jacobian " - "has inconsistent frozen data." - ); - } + "has inconsistent frozen data."); + } - ++m_displacementJacobianStatistics.preparations; + ++m_displacementJacobianStatistics.preparations; +} + +void PreparedHydrostaticEquilibriumOperator::AssembleCachedResidual() { + mfem::Vector localResidual(m_fem.enthalpyFes->GetVSize()); + + localResidual = 0.0; + mfem::Vector elementResidual; + + for (const ElementPAData &data : m_elements) { + elementResidual.SetSize(data.enthalpyDofs.Size()); + + data.enthalpyBasis.MultTranspose(data.weightedResidual, elementResidual); + + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->TransformDual(elementResidual); } - void PreparedHydrostaticEquilibriumOperator::AssembleCachedResidual() { - mfem::Vector localResidual(m_fem.enthalpyFes->GetVSize()); + localResidual.AddElementVector(data.enthalpyDofs, elementResidual); + } - localResidual = 0.0; - mfem::Vector elementResidual; + local_to_true(*m_fem.enthalpyFes, localResidual, m_fullEnthalpyAction); - for (const ElementPAData &data : m_elements) { - elementResidual.SetSize(data.enthalpyDofs.Size()); + m_cachedResidual.SetSize(m_context.GetEnthalpyMap().reduced_size()); + m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, m_cachedResidual); +} - data.enthalpyBasis.MultTranspose(data.weightedResidual, elementResidual); +void PreparedHydrostaticEquilibriumOperator::BuildResidual( + mfem::Vector &residual) const { + VerifyPrepared(); + residual = m_cachedResidual; + ++m_residualApplicationCount; +} - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementResidual); - } +void PreparedHydrostaticEquilibriumOperator::ApplyEnthalpyJacobianAction( + const mfem::Vector &enthalpyVariation, mfem::Vector &action) const { + VerifyPrepared(); - localResidual.AddElementVector(data.enthalpyDofs, elementResidual); - } + MFEM_VERIFY( + enthalpyVariation.Size() == m_context.GetEnthalpyMap().reduced_size(), + "Prepared hydrostatic enthalpy variation has the wrong supported size."); - local_to_true(*m_fem.enthalpyFes, localResidual, m_fullEnthalpyAction); + m_context.GetEnthalpyMap().scatter(enthalpyVariation, + m_enthalpyVariationTrue); - m_cachedResidual.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, m_cachedResidual); + mfem::Vector enthalpyVariationLocal; + + true_to_local(*m_fem.enthalpyFes, m_enthalpyVariationTrue, + enthalpyVariationLocal); + + mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); + + localAction = 0.0; + + mfem::Vector elementVariation; + mfem::Vector elementAction; + + for (const ElementPAData &data : m_elements) { + enthalpyVariationLocal.GetSubVector(data.enthalpyDofs, elementVariation); + + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->InvTransformPrimal(elementVariation); } - void PreparedHydrostaticEquilibriumOperator::BuildResidual(mfem::Vector &residual) const { - VerifyPrepared(); - residual = m_cachedResidual; - ++m_residualApplicationCount; + elementAction.SetSize(data.enthalpyJacobian.Height()); + + data.enthalpyJacobian.Mult(elementVariation, elementAction); + + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->TransformDual(elementAction); } - void PreparedHydrostaticEquilibriumOperator::ApplyEnthalpyJacobianAction( - const mfem::Vector &enthalpyVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); + localAction.AddElementVector(data.enthalpyDofs, elementAction); + } - MFEM_VERIFY( - enthalpyVariation.Size() == m_context.GetEnthalpyMap().reduced_size(), - "Prepared hydrostatic enthalpy variation has the wrong supported size." - ); + local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - m_context.GetEnthalpyMap().scatter(enthalpyVariation, m_enthalpyVariationTrue); + action.SetSize(m_context.GetEnthalpyMap().reduced_size()); + m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - mfem::Vector enthalpyVariationLocal; + ++m_algebraicJacobianStatistics.enthalpyApplications; +} - true_to_local(*m_fem.enthalpyFes, m_enthalpyVariationTrue, enthalpyVariationLocal); - - mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); - - localAction = 0.0; - - mfem::Vector elementVariation; - mfem::Vector elementAction; - - for (const ElementPAData &data : m_elements) { - enthalpyVariationLocal.GetSubVector(data.enthalpyDofs, elementVariation); - - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->InvTransformPrimal(elementVariation); - } - - elementAction.SetSize(data.enthalpyJacobian.Height()); - - data.enthalpyJacobian.Mult(elementVariation, elementAction); - - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } - - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - - ++m_algebraicJacobianStatistics.enthalpyApplications; - } - - void PreparedHydrostaticEquilibriumOperator::ApplyGravityPotentialJacobianAction( +void PreparedHydrostaticEquilibriumOperator:: + ApplyGravityPotentialJacobianAction( const mfem::Vector &gravityPotentialVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); + mfem::Vector &action) const { + VerifyPrepared(); - MFEM_VERIFY( - gravityPotentialVariation.Size() == m_context.GetGravityPotentialMap().reduced_size(), - "Prepared hydrostatic gravity-potential variation has the wrong supported size." - ); + MFEM_VERIFY(gravityPotentialVariation.Size() == + m_context.GetGravityPotentialMap().reduced_size(), + "Prepared hydrostatic gravity-potential variation has the wrong " + "supported size."); - m_context.GetGravityPotentialMap().scatter(gravityPotentialVariation, m_gravityPotentialVariationTrue); + m_context.GetGravityPotentialMap().scatter(gravityPotentialVariation, + m_gravityPotentialVariationTrue); - mfem::Vector gravityPotentialVariationLocal; + mfem::Vector gravityPotentialVariationLocal; - true_to_local( - *m_fem.gravityPotentialFes, m_gravityPotentialVariationTrue, gravityPotentialVariationLocal - ); + true_to_local(*m_fem.gravityPotentialFes, m_gravityPotentialVariationTrue, + gravityPotentialVariationLocal); - mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); + mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); - localAction = 0.0; + localAction = 0.0; - mfem::Vector elementVariation; - mfem::Vector elementAction; + mfem::Vector elementVariation; + mfem::Vector elementAction; - for (const ElementPAData &data : m_elements) { - gravityPotentialVariationLocal.GetSubVector(data.gravityPotentialDofs, elementVariation); + for (const ElementPAData &data : m_elements) { + gravityPotentialVariationLocal.GetSubVector(data.gravityPotentialDofs, + elementVariation); - if (data.gravityPotentialDofTransformation != nullptr) { - data.gravityPotentialDofTransformation->InvTransformPrimal(elementVariation); - } - - elementAction.SetSize(data.gravityPotentialJacobian.Height()); - - data.gravityPotentialJacobian.Mult(elementVariation, elementAction); - - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } - - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - - ++m_algebraicJacobianStatistics.gravityPotentialApplications; + if (data.gravityPotentialDofTransformation != nullptr) { + data.gravityPotentialDofTransformation->InvTransformPrimal( + elementVariation); } - void PreparedHydrostaticEquilibriumOperator::ApplyBernoulliConstantJacobianAction( - const double bernoulliConstantVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); + elementAction.SetSize(data.gravityPotentialJacobian.Height()); - MFEM_VERIFY( - std::isfinite(bernoulliConstantVariation), "Prepared hydrostatic Bernoulli-constant Jacobian " - "received a non-finite variation." - ); + data.gravityPotentialJacobian.Mult(elementVariation, elementAction); - mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); - - localAction = 0.0; - mfem::Vector elementAction; - - for (const ElementPAData &data : m_elements) { - elementAction = data.bernoulliConstantJacobian; - elementAction *= bernoulliConstantVariation; - - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } - - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - - ++m_algebraicJacobianStatistics.bernoulliConstantApplications; + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->TransformDual(elementAction); } - void PreparedHydrostaticEquilibriumOperator::ApplyAlgebraicJacobianAction( - const mfem::Vector &enthalpyVariation, - const mfem::Vector &gravityPotentialVariation, - const double bernoulliConstantVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); + localAction.AddElementVector(data.enthalpyDofs, elementAction); + } - MFEM_VERIFY( - std::isfinite(bernoulliConstantVariation), "Prepared hydrostatic algebraic Jacobian received " - "a non-finite Bernoulli-constant variation." - ); + local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - MFEM_VERIFY( - enthalpyVariation.Size() == m_context.GetEnthalpyMap().reduced_size(), - "Prepared hydrostatic algebraic enthalpy variation has the wrong supported size." - ); + action.SetSize(m_context.GetEnthalpyMap().reduced_size()); + m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - MFEM_VERIFY( - gravityPotentialVariation.Size() == m_context.GetGravityPotentialMap().reduced_size(), - "Prepared hydrostatic algebraic gravity-potential variation has the wrong supported size." - ); + ++m_algebraicJacobianStatistics.gravityPotentialApplications; +} - m_context.GetEnthalpyMap().scatter(enthalpyVariation, m_enthalpyVariationTrue); - m_context.GetGravityPotentialMap().scatter(gravityPotentialVariation, m_gravityPotentialVariationTrue); +void PreparedHydrostaticEquilibriumOperator:: + ApplyBernoulliConstantJacobianAction( + const double bernoulliConstantVariation, mfem::Vector &action) const { + VerifyPrepared(); - mfem::Vector enthalpyVariationLocal; - mfem::Vector gravityPotentialVariationLocal; + MFEM_VERIFY(std::isfinite(bernoulliConstantVariation), + "Prepared hydrostatic Bernoulli-constant Jacobian " + "received a non-finite variation."); - true_to_local(*m_fem.enthalpyFes, m_enthalpyVariationTrue, enthalpyVariationLocal); + mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); - true_to_local( - *m_fem.gravityPotentialFes, m_gravityPotentialVariationTrue, gravityPotentialVariationLocal - ); + localAction = 0.0; + mfem::Vector elementAction; - mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); + for (const ElementPAData &data : m_elements) { + elementAction = data.bernoulliConstantJacobian; + elementAction *= bernoulliConstantVariation; - localAction = 0.0; + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->TransformDual(elementAction); + } - mfem::Vector elementEnthalpyVariation; - mfem::Vector elementGravityPotentialVariation; - mfem::Vector elementAction; - mfem::Vector elementWorkspace; + localAction.AddElementVector(data.enthalpyDofs, elementAction); + } - for (const ElementPAData &data : m_elements) { - enthalpyVariationLocal.GetSubVector(data.enthalpyDofs, elementEnthalpyVariation); + local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - gravityPotentialVariationLocal.GetSubVector(data.gravityPotentialDofs, elementGravityPotentialVariation); + action.SetSize(m_context.GetEnthalpyMap().reduced_size()); + m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation); - } + ++m_algebraicJacobianStatistics.bernoulliConstantApplications; +} - if (data.gravityPotentialDofTransformation != nullptr) { - data.gravityPotentialDofTransformation->InvTransformPrimal(elementGravityPotentialVariation); - } +void PreparedHydrostaticEquilibriumOperator::ApplyAlgebraicJacobianAction( + const mfem::Vector &enthalpyVariation, + const mfem::Vector &gravityPotentialVariation, + const double bernoulliConstantVariation, mfem::Vector &action) const { + VerifyPrepared(); - MFEM_VERIFY( - data.enthalpyJacobian.Height() == data.gravityPotentialJacobian.Height() && - data.enthalpyJacobian.Width() == elementEnthalpyVariation.Size() && - data.gravityPotentialJacobian.Width() == elementGravityPotentialVariation.Size() && - data.bernoulliConstantJacobian.Size() == data.enthalpyJacobian.Height(), + MFEM_VERIFY(std::isfinite(bernoulliConstantVariation), + "Prepared hydrostatic algebraic Jacobian received " + "a non-finite Bernoulli-constant variation."); + + MFEM_VERIFY(enthalpyVariation.Size() == + m_context.GetEnthalpyMap().reduced_size(), + "Prepared hydrostatic algebraic enthalpy variation has the wrong " + "supported size."); + + MFEM_VERIFY(gravityPotentialVariation.Size() == + m_context.GetGravityPotentialMap().reduced_size(), + "Prepared hydrostatic algebraic gravity-potential variation has " + "the wrong supported size."); + + m_context.GetEnthalpyMap().scatter(enthalpyVariation, + m_enthalpyVariationTrue); + m_context.GetGravityPotentialMap().scatter(gravityPotentialVariation, + m_gravityPotentialVariationTrue); + + mfem::Vector enthalpyVariationLocal; + mfem::Vector gravityPotentialVariationLocal; + + true_to_local(*m_fem.enthalpyFes, m_enthalpyVariationTrue, + enthalpyVariationLocal); + + true_to_local(*m_fem.gravityPotentialFes, m_gravityPotentialVariationTrue, + gravityPotentialVariationLocal); + + mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); + + localAction = 0.0; + + mfem::Vector elementEnthalpyVariation; + mfem::Vector elementGravityPotentialVariation; + mfem::Vector elementAction; + mfem::Vector elementWorkspace; + + for (const ElementPAData &data : m_elements) { + enthalpyVariationLocal.GetSubVector(data.enthalpyDofs, + elementEnthalpyVariation); + + gravityPotentialVariationLocal.GetSubVector( + data.gravityPotentialDofs, elementGravityPotentialVariation); + + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->InvTransformPrimal( + elementEnthalpyVariation); + } + + if (data.gravityPotentialDofTransformation != nullptr) { + data.gravityPotentialDofTransformation->InvTransformPrimal( + elementGravityPotentialVariation); + } + + MFEM_VERIFY(data.enthalpyJacobian.Height() == + data.gravityPotentialJacobian.Height() && + data.enthalpyJacobian.Width() == + elementEnthalpyVariation.Size() && + data.gravityPotentialJacobian.Width() == + elementGravityPotentialVariation.Size() && + data.bernoulliConstantJacobian.Size() == + data.enthalpyJacobian.Height(), "Prepared hydrostatic algebraic Jacobian has " - "incompatible element dimensions." - ); + "incompatible element dimensions."); - elementAction.SetSize(data.enthalpyJacobian.Height()); + elementAction.SetSize(data.enthalpyJacobian.Height()); - elementWorkspace.SetSize(data.gravityPotentialJacobian.Height()); + elementWorkspace.SetSize(data.gravityPotentialJacobian.Height()); - data.enthalpyJacobian.Mult(elementEnthalpyVariation, elementAction); + data.enthalpyJacobian.Mult(elementEnthalpyVariation, elementAction); - data.gravityPotentialJacobian.Mult(elementGravityPotentialVariation, elementWorkspace); + data.gravityPotentialJacobian.Mult(elementGravityPotentialVariation, + elementWorkspace); - elementAction.Add(1.0, elementWorkspace); - elementAction.Add(bernoulliConstantVariation, data.bernoulliConstantJacobian); + elementAction.Add(1.0, elementWorkspace); + elementAction.Add(bernoulliConstantVariation, + data.bernoulliConstantJacobian); - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } - - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - - ++m_algebraicJacobianStatistics.combinedApplications; + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->TransformDual(elementAction); } - void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianAction( - const mfem::Vector &displacementVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); + localAction.AddElementVector(data.enthalpyDofs, elementAction); + } - MFEM_VERIFY( - displacementVariation.Size() == m_context.GetDisplacementMap().reduced_size(), - "Prepared hydrostatic displacement variation has the wrong supported size." - ); + local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - m_context.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue); + action.SetSize(m_context.GetEnthalpyMap().reduced_size()); + m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - mfem::Vector displacementVariationLocal; + ++m_algebraicJacobianStatistics.combinedApplications; +} - true_to_local(*m_fem.displacementFes, m_displacementVariationTrue, displacementVariationLocal); +void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianAction( + const mfem::Vector &displacementVariation, mfem::Vector &action) const { + VerifyPrepared(); - mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); + MFEM_VERIFY(displacementVariation.Size() == + m_context.GetDisplacementMap().reduced_size(), + "Prepared hydrostatic displacement variation has the wrong " + "supported size."); - localAction = 0.0; + m_context.GetDisplacementMap().scatter(displacementVariation, + m_displacementVariationTrue); - mapping::DomainMapperStateless::Workspace workspace(m_fem.mesh->Dimension()); + mfem::Vector displacementVariationLocal; - mfem::Vector elementDisplacementVariation; - mfem::Vector weightedQuadratureVariation; - mfem::Vector elementAction; + true_to_local(*m_fem.displacementFes, m_displacementVariationTrue, + displacementVariationLocal); - for (const ElementPAData &data : m_elements) { - MFEM_VERIFY( - data.baseDisplacementData.has_value() && data.compactificationData.has_value() && + mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); + + localAction = 0.0; + + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); + + mfem::Vector elementDisplacementVariation; + mfem::Vector weightedQuadratureVariation; + mfem::Vector elementAction; + + for (const ElementPAData &data : m_elements) { + MFEM_VERIFY(data.baseDisplacementData.has_value() && + data.compactificationData.has_value() && data.integrationRule != nullptr, "Prepared hydrostatic displacement Jacobian " - "has invalid frozen element data." - ); + "has invalid frozen element data."); - mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); + mfem::ElementTransformation *transformation = + m_fem.mesh->GetElementTransformation(data.elementId); - MFEM_VERIFY( - transformation != nullptr, "Prepared hydrostatic displacement Jacobian " - "received a null element transformation." - ); + MFEM_VERIFY(transformation != nullptr, + "Prepared hydrostatic displacement Jacobian " + "received a null element transformation."); - displacementVariationLocal.GetSubVector(data.displacementDofs, elementDisplacementVariation); + displacementVariationLocal.GetSubVector(data.displacementDofs, + elementDisplacementVariation); - if (data.displacementDofTransformation != nullptr) { - data.displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation); - } + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal( + elementDisplacementVariation); + } - const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); + const mfem::FiniteElement &displacementElement = + *m_fem.displacementFes->GetFE(data.elementId); - const mapping::ElementDisplacementData directionData = - mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacementVariation); + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacementVariation); - const mapping::ElementMappingData mappingData{ - .displacement = *data.baseDisplacementData, .compactification = *data.compactificationData - }; + const mapping::ElementMappingData mappingData{ + .displacement = *data.baseDisplacementData, + .compactification = *data.compactificationData}; - const int quadraturePointCount = data.integrationRule->GetNPoints(); + const int quadraturePointCount = data.integrationRule->GetNPoints(); - MFEM_VERIFY( - static_cast(data.baseMappingContexts.size()) == quadraturePointCount && + MFEM_VERIFY(static_cast(data.baseMappingContexts.size()) == + quadraturePointCount && data.quadratureWeights.Size() == quadraturePointCount && data.hydrostaticImbalance.Size() == quadraturePointCount && data.rotationGradient.Height() == quadraturePointCount && data.rotationGradient.Width() == m_fem.mesh->Dimension(), "Prepared hydrostatic displacement Jacobian " - "has inconsistent quadrature data." - ); + "has inconsistent quadrature data."); - weightedQuadratureVariation.SetSize(quadraturePointCount); + weightedQuadratureVariation.SetSize(quadraturePointCount); - for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { - const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; + ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = + data.integrationRule->IntPoint(quadraturePoint); - transformation->SetIntPoint(&integrationPoint); + transformation->SetIntPoint(&integrationPoint); - mapping::VolumeMappingVariation variation; + mapping::VolumeMappingVariation variation; - const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolumeVariation( - mappingData, directionData, *transformation, integrationPoint, - data.baseMappingContexts[quadraturePoint], workspace, variation - ); + const mapping::MappingStatus mappingStatus = + m_domainMapper.EvaluateVolumeVariation( + mappingData, directionData, *transformation, integrationPoint, + data.baseMappingContexts[quadraturePoint], workspace, variation); - MFEM_VERIFY( - mappingStatus == mapping::MappingStatus::valid, - "Stateless mapping variation failed while " - "applying the prepared hydrostatic " - "displacement Jacobian. Element: " - << data.elementId << ", attribute: " << transformation->Attribute - << ", quadrature point: " << quadraturePoint << ", status: " << static_cast(mappingStatus) - ); + MFEM_VERIFY(mappingStatus == mapping::MappingStatus::valid, + "Stateless mapping variation failed while " + "applying the prepared hydrostatic " + "displacement Jacobian. Element: " + << data.elementId + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadraturePoint + << ", status: " << static_cast(mappingStatus)); - double rotationPotentialVariation = 0.0; + double rotationPotentialVariation = 0.0; - for (int component = 0; component < m_fem.mesh->Dimension(); ++component) { - rotationPotentialVariation += data.rotationGradient(quadraturePoint, component) * - variation.mapping.physical_position_variation(component); - } + for (int component = 0; component < m_fem.mesh->Dimension(); + ++component) { + rotationPotentialVariation += + data.rotationGradient(quadraturePoint, component) * + variation.mapping.physical_position_variation(component); + } - const double weightedVariation = - data.hydrostaticImbalance(quadraturePoint) * variation.weight_variation - - data.quadratureWeights(quadraturePoint) * rotationPotentialVariation; + const double weightedVariation = + data.hydrostaticImbalance(quadraturePoint) * + variation.weight_variation - + data.quadratureWeights(quadraturePoint) * rotationPotentialVariation; - MFEM_VERIFY( - std::isfinite(weightedVariation), "Prepared hydrostatic displacement Jacobian " - "encountered a non-finite quadrature action." - ); + MFEM_VERIFY(std::isfinite(weightedVariation), + "Prepared hydrostatic displacement Jacobian " + "encountered a non-finite quadrature action."); - weightedQuadratureVariation(quadraturePoint) = weightedVariation; - } - - elementAction.SetSize(data.enthalpyDofs.Size()); - - data.enthalpyBasis.MultTranspose(weightedQuadratureVariation, elementAction); - - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementAction); - } - - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } - - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - - ++m_displacementJacobianStatistics.applications; + weightedQuadratureVariation(quadraturePoint) = weightedVariation; } - void PreparedHydrostaticEquilibriumOperator::ApplyCompleteJacobianAction( - const mfem::Vector &enthalpyVariation, - const mfem::Vector &gravityPotentialVariation, - const double bernoulliConstantVariation, - const mfem::Vector &displacementVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); + elementAction.SetSize(data.enthalpyDofs.Size()); - mfem::Vector displacementAction; + data.enthalpyBasis.MultTranspose(weightedQuadratureVariation, + elementAction); - ApplyAlgebraicJacobianAction(enthalpyVariation, gravityPotentialVariation, bernoulliConstantVariation, action); - - ApplyDisplacementJacobianAction(displacementVariation, displacementAction); - - MFEM_VERIFY( - action.Size() == displacementAction.Size(), "Prepared hydrostatic complete Jacobian produced " - "incompatible algebraic and displacement actions." - ); - - action += displacementAction; - ++m_completeJacobianStatistics.applications; + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->TransformDual(elementAction); } - bool PreparedHydrostaticEquilibriumOperator::IsPrepared() const noexcept { - return m_isPrepared; - } + localAction.AddElementVector(data.enthalpyDofs, elementAction); + } - const context::hydrostatic::HydrostaticPreparationStatistics & - PreparedHydrostaticEquilibriumOperator::GetContextPreparationStatistics() const noexcept { - return m_context.GetPreparationStatistics(); - } + local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); - std::uint64_t PreparedHydrostaticEquilibriumOperator::GetResidualPreparationCount() const noexcept { - return m_residualPreparationCount; - } + action.SetSize(m_context.GetEnthalpyMap().reduced_size()); + m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); - std::uint64_t PreparedHydrostaticEquilibriumOperator::GetResidualApplicationCount() const noexcept { - return m_residualApplicationCount; - } + ++m_displacementJacobianStatistics.applications; +} - const PreparedHydrostaticAlgebraicJacobianStatistics & - PreparedHydrostaticEquilibriumOperator::GetAlgebraicJacobianStatistics() const noexcept { - return m_algebraicJacobianStatistics; - } +void PreparedHydrostaticEquilibriumOperator::ApplyCompleteJacobianAction( + const mfem::Vector &enthalpyVariation, + const mfem::Vector &gravityPotentialVariation, + const double bernoulliConstantVariation, + const mfem::Vector &displacementVariation, mfem::Vector &action) const { + VerifyPrepared(); - const PreparedHydrostaticDisplacementJacobianStatistics & - PreparedHydrostaticEquilibriumOperator::GetDisplacementJacobianStatistics() const noexcept { - return m_displacementJacobianStatistics; - } + mfem::Vector displacementAction; - const PreparedHydrostaticCompleteJacobianStatistics & - PreparedHydrostaticEquilibriumOperator::GetCompleteJacobianStatistics() const noexcept { - return m_completeJacobianStatistics; - } + ApplyAlgebraicJacobianAction(enthalpyVariation, gravityPotentialVariation, + bernoulliConstantVariation, action); - std::size_t PreparedHydrostaticEquilibriumOperator::GetStellarElementCount() const noexcept { - return m_elements.size(); - } + ApplyDisplacementJacobianAction(displacementVariation, displacementAction); - const fem::FEM &PreparedHydrostaticEquilibriumOperator::GetFEM() const noexcept { - return m_fem; - } + MFEM_VERIFY(action.Size() == displacementAction.Size(), + "Prepared hydrostatic complete Jacobian produced " + "incompatible algebraic and displacement actions."); - const field::FieldDofMap &PreparedHydrostaticEquilibriumOperator::GetEnthalpyMap() const noexcept { - return m_context.GetEnthalpyMap(); - } + action += displacementAction; + ++m_completeJacobianStatistics.applications; +} - const field::FieldDofMap &PreparedHydrostaticEquilibriumOperator::GetGravityPotentialMap() const noexcept { - return m_context.GetGravityPotentialMap(); - } +bool PreparedHydrostaticEquilibriumOperator::IsPrepared() const noexcept { + return m_isPrepared; +} - const field::FieldDofMap &PreparedHydrostaticEquilibriumOperator::GetDisplacementMap() const noexcept { - return m_context.GetDisplacementMap(); - } +const context::hydrostatic::HydrostaticPreparationStatistics & +PreparedHydrostaticEquilibriumOperator::GetContextPreparationStatistics() + const noexcept { + return m_context.GetPreparationStatistics(); +} - void PreparedHydrostaticEquilibriumOperator::VerifyPrepared() const { - MFEM_VERIFY( - m_isPrepared, "PreparedHydrostaticEquilibriumOperator must be " - "prepared before residual or Jacobian application." - ); - } +std::uint64_t +PreparedHydrostaticEquilibriumOperator::GetResidualPreparationCount() + const noexcept { + return m_residualPreparationCount; +} - PreparedHydrostaticEquilibriumJacobianOperator::PreparedHydrostaticEquilibriumJacobianOperator( +std::uint64_t +PreparedHydrostaticEquilibriumOperator::GetResidualApplicationCount() + const noexcept { + return m_residualApplicationCount; +} + +const PreparedHydrostaticAlgebraicJacobianStatistics & +PreparedHydrostaticEquilibriumOperator::GetAlgebraicJacobianStatistics() + const noexcept { + return m_algebraicJacobianStatistics; +} + +const PreparedHydrostaticDisplacementJacobianStatistics & +PreparedHydrostaticEquilibriumOperator::GetDisplacementJacobianStatistics() + const noexcept { + return m_displacementJacobianStatistics; +} + +const PreparedHydrostaticCompleteJacobianStatistics & +PreparedHydrostaticEquilibriumOperator::GetCompleteJacobianStatistics() + const noexcept { + return m_completeJacobianStatistics; +} + +std::size_t PreparedHydrostaticEquilibriumOperator::GetStellarElementCount() + const noexcept { + return m_elements.size(); +} + +const fem::FEM & +PreparedHydrostaticEquilibriumOperator::GetFEM() const noexcept { + return m_fem; +} + +const field::FieldDofMap & +PreparedHydrostaticEquilibriumOperator::GetEnthalpyMap() const noexcept { + return m_context.GetEnthalpyMap(); +} + +const field::FieldDofMap & +PreparedHydrostaticEquilibriumOperator::GetGravityPotentialMap() + const noexcept { + return m_context.GetGravityPotentialMap(); +} + +const field::FieldDofMap & +PreparedHydrostaticEquilibriumOperator::GetDisplacementMap() const noexcept { + return m_context.GetDisplacementMap(); +} + +void PreparedHydrostaticEquilibriumOperator::VerifyPrepared() const { + MFEM_VERIFY(m_isPrepared, + "PreparedHydrostaticEquilibriumOperator must be " + "prepared before residual or Jacobian application."); +} + +PreparedHydrostaticEquilibriumJacobianOperator:: + PreparedHydrostaticEquilibriumJacobianOperator( const fem::FEM &f, - const PreparedHydrostaticEquilibriumOperator &preparedOperator - ) - : mfem::Operator( - HydrostaticJacobianBlockLayout(f).GetResidualSize(), - HydrostaticJacobianBlockLayout(f).GetTotalSize() - ), - m_layout(f), - m_preparedOperator(preparedOperator) { - MFEM_VERIFY( - &m_preparedOperator.GetFEM() == &f, "Prepared hydrostatic MFEM adapter and prepared " - "operator must use the same FEM object." - ); + const PreparedHydrostaticEquilibriumOperator &preparedOperator) + : mfem::Operator(HydrostaticJacobianBlockLayout(f).GetResidualSize(), + HydrostaticJacobianBlockLayout(f).GetTotalSize()), + m_layout(f), m_preparedOperator(preparedOperator) { + MFEM_VERIFY(&m_preparedOperator.GetFEM() == &f, + "Prepared hydrostatic MFEM adapter and prepared " + "operator must use the same FEM object."); - MFEM_VERIFY( - Height() == m_layout.GetResidualSize() && Width() == m_layout.GetTotalSize(), - "Prepared hydrostatic MFEM adapter has " - "inconsistent operator dimensions." - ); - } + MFEM_VERIFY(Height() == m_layout.GetResidualSize() && + Width() == m_layout.GetTotalSize(), + "Prepared hydrostatic MFEM adapter has " + "inconsistent operator dimensions."); +} - void PreparedHydrostaticEquilibriumJacobianOperator::Mult( - const mfem::Vector &direction, - mfem::Vector &action - ) const { - MFEM_VERIFY( - direction.Size() == Width(), "Prepared hydrostatic MFEM adapter received a " - "direction with the wrong size." - ); +void PreparedHydrostaticEquilibriumJacobianOperator::Mult( + const mfem::Vector &direction, mfem::Vector &action) const { + MFEM_VERIFY(direction.Size() == Width(), + "Prepared hydrostatic MFEM adapter received a " + "direction with the wrong size."); - mfem::Vector enthalpyVariation; - mfem::Vector gravityPotentialVariation; - mfem::Vector displacementVariation; + mfem::Vector enthalpyVariation; + mfem::Vector gravityPotentialVariation; + mfem::Vector displacementVariation; - copy_vector_block( - direction, m_layout.Offset(HydrostaticJacobianInputBlock::enthalpy), - m_layout.Size(HydrostaticJacobianInputBlock::enthalpy), enthalpyVariation - ); + copy_vector_block(direction, + m_layout.Offset(HydrostaticJacobianInputBlock::enthalpy), + m_layout.Size(HydrostaticJacobianInputBlock::enthalpy), + enthalpyVariation); - copy_vector_block( - direction, m_layout.Offset(HydrostaticJacobianInputBlock::gravityPotential), - m_layout.Size(HydrostaticJacobianInputBlock::gravityPotential), gravityPotentialVariation - ); + copy_vector_block( + direction, + m_layout.Offset(HydrostaticJacobianInputBlock::gravityPotential), + m_layout.Size(HydrostaticJacobianInputBlock::gravityPotential), + gravityPotentialVariation); - copy_vector_block( - direction, m_layout.Offset(HydrostaticJacobianInputBlock::displacement), - m_layout.Size(HydrostaticJacobianInputBlock::displacement), displacementVariation - ); + copy_vector_block( + direction, m_layout.Offset(HydrostaticJacobianInputBlock::displacement), + m_layout.Size(HydrostaticJacobianInputBlock::displacement), + displacementVariation); - const double bernoulliConstantVariation = - direction(m_layout.Offset(HydrostaticJacobianInputBlock::bernoulliConstant)); + const double bernoulliConstantVariation = direction( + m_layout.Offset(HydrostaticJacobianInputBlock::bernoulliConstant)); - m_preparedOperator.ApplyCompleteJacobianAction( - enthalpyVariation, gravityPotentialVariation, bernoulliConstantVariation, displacementVariation, action - ); + m_preparedOperator.ApplyCompleteJacobianAction( + enthalpyVariation, gravityPotentialVariation, bernoulliConstantVariation, + displacementVariation, action); - MFEM_VERIFY( - action.Size() == Height(), "Prepared hydrostatic MFEM adapter produced an " - "action with the wrong size." - ); - } + MFEM_VERIFY(action.Size() == Height(), + "Prepared hydrostatic MFEM adapter produced an " + "action with the wrong size."); +} - const HydrostaticJacobianBlockLayout &PreparedHydrostaticEquilibriumJacobianOperator::GetLayout() const noexcept { - return m_layout; - } +const HydrostaticJacobianBlockLayout & +PreparedHydrostaticEquilibriumJacobianOperator::GetLayout() const noexcept { + return m_layout; +} } // namespace mean_field::operators diff --git a/libmeanfield/impl/operators/prepared_mass_normalization.cpp b/libmeanfield/impl/operators/prepared_mass_normalization.cpp index 3097225..d254801 100644 --- a/libmeanfield/impl/operators/prepared_mass_normalization.cpp +++ b/libmeanfield/impl/operators/prepared_mass_normalization.cpp @@ -9,711 +9,742 @@ module mean_field; import :operators.prepared_mass_normalization; namespace { - void validate_finite_vector( - const mfem::Vector &vector, - const char *message - ) { - for (int index = 0; index < vector.Size(); ++index) { - MFEM_VERIFY(std::isfinite(vector(index)), message); - } - } +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - void true_to_local( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const mfem::Vector &trueVector, - mfem::Vector &localVector - ) { - MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), "True vector has the wrong size."); +[[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Vacuum>(attribute); +} - localVector.SetSize(finiteElementSpace.GetVSize()); +void validate_finite_vector(const mfem::Vector &vector, const char *message) { + for (int index = 0; index < vector.Size(); ++index) { + MFEM_VERIFY(std::isfinite(vector(index)), message); + } +} - const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); +void true_to_local(const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, mfem::Vector &localVector) { + MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), + "True vector has the wrong size."); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } - } + localVector.SetSize(finiteElementSpace.GetVSize()); - const mfem::IntegrationRule &get_mass_normalization_rule( - const mean_field::fem::FEM &f, - const mfem::FiniteElement &densityElement, - const mfem::ElementTransformation &transformation - ) { - using DensityField = mean_field::field::Field; + const mfem::Operator *prolongation = + finiteElementSpace.GetProlongationMatrix(); - MFEM_VERIFY( - densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder, - "The mass-normalization element does not match the registered " - "density field." - ); + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } +} - const mean_field::quadrature::Query query = - DensityField::make_query( - mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array{}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); +const mfem::IntegrationRule & +get_mass_normalization_rule(const mean_field::fem::FEM &f, + const mfem::FiniteElement &densityElement, + const mfem::ElementTransformation &transformation) { + using DensityField = mean_field::field::Field; - const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY(densityElement.GetOrder() == + mean_field::field::Density::Scalar::familyOrder, + "The mass-normalization element does not match the registered " + "density field."); - MFEM_VERIFY( - resolution.integration_rule != nullptr, "The quadrature policy did not return a mass-normalization rule." - ); + const mean_field::quadrature::Query query = DensityField::make_query< + mean_field::field::Density::Form::MassNormalization>( + mean_field::quadrature::QuadratureRole::discretization, + transformation.OrderW(), std::array{}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - return *resolution.integration_rule; - } + const auto resolution = + f.quadratureFactory->get(query, transformation.GetGeometryType()); - void validate_shared_gravity_revisions( - const mean_field::operators::context::gravity_field::GravityFieldLinearizationContext &gravityContext, - const mean_field::operators::MassNormalizationDependencies &dependencies - ) { - MFEM_VERIFY( - gravityContext.IsPrepared(), "PreparedMassNormalizationOperator requires the shared gravity " - "linearization context to be prepared first." - ); + MFEM_VERIFY( + resolution.integration_rule != nullptr, + "The quadrature policy did not return a mass-normalization rule."); - const auto &revisions = gravityContext.GetRevisions(); + return *resolution.integration_rule; +} - MFEM_VERIFY( - revisions.discretization.value == dependencies.discretization.revision && - revisions.density.value == dependencies.density.revision && - revisions.displacement.value == dependencies.displacement.revision, - "PreparedMassNormalizationOperator received dependency revisions " - "that do not match the shared gravity context." - ); - } +void validate_shared_gravity_revisions( + const mean_field::operators::context::gravity_field:: + GravityFieldLinearizationContext &gravityContext, + const mean_field::operators::MassNormalizationDependencies &dependencies) { + MFEM_VERIFY(gravityContext.IsPrepared(), + "PreparedMassNormalizationOperator requires the shared gravity " + "linearization context to be prepared first."); - void validate_shared_identity_transition( - const mean_field::operators::MassNormalizationDependencyStamp &prepared, - const mean_field::operators::MassNormalizationDependencyStamp &requested, - const char *message - ) { - MFEM_VERIFY(prepared.identity == requested.identity || prepared.revision != requested.revision, message); - } + const auto &revisions = gravityContext.GetRevisions(); + + MFEM_VERIFY( + revisions.discretization.value == dependencies.discretization.revision && + revisions.density.value == dependencies.density.revision && + revisions.displacement.value == dependencies.displacement.revision, + "PreparedMassNormalizationOperator received dependency revisions " + "that do not match the shared gravity context."); +} + +void validate_shared_identity_transition( + const mean_field::operators::MassNormalizationDependencyStamp &prepared, + const mean_field::operators::MassNormalizationDependencyStamp &requested, + const char *message) { + MFEM_VERIFY(prepared.identity == requested.identity || + prepared.revision != requested.revision, + message); +} } // namespace namespace mean_field::operators { - PreparedMassNormalizationOperator::PreparedMassNormalizationOperator( - const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, - const context::gravity_field::GravityFieldLinearizationContext &gravityContext - ) - : m_fem(f), - m_domainMapper(domainMapper), - m_gravityContext(gravityContext) { - MFEM_VERIFY(m_fem.mesh != nullptr, "PreparedMassNormalizationOperator requires a mesh."); - MFEM_VERIFY( - m_fem.densityFes != nullptr && m_fem.displacementFes != nullptr && m_fem.compactificationFes != nullptr && - m_fem.compactificationCoordinate != nullptr && m_fem.quadratureFactory != nullptr, - "PreparedMassNormalizationOperator requires density, " - "displacement, compactification, and quadrature data." - ); - MFEM_VERIFY( - m_domainMapper.GetDimension() == m_fem.mesh->Dimension(), - "PreparedMassNormalizationOperator received a mapper with the " - "wrong dimension." - ); +PreparedMassNormalizationOperator::PreparedMassNormalizationOperator( + const fem::FEM &f, const mapping::DomainMapper &domainMapper, + const context::gravity_field::GravityFieldLinearizationContext + &gravityContext) + : m_fem(f), m_domainMapper(domainMapper), m_gravityContext(gravityContext) { + MFEM_VERIFY(m_fem.mesh != nullptr, + "PreparedMassNormalizationOperator requires a mesh."); + MFEM_VERIFY(m_fem.densityFes != nullptr && m_fem.displacementFes != nullptr && + m_fem.compactificationFes != nullptr && + m_fem.compactificationCoordinate != nullptr && + m_fem.quadratureFactory != nullptr, + "PreparedMassNormalizationOperator requires density, " + "displacement, compactification, and quadrature data."); + MFEM_VERIFY(m_domainMapper.GetDimension() == m_fem.mesh->Dimension(), + "PreparedMassNormalizationOperator received a mapper with the " + "wrong dimension."); - MFEM_VERIFY( - m_gravityContext.GetDensityMap().full_size() == m_fem.densityFes->GetTrueVSize() && - m_gravityContext.GetDisplacementMap().full_size() == m_fem.displacementFes->GetTrueVSize(), - "PreparedMassNormalizationOperator received incompatible shared FieldDof maps." - ); + MFEM_VERIFY(m_gravityContext.GetDensityMap().full_size() == + m_fem.densityFes->GetTrueVSize() && + m_gravityContext.GetDisplacementMap().full_size() == + m_fem.displacementFes->GetTrueVSize(), + "PreparedMassNormalizationOperator received incompatible shared " + "FieldDof maps."); - m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size()); - m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size()); + m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size()); + m_displacementVariationTrue.SetSize( + m_gravityContext.GetDisplacementMap().full_size()); +} + +PreparedMassNormalizationReport PreparedMassNormalizationOperator::Prepare( + const MassNormalizationStateView &state, + const MassNormalizationDependencies &dependencies) { + MFEM_VERIFY(std::isfinite(state.targetMass) && state.targetMass > 0.0, + "PreparedMassNormalizationOperator requires a finite, positive " + "target mass."); + + validate_shared_gravity_revisions(m_gravityContext, dependencies); + + if (m_isPrepared) { + validate_shared_identity_transition( + m_preparedDependencies.discretization, dependencies.discretization, + "A new mass-normalization discretization identity must also " + "change the shared gravity revision."); + validate_shared_identity_transition( + m_preparedDependencies.density, dependencies.density, + "A new mass-normalization density identity must also change " + "the shared gravity revision."); + validate_shared_identity_transition( + m_preparedDependencies.displacement, dependencies.displacement, + "A new mass-normalization displacement identity must also " + "change the shared gravity revision."); + } + + const bool rebuildStaticPlan = + !m_isPrepared || + dependencies.discretization != m_preparedDependencies.discretization; + + const bool refreshGeometry = + rebuildStaticPlan || + dependencies.displacement != m_preparedDependencies.displacement; + + const bool refreshDensity = + rebuildStaticPlan || + dependencies.density != m_preparedDependencies.density; + + const bool updateTargetMass = + !m_isPrepared || + dependencies.targetMass != m_preparedDependencies.targetMass || + state.targetMass != m_targetMass; + + m_isPrepared = false; + + PreparedMassNormalizationReport report; + + if (rebuildStaticPlan) { + BuildStaticPlan(); + report.rebuiltStaticPlan = true; + } + + if (refreshGeometry) { + RefreshGeometry( + m_gravityContext.GetGeometryContext().GetDisplacementTrue()); + report.refreshedGeometry = true; + } + + if (refreshDensity) { + RefreshDensity(m_gravityContext.GetDensityTrue()); + report.refreshedDensity = true; + } + + if (updateTargetMass) { + m_targetMass = state.targetMass; + report.updatedTargetMass = true; + } + + if (refreshGeometry || refreshDensity) { + AssembleResidual(); + report.assembledResidual = true; + } else if (updateTargetMass) { + m_cachedResidual.SetSize(1); + m_cachedResidual(0) = m_currentMass - m_targetMass; + ++m_preparationCount; + report.assembledResidual = true; + } + + m_preparedDependencies = dependencies; + m_isPrepared = true; + return report; +} + +void PreparedMassNormalizationOperator::BuildStaticPlan() { + m_elements.clear(); + m_elements.reserve(m_fem.mesh->GetNE()); + + int localStellarElementCount = 0; + + for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + m_fem.mesh->GetElementTransformation(elementId); + + MFEM_VERIFY(transformation != nullptr, + "PreparedMassNormalizationOperator received a null element " + "transformation."); + + if (is_vacuum_attribute(transformation->Attribute)) { + continue; } - PreparedMassNormalizationReport PreparedMassNormalizationOperator::Prepare( - const MassNormalizationStateView &state, - const MassNormalizationDependencies &dependencies - ) { - MFEM_VERIFY( - std::isfinite(state.targetMass) && state.targetMass > 0.0, - "PreparedMassNormalizationOperator requires a finite, positive " - "target mass." - ); + ++localStellarElementCount; + m_elements.emplace_back(); + ElementPAData &data = m_elements.back(); + data.elementId = elementId; - validate_shared_gravity_revisions(m_gravityContext, dependencies); + data.densityDofTransformation = + m_fem.densityFes->GetElementDofs(elementId, data.densityDofs); - if (m_isPrepared) { - validate_shared_identity_transition( - m_preparedDependencies.discretization, dependencies.discretization, - "A new mass-normalization discretization identity must also " - "change the shared gravity revision." - ); - validate_shared_identity_transition( - m_preparedDependencies.density, dependencies.density, - "A new mass-normalization density identity must also change " - "the shared gravity revision." - ); - validate_shared_identity_transition( - m_preparedDependencies.displacement, dependencies.displacement, - "A new mass-normalization displacement identity must also " - "change the shared gravity revision." - ); - } + data.displacementDofTransformation = m_fem.displacementFes->GetElementVDofs( + elementId, data.displacementDofs); - const bool rebuildStaticPlan = - !m_isPrepared || dependencies.discretization != m_preparedDependencies.discretization; + data.compactificationDofTransformation = + m_fem.compactificationFes->GetElementDofs(elementId, + data.compactificationDofs); - const bool refreshGeometry = - rebuildStaticPlan || dependencies.displacement != m_preparedDependencies.displacement; + const mfem::FiniteElement &densityElement = + *m_fem.densityFes->GetFE(elementId); - const bool refreshDensity = rebuildStaticPlan || dependencies.density != m_preparedDependencies.density; + const mfem::IntegrationRule &integrationRule = + get_mass_normalization_rule(m_fem, densityElement, *transformation); - const bool updateTargetMass = !m_isPrepared || dependencies.targetMass != m_preparedDependencies.targetMass || - state.targetMass != m_targetMass; + data.quadraturePoints.resize(integrationRule.GetNPoints()); - m_isPrepared = false; + for (int quadraturePoint = 0; + quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) { + QuadraturePointData &point = data.quadraturePoints[quadraturePoint]; - PreparedMassNormalizationReport report; + point.integrationPoint = integrationRule.IntPoint(quadraturePoint); - if (rebuildStaticPlan) { - BuildStaticPlan(); - report.rebuiltStaticPlan = true; - } + point.densityShape.SetSize(densityElement.GetDof()); + densityElement.CalcShape(point.integrationPoint, point.densityShape); + } + } - if (refreshGeometry) { - RefreshGeometry(m_gravityContext.GetGeometryContext().GetDisplacementTrue()); - report.refreshedGeometry = true; - } + int globalStellarElementCount = 0; + MPI_Allreduce(&localStellarElementCount, &globalStellarElementCount, 1, + MPI_INT, MPI_SUM, m_fem.mesh->GetComm()); - if (refreshDensity) { - RefreshDensity(m_gravityContext.GetDensityTrue()); - report.refreshedDensity = true; - } + MFEM_VERIFY(globalStellarElementCount > 0, + "PreparedMassNormalizationOperator found no stellar elements."); +} - if (updateTargetMass) { - m_targetMass = state.targetMass; - report.updatedTargetMass = true; - } +void PreparedMassNormalizationOperator::RefreshGeometry( + const mfem::Vector &displacement) { + MFEM_VERIFY(displacement.Size() == m_fem.displacementFes->GetTrueVSize(), + "PreparedMassNormalizationOperator received a displacement " + "vector with the wrong size."); + validate_finite_vector( + displacement, "PreparedMassNormalizationOperator received a non-finite " + "displacement value."); - if (refreshGeometry || refreshDensity) { - AssembleResidual(); - report.assembledResidual = true; - } else if (updateTargetMass) { - m_cachedResidual.SetSize(1); - m_cachedResidual(0) = m_currentMass - m_targetMass; - ++m_preparationCount; - report.assembledResidual = true; - } + mfem::Vector displacementLocal; + true_to_local(*m_fem.displacementFes, displacement, displacementLocal); - m_preparedDependencies = dependencies; - m_isPrepared = true; - return report; + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); + + for (ElementPAData &data : m_elements) { + displacementLocal.GetSubVector(data.displacementDofs, + data.baseDisplacement); + + m_fem.compactificationCoordinate->GetSubVector(data.compactificationDofs, + data.compactification); + + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal( + data.baseDisplacement); } - void PreparedMassNormalizationOperator::BuildStaticPlan() { - m_elements.clear(); - m_elements.reserve(m_fem.mesh->GetNE()); - - const int vacuumAttribute = m_domainMapper.GetVacuumElementAttribute(); - - int localStellarElementCount = 0; - - for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId); - - MFEM_VERIFY( - transformation != nullptr, "PreparedMassNormalizationOperator received a null element " - "transformation." - ); - - if (transformation->Attribute == vacuumAttribute) { - continue; - } - - ++localStellarElementCount; - m_elements.emplace_back(); - ElementPAData &data = m_elements.back(); - data.elementId = elementId; - - data.densityDofTransformation = m_fem.densityFes->GetElementDofs(elementId, data.densityDofs); - - data.displacementDofTransformation = - m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs); - - data.compactificationDofTransformation = - m_fem.compactificationFes->GetElementDofs(elementId, data.compactificationDofs); - - const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(elementId); - - const mfem::IntegrationRule &integrationRule = - get_mass_normalization_rule(m_fem, densityElement, *transformation); - - data.quadraturePoints.resize(integrationRule.GetNPoints()); - - for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) { - QuadraturePointData &point = data.quadraturePoints[quadraturePoint]; - - point.integrationPoint = integrationRule.IntPoint(quadraturePoint); - - point.densityShape.SetSize(densityElement.GetDof()); - densityElement.CalcShape(point.integrationPoint, point.densityShape); - } - } - - int globalStellarElementCount = 0; - MPI_Allreduce( - &localStellarElementCount, &globalStellarElementCount, 1, MPI_INT, MPI_SUM, m_fem.mesh->GetComm() - ); - - MFEM_VERIFY(globalStellarElementCount > 0, "PreparedMassNormalizationOperator found no stellar elements."); + if (data.compactificationDofTransformation != nullptr) { + data.compactificationDofTransformation->InvTransformPrimal( + data.compactification); } - void PreparedMassNormalizationOperator::RefreshGeometry(const mfem::Vector &displacement) { - MFEM_VERIFY( - displacement.Size() == m_fem.displacementFes->GetTrueVSize(), - "PreparedMassNormalizationOperator received a displacement " - "vector with the wrong size." - ); - validate_finite_vector( - displacement, "PreparedMassNormalizationOperator received a non-finite " - "displacement value." - ); + const mfem::FiniteElement &displacementElement = + *m_fem.displacementFes->GetFE(data.elementId); - mfem::Vector displacementLocal; - true_to_local(*m_fem.displacementFes, displacement, displacementLocal); + const mfem::FiniteElement &compactificationElement = + *m_fem.compactificationFes->GetFE(data.elementId); - mapping::DomainMapperStateless::Workspace workspace(m_fem.mesh->Dimension()); + const mapping::ElementDisplacementData displacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, + data.baseDisplacement); - for (ElementPAData &data : m_elements) { - displacementLocal.GetSubVector(data.displacementDofs, data.baseDisplacement); + const mapping::ElementCompactificationData compactificationData( + compactificationElement, data.compactification); - m_fem.compactificationCoordinate->GetSubVector(data.compactificationDofs, data.compactification); + const mapping::ElementMappingData mappingData{ + .displacement = displacementData, + .compactification = compactificationData}; - if (data.displacementDofTransformation != nullptr) { - data.displacementDofTransformation->InvTransformPrimal(data.baseDisplacement); - } + mfem::ElementTransformation *transformation = + m_fem.mesh->GetElementTransformation(data.elementId); - if (data.compactificationDofTransformation != nullptr) { - data.compactificationDofTransformation->InvTransformPrimal(data.compactification); - } + for (QuadraturePointData &point : data.quadraturePoints) { + const mapping::MappingStatus status = m_domainMapper.EvaluateVolume( + mappingData, *transformation, point.integrationPoint, workspace, + point.mappingContext); - const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); + MFEM_VERIFY(status == mapping::MappingStatus::valid, + "Stateless mapping failed while preparing mass " + "normalization. Element: " + << data.elementId + << ", attribute: " << transformation->Attribute + << ", status: " << static_cast(status)); + } + } +} - const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); +void PreparedMassNormalizationOperator::RefreshDensity( + const mfem::Vector &density) { + MFEM_VERIFY(density.Size() == m_fem.densityFes->GetTrueVSize(), + "PreparedMassNormalizationOperator received a density vector " + "with the wrong size."); + validate_finite_vector( + density, + "PreparedMassNormalizationOperator received a non-finite density " + "value."); - const mapping::ElementDisplacementData displacementData = - mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement); + mfem::Vector densityLocal; + true_to_local(*m_fem.densityFes, density, densityLocal); - const mapping::ElementCompactificationData compactificationData( - compactificationElement, data.compactification - ); + mfem::Vector elementDensity; - const mapping::ElementMappingData mappingData{ - .displacement = displacementData, .compactification = compactificationData - }; + for (ElementPAData &data : m_elements) { + densityLocal.GetSubVector(data.densityDofs, elementDensity); - mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); - - for (QuadraturePointData &point : data.quadraturePoints) { - const mapping::MappingStatus status = m_domainMapper.EvaluateVolume( - mappingData, *transformation, point.integrationPoint, workspace, point.mappingContext - ); - - MFEM_VERIFY( - status == mapping::MappingStatus::valid, "Stateless mapping failed while preparing mass " - "normalization. Element: " - << data.elementId - << ", attribute: " << transformation->Attribute - << ", status: " << static_cast(status) - ); - } - } + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal(elementDensity); } - void PreparedMassNormalizationOperator::RefreshDensity(const mfem::Vector &density) { - MFEM_VERIFY( - density.Size() == m_fem.densityFes->GetTrueVSize(), - "PreparedMassNormalizationOperator received a density vector " - "with the wrong size." - ); - validate_finite_vector( - density, "PreparedMassNormalizationOperator received a non-finite density " - "value." - ); + for (QuadraturePointData &point : data.quadraturePoints) { + point.density = elementDensity * point.densityShape; + MFEM_VERIFY(std::isfinite(point.density), + "PreparedMassNormalizationOperator produced a non-finite " + "quadrature density."); + } + } +} - mfem::Vector densityLocal; - true_to_local(*m_fem.densityFes, density, densityLocal); +void PreparedMassNormalizationOperator::AssembleResidual() { + double localMass = 0.0; - mfem::Vector elementDensity; + for (const ElementPAData &data : m_elements) { + for (const QuadraturePointData &point : data.quadraturePoints) { + localMass += point.density * point.mappingContext.quadrature.weight; + } + } - for (ElementPAData &data : m_elements) { - densityLocal.GetSubVector(data.densityDofs, elementDensity); + m_currentMass = GlobalSum(localMass); + MFEM_VERIFY(std::isfinite(m_currentMass), + "PreparedMassNormalizationOperator assembled a non-finite mass."); - if (data.densityDofTransformation != nullptr) { - data.densityDofTransformation->InvTransformPrimal(elementDensity); - } + m_cachedResidual.SetSize(1); + m_cachedResidual(0) = m_currentMass - m_targetMass; + ++m_preparationCount; +} - for (QuadraturePointData &point : data.quadraturePoints) { - point.density = elementDensity * point.densityShape; - MFEM_VERIFY( - std::isfinite(point.density), "PreparedMassNormalizationOperator produced a non-finite " - "quadrature density." - ); - } - } +void PreparedMassNormalizationOperator::BuildResidual( + mfem::Vector &residual) const { + VerifyPrepared(); + residual = m_cachedResidual; + ++m_residualApplicationCount; +} + +double PreparedMassNormalizationOperator::EvaluateDensityActionLocal( + const mfem::Vector &densityVariation) const { + MFEM_VERIFY(densityVariation.Size() == m_fem.densityFes->GetTrueVSize(), + "Mass-normalization density action received a vector with the " + "wrong size."); + validate_finite_vector( + densityVariation, + "Mass-normalization density action received a non-finite value."); + + mfem::Vector densityVariationLocal; + true_to_local(*m_fem.densityFes, densityVariation, densityVariationLocal); + + mfem::Vector elementDensityVariation; + double localAction = 0.0; + + for (const ElementPAData &data : m_elements) { + densityVariationLocal.GetSubVector(data.densityDofs, + elementDensityVariation); + + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal( + elementDensityVariation); } - void PreparedMassNormalizationOperator::AssembleResidual() { - double localMass = 0.0; + for (const QuadraturePointData &point : data.quadraturePoints) { + localAction += (elementDensityVariation * point.densityShape) * + point.mappingContext.quadrature.weight; + } + } - for (const ElementPAData &data : m_elements) { - for (const QuadraturePointData &point : data.quadraturePoints) { - localMass += point.density * point.mappingContext.quadrature.weight; - } - } + return localAction; +} - m_currentMass = GlobalSum(localMass); - MFEM_VERIFY(std::isfinite(m_currentMass), "PreparedMassNormalizationOperator assembled a non-finite mass."); +double PreparedMassNormalizationOperator::EvaluateDisplacementActionLocal( + const mfem::Vector &displacementVariation) const { + MFEM_VERIFY(displacementVariation.Size() == + m_fem.displacementFes->GetTrueVSize(), + "Mass-normalization displacement action received a vector with " + "the wrong size."); + validate_finite_vector( + displacementVariation, + "Mass-normalization displacement action received a non-finite " + "value."); - m_cachedResidual.SetSize(1); - m_cachedResidual(0) = m_currentMass - m_targetMass; - ++m_preparationCount; + mfem::Vector displacementVariationLocal; + true_to_local(*m_fem.displacementFes, displacementVariation, + displacementVariationLocal); + + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); + + mfem::Vector elementDisplacementVariation; + double localAction = 0.0; + + for (const ElementPAData &data : m_elements) { + displacementVariationLocal.GetSubVector(data.displacementDofs, + elementDisplacementVariation); + + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal( + elementDisplacementVariation); } - void PreparedMassNormalizationOperator::BuildResidual(mfem::Vector &residual) const { - VerifyPrepared(); - residual = m_cachedResidual; - ++m_residualApplicationCount; + const mfem::FiniteElement &displacementElement = + *m_fem.displacementFes->GetFE(data.elementId); + + const mfem::FiniteElement &compactificationElement = + *m_fem.compactificationFes->GetFE(data.elementId); + + const mapping::ElementDisplacementData baseDisplacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, + data.baseDisplacement); + + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacementVariation); + + const mapping::ElementCompactificationData compactificationData( + compactificationElement, data.compactification); + + const mapping::ElementMappingData mappingData{ + .displacement = baseDisplacementData, + .compactification = compactificationData}; + + mfem::ElementTransformation *transformation = + m_fem.mesh->GetElementTransformation(data.elementId); + + for (const QuadraturePointData &point : data.quadraturePoints) { + mapping::VolumeMappingVariation variation; + + const mapping::MappingStatus status = + m_domainMapper.EvaluateVolumeVariation( + mappingData, directionData, *transformation, + point.integrationPoint, point.mappingContext, workspace, + variation); + + MFEM_VERIFY(status == mapping::MappingStatus::valid, + "Stateless mapping variation failed in the " + "mass-normalization displacement action. Element: " + << data.elementId + << ", status: " << static_cast(status)); + + localAction += point.density * variation.weight_variation; } + } - double PreparedMassNormalizationOperator::EvaluateDensityActionLocal(const mfem::Vector &densityVariation) const { - MFEM_VERIFY( - densityVariation.Size() == m_fem.densityFes->GetTrueVSize(), - "Mass-normalization density action received a vector with the " - "wrong size." - ); - validate_finite_vector(densityVariation, "Mass-normalization density action received a non-finite value."); + return localAction; +} - mfem::Vector densityVariationLocal; - true_to_local(*m_fem.densityFes, densityVariation, densityVariationLocal); +void PreparedMassNormalizationOperator::ApplyDensityJacobianAction( + const mfem::Vector &densityVariation, mfem::Vector &action) const { + VerifyPrepared(); - mfem::Vector elementDensityVariation; - double localAction = 0.0; + MFEM_VERIFY(densityVariation.Size() == + m_gravityContext.GetDensityMap().reduced_size(), + "Mass-normalization density action received a supported vector " + "with the wrong size."); + validate_finite_vector( + densityVariation, + "Mass-normalization density action received a non-finite value."); + m_gravityContext.GetDensityMap().scatter(densityVariation, + m_densityVariationTrue); - for (const ElementPAData &data : m_elements) { - densityVariationLocal.GetSubVector(data.densityDofs, elementDensityVariation); + action.SetSize(1); + action(0) = GlobalSum(EvaluateDensityActionLocal(m_densityVariationTrue)); + ++m_actionStatistics.densityApplications; +} - if (data.densityDofTransformation != nullptr) { - data.densityDofTransformation->InvTransformPrimal(elementDensityVariation); - } +void PreparedMassNormalizationOperator::ApplyDisplacementJacobianAction( + const mfem::Vector &displacementVariation, mfem::Vector &action) const { + VerifyPrepared(); - for (const QuadraturePointData &point : data.quadraturePoints) { - localAction += (elementDensityVariation * point.densityShape) * point.mappingContext.quadrature.weight; - } - } + MFEM_VERIFY(displacementVariation.Size() == + m_gravityContext.GetDisplacementMap().reduced_size(), + "Mass-normalization displacement action received a supported " + "vector with the wrong size."); + validate_finite_vector( + displacementVariation, + "Mass-normalization displacement action received a non-finite value."); + m_gravityContext.GetDisplacementMap().scatter(displacementVariation, + m_displacementVariationTrue); - return localAction; - } + action.SetSize(1); + action(0) = + GlobalSum(EvaluateDisplacementActionLocal(m_displacementVariationTrue)); + ++m_actionStatistics.displacementApplications; +} - double PreparedMassNormalizationOperator::EvaluateDisplacementActionLocal( - const mfem::Vector &displacementVariation - ) const { - MFEM_VERIFY( - displacementVariation.Size() == m_fem.displacementFes->GetTrueVSize(), - "Mass-normalization displacement action received a vector with " - "the wrong size." - ); - validate_finite_vector( - displacementVariation, "Mass-normalization displacement action received a non-finite " - "value." - ); +void PreparedMassNormalizationOperator::ApplyCompleteJacobianAction( + const mfem::Vector &densityVariation, + const mfem::Vector &displacementVariation, mfem::Vector &action) const { + VerifyPrepared(); - mfem::Vector displacementVariationLocal; - true_to_local(*m_fem.displacementFes, displacementVariation, displacementVariationLocal); + MFEM_VERIFY(densityVariation.Size() == + m_gravityContext.GetDensityMap().reduced_size(), + "Mass-normalization complete action received a supported density " + "vector with the wrong size."); + MFEM_VERIFY(displacementVariation.Size() == + m_gravityContext.GetDisplacementMap().reduced_size(), + "Mass-normalization complete action received a supported " + "displacement vector with the wrong size."); + validate_finite_vector( + densityVariation, + "Mass-normalization complete action received a non-finite density."); + validate_finite_vector( + displacementVariation, + "Mass-normalization complete action received a non-finite displacement."); - mapping::DomainMapperStateless::Workspace workspace(m_fem.mesh->Dimension()); + m_gravityContext.GetDensityMap().scatter(densityVariation, + m_densityVariationTrue); + m_gravityContext.GetDisplacementMap().scatter(displacementVariation, + m_displacementVariationTrue); - mfem::Vector elementDisplacementVariation; - double localAction = 0.0; + const double localAction = + EvaluateDensityActionLocal(m_densityVariationTrue) + + EvaluateDisplacementActionLocal(m_displacementVariationTrue); - for (const ElementPAData &data : m_elements) { - displacementVariationLocal.GetSubVector(data.displacementDofs, elementDisplacementVariation); + action.SetSize(1); + action(0) = GlobalSum(localAction); + ++m_actionStatistics.completeApplications; +} - if (data.displacementDofTransformation != nullptr) { - data.displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation); - } +double +PreparedMassNormalizationOperator::GlobalSum(const double localValue) const { + double globalValue = 0.0; + MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, + m_fem.mesh->GetComm()); + return globalValue; +} - const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); +bool PreparedMassNormalizationOperator::IsPrepared() const noexcept { + if (!m_isPrepared || !m_gravityContext.IsPrepared()) { + return false; + } - const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); + const auto &revisions = m_gravityContext.GetRevisions(); + return revisions.discretization.value == + m_preparedDependencies.discretization.revision && + revisions.density.value == m_preparedDependencies.density.revision && + revisions.displacement.value == + m_preparedDependencies.displacement.revision; +} - const mapping::ElementDisplacementData baseDisplacementData = - mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement); +double PreparedMassNormalizationOperator::GetCurrentMass() const { + VerifyPrepared(); + return m_currentMass; +} - const mapping::ElementDisplacementData directionData = - mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacementVariation); +double PreparedMassNormalizationOperator::GetTargetMass() const { + VerifyPrepared(); + return m_targetMass; +} - const mapping::ElementCompactificationData compactificationData( - compactificationElement, data.compactification - ); +std::uint64_t +PreparedMassNormalizationOperator::GetPreparationCount() const noexcept { + return m_preparationCount; +} - const mapping::ElementMappingData mappingData{ - .displacement = baseDisplacementData, .compactification = compactificationData - }; +std::uint64_t PreparedMassNormalizationOperator::GetResidualApplicationCount() + const noexcept { + return m_residualApplicationCount; +} - mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); +const PreparedMassNormalizationActionStatistics & +PreparedMassNormalizationOperator::GetActionStatistics() const noexcept { + return m_actionStatistics; +} - for (const QuadraturePointData &point : data.quadraturePoints) { - mapping::VolumeMappingVariation variation; +const fem::FEM &PreparedMassNormalizationOperator::GetFEM() const noexcept { + return m_fem; +} - const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation( - mappingData, directionData, *transformation, point.integrationPoint, point.mappingContext, - workspace, variation - ); +const context::gravity_field::GravityFieldLinearizationContext & +PreparedMassNormalizationOperator::GetGravityContext() const noexcept { + return m_gravityContext; +} - MFEM_VERIFY( - status == mapping::MappingStatus::valid, "Stateless mapping variation failed in the " - "mass-normalization displacement action. Element: " - << data.elementId - << ", status: " << static_cast(status) - ); +void PreparedMassNormalizationOperator::VerifyPrepared() const { + MFEM_VERIFY(IsPrepared(), + "PreparedMassNormalizationOperator must be prepared for the " + "current shared gravity-context revisions."); +} - localAction += point.density * variation.weight_variation; - } - } - - return localAction; - } - - void PreparedMassNormalizationOperator::ApplyDensityJacobianAction( - const mfem::Vector &densityVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); - - MFEM_VERIFY( - densityVariation.Size() == m_gravityContext.GetDensityMap().reduced_size(), - "Mass-normalization density action received a supported vector with the wrong size." - ); - validate_finite_vector(densityVariation, "Mass-normalization density action received a non-finite value."); - m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue); - - action.SetSize(1); - action(0) = GlobalSum(EvaluateDensityActionLocal(m_densityVariationTrue)); - ++m_actionStatistics.densityApplications; - } - - void PreparedMassNormalizationOperator::ApplyDisplacementJacobianAction( - const mfem::Vector &displacementVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); - - MFEM_VERIFY( - displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(), - "Mass-normalization displacement action received a supported vector with the wrong size." - ); - validate_finite_vector( - displacementVariation, "Mass-normalization displacement action received a non-finite value." - ); - m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue); - - action.SetSize(1); - action(0) = GlobalSum(EvaluateDisplacementActionLocal(m_displacementVariationTrue)); - ++m_actionStatistics.displacementApplications; - } - - void PreparedMassNormalizationOperator::ApplyCompleteJacobianAction( - const mfem::Vector &densityVariation, - const mfem::Vector &displacementVariation, - mfem::Vector &action - ) const { - VerifyPrepared(); - - MFEM_VERIFY( - densityVariation.Size() == m_gravityContext.GetDensityMap().reduced_size(), - "Mass-normalization complete action received a supported density vector with the wrong size." - ); - MFEM_VERIFY( - displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(), - "Mass-normalization complete action received a supported displacement vector with the wrong size." - ); - validate_finite_vector(densityVariation, "Mass-normalization complete action received a non-finite density."); - validate_finite_vector( - displacementVariation, "Mass-normalization complete action received a non-finite displacement." - ); - - m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue); - m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue); - - const double localAction = - EvaluateDensityActionLocal(m_densityVariationTrue) + - EvaluateDisplacementActionLocal(m_displacementVariationTrue); - - action.SetSize(1); - action(0) = GlobalSum(localAction); - ++m_actionStatistics.completeApplications; - } - - double PreparedMassNormalizationOperator::GlobalSum(const double localValue) const { - double globalValue = 0.0; - MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_fem.mesh->GetComm()); - return globalValue; - } - - bool PreparedMassNormalizationOperator::IsPrepared() const noexcept { - if (!m_isPrepared || !m_gravityContext.IsPrepared()) { - return false; - } - - const auto &revisions = m_gravityContext.GetRevisions(); - return revisions.discretization.value == m_preparedDependencies.discretization.revision && - revisions.density.value == m_preparedDependencies.density.revision && - revisions.displacement.value == m_preparedDependencies.displacement.revision; - } - - double PreparedMassNormalizationOperator::GetCurrentMass() const { - VerifyPrepared(); - return m_currentMass; - } - - double PreparedMassNormalizationOperator::GetTargetMass() const { - VerifyPrepared(); - return m_targetMass; - } - - std::uint64_t PreparedMassNormalizationOperator::GetPreparationCount() const noexcept { - return m_preparationCount; - } - - std::uint64_t PreparedMassNormalizationOperator::GetResidualApplicationCount() const noexcept { - return m_residualApplicationCount; - } - - const PreparedMassNormalizationActionStatistics & - PreparedMassNormalizationOperator::GetActionStatistics() const noexcept { - return m_actionStatistics; - } - - const fem::FEM &PreparedMassNormalizationOperator::GetFEM() const noexcept { - return m_fem; - } - - const context::gravity_field::GravityFieldLinearizationContext & - PreparedMassNormalizationOperator::GetGravityContext() const noexcept { - return m_gravityContext; - } - - void PreparedMassNormalizationOperator::VerifyPrepared() const { - MFEM_VERIFY( - IsPrepared(), "PreparedMassNormalizationOperator must be prepared for the " - "current shared gravity-context revisions." - ); - } - - PreparedMassNormalizationJacobianOperator::PreparedMassNormalizationJacobianOperator( +PreparedMassNormalizationJacobianOperator:: + PreparedMassNormalizationJacobianOperator( const MassNormalizationLayout &layout, - const PreparedMassNormalizationOperator &preparedOperator - ) - : mfem::Operator( - layout.residual_offsets().Last(), - layout.value_offsets().Last() - ), - m_layout(layout), - m_preparedOperator(preparedOperator) { - const fem::FEM &f = m_preparedOperator.GetFEM(); + const PreparedMassNormalizationOperator &preparedOperator) + : mfem::Operator(layout.residual_offsets().Last(), + layout.value_offsets().Last()), + m_layout(layout), m_preparedOperator(preparedOperator) { + const fem::FEM &f = m_preparedOperator.GetFEM(); - MFEM_VERIFY( - f.densityFes != nullptr && f.displacementFes != nullptr && f.gravityFluxFes != nullptr && - f.gravityPotentialFes != nullptr && f.enthalpyFes != nullptr, - "Prepared mass-normalization MFEM adapter requires every " - "finite-element space in the barotropic equilibrium layout." - ); + MFEM_VERIFY(f.densityFes != nullptr && f.displacementFes != nullptr && + f.gravityFluxFes != nullptr && + f.gravityPotentialFes != nullptr && f.enthalpyFes != nullptr, + "Prepared mass-normalization MFEM adapter requires every " + "finite-element space in the barotropic equilibrium layout."); - using Form = utils::blocks::barotropic_equilibrium_form; + using Form = utils::blocks::barotropic_equilibrium_form; - constexpr auto densityValue = utils::blocks::get_value_block(utils::blocks::density_field.mass_term); - constexpr auto displacementValue = - utils::blocks::get_value_block(utils::blocks::displacement_field.geometry_term); - constexpr auto gravityGradientValue = - utils::blocks::get_value_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravityPotentialValue = - utils::blocks::get_value_block(utils::blocks::gravity_field.poisson_term); - constexpr auto enthalpyValue = - utils::blocks::get_value_block(utils::blocks::enthalpy_field.specific_term); - constexpr auto barotropicConstantValue = - utils::blocks::get_value_block(utils::blocks::barotropic_constant_field.mass_normalization_term); - constexpr auto gravityGradientResidual = - utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravityPotentialResidual = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - constexpr auto densityResidual = - utils::blocks::get_residual_block(utils::blocks::density_field.mass_term); - constexpr auto displacementResidual = - utils::blocks::get_residual_block(utils::blocks::displacement_field.geometry_term); - constexpr auto enthalpyResidual = - utils::blocks::get_residual_block(utils::blocks::enthalpy_field.specific_term); - constexpr auto massResidual = - utils::blocks::get_residual_block(utils::blocks::barotropic_constant_field.mass_normalization_term); + constexpr auto densityValue = utils::blocks::get_value_block( + utils::blocks::density_field.mass_term); + constexpr auto displacementValue = utils::blocks::get_value_block( + utils::blocks::displacement_field.geometry_term); + constexpr auto gravityGradientValue = utils::blocks::get_value_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravityPotentialValue = utils::blocks::get_value_block( + utils::blocks::gravity_field.poisson_term); + constexpr auto enthalpyValue = utils::blocks::get_value_block( + utils::blocks::enthalpy_field.specific_term); + constexpr auto barotropicConstantValue = utils::blocks::get_value_block( + utils::blocks::barotropic_constant_field.mass_normalization_term); + constexpr auto gravityGradientResidual = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.gradient_term); + constexpr auto gravityPotentialResidual = + utils::blocks::get_residual_block( + utils::blocks::gravity_field.poisson_term); + constexpr auto densityResidual = utils::blocks::get_residual_block( + utils::blocks::density_field.mass_term); + constexpr auto displacementResidual = utils::blocks::get_residual_block( + utils::blocks::displacement_field.geometry_term); + constexpr auto enthalpyResidual = utils::blocks::get_residual_block( + utils::blocks::enthalpy_field.specific_term); + constexpr auto massResidual = utils::blocks::get_residual_block( + utils::blocks::barotropic_constant_field.mass_normalization_term); - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - const auto &gravityContext = m_preparedOperator.GetGravityContext(); + const auto &gravityContext = m_preparedOperator.GetGravityContext(); - const field::FieldDofMap enthalpyMap = - field::make_field_dof_map(*f.enthalpyFes); + const field::FieldDofMap enthalpyMap = + field::make_field_dof_map(*f.enthalpyFes); - MFEM_VERIFY( - m_layout.size(densityValue) == gravityContext.GetDensityMap().reduced_size() && - m_layout.size(displacementValue) == gravityContext.GetDisplacementMap().reduced_size() && - m_layout.size(gravityGradientValue) == gravityContext.GetGravityGradientMap().reduced_size() && - m_layout.size(gravityPotentialValue) == gravityContext.GetGravityPotentialMap().reduced_size() && - m_layout.size(enthalpyValue) == enthalpyMap.reduced_size() && - m_layout.size(barotropicConstantValue) == 1 && - m_layout.size(gravityGradientResidual) == gravityContext.GetGravityGradientMap().reduced_size() && - m_layout.size(gravityPotentialResidual) == gravityContext.GetGravityPotentialMap().reduced_size() && - m_layout.size(densityResidual) == gravityContext.GetDensityMap().reduced_size() && - m_layout.size(displacementResidual) == gravityContext.GetDisplacementMap().reduced_size() && - m_layout.size(enthalpyResidual) == enthalpyMap.reduced_size() && m_layout.size(massResidual) == 1, - "Prepared mass-normalization MFEM adapter received incompatible " - "barotropic block sizes." - ); - } + MFEM_VERIFY(m_layout.size(densityValue) == + gravityContext.GetDensityMap().reduced_size() && + m_layout.size(displacementValue) == + gravityContext.GetDisplacementMap().reduced_size() && + m_layout.size(gravityGradientValue) == + gravityContext.GetGravityGradientMap().reduced_size() && + m_layout.size(gravityPotentialValue) == + gravityContext.GetGravityPotentialMap().reduced_size() && + m_layout.size(enthalpyValue) == enthalpyMap.reduced_size() && + m_layout.size(barotropicConstantValue) == 1 && + m_layout.size(gravityGradientResidual) == + gravityContext.GetGravityGradientMap().reduced_size() && + m_layout.size(gravityPotentialResidual) == + gravityContext.GetGravityPotentialMap().reduced_size() && + m_layout.size(densityResidual) == + gravityContext.GetDensityMap().reduced_size() && + m_layout.size(displacementResidual) == + gravityContext.GetDisplacementMap().reduced_size() && + m_layout.size(enthalpyResidual) == + enthalpyMap.reduced_size() && + m_layout.size(massResidual) == 1, + "Prepared mass-normalization MFEM adapter received incompatible " + "barotropic block sizes."); +} - void PreparedMassNormalizationJacobianOperator::Mult( - const mfem::Vector &direction, - mfem::Vector &action - ) const { - MFEM_VERIFY( - m_preparedOperator.IsPrepared(), "Prepared mass-normalization MFEM adapter requires a prepared " - "row operator." - ); - MFEM_VERIFY( - direction.Size() == Width(), "Prepared mass-normalization MFEM adapter received a direction " - "with the wrong size." - ); +void PreparedMassNormalizationJacobianOperator::Mult( + const mfem::Vector &direction, mfem::Vector &action) const { + MFEM_VERIFY(m_preparedOperator.IsPrepared(), + "Prepared mass-normalization MFEM adapter requires a prepared " + "row operator."); + MFEM_VERIFY(direction.Size() == Width(), + "Prepared mass-normalization MFEM adapter received a direction " + "with the wrong size."); - using Form = utils::blocks::barotropic_equilibrium_form; + using Form = utils::blocks::barotropic_equilibrium_form; - constexpr auto densityValue = utils::blocks::get_value_block(utils::blocks::density_field.mass_term); - constexpr auto displacementValue = - utils::blocks::get_value_block(utils::blocks::displacement_field.geometry_term); - constexpr auto massResidual = - utils::blocks::get_residual_block(utils::blocks::barotropic_constant_field.mass_normalization_term); + constexpr auto densityValue = utils::blocks::get_value_block( + utils::blocks::density_field.mass_term); + constexpr auto displacementValue = utils::blocks::get_value_block( + utils::blocks::displacement_field.geometry_term); + constexpr auto massResidual = utils::blocks::get_residual_block( + utils::blocks::barotropic_constant_field.mass_normalization_term); - const mfem::Vector densityVariation( - const_cast(direction.GetData()) + m_layout.offset(densityValue), m_layout.size(densityValue) - ); + const mfem::Vector densityVariation( + const_cast(direction.GetData()) + + m_layout.offset(densityValue), + m_layout.size(densityValue)); - const mfem::Vector displacementVariation( - const_cast(direction.GetData()) + m_layout.offset(displacementValue), - m_layout.size(displacementValue) - ); + const mfem::Vector displacementVariation( + const_cast(direction.GetData()) + + m_layout.offset(displacementValue), + m_layout.size(displacementValue)); - mfem::Vector massAction; - m_preparedOperator.ApplyCompleteJacobianAction(densityVariation, displacementVariation, massAction); + mfem::Vector massAction; + m_preparedOperator.ApplyCompleteJacobianAction( + densityVariation, displacementVariation, massAction); - action.SetSize(Height()); - action = 0.0; - action(m_layout.offset(massResidual)) = massAction(0); - } + action.SetSize(Height()); + action = 0.0; + action(m_layout.offset(massResidual)) = massAction(0); +} - const MassNormalizationLayout &PreparedMassNormalizationJacobianOperator::GetLayout() const noexcept { - return m_layout; - } +const MassNormalizationLayout & +PreparedMassNormalizationJacobianOperator::GetLayout() const noexcept { + return m_layout; +} } // namespace mean_field::operators diff --git a/libmeanfield/impl/operators/prepared_pressure_force.cpp b/libmeanfield/impl/operators/prepared_pressure_force.cpp index 830de8c..32e930b 100644 --- a/libmeanfield/impl/operators/prepared_pressure_force.cpp +++ b/libmeanfield/impl/operators/prepared_pressure_force.cpp @@ -213,7 +213,7 @@ namespace mean_field::operators { PreparedPressureForceOperator::PreparedPressureForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState ) : PreparedPressureForceOperator( @@ -226,7 +226,7 @@ namespace mean_field::operators { PreparedPressureForceOperator::PreparedPressureForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, ConstructionData constructionData ) @@ -460,7 +460,7 @@ namespace mean_field::operators { true_to_local(*m_fem.displacementFes, m_baseDisplacementTrue, displacementLocal); - mapping::DomainMapperStateless::Workspace workspace(m_fem.mesh->Dimension()); + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); mfem::Vector elementDisplacement; mfem::Vector elementCompactification; @@ -807,7 +807,7 @@ namespace mean_field::operators { localAction = 0.0; - mapping::DomainMapperStateless::Workspace workspace(m_fem.mesh->Dimension()); + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); mfem::Vector elementDisplacementVariation; mfem::Vector elementAction; diff --git a/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp b/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp index 1def16c..894777f 100644 --- a/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp +++ b/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp @@ -10,7 +10,7 @@ import :operators.prepared_rotational_displacement_force; namespace mean_field::operators { PreparedRotationalDisplacementForceOperator::PreparedRotationalDisplacementForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper + const mapping::DomainMapper &domainMapper ) : m_fem(f), m_domainMapper(domainMapper), diff --git a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp index 318a488..ecb0366 100644 --- a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp @@ -9,28 +9,16 @@ module; module mean_field; import :operators.prepared_stellar_equilibrium; -import :physics.gravity; namespace { using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - [[nodiscard]] mean_field::fem::FEM &ensure_gravity_static_operators(mean_field::fem::FEM &f) { + void verify_coupled_discretization(const mean_field::fem::FEM &f) { MFEM_VERIFY( f.mesh != nullptr && f.densityFes != nullptr && f.displacementFes != nullptr && f.gravityFluxFes != nullptr && f.gravityPotentialFes != nullptr && f.enthalpyFes != nullptr, "PreparedStellarEquilibriumOperator requires the complete coupled finite-element discretization." ); - - if (f.gravityContext.b_form == nullptr || f.gravityContext.BT == nullptr) { - mean_field::physics::update_stiffness_matrix(f); - } - - MFEM_VERIFY( - f.gravityContext.b_form != nullptr && f.gravityContext.BT != nullptr, - "PreparedStellarEquilibriumOperator could not initialize the static gravity divergence operators." - ); - - return f; } [[nodiscard]] mean_field::operators::StellarEquilibriumLayout make_layout( @@ -318,13 +306,13 @@ namespace mean_field::operators { PreparedStellarEquilibriumOperator::ConstructionData PreparedStellarEquilibriumOperator::MakeConstructionData(fem::FEM &f) { - ensure_gravity_static_operators(f); + verify_coupled_discretization(f); return ConstructionData(f); } PreparedStellarEquilibriumOperator::PreparedStellarEquilibriumOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const models::StellarModel &stellarModel ) @@ -338,7 +326,7 @@ namespace mean_field::operators { PreparedStellarEquilibriumOperator::PreparedStellarEquilibriumOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const double targetMass ) @@ -353,7 +341,7 @@ namespace mean_field::operators { PreparedStellarEquilibriumOperator::PreparedStellarEquilibriumOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const double targetMass, ConstructionData constructionData @@ -587,8 +575,7 @@ namespace mean_field::operators { ); assign_residual_block( - m_cachedResidual, m_layout, enthalpyResidual, hydrostatic, - "The hydrostatic residual has the wrong size." + m_cachedResidual, m_layout, enthalpyResidual, hydrostatic, "The hydrostatic residual has the wrong size." ); assign_residual_block( @@ -712,8 +699,7 @@ namespace mean_field::operators { ); assign_residual_block( - action, m_layout, enthalpyResidual, hydrostaticAction, - "The hydrostatic Jacobian action has the wrong size." + action, m_layout, enthalpyResidual, hydrostaticAction, "The hydrostatic Jacobian action has the wrong size." ); assign_residual_block( diff --git a/libmeanfield/impl/physics/gravity.cpp b/libmeanfield/impl/physics/gravity.cpp index ae5dc04..af96b3e 100644 --- a/libmeanfield/impl/physics/gravity.cpp +++ b/libmeanfield/impl/physics/gravity.cpp @@ -2,171 +2,10 @@ module; #include "mfem.hpp" #include #include -#include -#include -#include -#include module mean_field; -import :mapping.coefficients; -import :analysis.integral; - -namespace { - double centrifugal_potential( - const mfem::Vector &phys_x, - const double omega - ) { - const double s2 = std::pow(phys_x(0), 2) + std::pow(phys_x(1), 2); - return -0.5 * s2 * std::pow(omega, 2); - } - - void grid_function_to_true_dofs( - const mfem::ParFiniteElementSpace &finite_element_space, - const mfem::GridFunction &grid_function, - mfem::Vector &true_dofs - ) { - MFEM_VERIFY( - grid_function.Size() == finite_element_space.GetVSize(), - "The grid function does not match the requested finite-element " - "space." - ); - - true_dofs.SetSize(finite_element_space.GetTrueVSize()); - - const mfem::Operator *restriction = finite_element_space.GetRestrictionMatrix(); - - if (restriction != nullptr) { - restriction->Mult(grid_function, true_dofs); - } else { - MFEM_VERIFY( - grid_function.Size() == true_dofs.Size(), "A finite-element space without a restriction operator must " - "have " - "matching local and true sizes." - ); - - true_dofs = grid_function; - } - } -} // namespace namespace mean_field::physics { - GravitySolution grav_potential( - fem::FEM &f, - const utils::Args &args, - const mfem::GridFunction &rho, - const bool phi_warm - ) { - MFEM_VERIFY( - f.densityFes != nullptr && rho.FESpace() == f.densityFes.get(), - "Gravity solve requires rho to use the registered density space." - ); - MFEM_VERIFY(f.gravityPotentialFes != nullptr, "Gravity solve requires the registered gravity-potential space."); - - mfem::Array outer_bdr_marker(f.mesh->bdr_attributes.Max()); - outer_bdr_marker = 0; - outer_bdr_marker[1] = 1; - - mfem::ParLinearForm g_rhs(f.gravityFluxFes.get()); - - // ReSharper disable once CppTooWideScope - std::unique_ptr boundary_potential_coeff; - - if (!f.has_mapping()) { // We only need to explicitly add a boundary - // integrator if a mapping is not being used. In - // the case where the outer domain has been - // compactified the φ=0 boundary condition is - // the natural condition and MFEM automatically - // handles this - auto boundary_potential = [&f](const mfem::Vector &x_physical) { - return l2_multipole_potential(f, utils::MASS, x_physical); - }; - - boundary_potential_coeff = std::make_unique(boundary_potential); - auto boundary_integrator = - std::make_unique(*boundary_potential_coeff); - const mfem::FiniteElement &boundary_element = *f.gravityFluxFes->GetTypicalTraceElement(); - - f.quadratureFactory->configure_gravity_boundary( - *boundary_integrator, quadrature::QuadratureRole::discretization, boundary_element, - utils::DOMAINS::VACUUM, quadrature::MappingKind::none - ); - g_rhs.AddBoundaryIntegrator(boundary_integrator.release(), outer_bdr_marker); - } - - g_rhs.Assemble(); - mfem::GridFunctionCoefficient rho_coeff(&rho); - mfem::ConstantCoefficient G4pi(4.0 * M_PI * utils::G); - mfem::ProductCoefficient source_coeff(G4pi, rho_coeff); - mfem::ParLinearForm f_rhs(f.gravityPotentialFes.get()); - - std::unique_ptr mapped_source_coeff; - mfem::Coefficient *active_source_coeff = &source_coeff; - quadrature::MappingKind source_mapping_kind = quadrature::MappingKind::none; - - if (f.has_mapping()) { - mapped_source_coeff = std::make_unique(*f.mapping, source_coeff); - active_source_coeff = mapped_source_coeff.get(); - source_mapping_kind = quadrature::MappingKind::general; - } - - auto source_integrator = std::make_unique(*active_source_coeff); - const mfem::FiniteElement &source_test_element = *f.gravityPotentialFes->GetTypicalFE(); - const mfem::ElementTransformation &source_transformation = *f.mesh->GetElementTransformation(0); - const int source_coefficient_order = f.densityFes->GetMaxElementOrder(); - - f.quadratureFactory->configure_gravity_source( - *source_integrator, quadrature::QuadratureRole::discretization, source_test_element, source_transformation, - source_coefficient_order, utils::DOMAINS::STELLAR, source_mapping_kind - ); - f_rhs.AddDomainIntegrator(source_integrator.release(), f.gravityContext.stellar_mask); - f_rhs.Assemble(); - - mfem::BlockVector RHS(f.gravityBlockTrueOffsets); - RHS.GetBlock(0) = *g_rhs.ParallelAssemble(); - RHS.GetBlock(1) = *f_rhs.ParallelAssemble(); - - mfem::BlockVector X(f.gravityBlockTrueOffsets); - X = 0.0; - f.gravityContext.minres->SetOperator(*f.gravityContext.block_A); - f.gravityContext.minres->Mult(RHS, X); - - GravitySolution solution(f); - solution.gradPhi.SetFromTrueDofs(X.GetBlock(0)); - solution.phi.SetFromTrueDofs(X.GetBlock(1)); - - return solution; - } - - mfem::GridFunction get_potential( - fem::FEM &fem, - const utils::Args &args, - const mfem::GridFunction &rho, - const bool warm - ) { - auto phi = grav_potential(fem, args, rho, warm); - - if (args.r.enabled) { - auto rot = [&fem, &args](const mfem::Vector &x) { - mfem::Vector rel_x = x; - rel_x -= fem.com; - return centrifugal_potential(rel_x, args.r.omega); - }; - - std::unique_ptr centrifugal_coeff; - if (fem.has_mapping()) { - centrifugal_coeff = std::make_unique(*fem.mapping, rot); - } else { - centrifugal_coeff = std::make_unique(rot); - } - - mfem::GridFunction centrifugal_gf(fem.gravityPotentialFes.get()); - centrifugal_gf.ProjectCoefficient(*centrifugal_coeff); - - phi.phi += centrifugal_gf; - } - return phi.phi; - } - mfem::DenseMatrix compute_quadrupole_moment_tensor( const fem::FEM &fem, const mfem::GridFunction &rho, @@ -175,9 +14,15 @@ namespace mean_field::physics { const int dim = fem.mesh->Dimension(); mfem::DenseMatrix local_Q(dim, dim); local_Q = 0.0; + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate + ); for (int i = 0; i < fem.mesh->GetNE(); ++i) { - if (fem.mesh->GetAttribute(i) == 3) + if (!DomainSchema::template attribute_belongs_to( + fem.mesh->GetAttribute(i))) continue; mfem::ElementTransformation *trans = fem.mesh->GetElementTransformation(i); @@ -193,20 +38,17 @@ namespace mean_field::physics { const mfem::IntegrationPoint &ip = ir.IntPoint(j); trans->SetIntPoint(&ip); - double weight = trans->Weight() * ip.weight; - - if (fem.has_mapping()) { - weight *= fem.mapping->ComputeDetJ(*trans, ip); - } + mapping::VolumeMappingContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluateVolume(*trans, ip, mapping_context) == + mapping::MappingStatus::valid, + "Quadrupole integration encountered an invalid mapping." + ); + const double weight = mapping_context.quadrature.weight; const double rho_val = rho.GetValue(i, ip); - mfem::Vector phys_point(dim); - if (fem.has_mapping()) { - fem.mapping->GetPhysicalPoint(*trans, ip, phys_point); - } else { - trans->Transform(ip, phys_point); - } + const mfem::Vector &phys_point = mapping_context.mapping.physical_position; mfem::Vector x_prime(dim); double r_sq = 0.0; @@ -261,141 +103,7 @@ namespace mean_field::physics { return l0_contrib + l2_contrib; } - void update_stiffness_matrix(fem::FEM &f) { - mfem::Array empty_tdofs; - - // ========================================== - // 1. Partially Assemble the High-Order Mass Block - // ========================================== - f.gravityContext.m_form = std::make_unique(f.gravityFluxFes.get()); - f.gravityContext.m_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL); - - std::unique_ptr hdiv_mass_integrator; - - if (f.has_mapping()) { - f.gravityContext.mapped_hdiv_mass_coeff = - std::make_unique(*f.mapping, f.mesh->Dimension()); - hdiv_mass_integrator = - std::make_unique(*f.gravityContext.mapped_hdiv_mass_coeff); - } else { - f.gravityContext.mapped_hdiv_mass_coeff.reset(); - hdiv_mass_integrator = std::make_unique(); - } - - const mfem::FiniteElement &hdiv_element = *f.gravityFluxFes->GetTypicalFE(); - const mfem::ElementTransformation &hdiv_transformation = *f.mesh->GetElementTransformation(0); - const quadrature::MappingKind mapping_kind = - f.has_mapping() ? quadrature::MappingKind::general : quadrature::MappingKind::none; - - f.quadratureFactory->configure_gravity_hdiv_mass( - *hdiv_mass_integrator, quadrature::QuadratureRole::discretization, hdiv_element, hdiv_transformation, - utils::DOMAINS::ALL, mapping_kind - ); - f.gravityContext.m_form->AddDomainIntegrator(hdiv_mass_integrator.release()); - - f.gravityContext.m_form->Assemble(); - - // ========================================== - // 2. Partially Assemble the High-Order Divergence Block - // ========================================== - f.gravityContext.b_form = - std::make_unique(f.gravityFluxFes.get(), f.gravityPotentialFes.get()); - f.gravityContext.b_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL); - - auto divergence_discretization_integrator = std::make_unique(); - const mfem::FiniteElement &divergence_discretization_test_element = *f.gravityPotentialFes->GetTypicalFE(); - - f.quadratureFactory->configure_gravity_divergence( - *divergence_discretization_integrator, quadrature::QuadratureRole::discretization, hdiv_element, - divergence_discretization_test_element, hdiv_transformation, utils::DOMAINS::ALL, - quadrature::MappingKind::none - ); - f.gravityContext.b_form->AddDomainIntegrator(divergence_discretization_integrator.release()); - - f.gravityContext.b_form->Assemble(); - - MFEM_VERIFY( - f.domainMapperStateless != nullptr, "Gravity source partial assembly requires the stateless domain " - "mapper." - ); - - mfem::Vector displacement_true(f.displacementFes->GetTrueVSize()); - displacement_true = 0.0; - - const mfem::GridFunction *active_displacement = f.mapping->GetDisplacement(); - - if (active_displacement != nullptr) { - grid_function_to_true_dofs(*f.displacementFes, *active_displacement, displacement_true); - } - - auto source_form = - std::make_unique(f, *f.domainMapperStateless); - - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - const field::FieldDofMap displacement_map = - field::make_field_dof_map(*f.displacementFes); - source_form->Prepare(displacement_map.gather(displacement_true)); - - f.gravityContext.source_form = std::move(source_form); - // ========================================== - // 3. Assemble Global Block Operator - // ========================================== - f.gravityContext.BT = std::make_unique(f.gravityContext.b_form.get()); - - f.gravityContext.block_A = std::make_unique(f.gravityBlockTrueOffsets); - f.gravityContext.block_A->SetBlock(0, 0, f.gravityContext.m_form.get()); - f.gravityContext.block_A->SetBlock(0, 1, f.gravityContext.BT.get()); - f.gravityContext.block_A->SetBlock(1, 0, f.gravityContext.b_form.get()); - - // ========================================== - // 4. Construct a mapped Schur preconditioner - // ========================================== - mfem::Vector mass_diagonal(f.gravityFluxFes->GetTrueVSize()); - f.gravityContext.m_form->AssembleDiagonal(mass_diagonal); - - mfem::Vector inverse_mass_diagonal(mass_diagonal); - - for (int i = 0; i < inverse_mass_diagonal.Size(); ++i) { - MFEM_VERIFY( - std::isfinite(inverse_mass_diagonal(i)) && inverse_mass_diagonal(i) > 0.0, - "Mapped RT mass matrix has a non-positive or non-finite " - "diagonal " - "entry." - ); - inverse_mass_diagonal(i) = 1.0 / inverse_mass_diagonal(i); - } - - mfem::ParMixedBilinearForm b_preconditioner(f.gravityFluxFes.get(), f.gravityPotentialFes.get()); - auto divergence_preconditioner_integrator = std::make_unique(); - - const mfem::FiniteElement &divergence_trial_element = *f.gravityFluxFes->GetTypicalFE(); - const mfem::FiniteElement &divergence_test_element = *f.gravityPotentialFes->GetTypicalFE(); - const mfem::ElementTransformation &divergence_transformation = *f.mesh->GetElementTransformation(0); - - f.quadratureFactory->configure_gravity_divergence( - *divergence_preconditioner_integrator, quadrature::QuadratureRole::preconditioner, divergence_trial_element, - divergence_test_element, divergence_transformation, utils::DOMAINS::ALL, quadrature::MappingKind::none - ); - b_preconditioner.AddDomainIntegrator(divergence_preconditioner_integrator.release()); - b_preconditioner.Assemble(); - b_preconditioner.Finalize(); - std::unique_ptr b_matrix(b_preconditioner.ParallelAssemble()); - std::unique_ptr inverse_mass_b_transpose(b_matrix->Transpose()); - - inverse_mass_b_transpose->ScaleRows(inverse_mass_diagonal); - - f.gravityContext.Schur.reset(mfem::ParMult(b_matrix.get(), inverse_mass_b_transpose.get())); - - // ========================================== - // 5. Wire Up the preconditioners - // ========================================== - f.gravityContext.prec_M = std::make_unique(mass_diagonal, empty_tdofs); - f.gravityContext.prec_Phi->SetOperator(*f.gravityContext.Schur); - f.gravityContext.block_prec->SetDiagonalBlock(0, f.gravityContext.prec_M.get()); - f.gravityContext.block_prec->SetDiagonalBlock(1, f.gravityContext.prec_Phi.get()); - } - - GravitySolution grav_potential_new( + GravitySolution solve_gravity_field( fem::FEM &f, const utils::Args &args, const mfem::GridFunction &rho, @@ -417,13 +125,6 @@ namespace mean_field::physics { "displacement finite-element space." ); MFEM_VERIFY(f.domainMapperStateless != nullptr, "Gravity initialization requires the stateless domain mapper."); - MFEM_VERIFY(f.gravityContext.b_form != nullptr, "Gravity initialization requires the divergence operator."); - MFEM_VERIFY( - f.gravityContext.BT != nullptr, "Gravity initialization requires the transpose divergence operator." - ); - MFEM_VERIFY( - f.gravityContext.block_prec != nullptr, "Gravity initialization requires the gravity block preconditioner." - ); MFEM_VERIFY( rho.FESpace() == f.densityFes.get(), "Gravity initialization requires density to use the FEM density " "space." @@ -434,6 +135,7 @@ namespace mean_field::physics { "Vec_H1 " "space." ); + MFEM_VERIFY(args.p.max_iters > 0, "Gravity solve requires a positive MINRES iteration limit."); using form = utils::blocks::gravity_field_form; @@ -444,13 +146,19 @@ namespace mean_field::physics { utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - const field::FieldDofMap density_map = field::make_field_dof_map(*f.densityFes); - const field::FieldDofMap displacement_map = - field::make_field_dof_map(*f.displacementFes); - const field::FieldDofMap gravity_flux_map = - field::make_field_dof_map(*f.gravityFluxFes); - const field::FieldDofMap gravity_potential_map = - field::make_field_dof_map(*f.gravityPotentialFes); + const field::FieldDofGridFunctionAdapter density_adapter = + field::make_field_dof_grid_function_adapter(*f.densityFes); + const field::FieldDofGridFunctionAdapter displacement_adapter = + field::make_field_dof_grid_function_adapter(*f.displacementFes); + const field::FieldDofGridFunctionAdapter gravity_flux_adapter = + field::make_field_dof_grid_function_adapter(*f.gravityFluxFes); + const field::FieldDofGridFunctionAdapter gravity_potential_adapter = + field::make_field_dof_grid_function_adapter(*f.gravityPotentialFes); + + const field::FieldDofMap &density_map = density_adapter.dof_map(); + const field::FieldDofMap &displacement_map = displacement_adapter.dof_map(); + const field::FieldDofMap &gravity_flux_map = gravity_flux_adapter.dof_map(); + const field::FieldDofMap &gravity_potential_map = gravity_potential_adapter.dof_map(); const std::array value_sizes{ density_map.reduced_size(), displacement_map.reduced_size(), gravity_flux_map.reduced_size(), @@ -463,14 +171,8 @@ namespace mean_field::physics { const utils::blocks::form_layout layout(value_sizes, residual_sizes); - mfem::Vector density_true; - mfem::Vector displacement_true; - - grid_function_to_true_dofs(*f.densityFes, rho, density_true); - grid_function_to_true_dofs(*f.displacementFes, displacement, displacement_true); - - const mfem::Vector density = density_map.gather(density_true); - const mfem::Vector reduced_displacement = displacement_map.gather(displacement_true); + const mfem::Vector density = density_adapter.gather(rho); + const mfem::Vector reduced_displacement = displacement_adapter.gather(displacement); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless @@ -491,6 +193,7 @@ namespace mean_field::physics { operators::ReducedGravityFieldOperator reduced_operator( gravity_operator, reduced_geometry_context, reduced_displacement ); + operators::ReducedGravityFieldPreconditioner reduced_preconditioner(f, reduced_geometry_context); mfem::Vector right_hand_side; reduced_operator.BuildRightHandSide(density, right_hand_side); @@ -505,25 +208,24 @@ namespace mean_field::physics { mfem::MINRESSolver minres(f.mesh->GetComm()); minres.SetOperator(reduced_operator); - minres.SetPreconditioner(*f.gravityContext.block_prec); + minres.SetPreconditioner(reduced_preconditioner); minres.SetRelTol(args.p.rtol); minres.SetAbsTol(args.p.atol); minres.SetMaxIter(args.p.max_iters); - minres.SetPrintLevel(1); + // minres.SetPrintLevel(args.verbose ? 1 : 0); + minres.SetPrintLevel(0); minres.Mult(right_hand_side, gravity_state); MFEM_VERIFY(minres.GetConverged(), "The reduced gravity solve failed to converge."); GravitySolution solution(f); - const mfem::Vector gravity_flux_true = - gravity_flux_map.scatter(gravity_state.GetBlock(gravity_gradient_residual_block)); - const mfem::Vector gravity_potential_true = - gravity_potential_map.scatter(gravity_state.GetBlock(gravity_poisson_residual_block)); - - solution.gradPhi.SetFromTrueDofs(gravity_flux_true); - - solution.phi.SetFromTrueDofs(gravity_potential_true); + gravity_flux_adapter.scatter( + gravity_state.GetBlock(gravity_gradient_residual_block), solution.gradPhi + ); + gravity_potential_adapter.scatter( + gravity_state.GetBlock(gravity_poisson_residual_block), solution.phi + ); return solution; } diff --git a/libmeanfield/impl/physics/solid.cpp b/libmeanfield/impl/physics/solid.cpp index 96f26bf..2cf001c 100644 --- a/libmeanfield/impl/physics/solid.cpp +++ b/libmeanfield/impl/physics/solid.cpp @@ -10,9 +10,15 @@ namespace mean_field::physics { const mfem::GridFunction &rho_ref ) { double local_I = 0.0; + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate + ); for (int i = 0; i < fem.mesh->GetNE(); i++) { - if (fem.mesh->GetAttribute(i) == 3) + if (!DomainSchema::template attribute_belongs_to( + fem.mesh->GetAttribute(i))) continue; mfem::ElementTransformation *T = fem.mesh->GetElementTransformation(i); @@ -29,12 +35,16 @@ namespace mean_field::physics { const double rho_hat = rho_ref.GetValue(i, ip); - mfem::Vector x_phys; - fem.mapping->GetPhysicalPoint(*T, ip, x_phys); + mapping::VolumeMappingContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluateVolume(*T, ip, mapping_context) == + mapping::MappingStatus::valid, + "Moment-of-inertia integration encountered an invalid mapping." + ); + const mfem::Vector &x_phys = mapping_context.mapping.physical_position; const double r_cyl_sq = x_phys(0) * x_phys(0) + x_phys(1) * x_phys(1); - const double detJ = std::fabs(fem.mapping->ComputeDetJ(*T, ip)); - const double weight = T->Weight() * ip.weight * detJ; + const double weight = mapping_context.quadrature.weight; local_I += rho_hat * r_cyl_sq * weight; } diff --git a/libmeanfield/impl/utils/domain.cpp b/libmeanfield/impl/utils/domain.cpp index 3aa55bf..e37ea83 100644 --- a/libmeanfield/impl/utils/domain.cpp +++ b/libmeanfield/impl/utils/domain.cpp @@ -12,6 +12,10 @@ namespace mean_field::utils { ) { const int dim = fem.mesh->Dimension(); x_ref = x_phys_target; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate + ); mfem::Array init_elem; mfem::Array init_ip; @@ -29,15 +33,18 @@ namespace mean_field::utils { mfem::Array origin_ip; fem.mesh->FindPoints(P_origin, origin_elem, origin_ip, false); - if (origin_elem.Size() > 0 && origin_elem[0] >= 0 && fem.mapping->HasDisplacementField()) { + if (origin_elem.Size() > 0 && origin_elem[0] >= 0) { mfem::ElementTransformation *T0 = fem.mesh->GetElementTransformation(origin_elem[0]); T0->SetIntPoint(&origin_ip[0]); - mfem::DenseMatrix J0(dim, dim), J0_inv(dim, dim); - fem.mapping->ComputeJacobian(*T0, J0); - mfem::CalcInverse(J0, J0_inv); + mapping::MappingPointContext context; + MFEM_VERIFY( + mapping_evaluator.EvaluatePoint(*T0, origin_ip[0], context) == + mapping::MappingStatus::valid, + "Reference-point initialization encountered an invalid mapping." + ); - J0_inv.Mult(x_phys_target, x_ref); + context.inverse_mapping_jacobian.Mult(x_phys_target, x_ref); } init_P.SetCol(0, x_ref); @@ -70,9 +77,6 @@ namespace mean_field::utils { mfem::Vector residual(dim); mfem::Vector step(dim); - mfem::DenseMatrix J_map(dim, dim); - mfem::DenseMatrix J_map_inv(dim, dim); - int find_failures = 0; for (int iter = 0; iter < max_iter; ++iter) { @@ -99,8 +103,12 @@ namespace mean_field::utils { mfem::ElementTransformation *T = fem.mesh->GetElementTransformation(elemID); T->SetIntPoint(&ip); - mfem::Vector current_x_phys(dim); - fem.mapping->GetPhysicalPoint(*T, ip, current_x_phys); + mapping::MappingPointContext context; + if (mapping_evaluator.EvaluatePoint(*T, ip, context) != + mapping::MappingStatus::valid) { + return false; + } + const mfem::Vector ¤t_x_phys = context.physical_position; for (int i = 0; i < dim; ++i) { residual(i) = current_x_phys(i) - x_phys_target(i); @@ -110,9 +118,7 @@ namespace mean_field::utils { return true; } - fem.mapping->ComputeJacobian(*T, J_map); - mfem::CalcInverse(J_map, J_map_inv); - J_map_inv.Mult(residual, step); + context.inverse_mapping_jacobian.Mult(residual, step); double alpha = 1.0; mfem::Vector x_ref_candidate(dim); diff --git a/libmeanfield/impl/utils/misc.cpp b/libmeanfield/impl/utils/misc.cpp index ce3a766..8d3db90 100644 --- a/libmeanfield/impl/utils/misc.cpp +++ b/libmeanfield/impl/utils/misc.cpp @@ -1,123 +1,24 @@ module; -#include #include module mean_field; -import :boundary.contexts; namespace mean_field::utils { - DOMAINS operator|( - DOMAINS lhs, - DOMAINS rhs - ) { - return static_cast(static_cast(lhs) | static_cast(rhs)); - } +DOMAINS operator|(DOMAINS lhs, DOMAINS rhs) { + return static_cast(static_cast(lhs) | + static_cast(rhs)); +} - DOMAINS operator&( - DOMAINS lhs, - DOMAINS rhs - ) { - return static_cast(static_cast(lhs) & static_cast(rhs)); - } +DOMAINS operator&(DOMAINS lhs, DOMAINS rhs) { + return static_cast(static_cast(lhs) & + static_cast(rhs)); +} - void populate_element_mask( - const mfem::Mesh *mesh, - const DOMAINS domain, - mfem::Array &mask - ) { - const int max_attr = mesh->attributes.Max(); - mask.SetSize(max_attr); - mask = 0; +int get_mesh_order(const mfem::Mesh &mesh) { + if (mesh.GetNodes() != nullptr) { + return mesh.GetNodes()->FESpace()->GetMaxElementOrder(); + } + return 1; +} - if ((domain & DOMAINS::CORE) == DOMAINS::CORE && max_attr >= 1) { - mask[0] = 1; - } - - if ((domain & DOMAINS::ENVELOPE) == DOMAINS::ENVELOPE && max_attr >= 2) { - mask[1] = 1; - } - - if ((domain & DOMAINS::VACUUM) == DOMAINS::VACUUM && max_attr >= 3) { - mask[2] = 1; - } - } - - void populate_domain_tdofs( - const mfem::ParFiniteElementSpace *fes, - const mfem::Array &element_mask, - mfem::Array &ess_tdof - ) { - mfem::Array vdof_marker(fes->GetVSize()); - vdof_marker = 0; - - for (int i = 0; i < fes->GetMesh()->GetNE(); i++) { - const int attr = fes->GetMesh()->GetAttribute(i); - - if (element_mask[attr - 1]) { - mfem::Array dofs; - fes->GetElementVDofs(i, dofs); - - for (int j = 0; j < dofs.Size(); j++) { - int index = dofs[j]; - if (index < 0) - index = -1 - index; - vdof_marker[index] = 1; - } - } - } - - fes->MarkerToList(vdof_marker, ess_tdof); - } - - std::expected< - boundary::Bounds, - boundary::BoundsError> - discover_bounds( - const mfem::Mesh *mesh, - const int vacuum_attr - ) { - double local_min_r = std::numeric_limits::max(); - double local_max_r = -std::numeric_limits::max(); - bool found_vacuum = false; - - for (int i = 0; i < mesh->GetNE(); ++i) { - if (mesh->GetAttribute(i) == vacuum_attr) { - found_vacuum = true; - mfem::Array vertices; - mesh->GetElementVertices(i, vertices); - for (const int v : vertices) { - const double *coords = mesh->GetVertex(v); - double r = std::sqrt(coords[0] * coords[0] + coords[1] * coords[1] + coords[2] * coords[2]); - local_min_r = std::min(local_min_r, r); - local_max_r = std::max(local_max_r, r); - } - } - } - - double global_min_r, global_max_r; - int global_found_vacuum; - int l_found = found_vacuum ? 1 : 0; - - MPI_Comm comm = MPI_COMM_WORLD; - if (const auto *pmesh = dynamic_cast(mesh)) { - comm = pmesh->GetComm(); - } - - MPI_Allreduce(&local_min_r, &global_min_r, 1, MPI_DOUBLE, MPI_MIN, comm); - MPI_Allreduce(&local_max_r, &global_max_r, 1, MPI_DOUBLE, MPI_MAX, comm); - MPI_Allreduce(&l_found, &global_found_vacuum, 1, MPI_INT, MPI_MAX, comm); - - if (global_found_vacuum) { - return boundary::Bounds(global_min_r, global_max_r); - } - return std::unexpected(boundary::BoundsError::CANNOT_FIND_VACUUM); - } - - int get_mesh_order(const mfem::Mesh &mesh) { - if (mesh.GetNodes() != nullptr) { - return mesh.GetNodes()->FESpace()->GetMaxElementOrder(); - } - return 1; - } - -} // namespace mean_field::utils \ No newline at end of file +} // namespace mean_field::utils diff --git a/libmeanfield/interface/eos/polytropic.cppm b/libmeanfield/interface/eos/polytropic.cppm index 1481b42..36409fc 100644 --- a/libmeanfield/interface/eos/polytropic.cppm +++ b/libmeanfield/interface/eos/polytropic.cppm @@ -121,7 +121,7 @@ export namespace mean_field::eos { std::pow(density, 1.0 / m_polytropic_index); } - [[nodiscard]] double enthalpy_from_pressure(double pressure) const override { + [[nodiscard]] double enthalpy_from_pressure(const double pressure) const override { validate_nonnegativity(pressure, "pressure"); const double np1 = m_polytropic_index + 1; return np1 * std::pow(m_polytropic_constant, m_polytropic_index / np1) * std::pow(pressure, 1.0 / np1); @@ -159,12 +159,8 @@ export namespace mean_field::eos { ); } } - - public: - - private: double m_polytropic_index; double m_polytropic_constant; double m_enthalpy_scale; }; -} // namespace mean_field::eos \ No newline at end of file +} // namespace mean_field::eos diff --git a/libmeanfield/interface/fem.cppm b/libmeanfield/interface/fem.cppm index 9f7abb6..d6897b0 100644 --- a/libmeanfield/interface/fem.cppm +++ b/libmeanfield/interface/fem.cppm @@ -8,7 +8,6 @@ module; export module mean_field:fem; -export import :physics.contexts; export import :boundary.contexts; export import :mapping.domain_mapper; export import :utils.misc; @@ -92,38 +91,9 @@ export namespace mean_field::fem { // ===================================================================== // Domain mapping - // - // These are declared after displacement so that they are destroyed - // before the displacement grid function to which mapping may refer. - // DomainMapper is retained only for legacy integrators. New operators - // use DomainMapperStateless exclusively. // ===================================================================== - std::unique_ptr mapping; - - std::unique_ptr domainMapperStateless; - - // ===================================================================== - // Block layouts - // - // These arrays are retained only for legacy code. Canonical operator - // layouts are defined by the compile-time forms in :utils.blocks. - // - // Main system: [Displacement | Density] - // Gravity system: [Flux | Potential] - // ===================================================================== - - mfem::Array blockTrueOffsets; - mfem::Array gravityBlockTrueOffsets; - - // ===================================================================== - // Boundary conditions and domain masks - // ===================================================================== - - mfem::Array essentialDisplacementTdofs; - mfem::Array vacuumDensityTdofs; - mfem::Array vacuumEnthalpyTdofs; - mfem::Array vacuumDisplacementTdofs; + std::unique_ptr domainMapperStateless; // ===================================================================== // Global diagnostics @@ -133,10 +103,9 @@ export namespace mean_field::fem { mfem::DenseMatrix Q; // ===================================================================== - // Physics and boundary contexts + // Boundary context // ===================================================================== - physics::GravityContext gravityContext; boundary::BoundaryContext boundaryContext; std::unique_ptr quadratureFactory; @@ -160,13 +129,11 @@ export namespace mean_field::fem { compactificationFec != nullptr && compactificationFes != nullptr && compactificationCoordinate != nullptr && - mapping != nullptr && domainMapperStateless != nullptr && quadratureFactory != nullptr && - - blockTrueOffsets.Size() == 3 && gravityBlockTrueOffsets.Size() == 3; + domainMapperStateless != nullptr && quadratureFactory != nullptr; } [[nodiscard]] bool has_mapping() const { - return mapping != nullptr; + return domainMapperStateless != nullptr && displacement != nullptr && compactificationCoordinate != nullptr; } }; diff --git a/libmeanfield/interface/field/field_mfem.cppm b/libmeanfield/interface/field/field_mfem.cppm index c08751e..e7c014d 100644 --- a/libmeanfield/interface/field/field_mfem.cppm +++ b/libmeanfield/interface/field/field_mfem.cppm @@ -5,6 +5,7 @@ module; #include #include #include +#include #include @@ -887,6 +888,110 @@ export namespace mean_field::field { mfem::Array m_trueToReduced; }; + /* + * Canonical adapter between an MFEM GridFunction and a reduced field + * vector. + * + * FieldDofMap deliberately contains only indexing information. This + * adapter binds that indexing to the exact finite-element space whose true + * DOFs the map describes. Consequently, a grid function from another + * finite-element space is rejected even when it happens to have the same + * vector size. + * + * The finite-element space must outlive the adapter. + */ + class FieldDofGridFunctionAdapter { + public: + FieldDofGridFunctionAdapter( + FieldDofMap dofMap, + const mfem::FiniteElementSpace &finiteElementSpace + ) + : m_dofMap(std::move(dofMap)), + m_finiteElementSpace(&finiteElementSpace) { + if (m_dofMap.full_size() != finiteElementSpace.GetTrueVSize()) { + throw std::invalid_argument( + "FieldDofGridFunctionAdapter map and finite-element " + "space have incompatible true-DOF sizes." + ); + } + } + + [[nodiscard]] + const FieldDofMap &dof_map() const noexcept { + return m_dofMap; + } + + [[nodiscard]] + const mfem::FiniteElementSpace &finite_element_space() const noexcept { + return *m_finiteElementSpace; + } + + /* + * Gather the grid function's true DOFs into reduced field ordering. + * The output vector is not resized so MFEM vector views remain valid. + */ + void gather( + const mfem::GridFunction &gridFunction, + mfem::Vector &reduced + ) const { + validate_grid_function(gridFunction); + + mfem::Vector full; + gridFunction.GetTrueDofs(full); + m_dofMap.gather(full, reduced); + } + + [[nodiscard]] + mfem::Vector gather(const mfem::GridFunction &gridFunction) const { + mfem::Vector reduced(m_dofMap.reduced_size()); + gather(gridFunction, reduced); + return reduced; + } + + /* + * Scatter with projection semantics. Unsupported true DOFs are zeroed + * before the complete true vector is distributed to the grid function. + */ + void scatter( + const mfem::Vector &reduced, + mfem::GridFunction &gridFunction + ) const { + validate_grid_function(gridFunction); + + const mfem::Vector full = m_dofMap.scatter(reduced); + gridFunction.SetFromTrueDofs(full); + } + + /* + * Scatter while preserving the grid function's existing unsupported + * true DOFs. + */ + void scatter_into( + const mfem::Vector &reduced, + mfem::GridFunction &gridFunction + ) const { + validate_grid_function(gridFunction); + + mfem::Vector full; + gridFunction.GetTrueDofs(full); + m_dofMap.scatter_into(reduced, full); + gridFunction.SetFromTrueDofs(full); + } + + private: + void validate_grid_function(const mfem::GridFunction &gridFunction) const { + if (gridFunction.FESpace() != m_finiteElementSpace) { + throw std::invalid_argument( + "FieldDofGridFunctionAdapter received a grid function " + "from a different finite-element space." + ); + } + } + + FieldDofMap m_dofMap; + const mfem::FiniteElementSpace *m_finiteElementSpace; + }; + /* * Construct the canonical solver map for a registered spatial field. * @@ -903,4 +1008,16 @@ export namespace mean_field::field { return FieldDofMap(support); } + + template < + MfemDomainField FieldT, + utils::domain::IsSchema SchemaT> + [[nodiscard]] + FieldDofGridFunctionAdapter + make_field_dof_grid_function_adapter(const mfem::ParFiniteElementSpace &finiteElementSpace) { + return FieldDofGridFunctionAdapter( + make_field_dof_map(finiteElementSpace), + finiteElementSpace + ); + } } // namespace mean_field::field diff --git a/libmeanfield/interface/integrators/advection.cppm b/libmeanfield/interface/integrators/advection.cppm index 60d243e..14419e6 100644 --- a/libmeanfield/interface/integrators/advection.cppm +++ b/libmeanfield/interface/integrators/advection.cppm @@ -6,7 +6,11 @@ import :mapping.domain_mapper; export namespace mean_field::integrators { class AdvectionIntegrator : public mfem::BlockNonlinearFormIntegrator { public: - explicit AdvectionIntegrator(const mapping::DomainMapper &map); + AdvectionIntegrator( + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate + ); void AssembleElementVector( const mfem::Array &el, @@ -23,6 +27,6 @@ export namespace mean_field::integrators { ) override; private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; }; -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/interface/integrators/centrifugal.cppm b/libmeanfield/interface/integrators/centrifugal.cppm index 2e7b1b6..3a8832b 100644 --- a/libmeanfield/interface/integrators/centrifugal.cppm +++ b/libmeanfield/interface/integrators/centrifugal.cppm @@ -7,7 +7,9 @@ export namespace mean_field::integrators { class CentrifugalForceIntegrator : public mfem::BlockNonlinearFormIntegrator { public: CentrifugalForceIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const mfem::Vector &omega ); @@ -29,9 +31,9 @@ export namespace mean_field::integrators { ) override; private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; mfem::Vector m_omega; const mfem::IntegrationRule *m_ir = nullptr; }; -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/interface/integrators/coriolis.cppm b/libmeanfield/interface/integrators/coriolis.cppm index e2616ef..48d9045 100644 --- a/libmeanfield/interface/integrators/coriolis.cppm +++ b/libmeanfield/interface/integrators/coriolis.cppm @@ -7,7 +7,9 @@ export namespace mean_field::integrators { class CoriolisIntegrator : public mfem::BlockNonlinearFormIntegrator { public: CoriolisIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const mfem::Vector &omega ); @@ -26,9 +28,9 @@ export namespace mean_field::integrators { ) override; private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; mfem::Vector m_omega; mfem::DenseMatrix m_omega_mat; }; -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/interface/integrators/gravity.cppm b/libmeanfield/interface/integrators/gravity.cppm index 9b531c3..e063342 100644 --- a/libmeanfield/interface/integrators/gravity.cppm +++ b/libmeanfield/interface/integrators/gravity.cppm @@ -10,7 +10,9 @@ export namespace mean_field::integrators { class GravityMomentumIntegrator : public mfem::BlockNonlinearFormIntegrator { public: explicit GravityMomentumIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, GravityForceJacobianMode jacobian_mode = GravityForceJacobianMode::field_coupled ); @@ -33,8 +35,8 @@ export namespace mean_field::integrators { ) override; private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; GravityForceJacobianMode m_jacobian_mode; const mfem::IntegrationRule *m_integration_rule{nullptr}; }; -} // namespace mean_field::integrators \ No newline at end of file +} // namespace mean_field::integrators diff --git a/libmeanfield/interface/integrators/mass_continuity.cppm b/libmeanfield/interface/integrators/mass_continuity.cppm index 28b98c2..76da98b 100644 --- a/libmeanfield/interface/integrators/mass_continuity.cppm +++ b/libmeanfield/interface/integrators/mass_continuity.cppm @@ -6,7 +6,11 @@ import :mapping.domain_mapper; export namespace mean_field::integrators { class ContinuityVolumeIntegrator : public mfem::BlockNonlinearFormIntegrator { public: - explicit ContinuityVolumeIntegrator(const mapping::DomainMapper &map); + ContinuityVolumeIntegrator( + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate + ); void AssembleElementVector( const mfem::Array &el, @@ -23,12 +27,16 @@ export namespace mean_field::integrators { ) override; private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; }; class ContinuityFaceIntegrator : public mfem::BlockNonlinearFormIntegrator { public: - explicit ContinuityFaceIntegrator(const mapping::DomainMapper &map); + ContinuityFaceIntegrator( + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate + ); void AssembleFaceVector( const mfem::Array &el1, @@ -58,7 +66,7 @@ export namespace mean_field::integrators { ); private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; }; } // namespace mean_field::integrators diff --git a/libmeanfield/interface/integrators/pressure_gradient.cppm b/libmeanfield/interface/integrators/pressure_gradient.cppm index 89b7e37..b8c736c 100644 --- a/libmeanfield/interface/integrators/pressure_gradient.cppm +++ b/libmeanfield/interface/integrators/pressure_gradient.cppm @@ -10,7 +10,9 @@ export namespace mean_field::integrators { template class PressureGradientIntegrator : public mfem::BlockNonlinearFormIntegrator { public: PressureGradientIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, utils::EOS_P eos ); @@ -28,16 +30,18 @@ export namespace mean_field::integrators { ) override; private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; utils::EOS_P m_eos; }; template PressureGradientIntegrator::PressureGradientIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, utils::EOS_P eos ) - : m_map(map), + : m_mapping(mapper, displacement, compactification_coordinate), m_eos(std::move(eos)) { } @@ -48,6 +52,8 @@ export namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array &elvec ) { + m_mapping.InvalidateCache(); + if (utils::is_vacuum(Tr, elvec)) { return; } @@ -78,7 +84,7 @@ export namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcDShape(ip, dshape_v_ref); mfem::Mult(dshape_v_ref, J_inv, dshape_v_phys); @@ -111,6 +117,8 @@ export namespace mean_field::integrators { const mfem::Array &elfun, const mfem::Array2D &elmats ) { + m_mapping.InvalidateCache(); + const mfem::FiniteElement *fe_v = el[0]; const mfem::FiniteElement *fe_rho = el[1]; @@ -141,7 +149,7 @@ export namespace mean_field::integrators { const mfem::IntegrationPoint &ip = ir->IntPoint(q); Tr.SetIntPoint(&ip); - auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip); + auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip); fe_v->CalcDShape(ip, dshape_v_ref); mfem::Mult(dshape_v_ref, J_inv, dshape_v_phys); diff --git a/libmeanfield/interface/integrators/viscosity.cppm b/libmeanfield/interface/integrators/viscosity.cppm index b7b589b..6801845 100644 --- a/libmeanfield/interface/integrators/viscosity.cppm +++ b/libmeanfield/interface/integrators/viscosity.cppm @@ -7,7 +7,9 @@ export namespace mean_field::integrators { class ViscosityIntegrator : public mfem::BlockNonlinearFormIntegrator { public: ViscosityIntegrator( - const mapping::DomainMapper &map, + const mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, double mu, int quad_boost ); @@ -29,7 +31,7 @@ export namespace mean_field::integrators { ) override; private: - const mapping::DomainMapper &m_map; + mapping::GridFunctionMappingEvaluator m_mapping; double m_mu; int m_quad_boost; }; diff --git a/libmeanfield/interface/mapping/coefficients.cppm b/libmeanfield/interface/mapping/coefficients.cppm index 2df74de..19d99ea 100644 --- a/libmeanfield/interface/mapping/coefficients.cppm +++ b/libmeanfield/interface/mapping/coefficients.cppm @@ -9,7 +9,9 @@ export namespace mean_field::mapping { class MappedScalarCoefficient : public mfem::Coefficient { public: MappedScalarCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, Coefficient &coeff, COORDINATE_SPACE coord_space = COORDINATE_SPACE::PHYSICAL ); @@ -27,7 +29,7 @@ export namespace mean_field::mapping { ); private: - const DomainMapper &m_map; + GridFunctionMappingEvaluator m_mapping; Coefficient &m_coeff; COORDINATE_SPACE m_coord_space; }; @@ -35,13 +37,17 @@ export namespace mean_field::mapping { class MappedDiffusionCoefficient : public mfem::MatrixCoefficient { public: MappedDiffusionCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, mfem::Coefficient &sigma, int dim ); MappedDiffusionCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, MatrixCoefficient &sigma ); @@ -52,7 +58,7 @@ export namespace mean_field::mapping { ) override; private: - const DomainMapper &m_map; + GridFunctionMappingEvaluator m_mapping; mfem::Coefficient *m_scalar; MatrixCoefficient *m_tensor; }; @@ -60,7 +66,9 @@ export namespace mean_field::mapping { class MappedVectorCoefficient : public mfem::VectorCoefficient { public: MappedVectorCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, VectorCoefficient &coeff ); @@ -71,7 +79,7 @@ export namespace mean_field::mapping { ) override; private: - const DomainMapper &m_map; + GridFunctionMappingEvaluator m_mapping; VectorCoefficient &m_coeff; }; @@ -80,7 +88,9 @@ export namespace mean_field::mapping { using Func = std::function; PhysicalPositionFunctionCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, Func f ); @@ -91,13 +101,15 @@ export namespace mean_field::mapping { private: Func m_f; - const DomainMapper &m_map; + GridFunctionMappingEvaluator m_mapping; }; class MappedHDivMassCoefficient final : public mfem::MatrixCoefficient { public: MappedHDivMassCoefficient( - const DomainMapper &map, + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, const int dim ); @@ -108,6 +120,6 @@ export namespace mean_field::mapping { ) override; private: - const DomainMapper &m_map; + GridFunctionMappingEvaluator m_mapping; }; } // namespace mean_field::mapping diff --git a/libmeanfield/interface/mapping/domain_mapper.cppm b/libmeanfield/interface/mapping/domain_mapper.cppm index cc82423..dc07633 100644 --- a/libmeanfield/interface/mapping/domain_mapper.cppm +++ b/libmeanfield/interface/mapping/domain_mapper.cppm @@ -8,347 +8,259 @@ import :mapping.compactification; import :utils.user; export namespace mean_field::mapping { - enum class FaceElementSide : uint8_t { element_1, element_2 }; - - class ElementDisplacementData { - public: - ElementDisplacementData( - const mfem::FiniteElement &element, - const mfem::Vector &displacement_dofs, - mfem::Ordering::Type ordering = mfem::Ordering::byNODES - ); - - [[nodiscard]] const mfem::FiniteElement &GetElement() const noexcept; - [[nodiscard]] const mfem::DenseMatrix &GetDofMatrix() const noexcept; - [[nodiscard]] int GetDimension() const noexcept; - [[nodiscard]] int GetDofCount() const noexcept; - [[nodiscard]] mfem::Ordering::Type GetOrdering() const noexcept; - - private: - const mfem::FiniteElement *m_element; - mfem::DenseMatrix m_dof_matrix; - int m_dimension; - mfem::Ordering::Type m_ordering; - }; - - struct CompactificationPointData { - double coordinate{0.0}; - mfem::Vector coordinate_gradient; - }; - - [[nodiscard]] ElementDisplacementData ElementDisplacementDataFromElementVDofs( - const mfem::FiniteElement &element, - const mfem::Vector &displacement_dofs - ); - - class ElementCompactificationData { - public: - ElementCompactificationData( - const mfem::FiniteElement &element, - const mfem::Vector &dofs - ); - - [[nodiscard]] const mfem::FiniteElement &GetElement() const noexcept; - [[nodiscard]] const mfem::Vector &GetDofs() const noexcept; - [[nodiscard]] int GetDofCount() const noexcept; - - private: - const mfem::FiniteElement *m_element; - mfem::Vector m_dofs; - }; - - struct ElementMappingData { - const ElementDisplacementData &displacement; - const ElementCompactificationData &compactification; - }; - - class DomainMapperStateless { - public: - class Workspace { - public: - explicit Workspace(int dimension = 3); - - void SetDimension(int dimension); - - [[nodiscard]] int GetDimension() const noexcept; - - private: - friend class DomainMapperStateless; - - int m_dimension; - - mfem::Vector m_shape; - mfem::DenseMatrix m_mesh_dshape; - mfem::Vector m_field_value; - mfem::DenseMatrix m_field_jacobian; - - mfem::Vector m_compactification_shape; - mfem::DenseMatrix m_compactification_dshape; - CompactificationPointData m_compactification_point; - - mfem::Vector m_reference_normal; - mfem::Vector m_mapped_normal; - mfem::DenseMatrix m_full_element_jacobian; - - mfem::Vector m_vector_temp; - mfem::DenseMatrix m_matrix_temp_1; - mfem::DenseMatrix m_matrix_temp_2; - - compactification::ExteriorMapResult m_exterior_result; - compactification::ExteriorMapVariation m_exterior_variation; - }; - - public: - DomainMapperStateless( - utils::DomainMapperStatelessOptions options, - std::unique_ptr exterior_map - ); - - DomainMapperStateless(const DomainMapperStateless &) = delete; - DomainMapperStateless &operator=(const DomainMapperStateless &) = delete; - DomainMapperStateless(DomainMapperStateless &&) = default; - DomainMapperStateless &operator=(DomainMapperStateless &&) = default; - - [[nodiscard]] MappingStatus EvaluatePoint( - const ElementMappingData &element_data, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - MappingPointContext &context - ) const; - - [[nodiscard]] MappingStatus EvaluateVolume( - const ElementMappingData &element_data, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - VolumeMappingContext &context - ) const; - - [[nodiscard]] MappingStatus EvaluateFace( - const ElementMappingData &element_data, - mfem::FaceElementTransformations &transformation, - FaceElementSide side, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - FaceMappingContext &context - ) const; - - [[nodiscard]] MappingStatus EvaluatePointVariation( - const ElementMappingData &element_data, - const ElementDisplacementData &direction, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const MappingPointContext &base_context, - Workspace &workspace, - MappingPointVariation &variation - ) const; - - [[nodiscard]] MappingStatus EvaluateVolumeVariation( - const ElementMappingData &element_data, - const ElementDisplacementData &direction, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const VolumeMappingContext &base_context, - Workspace &workspace, - VolumeMappingVariation &variation - ) const; - - [[nodiscard]] MappingStatus EvaluateFaceVariation( - const ElementMappingData &element_data, - const ElementDisplacementData &direction, - mfem::FaceElementTransformations &transformation, - FaceElementSide side, - const mfem::IntegrationPoint &integration_point, - const FaceMappingContext &base_context, - Workspace &workspace, - FaceMappingVariation &variation - ) const; - - [[nodiscard]] bool IsCompactifiedElement(const mfem::ElementTransformation &transformation) const noexcept; - [[nodiscard]] int GetDimension() const noexcept; - [[nodiscard]] int GetVacuumElementAttribute() const noexcept; - [[nodiscard]] const compactification::ExteriorDomainMap &GetExteriorMap() const noexcept; - - private: - void ValidateElementData(const ElementMappingData &element_data) const; - - void EvaluateField( - const ElementDisplacementData &field, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - mfem::Vector &value, - mfem::DenseMatrix &jacobian - ) const; - - [[nodiscard]] MappingStatus EvaluateCompactificationCoordinate( - const ElementCompactificationData &compactification, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - Workspace &workspace, - CompactificationPointData &point_data - ) const; - - [[nodiscard]] static mfem::ElementTransformation &SelectFaceElementTransformation( - mfem::FaceElementTransformations &transformation, - FaceElementSide side - ); - - [[nodiscard]] static const mfem::IntegrationPoint &SelectFaceElementIntegrationPoint( - mfem::FaceElementTransformations &transformation, - FaceElementSide side - ); - - utils::DomainMapperStatelessOptions m_options; - std::unique_ptr m_exterior_map; - }; - class DomainMapper { - - public: - explicit DomainMapper( - const double r_star_ref, - const double r_inf_ref - ); - - explicit DomainMapper( - const mfem::GridFunction &d, - const double r_star_ref, - const double r_inf_ref - ); - - [[nodiscard]] bool is_vacuum(const mfem::ElementTransformation &T) const; - - void SetDisplacement(const mfem::GridFunction &d); - - [[nodiscard]] bool HasCompactification() const noexcept; - [[nodiscard]] bool HasDisplacementField() const noexcept; - [[nodiscard]] bool CalcIsIdentity() const; - - void ResetDisplacement(); - - void ComputeJacobian( - mfem::ElementTransformation &T, - mfem::DenseMatrix &J - ) const; - - double ComputeDetJ( - mfem::ElementTransformation &T, - const mfem::IntegrationPoint &ip - ) const; - - void ComputeMappedDiffusionTensor( - mfem::ElementTransformation &T, - mfem::DenseMatrix &D - ) const; - - void ComputeInverseJacobian( - mfem::ElementTransformation &T, - mfem::DenseMatrix &JInv - ) const; - - VolumeQuadratureContext GetQuadratureContext( - mfem::ElementTransformation &T, - const mfem::IntegrationPoint &ip - ) const; - - FaceQuadratureContext GetFaceQuadratureContext( - mfem::FaceElementTransformations &T, - const mfem::IntegrationPoint &ip - ) const; - - void GetPhysicalPoint( - mfem::ElementTransformation &T, - const mfem::IntegrationPoint &ip, - mfem::Vector &x_phys - ) const; - - void GetVectorValue( - const int i, - const mfem::IntegrationPoint &ip, - mfem::Vector &val - ) const; - - void MapHDivFluxToPhysical( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const mfem::Vector &reference_flux, - mfem::Vector &physical_flux - ) const; - - void MapPhysicalFluxToHDivReference( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const mfem::Vector &physical_flux, - mfem::Vector &reference_flux - ) const; - - void MapReferenceGradientToPhysical( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - const mfem::Vector &reference_gradient, - mfem::Vector &physical_gradient - ) const; - [[nodiscard]] const mfem::GridFunction *GetDisplacement() const; - - [[nodiscard]] double GetPhysInfRadius() const; - - [[nodiscard]] size_t GetCacheHits() const; - - [[nodiscard]] size_t GetCacheMisses() const; - - [[nodiscard]] double GetCacheHitRate() const; - - void ResetCacheStats() const; - - private: - void InitAllScratchSpaces() const; - - void ApplyKelvinMapping( - const mfem::Vector &x_ref, - mfem::Vector &x_phys - ) const; - - void ComputeKelvinJacobian( - const mfem::Vector &x_ref, - const mfem::Vector &x_disp, - const mfem::DenseMatrix &J_D, - mfem::DenseMatrix &J - ) const; - - void InvalidateCache() const; - - void UpdateElementCache(const mfem::ElementTransformation &T) const; - - private: - const mfem::GridFunction *m_d; - std::unique_ptr m_internal_d; - const int m_dim{3}; - const int m_vacuum_attr{3}; - const double m_r_star_ref{1.0}; - const double m_r_inf_ref{2.0}; - const double m_xi_clamp{0.9999}; - - mutable int m_cached_elem_id{-1}; - mutable int m_cached_elem_type{mfem::ElementTransformation::ELEMENT}; - mutable const mfem::FiniteElement *m_fe{nullptr}; - - mutable mfem::Vector m_elem_dofs; - mutable mfem::DenseMatrix m_dof_mat; - mutable mfem::DenseMatrix m_dshape; - mutable mfem::Vector m_shape; - - mutable size_t m_cache_hits{0}; - mutable size_t m_cache_misses{0}; - - mutable mfem::DenseMatrix m_J_D; - mutable mfem::DenseMatrix m_J_temp; - mutable mfem::DenseMatrix m_JInv_temp; - mutable mfem::Vector m_x_ref; - mutable mfem::Vector m_x_disp; - mutable mfem::Vector m_d_val; - - bool m_displacement_is_identity{true}; - }; +enum class FaceElementSide : uint8_t { element_1, element_2 }; + +class ElementDisplacementData { +public: + ElementDisplacementData( + const mfem::FiniteElement &element, const mfem::Vector &displacement_dofs, + mfem::Ordering::Type ordering = mfem::Ordering::byNODES); + + [[nodiscard]] const mfem::FiniteElement &GetElement() const noexcept; + [[nodiscard]] const mfem::DenseMatrix &GetDofMatrix() const noexcept; + [[nodiscard]] int GetDimension() const noexcept; + [[nodiscard]] int GetDofCount() const noexcept; + [[nodiscard]] mfem::Ordering::Type GetOrdering() const noexcept; + +private: + const mfem::FiniteElement *m_element; + mfem::DenseMatrix m_dof_matrix; + int m_dimension; + mfem::Ordering::Type m_ordering; +}; + +struct CompactificationPointData { + double coordinate{0.0}; + mfem::Vector coordinate_gradient; +}; + +[[nodiscard]] ElementDisplacementData +ElementDisplacementDataFromElementVDofs(const mfem::FiniteElement &element, + const mfem::Vector &displacement_dofs); + +class ElementCompactificationData { +public: + ElementCompactificationData(const mfem::FiniteElement &element, + const mfem::Vector &dofs); + + [[nodiscard]] const mfem::FiniteElement &GetElement() const noexcept; + [[nodiscard]] const mfem::Vector &GetDofs() const noexcept; + [[nodiscard]] int GetDofCount() const noexcept; + +private: + const mfem::FiniteElement *m_element; + mfem::Vector m_dofs; +}; + +struct ElementMappingData { + const ElementDisplacementData &displacement; + const ElementCompactificationData &compactification; +}; + +class DomainMapper { +public: + class Workspace { + public: + explicit Workspace(int dimension = 3); + + void SetDimension(int dimension); + + [[nodiscard]] int GetDimension() const noexcept; + + private: + friend class DomainMapper; + + int m_dimension; + + mfem::Vector m_shape; + mfem::DenseMatrix m_mesh_dshape; + mfem::Vector m_field_value; + mfem::DenseMatrix m_field_jacobian; + + mfem::Vector m_compactification_shape; + mfem::DenseMatrix m_compactification_dshape; + CompactificationPointData m_compactification_point; + + mfem::Vector m_reference_normal; + mfem::Vector m_mapped_normal; + mfem::DenseMatrix m_full_element_jacobian; + + mfem::Vector m_vector_temp; + mfem::DenseMatrix m_matrix_temp_1; + mfem::DenseMatrix m_matrix_temp_2; + + compactification::ExteriorMapResult m_exterior_result; + compactification::ExteriorMapVariation m_exterior_variation; + }; + +public: + DomainMapper( + utils::DomainMapperOptions options, + std::unique_ptr exterior_map); + + DomainMapper(const DomainMapper &) = delete; + DomainMapper &operator=(const DomainMapper &) = delete; + DomainMapper(DomainMapper &&) = default; + DomainMapper &operator=(DomainMapper &&) = default; + + [[nodiscard]] MappingStatus + EvaluatePoint(const ElementMappingData &element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + Workspace &workspace, MappingPointContext &context) const; + + [[nodiscard]] MappingStatus + EvaluateVolume(const ElementMappingData &element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + Workspace &workspace, VolumeMappingContext &context) const; + + [[nodiscard]] MappingStatus + EvaluateFace(const ElementMappingData &element_data, + mfem::FaceElementTransformations &transformation, + FaceElementSide side, + const mfem::IntegrationPoint &integration_point, + Workspace &workspace, FaceMappingContext &context) const; + + [[nodiscard]] MappingStatus + EvaluatePointVariation(const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + const MappingPointContext &base_context, + Workspace &workspace, + MappingPointVariation &variation) const; + + [[nodiscard]] MappingStatus + EvaluateVolumeVariation(const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + const VolumeMappingContext &base_context, + Workspace &workspace, + VolumeMappingVariation &variation) const; + + [[nodiscard]] MappingStatus EvaluateFaceVariation( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::FaceElementTransformations &transformation, FaceElementSide side, + const mfem::IntegrationPoint &integration_point, + const FaceMappingContext &base_context, Workspace &workspace, + FaceMappingVariation &variation) const; + + [[nodiscard]] bool IsCompactifiedElement( + const mfem::ElementTransformation &transformation) const noexcept; + [[nodiscard]] int GetDimension() const noexcept; + [[nodiscard]] const compactification::ExteriorDomainMap & + GetExteriorMap() const noexcept; + +private: + void ValidateElementData(const ElementMappingData &element_data) const; + + void EvaluateField(const ElementDisplacementData &field, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + Workspace &workspace, mfem::Vector &value, + mfem::DenseMatrix &jacobian) const; + + [[nodiscard]] MappingStatus EvaluateCompactificationCoordinate( + const ElementCompactificationData &compactification, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, Workspace &workspace, + CompactificationPointData &point_data) const; + + [[nodiscard]] static mfem::ElementTransformation & + SelectFaceElementTransformation( + mfem::FaceElementTransformations &transformation, FaceElementSide side); + + [[nodiscard]] static const mfem::IntegrationPoint & + SelectFaceElementIntegrationPoint( + mfem::FaceElementTransformations &transformation, FaceElementSide side); + + utils::DomainMapperOptions m_options; + std::unique_ptr m_exterior_map; +}; + +class GridFunctionMappingEvaluator { +public: + /* + * The evaluator references the supplied grid functions and caches copies of + * their element-local DOFs. Call InvalidateCache() or Refresh() after either + * grid function's values are modified. Finite-element-space sequence changes + * are detected automatically. + * + * This object owns mutable workspace and cache state and is not thread-safe. + */ + GridFunctionMappingEvaluator( + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate); + + /* + * Discard all element-local field data. The next evaluation reloads its + * requested element lazily. This operation is idempotent. + */ + void InvalidateCache() noexcept; + + /* + * Reload the currently cached element immediately. If no element has been + * evaluated yet, Refresh() is a validated no-op. If either finite-element + * space changed sequence, the old element ID is discarded and the next + * evaluation reloads lazily against the updated spaces. + */ + void Refresh(); + + [[nodiscard]] MappingStatus + EvaluatePoint(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + MappingPointContext &context); + + [[nodiscard]] MappingStatus + EvaluateVolume(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + VolumeMappingContext &context); + + [[nodiscard]] MappingStatus + EvaluateFace(mfem::FaceElementTransformations &transformation, + FaceElementSide side, + const mfem::IntegrationPoint &integration_point, + FaceMappingContext &context); + + [[nodiscard]] VolumeQuadratureContext + GetQuadratureContext(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point); + + [[nodiscard]] FaceQuadratureContext + GetFaceQuadratureContext( + mfem::FaceElementTransformations &transformation, + const mfem::IntegrationPoint &integration_point, + FaceElementSide side = FaceElementSide::element_1); + + void GetPhysicalPoint(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + mfem::Vector &physical_position); + +private: + void ValidateFieldBindings() const; + [[nodiscard]] bool InvalidateForChangedSpaces(); + void LoadElement(int element_id); + + const DomainMapper &m_mapper; + const mfem::GridFunction &m_displacement; + const mfem::GridFunction &m_compactification_coordinate; + const mfem::FiniteElementSpace *m_displacement_space; + const mfem::FiniteElementSpace *m_compactification_space; + long m_displacement_space_sequence; + long m_compactification_space_sequence; + DomainMapper::Workspace m_workspace; + + mfem::Array m_displacement_dofs; + mfem::Array m_compactification_dofs; + mfem::Vector m_element_displacement; + mfem::Vector m_element_compactification; + std::unique_ptr m_displacement_data; + std::unique_ptr m_compactification_data; + int m_cached_element_id{-1}; +}; } // namespace mean_field::mapping diff --git a/libmeanfield/interface/mean_field.cppm b/libmeanfield/interface/mean_field.cppm index 36be90c..3a1b381 100644 --- a/libmeanfield/interface/mean_field.cppm +++ b/libmeanfield/interface/mean_field.cppm @@ -6,8 +6,6 @@ export import :utils.user; export import :utils.domain; export import :physics.gravity; export import :physics.solid_body; -export import :physics.barotrope; -export import :physics.contexts; export import :boundary.contexts; export import :analysis.integral; export import :mapping.domain_mapper; diff --git a/libmeanfield/interface/operators/contexts/barotropic_closure_linearization_context.cppm b/libmeanfield/interface/operators/contexts/barotropic_closure_linearization_context.cppm index 5f037b6..529bdf9 100644 --- a/libmeanfield/interface/operators/contexts/barotropic_closure_linearization_context.cppm +++ b/libmeanfield/interface/operators/contexts/barotropic_closure_linearization_context.cppm @@ -74,7 +74,7 @@ export namespace mean_field::operators::context::barotropic { public: BarotropicClosureLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const field::FieldDofMap &densityMap, const field::FieldDofMap &enthalpyMap, const field::FieldDofMap &displacementMap @@ -103,7 +103,7 @@ export namespace mean_field::operators::context::barotropic { void VerifyPrepared() const; const fem::FEM &m_f; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; int m_densitySize{0}; int m_enthalpySize{0}; diff --git a/libmeanfield/interface/operators/contexts/gravity_field_context.cppm b/libmeanfield/interface/operators/contexts/gravity_field_context.cppm index 9ef3f03..d63fb16 100644 --- a/libmeanfield/interface/operators/contexts/gravity_field_context.cppm +++ b/libmeanfield/interface/operators/contexts/gravity_field_context.cppm @@ -49,11 +49,12 @@ export namespace mean_field::operators::context::gravity_field { bool reconstructed_operators{false}; bool rebuilt_mass_operator{false}; bool rebuilt_source_operator{false}; + bool rebuilt_divergence_operator{false}; bool refreshed_variation_state{false}; [[nodiscard]] bool DidAnyWork() const noexcept { return reconstructed_operators || rebuilt_mass_operator || rebuilt_source_operator || - refreshed_variation_state; + rebuilt_divergence_operator || refreshed_variation_state; } }; @@ -61,7 +62,7 @@ export namespace mean_field::operators::context::gravity_field { public: GravityFieldGeometryContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper + const mapping::DomainMapper &domain_mapper ); GravityFieldGeometryContext(const GravityFieldGeometryContext &) = delete; @@ -77,6 +78,8 @@ export namespace mean_field::operators::context::gravity_field { [[nodiscard]] const PreparedMappedHDivMassOperator &GetMassOperator() const; [[nodiscard]] const PreparedMappedGravitySourceOperator &GetSourceOperator() const; + [[nodiscard]] const mfem::Operator &GetDivergenceOperator() const; + [[nodiscard]] const mfem::Operator &GetTransposeDivergenceOperator() const; [[nodiscard]] const mfem::Vector &GetDisplacementTrue() const; [[nodiscard]] const field::FieldDofMap &GetDisplacementMap() const noexcept; [[nodiscard]] DiscretizationRevision GetDiscretizationRevision() const noexcept; @@ -85,10 +88,12 @@ export namespace mean_field::operators::context::gravity_field { private: const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domain_mapper; + const mapping::DomainMapper &m_domain_mapper; std::unique_ptr m_mass_operator; std::unique_ptr m_source_operator; + std::unique_ptr m_divergence_operator; + std::unique_ptr m_transpose_divergence_operator; field::FieldDofMap m_displacement_map; mfem::Vector m_displacement_true; @@ -113,7 +118,7 @@ export namespace mean_field::operators::context::gravity_field { public: GravityFieldLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper + const mapping::DomainMapper &domain_mapper ); GravityFieldLinearizationContext(const GravityFieldLinearizationContext &) = delete; diff --git a/libmeanfield/interface/operators/contexts/hydrostatic_equilibrium_context.cppm b/libmeanfield/interface/operators/contexts/hydrostatic_equilibrium_context.cppm index d5d8b26..27b3482 100644 --- a/libmeanfield/interface/operators/contexts/hydrostatic_equilibrium_context.cppm +++ b/libmeanfield/interface/operators/contexts/hydrostatic_equilibrium_context.cppm @@ -96,7 +96,7 @@ export namespace mean_field::operators::context::hydrostatic { public: HydrostaticEquilibriumContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper + const mapping::DomainMapper &domainMapper ); HydrostaticEquilibriumContext(const HydrostaticEquilibriumContext &) = delete; @@ -138,7 +138,7 @@ export namespace mean_field::operators::context::hydrostatic { void VerifyPrepared() const; const fem::FEM &m_f; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; field::FieldDofMap m_enthalpyMap; field::FieldDofMap m_gravityPotentialMap; diff --git a/libmeanfield/interface/operators/contexts/pressure_force_context.cppm b/libmeanfield/interface/operators/contexts/pressure_force_context.cppm index 9309883..3c99384 100644 --- a/libmeanfield/interface/operators/contexts/pressure_force_context.cppm +++ b/libmeanfield/interface/operators/contexts/pressure_force_context.cppm @@ -81,7 +81,7 @@ export namespace mean_field::operators::context::pressure_force { public: PressureForceLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const field::FieldDofMap &enthalpyMap, const field::FieldDofMap &displacementMap ); diff --git a/libmeanfield/interface/operators/contexts/rotation_displacement_force_context.cppm b/libmeanfield/interface/operators/contexts/rotation_displacement_force_context.cppm index 95971d6..4ea9560 100644 --- a/libmeanfield/interface/operators/contexts/rotation_displacement_force_context.cppm +++ b/libmeanfield/interface/operators/contexts/rotation_displacement_force_context.cppm @@ -78,7 +78,7 @@ export namespace mean_field::operators::context::rotational_displacement_force { public: RotationalDisplacementForceLinearizationContext( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper + const mapping::DomainMapper &domainMapper ); RotationalDisplacementForceLinearizationContext(const RotationalDisplacementForceLinearizationContext &) = diff --git a/libmeanfield/interface/operators/gravity_field.cppm b/libmeanfield/interface/operators/gravity_field.cppm index 24f5173..99c87ca 100644 --- a/libmeanfield/interface/operators/gravity_field.cppm +++ b/libmeanfield/interface/operators/gravity_field.cppm @@ -13,7 +13,7 @@ export namespace mean_field::operators { public: GravityFieldOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, + const mapping::DomainMapper &domain_mapper, context::gravity_field::GravityFieldLinearizationContext &linearization_context, const mfem::Array &state_offsets, GravityFieldJacobianOperator &jacobian @@ -55,7 +55,7 @@ export namespace mean_field::operators { private: fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domain_mapper; + const mapping::DomainMapper &m_domain_mapper; context::gravity_field::GravityFieldLinearizationContext &m_linearization_context; mfem::Array m_state_offsets; mfem::Array m_residual_offsets; @@ -112,4 +112,39 @@ export namespace mean_field::operators { context::gravity_field::GravityFieldGeometryContext &m_gravity_field_geometry_context; mfem::Vector m_displacement; }; + + class ReducedGravityFieldPreconditioner final : public mfem::Solver { + public: + ReducedGravityFieldPreconditioner( + const fem::FEM &f, + const context::gravity_field::GravityFieldGeometryContext &geometry_context + ); + + ReducedGravityFieldPreconditioner(const ReducedGravityFieldPreconditioner &) = delete; + ReducedGravityFieldPreconditioner &operator=(const ReducedGravityFieldPreconditioner &) = delete; + ReducedGravityFieldPreconditioner(ReducedGravityFieldPreconditioner &&) = delete; + ReducedGravityFieldPreconditioner &operator=(ReducedGravityFieldPreconditioner &&) = delete; + + void SetOperator(const mfem::Operator &gravity_operator) override; + + void Mult( + const mfem::Vector &right_hand_side, + mfem::Vector &action + ) const override; + + [[nodiscard]] const mfem::Array &GetOffsets() const noexcept; + + private: + field::FieldDofMap m_flux_map; + field::FieldDofMap m_potential_map; + mfem::Array m_offsets; + mfem::Array m_empty_tdofs; + + std::unique_ptr m_mass_preconditioner; + std::unique_ptr m_schur; + std::unique_ptr m_potential_preconditioner; + + mutable mfem::Vector m_potential_rhs_true; + mutable mfem::Vector m_potential_action_true; + }; } // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/gravity_field_jacobian.cppm b/libmeanfield/interface/operators/gravity_field_jacobian.cppm index 2a0f7b0..58fc1bb 100644 --- a/libmeanfield/interface/operators/gravity_field_jacobian.cppm +++ b/libmeanfield/interface/operators/gravity_field_jacobian.cppm @@ -11,7 +11,7 @@ export namespace mean_field::operators { public: GravityFieldJacobianOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, + const mapping::DomainMapper &domain_mapper, const context::gravity_field::GravityFieldLinearizationContext &linearization_context, const mfem::Array &state_offsets, const mfem::Array &residual_offsets @@ -27,7 +27,7 @@ export namespace mean_field::operators { private: fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domain_mapper; + const mapping::DomainMapper &m_domain_mapper; const context::gravity_field::GravityFieldLinearizationContext &m_linearization_context; mfem::Array m_state_offsets; mfem::Array m_residual_offsets; diff --git a/libmeanfield/interface/operators/kernels/barotropic_closure_kernels.cppm b/libmeanfield/interface/operators/kernels/barotropic_closure_kernels.cppm index 67cd172..bca119f 100644 --- a/libmeanfield/interface/operators/kernels/barotropic_closure_kernels.cppm +++ b/libmeanfield/interface/operators/kernels/barotropic_closure_kernels.cppm @@ -22,7 +22,7 @@ export namespace mean_field::operators::kernels { */ void apply_barotropic_closure( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const mfem::Vector &densityTrue, const mfem::Vector &enthalpyTrue, @@ -32,7 +32,7 @@ export namespace mean_field::operators::kernels { void apply_barotropic_closure_density_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const mfem::Vector &densityVariationTrue, const mfem::Vector &displacementTrue, @@ -41,7 +41,7 @@ export namespace mean_field::operators::kernels { void apply_barotropic_closure_enthalpy_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &enthalpyVariationTrue, @@ -51,7 +51,7 @@ export namespace mean_field::operators::kernels { void apply_barotropic_closure_displacement_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const mfem::Vector &baseDensityTrue, const mfem::Vector &baseEnthalpyTrue, diff --git a/libmeanfield/interface/operators/kernels/gravity_displacement_force_kernels.cppm b/libmeanfield/interface/operators/kernels/gravity_displacement_force_kernels.cppm index 446496e..6a27b4b 100644 --- a/libmeanfield/interface/operators/kernels/gravity_displacement_force_kernels.cppm +++ b/libmeanfield/interface/operators/kernels/gravity_displacement_force_kernels.cppm @@ -10,7 +10,7 @@ export import :mapping.domain_mapper; export namespace mean_field::operators::kernels { void apply_gravity_displacement_force_residual( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const mfem::Vector &densityTrue, const mfem::Vector &gravityGradientTrue, const mfem::Vector &displacementTrue, @@ -19,7 +19,7 @@ export namespace mean_field::operators::kernels { void apply_gravity_displacement_force_density_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const mfem::Vector &densityVariationTrue, const mfem::Vector &baseGravityGradientTrue, const mfem::Vector &displacementTrue, @@ -28,7 +28,7 @@ export namespace mean_field::operators::kernels { void apply_gravity_displacement_force_gradient_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const mfem::Vector &baseDensityTrue, const mfem::Vector &gravityGradientVariationTrue, const mfem::Vector &displacementTrue, @@ -37,7 +37,7 @@ export namespace mean_field::operators::kernels { void apply_gravity_displacement_force_displacement_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const mfem::Vector &baseDensityTrue, const mfem::Vector &baseGravityGradientTrue, const mfem::Vector &displacementVariationTrue, @@ -47,7 +47,7 @@ export namespace mean_field::operators::kernels { void apply_gravity_displacement_force_complete_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const mfem::Vector &baseDensityTrue, const mfem::Vector &densityVariationTrue, const mfem::Vector &baseGravityGradientTrue, diff --git a/libmeanfield/interface/operators/kernels/gravity_kernels.cppm b/libmeanfield/interface/operators/kernels/gravity_kernels.cppm index d5a1025..e122b5c 100644 --- a/libmeanfield/interface/operators/kernels/gravity_kernels.cppm +++ b/libmeanfield/interface/operators/kernels/gravity_kernels.cppm @@ -15,7 +15,7 @@ export namespace mean_field::operators::kernels { void apply_mapped_hdiv_mass( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, + const mapping::DomainMapper &domain_mapper, const mfem::Vector &gravity_gradient_true, const mfem::Vector &displacement_true, mfem::Vector &action @@ -23,7 +23,7 @@ export namespace mean_field::operators::kernels { void apply_mapped_source( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, + const mapping::DomainMapper &domain_mapper, const mfem::Vector &density_true, const mfem::Vector &displacement_true, mfem::Vector &action @@ -31,7 +31,7 @@ export namespace mean_field::operators::kernels { void apply_mapped_hdiv_mass_variation( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, + const mapping::DomainMapper &domain_mapper, const mfem::Vector &gravity_gradient_true, const mfem::Vector &displacement_true, const mfem::Vector &displacement_variation_true, @@ -40,7 +40,7 @@ export namespace mean_field::operators::kernels { void apply_mapped_source_variation( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, + const mapping::DomainMapper &domain_mapper, const mfem::Vector &density_true, const mfem::Vector &displacement_true, const mfem::Vector &displacement_variation_true, diff --git a/libmeanfield/interface/operators/kernels/hydrostatic_equilibrium_kernels.cppm b/libmeanfield/interface/operators/kernels/hydrostatic_equilibrium_kernels.cppm index f97f606..722a35f 100644 --- a/libmeanfield/interface/operators/kernels/hydrostatic_equilibrium_kernels.cppm +++ b/libmeanfield/interface/operators/kernels/hydrostatic_equilibrium_kernels.cppm @@ -11,7 +11,7 @@ export import :physics.rigid_rotation; export namespace mean_field::operators::kernels { void apply_hydrostatic_equilibrium( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const physics::RigidRotation &rotation, const mfem::Vector &enthalpyTrue, const mfem::Vector &potentialTrue, @@ -22,7 +22,7 @@ export namespace mean_field::operators::kernels { void apply_hydrostatic_equilibrium_enthalpy_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const mfem::Vector &enthalpyVariationTrue, const mfem::Vector &displacementTrue, mfem::Vector &action @@ -30,7 +30,7 @@ export namespace mean_field::operators::kernels { void apply_hydrostatic_equilibrium_potential_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const mfem::Vector &potentialVariationTrue, const mfem::Vector &displacementTrue, mfem::Vector &action @@ -38,7 +38,7 @@ export namespace mean_field::operators::kernels { void apply_hydrostatic_equilibrium_constant_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, double constantVariation, const mfem::Vector &displacementTrue, mfem::Vector &action @@ -46,7 +46,7 @@ export namespace mean_field::operators::kernels { void apply_hydrostatic_equilibrium_displacement_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const physics::RigidRotation &rotation, const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &basePotentialTrue, @@ -58,7 +58,7 @@ export namespace mean_field::operators::kernels { void apply_hydrostatic_equilibrium_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const physics::RigidRotation &rotation, const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &basePotentialTrue, diff --git a/libmeanfield/interface/operators/kernels/pressure_force_kernels.cppm b/libmeanfield/interface/operators/kernels/pressure_force_kernels.cppm index f3cf927..0c422f0 100644 --- a/libmeanfield/interface/operators/kernels/pressure_force_kernels.cppm +++ b/libmeanfield/interface/operators/kernels/pressure_force_kernels.cppm @@ -11,7 +11,7 @@ export import :eos.polytrope; export namespace mean_field::operators::kernels { void apply_pressure_force_residual( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const mfem::Vector &enthalpyTrue, const mfem::Vector &displacementTrue, @@ -20,7 +20,7 @@ export namespace mean_field::operators::kernels { void apply_pressure_force_enthalpy_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &enthalpyVariationTrue, @@ -30,7 +30,7 @@ export namespace mean_field::operators::kernels { void apply_pressure_force_displacement_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const mfem::Vector &baseEnthalpyTrue, const mfem::Vector &displacementVariationTrue, diff --git a/libmeanfield/interface/operators/kernels/rotation_displacement_force_kernels.cppm b/libmeanfield/interface/operators/kernels/rotation_displacement_force_kernels.cppm index cedf8cc..91fe8d4 100644 --- a/libmeanfield/interface/operators/kernels/rotation_displacement_force_kernels.cppm +++ b/libmeanfield/interface/operators/kernels/rotation_displacement_force_kernels.cppm @@ -25,7 +25,7 @@ export namespace mean_field::operators::kernels { */ void apply_rotational_displacement_force_residual( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const physics::RigidRotation &rotation, const mfem::Vector &densityTrue, const mfem::Vector &displacementTrue, @@ -34,7 +34,7 @@ export namespace mean_field::operators::kernels { void apply_rotational_displacement_force_density_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const physics::RigidRotation &rotation, const mfem::Vector &densityVariationTrue, const mfem::Vector &displacementTrue, @@ -43,7 +43,7 @@ export namespace mean_field::operators::kernels { void apply_rotational_displacement_force_displacement_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const physics::RigidRotation &rotation, const mfem::Vector &baseDensityTrue, const mfem::Vector &displacementVariationTrue, @@ -53,7 +53,7 @@ export namespace mean_field::operators::kernels { void apply_rotational_displacement_force_complete_action( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const physics::RigidRotation &rotation, const mfem::Vector &baseDensityTrue, const mfem::Vector &densityVariationTrue, diff --git a/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm b/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm index e3b5455..08c8dd6 100644 --- a/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm +++ b/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm @@ -26,7 +26,7 @@ export namespace mean_field::operators { public: PreparedBarotropicClosureOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState ); @@ -71,7 +71,7 @@ export namespace mean_field::operators { PreparedBarotropicClosureOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, ConstructionData constructionData ); @@ -100,7 +100,7 @@ export namespace mean_field::operators { }; const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; const eos::Polytrope &m_equationOfState; field::FieldDofMap m_densityMap; diff --git a/libmeanfield/interface/operators/prepared_displacement_operator.cppm b/libmeanfield/interface/operators/prepared_displacement_operator.cppm index c190806..04ebe25 100644 --- a/libmeanfield/interface/operators/prepared_displacement_operator.cppm +++ b/libmeanfield/interface/operators/prepared_displacement_operator.cppm @@ -85,7 +85,7 @@ export namespace mean_field::operators { public: PreparedDisplacementResidualOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &barotrope, const context::gravity_field::GravityFieldLinearizationContext &gravityContext ); @@ -158,7 +158,7 @@ export namespace mean_field::operators { void VerifyPrepared() const; const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; const context::gravity_field::GravityFieldLinearizationContext &m_gravityContext; PreparedPressureForceOperator m_pressureOperator; diff --git a/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm b/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm index 90be52c..20e92c7 100644 --- a/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm +++ b/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm @@ -37,7 +37,7 @@ export namespace mean_field::operators { public: PreparedGravityDisplacementForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const context::gravity_field::GravityFieldLinearizationContext &gravityContext ); @@ -107,7 +107,7 @@ export namespace mean_field::operators { void VerifyPrepared() const; const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; const context::gravity_field::GravityFieldLinearizationContext &m_gravityContext; context::gravity_field::GravityFieldRevisions m_preparedRevisions; diff --git a/libmeanfield/interface/operators/prepared_gravity_source.cppm b/libmeanfield/interface/operators/prepared_gravity_source.cppm index 9d6b9a8..391a4d6 100644 --- a/libmeanfield/interface/operators/prepared_gravity_source.cppm +++ b/libmeanfield/interface/operators/prepared_gravity_source.cppm @@ -14,7 +14,7 @@ export namespace mean_field::operators { public: PreparedMappedGravitySourceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper + const mapping::DomainMapper &domain_mapper ); void Prepare(const mfem::Vector &displacement); @@ -55,7 +55,7 @@ export namespace mean_field::operators { }; const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domain_mapper; + const mapping::DomainMapper &m_domain_mapper; field::FieldDofMap m_density_map; field::FieldDofMap m_potential_map; diff --git a/libmeanfield/interface/operators/prepared_hdiv_mass.cppm b/libmeanfield/interface/operators/prepared_hdiv_mass.cppm index 00a489d..fa2fb6b 100644 --- a/libmeanfield/interface/operators/prepared_hdiv_mass.cppm +++ b/libmeanfield/interface/operators/prepared_hdiv_mass.cppm @@ -13,7 +13,7 @@ export namespace mean_field::operators { public: PreparedMappedHDivMassOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper + const mapping::DomainMapper &domain_mapper ); void Prepare(const mfem::Vector &displacement); @@ -21,6 +21,8 @@ export namespace mean_field::operators { const mfem::Vector &gravity_gradient, mfem::Vector &action ) const override; + void AssembleDiagonal(mfem::Vector &diagonal) const override; + void AssembleTrueDiagonal(mfem::Vector &diagonal) const; [[nodiscard]] bool IsPrepared() const noexcept; [[nodiscard]] std::uint64_t GetPreparationCount() const noexcept; @@ -30,7 +32,7 @@ export namespace mean_field::operators { private: const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domain_mapper; + const mapping::DomainMapper &m_domain_mapper; field::FieldDofMap m_flux_map; field::FieldDofMap m_displacement_map; @@ -40,9 +42,11 @@ export namespace mean_field::operators { std::unique_ptr m_stellar_mass_coefficient; std::unique_ptr m_vacuum_mass_coefficient; - std::unique_ptr m_mass_form; + std::unique_ptr m_stellar_mass_form; + std::unique_ptr m_vacuum_mass_form; mutable mfem::Vector m_flux_true; mutable mfem::Vector m_action_true; + mutable mfem::Vector m_domain_action_true; mfem::Vector m_displacement_true; std::uint64_t m_preparation_count{0}; bool m_is_prepared{false}; diff --git a/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm b/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm index 45ece03..b7b2121 100644 --- a/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm +++ b/libmeanfield/interface/operators/prepared_hydrostatic_equilibrium_operator.cppm @@ -84,7 +84,7 @@ export namespace mean_field::operators { public: PreparedHydrostaticEquilibriumOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper + const mapping::DomainMapper &domainMapper ); PreparedHydrostaticEquilibriumOperator(const PreparedHydrostaticEquilibriumOperator &) = delete; @@ -217,7 +217,7 @@ export namespace mean_field::operators { void VerifyPrepared() const; const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; context::hydrostatic::HydrostaticEquilibriumContext m_context; diff --git a/libmeanfield/interface/operators/prepared_mass_normalization.cppm b/libmeanfield/interface/operators/prepared_mass_normalization.cppm index 4a8b4da..4d10afe 100644 --- a/libmeanfield/interface/operators/prepared_mass_normalization.cppm +++ b/libmeanfield/interface/operators/prepared_mass_normalization.cppm @@ -68,7 +68,7 @@ export namespace mean_field::operators { public: PreparedMassNormalizationOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const context::gravity_field::GravityFieldLinearizationContext &gravityContext ); @@ -148,7 +148,7 @@ export namespace mean_field::operators { [[nodiscard]] double GlobalSum(double localValue) const; const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; const context::gravity_field::GravityFieldLinearizationContext &m_gravityContext; std::vector m_elements; diff --git a/libmeanfield/interface/operators/prepared_pressure_force.cppm b/libmeanfield/interface/operators/prepared_pressure_force.cppm index 6dd2589..ab2c6ef 100644 --- a/libmeanfield/interface/operators/prepared_pressure_force.cppm +++ b/libmeanfield/interface/operators/prepared_pressure_force.cppm @@ -75,7 +75,7 @@ export namespace mean_field::operators { public: PreparedPressureForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState ); @@ -155,7 +155,7 @@ export namespace mean_field::operators { PreparedPressureForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, ConstructionData constructionData ); @@ -217,7 +217,7 @@ export namespace mean_field::operators { const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; const eos::Polytrope &m_equationOfState; diff --git a/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm b/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm index 76cd209..453b521 100644 --- a/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm +++ b/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm @@ -42,7 +42,7 @@ export namespace mean_field::operators { public: PreparedRotationalDisplacementForceOperator( const fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper + const mapping::DomainMapper &domainMapper ); PreparedRotationalDisplacementForceOperator(const PreparedRotationalDisplacementForceOperator &) = delete; @@ -105,7 +105,7 @@ export namespace mean_field::operators { void VerifyPrepared() const; const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domainMapper; + const mapping::DomainMapper &m_domainMapper; context::rotational_displacement_force::RotationalDisplacementForceLinearizationContext m_context; diff --git a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm index ab5dd9d..bbb2ede 100644 --- a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm +++ b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm @@ -76,14 +76,14 @@ export namespace mean_field::operators { public: PreparedStellarEquilibriumOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, double targetMass ); PreparedStellarEquilibriumOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const models::StellarModel &stellarModel ); @@ -130,7 +130,7 @@ export namespace mean_field::operators { PreparedStellarEquilibriumOperator( fem::FEM &f, - const mapping::DomainMapperStateless &domainMapper, + const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, double targetMass, ConstructionData constructionData diff --git a/libmeanfield/interface/physics/barotrope.cppm b/libmeanfield/interface/physics/barotrope.cppm deleted file mode 100644 index 516373d..0000000 --- a/libmeanfield/interface/physics/barotrope.cppm +++ /dev/null @@ -1,162 +0,0 @@ -module; - -#include -#include -#include - -export module mean_field:physics.barotrope; - -export namespace mean_field::physics { - class PolytropicBarotrope final { - public: - PolytropicBarotrope( - const double polytropic_index, - const double polytropic_constant - ) - : m_polytropic_index(polytropic_index), - m_polytropic_constant(polytropic_constant), - m_enthalpy_scale((polytropic_index + 1.0) * polytropic_constant) { - if (!std::isfinite(polytropic_index) || polytropic_index < 1.0) { - throw std::invalid_argument( - std::format( - "The differentiable polytropic closure requires a " - "finite polytropic index greater than or equal to one. " - "Instead a value of {} has been provided", - polytropic_index - ) - ); - } - - if (!std::isfinite(polytropic_constant) || polytropic_constant <= 0.0) { - throw std::invalid_argument( - std::format( - "The polytropic constant must be finite and positive. " - "Instead a value of {} has been provided", - polytropic_constant - ) - ); - } - }; - - [[nodiscard]] double polytropic_index() const noexcept { - return m_polytropic_index; - } - - [[nodiscard]] double polytropic_constant() const noexcept { - return m_polytropic_constant; - } - - [[nodiscard]] double enthalpy_scale() const noexcept { - return m_enthalpy_scale; - } - - [[nodiscard]] double pressure_from_density(const double density) const { - validate_nonnegativity(density, "density"); - if (density == 0.0) { - return 0.0; - } - - return m_polytropic_constant * std::pow(density, 1.0 + 1.0 / m_polytropic_index); - } - - [[nodiscard]] double enthalpy_from_density(const double density) const { - validate_nonnegativity(density, "density"); - if (density == 0.0) { - return 0.0; - } - - return m_enthalpy_scale * std::pow(density, 1.0 / m_polytropic_index); - } - - [[nodiscard]] double density_from_enthalpy(const double enthalpy) const { - validate_finite(enthalpy, "enthalpy"); - - if (enthalpy <= 0.0) { - return 0.0; - } - - return std::pow(enthalpy / m_enthalpy_scale, m_polytropic_index); - } - - [[nodiscard]] double pressure_from_enthalpy(const double enthalpy) const { - validate_finite(enthalpy, "enthalpy"); - - if (enthalpy <= 0.0) { - return 0.0; - } - - return density_from_enthalpy(enthalpy) * enthalpy / (m_polytropic_index + 1.0); - } - - [[nodiscard]] double density_derivative_from_enthalpy(const double enthalpy) const { - validate_finite(enthalpy, "enthalpy"); - if (enthalpy < 0.0) { - return 0.0; - } - - if (enthalpy == 0.0) { - return m_polytropic_index == 1.0 ? 1.0 / m_enthalpy_scale : 0.0; - } - - return m_polytropic_index / m_enthalpy_scale * - std::pow(enthalpy / m_enthalpy_scale, m_polytropic_index - 1.0); - } - - [[nodiscard]] double pressure_derivative_from_enthalpy(const double enthalpy) const { - validate_finite(enthalpy, "enthalpy"); - - if (enthalpy <= 0.0) { - return 0.0; - } - - return density_from_enthalpy(enthalpy); - } - - [[nodiscard]] double pressure_derivative_from_density(const double density) const { - validate_nonnegativity(density, "density"); - if (density == 0.0) { - return 0.0; - } - - return m_polytropic_constant * (1.0 + 1.0 / m_polytropic_index) * - std::pow(density, 1.0 / m_polytropic_index); - } - - private: - static void validate_finite( - const double value, - const char *quantity - ) { - if (!std::isfinite(value)) { - throw std::domain_error( - std::format( - "The {} must be finite. Instead a value of {} has been " - "provided", - quantity, value - ) - ); - } - } - - static void validate_nonnegativity( - const double value, - const char *quantity - ) { - validate_finite(value, quantity); - if (value < 0.0) { - throw std::domain_error( - std::format( - "The {} must be non-negative. Instead a value of {} " - "has been " - "provided", - quantity, value - ) - ); - } - } - - double m_polytropic_index; - double m_polytropic_constant; - double m_enthalpy_scale; - }; -} // namespace mean_field::physics \ No newline at end of file diff --git a/libmeanfield/interface/physics/context.cppm b/libmeanfield/interface/physics/context.cppm deleted file mode 100644 index 80e405d..0000000 --- a/libmeanfield/interface/physics/context.cppm +++ /dev/null @@ -1,29 +0,0 @@ -module; -#include -#include - -export module mean_field:physics.contexts; -export import :mapping.coefficients; - -export namespace mean_field::physics { - struct GravityContext { - std::unique_ptr m_form; - std::unique_ptr b_form; - - std::unique_ptr block_A; - - std::unique_ptr prec_M; - std::unique_ptr prec_Phi; - std::unique_ptr block_prec; - - std::unique_ptr minres; - - mfem::Array stellar_mask; - - std::unique_ptr BT; - std::unique_ptr Schur; - - std::unique_ptr mapped_hdiv_mass_coeff; - std::unique_ptr source_form; - }; -} // namespace mean_field::physics diff --git a/libmeanfield/interface/physics/gravity.cppm b/libmeanfield/interface/physics/gravity.cppm index 6fa5f90..28aba5c 100644 --- a/libmeanfield/interface/physics/gravity.cppm +++ b/libmeanfield/interface/physics/gravity.cppm @@ -16,27 +16,13 @@ export namespace mean_field::physics { } }; - GravitySolution grav_potential( - fem::FEM &f, - const utils::Args &args, - const mfem::GridFunction &rho, - bool phi_warm = false - ); - - GravitySolution grav_potential_new( + GravitySolution solve_gravity_field( fem::FEM &f, const utils::Args &args, const mfem::GridFunction &rho, const mfem::GridFunction &displacement ); - mfem::GridFunction get_potential( - fem::FEM &fem, - const utils::Args &args, - const mfem::GridFunction &rho, - bool warm = false - ); - mfem::DenseMatrix compute_quadrupole_moment_tensor( const fem::FEM &fem, const mfem::GridFunction &rho, @@ -49,5 +35,4 @@ export namespace mean_field::physics { const mfem::Vector &phys_x ); - void update_stiffness_matrix(fem::FEM &fem); } // namespace mean_field::physics diff --git a/libmeanfield/interface/utils/domain.cppm b/libmeanfield/interface/utils/domain.cppm index 164a4d3..cc3e7c6 100644 --- a/libmeanfield/interface/utils/domain.cppm +++ b/libmeanfield/interface/utils/domain.cppm @@ -11,1082 +11,1179 @@ module; export module mean_field:utils.domain; export namespace mean_field::utils::domain { - struct Domain { }; +struct Domain {}; - struct Core final : public Domain { - static constexpr std::string_view name = "core"; - }; +struct Core final : public Domain { + static constexpr std::string_view name = "core"; +}; - struct Envelope final : public Domain { - static constexpr std::string_view name = "envelope"; - }; +struct Envelope final : public Domain { + static constexpr std::string_view name = "envelope"; +}; - struct Vacuum final : public Domain { - static constexpr std::string_view name = "vacuum"; - }; +struct Vacuum final : public Domain { + static constexpr std::string_view name = "vacuum"; +}; - struct Boundary { }; +struct Boundary {}; - struct StellarSurface final : public Boundary { - static constexpr std::string_view name = "stellar_surface"; - }; +struct StellarSurface final : public Boundary { + static constexpr std::string_view name = "stellar_surface"; +}; - struct InfinitySurface final : public Boundary { - static constexpr std::string_view name = "infinity_surface"; - }; +struct InfinitySurface final : public Boundary { + static constexpr std::string_view name = "infinity_surface"; +}; - template - concept IsDomain = std::is_base_of_v; +template +concept IsDomain = std::is_base_of_v; - template - concept IsBoundary = std::is_base_of_v; +template +concept IsBoundary = std::is_base_of_v; - template struct DomainSet { }; +template struct DomainSet {}; - template constexpr bool is_domain_set_v = false; +template constexpr bool is_domain_set_v = false; - template constexpr bool is_domain_set_v> = true; +template +constexpr bool is_domain_set_v> = true; - template - concept IsDomainSet = is_domain_set_v; +template +concept IsDomainSet = is_domain_set_v; - template - concept IsDomainOrSet = IsDomain || IsDomainSet; +template +concept IsDomainOrSet = IsDomain || IsDomainSet; - using Stellar = DomainSet; - using All = DomainSet; +using Stellar = DomainSet; +using All = DomainSet; - struct DomainRelation { }; +struct DomainRelation {}; - template struct Inscribed final : public DomainRelation { - using inner_type = A; - using outer_type = B; +template +struct Inscribed final : public DomainRelation { + using inner_type = A; + using outer_type = B; - static constexpr std::string_view name = "inscribed"; - }; + static constexpr std::string_view name = "inscribed"; +}; - template struct Connected final : public DomainRelation { - using domain_type = A; +template struct Connected final : public DomainRelation { + using domain_type = A; - static constexpr std::string_view name = "connected"; - }; + static constexpr std::string_view name = "connected"; +}; - template - concept IsRelation = std::is_base_of_v; +template +concept IsRelation = std::is_base_of_v; - template struct Material { - using domain_type = D; +template struct Material { + using domain_type = D; - static constexpr int id = Id; - }; + static constexpr int id = Id; +}; - template struct BoundaryAttribute { - using boundary_type = B; +template struct BoundaryAttribute { + using boundary_type = B; - static constexpr int id = Id; - }; + static constexpr int id = Id; +}; - template constexpr bool is_material_v = false; +template constexpr bool is_material_v = false; - template constexpr bool is_material_v> = true; +template +constexpr bool is_material_v> = true; - template - concept IsMaterial = is_material_v; +template +concept IsMaterial = is_material_v; - template constexpr bool is_boundary_attr_v = false; +template constexpr bool is_boundary_attr_v = false; - template constexpr bool is_boundary_attr_v> = true; +template +constexpr bool is_boundary_attr_v> = true; - template - concept IsBoundaryAttr = is_boundary_attr_v; +template +concept IsBoundaryAttr = is_boundary_attr_v; - struct MaterialDescriptor { - std::string_view name; - int id; - }; +struct MaterialDescriptor { + std::string_view name; + int id; +}; - struct BoundaryDescriptor { - std::string_view name; - int id; - }; +struct BoundaryDescriptor { + std::string_view name; + int id; +}; - template - [[nodiscard]] - consteval bool material_ids_are_unique() noexcept { - constexpr std::array materialIds{MaterialTs::id...}; +template +[[nodiscard]] +consteval bool material_ids_are_unique() noexcept { + constexpr std::array materialIds{ + MaterialTs::id...}; - for (std::size_t firstIndex = 0; firstIndex < materialIds.size(); ++firstIndex) { - for (std::size_t secondIndex = firstIndex + 1; secondIndex < materialIds.size(); ++secondIndex) { - if (materialIds[firstIndex] == materialIds[secondIndex]) { - return false; - } - } - } + for (std::size_t firstIndex = 0; firstIndex < materialIds.size(); + ++firstIndex) { + for (std::size_t secondIndex = firstIndex + 1; + secondIndex < materialIds.size(); ++secondIndex) { + if (materialIds[firstIndex] == materialIds[secondIndex]) { + return false; + } + } + } - return true; + return true; +} + +template +[[nodiscard]] +consteval bool boundary_ids_are_unique() noexcept { + constexpr std::array boundaryIds{ + BoundaryTs::id...}; + + for (std::size_t firstIndex = 0; firstIndex < boundaryIds.size(); + ++firstIndex) { + for (std::size_t secondIndex = firstIndex + 1; + secondIndex < boundaryIds.size(); ++secondIndex) { + if (boundaryIds[firstIndex] == boundaryIds[secondIndex]) { + return false; + } + } + } + + return true; +} + +template struct MaterialDomainsAreUnique; + +template <> struct MaterialDomainsAreUnique<> : std::true_type {}; + +template +struct MaterialDomainsAreUnique : std::true_type {}; + +template +struct MaterialDomainsAreUnique + : std::bool_constant< + (!std::is_same_v && + ...) && + MaterialDomainsAreUnique::value> {}; + +template struct BoundaryTypesAreUnique; + +template <> struct BoundaryTypesAreUnique<> : std::true_type {}; + +template +struct BoundaryTypesAreUnique : std::true_type {}; + +template +struct BoundaryTypesAreUnique + : std::bool_constant< + (!std::is_same_v && + ...) && + BoundaryTypesAreUnique::value> {}; + +template +concept HaveUniqueMaterialIds = material_ids_are_unique(); + +template +concept HaveUniqueMaterialDomains = + MaterialDomainsAreUnique::value; + +template +concept HaveUniqueBoundaryIds = boundary_ids_are_unique(); + +template +concept HaveUniqueBoundaryTypes = BoundaryTypesAreUnique::value; + +template + requires(HaveUniqueMaterialIds && + HaveUniqueMaterialDomains) +struct MaterialList { + static constexpr std::size_t count = sizeof...(MaterialTs); + + [[nodiscard]] + static constexpr std::array + descriptors() noexcept { + return {MaterialDescriptor{.name = MaterialTs::domain_type::name, + .id = MaterialTs::id}...}; + } +}; + +template + requires(HaveUniqueBoundaryIds && + HaveUniqueBoundaryTypes) +struct BoundaryList { + static constexpr std::size_t count = sizeof...(BoundaryTs); + + [[nodiscard]] + static constexpr std::array + descriptors() noexcept { + return {BoundaryDescriptor{.name = BoundaryTs::boundary_type::name, + .id = BoundaryTs::id}...}; + } +}; + +template +struct DomainMaterialResolver; + +template +struct DomainMaterialResolver> { + static constexpr bool registered = + (std::is_same_v || ...); + + [[nodiscard]] + static constexpr bool contains_attribute(int materialId) noexcept { + return ((std::is_same_v && + MaterialTs::id == materialId) || + ...); + } + + [[nodiscard]] + static consteval int attribute() { + static_assert(registered, + "Requested domain is not registered in this schema."); + + int result = 0; + ((std::is_same_v + ? result = MaterialTs::id + : result), + ...); + return result; + } +}; + +template +struct DomainMaterialResolver, + MaterialList> { + static constexpr bool registered = + (DomainMaterialResolver>::registered && + ...); + + [[nodiscard]] + static constexpr bool contains_attribute(int materialId) noexcept { + return (DomainMaterialResolver>:: + contains_attribute(materialId) || + ...); + } +}; + +template +struct BoundaryAttributeResolver; + +template +struct BoundaryAttributeResolver> { + static constexpr bool registered = + (std::is_same_v || ...); + + [[nodiscard]] + static constexpr bool matches_attribute(int boundaryId) noexcept { + return ((std::is_same_v && + BoundaryTs::id == boundaryId) || + ...); + } + + [[nodiscard]] + static consteval int attribute() { + static_assert(registered, "Requested boundary is not registered " + "in this schema."); + + int result = 0; + + ((std::is_same_v + ? result = BoundaryTs::id + : result), + ...); + + return result; + } +}; + +template constexpr bool is_material_list_v = false; + +template +constexpr bool is_material_list_v> = true; + +template +concept IsMaterialList = is_material_list_v; + +template constexpr bool is_boundary_list_v = false; + +template +constexpr bool is_boundary_list_v> = true; + +template +concept IsBoundaryList = is_boundary_list_v; + +template struct DomainOperandList { + static constexpr std::size_t count = sizeof...(DomainTs); +}; + +template + requires(sizeof...(DomainTs) == 1 || sizeof...(DomainTs) == 2) +struct DomainBoundary final : public DomainRelation { + using boundary_type = BoundaryT; + + using domains_type = DomainOperandList; + + static constexpr std::size_t domainCount = sizeof...(DomainTs); + + static constexpr std::string_view name = "domain_boundary"; +}; + +template struct RelationList { + static constexpr std::size_t count = sizeof...(RelationTs); +}; + +template constexpr bool is_relation_list_v = false; + +template +constexpr bool is_relation_list_v> = true; + +template +concept IsRelationList = is_relation_list_v; + +/* + * Compile-time validation that every semantic entity + * referenced by a relation is registered by the schema. + * + * Connected and Inscribed only reference domains. + * DomainBoundary references both a boundary and one or + * two domains. + */ +template +struct RelationUsesRegisteredEntities; + +template +struct RelationUsesRegisteredEntities, MaterialsT, + BoundariesT> + : std::bool_constant< + DomainMaterialResolver::registered> {}; + +template +struct RelationUsesRegisteredEntities, MaterialsT, + BoundariesT> + : std::bool_constant< + DomainMaterialResolver::registered && + DomainMaterialResolver::registered> {}; + +template +struct RelationUsesRegisteredEntities, + MaterialsT, BoundariesT> + : std::bool_constant< + BoundaryAttributeResolver::registered && + (DomainMaterialResolver::registered && ...)> {}; + +template +struct RelationsUseRegisteredEntities; + +template +struct RelationsUseRegisteredEntities> + : std::bool_constant<(RelationUsesRegisteredEntities::value && + ...)> {}; + +template +concept HaveValidRelationEntities = + RelationsUseRegisteredEntities::value; + +template + requires HaveValidRelationEntities +struct DomainSchema { + using materials_type = Materials; + using boundaries_type = Boundaries; + using relations_type = Relations; + + static constexpr std::size_t materialCount = Materials::count; + + static constexpr std::size_t boundaryCount = Boundaries::count; + + static constexpr std::size_t relationCount = Relations::count; + + [[nodiscard]] + static constexpr auto materials() noexcept { + return Materials::descriptors(); + } + + [[nodiscard]] + static constexpr auto boundaries() noexcept { + return Boundaries::descriptors(); + } + + template + [[nodiscard]] + static consteval bool contains_domain() noexcept { + return DomainMaterialResolver::registered; + } + + template + [[nodiscard]] + static constexpr bool attribute_belongs_to(int materialId) noexcept { + static_assert(contains_domain(), + "Requested domain is not completely " + "registered in this schema."); + + return DomainMaterialResolver::contains_attribute( + materialId); + } + + template + [[nodiscard]] + static consteval int material_attribute() noexcept { + static_assert(contains_domain(), + "Requested domain is not registered in this schema."); + return DomainMaterialResolver::attribute(); + } + + template + [[nodiscard]] + static consteval bool contains_boundary() noexcept { + return BoundaryAttributeResolver::registered; + } + + template + [[nodiscard]] + static consteval int boundary_attribute() noexcept { + return BoundaryAttributeResolver::attribute(); + } + + template + [[nodiscard]] + static constexpr bool boundary_attribute_matches(int boundaryId) noexcept { + static_assert(contains_boundary(), + "Requested boundary is not registered " + "in this schema."); + + return BoundaryAttributeResolver::matches_attribute( + boundaryId); + } +}; + +template constexpr bool is_schema_v = false; + +template +constexpr bool is_schema_v> = + true; + +template +concept IsSchema = is_schema_v; + +enum class RelationValidationFailure { + None, + + // Connected + DomainAbsent, + DomainDisconnected, + + // Inscribed + InnerDomainAbsent, + OuterDomainAbsent, + InnerDomainHasNoBoundary, + InnerDomainTouchesMeshBoundary, + InnerDomainTouchesUnexpectedMaterial, + + // DomainBoundary + DomainBoundaryAbsent, + DomainBoundaryTaggedFaceHasWrongTopology, + DomainBoundaryTaggedFaceTouchesUnexpectedMaterial, + DomainBoundaryExpectedFaceIsUntagged, + DomainBoundaryExpectedFaceHasWrongAttribute +}; + +struct RelationValidationResult { + RelationValidationFailure failure{RelationValidationFailure::None}; + + struct InscribedDiagnostics { + int faceId{-1}; + int innerElementId{-1}; + int adjacentElementId{-1}; + int adjacentMaterialId{-1}; + }; + + std::optional inscribedDiagnostics = std::nullopt; + + struct ConnectedDiagnostics { + int elementId{-1}; + int domainElementCount{0}; + int visitedElementCount{0}; + }; + + std::optional connectedDiagnostics = std::nullopt; + + struct DomainBoundaryDiagnostics { + int faceId{-1}; + int boundaryElementId{-1}; + + int expectedBoundaryAttribute{0}; + std::optional actualBoundaryAttribute = std::nullopt; + + int firstElementId{-1}; + int secondElementId{-1}; + + std::optional firstMaterialId = std::nullopt; + std::optional secondMaterialId = std::nullopt; + }; + + std::optional domainBoundaryDiagnostics = + std::nullopt; + + [[nodiscard]] + bool valid() const noexcept { + return failure == RelationValidationFailure::None; + } + + [[nodiscard]] + explicit operator bool() const noexcept { + return valid(); + } +}; + +template struct RelationValidator; + +template +struct RelationValidator> { + template + [[nodiscard]] + static RelationValidationResult validate(const mfem::Mesh &mesh) { + static_assert(SchemaT::template contains_domain(), + "The inner domain of Inscribed is not " + "registered in the supplied schema."); + + static_assert(SchemaT::template contains_domain(), + "The outer domain of Inscribed is not " + "registered in the supplied schema."); + + bool foundInnerElement = false; + bool foundOuterElement = false; + bool foundInnerBoundary = false; + + for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { + const int materialId = mesh.GetAttribute(elementId); + + foundInnerElement = + foundInnerElement || + SchemaT::template attribute_belongs_to(materialId); + + foundOuterElement = + foundOuterElement || + SchemaT::template attribute_belongs_to(materialId); } - template - [[nodiscard]] - consteval bool boundary_ids_are_unique() noexcept { - constexpr std::array boundaryIds{BoundaryTs::id...}; - - for (std::size_t firstIndex = 0; firstIndex < boundaryIds.size(); ++firstIndex) { - for (std::size_t secondIndex = firstIndex + 1; secondIndex < boundaryIds.size(); ++secondIndex) { - if (boundaryIds[firstIndex] == boundaryIds[secondIndex]) { - return false; - } - } - } - - return true; + if (!foundInnerElement) { + return {.failure = RelationValidationFailure::InnerDomainAbsent}; } - template struct MaterialDomainsAreUnique; + if (!foundOuterElement) { + return {.failure = RelationValidationFailure::OuterDomainAbsent}; + } - template <> struct MaterialDomainsAreUnique<> : std::true_type { }; + for (int faceId = 0; faceId < mesh.GetNumFaces(); ++faceId) { + int firstElementId = -1; + int secondElementId = -1; - template struct MaterialDomainsAreUnique : std::true_type { }; + mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); - template - struct MaterialDomainsAreUnique - : std::bool_constant< - (!std::is_same_v && - ...) && - MaterialDomainsAreUnique::value> { }; + const bool firstIsInner = + firstElementId >= 0 && SchemaT::template attribute_belongs_to( + mesh.GetAttribute(firstElementId)); - template struct BoundaryTypesAreUnique; + const bool secondIsInner = secondElementId >= 0 && + SchemaT::template attribute_belongs_to( + mesh.GetAttribute(secondElementId)); - template <> struct BoundaryTypesAreUnique<> : std::true_type { }; + if (firstIsInner == secondIsInner) { + continue; + } - template struct BoundaryTypesAreUnique : std::true_type { }; + foundInnerBoundary = true; - template - struct BoundaryTypesAreUnique - : std::bool_constant< - (!std::is_same_v && - ...) && - BoundaryTypesAreUnique::value> { }; + const int innerElementId = + firstIsInner ? firstElementId : secondElementId; - template - concept HaveUniqueMaterialIds = material_ids_are_unique(); + const int adjacentElementId = + firstIsInner ? secondElementId : firstElementId; - template - concept HaveUniqueMaterialDomains = MaterialDomainsAreUnique::value; + if (adjacentElementId < 0) { + return {.failure = + RelationValidationFailure::InnerDomainTouchesMeshBoundary, + .inscribedDiagnostics = std::make_optional< + RelationValidationResult::InscribedDiagnostics>( + {.faceId = faceId, .innerElementId = innerElementId})}; + } - template - concept HaveUniqueBoundaryIds = boundary_ids_are_unique(); + const int adjacentMaterialId = mesh.GetAttribute(adjacentElementId); - template - concept HaveUniqueBoundaryTypes = BoundaryTypesAreUnique::value; + if (!SchemaT::template attribute_belongs_to(adjacentMaterialId)) { + return { + .failure = + RelationValidationFailure::InnerDomainTouchesUnexpectedMaterial, + .inscribedDiagnostics = std::make_optional< + RelationValidationResult::InscribedDiagnostics>( + {.faceId = faceId, + .innerElementId = innerElementId, + .adjacentElementId = adjacentElementId, + .adjacentMaterialId = adjacentMaterialId})}; + } + } - template - requires(HaveUniqueMaterialIds && HaveUniqueMaterialDomains) - struct MaterialList { - static constexpr std::size_t count = sizeof...(MaterialTs); + if (!foundInnerBoundary) { + return {.failure = RelationValidationFailure::InnerDomainHasNoBoundary}; + } - [[nodiscard]] - static constexpr std::array< - MaterialDescriptor, - count> descriptors() noexcept { - return {MaterialDescriptor{.name = MaterialTs::domain_type::name, .id = MaterialTs::id}...}; + return {}; + } +}; + +template struct RelationValidator> { + template + [[nodiscard]] + static RelationValidationResult validate(const mfem::Mesh &mesh) { + static_assert(SchemaT::template contains_domain(), + "Connected refers to a domain which is " + "not completely registered in the " + "supplied DomainSchema."); + + std::vector belongsToDomain(static_cast(mesh.GetNE()), + false); + + int domainElementCount = 0; + int firstDomainElement = -1; + + for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { + const int materialId = mesh.GetAttribute(elementId); + + const bool belongs = + SchemaT::template attribute_belongs_to(materialId); + + belongsToDomain[static_cast(elementId)] = belongs; + + if (!belongs) { + continue; + } + + ++domainElementCount; + + if (firstDomainElement < 0) { + firstDomainElement = elementId; + } + } + + if (domainElementCount == 0) { + return {.failure = RelationValidationFailure::DomainAbsent, + .connectedDiagnostics = std::make_optional< + RelationValidationResult::ConnectedDiagnostics>( + {.domainElementCount = 0, .visitedElementCount = 0})}; + } + + std::vector> adjacency( + static_cast(mesh.GetNE())); + + for (int faceId = 0; faceId < mesh.GetNumFaces(); ++faceId) { + int firstElementId = -1; + int secondElementId = -1; + + mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); + + if (firstElementId < 0 || secondElementId < 0) { + continue; + } + + const bool firstBelongs = + belongsToDomain[static_cast(firstElementId)]; + + const bool secondBelongs = + belongsToDomain[static_cast(secondElementId)]; + + if (!(firstBelongs && secondBelongs)) { + continue; + } + + adjacency[static_cast(firstElementId)].push_back( + secondElementId); + + adjacency[static_cast(secondElementId)].push_back( + firstElementId); + } + + std::vector visited(static_cast(mesh.GetNE()), false); + + std::vector pending; + + pending.reserve(static_cast(domainElementCount)); + + pending.push_back(firstDomainElement); + + int visitedElementCount = 0; + + while (!pending.empty()) { + const int elementId = pending.back(); + + pending.pop_back(); + + if (visited[static_cast(elementId)]) { + continue; + } + + visited[static_cast(elementId)] = true; + + ++visitedElementCount; + + for (const int neighborElementId : + adjacency[static_cast(elementId)]) { + if (!visited[static_cast(neighborElementId)]) { + pending.push_back(neighborElementId); } - }; + } + } - template - requires(HaveUniqueBoundaryIds && HaveUniqueBoundaryTypes) - struct BoundaryList { - static constexpr std::size_t count = sizeof...(BoundaryTs); + if (visitedElementCount == domainElementCount) { + return {.connectedDiagnostics = std::make_optional< + RelationValidationResult::ConnectedDiagnostics>( + {.domainElementCount = domainElementCount, + .visitedElementCount = visitedElementCount})}; + } - [[nodiscard]] - static constexpr std::array< - BoundaryDescriptor, - count> descriptors() noexcept { - return {BoundaryDescriptor{.name = BoundaryTs::boundary_type::name, .id = BoundaryTs::id}...}; - } - }; + int disconnectedElementId = -1; - template struct DomainMaterialResolver; + for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { + const std::size_t index = static_cast(elementId); - template - struct DomainMaterialResolver> { - static constexpr bool registered = (std::is_same_v || ...); + if (belongsToDomain[index] && !visited[index]) { + disconnectedElementId = elementId; - [[nodiscard]] - static constexpr bool contains_attribute(int materialId) noexcept { - return ((std::is_same_v && MaterialTs::id == materialId) || ...); - } - }; + break; + } + } - template - struct DomainMaterialResolver, MaterialList> { - static constexpr bool registered = - (DomainMaterialResolver>::registered && ...); + return { + .failure = RelationValidationFailure::DomainDisconnected, + .connectedDiagnostics = + std::make_optional( + {.elementId = disconnectedElementId, + .domainElementCount = domainElementCount, + .visitedElementCount = visitedElementCount})}; + } +}; - [[nodiscard]] - static constexpr bool contains_attribute(int materialId) noexcept { - return ( - DomainMaterialResolver>::contains_attribute(materialId) || ... - ); - } - }; +template +struct RelationValidator> { + template + [[nodiscard]] + static RelationValidationResult validate(const mfem::Mesh &mesh) { + static_assert(sizeof...(DomainTs) == 1 || sizeof...(DomainTs) == 2, + "DomainBoundary requires exactly one or two domains."); - template struct BoundaryAttributeResolver; + static_assert(SchemaT::template contains_boundary(), + "DomainBoundary refers to a boundary which is not " + "registered in the supplied DomainSchema."); - template - struct BoundaryAttributeResolver> { - static constexpr bool registered = (std::is_same_v || ...); + static_assert((SchemaT::template contains_domain() && ...), + "DomainBoundary refers to a domain which is not " + "completely registered in the supplied DomainSchema."); - [[nodiscard]] - static constexpr bool matches_attribute(int boundaryId) noexcept { - return ( - (std::is_same_v && BoundaryTs::id == boundaryId) || ... - ); - } + constexpr int expectedBoundaryAttribute = + SchemaT::template boundary_attribute(); - [[nodiscard]] - static consteval int attribute() { - static_assert( - registered, "Requested boundary is not registered " - "in this schema." - ); - - int result = 0; - - ((std::is_same_v ? result = BoundaryTs::id : result), ...); - - return result; - } - }; - - template constexpr bool is_material_list_v = false; - - template constexpr bool is_material_list_v> = true; - - template - concept IsMaterialList = is_material_list_v; - - template constexpr bool is_boundary_list_v = false; - - template constexpr bool is_boundary_list_v> = true; - - template - concept IsBoundaryList = is_boundary_list_v; - - template struct DomainOperandList { - static constexpr std::size_t count = sizeof...(DomainTs); - }; - - template - requires(sizeof...(DomainTs) == 1 || sizeof...(DomainTs) == 2) - struct DomainBoundary final : public DomainRelation { - using boundary_type = BoundaryT; - - using domains_type = DomainOperandList; - - static constexpr std::size_t domainCount = sizeof...(DomainTs); - - static constexpr std::string_view name = "domain_boundary"; - }; - - template struct RelationList { - static constexpr std::size_t count = sizeof...(RelationTs); - }; - - template constexpr bool is_relation_list_v = false; - - template constexpr bool is_relation_list_v> = true; - - template - concept IsRelationList = is_relation_list_v; + using DomainsTuple = std::tuple; /* - * Compile-time validation that every semantic entity - * referenced by a relation is registered by the schema. + * Record all MFEM boundary elements associated with each + * mesh face. * - * Connected and Inscribed only reference domains. - * DomainBoundary references both a boundary and one or - * two domains. + * A face can in principle have more than one boundary + * element associated with it. We do not require exactly + * one here; instead, every boundary element on an expected + * face must carry the expected semantic boundary attribute. */ - template - struct RelationUsesRegisteredEntities; + std::vector> boundaryElementsByFace( + static_cast(mesh.GetNumFaces())); - template - struct RelationUsesRegisteredEntities, MaterialsT, BoundariesT> - : std::bool_constant::registered> { }; + for (int boundaryElementId = 0; boundaryElementId < mesh.GetNBE(); + ++boundaryElementId) { + const int faceId = mesh.GetBdrElementFaceIndex(boundaryElementId); - template - struct RelationUsesRegisteredEntities, MaterialsT, BoundariesT> - : std::bool_constant< - DomainMaterialResolver::registered && - DomainMaterialResolver::registered> { }; - - template - struct RelationUsesRegisteredEntities, MaterialsT, BoundariesT> - : std::bool_constant< - BoundaryAttributeResolver::registered && - (DomainMaterialResolver::registered && ...)> { }; - - template - struct RelationsUseRegisteredEntities; - - template - struct RelationsUseRegisteredEntities> - : std::bool_constant<(RelationUsesRegisteredEntities::value && ...)> { }; - - template - concept HaveValidRelationEntities = RelationsUseRegisteredEntities::value; - - template - requires HaveValidRelationEntities - struct DomainSchema { - using materials_type = Materials; - using boundaries_type = Boundaries; - using relations_type = Relations; - - static constexpr std::size_t materialCount = Materials::count; - - static constexpr std::size_t boundaryCount = Boundaries::count; - - static constexpr std::size_t relationCount = Relations::count; - - [[nodiscard]] - static constexpr auto materials() noexcept { - return Materials::descriptors(); - } - - [[nodiscard]] - static constexpr auto boundaries() noexcept { - return Boundaries::descriptors(); - } - - template - [[nodiscard]] - static consteval bool contains_domain() noexcept { - return DomainMaterialResolver::registered; - } - - template - [[nodiscard]] - static constexpr bool attribute_belongs_to(int materialId) noexcept { - static_assert( - contains_domain(), "Requested domain is not completely " - "registered in this schema." - ); - - return DomainMaterialResolver::contains_attribute(materialId); - } - - template - [[nodiscard]] - static consteval bool contains_boundary() noexcept { - return BoundaryAttributeResolver::registered; - } - - template - [[nodiscard]] - static consteval int boundary_attribute() noexcept { - return BoundaryAttributeResolver::attribute(); - } - - template - [[nodiscard]] - static constexpr bool boundary_attribute_matches(int boundaryId) noexcept { - static_assert( - contains_boundary(), "Requested boundary is not registered " - "in this schema." - ); - - return BoundaryAttributeResolver::matches_attribute(boundaryId); - } - }; - - template constexpr bool is_schema_v = false; - - template - constexpr bool is_schema_v> = true; - - template - concept IsSchema = is_schema_v; - - enum class RelationValidationFailure { - None, - - // Connected - DomainAbsent, - DomainDisconnected, - - // Inscribed - InnerDomainAbsent, - OuterDomainAbsent, - InnerDomainHasNoBoundary, - InnerDomainTouchesMeshBoundary, - InnerDomainTouchesUnexpectedMaterial, - - // DomainBoundary - DomainBoundaryAbsent, - DomainBoundaryTaggedFaceHasWrongTopology, - DomainBoundaryTaggedFaceTouchesUnexpectedMaterial, - DomainBoundaryExpectedFaceIsUntagged, - DomainBoundaryExpectedFaceHasWrongAttribute - }; - - struct RelationValidationResult { - RelationValidationFailure failure{RelationValidationFailure::None}; - - struct InscribedDiagnostics { - int faceId{-1}; - int innerElementId{-1}; - int adjacentElementId{-1}; - int adjacentMaterialId{-1}; - }; - - std::optional inscribedDiagnostics = std::nullopt; - - struct ConnectedDiagnostics { - int elementId{-1}; - int domainElementCount{0}; - int visitedElementCount{0}; - }; - - std::optional connectedDiagnostics = std::nullopt; - - struct DomainBoundaryDiagnostics { - int faceId{-1}; - int boundaryElementId{-1}; - - int expectedBoundaryAttribute{0}; - std::optional actualBoundaryAttribute = std::nullopt; - - int firstElementId{-1}; - int secondElementId{-1}; - - std::optional firstMaterialId = std::nullopt; - std::optional secondMaterialId = std::nullopt; - }; - - std::optional domainBoundaryDiagnostics = std::nullopt; - - [[nodiscard]] - bool valid() const noexcept { - return failure == RelationValidationFailure::None; - } - - [[nodiscard]] - explicit operator bool() const noexcept { - return valid(); - } - }; - - template struct RelationValidator; - - template struct RelationValidator> { - template - [[nodiscard]] - static RelationValidationResult validate(const mfem::Mesh &mesh) { - static_assert( - SchemaT::template contains_domain(), "The inner domain of Inscribed is not " - "registered in the supplied schema." - ); - - static_assert( - SchemaT::template contains_domain(), "The outer domain of Inscribed is not " - "registered in the supplied schema." - ); - - bool foundInnerElement = false; - bool foundOuterElement = false; - bool foundInnerBoundary = false; - - for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { - const int materialId = mesh.GetAttribute(elementId); - - foundInnerElement = foundInnerElement || SchemaT::template attribute_belongs_to(materialId); - - foundOuterElement = foundOuterElement || SchemaT::template attribute_belongs_to(materialId); - } - - if (!foundInnerElement) { - return {.failure = RelationValidationFailure::InnerDomainAbsent}; - } - - if (!foundOuterElement) { - return {.failure = RelationValidationFailure::OuterDomainAbsent}; - } - - for (int faceId = 0; faceId < mesh.GetNumFaces(); ++faceId) { - int firstElementId = -1; - int secondElementId = -1; - - mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); - - const bool firstIsInner = - firstElementId >= 0 && - SchemaT::template attribute_belongs_to(mesh.GetAttribute(firstElementId)); - - const bool secondIsInner = - secondElementId >= 0 && - SchemaT::template attribute_belongs_to(mesh.GetAttribute(secondElementId)); - - if (firstIsInner == secondIsInner) { - continue; - } - - foundInnerBoundary = true; - - const int innerElementId = firstIsInner ? firstElementId : secondElementId; - - const int adjacentElementId = firstIsInner ? secondElementId : firstElementId; - - if (adjacentElementId < 0) { - return { - .failure = RelationValidationFailure::InnerDomainTouchesMeshBoundary, - .inscribedDiagnostics = std::make_optional( - {.faceId = faceId, .innerElementId = innerElementId} - ) - }; - } - - const int adjacentMaterialId = mesh.GetAttribute(adjacentElementId); - - if (!SchemaT::template attribute_belongs_to(adjacentMaterialId)) { - return { - .failure = RelationValidationFailure::InnerDomainTouchesUnexpectedMaterial, - .inscribedDiagnostics = std::make_optional( - {.faceId = faceId, - .innerElementId = innerElementId, - .adjacentElementId = adjacentElementId, - .adjacentMaterialId = adjacentMaterialId} - ) - }; - } - } - - if (!foundInnerBoundary) { - return {.failure = RelationValidationFailure::InnerDomainHasNoBoundary}; - } - - return {}; - } - }; - - template struct RelationValidator> { - template - [[nodiscard]] - static RelationValidationResult validate(const mfem::Mesh &mesh) { - static_assert( - SchemaT::template contains_domain(), "Connected refers to a domain which is " - "not completely registered in the " - "supplied DomainSchema." - ); - - std::vector belongsToDomain(static_cast(mesh.GetNE()), false); - - int domainElementCount = 0; - int firstDomainElement = -1; - - for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { - const int materialId = mesh.GetAttribute(elementId); - - const bool belongs = SchemaT::template attribute_belongs_to(materialId); - - belongsToDomain[static_cast(elementId)] = belongs; - - if (!belongs) { - continue; - } - - ++domainElementCount; - - if (firstDomainElement < 0) { - firstDomainElement = elementId; - } - } - - if (domainElementCount == 0) { - return { - .failure = RelationValidationFailure::DomainAbsent, - .connectedDiagnostics = std::make_optional( - {.domainElementCount = 0, .visitedElementCount = 0} - ) - }; - } - - std::vector> adjacency(static_cast(mesh.GetNE())); - - for (int faceId = 0; faceId < mesh.GetNumFaces(); ++faceId) { - int firstElementId = -1; - int secondElementId = -1; - - mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); - - if (firstElementId < 0 || secondElementId < 0) { - continue; - } - - const bool firstBelongs = belongsToDomain[static_cast(firstElementId)]; - - const bool secondBelongs = belongsToDomain[static_cast(secondElementId)]; - - if (!(firstBelongs && secondBelongs)) { - continue; - } - - adjacency[static_cast(firstElementId)].push_back(secondElementId); - - adjacency[static_cast(secondElementId)].push_back(firstElementId); - } - - std::vector visited(static_cast(mesh.GetNE()), false); - - std::vector pending; - - pending.reserve(static_cast(domainElementCount)); - - pending.push_back(firstDomainElement); - - int visitedElementCount = 0; - - while (!pending.empty()) { - const int elementId = pending.back(); - - pending.pop_back(); - - if (visited[static_cast(elementId)]) { - continue; - } - - visited[static_cast(elementId)] = true; - - ++visitedElementCount; - - for (const int neighborElementId : adjacency[static_cast(elementId)]) { - if (!visited[static_cast(neighborElementId)]) { - pending.push_back(neighborElementId); - } - } - } - - if (visitedElementCount == domainElementCount) { - return { - .connectedDiagnostics = std::make_optional( - {.domainElementCount = domainElementCount, .visitedElementCount = visitedElementCount} - ) - }; - } - - int disconnectedElementId = -1; - - for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { - const std::size_t index = static_cast(elementId); - - if (belongsToDomain[index] && !visited[index]) { - disconnectedElementId = elementId; - - break; - } - } - - return { - .failure = RelationValidationFailure::DomainDisconnected, - .connectedDiagnostics = std::make_optional( - {.elementId = disconnectedElementId, - .domainElementCount = domainElementCount, - .visitedElementCount = visitedElementCount} - ) - }; - } - }; - - template - struct RelationValidator> { - template - [[nodiscard]] - static RelationValidationResult validate(const mfem::Mesh &mesh) { - static_assert( - sizeof...(DomainTs) == 1 || sizeof...(DomainTs) == 2, - "DomainBoundary requires exactly one or two domains." - ); - - static_assert( - SchemaT::template contains_boundary(), "DomainBoundary refers to a boundary which is not " - "registered in the supplied DomainSchema." - ); - - static_assert( - (SchemaT::template contains_domain() && ...), - "DomainBoundary refers to a domain which is not " - "completely registered in the supplied DomainSchema." - ); - - constexpr int expectedBoundaryAttribute = SchemaT::template boundary_attribute(); - - using DomainsTuple = std::tuple; - - /* - * Record all MFEM boundary elements associated with each - * mesh face. - * - * A face can in principle have more than one boundary - * element associated with it. We do not require exactly - * one here; instead, every boundary element on an expected - * face must carry the expected semantic boundary attribute. - */ - std::vector> boundaryElementsByFace(static_cast(mesh.GetNumFaces())); - - for (int boundaryElementId = 0; boundaryElementId < mesh.GetNBE(); ++boundaryElementId) { - const int faceId = mesh.GetBdrElementFaceIndex(boundaryElementId); - - if (faceId >= 0 && faceId < mesh.GetNumFaces()) { - boundaryElementsByFace[static_cast(faceId)].push_back(boundaryElementId); - } - } - - /* - * Build detailed diagnostics for one face. - */ - const auto make_diagnostics = [&mesh, expectedBoundaryAttribute]( - int faceId, int boundaryElementId, - std::optional actualBoundaryAttribute - ) { - RelationValidationResult::DomainBoundaryDiagnostics diagnostics{ - .faceId = faceId, - .boundaryElementId = boundaryElementId, - .expectedBoundaryAttribute = expectedBoundaryAttribute, - .actualBoundaryAttribute = actualBoundaryAttribute - }; - - if (faceId < 0 || faceId >= mesh.GetNumFaces()) { - return diagnostics; - } - - mesh.GetFaceElements(faceId, &diagnostics.firstElementId, &diagnostics.secondElementId); - - if (diagnostics.firstElementId >= 0) { - diagnostics.firstMaterialId = mesh.GetAttribute(diagnostics.firstElementId); - } - - if (diagnostics.secondElementId >= 0) { - diagnostics.secondMaterialId = mesh.GetAttribute(diagnostics.secondElementId); - } - - return diagnostics; - }; - - /* - * Check only the cardinality/topological shape required by - * the relation. - * - * One-domain form: - * - * Domain | computational exterior - * - * Exactly one adjacent volume element must exist. - * - * Two-domain form: - * - * DomainA | DomainB - * - * Both adjacent volume elements must exist. - */ - const auto has_required_topology = [](int firstElementId, int secondElementId) { - if constexpr (sizeof...(DomainTs) == 1) { - const bool firstExists = firstElementId >= 0; - - const bool secondExists = secondElementId >= 0; - - return firstExists != secondExists; - } else { - return firstElementId >= 0 && secondElementId >= 0; - } - }; - - /* - * Determine whether a face is exactly one of the faces - * described by DomainBoundary. - * - * For two domains, ordering is intentionally irrelevant. - */ - const auto face_matches_domains = [&mesh](int firstElementId, int secondElementId) { - if constexpr (sizeof...(DomainTs) == 1) { - using DomainT = std::tuple_element_t<0, DomainsTuple>; - - const bool firstExists = firstElementId >= 0; - - const bool secondExists = secondElementId >= 0; - - if (firstExists == secondExists) { - return false; - } - - const int elementId = firstExists ? firstElementId : secondElementId; - - const int materialId = mesh.GetAttribute(elementId); - - return SchemaT::template attribute_belongs_to(materialId); - } else { - using FirstDomainT = std::tuple_element_t<0, DomainsTuple>; - - using SecondDomainT = std::tuple_element_t<1, DomainsTuple>; - - if (firstElementId < 0 || secondElementId < 0) { - return false; - } - - const int firstMaterialId = mesh.GetAttribute(firstElementId); - - const int secondMaterialId = mesh.GetAttribute(secondElementId); - - const bool forwardMatch = SchemaT::template attribute_belongs_to(firstMaterialId) && - SchemaT::template attribute_belongs_to(secondMaterialId); - - const bool reverseMatch = SchemaT::template attribute_belongs_to(firstMaterialId) && - SchemaT::template attribute_belongs_to(secondMaterialId); - - return forwardMatch || reverseMatch; - } - }; - - bool foundTaggedBoundary = false; - - /* - * Forward validation: - * - * Every boundary element carrying BoundaryT must lie on - * exactly the topology/material interface declared by - * DomainBoundary. - */ - for (int boundaryElementId = 0; boundaryElementId < mesh.GetNBE(); ++boundaryElementId) { - const int boundaryAttribute = mesh.GetBdrAttribute(boundaryElementId); - - if (boundaryAttribute != expectedBoundaryAttribute) { - continue; - } - - foundTaggedBoundary = true; - - const int faceId = mesh.GetBdrElementFaceIndex(boundaryElementId); - - int firstElementId = -1; - int secondElementId = -1; - - mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); - - if (!has_required_topology(firstElementId, secondElementId)) { - return { - .failure = RelationValidationFailure::DomainBoundaryTaggedFaceHasWrongTopology, - .domainBoundaryDiagnostics = - std::make_optional( - make_diagnostics(faceId, boundaryElementId, boundaryAttribute) - ) - }; - } - - if (!face_matches_domains(firstElementId, secondElementId)) { - return { - .failure = RelationValidationFailure::DomainBoundaryTaggedFaceTouchesUnexpectedMaterial, - .domainBoundaryDiagnostics = - std::make_optional( - make_diagnostics(faceId, boundaryElementId, boundaryAttribute) - ) - }; - } - } - - bool foundExpectedFace = false; - - /* - * Reverse validation: - * - * Every face having the declared domain adjacency must - * carry BoundaryT. - * - * This is important for physical constraints: a partially - * tagged Stellar/Vacuum interface must fail rather than - * silently leaving part of the stellar surface unconstrained. - */ - for (int faceId = 0; faceId < mesh.GetNumFaces(); ++faceId) { - int firstElementId = -1; - int secondElementId = -1; - - mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); - - if (!face_matches_domains(firstElementId, secondElementId)) { - continue; - } - - foundExpectedFace = true; - - const auto &boundaryElementIds = boundaryElementsByFace[static_cast(faceId)]; - - if (boundaryElementIds.empty()) { - return { - .failure = RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged, - .domainBoundaryDiagnostics = - std::make_optional( - make_diagnostics(faceId, -1, std::nullopt) - ) - }; - } - - for (const int boundaryElementId : boundaryElementIds) { - const int actualBoundaryAttribute = mesh.GetBdrAttribute(boundaryElementId); - - if (actualBoundaryAttribute == expectedBoundaryAttribute) { - continue; - } - - return { - .failure = RelationValidationFailure::DomainBoundaryExpectedFaceHasWrongAttribute, - .domainBoundaryDiagnostics = - std::make_optional( - make_diagnostics(faceId, boundaryElementId, actualBoundaryAttribute) - ) - }; - } - } - - /* - * If neither a correctly tagged boundary nor a face having - * the required semantic topology exists, the declared - * DomainBoundary simply is not realized by this mesh. - * - * In the usual partial-failure cases above we will already - * have returned a more specific diagnostic. - */ - if (!foundTaggedBoundary || !foundExpectedFace) { - return { - .failure = RelationValidationFailure::DomainBoundaryAbsent, - .domainBoundaryDiagnostics = - std::make_optional( - make_diagnostics(-1, -1, std::nullopt) - ) - }; - } - - return {}; - } - }; - - struct SchemaRelationValidationResult { - std::size_t relationIndex{0}; - std::string_view relationName; - RelationValidationResult result; - - [[nodiscard]] - bool valid() const noexcept { - return result.valid(); - } - - [[nodiscard]] - explicit operator bool() const noexcept { - return valid(); - } - }; - - struct SchemaValidationResult { - std::vector relationResults; - - [[nodiscard]] - bool valid() const noexcept { - for (const auto &relationResult : relationResults) { - if (!relationResult.valid()) { - return false; - } - } - - return true; - } - - [[nodiscard]] - explicit operator bool() const noexcept { - return valid(); - } - - [[nodiscard]] - std::size_t relation_count() const noexcept { - return relationResults.size(); - } - - [[nodiscard]] - std::size_t failed_relation_count() const noexcept { - std::size_t failureCount = 0; - - for (const auto &relationResult : relationResults) { - if (!relationResult.valid()) { - ++failureCount; - } - } - - return failureCount; - } - - [[nodiscard]] - std::size_t passed_relation_count() const noexcept { - return relationResults.size() - failed_relation_count(); - } - - [[nodiscard]] - std::optional first_failed_relation_index() const noexcept { - for (std::size_t relationIndex = 0; relationIndex < relationResults.size(); ++relationIndex) { - if (!relationResults[relationIndex].valid()) { - return relationIndex; - } - } - - return std::nullopt; - } - }; - - template struct SchemaRelationValidator; - - template - struct SchemaRelationValidator> { - [[nodiscard]] - static SchemaValidationResult validate(const mfem::Mesh &mesh) { - SchemaValidationResult schemaResult; - - schemaResult.relationResults.reserve(sizeof...(RelationTs)); - - std::size_t relationIndex = 0; - - (schemaResult.relationResults.push_back( - SchemaRelationValidationResult{ - .relationIndex = relationIndex++, - .relationName = RelationTs::name, - .result = RelationValidator::template validate(mesh) - } - ), - ...); - - return schemaResult; - } - }; - - template - [[nodiscard]] - SchemaValidationResult validate_schema(const mfem::Mesh &mesh) { - using RelationsT = typename SchemaT::relations_type; - - return SchemaRelationValidator::validate(mesh); + if (faceId >= 0 && faceId < mesh.GetNumFaces()) { + boundaryElementsByFace[static_cast(faceId)].push_back( + boundaryElementId); + } } - using CoreEnvelopeVacuumDomainSchema = DomainSchema< - MaterialList, Material, Material>, - BoundaryList, BoundaryAttribute>, - RelationList< - // All Domains must be fully connected - Connected, - Connected, - Connected, + /* + * Build detailed diagnostics for one face. + */ + const auto make_diagnostics = + [&mesh, expectedBoundaryAttribute]( + int faceId, int boundaryElementId, + std::optional actualBoundaryAttribute) { + RelationValidationResult::DomainBoundaryDiagnostics diagnostics{ + .faceId = faceId, + .boundaryElementId = boundaryElementId, + .expectedBoundaryAttribute = expectedBoundaryAttribute, + .actualBoundaryAttribute = actualBoundaryAttribute}; - // Describe the topology of the mesh (core must be within envelope and the stellar domain (core + envelope) - // must be inscribed within vacuum region - Inscribed, - Inscribed, + if (faceId < 0 || faceId >= mesh.GetNumFaces()) { + return diagnostics; + } - // The stellar surface sits between the stellar and vacuum domain and the infinity surface sits at the - // outside of the vacuum domain - DomainBoundary, - DomainBoundary>>; -} // namespace mean_field::utils::domain \ No newline at end of file + mesh.GetFaceElements(faceId, &diagnostics.firstElementId, + &diagnostics.secondElementId); + + if (diagnostics.firstElementId >= 0) { + diagnostics.firstMaterialId = + mesh.GetAttribute(diagnostics.firstElementId); + } + + if (diagnostics.secondElementId >= 0) { + diagnostics.secondMaterialId = + mesh.GetAttribute(diagnostics.secondElementId); + } + + return diagnostics; + }; + + /* + * Check only the cardinality/topological shape required by + * the relation. + * + * One-domain form: + * + * Domain | computational exterior + * + * Exactly one adjacent volume element must exist. + * + * Two-domain form: + * + * DomainA | DomainB + * + * Both adjacent volume elements must exist. + */ + const auto has_required_topology = [](int firstElementId, + int secondElementId) { + if constexpr (sizeof...(DomainTs) == 1) { + const bool firstExists = firstElementId >= 0; + + const bool secondExists = secondElementId >= 0; + + return firstExists != secondExists; + } else { + return firstElementId >= 0 && secondElementId >= 0; + } + }; + + /* + * Determine whether a face is exactly one of the faces + * described by DomainBoundary. + * + * For two domains, ordering is intentionally irrelevant. + */ + const auto face_matches_domains = [&mesh](int firstElementId, + int secondElementId) { + if constexpr (sizeof...(DomainTs) == 1) { + using DomainT = std::tuple_element_t<0, DomainsTuple>; + + const bool firstExists = firstElementId >= 0; + + const bool secondExists = secondElementId >= 0; + + if (firstExists == secondExists) { + return false; + } + + const int elementId = firstExists ? firstElementId : secondElementId; + + const int materialId = mesh.GetAttribute(elementId); + + return SchemaT::template attribute_belongs_to(materialId); + } else { + using FirstDomainT = std::tuple_element_t<0, DomainsTuple>; + + using SecondDomainT = std::tuple_element_t<1, DomainsTuple>; + + if (firstElementId < 0 || secondElementId < 0) { + return false; + } + + const int firstMaterialId = mesh.GetAttribute(firstElementId); + + const int secondMaterialId = mesh.GetAttribute(secondElementId); + + const bool forwardMatch = + SchemaT::template attribute_belongs_to( + firstMaterialId) && + SchemaT::template attribute_belongs_to( + secondMaterialId); + + const bool reverseMatch = + SchemaT::template attribute_belongs_to( + firstMaterialId) && + SchemaT::template attribute_belongs_to( + secondMaterialId); + + return forwardMatch || reverseMatch; + } + }; + + bool foundTaggedBoundary = false; + + /* + * Forward validation: + * + * Every boundary element carrying BoundaryT must lie on + * exactly the topology/material interface declared by + * DomainBoundary. + */ + for (int boundaryElementId = 0; boundaryElementId < mesh.GetNBE(); + ++boundaryElementId) { + const int boundaryAttribute = mesh.GetBdrAttribute(boundaryElementId); + + if (boundaryAttribute != expectedBoundaryAttribute) { + continue; + } + + foundTaggedBoundary = true; + + const int faceId = mesh.GetBdrElementFaceIndex(boundaryElementId); + + int firstElementId = -1; + int secondElementId = -1; + + mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); + + if (!has_required_topology(firstElementId, secondElementId)) { + return {.failure = RelationValidationFailure:: + DomainBoundaryTaggedFaceHasWrongTopology, + .domainBoundaryDiagnostics = std::make_optional< + RelationValidationResult::DomainBoundaryDiagnostics>( + make_diagnostics(faceId, boundaryElementId, + boundaryAttribute))}; + } + + if (!face_matches_domains(firstElementId, secondElementId)) { + return {.failure = RelationValidationFailure:: + DomainBoundaryTaggedFaceTouchesUnexpectedMaterial, + .domainBoundaryDiagnostics = std::make_optional< + RelationValidationResult::DomainBoundaryDiagnostics>( + make_diagnostics(faceId, boundaryElementId, + boundaryAttribute))}; + } + } + + bool foundExpectedFace = false; + + /* + * Reverse validation: + * + * Every face having the declared domain adjacency must + * carry BoundaryT. + * + * This is important for physical constraints: a partially + * tagged Stellar/Vacuum interface must fail rather than + * silently leaving part of the stellar surface unconstrained. + */ + for (int faceId = 0; faceId < mesh.GetNumFaces(); ++faceId) { + int firstElementId = -1; + int secondElementId = -1; + + mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); + + if (!face_matches_domains(firstElementId, secondElementId)) { + continue; + } + + foundExpectedFace = true; + + const auto &boundaryElementIds = + boundaryElementsByFace[static_cast(faceId)]; + + if (boundaryElementIds.empty()) { + return { + .failure = + RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged, + .domainBoundaryDiagnostics = std::make_optional< + RelationValidationResult::DomainBoundaryDiagnostics>( + make_diagnostics(faceId, -1, std::nullopt))}; + } + + for (const int boundaryElementId : boundaryElementIds) { + const int actualBoundaryAttribute = + mesh.GetBdrAttribute(boundaryElementId); + + if (actualBoundaryAttribute == expectedBoundaryAttribute) { + continue; + } + + return {.failure = RelationValidationFailure:: + DomainBoundaryExpectedFaceHasWrongAttribute, + .domainBoundaryDiagnostics = std::make_optional< + RelationValidationResult::DomainBoundaryDiagnostics>( + make_diagnostics(faceId, boundaryElementId, + actualBoundaryAttribute))}; + } + } + + /* + * If neither a correctly tagged boundary nor a face having + * the required semantic topology exists, the declared + * DomainBoundary simply is not realized by this mesh. + * + * In the usual partial-failure cases above we will already + * have returned a more specific diagnostic. + */ + if (!foundTaggedBoundary || !foundExpectedFace) { + return {.failure = RelationValidationFailure::DomainBoundaryAbsent, + .domainBoundaryDiagnostics = std::make_optional< + RelationValidationResult::DomainBoundaryDiagnostics>( + make_diagnostics(-1, -1, std::nullopt))}; + } + + return {}; + } +}; + +struct SchemaRelationValidationResult { + std::size_t relationIndex{0}; + std::string_view relationName; + RelationValidationResult result; + + [[nodiscard]] + bool valid() const noexcept { + return result.valid(); + } + + [[nodiscard]] + explicit operator bool() const noexcept { + return valid(); + } +}; + +struct SchemaValidationResult { + std::vector relationResults; + + [[nodiscard]] + bool valid() const noexcept { + for (const auto &relationResult : relationResults) { + if (!relationResult.valid()) { + return false; + } + } + + return true; + } + + [[nodiscard]] + explicit operator bool() const noexcept { + return valid(); + } + + [[nodiscard]] + std::size_t relation_count() const noexcept { + return relationResults.size(); + } + + [[nodiscard]] + std::size_t failed_relation_count() const noexcept { + std::size_t failureCount = 0; + + for (const auto &relationResult : relationResults) { + if (!relationResult.valid()) { + ++failureCount; + } + } + + return failureCount; + } + + [[nodiscard]] + std::size_t passed_relation_count() const noexcept { + return relationResults.size() - failed_relation_count(); + } + + [[nodiscard]] + std::optional first_failed_relation_index() const noexcept { + for (std::size_t relationIndex = 0; relationIndex < relationResults.size(); + ++relationIndex) { + if (!relationResults[relationIndex].valid()) { + return relationIndex; + } + } + + return std::nullopt; + } +}; + +template +struct SchemaRelationValidator; + +template +struct SchemaRelationValidator> { + [[nodiscard]] + static SchemaValidationResult validate(const mfem::Mesh &mesh) { + SchemaValidationResult schemaResult; + + schemaResult.relationResults.reserve(sizeof...(RelationTs)); + + std::size_t relationIndex = 0; + + (schemaResult.relationResults.push_back(SchemaRelationValidationResult{ + .relationIndex = relationIndex++, + .relationName = RelationTs::name, + .result = + RelationValidator::template validate(mesh)}), + ...); + + return schemaResult; + } +}; + +template +[[nodiscard]] +SchemaValidationResult validate_schema(const mfem::Mesh &mesh) { + using RelationsT = typename SchemaT::relations_type; + + return SchemaRelationValidator::validate(mesh); +} + +template +[[nodiscard]] +mfem::Array make_attribute_marker(const mfem::Mesh &mesh) { + static_assert(SchemaT::template contains_domain(), + "Requested marker domain is not completely registered in the " + "supplied DomainSchema."); + + mfem::Array marker(mesh.attributes.Max()); + + for (int attribute = 1; attribute <= marker.Size(); ++attribute) { + marker[attribute - 1] = + SchemaT::template attribute_belongs_to(attribute) ? 1 : 0; + } + + return marker; +} + +using CoreEnvelopeVacuumDomainSchema = DomainSchema< + MaterialList, Material, Material>, + BoundaryList, + BoundaryAttribute>, + RelationList< + // All Domains must be fully connected + Connected, Connected, Connected, + + // Describe the topology of the mesh (core must be within envelope and + // the stellar domain (core + envelope) must be inscribed within vacuum + // region + Inscribed, Inscribed, + + // The stellar surface sits between the stellar and vacuum domain and + // the infinity surface sits at the outside of the vacuum domain + DomainBoundary, + DomainBoundary>>; +} // namespace mean_field::utils::domain diff --git a/libmeanfield/interface/utils/misc.cppm b/libmeanfield/interface/utils/misc.cppm index 6f9e24b..e0a847d 100644 --- a/libmeanfield/interface/utils/misc.cppm +++ b/libmeanfield/interface/utils/misc.cppm @@ -1,5 +1,4 @@ module; -#include #include #include @@ -9,109 +8,85 @@ module; #include export module mean_field:utils.misc; -import :boundary.contexts; +import :utils.domain; export namespace mean_field::utils { - constexpr double APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING = 1e-4; +constexpr double APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING = 1e-4; - bool is_vacuum( - const mfem::ElementTransformation &Tr, - mfem::Array elvec - ) { - if (Tr.Attribute == 3) { - const int size_elvec = elvec.Size(); - for (int i = 0; i < size_elvec; i++) { - if (elvec[i]) { - *elvec[i] = 0.0; - } - } - return true; - } - return false; +bool is_vacuum(const mfem::ElementTransformation &Tr, + mfem::Array elvec) { + using Schema = domain::CoreEnvelopeVacuumDomainSchema; + + if (Schema::template attribute_belongs_to(Tr.Attribute)) { + const int size_elvec = elvec.Size(); + for (int i = 0; i < size_elvec; i++) { + if (elvec[i]) { + *elvec[i] = 0.0; + } } + return true; + } + return false; +} - bool is_vacuum( - const mfem::ElementTransformation &Tr, - const mfem::Array2D &elmats - ) { - if (Tr.Attribute == 3) { - const int cols = elmats.NumCols(); - const int rows = elmats.NumRows(); - for (int rowID = 0; rowID < rows; rowID++) { - for (int colID = 0; colID < cols; colID++) { - if (elmats(rowID, colID)) { - *elmats(rowID, colID) = 0.0; - } - } - } - return true; +bool is_vacuum(const mfem::ElementTransformation &Tr, + const mfem::Array2D &elmats) { + using Schema = domain::CoreEnvelopeVacuumDomainSchema; + + if (Schema::template attribute_belongs_to(Tr.Attribute)) { + const int cols = elmats.NumCols(); + const int rows = elmats.NumRows(); + for (int rowID = 0; rowID < rows; rowID++) { + for (int colID = 0; colID < cols; colID++) { + if (elmats(rowID, colID)) { + *elmats(rowID, colID) = 0.0; } - return false; + } } + return true; + } + return false; +} - constexpr std::string_view ANSI_GREEN = "\033[32m"; - constexpr std::string_view ANSI_RED = "\033[31m"; - constexpr std::string_view ANSI_YELLOW = "\033[33m"; - constexpr std::string_view ANSI_BLUE = "\033[34m"; - constexpr std::string_view ANSI_MAGENTA = "\033[35m"; - constexpr std::string_view ANSI_CYAN = "\033[36m"; - constexpr std::string_view ANSI_RESET = "\033[0m"; - constexpr std::string_view ANSI_BCYAN = "\033[1;36m"; +constexpr std::string_view ANSI_GREEN = "\033[32m"; +constexpr std::string_view ANSI_RED = "\033[31m"; +constexpr std::string_view ANSI_YELLOW = "\033[33m"; +constexpr std::string_view ANSI_BLUE = "\033[34m"; +constexpr std::string_view ANSI_MAGENTA = "\033[35m"; +constexpr std::string_view ANSI_CYAN = "\033[36m"; +constexpr std::string_view ANSI_RESET = "\033[0m"; +constexpr std::string_view ANSI_BCYAN = "\033[1;36m"; - constexpr double G = 1.0; - constexpr double MASS = 1.0; - constexpr double RADIUS = 1.0; +constexpr double G = 1.0; +constexpr double MASS = 1.0; +constexpr double RADIUS = 1.0; - [[maybe_unused]] constexpr char HOST[10] = "localhost"; - [[maybe_unused]] constexpr int PORT = 19916; +[[maybe_unused]] constexpr char HOST[10] = "localhost"; +[[maybe_unused]] constexpr int PORT = 19916; - template - concept is_xad = std::is_same_v> || std::is_same_v> || - std::is_same_v>; +template +concept is_xad = std::is_same_v> || + std::is_same_v> || + std::is_same_v>; - template - concept is_real = std::is_floating_point_v || is_xad; +template +concept is_real = std::is_floating_point_v || is_xad; - template using EOS_P = std::function; +template +using EOS_P = std::function; - enum class DOMAINS : uint8_t { - CORE = 1 << 0, - ENVELOPE = 1 << 1, - VACUUM = 1 << 2, - STELLAR = CORE | ENVELOPE, - ALL = CORE | ENVELOPE | VACUUM - }; +enum class DOMAINS : uint8_t { + CORE = 1 << 0, + ENVELOPE = 1 << 1, + VACUUM = 1 << 2, + STELLAR = CORE | ENVELOPE, + ALL = CORE | ENVELOPE | VACUUM +}; - DOMAINS operator|( - DOMAINS lhs, - DOMAINS rhs - ); +DOMAINS operator|(DOMAINS lhs, DOMAINS rhs); - DOMAINS operator&( - DOMAINS lhs, - DOMAINS rhs - ); +DOMAINS operator&(DOMAINS lhs, DOMAINS rhs); - void populate_element_mask( - const mfem::Mesh *mesh, - DOMAINS domain, - mfem::Array &mask - ); - - void populate_domain_tdofs( - const mfem::ParFiniteElementSpace *fes, - const mfem::Array &element_mask, - mfem::Array &ess_tdof - ); - - std::expected< - boundary::Bounds, - boundary::BoundsError> - discover_bounds( - const mfem::Mesh *mesh, - int vacuum_attr - ); - - int get_mesh_order(const mfem::Mesh &mesh); +int get_mesh_order(const mfem::Mesh &mesh); } // namespace mean_field::utils diff --git a/libmeanfield/interface/utils/user.cppm b/libmeanfield/interface/utils/user.cppm index 7f0f251..da069b8 100644 --- a/libmeanfield/interface/utils/user.cppm +++ b/libmeanfield/interface/utils/user.cppm @@ -7,9 +7,9 @@ export import :mapping.compactification.options; export namespace mean_field::utils { struct potential { - double rtol; - double atol; - int max_iters; + double rtol{1.0e-12}; + double atol{1.0e-12}; + int max_iters{1000}; }; struct rot { @@ -18,7 +18,7 @@ export namespace mean_field::utils { double L; }; - struct DomainMapperStatelessOptions { + struct DomainMapperOptions { int dimension{3}; int vacuum_element_attribute{3}; }; @@ -31,7 +31,7 @@ export namespace mean_field::utils { double index{}; double mass{}; double c{}; - DomainMapperStatelessOptions domain_mapper_options{}; + DomainMapperOptions domain_mapper_options{}; mapping::compactification::options::KelvinCompactificationOptions kelvin_options{}; int max_iters{}; double tol{}; diff --git a/tests/field/field_dof_map.cpp b/tests/field/field_dof_map.cpp index 9a9a9a8..0564e40 100644 --- a/tests/field/field_dof_map.cpp +++ b/tests/field/field_dof_map.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +80,11 @@ namespace field_dof_map_test_utils { concept CanMakeFieldDofMap = requires(const mfem::ParFiniteElementSpace &space) { field::make_field_dof_map(space); }; + template + concept CanMakeFieldDofGridFunctionAdapter = requires(const mfem::ParFiniteElementSpace &space) { + field::make_field_dof_grid_function_adapter(space); + }; + using AlternateSchema = domain::DomainSchema< domain::MaterialList< domain::Material, @@ -90,7 +96,7 @@ namespace field_dof_map_test_utils { TEST_CASE( "Field DOF Map Preserves Canonical Bidirectional Indexing", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -166,7 +172,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Rejects Invalid Canonical Mappings", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -191,7 +197,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Rejects Out Of Range Index Queries", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -212,7 +218,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Gather Selects Exactly The Active True DOFs", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -247,7 +253,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Scatter Produces The Canonical Supported Projection", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -281,7 +287,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Gather Scatter Projects A Full Vector Onto Field Support", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -310,7 +316,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Scatter Into Preserves Unsupported True DOFs", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -337,7 +343,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Scatter Add Accumulates Only Onto Active True DOFs", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -364,7 +370,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Operations Support MFEM Vector Views Without Resizing", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -406,7 +412,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Operations Reject Incompatible Vector Sizes", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -433,7 +439,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Identity Mapping Is An Exact Vector Identity", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -463,7 +469,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Validates Field DOF Support Consistency", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -495,7 +501,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Factory Is Available Only For Spatial Registered Fields", - tags::unit &tags::field + tags::field_dof_unit ) { namespace field = mean_field::field; @@ -509,12 +515,24 @@ TEST_CASE( STATIC_REQUIRE_FALSE(field_dof_map_test_utils::CanMakeFieldDofMap); + STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter); + + STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter); + + STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter); + + STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter); + + STATIC_REQUIRE_FALSE( + field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter + ); + CHECK(true); } TEST_CASE( "Field DOF Map Factory Exactly Preserves Density Support", - tags::integration &tags::field + tags::field_dof_integration ) { namespace field = mean_field::field; @@ -567,7 +585,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Factory Exactly Preserves H1 Enthalpy Support", - tags::integration &tags::field + tags::field_dof_integration ) { namespace field = mean_field::field; @@ -614,7 +632,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Factory Produces Identity Maps For All Supported Fields", - tags::integration &tags::field + tags::field_dof_integration ) { namespace field = mean_field::field; @@ -648,7 +666,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Factory Uses Schema Material Bindings Rather Than Numeric Conventions", - tags::integration &tags::field + tags::field_dof_integration ) { namespace field = mean_field::field; @@ -681,7 +699,7 @@ TEST_CASE( TEST_CASE( "Field DOF Map Reduced Vectors Round Trip Through Real Field Support", - tags::integration &tags::field + tags::field_dof_integration ) { namespace field = mean_field::field; @@ -721,4 +739,246 @@ TEST_CASE( CHECK(full(trueDof) == 0.0); } } -} \ No newline at end of file +} + +TEST_CASE( + "Field DOF Grid Function Adapter Gathers Exactly The Supported True DOFs", + tags::field_dof_integration +) { + namespace field = mean_field::field; + + mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh(); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + + auto fec = field::Field::make_fec(2); + auto finiteElementSpace = + field::Field::make_fespace(mesh, *fec); + + REQUIRE(finiteElementSpace != nullptr); + + const field::FieldDofGridFunctionAdapter adapter = + field::make_field_dof_grid_function_adapter( + *finiteElementSpace + ); + + mfem::Vector full(adapter.dof_map().full_size()); + for (int trueDof = 0; trueDof < full.Size(); ++trueDof) { + full(trueDof) = 1.25 + 0.375 * static_cast(trueDof + 1); + } + + mfem::ParGridFunction gridFunction(finiteElementSpace.get()); + gridFunction.SetFromTrueDofs(full); + + const mfem::Vector expected = adapter.dof_map().gather(full); + const mfem::Vector actual = adapter.gather(gridFunction); + + REQUIRE(actual.Size() == expected.Size()); + for (int reducedDof = 0; reducedDof < actual.Size(); ++reducedDof) { + CAPTURE(reducedDof); + CHECK(actual(reducedDof) == expected(reducedDof)); + } + + mfem::Vector output(adapter.dof_map().reduced_size()); + adapter.gather(gridFunction, output); + + for (int reducedDof = 0; reducedDof < output.Size(); ++reducedDof) { + CAPTURE(reducedDof); + CHECK(output(reducedDof) == expected(reducedDof)); + } +} + +TEST_CASE( + "Field DOF Grid Function Adapter Scatter Projects And Round Trips Reduced Fields", + tags::field_dof_integration +) { + namespace field = mean_field::field; + + mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh(); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + + auto fec = field::Field::make_fec(2); + auto finiteElementSpace = + field::Field::make_fespace(mesh, *fec); + + REQUIRE(finiteElementSpace != nullptr); + + const field::FieldDofGridFunctionAdapter adapter = + field::make_field_dof_grid_function_adapter( + *finiteElementSpace + ); + + mfem::Vector reduced(adapter.dof_map().reduced_size()); + for (int reducedDof = 0; reducedDof < reduced.Size(); ++reducedDof) { + reduced(reducedDof) = -0.75 + 0.0625 * static_cast(reducedDof + 1); + } + + mfem::ParGridFunction gridFunction(finiteElementSpace.get()); + gridFunction = 91.0; + adapter.scatter(reduced, gridFunction); + + mfem::Vector actualFull; + gridFunction.GetTrueDofs(actualFull); + + const mfem::Vector expectedFull = adapter.dof_map().scatter(reduced); + + REQUIRE(actualFull.Size() == expectedFull.Size()); + for (int trueDof = 0; trueDof < actualFull.Size(); ++trueDof) { + CAPTURE(trueDof); + CHECK(actualFull(trueDof) == expectedFull(trueDof)); + + if (!adapter.dof_map().contains_true_dof(trueDof)) { + CHECK(actualFull(trueDof) == 0.0); + } + } + + const mfem::Vector recovered = adapter.gather(gridFunction); + REQUIRE(recovered.Size() == reduced.Size()); + + for (int reducedDof = 0; reducedDof < reduced.Size(); ++reducedDof) { + CAPTURE(reducedDof); + CHECK(recovered(reducedDof) == reduced(reducedDof)); + } +} + +TEST_CASE( + "Field DOF Grid Function Adapter Scatter Into Preserves Unsupported True DOFs", + tags::field_dof_integration +) { + namespace field = mean_field::field; + + mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh(); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + + auto fec = field::Field::make_fec(2); + auto finiteElementSpace = + field::Field::make_fespace(mesh, *fec); + + REQUIRE(finiteElementSpace != nullptr); + + const field::FieldDofGridFunctionAdapter adapter = + field::make_field_dof_grid_function_adapter( + *finiteElementSpace + ); + + mfem::Vector initialFull(adapter.dof_map().full_size()); + for (int trueDof = 0; trueDof < initialFull.Size(); ++trueDof) { + initialFull(trueDof) = 40.0 + static_cast(trueDof); + } + + mfem::Vector reduced(adapter.dof_map().reduced_size()); + for (int reducedDof = 0; reducedDof < reduced.Size(); ++reducedDof) { + reduced(reducedDof) = -10.0 - static_cast(reducedDof); + } + + mfem::ParGridFunction gridFunction(finiteElementSpace.get()); + gridFunction.SetFromTrueDofs(initialFull); + adapter.scatter_into(reduced, gridFunction); + + mfem::Vector actualFull; + gridFunction.GetTrueDofs(actualFull); + + mfem::Vector expectedFull(initialFull); + adapter.dof_map().scatter_into(reduced, expectedFull); + + REQUIRE(actualFull.Size() == expectedFull.Size()); + for (int trueDof = 0; trueDof < actualFull.Size(); ++trueDof) { + CAPTURE(trueDof); + CHECK(actualFull(trueDof) == expectedFull(trueDof)); + + if (!adapter.dof_map().contains_true_dof(trueDof)) { + CHECK(actualFull(trueDof) == initialFull(trueDof)); + } + } +} + +TEST_CASE( + "Field DOF Grid Function Adapter Is Exact For Identity Vector Field Maps", + tags::field_dof_integration +) { + namespace field = mean_field::field; + + mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh(); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + + auto fec = field::Field::make_fec(2); + auto finiteElementSpace = + field::Field::make_fespace(mesh, *fec); + + REQUIRE(finiteElementSpace != nullptr); + + const field::FieldDofGridFunctionAdapter adapter = + field::make_field_dof_grid_function_adapter( + *finiteElementSpace + ); + + REQUIRE(adapter.dof_map().is_identity()); + + mfem::Vector reduced(adapter.dof_map().reduced_size()); + for (int dof = 0; dof < reduced.Size(); ++dof) { + reduced(dof) = std::sin(0.23 * static_cast(dof + 1)); + } + + mfem::ParGridFunction gridFunction(finiteElementSpace.get()); + adapter.scatter(reduced, gridFunction); + + const mfem::Vector recovered = adapter.gather(gridFunction); + + REQUIRE(recovered.Size() == reduced.Size()); + for (int dof = 0; dof < reduced.Size(); ++dof) { + CAPTURE(dof); + CHECK(recovered(dof) == reduced(dof)); + } +} + +TEST_CASE( + "Field DOF Grid Function Adapter Rejects Incompatible Maps Spaces And Vectors", + tags::field_dof_integration +) { + namespace field = mean_field::field; + + mfem::Mesh serialMesh = field_dof_map_test_utils::make_split_mesh(); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + + auto fec = field::Field::make_fec(2); + auto finiteElementSpace = + field::Field::make_fespace(mesh, *fec); + + auto otherFec = field::Field::make_fec(2); + auto otherFiniteElementSpace = + field::Field::make_fespace(mesh, *otherFec); + + REQUIRE(finiteElementSpace != nullptr); + REQUIRE(otherFiniteElementSpace != nullptr); + REQUIRE(finiteElementSpace->GetTrueVSize() == otherFiniteElementSpace->GetTrueVSize()); + + const field::FieldDofGridFunctionAdapter adapter = + field::make_field_dof_grid_function_adapter( + *finiteElementSpace + ); + + const mfem::Array empty; + CHECK_THROWS_AS( + (field::FieldDofGridFunctionAdapter( + field::FieldDofMap(finiteElementSpace->GetTrueVSize() + 1, empty), + *finiteElementSpace + )), + std::invalid_argument + ); + + mfem::ParGridFunction gridFunction(finiteElementSpace.get()); + mfem::ParGridFunction otherGridFunction(otherFiniteElementSpace.get()); + + mfem::Vector reduced(adapter.dof_map().reduced_size()); + reduced = 1.0; + + mfem::Vector wrongReduced(adapter.dof_map().reduced_size() + 1); + mfem::Vector wrongOutput(adapter.dof_map().reduced_size() + 1); + + CHECK_THROWS_AS(adapter.gather(otherGridFunction), std::invalid_argument); + CHECK_THROWS_AS(adapter.scatter(reduced, otherGridFunction), std::invalid_argument); + CHECK_THROWS_AS(adapter.scatter_into(reduced, otherGridFunction), std::invalid_argument); + + CHECK_THROWS_AS(adapter.gather(gridFunction, wrongOutput), std::invalid_argument); + CHECK_THROWS_AS(adapter.scatter(wrongReduced, gridFunction), std::invalid_argument); + CHECK_THROWS_AS(adapter.scatter_into(wrongReduced, gridFunction), std::invalid_argument); +} diff --git a/tests/integrators/centrifugal.cpp b/tests/integrators/centrifugal.cpp index 3af3ccd..b7773eb 100644 --- a/tests/integrators/centrifugal.cpp +++ b/tests/integrators/centrifugal.cpp @@ -9,6 +9,20 @@ import test_helpers; using namespace mean_field; namespace { + struct SerialMappingData { + explicit SerialMappingData(mfem::Mesh &mesh) + : compactification_fes(&mesh, &compactification_fec), + compactification_coordinate(&compactification_fes), + mapper(field_dof_test_utils::make_domain_mapper()) { + compactification_coordinate = 0.0; + } + + mfem::H1_FECollection compactification_fec{1, 3}; + mfem::FiniteElementSpace compactification_fes; + mfem::GridFunction compactification_coordinate; + mapping::DomainMapper mapper; + }; + double compute_roche_surface_scale( const double rotation_fraction, const double sine_theta_squared @@ -29,7 +43,7 @@ namespace { TEST_CASE( "Centrifugal Integrator Matches Manufactured Cartesian Load", - tags::unit &tags::solver &tags::integrator &tags::centrifugal + tags::rotation_integrator_unit ) { constexpr int dim = 3; constexpr double density = 1.7; @@ -49,13 +63,15 @@ TEST_CASE( mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; - mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0); + SerialMappingData mapping_data(mesh); mfem::Vector omega(dim); omega = 0.0; omega(2) = omega_value; - integrators::CentrifugalForceIntegrator integrator(domain_mapper, omega); + integrators::CentrifugalForceIntegrator integrator( + mapping_data.mapper, displacement, mapping_data.compactification_coordinate, omega + ); const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0); const mfem::FiniteElement *density_element = density_fes.GetFE(0); @@ -66,8 +82,7 @@ TEST_CASE( quadrature::Policy policy(std::move(rule_set)); quadrature::RuleFactory quadrature_factory(std::move(policy)); - const quadrature::MappingKind mapping_kind = - !domain_mapper.HasDisplacementField() ? quadrature::MappingKind::none : quadrature::MappingKind::general; + const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general; const int position_order = displacement_element->GetOrder(); quadrature_factory.configure_centrifugal( @@ -127,7 +142,7 @@ TEST_CASE( TEST_CASE( "Centrifugal Integrator Jacobian Matches Residual Linearization", - tags::unit &tags::solver &tags::integrator &tags::centrifugal + tags::rotation_integrator_unit ) { constexpr int dim = 3; constexpr double step = 1.0e-6; @@ -147,14 +162,16 @@ TEST_CASE( mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; - mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0); + SerialMappingData mapping_data(mesh); mfem::Vector omega(dim); omega(0) = 0.7; omega(1) = -1.1; omega(2) = 1.6; - integrators::CentrifugalForceIntegrator integrator(domain_mapper, omega); + integrators::CentrifugalForceIntegrator integrator( + mapping_data.mapper, displacement, mapping_data.compactification_coordinate, omega + ); const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0); const mfem::FiniteElement *density_element = density_fes.GetFE(0); @@ -165,8 +182,7 @@ TEST_CASE( quadrature::Policy policy(std::move(rule_set)); quadrature::RuleFactory quadrature_factory(std::move(policy)); - const quadrature::MappingKind mapping_kind = - !domain_mapper.HasDisplacementField() ? quadrature::MappingKind::none : quadrature::MappingKind::general; + const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general; const int position_order = displacement_element->GetOrder(); quadrature_factory.configure_centrifugal( @@ -287,7 +303,7 @@ TEST_CASE( TEST_CASE( "Centrifugal Integrator Preserves Rotation Identities", - tags::unit &tags::solver &tags::integrator &tags::centrifugal + tags::rotation_integrator_unit ) { constexpr int dim = 3; constexpr double density = 1.4; @@ -307,14 +323,16 @@ TEST_CASE( mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; - mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0); + SerialMappingData mapping_data(mesh); mfem::Vector omega(dim); omega(0) = 0.7; omega(1) = -1.1; omega(2) = 1.6; - integrators::CentrifugalForceIntegrator integrator(domain_mapper, omega); + integrators::CentrifugalForceIntegrator integrator( + mapping_data.mapper, displacement, mapping_data.compactification_coordinate, omega + ); const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0); const mfem::FiniteElement *density_element = density_fes.GetFE(0); @@ -325,8 +343,7 @@ TEST_CASE( quadrature::Policy policy(std::move(rule_set)); quadrature::RuleFactory quadrature_factory(std::move(policy)); - const quadrature::MappingKind mapping_kind = - !domain_mapper.HasDisplacementField() ? quadrature::MappingKind::none : quadrature::MappingKind::general; + const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general; const int position_order = displacement_element->GetOrder(); quadrature_factory.configure_centrifugal( @@ -424,7 +441,7 @@ TEST_CASE( TEST_CASE( "Centrifugal Integrator Matches Rotational Virial On Roche Mappings", - tags::integration &tags::solver &tags::integrator &tags::centrifugal + tags::rotation_integrator_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); @@ -486,16 +503,20 @@ TEST_CASE( mfem::VectorFunctionCoefficient displacement_coefficient(dim, rotation_displacement); displacement.ProjectCoefficient(displacement_coefficient); - f.mapping->SetDisplacement(displacement); + *f.displacement = displacement; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate + ); mfem::Vector omega(dim); omega = 0.0; omega(2) = rotation_fraction; - integrators::CentrifugalForceIntegrator integrator(*f.mapping, omega); + integrators::CentrifugalForceIntegrator integrator( + *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, omega + ); - const quadrature::MappingKind mapping_kind = - !f.mapping->HasDisplacementField() ? quadrature::MappingKind::none : quadrature::MappingKind::general; + const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general; f.quadratureFactory->configure_centrifugal( integrator, quadrature::QuadratureRole::discretization, representative_density_element, representative_velocity_element, representative_transformation, position_order, utils::DOMAINS::STELLAR, @@ -560,7 +581,7 @@ TEST_CASE( for (int i = 0; i < velocity_dofs_count; ++i) { const mfem::IntegrationPoint &node = velocity_nodes.IntPoint(i); transformation->SetIntPoint(&node); - f.mapping->GetPhysicalPoint(*transformation, node, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, node, x_physical); for (int d = 0; d < dim; ++d) { position_test_dofs(i + d * velocity_dofs_count) = x_physical(d); @@ -581,14 +602,14 @@ TEST_CASE( const mfem::IntegrationPoint &integration_point = reference_rule.IntPoint(q); transformation->SetIntPoint(&integration_point); - const double signed_map_determinant = f.mapping->ComputeDetJ(*transformation, integration_point); const mapping::VolumeQuadratureContext context = - f.mapping->GetQuadratureContext(*transformation, integration_point); + mapping_evaluator.GetQuadratureContext(*transformation, integration_point); + const double signed_map_determinant = context.detJ; local_minimum_map_determinant = std::min(local_minimum_map_determinant, signed_map_determinant); local_maximum_map_determinant = std::max(local_maximum_map_determinant, signed_map_determinant); - f.mapping->GetPhysicalPoint(*transformation, integration_point, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical); velocity_element->CalcShape(integration_point, velocity_shape); position_test_value = 0.0; @@ -661,13 +682,13 @@ TEST_CASE( CHECK_THAT(relative_position_error, Catch::Matchers::WithinAbs(0.0, position_tolerance)); } - f.mapping->ResetDisplacement(); + *f.displacement = 0.0; } TEST_CASE( "Centrifugal Virial Position Representation Is Consistent At The " "Registered Order", - tags::integration &tags::solver &tags::integrator &tags::centrifugal + tags::rotation_integrator_integration ) { constexpr int dim = 3; constexpr double concentration = 4.0; @@ -728,7 +749,10 @@ TEST_CASE( mfem::VectorFunctionCoefficient displacement_coefficient(dim, rotation_displacement); displacement.ProjectCoefficient(displacement_coefficient); - f.mapping->SetDisplacement(displacement); + *f.displacement = displacement; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate + ); mfem::Vector omega(dim); omega = 0.0; @@ -761,7 +785,7 @@ TEST_CASE( for (int i = 0; i < velocity_dofs_count; ++i) { const mfem::IntegrationPoint &node = velocity_nodes.IntPoint(i); transformation->SetIntPoint(&node); - f.mapping->GetPhysicalPoint(*transformation, node, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, node, x_physical); for (int d = 0; d < dim; ++d) { position_test_dofs(i + d * velocity_dofs_count) = x_physical(d); @@ -780,13 +804,13 @@ TEST_CASE( const mfem::IntegrationPoint &integration_point = reference_rule.IntPoint(q); transformation->SetIntPoint(&integration_point); - const double signed_map_determinant = f.mapping->ComputeDetJ(*transformation, integration_point); const mapping::VolumeQuadratureContext context = - f.mapping->GetQuadratureContext(*transformation, integration_point); + mapping_evaluator.GetQuadratureContext(*transformation, integration_point); + const double signed_map_determinant = context.detJ; local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant); - f.mapping->GetPhysicalPoint(*transformation, integration_point, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical); velocity_element->CalcShape(integration_point, velocity_shape); position_test_value = 0.0; @@ -833,7 +857,7 @@ TEST_CASE( minimum_determinants[rotation_index][order_index] = global_minimum_determinant; } - f.mapping->ResetDisplacement(); + *f.displacement = 0.0; } for (std::size_t rotation_index = 0; rotation_index < rotation_fractions.size(); ++rotation_index) { @@ -853,7 +877,7 @@ TEST_CASE( TEST_CASE( "Centrifugal Virial Position Representation Converges Under H Refinement", - tags::integration &tags::solver &tags::integrator &tags::convergence &tags::h_refinement &tags::centrifugal + tags::rotation_integrator_convergence ) { constexpr int dim = 3; constexpr double concentration = 4.0; @@ -916,7 +940,10 @@ TEST_CASE( mfem::VectorFunctionCoefficient displacement_coefficient(dim, rotation_displacement); displacement.ProjectCoefficient(displacement_coefficient); - f.mapping->SetDisplacement(displacement); + *f.displacement = displacement; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate + ); mfem::Vector omega(dim); omega = 0.0; @@ -949,7 +976,7 @@ TEST_CASE( for (int i = 0; i < velocity_dofs_count; ++i) { const mfem::IntegrationPoint &node = velocity_nodes.IntPoint(i); transformation->SetIntPoint(&node); - f.mapping->GetPhysicalPoint(*transformation, node, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, node, x_physical); for (int d = 0; d < dim; ++d) { position_test_dofs(i + d * velocity_dofs_count) = x_physical(d); @@ -968,13 +995,13 @@ TEST_CASE( const mfem::IntegrationPoint &integration_point = reference_rule.IntPoint(q); transformation->SetIntPoint(&integration_point); - const double signed_map_determinant = f.mapping->ComputeDetJ(*transformation, integration_point); const mapping::VolumeQuadratureContext context = - f.mapping->GetQuadratureContext(*transformation, integration_point); + mapping_evaluator.GetQuadratureContext(*transformation, integration_point); + const double signed_map_determinant = context.detJ; local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant); - f.mapping->GetPhysicalPoint(*transformation, integration_point, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical); velocity_element->CalcShape(integration_point, velocity_shape); position_test_value = 0.0; @@ -1021,7 +1048,7 @@ TEST_CASE( minimum_determinants[rotation_index][refinement_index] = global_minimum_determinant; } - f.mapping->ResetDisplacement(); + *f.displacement = 0.0; } for (std::size_t rotation_index = 0; rotation_index < rotation_fractions.size(); ++rotation_index) { diff --git a/tests/integrators/gravity.cpp b/tests/integrators/gravity.cpp index 16769b1..8ef0eea 100644 --- a/tests/integrators/gravity.cpp +++ b/tests/integrators/gravity.cpp @@ -10,7 +10,7 @@ using namespace mean_field; TEST_CASE( "Gravity Force Integrator Jacobian Matches Residual Linearization", - tags::unit &tags::solver &tags::integrator &tags::gravity + tags::gravity_integrator_unit ) { constexpr int dim = 3; constexpr double finite_difference_step = 1.0e-3; @@ -31,21 +31,20 @@ TEST_CASE( mfem::RT_FECollection gravity_gradient_fec(1, dim); mfem::L2_FECollection gravity_potential_fec(1, dim); mfem::H1_FECollection displacement_fec(2, dim); + mfem::H1_FECollection compactification_fec(1, dim); mfem::FiniteElementSpace velocity_fes(&mesh, &velocity_fec, dim, mfem::Ordering::byVDIM); mfem::FiniteElementSpace density_fes(&mesh, &density_fec); mfem::FiniteElementSpace gravity_gradient_fes(&mesh, &gravity_gradient_fec); mfem::FiniteElementSpace gravity_potential_fes(&mesh, &gravity_potential_fec); mfem::FiniteElementSpace displacement_fes(&mesh, &displacement_fec, dim, mfem::Ordering::byVDIM); + mfem::FiniteElementSpace compactification_fes(&mesh, &compactification_fec); mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; - - mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0); - INFO(std::format("Domain mapping is has displacement field: {}", domain_mapper.HasDisplacementField())); - INFO(std::format("Domain mapping is identity: {}", domain_mapper.CalcIsIdentity())); - - REQUIRE(domain_mapper.CalcIsIdentity()); + mfem::GridFunction compactification_coordinate(&compactification_fes); + compactification_coordinate = 0.0; + mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper(); const mfem::FiniteElement *velocity_element = velocity_fes.GetFE(0); const mfem::FiniteElement *density_element = density_fes.GetFE(0); @@ -130,7 +129,8 @@ TEST_CASE( element_residual[displacement_block] = &displacement_residual; integrators::GravityMomentumIntegrator integrator( - domain_mapper, integrators::GravityForceJacobianMode::field_coupled + domain_mapper, displacement, compactification_coordinate, + integrators::GravityForceJacobianMode::field_coupled ); const int maximum_order = std::max( @@ -268,7 +268,7 @@ TEST_CASE( TEST_CASE( "Gravity Force Integrator Matches Manufactured Cartesian Load", - tags::unit &tags::solver &tags::integrator &tags::gravity + tags::gravity_integrator_unit ) { constexpr int dim = 3; constexpr double tolerance = 1.0e-12; @@ -286,18 +286,23 @@ TEST_CASE( mfem::L2_FECollection density_fec(1, dim); mfem::RT_FECollection gravity_gradient_fec(0, dim); mfem::H1_FECollection displacement_fec(1, dim); + mfem::H1_FECollection compactification_fec(1, dim); mfem::FiniteElementSpace velocity_fes(&mesh, &velocity_fec, dim, mfem::Ordering::byVDIM); mfem::FiniteElementSpace density_fes(&mesh, &density_fec); mfem::FiniteElementSpace gravity_gradient_fes(&mesh, &gravity_gradient_fec); mfem::FiniteElementSpace displacement_fes(&mesh, &displacement_fec, dim, mfem::Ordering::byVDIM); + mfem::FiniteElementSpace compactification_fes(&mesh, &compactification_fec); mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; + mfem::GridFunction compactification_coordinate(&compactification_fes); + compactification_coordinate = 0.0; + mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper(); - mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0); - - REQUIRE(domain_mapper.CalcIsIdentity()); + mapping::GridFunctionMappingEvaluator mapping_evaluator( + domain_mapper, displacement, compactification_coordinate + ); auto reference_density = [](const mfem::Vector &x) { return 1.0 + x(0); }; @@ -376,7 +381,8 @@ TEST_CASE( element_residual[displacement_block] = &displacement_residual; integrators::GravityMomentumIntegrator integrator( - domain_mapper, integrators::GravityForceJacobianMode::field_coupled + domain_mapper, displacement, compactification_coordinate, + integrators::GravityForceJacobianMode::field_coupled ); const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(velocity_element->GetGeomType(), 8); @@ -395,7 +401,7 @@ TEST_CASE( for (int i = 0; i < velocity_dofs_count; ++i) { const mfem::IntegrationPoint &node = velocity_nodes.IntPoint(i); transformation->SetIntPoint(&node); - domain_mapper.GetPhysicalPoint(*transformation, node, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, node, x_physical); test_dofs(i + component * velocity_dofs_count) = coordinate_weight < 0 ? 1.0 : x_physical(coordinate_weight); } @@ -433,7 +439,7 @@ TEST_CASE( } TEST_CASE( "Gravity Force Integrator Preserves Gravity Identities", - tags::unit &tags::solver &tags::integrator &tags::gravity + tags::gravity_integrator_unit ) { constexpr int dim = 3; constexpr double density_value = 1.7; @@ -454,18 +460,22 @@ TEST_CASE( mfem::L2_FECollection density_fec(0, dim); mfem::RT_FECollection gravity_gradient_fec(0, dim); mfem::H1_FECollection displacement_fec(1, dim); + mfem::H1_FECollection compactification_fec(1, dim); mfem::FiniteElementSpace velocity_fes(&mesh, &velocity_fec, dim, mfem::Ordering::byVDIM); mfem::FiniteElementSpace density_fes(&mesh, &density_fec); mfem::FiniteElementSpace gravity_gradient_fes(&mesh, &gravity_gradient_fec); mfem::FiniteElementSpace displacement_fes(&mesh, &displacement_fec, dim, mfem::Ordering::byVDIM); + mfem::FiniteElementSpace compactification_fes(&mesh, &compactification_fec); mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; - - mapping::DomainMapper domain_mapper(displacement, 1.0, 2.0); - - REQUIRE(domain_mapper.HasDisplacementField()); + mfem::GridFunction compactification_coordinate(&compactification_fes); + compactification_coordinate = 0.0; + mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper(); + mapping::GridFunctionMappingEvaluator mapping_evaluator( + domain_mapper, displacement, compactification_coordinate + ); auto radial_gravity = [](const mfem::Vector &x, mfem::Vector &gravity) { gravity.SetSize(3); @@ -543,7 +553,8 @@ TEST_CASE( element_residual[displacement_block] = &displacement_residual; integrators::GravityMomentumIntegrator integrator( - domain_mapper, integrators::GravityForceJacobianMode::field_coupled + domain_mapper, displacement, compactification_coordinate, + integrators::GravityForceJacobianMode::field_coupled ); const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(velocity_element->GetGeomType(), 8); @@ -609,7 +620,7 @@ TEST_CASE( for (int i = 0; i < velocity_dofs_count; ++i) { const mfem::IntegrationPoint &node = velocity_nodes.IntPoint(i); transformation->SetIntPoint(&node); - domain_mapper.GetPhysicalPoint(*transformation, node, x_physical); + mapping_evaluator.GetPhysicalPoint(*transformation, node, x_physical); for (int component = 0; component < dim; ++component) { centered_position(component) = x_physical(component) - 0.5; @@ -714,4 +725,4 @@ TEST_CASE( Catch::Matchers::WithinAbs(0.0, tolerance) ); CHECK_THAT(zero_density_field_action.Norml2(), Catch::Matchers::WithinAbs(0.0, tolerance)); -} \ No newline at end of file +} diff --git a/tests/mapping/domain_mapper.cpp b/tests/mapping/domain_mapper.cpp index faa492e..1bcb387 100644 --- a/tests/mapping/domain_mapper.cpp +++ b/tests/mapping/domain_mapper.cpp @@ -13,2685 +13,3011 @@ using namespace mean_field; using Catch::Matchers::WithinAbs; namespace { - constexpr int dimension = 3; - constexpr double tolerance = 1.0e-12; +constexpr int dimension = 3; +constexpr double tolerance = 1.0e-12; - std::unique_ptr make_kelvin_compactification() { - return std::make_unique( - mapping::compactification::options::KelvinCompactificationOptions{.r_star_ref = 1.0, .r_inf_ref = 4.0} - ); +std::unique_ptr +make_kelvin_compactification() { + return std::make_unique( + mapping::compactification::options::KelvinCompactificationOptions{ + .r_star_ref = 1.0, .r_inf_ref = 4.0}); +} + +mfem::DenseMatrix make_identity_matrix(const int size) { + mfem::DenseMatrix matrix(size); + matrix = 0.0; + for (int i = 0; i < size; ++i) + matrix(i, i) = 1.0; + return matrix; +} + +mfem::Vector +make_constant_compactification_dofs(const mfem::FiniteElement &element, + const double value = 0.0) { + mfem::Vector dofs(element.GetDof()); + dofs = value; + return dofs; +} + +template +mfem::Vector +make_compactification_element_dofs(const mfem::FiniteElement &element, + mfem::ElementTransformation &transformation, + Function &&function) { + const mfem::IntegrationRule &nodes = element.GetNodes(); + REQUIRE(nodes.GetNPoints() == element.GetDof()); + + mfem::Vector dofs(element.GetDof()); + mfem::Vector reference_position(dimension); + + for (int i = 0; i < element.GetDof(); ++i) { + transformation.Transform(nodes.IntPoint(i), reference_position); + dofs(i) = function(reference_position); + } + + return dofs; +} + +class ElementMappingDataOwner { +public: + explicit ElementMappingDataOwner( + const mapping::ElementDisplacementData &displacement) + : m_compactification( + displacement.GetElement(), + make_constant_compactification_dofs(displacement.GetElement())), + m_element_data{.displacement = displacement, + .compactification = m_compactification} {} + + ElementMappingDataOwner(const mapping::ElementDisplacementData &displacement, + const mfem::FiniteElement &compactification_element, + const mfem::Vector &compactification_dofs) + : m_compactification(compactification_element, compactification_dofs), + m_element_data{.displacement = displacement, + .compactification = m_compactification} {} + + ElementMappingDataOwner(const ElementMappingDataOwner &) = delete; + ElementMappingDataOwner &operator=(const ElementMappingDataOwner &) = delete; + ElementMappingDataOwner(ElementMappingDataOwner &&) = delete; + ElementMappingDataOwner &operator=(ElementMappingDataOwner &&) = delete; + + [[nodiscard]] const mapping::ElementMappingData &Get() const noexcept { + return m_element_data; + } + +private: + mapping::ElementCompactificationData m_compactification; + mapping::ElementMappingData m_element_data; +}; + +void check_vector(const mfem::Vector &actual, const mfem::Vector &expected, + const double comparison_tolerance = tolerance) { + REQUIRE(actual.Size() == expected.Size()); + for (int i = 0; i < actual.Size(); ++i) + CHECK_THAT(actual(i), WithinAbs(expected(i), comparison_tolerance)); +} + +void check_matrix(const mfem::DenseMatrix &actual, + const mfem::DenseMatrix &expected, + const double comparison_tolerance = tolerance) { + REQUIRE(actual.Height() == expected.Height()); + REQUIRE(actual.Width() == expected.Width()); + + for (int i = 0; i < actual.Height(); ++i) { + for (int j = 0; j < actual.Width(); ++j) + CHECK_THAT(actual(i, j), WithinAbs(expected(i, j), comparison_tolerance)); + } +} + +struct SingleElementFixture { + mfem::Mesh mesh; + mfem::H1_FECollection displacement_collection; + mfem::FiniteElementSpace displacement_space; + + SingleElementFixture() + : mesh(mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON, + 2.0, 3.0, 4.0)), + displacement_collection(1, dimension), + displacement_space(&mesh, &displacement_collection, dimension, + mfem::Ordering::byVDIM) {} + + [[nodiscard]] const mfem::FiniteElement &GetElement() const { + return *displacement_space.GetFE(0); + } + + [[nodiscard]] mfem::Vector MakeZeroElementDofs() const { + mfem::Vector element_dofs(GetElement().GetDof() * dimension); + element_dofs = 0.0; + return element_dofs; + } +}; + +mfem::Vector make_vector(const double x, const double y, const double z) { + mfem::Vector vector(3); + vector(0) = x; + vector(1) = y; + vector(2) = z; + return vector; +} + +mfem::DenseMatrix make_affine_displacement_gradient() { + mfem::DenseMatrix gradient(3); + gradient(0, 0) = 0.10; + gradient(0, 1) = 0.04; + gradient(0, 2) = -0.02; + gradient(1, 0) = -0.03; + gradient(1, 1) = 0.08; + gradient(1, 2) = 0.01; + gradient(2, 0) = 0.02; + gradient(2, 1) = -0.01; + gradient(2, 2) = -0.05; + return gradient; +} + +mfem::Vector +make_affine_element_dofs(const mfem::FiniteElement &element, + mfem::ElementTransformation &transformation, + const mfem::DenseMatrix &displacement_gradient, + const mfem::Vector &displacement_offset, + const mfem::Ordering::Type ordering) { + const int dof_count = element.GetDof(); + const int field_dimension = displacement_offset.Size(); + const mfem::IntegrationRule &nodes = element.GetNodes(); + + REQUIRE(nodes.GetNPoints() == dof_count); + + mfem::Vector element_dofs(dof_count * field_dimension); + mfem::Vector reference_position(field_dimension); + mfem::Vector displacement(field_dimension); + + for (int i = 0; i < dof_count; ++i) { + transformation.Transform(nodes.IntPoint(i), reference_position); + displacement_gradient.Mult(reference_position, displacement); + displacement += displacement_offset; + + for (int component = 0; component < field_dimension; ++component) { + const int index = ordering == mfem::Ordering::byNODES + ? i + component * dof_count + : component + i * field_dimension; + element_dofs(index) = displacement(component); } + } - mfem::DenseMatrix make_identity_matrix(const int size) { - mfem::DenseMatrix matrix(size); - matrix = 0.0; - for (int i = 0; i < size; ++i) - matrix(i, i) = 1.0; - return matrix; + return element_dofs; +} + +mfem::DenseMatrix +make_deformation_jacobian(const mfem::DenseMatrix &displacement_gradient) { + mfem::DenseMatrix deformation_jacobian = + make_identity_matrix(displacement_gradient.Height()); + deformation_jacobian.Add(1.0, displacement_gradient); + return deformation_jacobian; +} + +mfem::Vector evaluate_affine_physical_position( + const mfem::Vector &reference_position, + const mfem::DenseMatrix &displacement_gradient, + const mfem::Vector &displacement_offset) { + mfem::Vector physical_position(reference_position); + mfem::Vector displacement(reference_position.Size()); + displacement_gradient.Mult(reference_position, displacement); + physical_position += displacement; + physical_position += displacement_offset; + return physical_position; +} + +void check_point_context(const mapping::MappingPointContext &actual, + const mapping::MappingPointContext &expected, + const double comparison_tolerance = tolerance) { + CHECK(actual.compactified == expected.compactified); + check_vector(actual.reference_position, expected.reference_position, + comparison_tolerance); + check_vector(actual.displaced_position, expected.displaced_position, + comparison_tolerance); + check_vector(actual.physical_position, expected.physical_position, + comparison_tolerance); + check_matrix(actual.displacement_jacobian, expected.displacement_jacobian, + comparison_tolerance); + check_matrix(actual.mapping_jacobian, expected.mapping_jacobian, + comparison_tolerance); + check_matrix(actual.inverse_mapping_jacobian, + expected.inverse_mapping_jacobian, comparison_tolerance); + CHECK_THAT(actual.mapping_determinant, + WithinAbs(expected.mapping_determinant, comparison_tolerance)); +} + +constexpr double polynomial_tolerance = 2.0e-11; +constexpr double difference_step = 2.0e-6; + +struct QuadraticElementFixture { + mfem::Mesh mesh; + mfem::H1_FECollection displacement_collection; + mfem::FiniteElementSpace displacement_space; + + QuadraticElementFixture() + : mesh(mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON, + 2.0, 3.0, 4.0)), + displacement_collection(2, dimension), + displacement_space(&mesh, &displacement_collection, dimension, + mfem::Ordering::byVDIM) {} + + [[nodiscard]] const mfem::FiniteElement &GetElement() const { + return *displacement_space.GetFE(0); + } +}; + +template +mfem::Vector +make_function_element_dofs(const mfem::FiniteElement &element, + mfem::ElementTransformation &transformation, + Function &&function, + const mfem::Ordering::Type ordering) { + const int dof_count = element.GetDof(); + const mfem::IntegrationRule &nodes = element.GetNodes(); + + REQUIRE(nodes.GetNPoints() == dof_count); + + mfem::Vector element_dofs(dof_count * dimension); + mfem::Vector reference_position(dimension); + mfem::Vector value(dimension); + + for (int i = 0; i < dof_count; ++i) { + transformation.Transform(nodes.IntPoint(i), reference_position); + function(reference_position, value); + + for (int component = 0; component < dimension; ++component) { + const int index = ordering == mfem::Ordering::byNODES + ? i + component * dof_count + : component + i * dimension; + element_dofs(index) = value(component); } + } - mfem::Vector make_constant_compactification_dofs( - const mfem::FiniteElement &element, - const double value = 0.0 - ) { - mfem::Vector dofs(element.GetDof()); - dofs = value; - return dofs; + return element_dofs; +} + +void evaluate_quadratic_displacement(const mfem::Vector &position, + mfem::Vector &displacement) { + const double x = position(0); + const double y = position(1); + const double z = position(2); + + displacement.SetSize(dimension); + displacement(0) = 0.01 + 0.010 * x * x + 0.005 * y * z; + displacement(1) = -0.02 - 0.004 * x * y + 0.006 * z * z; + displacement(2) = 0.015 + 0.003 * x * z - 0.002 * y * y; +} + +mfem::DenseMatrix +evaluate_quadratic_displacement_gradient(const mfem::Vector &position) { + const double x = position(0); + const double y = position(1); + const double z = position(2); + + mfem::DenseMatrix gradient(dimension); + gradient(0, 0) = 0.020 * x; + gradient(0, 1) = 0.005 * z; + gradient(0, 2) = 0.005 * y; + gradient(1, 0) = -0.004 * y; + gradient(1, 1) = -0.004 * x; + gradient(1, 2) = 0.012 * z; + gradient(2, 0) = 0.003 * z; + gradient(2, 1) = -0.004 * y; + gradient(2, 2) = 0.003 * x; + return gradient; +} + +void evaluate_quadratic_direction(const mfem::Vector &position, + mfem::Vector &direction) { + const double x = position(0); + const double y = position(1); + const double z = position(2); + + direction.SetSize(dimension); + direction(0) = 0.020 * x - 0.010 * y * z; + direction(1) = -0.015 * y + 0.005 * x * z; + direction(2) = 0.010 * z + 0.004 * x * y; +} + +mfem::DenseMatrix +evaluate_quadratic_direction_gradient(const mfem::Vector &position) { + const double x = position(0); + const double y = position(1); + const double z = position(2); + + mfem::DenseMatrix gradient(dimension); + gradient(0, 0) = 0.020; + gradient(0, 1) = -0.010 * z; + gradient(0, 2) = -0.010 * y; + gradient(1, 0) = 0.005 * z; + gradient(1, 1) = -0.015; + gradient(1, 2) = 0.005 * x; + gradient(2, 0) = 0.004 * y; + gradient(2, 1) = 0.004 * x; + gradient(2, 2) = 0.010; + return gradient; +} + +void check_vector_central_difference(const mfem::Vector &plus, + const mfem::Vector &minus, + const mfem::Vector &expected, + const double step, + const double comparison_tolerance) { + REQUIRE(plus.Size() == minus.Size()); + REQUIRE(plus.Size() == expected.Size()); + + for (int i = 0; i < expected.Size(); ++i) { + const double finite_difference = (plus(i) - minus(i)) / (2.0 * step); + CHECK_THAT(finite_difference, WithinAbs(expected(i), comparison_tolerance)); + } +} + +void check_matrix_central_difference(const mfem::DenseMatrix &plus, + const mfem::DenseMatrix &minus, + const mfem::DenseMatrix &expected, + const double step, + const double comparison_tolerance) { + REQUIRE(plus.Height() == minus.Height()); + REQUIRE(plus.Width() == minus.Width()); + REQUIRE(plus.Height() == expected.Height()); + REQUIRE(plus.Width() == expected.Width()); + + for (int i = 0; i < expected.Height(); ++i) { + for (int j = 0; j < expected.Width(); ++j) { + const double finite_difference = + (plus(i, j) - minus(i, j)) / (2.0 * step); + CHECK_THAT(finite_difference, + WithinAbs(expected(i, j), comparison_tolerance)); } + } +} - template - mfem::Vector make_compactification_element_dofs( - const mfem::FiniteElement &element, - mfem::ElementTransformation &transformation, - Function &&function - ) { - const mfem::IntegrationRule &nodes = element.GetNodes(); - REQUIRE(nodes.GetNPoints() == element.GetDof()); +struct QuadraticMappingData { + mfem::Vector base_dofs; + mfem::Vector direction_dofs; + mfem::Vector plus_dofs; + mfem::Vector minus_dofs; - mfem::Vector dofs(element.GetDof()); - mfem::Vector reference_position(dimension); + QuadraticMappingData(const mfem::FiniteElement &element, + mfem::ElementTransformation &transformation) { + base_dofs = make_function_element_dofs(element, transformation, + evaluate_quadratic_displacement, + mfem::Ordering::byVDIM); + direction_dofs = make_function_element_dofs(element, transformation, + evaluate_quadratic_direction, + mfem::Ordering::byVDIM); + plus_dofs = base_dofs; + minus_dofs = base_dofs; + plus_dofs.Add(difference_step, direction_dofs); + minus_dofs.Add(-difference_step, direction_dofs); + } +}; - for (int i = 0; i < element.GetDof(); ++i) { - transformation.Transform(nodes.IntPoint(i), reference_position); - dofs(i) = function(reference_position); - } +void check_scalar_relative(const double actual, const double expected, + const double relative_tolerance, + const double absolute_tolerance = 1.0e-11) { + CHECK_THAT(actual, + WithinAbs(expected, absolute_tolerance + + relative_tolerance * std::abs(expected))); +} - return dofs; +void check_vector_central_difference_relative(const mfem::Vector &plus, + const mfem::Vector &minus, + const mfem::Vector &expected, + const double step, + const double relative_tolerance) { + REQUIRE(plus.Size() == minus.Size()); + REQUIRE(plus.Size() == expected.Size()); + + for (int i = 0; i < expected.Size(); ++i) { + const double finite_difference = (plus(i) - minus(i)) / (2.0 * step); + check_scalar_relative(finite_difference, expected(i), relative_tolerance); + } +} + +void check_matrix_central_difference_relative(const mfem::DenseMatrix &plus, + const mfem::DenseMatrix &minus, + const mfem::DenseMatrix &expected, + const double step, + const double relative_tolerance) { + REQUIRE(plus.Height() == minus.Height()); + REQUIRE(plus.Width() == minus.Width()); + REQUIRE(plus.Height() == expected.Height()); + REQUIRE(plus.Width() == expected.Width()); + + for (int i = 0; i < expected.Height(); ++i) { + for (int j = 0; j < expected.Width(); ++j) { + const double finite_difference = + (plus(i, j) - minus(i, j)) / (2.0 * step); + check_scalar_relative(finite_difference, expected(i, j), + relative_tolerance); } + } +} - class ElementMappingDataOwner { - public: - explicit ElementMappingDataOwner(const mapping::ElementDisplacementData &displacement) - : m_compactification( - displacement.GetElement(), - make_constant_compactification_dofs(displacement.GetElement()) - ), - m_element_data{ - .displacement = displacement, - .compactification = m_compactification - } { - } +mfem::Vector evaluate_reference_hdiv_field(const mfem::Vector &position) { + const double x = position(0); + const double y = position(1); + const double z = position(2); + return make_vector(x * x + 0.1 * y, y * y - 0.2 * z, z * z + 0.3 * x); +} - ElementMappingDataOwner( - const mapping::ElementDisplacementData &displacement, - const mfem::FiniteElement &compactification_element, - const mfem::Vector &compactification_dofs - ) - : m_compactification( - compactification_element, - compactification_dofs - ), - m_element_data{ - .displacement = displacement, - .compactification = m_compactification - } { - } +double evaluate_reference_hdiv_divergence(const mfem::Vector &position) { + return 2.0 * (position(0) + position(1) + position(2)); +} - ElementMappingDataOwner(const ElementMappingDataOwner &) = delete; - ElementMappingDataOwner &operator=(const ElementMappingDataOwner &) = delete; - ElementMappingDataOwner(ElementMappingDataOwner &&) = delete; - ElementMappingDataOwner &operator=(ElementMappingDataOwner &&) = delete; +mfem::Vector matrix_curl(const mfem::DenseMatrix &gradient) { + return make_vector(gradient(2, 1) - gradient(1, 2), + gradient(0, 2) - gradient(2, 0), + gradient(1, 0) - gradient(0, 1)); +} - [[nodiscard]] const mapping::ElementMappingData &Get() const noexcept { - return m_element_data; - } +void check_centered_difference(const double analytic, + const double finite_difference, + const double plus_value, + const double minus_value, const double step, + const double relative_tolerance = 2.0e-6, + const double absolute_tolerance = 1.0e-11) { + const double derivative_scale = + std::max(std::abs(analytic), std::abs(finite_difference)); + const double primal_scale = + std::max(std::abs(plus_value), std::abs(minus_value)); + const double roundoff_tolerance = + 8.0 * std::numeric_limits::epsilon() * primal_scale / step; + const double tolerance = absolute_tolerance + + relative_tolerance * derivative_scale + + roundoff_tolerance; - private: - mapping::ElementCompactificationData m_compactification; - mapping::ElementMappingData m_element_data; - }; + CHECK_THAT(finite_difference, + Catch::Matchers::WithinAbs(analytic, tolerance)); +} - void check_vector( - const mfem::Vector &actual, - const mfem::Vector &expected, - const double comparison_tolerance = tolerance - ) { - REQUIRE(actual.Size() == expected.Size()); - for (int i = 0; i < actual.Size(); ++i) - CHECK_THAT(actual(i), WithinAbs(expected(i), comparison_tolerance)); - } +double relative_vector_difference(const mfem::Vector &lhs, + const mfem::Vector &rhs) { + mfem::Vector difference(lhs); + difference -= rhs; - void check_matrix( - const mfem::DenseMatrix &actual, - const mfem::DenseMatrix &expected, - const double comparison_tolerance = tolerance - ) { - REQUIRE(actual.Height() == expected.Height()); - REQUIRE(actual.Width() == expected.Width()); + const double scale = std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-12}); + return difference.Norml2() / scale; +} - for (int i = 0; i < actual.Height(); ++i) { - for (int j = 0; j < actual.Width(); ++j) - CHECK_THAT(actual(i, j), WithinAbs(expected(i, j), comparison_tolerance)); - } - } +double relative_matrix_difference(const mfem::DenseMatrix &lhs, + const mfem::DenseMatrix &rhs) { + mfem::DenseMatrix difference(lhs); + difference -= rhs; - struct SingleElementFixture { - mfem::Mesh mesh; - mfem::H1_FECollection displacement_collection; - mfem::FiniteElementSpace displacement_space; - - SingleElementFixture() - : mesh( - mfem::Mesh::MakeCartesian3D( - 1, - 1, - 1, - mfem::Element::HEXAHEDRON, - 2.0, - 3.0, - 4.0 - ) - ), - displacement_collection( - 1, - dimension - ), - displacement_space( - &mesh, - &displacement_collection, - dimension, - mfem::Ordering::byVDIM - ) { - } - - [[nodiscard]] const mfem::FiniteElement &GetElement() const { - return *displacement_space.GetFE(0); - } - - [[nodiscard]] mfem::Vector MakeZeroElementDofs() const { - mfem::Vector element_dofs(GetElement().GetDof() * dimension); - element_dofs = 0.0; - return element_dofs; - } - }; - - mfem::Vector make_vector( - const double x, - const double y, - const double z - ) { - mfem::Vector vector(3); - vector(0) = x; - vector(1) = y; - vector(2) = z; - return vector; - } - - mfem::DenseMatrix make_affine_displacement_gradient() { - mfem::DenseMatrix gradient(3); - gradient(0, 0) = 0.10; - gradient(0, 1) = 0.04; - gradient(0, 2) = -0.02; - gradient(1, 0) = -0.03; - gradient(1, 1) = 0.08; - gradient(1, 2) = 0.01; - gradient(2, 0) = 0.02; - gradient(2, 1) = -0.01; - gradient(2, 2) = -0.05; - return gradient; - } - - mfem::Vector make_affine_element_dofs( - const mfem::FiniteElement &element, - mfem::ElementTransformation &transformation, - const mfem::DenseMatrix &displacement_gradient, - const mfem::Vector &displacement_offset, - const mfem::Ordering::Type ordering - ) { - const int dof_count = element.GetDof(); - const int field_dimension = displacement_offset.Size(); - const mfem::IntegrationRule &nodes = element.GetNodes(); - - REQUIRE(nodes.GetNPoints() == dof_count); - - mfem::Vector element_dofs(dof_count * field_dimension); - mfem::Vector reference_position(field_dimension); - mfem::Vector displacement(field_dimension); - - for (int i = 0; i < dof_count; ++i) { - transformation.Transform(nodes.IntPoint(i), reference_position); - displacement_gradient.Mult(reference_position, displacement); - displacement += displacement_offset; - - for (int component = 0; component < field_dimension; ++component) { - const int index = - ordering == mfem::Ordering::byNODES ? i + component * dof_count : component + i * field_dimension; - element_dofs(index) = displacement(component); - } - } - - return element_dofs; - } - - mfem::DenseMatrix make_deformation_jacobian(const mfem::DenseMatrix &displacement_gradient) { - mfem::DenseMatrix deformation_jacobian = make_identity_matrix(displacement_gradient.Height()); - deformation_jacobian.Add(1.0, displacement_gradient); - return deformation_jacobian; - } - - mfem::Vector evaluate_affine_physical_position( - const mfem::Vector &reference_position, - const mfem::DenseMatrix &displacement_gradient, - const mfem::Vector &displacement_offset - ) { - mfem::Vector physical_position(reference_position); - mfem::Vector displacement(reference_position.Size()); - displacement_gradient.Mult(reference_position, displacement); - physical_position += displacement; - physical_position += displacement_offset; - return physical_position; - } - - void check_point_context( - const mapping::MappingPointContext &actual, - const mapping::MappingPointContext &expected, - const double comparison_tolerance = tolerance - ) { - CHECK(actual.compactified == expected.compactified); - check_vector(actual.reference_position, expected.reference_position, comparison_tolerance); - check_vector(actual.displaced_position, expected.displaced_position, comparison_tolerance); - check_vector(actual.physical_position, expected.physical_position, comparison_tolerance); - check_matrix(actual.displacement_jacobian, expected.displacement_jacobian, comparison_tolerance); - check_matrix(actual.mapping_jacobian, expected.mapping_jacobian, comparison_tolerance); - check_matrix(actual.inverse_mapping_jacobian, expected.inverse_mapping_jacobian, comparison_tolerance); - CHECK_THAT(actual.mapping_determinant, WithinAbs(expected.mapping_determinant, comparison_tolerance)); - } - - constexpr double polynomial_tolerance = 2.0e-11; - constexpr double difference_step = 2.0e-6; - - struct QuadraticElementFixture { - mfem::Mesh mesh; - mfem::H1_FECollection displacement_collection; - mfem::FiniteElementSpace displacement_space; - - QuadraticElementFixture() - : mesh( - mfem::Mesh::MakeCartesian3D( - 1, - 1, - 1, - mfem::Element::HEXAHEDRON, - 2.0, - 3.0, - 4.0 - ) - ), - displacement_collection( - 2, - dimension - ), - displacement_space( - &mesh, - &displacement_collection, - dimension, - mfem::Ordering::byVDIM - ) { - } - - [[nodiscard]] const mfem::FiniteElement &GetElement() const { - return *displacement_space.GetFE(0); - } - }; - - template - mfem::Vector make_function_element_dofs( - const mfem::FiniteElement &element, - mfem::ElementTransformation &transformation, - Function &&function, - const mfem::Ordering::Type ordering - ) { - const int dof_count = element.GetDof(); - const mfem::IntegrationRule &nodes = element.GetNodes(); - - REQUIRE(nodes.GetNPoints() == dof_count); - - mfem::Vector element_dofs(dof_count * dimension); - mfem::Vector reference_position(dimension); - mfem::Vector value(dimension); - - for (int i = 0; i < dof_count; ++i) { - transformation.Transform(nodes.IntPoint(i), reference_position); - function(reference_position, value); - - for (int component = 0; component < dimension; ++component) { - const int index = - ordering == mfem::Ordering::byNODES ? i + component * dof_count : component + i * dimension; - element_dofs(index) = value(component); - } - } - - return element_dofs; - } - - void evaluate_quadratic_displacement( - const mfem::Vector &position, - mfem::Vector &displacement - ) { - const double x = position(0); - const double y = position(1); - const double z = position(2); - - displacement.SetSize(dimension); - displacement(0) = 0.01 + 0.010 * x * x + 0.005 * y * z; - displacement(1) = -0.02 - 0.004 * x * y + 0.006 * z * z; - displacement(2) = 0.015 + 0.003 * x * z - 0.002 * y * y; - } - - mfem::DenseMatrix evaluate_quadratic_displacement_gradient(const mfem::Vector &position) { - const double x = position(0); - const double y = position(1); - const double z = position(2); - - mfem::DenseMatrix gradient(dimension); - gradient(0, 0) = 0.020 * x; - gradient(0, 1) = 0.005 * z; - gradient(0, 2) = 0.005 * y; - gradient(1, 0) = -0.004 * y; - gradient(1, 1) = -0.004 * x; - gradient(1, 2) = 0.012 * z; - gradient(2, 0) = 0.003 * z; - gradient(2, 1) = -0.004 * y; - gradient(2, 2) = 0.003 * x; - return gradient; - } - - void evaluate_quadratic_direction( - const mfem::Vector &position, - mfem::Vector &direction - ) { - const double x = position(0); - const double y = position(1); - const double z = position(2); - - direction.SetSize(dimension); - direction(0) = 0.020 * x - 0.010 * y * z; - direction(1) = -0.015 * y + 0.005 * x * z; - direction(2) = 0.010 * z + 0.004 * x * y; - } - - mfem::DenseMatrix evaluate_quadratic_direction_gradient(const mfem::Vector &position) { - const double x = position(0); - const double y = position(1); - const double z = position(2); - - mfem::DenseMatrix gradient(dimension); - gradient(0, 0) = 0.020; - gradient(0, 1) = -0.010 * z; - gradient(0, 2) = -0.010 * y; - gradient(1, 0) = 0.005 * z; - gradient(1, 1) = -0.015; - gradient(1, 2) = 0.005 * x; - gradient(2, 0) = 0.004 * y; - gradient(2, 1) = 0.004 * x; - gradient(2, 2) = 0.010; - return gradient; - } - - void check_vector_central_difference( - const mfem::Vector &plus, - const mfem::Vector &minus, - const mfem::Vector &expected, - const double step, - const double comparison_tolerance - ) { - REQUIRE(plus.Size() == minus.Size()); - REQUIRE(plus.Size() == expected.Size()); - - for (int i = 0; i < expected.Size(); ++i) { - const double finite_difference = (plus(i) - minus(i)) / (2.0 * step); - CHECK_THAT(finite_difference, WithinAbs(expected(i), comparison_tolerance)); - } - } - - void check_matrix_central_difference( - const mfem::DenseMatrix &plus, - const mfem::DenseMatrix &minus, - const mfem::DenseMatrix &expected, - const double step, - const double comparison_tolerance - ) { - REQUIRE(plus.Height() == minus.Height()); - REQUIRE(plus.Width() == minus.Width()); - REQUIRE(plus.Height() == expected.Height()); - REQUIRE(plus.Width() == expected.Width()); - - for (int i = 0; i < expected.Height(); ++i) { - for (int j = 0; j < expected.Width(); ++j) { - const double finite_difference = (plus(i, j) - minus(i, j)) / (2.0 * step); - CHECK_THAT(finite_difference, WithinAbs(expected(i, j), comparison_tolerance)); - } - } - } - - struct QuadraticMappingData { - mfem::Vector base_dofs; - mfem::Vector direction_dofs; - mfem::Vector plus_dofs; - mfem::Vector minus_dofs; - - QuadraticMappingData( - const mfem::FiniteElement &element, - mfem::ElementTransformation &transformation - ) { - base_dofs = make_function_element_dofs( - element, transformation, evaluate_quadratic_displacement, mfem::Ordering::byVDIM - ); - direction_dofs = make_function_element_dofs( - element, transformation, evaluate_quadratic_direction, mfem::Ordering::byVDIM - ); - plus_dofs = base_dofs; - minus_dofs = base_dofs; - plus_dofs.Add(difference_step, direction_dofs); - minus_dofs.Add(-difference_step, direction_dofs); - } - }; - - void check_scalar_relative( - const double actual, - const double expected, - const double relative_tolerance, - const double absolute_tolerance = 1.0e-11 - ) { - CHECK_THAT(actual, WithinAbs(expected, absolute_tolerance + relative_tolerance * std::abs(expected))); - } - - void check_vector_central_difference_relative( - const mfem::Vector &plus, - const mfem::Vector &minus, - const mfem::Vector &expected, - const double step, - const double relative_tolerance - ) { - REQUIRE(plus.Size() == minus.Size()); - REQUIRE(plus.Size() == expected.Size()); - - for (int i = 0; i < expected.Size(); ++i) { - const double finite_difference = (plus(i) - minus(i)) / (2.0 * step); - check_scalar_relative(finite_difference, expected(i), relative_tolerance); - } - } - - void check_matrix_central_difference_relative( - const mfem::DenseMatrix &plus, - const mfem::DenseMatrix &minus, - const mfem::DenseMatrix &expected, - const double step, - const double relative_tolerance - ) { - REQUIRE(plus.Height() == minus.Height()); - REQUIRE(plus.Width() == minus.Width()); - REQUIRE(plus.Height() == expected.Height()); - REQUIRE(plus.Width() == expected.Width()); - - for (int i = 0; i < expected.Height(); ++i) { - for (int j = 0; j < expected.Width(); ++j) { - const double finite_difference = (plus(i, j) - minus(i, j)) / (2.0 * step); - check_scalar_relative(finite_difference, expected(i, j), relative_tolerance); - } - } - } - - mfem::Vector evaluate_reference_hdiv_field(const mfem::Vector &position) { - const double x = position(0); - const double y = position(1); - const double z = position(2); - return make_vector(x * x + 0.1 * y, y * y - 0.2 * z, z * z + 0.3 * x); - } - - double evaluate_reference_hdiv_divergence(const mfem::Vector &position) { - return 2.0 * (position(0) + position(1) + position(2)); - } - - mfem::Vector matrix_curl(const mfem::DenseMatrix &gradient) { - return make_vector( - gradient(2, 1) - gradient(1, 2), gradient(0, 2) - gradient(2, 0), gradient(1, 0) - gradient(0, 1) - ); - } - - void check_centered_difference( - const double analytic, - const double finite_difference, - const double plus_value, - const double minus_value, - const double step, - const double relative_tolerance = 2.0e-6, - const double absolute_tolerance = 1.0e-11 - ) { - const double derivative_scale = std::max(std::abs(analytic), std::abs(finite_difference)); - const double primal_scale = std::max(std::abs(plus_value), std::abs(minus_value)); - const double roundoff_tolerance = 8.0 * std::numeric_limits::epsilon() * primal_scale / step; - const double tolerance = absolute_tolerance + relative_tolerance * derivative_scale + roundoff_tolerance; - - CHECK_THAT(finite_difference, Catch::Matchers::WithinAbs(analytic, tolerance)); - } - - double relative_vector_difference( - const mfem::Vector &lhs, - const mfem::Vector &rhs - ) { - mfem::Vector difference(lhs); - difference -= rhs; - - const double scale = std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-12}); - return difference.Norml2() / scale; - } - - double relative_matrix_difference( - const mfem::DenseMatrix &lhs, - const mfem::DenseMatrix &rhs - ) { - mfem::DenseMatrix difference(lhs); - difference -= rhs; - - const double scale = std::max({lhs.FNorm(), rhs.FNorm(), 1.0e-12}); - return difference.FNorm() / scale; - } + const double scale = std::max({lhs.FNorm(), rhs.FNorm(), 1.0e-12}); + return difference.FNorm() / scale; +} } // namespace -TEST_CASE( - "Element Displacement Data Preserves MFEM Ordering", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; +TEST_CASE("Element Displacement Data Preserves MFEM Ordering", + tags::unit &tags::mapping) { + SingleElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - const int dof_count = element.GetDof(); + const mfem::FiniteElement &element = fixture.GetElement(); + const int dof_count = element.GetDof(); - mfem::Vector by_vdim_dofs(dof_count * dimension); - mfem::Vector by_nodes_dofs(dof_count * dimension); + mfem::Vector by_vdim_dofs(dof_count * dimension); + mfem::Vector by_nodes_dofs(dof_count * dimension); - for (int i = 0; i < dof_count; ++i) { - for (int component = 0; component < dimension; ++component) { - const double value = 100.0 * component + i + 1.0; - by_vdim_dofs(component + i * dimension) = value; - by_nodes_dofs(i + component * dof_count) = value; - } + for (int i = 0; i < dof_count; ++i) { + for (int component = 0; component < dimension; ++component) { + const double value = 100.0 * component + i + 1.0; + by_vdim_dofs(component + i * dimension) = value; + by_nodes_dofs(i + component * dof_count) = value; } + } - const mapping::ElementDisplacementData by_vdim_data(element, by_vdim_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData by_nodes_data(element, by_nodes_dofs, mfem::Ordering::byNODES); + const mapping::ElementDisplacementData by_vdim_data(element, by_vdim_dofs, + mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData by_nodes_data(element, by_nodes_dofs, + mfem::Ordering::byNODES); - REQUIRE(&by_vdim_data.GetElement() == &element); - REQUIRE(&by_nodes_data.GetElement() == &element); - REQUIRE(by_vdim_data.GetDimension() == dimension); - REQUIRE(by_nodes_data.GetDimension() == dimension); - REQUIRE(by_vdim_data.GetDofCount() == dof_count); - REQUIRE(by_nodes_data.GetDofCount() == dof_count); - REQUIRE(by_vdim_data.GetOrdering() == mfem::Ordering::byVDIM); - REQUIRE(by_nodes_data.GetOrdering() == mfem::Ordering::byNODES); + REQUIRE(&by_vdim_data.GetElement() == &element); + REQUIRE(&by_nodes_data.GetElement() == &element); + REQUIRE(by_vdim_data.GetDimension() == dimension); + REQUIRE(by_nodes_data.GetDimension() == dimension); + REQUIRE(by_vdim_data.GetDofCount() == dof_count); + REQUIRE(by_nodes_data.GetDofCount() == dof_count); + REQUIRE(by_vdim_data.GetOrdering() == mfem::Ordering::byVDIM); + REQUIRE(by_nodes_data.GetOrdering() == mfem::Ordering::byNODES); - check_matrix(by_vdim_data.GetDofMatrix(), by_nodes_data.GetDofMatrix(), 0.0); + check_matrix(by_vdim_data.GetDofMatrix(), by_nodes_data.GetDofMatrix(), 0.0); - for (int i = 0; i < dof_count; ++i) { - for (int component = 0; component < dimension; ++component) { - CHECK_THAT(by_vdim_data.GetDofMatrix()(i, component), WithinAbs(100.0 * component + i + 1.0, 0.0)); - } + for (int i = 0; i < dof_count; ++i) { + for (int component = 0; component < dimension; ++component) { + CHECK_THAT(by_vdim_data.GetDofMatrix()(i, component), + WithinAbs(100.0 * component + i + 1.0, 0.0)); } + } } -TEST_CASE( - "Element Displacement Data Rejects Invalid Vector Sizes", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; +TEST_CASE("Element Displacement Data Rejects Invalid Vector Sizes", + tags::unit &tags::mapping) { + SingleElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - const int dof_count = element.GetDof(); + const mfem::FiniteElement &element = fixture.GetElement(); + const int dof_count = element.GetDof(); - mfem::Vector empty_dofs; - mfem::Vector incomplete_dofs(dof_count * dimension - 1); - incomplete_dofs = 0.0; + mfem::Vector empty_dofs; + mfem::Vector incomplete_dofs(dof_count * dimension - 1); + incomplete_dofs = 0.0; - CHECK_THROWS_AS( - mapping::ElementDisplacementData(element, empty_dofs, mfem::Ordering::byVDIM), std::invalid_argument - ); - CHECK_THROWS_AS( - mapping::ElementDisplacementData(element, incomplete_dofs, mfem::Ordering::byVDIM), std::invalid_argument - ); + CHECK_THROWS_AS(mapping::ElementDisplacementData(element, empty_dofs, + mfem::Ordering::byVDIM), + std::invalid_argument); + CHECK_THROWS_AS(mapping::ElementDisplacementData(element, incomplete_dofs, + mfem::Ordering::byVDIM), + std::invalid_argument); } -TEST_CASE( - "Domain Mapping Workspace Tracks Its Dimension", - tags::unit &tags::mapping -) { - mapping::DomainMapperStateless::Workspace workspace(dimension); +TEST_CASE("Domain Mapping Workspace Tracks Its Dimension", + tags::unit &tags::mapping) { + mapping::DomainMapper::Workspace workspace(dimension); - REQUIRE(workspace.GetDimension() == dimension); + REQUIRE(workspace.GetDimension() == dimension); - workspace.SetDimension(2); - REQUIRE(workspace.GetDimension() == 2); + workspace.SetDimension(2); + REQUIRE(workspace.GetDimension() == 2); - workspace.SetDimension(dimension); - REQUIRE(workspace.GetDimension() == dimension); + workspace.SetDimension(dimension); + REQUIRE(workspace.GetDimension() == dimension); - CHECK_THROWS_AS(workspace.SetDimension(0), std::invalid_argument); - CHECK_THROWS_AS(workspace.SetDimension(-1), std::invalid_argument); - CHECK_THROWS_AS(mapping::DomainMapperStateless::Workspace(0), std::invalid_argument); + CHECK_THROWS_AS(workspace.SetDimension(0), std::invalid_argument); + CHECK_THROWS_AS(workspace.SetDimension(-1), std::invalid_argument); + CHECK_THROWS_AS(mapping::DomainMapper::Workspace(0), + std::invalid_argument); } -TEST_CASE( - "Stateless Domain Mapper Validates Its Configuration", - tags::unit &tags::mapping -) { - const utils::DomainMapperStatelessOptions valid_options{.dimension = dimension, .vacuum_element_attribute = 3}; - mapping::DomainMapperStateless mapper(valid_options, make_kelvin_compactification()); +TEST_CASE("Stateless Domain Mapper Validates Its Configuration", + tags::unit &tags::mapping) { + const utils::DomainMapperOptions valid_options{ + .dimension = dimension, .vacuum_element_attribute = 3}; + mapping::DomainMapper mapper(valid_options, + make_kelvin_compactification()); - REQUIRE(mapper.GetDimension() == dimension); - REQUIRE(mapper.GetVacuumElementAttribute() == 3); - REQUIRE(mapper.GetExteriorMap().GetName() == "KelvinCompactification"); + REQUIRE(mapper.GetDimension() == dimension); + REQUIRE(mapper.GetExteriorMap().GetName() == "KelvinCompactification"); - const utils::DomainMapperStatelessOptions invalid_dimension{.dimension = 0, .vacuum_element_attribute = 3}; - const utils::DomainMapperStatelessOptions invalid_attribute{.dimension = dimension, .vacuum_element_attribute = 0}; + const utils::DomainMapperOptions invalid_dimension{ + .dimension = 0, .vacuum_element_attribute = 3}; + const utils::DomainMapperOptions invalid_attribute{ + .dimension = dimension, .vacuum_element_attribute = 0}; - CHECK_THROWS_AS( - mapping::DomainMapperStateless(invalid_dimension, make_kelvin_compactification()), std::invalid_argument - ); - CHECK_THROWS_AS( - mapping::DomainMapperStateless(invalid_attribute, make_kelvin_compactification()), std::invalid_argument - ); + CHECK_THROWS_AS(mapping::DomainMapper( + invalid_dimension, make_kelvin_compactification()), + std::invalid_argument); + CHECK_THROWS_AS(mapping::DomainMapper( + invalid_attribute, make_kelvin_compactification()), + std::invalid_argument); - std::unique_ptr null_exterior_map; - CHECK_THROWS_AS(mapping::DomainMapperStateless(valid_options, std::move(null_exterior_map)), std::invalid_argument); + std::unique_ptr + null_exterior_map; + CHECK_THROWS_AS(mapping::DomainMapper(valid_options, + std::move(null_exterior_map)), + std::invalid_argument); } -TEST_CASE( - "Stateless Domain Mapper Preserves Identity Point Geometry", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; +TEST_CASE("Stateless Domain Mapper Preserves Identity Point Geometry", + tags::unit &tags::mapping) { + SingleElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); - const mapping::ElementDisplacementData displacement(element, zero_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); + const mfem::FiniteElement &element = fixture.GetElement(); + const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); + const mapping::ElementDisplacementData displacement(element, zero_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::MappingPointContext context; + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::MappingPointContext context; - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - REQUIRE_FALSE(mapper.IsCompactifiedElement(*transformation)); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + REQUIRE_FALSE(mapper.IsCompactifiedElement(*transformation)); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 4); - const mfem::DenseMatrix identity = make_identity_matrix(dimension); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 4); + const mfem::DenseMatrix identity = make_identity_matrix(dimension); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + mfem::Vector expected_position(dimension); + transformation->Transform(integration_point, expected_position); + + REQUIRE(mapper.EvaluatePoint(element_data.Get(), *transformation, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + CAPTURE(q); + REQUIRE_FALSE(context.compactified); + check_vector(context.reference_position, expected_position); + check_vector(context.displaced_position, expected_position); + check_vector(context.physical_position, expected_position); + check_matrix(context.displacement_jacobian, identity); + check_matrix(context.mapping_jacobian, identity); + check_matrix(context.inverse_mapping_jacobian, identity); + CHECK_THAT(context.mapping_determinant, WithinAbs(1.0, tolerance)); + } +} + +TEST_CASE("Stateless Domain Mapper Preserves Identity Volume Geometry", + tags::unit &tags::mapping) { + SingleElementFixture fixture; + + const mfem::FiniteElement &element = fixture.GetElement(); + const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); + const mapping::ElementDisplacementData displacement(element, zero_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::VolumeMappingContext context; + + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 4); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + + REQUIRE(mapper.EvaluateVolume(element_data.Get(), *transformation, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + transformation->SetIntPoint(&integration_point); + + mfem::DenseMatrix expected_inverse(dimension); + mfem::CalcInverse(transformation->Jacobian(), expected_inverse); + + const double expected_weight = + integration_point.weight * transformation->Weight(); + + CAPTURE(q); + REQUIRE_FALSE(context.mapping.compactified); + check_matrix(context.quadrature.J_inv, expected_inverse); + CHECK_THAT(context.quadrature.detJ, WithinAbs(1.0, tolerance)); + CHECK_THAT(context.quadrature.weight, + WithinAbs(expected_weight, tolerance)); + CHECK(context.quadrature.weight > 0.0); + } +} + +TEST_CASE("Stateless Domain Mapper Preserves Identity Face Geometry", + tags::unit &tags::mapping) { + SingleElementFixture fixture; + + const mfem::FiniteElement &element = fixture.GetElement(); + const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); + const mapping::ElementDisplacementData displacement(element, zero_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::FaceMappingContext context; + + for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); + ++boundary_element) { + mfem::FaceElementTransformations *transformation = + fixture.mesh.GetBdrFaceTransformations(boundary_element); + REQUIRE(transformation != nullptr); + REQUIRE(transformation->Elem1 != nullptr); + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 4); for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - mfem::Vector expected_position(dimension); - transformation->Transform(integration_point, expected_position); + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); - REQUIRE( - mapper.EvaluatePoint(element_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::valid - ); + REQUIRE(mapper.EvaluateFace(element_data.Get(), *transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, + context) == mapping::MappingStatus::valid); - CAPTURE(q); - REQUIRE_FALSE(context.compactified); - check_vector(context.reference_position, expected_position); - check_vector(context.displaced_position, expected_position); - check_vector(context.physical_position, expected_position); - check_matrix(context.displacement_jacobian, identity); - check_matrix(context.mapping_jacobian, identity); - check_matrix(context.inverse_mapping_jacobian, identity); - CHECK_THAT(context.mapping_determinant, WithinAbs(1.0, tolerance)); + transformation->SetAllIntPoints(&integration_point); + + mfem::Vector raw_normal(dimension); + mfem::CalcOrtho(transformation->Jacobian(), raw_normal); + + const double raw_normal_magnitude = raw_normal.Norml2(); + mfem::Vector expected_normal(raw_normal); + expected_normal /= raw_normal_magnitude; + + const double expected_surface_weight = + integration_point.weight * raw_normal_magnitude; + + const mfem::IntegrationPoint element_integration_point = + transformation->Elem1->GetIntPoint(); + + mfem::Vector expected_position(dimension); + transformation->Elem1->Transform(element_integration_point, + expected_position); + + CAPTURE(boundary_element, q); + REQUIRE_FALSE(context.mapping.compactified); + check_vector(context.mapping.reference_position, expected_position); + check_vector(context.mapping.displaced_position, expected_position); + check_vector(context.mapping.physical_position, expected_position); + check_vector(context.reference_normal, expected_normal); + check_vector(context.quadrature.normal, expected_normal); + CHECK_THAT(context.reference_surface_weight, + WithinAbs(expected_surface_weight, tolerance)); + CHECK_THAT(context.physical_surface_weight, + WithinAbs(expected_surface_weight, tolerance)); + CHECK_THAT(context.quadrature.ds, + WithinAbs(expected_surface_weight, tolerance)); + CHECK_THAT(context.quadrature.v_dot_n_scale, WithinAbs(1.0, tolerance)); } + } } -TEST_CASE( - "Stateless Domain Mapper Preserves Identity Volume Geometry", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; +TEST_CASE("Stateless Domain Mapper Matches Exact Affine Point Mapping", + tags::unit &tags::mapping) { + SingleElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); - const mapping::ElementDisplacementData displacement(element, zero_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::VolumeMappingContext context; + const mfem::DenseMatrix displacement_gradient = + make_affine_displacement_gradient(); + const mfem::DenseMatrix deformation_jacobian = + make_deformation_jacobian(displacement_gradient); + const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); + const mfem::Vector element_dofs = + make_affine_element_dofs(element, *transformation, displacement_gradient, + displacement_offset, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData displacement(element, element_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 4); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::MappingPointContext context; + + mfem::DenseMatrix inverse_deformation_jacobian(dimension); + mfem::CalcInverse(deformation_jacobian, inverse_deformation_jacobian); + const double deformation_determinant = deformation_jacobian.Det(); + + REQUIRE(deformation_determinant > 0.0); + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + mfem::Vector reference_position(dimension); + transformation->Transform(integration_point, reference_position); + + const mfem::Vector expected_position = evaluate_affine_physical_position( + reference_position, displacement_gradient, displacement_offset); + + REQUIRE(mapper.EvaluatePoint(element_data.Get(), *transformation, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + CAPTURE(q); + REQUIRE_FALSE(context.compactified); + check_vector(context.reference_position, reference_position); + check_vector(context.displaced_position, expected_position); + check_vector(context.physical_position, expected_position); + check_matrix(context.displacement_jacobian, deformation_jacobian); + check_matrix(context.mapping_jacobian, deformation_jacobian); + check_matrix(context.inverse_mapping_jacobian, + inverse_deformation_jacobian); + CHECK_THAT(context.mapping_determinant, + WithinAbs(deformation_determinant, tolerance)); + } +} + +TEST_CASE("Stateless Domain Mapper Produces Equivalent Results For Both MFEM " + "Orderings", + tags::unit &tags::mapping) { + SingleElementFixture fixture; + + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + + const mfem::DenseMatrix displacement_gradient = + make_affine_displacement_gradient(); + const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); + + const mfem::Vector by_vdim_dofs = + make_affine_element_dofs(element, *transformation, displacement_gradient, + displacement_offset, mfem::Ordering::byVDIM); + const mfem::Vector by_nodes_dofs = + make_affine_element_dofs(element, *transformation, displacement_gradient, + displacement_offset, mfem::Ordering::byNODES); + + const mapping::ElementDisplacementData by_vdim_displacement( + element, by_vdim_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData by_nodes_displacement( + element, by_nodes_dofs, mfem::Ordering::byNODES); + const ElementMappingDataOwner by_vdim_data(by_vdim_displacement); + const ElementMappingDataOwner by_nodes_data(by_nodes_displacement); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + + mapping::VolumeMappingContext by_vdim_context; + mapping::VolumeMappingContext by_nodes_context; + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + + REQUIRE(mapper.EvaluateVolume( + by_vdim_data.Get(), *transformation, integration_point, + workspace, by_vdim_context) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateVolume( + by_nodes_data.Get(), *transformation, integration_point, + workspace, by_nodes_context) == mapping::MappingStatus::valid); + + CAPTURE(q); + check_point_context(by_vdim_context.mapping, by_nodes_context.mapping); + check_matrix(by_vdim_context.quadrature.J_inv, + by_nodes_context.quadrature.J_inv); + CHECK_THAT(by_vdim_context.quadrature.detJ, + WithinAbs(by_nodes_context.quadrature.detJ, tolerance)); + CHECK_THAT(by_vdim_context.quadrature.weight, + WithinAbs(by_nodes_context.quadrature.weight, tolerance)); + } +} + +TEST_CASE("Stateless Domain Mapper Composes Affine Volume Jacobians", + tags::unit &tags::mapping) { + SingleElementFixture fixture; + + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + + const mfem::DenseMatrix displacement_gradient = + make_affine_displacement_gradient(); + const mfem::DenseMatrix mapping_jacobian = + make_deformation_jacobian(displacement_gradient); + const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); + const mfem::Vector element_dofs = + make_affine_element_dofs(element, *transformation, displacement_gradient, + displacement_offset, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData displacement(element, element_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::VolumeMappingContext context; + + const double mapping_determinant = mapping_jacobian.Det(); + REQUIRE(mapping_determinant > 0.0); + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); + + mfem::DenseMatrix full_element_jacobian(dimension); + mfem::DenseMatrix expected_inverse(dimension); + mfem::Mult(mapping_jacobian, transformation->Jacobian(), + full_element_jacobian); + mfem::CalcInverse(full_element_jacobian, expected_inverse); + + const double expected_weight = integration_point.weight * + transformation->Weight() * + mapping_determinant; + + REQUIRE(mapper.EvaluateVolume(element_data.Get(), *transformation, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + CAPTURE(q); + check_matrix(context.mapping.mapping_jacobian, mapping_jacobian); + check_matrix(context.quadrature.J_inv, expected_inverse); + CHECK_THAT(context.quadrature.detJ, + WithinAbs(mapping_determinant, tolerance)); + CHECK_THAT(context.quadrature.weight, + WithinAbs(expected_weight, tolerance)); + } +} + +TEST_CASE("Stateless Domain Mapper Applies Nanson Formula On Affine Faces", + tags::unit &tags::mapping) { + SingleElementFixture fixture; + + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *element_transformation = + fixture.mesh.GetElementTransformation(0); + + const mfem::DenseMatrix displacement_gradient = + make_affine_displacement_gradient(); + const mfem::DenseMatrix mapping_jacobian = + make_deformation_jacobian(displacement_gradient); + const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); + const mfem::Vector element_dofs = make_affine_element_dofs( + element, *element_transformation, displacement_gradient, + displacement_offset, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData displacement(element, element_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); + + mfem::DenseMatrix inverse_mapping_jacobian(dimension); + mfem::CalcInverse(mapping_jacobian, inverse_mapping_jacobian); + const double mapping_determinant = mapping_jacobian.Det(); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::FaceMappingContext context; + + for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); + ++boundary_element) { + mfem::FaceElementTransformations *transformation = + fixture.mesh.GetBdrFaceTransformations(boundary_element); + REQUIRE(transformation != nullptr); + REQUIRE(transformation->Elem1 != nullptr); + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetAllIntPoints(&integration_point); - REQUIRE( - mapper.EvaluateVolume(element_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::valid - ); + mfem::Vector raw_normal(dimension); + mfem::Vector mapped_normal(dimension); + mfem::CalcOrtho(transformation->Jacobian(), raw_normal); - transformation->SetIntPoint(&integration_point); + inverse_mapping_jacobian.MultTranspose(raw_normal, mapped_normal); + mapped_normal *= mapping_determinant; - mfem::DenseMatrix expected_inverse(dimension); - mfem::CalcInverse(transformation->Jacobian(), expected_inverse); + const double raw_normal_magnitude = raw_normal.Norml2(); + const double mapped_normal_magnitude = mapped_normal.Norml2(); - const double expected_weight = integration_point.weight * transformation->Weight(); + mfem::Vector expected_reference_normal(raw_normal); + mfem::Vector expected_physical_normal(mapped_normal); + expected_reference_normal /= raw_normal_magnitude; + expected_physical_normal /= mapped_normal_magnitude; - CAPTURE(q); - REQUIRE_FALSE(context.mapping.compactified); - check_matrix(context.quadrature.J_inv, expected_inverse); - CHECK_THAT(context.quadrature.detJ, WithinAbs(1.0, tolerance)); - CHECK_THAT(context.quadrature.weight, WithinAbs(expected_weight, tolerance)); - CHECK(context.quadrature.weight > 0.0); + const double expected_reference_weight = + integration_point.weight * raw_normal_magnitude; + const double expected_physical_weight = + integration_point.weight * mapped_normal_magnitude; + const double expected_normal_scale = + mapped_normal_magnitude / raw_normal_magnitude; + + REQUIRE(mapper.EvaluateFace(element_data.Get(), *transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + CAPTURE(boundary_element, q); + check_matrix(context.mapping.mapping_jacobian, mapping_jacobian); + check_vector(context.reference_normal, expected_reference_normal); + check_vector(context.quadrature.normal, expected_physical_normal); + CHECK_THAT(context.reference_surface_weight, + WithinAbs(expected_reference_weight, tolerance)); + CHECK_THAT(context.physical_surface_weight, + WithinAbs(expected_physical_weight, tolerance)); + CHECK_THAT(context.quadrature.ds, + WithinAbs(expected_reference_weight, tolerance)); + CHECK_THAT(context.quadrature.v_dot_n_scale, + WithinAbs(expected_normal_scale, tolerance)); } + } } -TEST_CASE( - "Stateless Domain Mapper Preserves Identity Face Geometry", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; +TEST_CASE("Stateless Domain Mapper Has No Cross State Contamination", + tags::unit &tags::mapping) { + SingleElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); - const mapping::ElementDisplacementData displacement(element, zero_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::FaceMappingContext context; + const mfem::DenseMatrix gradient_a = make_affine_displacement_gradient(); + mfem::DenseMatrix gradient_b(3); + gradient_b = 0.0; + gradient_b(0, 0) = -0.06; + gradient_b(0, 2) = 0.03; + gradient_b(1, 0) = 0.02; + gradient_b(1, 1) = 0.12; + gradient_b(2, 1) = -0.04; + gradient_b(2, 2) = 0.07; - for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); ++boundary_element) { - mfem::FaceElementTransformations *transformation = fixture.mesh.GetBdrFaceTransformations(boundary_element); - REQUIRE(transformation != nullptr); - REQUIRE(transformation->Elem1 != nullptr); + const mfem::Vector offset_a = make_vector(0.07, -0.04, 0.03); + const mfem::Vector offset_b = make_vector(-0.05, 0.08, -0.02); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 4); + const mfem::Vector dofs_a = make_affine_element_dofs( + element, *transformation, gradient_a, offset_a, mfem::Ordering::byVDIM); + const mfem::Vector dofs_b = make_affine_element_dofs( + element, *transformation, gradient_b, offset_b, mfem::Ordering::byVDIM); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + const mapping::ElementDisplacementData displacement_a(element, dofs_a, + mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData displacement_b(element, dofs_b, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data_a(displacement_a); + const ElementMappingDataOwner element_data_b(displacement_b); - REQUIRE( - mapper.EvaluateFace( - element_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, context - ) == mapping::MappingStatus::valid - ); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); - transformation->SetAllIntPoints(&integration_point); + const mfem::IntegrationPoint &integration_point = + mfem::Geometries.GetCenter(transformation->GetGeometryType()); - mfem::Vector raw_normal(dimension); - mfem::CalcOrtho(transformation->Jacobian(), raw_normal); + mapping::MappingPointContext first_a; + mapping::MappingPointContext result_b; + mapping::MappingPointContext second_a; - const double raw_normal_magnitude = raw_normal.Norml2(); - mfem::Vector expected_normal(raw_normal); - expected_normal /= raw_normal_magnitude; + REQUIRE(mapper.EvaluatePoint(element_data_a.Get(), *transformation, + integration_point, workspace, + first_a) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluatePoint(element_data_b.Get(), *transformation, + integration_point, workspace, + result_b) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluatePoint(element_data_a.Get(), *transformation, + integration_point, workspace, + second_a) == mapping::MappingStatus::valid); - const double expected_surface_weight = integration_point.weight * raw_normal_magnitude; + check_point_context(first_a, second_a, 0.0); - const mfem::IntegrationPoint element_integration_point = transformation->Elem1->GetIntPoint(); - - mfem::Vector expected_position(dimension); - transformation->Elem1->Transform(element_integration_point, expected_position); - - CAPTURE(boundary_element, q); - REQUIRE_FALSE(context.mapping.compactified); - check_vector(context.mapping.reference_position, expected_position); - check_vector(context.mapping.displaced_position, expected_position); - check_vector(context.mapping.physical_position, expected_position); - check_vector(context.reference_normal, expected_normal); - check_vector(context.quadrature.normal, expected_normal); - CHECK_THAT(context.reference_surface_weight, WithinAbs(expected_surface_weight, tolerance)); - CHECK_THAT(context.physical_surface_weight, WithinAbs(expected_surface_weight, tolerance)); - CHECK_THAT(context.quadrature.ds, WithinAbs(expected_surface_weight, tolerance)); - CHECK_THAT(context.quadrature.v_dot_n_scale, WithinAbs(1.0, tolerance)); - } - } + mfem::Vector state_difference(result_b.physical_position); + state_difference -= first_a.physical_position; + CHECK(state_difference.Norml2() > 1.0e-3); } -TEST_CASE( - "Stateless Domain Mapper Matches Exact Affine Point Mapping", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; +TEST_CASE("Stateless Domain Mapper Reports Invalid Element States", + tags::unit &tags::mapping) { + SingleElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + const mfem::IntegrationPoint &integration_point = + mfem::Geometries.GetCenter(transformation->GetGeometryType()); - const mfem::DenseMatrix displacement_gradient = make_affine_displacement_gradient(); - const mfem::DenseMatrix deformation_jacobian = make_deformation_jacobian(displacement_gradient); - const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); - const mfem::Vector element_dofs = make_affine_element_dofs( - element, *transformation, displacement_gradient, displacement_offset, mfem::Ordering::byVDIM - ); - const mapping::ElementDisplacementData displacement(element, element_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::MappingPointContext context; - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::MappingPointContext context; + mfem::Vector non_finite_dofs = fixture.MakeZeroElementDofs(); + non_finite_dofs(0) = std::numeric_limits::quiet_NaN(); + const mapping::ElementDisplacementData non_finite_displacement( + element, non_finite_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner non_finite_data(non_finite_displacement); - mfem::DenseMatrix inverse_deformation_jacobian(dimension); - mfem::CalcInverse(deformation_jacobian, inverse_deformation_jacobian); - const double deformation_determinant = deformation_jacobian.Det(); + CHECK(mapper.EvaluatePoint(non_finite_data.Get(), *transformation, + integration_point, workspace, context) == + mapping::MappingStatus::non_finite_input); - REQUIRE(deformation_determinant > 0.0); + mfem::DenseMatrix singular_gradient(dimension); + singular_gradient = 0.0; + for (int i = 0; i < dimension; ++i) + singular_gradient(i, i) = -1.0; - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); + const mfem::Vector zero_offset(dimension); + const mfem::Vector singular_dofs = + make_affine_element_dofs(element, *transformation, singular_gradient, + zero_offset, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData singular_displacement( + element, singular_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner singular_data(singular_displacement); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - mfem::Vector reference_position(dimension); - transformation->Transform(integration_point, reference_position); + CHECK(mapper.EvaluatePoint(singular_data.Get(), *transformation, + integration_point, workspace, context) == + mapping::MappingStatus::non_positive_determinant); - const mfem::Vector expected_position = - evaluate_affine_physical_position(reference_position, displacement_gradient, displacement_offset); + mfem::DenseMatrix inverted_gradient(dimension); + inverted_gradient = 0.0; + for (int i = 0; i < dimension; ++i) + inverted_gradient(i, i) = -2.0; - REQUIRE( - mapper.EvaluatePoint(element_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::valid - ); + const mfem::Vector inverted_dofs = + make_affine_element_dofs(element, *transformation, inverted_gradient, + zero_offset, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData inverted_displacement( + element, inverted_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner inverted_data(inverted_displacement); - CAPTURE(q); - REQUIRE_FALSE(context.compactified); - check_vector(context.reference_position, reference_position); - check_vector(context.displaced_position, expected_position); - check_vector(context.physical_position, expected_position); - check_matrix(context.displacement_jacobian, deformation_jacobian); - check_matrix(context.mapping_jacobian, deformation_jacobian); - check_matrix(context.inverse_mapping_jacobian, inverse_deformation_jacobian); - CHECK_THAT(context.mapping_determinant, WithinAbs(deformation_determinant, tolerance)); - } + CHECK(mapper.EvaluatePoint(inverted_data.Get(), *transformation, + integration_point, workspace, context) == + mapping::MappingStatus::non_positive_determinant); + + mapping::DomainMapper::Workspace wrong_workspace(2); + CHECK_THROWS_AS(mapper.EvaluatePoint(singular_data.Get(), *transformation, + integration_point, wrong_workspace, + context), + std::invalid_argument); + + mfem::Vector two_dimensional_dofs(element.GetDof() * 2); + two_dimensional_dofs = 0.0; + const mapping::ElementDisplacementData two_dimensional_displacement( + element, two_dimensional_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner two_dimensional_data( + two_dimensional_displacement); + + CHECK_THROWS_AS(mapper.EvaluatePoint(two_dimensional_data.Get(), + *transformation, integration_point, + workspace, context), + std::invalid_argument); } -TEST_CASE( - "Stateless Domain Mapper Produces Equivalent Results For Both MFEM " - "Orderings", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; - - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - - const mfem::DenseMatrix displacement_gradient = make_affine_displacement_gradient(); - const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); - - const mfem::Vector by_vdim_dofs = make_affine_element_dofs( - element, *transformation, displacement_gradient, displacement_offset, mfem::Ordering::byVDIM - ); - const mfem::Vector by_nodes_dofs = make_affine_element_dofs( - element, *transformation, displacement_gradient, displacement_offset, mfem::Ordering::byNODES - ); - - const mapping::ElementDisplacementData by_vdim_displacement(element, by_vdim_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData by_nodes_displacement(element, by_nodes_dofs, mfem::Ordering::byNODES); - const ElementMappingDataOwner by_vdim_data(by_vdim_displacement); - const ElementMappingDataOwner by_nodes_data(by_nodes_displacement); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - - mapping::VolumeMappingContext by_vdim_context; - mapping::VolumeMappingContext by_nodes_context; - - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - - REQUIRE( - mapper.EvaluateVolume(by_vdim_data.Get(), *transformation, integration_point, workspace, by_vdim_context) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateVolume( - by_nodes_data.Get(), *transformation, integration_point, workspace, by_nodes_context - ) == mapping::MappingStatus::valid - ); - - CAPTURE(q); - check_point_context(by_vdim_context.mapping, by_nodes_context.mapping); - check_matrix(by_vdim_context.quadrature.J_inv, by_nodes_context.quadrature.J_inv); - CHECK_THAT(by_vdim_context.quadrature.detJ, WithinAbs(by_nodes_context.quadrature.detJ, tolerance)); - CHECK_THAT(by_vdim_context.quadrature.weight, WithinAbs(by_nodes_context.quadrature.weight, tolerance)); - } -} - -TEST_CASE( - "Stateless Domain Mapper Composes Affine Volume Jacobians", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; - - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - - const mfem::DenseMatrix displacement_gradient = make_affine_displacement_gradient(); - const mfem::DenseMatrix mapping_jacobian = make_deformation_jacobian(displacement_gradient); - const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); - const mfem::Vector element_dofs = make_affine_element_dofs( - element, *transformation, displacement_gradient, displacement_offset, mfem::Ordering::byVDIM - ); - const mapping::ElementDisplacementData displacement(element, element_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::VolumeMappingContext context; - - const double mapping_determinant = mapping_jacobian.Det(); - REQUIRE(mapping_determinant > 0.0); - - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); - - mfem::DenseMatrix full_element_jacobian(dimension); - mfem::DenseMatrix expected_inverse(dimension); - mfem::Mult(mapping_jacobian, transformation->Jacobian(), full_element_jacobian); - mfem::CalcInverse(full_element_jacobian, expected_inverse); - - const double expected_weight = integration_point.weight * transformation->Weight() * mapping_determinant; - - REQUIRE( - mapper.EvaluateVolume(element_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::valid - ); - - CAPTURE(q); - check_matrix(context.mapping.mapping_jacobian, mapping_jacobian); - check_matrix(context.quadrature.J_inv, expected_inverse); - CHECK_THAT(context.quadrature.detJ, WithinAbs(mapping_determinant, tolerance)); - CHECK_THAT(context.quadrature.weight, WithinAbs(expected_weight, tolerance)); - } -} - -TEST_CASE( - "Stateless Domain Mapper Applies Nanson Formula On Affine Faces", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; - - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *element_transformation = fixture.mesh.GetElementTransformation(0); - - const mfem::DenseMatrix displacement_gradient = make_affine_displacement_gradient(); - const mfem::DenseMatrix mapping_jacobian = make_deformation_jacobian(displacement_gradient); - const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); - const mfem::Vector element_dofs = make_affine_element_dofs( - element, *element_transformation, displacement_gradient, displacement_offset, mfem::Ordering::byVDIM - ); - const mapping::ElementDisplacementData displacement(element, element_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); - - mfem::DenseMatrix inverse_mapping_jacobian(dimension); - mfem::CalcInverse(mapping_jacobian, inverse_mapping_jacobian); - const double mapping_determinant = mapping_jacobian.Det(); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::FaceMappingContext context; - - for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); ++boundary_element) { - mfem::FaceElementTransformations *transformation = fixture.mesh.GetBdrFaceTransformations(boundary_element); - REQUIRE(transformation != nullptr); - REQUIRE(transformation->Elem1 != nullptr); - - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetAllIntPoints(&integration_point); - - mfem::Vector raw_normal(dimension); - mfem::Vector mapped_normal(dimension); - mfem::CalcOrtho(transformation->Jacobian(), raw_normal); - - inverse_mapping_jacobian.MultTranspose(raw_normal, mapped_normal); - mapped_normal *= mapping_determinant; - - const double raw_normal_magnitude = raw_normal.Norml2(); - const double mapped_normal_magnitude = mapped_normal.Norml2(); - - mfem::Vector expected_reference_normal(raw_normal); - mfem::Vector expected_physical_normal(mapped_normal); - expected_reference_normal /= raw_normal_magnitude; - expected_physical_normal /= mapped_normal_magnitude; - - const double expected_reference_weight = integration_point.weight * raw_normal_magnitude; - const double expected_physical_weight = integration_point.weight * mapped_normal_magnitude; - const double expected_normal_scale = mapped_normal_magnitude / raw_normal_magnitude; - - REQUIRE( - mapper.EvaluateFace( - element_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, context - ) == mapping::MappingStatus::valid - ); - - CAPTURE(boundary_element, q); - check_matrix(context.mapping.mapping_jacobian, mapping_jacobian); - check_vector(context.reference_normal, expected_reference_normal); - check_vector(context.quadrature.normal, expected_physical_normal); - CHECK_THAT(context.reference_surface_weight, WithinAbs(expected_reference_weight, tolerance)); - CHECK_THAT(context.physical_surface_weight, WithinAbs(expected_physical_weight, tolerance)); - CHECK_THAT(context.quadrature.ds, WithinAbs(expected_reference_weight, tolerance)); - CHECK_THAT(context.quadrature.v_dot_n_scale, WithinAbs(expected_normal_scale, tolerance)); - } - } -} - -TEST_CASE( - "Stateless Domain Mapper Has No Cross State Contamination", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; - - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - - const mfem::DenseMatrix gradient_a = make_affine_displacement_gradient(); - mfem::DenseMatrix gradient_b(3); - gradient_b = 0.0; - gradient_b(0, 0) = -0.06; - gradient_b(0, 2) = 0.03; - gradient_b(1, 0) = 0.02; - gradient_b(1, 1) = 0.12; - gradient_b(2, 1) = -0.04; - gradient_b(2, 2) = 0.07; - - const mfem::Vector offset_a = make_vector(0.07, -0.04, 0.03); - const mfem::Vector offset_b = make_vector(-0.05, 0.08, -0.02); - - const mfem::Vector dofs_a = - make_affine_element_dofs(element, *transformation, gradient_a, offset_a, mfem::Ordering::byVDIM); - const mfem::Vector dofs_b = - make_affine_element_dofs(element, *transformation, gradient_b, offset_b, mfem::Ordering::byVDIM); - - const mapping::ElementDisplacementData displacement_a(element, dofs_a, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData displacement_b(element, dofs_b, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data_a(displacement_a); - const ElementMappingDataOwner element_data_b(displacement_b); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - - const mfem::IntegrationPoint &integration_point = mfem::Geometries.GetCenter(transformation->GetGeometryType()); - - mapping::MappingPointContext first_a; - mapping::MappingPointContext result_b; - mapping::MappingPointContext second_a; - - REQUIRE( - mapper.EvaluatePoint(element_data_a.Get(), *transformation, integration_point, workspace, first_a) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluatePoint(element_data_b.Get(), *transformation, integration_point, workspace, result_b) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluatePoint(element_data_a.Get(), *transformation, integration_point, workspace, second_a) == - mapping::MappingStatus::valid - ); - - check_point_context(first_a, second_a, 0.0); - - mfem::Vector state_difference(result_b.physical_position); - state_difference -= first_a.physical_position; - CHECK(state_difference.Norml2() > 1.0e-3); -} - -TEST_CASE( - "Stateless Domain Mapper Reports Invalid Element States", - tags::unit &tags::mapping -) { - SingleElementFixture fixture; - - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - const mfem::IntegrationPoint &integration_point = mfem::Geometries.GetCenter(transformation->GetGeometryType()); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::MappingPointContext context; - - mfem::Vector non_finite_dofs = fixture.MakeZeroElementDofs(); - non_finite_dofs(0) = std::numeric_limits::quiet_NaN(); - const mapping::ElementDisplacementData non_finite_displacement(element, non_finite_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner non_finite_data(non_finite_displacement); - - CHECK( - mapper.EvaluatePoint(non_finite_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::non_finite_input - ); - - mfem::DenseMatrix singular_gradient(dimension); - singular_gradient = 0.0; - for (int i = 0; i < dimension; ++i) - singular_gradient(i, i) = -1.0; - - const mfem::Vector zero_offset(dimension); - const mfem::Vector singular_dofs = - make_affine_element_dofs(element, *transformation, singular_gradient, zero_offset, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData singular_displacement(element, singular_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner singular_data(singular_displacement); - - CHECK( - mapper.EvaluatePoint(singular_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::non_positive_determinant - ); - - mfem::DenseMatrix inverted_gradient(dimension); - inverted_gradient = 0.0; - for (int i = 0; i < dimension; ++i) - inverted_gradient(i, i) = -2.0; - - const mfem::Vector inverted_dofs = - make_affine_element_dofs(element, *transformation, inverted_gradient, zero_offset, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData inverted_displacement(element, inverted_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner inverted_data(inverted_displacement); - - CHECK( - mapper.EvaluatePoint(inverted_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::non_positive_determinant - ); - - mapping::DomainMapperStateless::Workspace wrong_workspace(2); - CHECK_THROWS_AS( - mapper.EvaluatePoint(singular_data.Get(), *transformation, integration_point, wrong_workspace, context), - std::invalid_argument - ); - - mfem::Vector two_dimensional_dofs(element.GetDof() * 2); - two_dimensional_dofs = 0.0; - const mapping::ElementDisplacementData two_dimensional_displacement( - element, two_dimensional_dofs, mfem::Ordering::byVDIM - ); - const ElementMappingDataOwner two_dimensional_data(two_dimensional_displacement); - - CHECK_THROWS_AS( - mapper.EvaluatePoint(two_dimensional_data.Get(), *transformation, integration_point, workspace, context), - std::invalid_argument - ); -} - -TEST_CASE( - "Stateless Domain Mapper Matches Exact Quadratic Point Mapping", - tags::unit &tags::mapping -) { - QuadraticElementFixture fixture; - - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - - const mfem::Vector element_dofs = - make_function_element_dofs(element, *transformation, evaluate_quadratic_displacement, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData displacement(element, element_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - mapping::MappingPointContext context; - - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - - mfem::Vector reference_position(dimension); - mfem::Vector expected_displacement(dimension); - transformation->Transform(integration_point, reference_position); - evaluate_quadratic_displacement(reference_position, expected_displacement); - - mfem::Vector expected_position(reference_position); - expected_position += expected_displacement; - - const mfem::DenseMatrix displacement_gradient = evaluate_quadratic_displacement_gradient(reference_position); - mfem::DenseMatrix expected_jacobian = make_identity_matrix(dimension); - expected_jacobian.Add(1.0, displacement_gradient); - - mfem::DenseMatrix expected_inverse(dimension); - mfem::CalcInverse(expected_jacobian, expected_inverse); - const double expected_determinant = expected_jacobian.Det(); - - REQUIRE(expected_determinant > 0.0); - REQUIRE( - mapper.EvaluatePoint(element_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::valid - ); - - CAPTURE(q); - REQUIRE_FALSE(context.compactified); - check_vector(context.reference_position, reference_position, polynomial_tolerance); - check_vector(context.displaced_position, expected_position, polynomial_tolerance); - check_vector(context.physical_position, expected_position, polynomial_tolerance); - check_matrix(context.displacement_jacobian, expected_jacobian, polynomial_tolerance); - check_matrix(context.mapping_jacobian, expected_jacobian, polynomial_tolerance); - check_matrix(context.inverse_mapping_jacobian, expected_inverse, polynomial_tolerance); - CHECK_THAT(context.mapping_determinant, WithinAbs(expected_determinant, polynomial_tolerance)); - } +TEST_CASE("Stateless Domain Mapper Matches Exact Quadratic Point Mapping", + tags::unit &tags::mapping) { + QuadraticElementFixture fixture; + + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + + const mfem::Vector element_dofs = make_function_element_dofs( + element, *transformation, evaluate_quadratic_displacement, + mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData displacement(element, element_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::MappingPointContext context; + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + + mfem::Vector reference_position(dimension); + mfem::Vector expected_displacement(dimension); + transformation->Transform(integration_point, reference_position); + evaluate_quadratic_displacement(reference_position, expected_displacement); + + mfem::Vector expected_position(reference_position); + expected_position += expected_displacement; + + const mfem::DenseMatrix displacement_gradient = + evaluate_quadratic_displacement_gradient(reference_position); + mfem::DenseMatrix expected_jacobian = make_identity_matrix(dimension); + expected_jacobian.Add(1.0, displacement_gradient); + + mfem::DenseMatrix expected_inverse(dimension); + mfem::CalcInverse(expected_jacobian, expected_inverse); + const double expected_determinant = expected_jacobian.Det(); + + REQUIRE(expected_determinant > 0.0); + REQUIRE(mapper.EvaluatePoint(element_data.Get(), *transformation, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + CAPTURE(q); + REQUIRE_FALSE(context.compactified); + check_vector(context.reference_position, reference_position, + polynomial_tolerance); + check_vector(context.displaced_position, expected_position, + polynomial_tolerance); + check_vector(context.physical_position, expected_position, + polynomial_tolerance); + check_matrix(context.displacement_jacobian, expected_jacobian, + polynomial_tolerance); + check_matrix(context.mapping_jacobian, expected_jacobian, + polynomial_tolerance); + check_matrix(context.inverse_mapping_jacobian, expected_inverse, + polynomial_tolerance); + CHECK_THAT(context.mapping_determinant, + WithinAbs(expected_determinant, polynomial_tolerance)); + } } TEST_CASE( "Stateless Domain Mapper Point Linearization Matches Centered Differences", - tags::unit &tags::mapping -) { - constexpr double linearization_tolerance = 2.0e-9; + tags::unit &tags::mapping) { + constexpr double linearization_tolerance = 2.0e-9; - QuadraticElementFixture fixture; + QuadraticElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - const QuadraticMappingData mapping_data(element, *transformation); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + const QuadraticMappingData mapping_data(element, *transformation); - const mapping::ElementDisplacementData base_displacement(element, mapping_data.base_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData direction(element, mapping_data.direction_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData plus_displacement(element, mapping_data.plus_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData minus_displacement(element, mapping_data.minus_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner base_data(base_displacement); - const ElementMappingDataOwner plus_data(plus_displacement); - const ElementMappingDataOwner minus_data(minus_displacement); + const mapping::ElementDisplacementData base_displacement( + element, mapping_data.base_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData direction( + element, mapping_data.direction_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData plus_displacement( + element, mapping_data.plus_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData minus_displacement( + element, mapping_data.minus_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner base_data(base_displacement); + const ElementMappingDataOwner plus_data(plus_displacement); + const ElementMappingDataOwner minus_data(minus_displacement); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); - mapping::MappingPointContext base_context; - mapping::MappingPointContext plus_context; - mapping::MappingPointContext minus_context; - mapping::MappingPointVariation variation; + mapping::MappingPointContext base_context; + mapping::MappingPointContext plus_context; + mapping::MappingPointContext minus_context; + mapping::MappingPointVariation variation; - REQUIRE( - mapper.EvaluatePoint(base_data.Get(), *transformation, integration_point, workspace, base_context) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluatePointVariation( - base_data.Get(), direction, *transformation, integration_point, base_context, workspace, variation - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluatePoint(plus_data.Get(), *transformation, integration_point, workspace, plus_context) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluatePoint(minus_data.Get(), *transformation, integration_point, workspace, minus_context) == - mapping::MappingStatus::valid - ); + REQUIRE(mapper.EvaluatePoint(base_data.Get(), *transformation, + integration_point, workspace, base_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluatePointVariation(base_data.Get(), direction, + *transformation, integration_point, + base_context, workspace, variation) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluatePoint(plus_data.Get(), *transformation, + integration_point, workspace, plus_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluatePoint(minus_data.Get(), *transformation, + integration_point, workspace, minus_context) == + mapping::MappingStatus::valid); - mfem::Vector reference_position(dimension); - mfem::Vector expected_direction(dimension); - transformation->Transform(integration_point, reference_position); - evaluate_quadratic_direction(reference_position, expected_direction); + mfem::Vector reference_position(dimension); + mfem::Vector expected_direction(dimension); + transformation->Transform(integration_point, reference_position); + evaluate_quadratic_direction(reference_position, expected_direction); - const mfem::DenseMatrix expected_direction_gradient = evaluate_quadratic_direction_gradient(reference_position); + const mfem::DenseMatrix expected_direction_gradient = + evaluate_quadratic_direction_gradient(reference_position); - CAPTURE(q); - check_vector(variation.displacement_variation, expected_direction, polynomial_tolerance); - check_vector(variation.physical_position_variation, expected_direction, polynomial_tolerance); - check_matrix(variation.displacement_jacobian_variation, expected_direction_gradient, polynomial_tolerance); - check_matrix(variation.mapping_jacobian_variation, expected_direction_gradient, polynomial_tolerance); + CAPTURE(q); + check_vector(variation.displacement_variation, expected_direction, + polynomial_tolerance); + check_vector(variation.physical_position_variation, expected_direction, + polynomial_tolerance); + check_matrix(variation.displacement_jacobian_variation, + expected_direction_gradient, polynomial_tolerance); + check_matrix(variation.mapping_jacobian_variation, + expected_direction_gradient, polynomial_tolerance); - check_vector_central_difference( - plus_context.physical_position, minus_context.physical_position, variation.physical_position_variation, - difference_step, linearization_tolerance - ); - check_matrix_central_difference( - plus_context.mapping_jacobian, minus_context.mapping_jacobian, variation.mapping_jacobian_variation, - difference_step, linearization_tolerance - ); - check_matrix_central_difference( - plus_context.inverse_mapping_jacobian, minus_context.inverse_mapping_jacobian, - variation.inverse_mapping_jacobian_variation, difference_step, linearization_tolerance - ); + check_vector_central_difference(plus_context.physical_position, + minus_context.physical_position, + variation.physical_position_variation, + difference_step, linearization_tolerance); + check_matrix_central_difference(plus_context.mapping_jacobian, + minus_context.mapping_jacobian, + variation.mapping_jacobian_variation, + difference_step, linearization_tolerance); + check_matrix_central_difference( + plus_context.inverse_mapping_jacobian, + minus_context.inverse_mapping_jacobian, + variation.inverse_mapping_jacobian_variation, difference_step, + linearization_tolerance); - const double determinant_finite_difference = - (plus_context.mapping_determinant - minus_context.mapping_determinant) / (2.0 * difference_step); - CHECK_THAT( - determinant_finite_difference, WithinAbs(variation.mapping_determinant_variation, linearization_tolerance) - ); - } + const double determinant_finite_difference = + (plus_context.mapping_determinant - minus_context.mapping_determinant) / + (2.0 * difference_step); + CHECK_THAT(determinant_finite_difference, + WithinAbs(variation.mapping_determinant_variation, + linearization_tolerance)); + } } TEST_CASE( "Stateless Domain Mapper Volume Linearization Matches Centered Differences", - tags::unit &tags::mapping -) { - constexpr double linearization_tolerance = 5.0e-9; + tags::unit &tags::mapping) { + constexpr double linearization_tolerance = 5.0e-9; - QuadraticElementFixture fixture; + QuadraticElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - const QuadraticMappingData mapping_data(element, *transformation); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + const QuadraticMappingData mapping_data(element, *transformation); - const mapping::ElementDisplacementData base_displacement(element, mapping_data.base_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData direction(element, mapping_data.direction_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData plus_displacement(element, mapping_data.plus_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData minus_displacement(element, mapping_data.minus_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner base_data(base_displacement); - const ElementMappingDataOwner plus_data(plus_displacement); - const ElementMappingDataOwner minus_data(minus_displacement); + const mapping::ElementDisplacementData base_displacement( + element, mapping_data.base_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData direction( + element, mapping_data.direction_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData plus_displacement( + element, mapping_data.plus_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData minus_displacement( + element, mapping_data.minus_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner base_data(base_displacement); + const ElementMappingDataOwner plus_data(plus_displacement); + const ElementMappingDataOwner minus_data(minus_displacement); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); - mapping::VolumeMappingContext base_context; - mapping::VolumeMappingContext plus_context; - mapping::VolumeMappingContext minus_context; - mapping::VolumeMappingVariation variation; + mapping::VolumeMappingContext base_context; + mapping::VolumeMappingContext plus_context; + mapping::VolumeMappingContext minus_context; + mapping::VolumeMappingVariation variation; - REQUIRE( - mapper.EvaluateVolume(base_data.Get(), *transformation, integration_point, workspace, base_context) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateVolumeVariation( - base_data.Get(), direction, *transformation, integration_point, base_context, workspace, variation - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateVolume(plus_data.Get(), *transformation, integration_point, workspace, plus_context) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateVolume(minus_data.Get(), *transformation, integration_point, workspace, minus_context) == - mapping::MappingStatus::valid - ); + REQUIRE(mapper.EvaluateVolume(base_data.Get(), *transformation, + integration_point, workspace, base_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateVolumeVariation( + base_data.Get(), direction, *transformation, integration_point, + base_context, workspace, + variation) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateVolume(plus_data.Get(), *transformation, + integration_point, workspace, plus_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateVolume( + minus_data.Get(), *transformation, integration_point, workspace, + minus_context) == mapping::MappingStatus::valid); - CAPTURE(q); - check_matrix_central_difference( - plus_context.quadrature.J_inv, minus_context.quadrature.J_inv, variation.inverse_element_jacobian_variation, - difference_step, linearization_tolerance - ); + CAPTURE(q); + check_matrix_central_difference( + plus_context.quadrature.J_inv, minus_context.quadrature.J_inv, + variation.inverse_element_jacobian_variation, difference_step, + linearization_tolerance); - const double determinant_finite_difference = - (plus_context.quadrature.detJ - minus_context.quadrature.detJ) / (2.0 * difference_step); - const double weight_finite_difference = - (plus_context.quadrature.weight - minus_context.quadrature.weight) / (2.0 * difference_step); + const double determinant_finite_difference = + (plus_context.quadrature.detJ - minus_context.quadrature.detJ) / + (2.0 * difference_step); + const double weight_finite_difference = + (plus_context.quadrature.weight - minus_context.quadrature.weight) / + (2.0 * difference_step); - CHECK_THAT( - determinant_finite_difference, - WithinAbs(variation.mapping.mapping_determinant_variation, linearization_tolerance) - ); - CHECK_THAT(weight_finite_difference, WithinAbs(variation.weight_variation, linearization_tolerance)); - } + CHECK_THAT(determinant_finite_difference, + WithinAbs(variation.mapping.mapping_determinant_variation, + linearization_tolerance)); + CHECK_THAT(weight_finite_difference, + WithinAbs(variation.weight_variation, linearization_tolerance)); + } } TEST_CASE( "Stateless Domain Mapper Face Linearization Matches Centered Differences", - tags::unit &tags::mapping -) { - constexpr double normal_tolerance = 2.0e-8; - constexpr double measure_tolerance = 2.0e-8; + tags::unit &tags::mapping) { + constexpr double normal_tolerance = 2.0e-8; + constexpr double measure_tolerance = 2.0e-8; - QuadraticElementFixture fixture; + QuadraticElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *element_transformation = fixture.mesh.GetElementTransformation(0); - const QuadraticMappingData mapping_data(element, *element_transformation); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *element_transformation = + fixture.mesh.GetElementTransformation(0); + const QuadraticMappingData mapping_data(element, *element_transformation); - const mapping::ElementDisplacementData base_displacement(element, mapping_data.base_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData direction(element, mapping_data.direction_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData plus_displacement(element, mapping_data.plus_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData minus_displacement(element, mapping_data.minus_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner base_data(base_displacement); - const ElementMappingDataOwner plus_data(plus_displacement); - const ElementMappingDataOwner minus_data(minus_displacement); + const mapping::ElementDisplacementData base_displacement( + element, mapping_data.base_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData direction( + element, mapping_data.direction_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData plus_displacement( + element, mapping_data.plus_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData minus_displacement( + element, mapping_data.minus_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner base_data(base_displacement); + const ElementMappingDataOwner plus_data(plus_displacement); + const ElementMappingDataOwner minus_data(minus_displacement); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); - for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); ++boundary_element) { - mfem::FaceElementTransformations *transformation = fixture.mesh.GetBdrFaceTransformations(boundary_element); - REQUIRE(transformation != nullptr); - REQUIRE(transformation->Elem1 != nullptr); + for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); + ++boundary_element) { + mfem::FaceElementTransformations *transformation = + fixture.mesh.GetBdrFaceTransformations(boundary_element); + REQUIRE(transformation != nullptr); + REQUIRE(transformation->Elem1 != nullptr); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 4); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 4); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); - mapping::FaceMappingContext base_context; - mapping::FaceMappingContext plus_context; - mapping::FaceMappingContext minus_context; - mapping::FaceMappingVariation variation; + mapping::FaceMappingContext base_context; + mapping::FaceMappingContext plus_context; + mapping::FaceMappingContext minus_context; + mapping::FaceMappingVariation variation; - REQUIRE( - mapper.EvaluateFace( - base_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, workspace, - base_context - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateFaceVariation( - base_data.Get(), direction, *transformation, mapping::FaceElementSide::element_1, integration_point, - base_context, workspace, variation - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateFace( - plus_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, workspace, - plus_context - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateFace( - minus_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, minus_context - ) == mapping::MappingStatus::valid - ); + REQUIRE(mapper.EvaluateFace(base_data.Get(), *transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, base_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFaceVariation( + base_data.Get(), direction, *transformation, + mapping::FaceElementSide::element_1, integration_point, + base_context, workspace, + variation) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFace(plus_data.Get(), *transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, plus_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFace( + minus_data.Get(), *transformation, + mapping::FaceElementSide::element_1, integration_point, + workspace, minus_context) == mapping::MappingStatus::valid); - CAPTURE(boundary_element, q); - check_vector_central_difference( - plus_context.quadrature.normal, minus_context.quadrature.normal, variation.physical_normal_variation, - difference_step, normal_tolerance - ); + CAPTURE(boundary_element, q); + check_vector_central_difference(plus_context.quadrature.normal, + minus_context.quadrature.normal, + variation.physical_normal_variation, + difference_step, normal_tolerance); - const double surface_weight_finite_difference = - (plus_context.physical_surface_weight - minus_context.physical_surface_weight) / - (2.0 * difference_step); - const double normal_scale_finite_difference = - (plus_context.quadrature.v_dot_n_scale - minus_context.quadrature.v_dot_n_scale) / - (2.0 * difference_step); + const double surface_weight_finite_difference = + (plus_context.physical_surface_weight - + minus_context.physical_surface_weight) / + (2.0 * difference_step); + const double normal_scale_finite_difference = + (plus_context.quadrature.v_dot_n_scale - + minus_context.quadrature.v_dot_n_scale) / + (2.0 * difference_step); - CHECK_THAT( - surface_weight_finite_difference, - WithinAbs(variation.physical_surface_weight_variation, measure_tolerance) - ); - CHECK_THAT( - normal_scale_finite_difference, WithinAbs(variation.normal_flux_scale_variation, measure_tolerance) - ); - } + CHECK_THAT(surface_weight_finite_difference, + WithinAbs(variation.physical_surface_weight_variation, + measure_tolerance)); + CHECK_THAT( + normal_scale_finite_difference, + WithinAbs(variation.normal_flux_scale_variation, measure_tolerance)); } + } } -TEST_CASE( - "Stateless Domain Mapper Rejects Invalid Linearization Directions", - tags::unit &tags::mapping -) { - QuadraticElementFixture fixture; +TEST_CASE("Stateless Domain Mapper Rejects Invalid Linearization Directions", + tags::unit &tags::mapping) { + QuadraticElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - const mfem::IntegrationPoint &integration_point = mfem::Geometries.GetCenter(transformation->GetGeometryType()); - const QuadraticMappingData mapping_data(element, *transformation); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = + fixture.mesh.GetElementTransformation(0); + const mfem::IntegrationPoint &integration_point = + mfem::Geometries.GetCenter(transformation->GetGeometryType()); + const QuadraticMappingData mapping_data(element, *transformation); - const mapping::ElementDisplacementData base_displacement(element, mapping_data.base_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner base_data(base_displacement); + const mapping::ElementDisplacementData base_displacement( + element, mapping_data.base_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner base_data(base_displacement); - mfem::Vector non_finite_direction_dofs(mapping_data.direction_dofs); - non_finite_direction_dofs(0) = std::numeric_limits::quiet_NaN(); - const mapping::ElementDisplacementData non_finite_direction( - element, non_finite_direction_dofs, mfem::Ordering::byVDIM - ); + mfem::Vector non_finite_direction_dofs(mapping_data.direction_dofs); + non_finite_direction_dofs(0) = std::numeric_limits::quiet_NaN(); + const mapping::ElementDisplacementData non_finite_direction( + element, non_finite_direction_dofs, mfem::Ordering::byVDIM); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); - mapping::MappingPointContext base_context; - mapping::MappingPointVariation variation; + mapping::MappingPointContext base_context; + mapping::MappingPointVariation variation; - REQUIRE( - mapper.EvaluatePoint(base_data.Get(), *transformation, integration_point, workspace, base_context) == - mapping::MappingStatus::valid - ); - CHECK( - mapper.EvaluatePointVariation( - base_data.Get(), non_finite_direction, *transformation, integration_point, base_context, workspace, - variation - ) == mapping::MappingStatus::non_finite_input - ); + REQUIRE(mapper.EvaluatePoint(base_data.Get(), *transformation, + integration_point, workspace, + base_context) == mapping::MappingStatus::valid); + CHECK(mapper.EvaluatePointVariation(base_data.Get(), non_finite_direction, + *transformation, integration_point, + base_context, workspace, variation) == + mapping::MappingStatus::non_finite_input); - SingleElementFixture linear_fixture; - const mfem::FiniteElement &linear_element = linear_fixture.GetElement(); - const mfem::Vector linear_direction_dofs = linear_fixture.MakeZeroElementDofs(); - const mapping::ElementDisplacementData incompatible_direction( - linear_element, linear_direction_dofs, mfem::Ordering::byVDIM - ); + SingleElementFixture linear_fixture; + const mfem::FiniteElement &linear_element = linear_fixture.GetElement(); + const mfem::Vector linear_direction_dofs = + linear_fixture.MakeZeroElementDofs(); + const mapping::ElementDisplacementData incompatible_direction( + linear_element, linear_direction_dofs, mfem::Ordering::byVDIM); - CHECK_THROWS_AS( - mapper.EvaluatePointVariation( - base_data.Get(), incompatible_direction, *transformation, integration_point, base_context, workspace, - variation - ), - std::invalid_argument - ); + CHECK_THROWS_AS(mapper.EvaluatePointVariation( + base_data.Get(), incompatible_direction, *transformation, + integration_point, base_context, workspace, variation), + std::invalid_argument); } -TEST_CASE( - "Mapping Field Transforms Satisfy Piola And Gradient Identities", - tags::unit &tags::mapping -) { - constexpr double transform_tolerance = 1.0e-12; +TEST_CASE("Mapping Field Transforms Satisfy Piola And Gradient Identities", + tags::unit &tags::mapping) { + constexpr double transform_tolerance = 1.0e-12; - mapping::MappingPointContext context; - context.mapping_jacobian.SetSize(3); - context.mapping_jacobian(0, 0) = 1.20; - context.mapping_jacobian(0, 1) = 0.15; - context.mapping_jacobian(0, 2) = -0.05; - context.mapping_jacobian(1, 0) = -0.08; - context.mapping_jacobian(1, 1) = 0.95; - context.mapping_jacobian(1, 2) = 0.12; - context.mapping_jacobian(2, 0) = 0.04; - context.mapping_jacobian(2, 1) = -0.10; - context.mapping_jacobian(2, 2) = 1.10; + mapping::MappingPointContext context; + context.mapping_jacobian.SetSize(3); + context.mapping_jacobian(0, 0) = 1.20; + context.mapping_jacobian(0, 1) = 0.15; + context.mapping_jacobian(0, 2) = -0.05; + context.mapping_jacobian(1, 0) = -0.08; + context.mapping_jacobian(1, 1) = 0.95; + context.mapping_jacobian(1, 2) = 0.12; + context.mapping_jacobian(2, 0) = 0.04; + context.mapping_jacobian(2, 1) = -0.10; + context.mapping_jacobian(2, 2) = 1.10; - context.mapping_determinant = context.mapping_jacobian.Det(); - REQUIRE(context.mapping_determinant > 0.0); + context.mapping_determinant = context.mapping_jacobian.Det(); + REQUIRE(context.mapping_determinant > 0.0); - context.inverse_mapping_jacobian.SetSize(3); - mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian); + context.inverse_mapping_jacobian.SetSize(3); + mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian); - const mfem::Vector reference_flux = make_vector(0.7, -0.4, 1.1); - const mfem::Vector reference_test_flux = make_vector(-0.2, 0.9, 0.5); - const mfem::Vector reference_gradient = make_vector(0.3, -0.6, 0.8); - const mfem::Vector reference_test_gradient = make_vector(-0.7, 0.2, 0.4); + const mfem::Vector reference_flux = make_vector(0.7, -0.4, 1.1); + const mfem::Vector reference_test_flux = make_vector(-0.2, 0.9, 0.5); + const mfem::Vector reference_gradient = make_vector(0.3, -0.6, 0.8); + const mfem::Vector reference_test_gradient = make_vector(-0.7, 0.2, 0.4); - mfem::Vector physical_flux; - mfem::Vector recovered_flux; - mfem::Vector physical_test_flux; - mfem::Vector physical_gradient; - mfem::Vector recovered_gradient; - mfem::Vector physical_test_gradient; + mfem::Vector physical_flux; + mfem::Vector recovered_flux; + mfem::Vector physical_test_flux; + mfem::Vector physical_gradient; + mfem::Vector recovered_gradient; + mfem::Vector physical_test_gradient; - mapping::MapHDivFluxToPhysical(context, reference_flux, physical_flux); - mapping::MapPhysicalFluxToHDivReference(context, physical_flux, recovered_flux); - mapping::MapHDivFluxToPhysical(context, reference_test_flux, physical_test_flux); + mapping::MapHDivFluxToPhysical(context, reference_flux, physical_flux); + mapping::MapPhysicalFluxToHDivReference(context, physical_flux, + recovered_flux); + mapping::MapHDivFluxToPhysical(context, reference_test_flux, + physical_test_flux); - mapping::MapReferenceGradientToPhysical(context, reference_gradient, physical_gradient); - mapping::MapPhysicalGradientToReference(context, physical_gradient, recovered_gradient); - mapping::MapReferenceGradientToPhysical(context, reference_test_gradient, physical_test_gradient); + mapping::MapReferenceGradientToPhysical(context, reference_gradient, + physical_gradient); + mapping::MapPhysicalGradientToReference(context, physical_gradient, + recovered_gradient); + mapping::MapReferenceGradientToPhysical(context, reference_test_gradient, + physical_test_gradient); - check_vector(recovered_flux, reference_flux, transform_tolerance); - check_vector(recovered_gradient, reference_gradient, transform_tolerance); + check_vector(recovered_flux, reference_flux, transform_tolerance); + check_vector(recovered_gradient, reference_gradient, transform_tolerance); - mfem::DenseMatrix reference_vector_gradient(3); - reference_vector_gradient(0, 0) = 0.20; - reference_vector_gradient(0, 1) = -0.10; - reference_vector_gradient(0, 2) = 0.04; - reference_vector_gradient(1, 0) = 0.03; - reference_vector_gradient(1, 1) = 0.15; - reference_vector_gradient(1, 2) = -0.08; - reference_vector_gradient(2, 0) = -0.05; - reference_vector_gradient(2, 1) = 0.02; - reference_vector_gradient(2, 2) = 0.11; + mfem::DenseMatrix reference_vector_gradient(3); + reference_vector_gradient(0, 0) = 0.20; + reference_vector_gradient(0, 1) = -0.10; + reference_vector_gradient(0, 2) = 0.04; + reference_vector_gradient(1, 0) = 0.03; + reference_vector_gradient(1, 1) = 0.15; + reference_vector_gradient(1, 2) = -0.08; + reference_vector_gradient(2, 0) = -0.05; + reference_vector_gradient(2, 1) = 0.02; + reference_vector_gradient(2, 2) = 0.11; - mfem::DenseMatrix physical_vector_gradient; - mfem::DenseMatrix recovered_vector_gradient; + mfem::DenseMatrix physical_vector_gradient; + mfem::DenseMatrix recovered_vector_gradient; - mapping::MapReferenceVectorGradientToPhysical(context, reference_vector_gradient, physical_vector_gradient); - mapping::MapPhysicalVectorGradientToReference(context, physical_vector_gradient, recovered_vector_gradient); - check_matrix(recovered_vector_gradient, reference_vector_gradient, transform_tolerance); + mapping::MapReferenceVectorGradientToPhysical( + context, reference_vector_gradient, physical_vector_gradient); + mapping::MapPhysicalVectorGradientToReference( + context, physical_vector_gradient, recovered_vector_gradient); + check_matrix(recovered_vector_gradient, reference_vector_gradient, + transform_tolerance); - mfem::DenseMatrix hdiv_mass_tensor; - mfem::DenseMatrix diffusion_tensor; - mapping::ComputeHDivMassTensor(context, hdiv_mass_tensor); - mapping::ComputeScalarDiffusionTensor(context, diffusion_tensor); + mfem::DenseMatrix hdiv_mass_tensor; + mfem::DenseMatrix diffusion_tensor; + mapping::ComputeHDivMassTensor(context, hdiv_mass_tensor); + mapping::ComputeScalarDiffusionTensor(context, diffusion_tensor); - mfem::Vector mass_action(3); - mfem::Vector diffusion_action(3); - hdiv_mass_tensor.Mult(reference_test_flux, mass_action); - diffusion_tensor.Mult(reference_test_gradient, diffusion_action); + mfem::Vector mass_action(3); + mfem::Vector diffusion_action(3); + hdiv_mass_tensor.Mult(reference_test_flux, mass_action); + diffusion_tensor.Mult(reference_test_gradient, diffusion_action); - const double physical_hdiv_inner_product = context.mapping_determinant * (physical_flux * physical_test_flux); - const double reference_hdiv_inner_product = reference_flux * mass_action; - const double physical_gradient_inner_product = - context.mapping_determinant * (physical_gradient * physical_test_gradient); - const double reference_gradient_inner_product = reference_gradient * diffusion_action; + const double physical_hdiv_inner_product = + context.mapping_determinant * (physical_flux * physical_test_flux); + const double reference_hdiv_inner_product = reference_flux * mass_action; + const double physical_gradient_inner_product = + context.mapping_determinant * + (physical_gradient * physical_test_gradient); + const double reference_gradient_inner_product = + reference_gradient * diffusion_action; - CHECK_THAT(physical_hdiv_inner_product, WithinAbs(reference_hdiv_inner_product, transform_tolerance)); - CHECK_THAT(physical_gradient_inner_product, WithinAbs(reference_gradient_inner_product, transform_tolerance)); + CHECK_THAT(physical_hdiv_inner_product, + WithinAbs(reference_hdiv_inner_product, transform_tolerance)); + CHECK_THAT(physical_gradient_inner_product, + WithinAbs(reference_gradient_inner_product, transform_tolerance)); - const double reference_divergence = 0.73; - const double physical_divergence = mapping::MapHDivDivergenceToPhysical(context, reference_divergence); - CHECK_THAT(physical_divergence, WithinAbs(reference_divergence / context.mapping_determinant, transform_tolerance)); + const double reference_divergence = 0.73; + const double physical_divergence = + mapping::MapHDivDivergenceToPhysical(context, reference_divergence); + CHECK_THAT(physical_divergence, + WithinAbs(reference_divergence / context.mapping_determinant, + transform_tolerance)); } -TEST_CASE( - "Mapped Hdiv Flux Preserves Physical Face Flux", - tags::unit &tags::mapping -) { - constexpr double flux_tolerance = 2.0e-11; +TEST_CASE("Mapped Hdiv Flux Preserves Physical Face Flux", + tags::unit &tags::mapping) { + constexpr double flux_tolerance = 2.0e-11; - SingleElementFixture fixture; + SingleElementFixture fixture; - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *element_transformation = fixture.mesh.GetElementTransformation(0); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *element_transformation = + fixture.mesh.GetElementTransformation(0); - const mfem::DenseMatrix displacement_gradient = make_affine_displacement_gradient(); - const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); - const mfem::Vector element_dofs = make_affine_element_dofs( - element, *element_transformation, displacement_gradient, displacement_offset, mfem::Ordering::byVDIM - ); - const mapping::ElementDisplacementData displacement(element, element_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); + const mfem::DenseMatrix displacement_gradient = + make_affine_displacement_gradient(); + const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); + const mfem::Vector element_dofs = make_affine_element_dofs( + element, *element_transformation, displacement_gradient, + displacement_offset, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData displacement(element, element_dofs, + mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); - const mfem::Vector reference_flux = make_vector(0.7, -0.4, 1.1); + const mfem::Vector reference_flux = make_vector(0.7, -0.4, 1.1); - for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); ++boundary_element) { - mfem::FaceElementTransformations *transformation = fixture.mesh.GetBdrFaceTransformations(boundary_element); - REQUIRE(transformation != nullptr); + for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); + ++boundary_element) { + mfem::FaceElementTransformations *transformation = + fixture.mesh.GetBdrFaceTransformations(boundary_element); + REQUIRE(transformation != nullptr); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - mapping::FaceMappingContext context; - mfem::Vector physical_flux; + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + mapping::FaceMappingContext context; + mfem::Vector physical_flux; - REQUIRE( - mapper.EvaluateFace( - element_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, context - ) == mapping::MappingStatus::valid - ); + REQUIRE(mapper.EvaluateFace(element_data.Get(), *transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, + context) == mapping::MappingStatus::valid); - mapping::MapHDivFluxToPhysical(context.mapping, reference_flux, physical_flux); + mapping::MapHDivFluxToPhysical(context.mapping, reference_flux, + physical_flux); - const double reference_integrated_flux = - (reference_flux * context.reference_normal) * context.reference_surface_weight; - const double physical_integrated_flux = - (physical_flux * context.quadrature.normal) * context.physical_surface_weight; + const double reference_integrated_flux = + (reference_flux * context.reference_normal) * + context.reference_surface_weight; + const double physical_integrated_flux = + (physical_flux * context.quadrature.normal) * + context.physical_surface_weight; - CAPTURE(boundary_element, q); - CHECK_THAT(physical_integrated_flux, WithinAbs(reference_integrated_flux, flux_tolerance)); - } + CAPTURE(boundary_element, q); + CHECK_THAT(physical_integrated_flux, + WithinAbs(reference_integrated_flux, flux_tolerance)); } + } } -TEST_CASE( - "Stateless Domain Mapper Represents Strong Rotating Star Geometry", - tags::integration &tags::mapping -) { - constexpr double r_star = 1.0; - constexpr double r_infinity = 4.0; - constexpr double transform_tolerance = 2.0e-10; +TEST_CASE("Stateless Domain Mapper Represents Strong Rotating Star Geometry", + tags::integration &tags::mapping) { + constexpr double r_star = 1.0; + constexpr double r_infinity = 4.0; + constexpr double transform_tolerance = 2.0e-10; - stroid::config::MeshConfig mesh_config; - mesh_config.refinement_levels = 0; - mesh_config.order = 2; - mesh_config.include_external_domain = true; - mesh_config.r_core = 0.25; - mesh_config.r_star = r_star; - mesh_config.r_infinity = r_infinity; - mesh_config.flattening = 0.0; - mesh_config.optimization_methods = stroid::config::OptimizationMethods{false, true}; + stroid::config::MeshConfig mesh_config; + mesh_config.refinement_levels = 0; + mesh_config.order = 2; + mesh_config.include_external_domain = true; + mesh_config.r_core = 0.25; + mesh_config.r_star = r_star; + mesh_config.r_infinity = r_infinity; + mesh_config.flattening = 0.0; + mesh_config.optimization_methods = + stroid::config::OptimizationMethods{false, true}; - stroid::StroidMesh stroid_mesh = stroid::GenerateMesh(mesh_config); - mfem::Mesh &mesh = *stroid_mesh.mesh; - REQUIRE(stroid_mesh.exterior_coordinate != nullptr); - REQUIRE(stroid_mesh.exterior_coordinate->space != nullptr); - REQUIRE(stroid_mesh.exterior_coordinate->values != nullptr); + stroid::StroidMesh stroid_mesh = stroid::GenerateMesh(mesh_config); + mfem::Mesh &mesh = *stroid_mesh.mesh; + REQUIRE(stroid_mesh.exterior_coordinate != nullptr); + REQUIRE(stroid_mesh.exterior_coordinate->space != nullptr); + REQUIRE(stroid_mesh.exterior_coordinate->values != nullptr); - mfem::FiniteElementSpace &compactification_space = *stroid_mesh.exterior_coordinate->space; - mfem::GridFunction &compactification_coordinate = *stroid_mesh.exterior_coordinate->values; + mfem::FiniteElementSpace &compactification_space = + *stroid_mesh.exterior_coordinate->space; + mfem::GridFunction &compactification_coordinate = + *stroid_mesh.exterior_coordinate->values; - mfem::H1_FECollection displacement_collection(3, dimension); - mfem::FiniteElementSpace displacement_space(&mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); - mfem::GridFunction displacement(&displacement_space); + mfem::H1_FECollection displacement_collection(3, dimension); + mfem::FiniteElementSpace displacement_space( + &mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); + mfem::GridFunction displacement(&displacement_space); - auto rotating_displacement = - [r_star, r_infinity](const mfem::Vector &reference_position, mfem::Vector &displacement_value) { - const double x = reference_position(0); - const double y = reference_position(1); - const double z = reference_position(2); - const double radius_squared = x * x + y * y + z * z; - const double radius = std::sqrt(radius_squared); + auto rotating_displacement = + [r_star, r_infinity](const mfem::Vector &reference_position, + mfem::Vector &displacement_value) { + const double x = reference_position(0); + const double y = reference_position(1); + const double z = reference_position(2); + const double radius_squared = x * x + y * y + z * z; + const double radius = std::sqrt(radius_squared); - displacement_value.SetSize(3); - displacement_value = 0.0; + displacement_value.SetSize(3); + displacement_value = 0.0; - if (radius <= 1.0e-14) - return; + if (radius <= 1.0e-14) + return; - const double cylindrical_fraction = (x * x + y * y) / radius_squared; - const double angular_deformation = - 0.20 * cylindrical_fraction + 0.12 * cylindrical_fraction * cylindrical_fraction; + const double cylindrical_fraction = (x * x + y * y) / radius_squared; + const double angular_deformation = + 0.20 * cylindrical_fraction + + 0.12 * cylindrical_fraction * cylindrical_fraction; - double radial_extension = 0.0; - if (radius <= r_star) { - radial_extension = radius_squared / (r_star * r_star); - } else { - radial_extension = std::max(0.0, (r_infinity - radius) / (r_infinity - r_star)); - } - - const double scale = radial_extension * angular_deformation; - displacement_value(0) = scale * x; - displacement_value(1) = scale * y; - displacement_value(2) = scale * z; - }; - - mfem::VectorFunctionCoefficient displacement_coefficient(dimension, rotating_displacement); - displacement.ProjectCoefficient(displacement_coefficient); - - std::unique_ptr exterior_map = - std::make_unique( - mapping::compactification::options::KelvinCompactificationOptions{ - .r_star_ref = r_star, .r_inf_ref = r_infinity - } - ); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, std::move(exterior_map) - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - - double minimum_mapping_determinant = std::numeric_limits::infinity(); - double maximum_mapping_determinant = 0.0; - double stellar_volume = 0.0; - double moment_x = 0.0; - double moment_y = 0.0; - double moment_z = 0.0; - - int stellar_elements = 0; - int vacuum_elements = 0; - - for (int element_id = 0; element_id < mesh.GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = mesh.GetElementTransformation(element_id); - const mfem::FiniteElement *element = displacement_space.GetFE(element_id); - - mfem::Array element_vdofs; - mfem::Vector element_dofs; - displacement_space.GetElementVDofs(element_id, element_vdofs); - displacement.GetSubVector(element_vdofs, element_dofs); - - const mapping::ElementDisplacementData element_displacement = - mapping::ElementDisplacementDataFromElementVDofs(*element, element_dofs); - - mfem::Array compactification_dof_indices; - mfem::Vector compactification_dofs; - compactification_space.GetElementDofs(element_id, compactification_dof_indices); - compactification_coordinate.GetSubVector(compactification_dof_indices, compactification_dofs); - - const ElementMappingDataOwner element_data( - element_displacement, *compactification_space.GetFE(element_id), compactification_dofs - ); - const int quadrature_order = 2 * element->GetOrder() + 6; - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - - if (transformation->Attribute == 3) { - ++vacuum_elements; + double radial_extension = 0.0; + if (radius <= r_star) { + radial_extension = radius_squared / (r_star * r_star); } else { - ++stellar_elements; + radial_extension = + std::max(0.0, (r_infinity - radius) / (r_infinity - r_star)); } - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - mapping::VolumeMappingContext context; + const double scale = radial_extension * angular_deformation; + displacement_value(0) = scale * x; + displacement_value(1) = scale * y; + displacement_value(2) = scale * z; + }; - REQUIRE( - mapper.EvaluateVolume(element_data.Get(), *transformation, integration_point, workspace, context) == - mapping::MappingStatus::valid - ); + mfem::VectorFunctionCoefficient displacement_coefficient( + dimension, rotating_displacement); + displacement.ProjectCoefficient(displacement_coefficient); - minimum_mapping_determinant = std::min(minimum_mapping_determinant, context.mapping.mapping_determinant); - maximum_mapping_determinant = std::max(maximum_mapping_determinant, context.mapping.mapping_determinant); + std::unique_ptr + exterior_map = + std::make_unique( + mapping::compactification::options::KelvinCompactificationOptions{ + .r_star_ref = r_star, .r_inf_ref = r_infinity}); - REQUIRE(context.mapping.mapping_determinant > 0.0); - REQUIRE(context.quadrature.weight > 0.0); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + std::move(exterior_map)); + mapping::DomainMapper::Workspace workspace(dimension); - mfem::Vector reference_flux = make_vector( - 0.4 + context.mapping.reference_position(0), -0.3 + 0.5 * context.mapping.reference_position(1), - 0.7 - 0.2 * context.mapping.reference_position(2) - ); + double minimum_mapping_determinant = std::numeric_limits::infinity(); + double maximum_mapping_determinant = 0.0; + double stellar_volume = 0.0; + double moment_x = 0.0; + double moment_y = 0.0; + double moment_z = 0.0; - mfem::Vector physical_flux; - mfem::Vector recovered_flux; - mapping::MapHDivFluxToPhysical(context.mapping, reference_flux, physical_flux); - mapping::MapPhysicalFluxToHDivReference(context.mapping, physical_flux, recovered_flux); - check_vector(recovered_flux, reference_flux, transform_tolerance); + int stellar_elements = 0; + int vacuum_elements = 0; - const mfem::Vector reference_gradient = make_vector(0.3, -0.5, 0.8); - mfem::Vector physical_gradient; - mfem::Vector recovered_gradient; - mapping::MapReferenceGradientToPhysical(context.mapping, reference_gradient, physical_gradient); - mapping::MapPhysicalGradientToReference(context.mapping, physical_gradient, recovered_gradient); - check_vector(recovered_gradient, reference_gradient, transform_tolerance); + for (int element_id = 0; element_id < mesh.GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + mesh.GetElementTransformation(element_id); + const mfem::FiniteElement *element = displacement_space.GetFE(element_id); - if (transformation->Attribute == 3) { - transformation->SetIntPoint(&integration_point); + mfem::Array element_vdofs; + mfem::Vector element_dofs; + displacement_space.GetElementVDofs(element_id, element_vdofs); + displacement.GetSubVector(element_vdofs, element_dofs); - mfem::Vector compactification_gradient(dimension); - const double coordinate = compactification_coordinate.GetValue(element_id, integration_point); - compactification_coordinate.GetGradient(*transformation, compactification_gradient); + const mapping::ElementDisplacementData element_displacement = + mapping::ElementDisplacementDataFromElementVDofs(*element, + element_dofs); - mapping::compactification::ExteriorMapResult direct_result; - const mapping::compactification::ExteriorMapInput direct_input{ - .reference_position = context.mapping.reference_position, - .displaced_position = context.mapping.displaced_position, - .displacement_jacobian = context.mapping.displacement_jacobian, - .compactification_coordinate = coordinate, - .compactification_coordinate_gradient = compactification_gradient - }; + mfem::Array compactification_dof_indices; + mfem::Vector compactification_dofs; + compactification_space.GetElementDofs(element_id, + compactification_dof_indices); + compactification_coordinate.GetSubVector(compactification_dof_indices, + compactification_dofs); - REQUIRE(mapper.GetExteriorMap().Evaluate(direct_input, direct_result) == mapping::MappingStatus::valid); - check_vector(context.mapping.physical_position, direct_result.physical_position, transform_tolerance); - check_matrix(context.mapping.mapping_jacobian, direct_result.mapping_jacobian, transform_tolerance); - } else { - check_vector( - context.mapping.physical_position, context.mapping.displaced_position, transform_tolerance - ); + const ElementMappingDataOwner element_data( + element_displacement, *compactification_space.GetFE(element_id), + compactification_dofs); + const int quadrature_order = 2 * element->GetOrder() + 6; + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - const double x = context.mapping.physical_position(0); - const double y = context.mapping.physical_position(1); - const double z = context.mapping.physical_position(2); - const double weight = context.quadrature.weight; - - stellar_volume += weight; - moment_x += x * x * weight; - moment_y += y * y * weight; - moment_z += z * z * weight; - } - } + if (transformation->Attribute == 3) { + ++vacuum_elements; + } else { + ++stellar_elements; } - REQUIRE(stellar_elements > 0); - REQUIRE(vacuum_elements > 0); - REQUIRE(stellar_volume > 0.0); - REQUIRE(std::isfinite(minimum_mapping_determinant)); - REQUIRE(minimum_mapping_determinant > 0.0); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + mapping::VolumeMappingContext context; - const double moment_trace = moment_x + moment_y + moment_z; - const double quadrupole_x = 3.0 * moment_x - moment_trace; - const double quadrupole_y = 3.0 * moment_y - moment_trace; - const double quadrupole_z = 3.0 * moment_z - moment_trace; - const double normalized_quadrupole = - std::sqrt(quadrupole_x * quadrupole_x + quadrupole_y * quadrupole_y + quadrupole_z * quadrupole_z) / - moment_trace; - const double axisymmetry_error = std::abs(moment_x - moment_y) / (0.5 * (moment_x + moment_y)); + REQUIRE(mapper.EvaluateVolume(element_data.Get(), *transformation, + integration_point, workspace, + context) == mapping::MappingStatus::valid); - INFO("Stellar volume = " << stellar_volume); - INFO("Minimum mapping determinant = " << minimum_mapping_determinant); - INFO("Maximum mapping determinant = " << maximum_mapping_determinant); - INFO("Normalized geometric quadrupole = " << normalized_quadrupole); - INFO("Axisymmetry error = " << axisymmetry_error); + minimum_mapping_determinant = std::min( + minimum_mapping_determinant, context.mapping.mapping_determinant); + maximum_mapping_determinant = std::max( + maximum_mapping_determinant, context.mapping.mapping_determinant); - CHECK(moment_x > moment_z); - CHECK(moment_y > moment_z); - CHECK(normalized_quadrupole > 1.0e-2); - CHECK(axisymmetry_error < 5.0e-2); + REQUIRE(context.mapping.mapping_determinant > 0.0); + REQUIRE(context.quadrature.weight > 0.0); + + mfem::Vector reference_flux = + make_vector(0.4 + context.mapping.reference_position(0), + -0.3 + 0.5 * context.mapping.reference_position(1), + 0.7 - 0.2 * context.mapping.reference_position(2)); + + mfem::Vector physical_flux; + mfem::Vector recovered_flux; + mapping::MapHDivFluxToPhysical(context.mapping, reference_flux, + physical_flux); + mapping::MapPhysicalFluxToHDivReference(context.mapping, physical_flux, + recovered_flux); + check_vector(recovered_flux, reference_flux, transform_tolerance); + + const mfem::Vector reference_gradient = make_vector(0.3, -0.5, 0.8); + mfem::Vector physical_gradient; + mfem::Vector recovered_gradient; + mapping::MapReferenceGradientToPhysical( + context.mapping, reference_gradient, physical_gradient); + mapping::MapPhysicalGradientToReference( + context.mapping, physical_gradient, recovered_gradient); + check_vector(recovered_gradient, reference_gradient, transform_tolerance); + + if (transformation->Attribute == 3) { + transformation->SetIntPoint(&integration_point); + + mfem::Vector compactification_gradient(dimension); + const double coordinate = + compactification_coordinate.GetValue(element_id, integration_point); + compactification_coordinate.GetGradient(*transformation, + compactification_gradient); + + mapping::compactification::ExteriorMapResult direct_result; + const mapping::compactification::ExteriorMapInput direct_input{ + .reference_position = context.mapping.reference_position, + .displaced_position = context.mapping.displaced_position, + .displacement_jacobian = context.mapping.displacement_jacobian, + .compactification_coordinate = coordinate, + .compactification_coordinate_gradient = compactification_gradient}; + + REQUIRE(mapper.GetExteriorMap().Evaluate(direct_input, direct_result) == + mapping::MappingStatus::valid); + check_vector(context.mapping.physical_position, + direct_result.physical_position, transform_tolerance); + check_matrix(context.mapping.mapping_jacobian, + direct_result.mapping_jacobian, transform_tolerance); + } else { + check_vector(context.mapping.physical_position, + context.mapping.displaced_position, transform_tolerance); + + const double x = context.mapping.physical_position(0); + const double y = context.mapping.physical_position(1); + const double z = context.mapping.physical_position(2); + const double weight = context.quadrature.weight; + + stellar_volume += weight; + moment_x += x * x * weight; + moment_y += y * y * weight; + moment_z += z * z * weight; + } + } + } + + REQUIRE(stellar_elements > 0); + REQUIRE(vacuum_elements > 0); + REQUIRE(stellar_volume > 0.0); + REQUIRE(std::isfinite(minimum_mapping_determinant)); + REQUIRE(minimum_mapping_determinant > 0.0); + + const double moment_trace = moment_x + moment_y + moment_z; + const double quadrupole_x = 3.0 * moment_x - moment_trace; + const double quadrupole_y = 3.0 * moment_y - moment_trace; + const double quadrupole_z = 3.0 * moment_z - moment_trace; + const double normalized_quadrupole = + std::sqrt(quadrupole_x * quadrupole_x + quadrupole_y * quadrupole_y + + quadrupole_z * quadrupole_z) / + moment_trace; + const double axisymmetry_error = + std::abs(moment_x - moment_y) / (0.5 * (moment_x + moment_y)); + + INFO("Stellar volume = " << stellar_volume); + INFO("Minimum mapping determinant = " << minimum_mapping_determinant); + INFO("Maximum mapping determinant = " << maximum_mapping_determinant); + INFO("Normalized geometric quadrupole = " << normalized_quadrupole); + INFO("Axisymmetry error = " << axisymmetry_error); + + CHECK(moment_x > moment_z); + CHECK(moment_y > moment_z); + CHECK(normalized_quadrupole > 1.0e-2); + CHECK(axisymmetry_error < 5.0e-2); } TEST_CASE( "Kelvin Composed Domain Mapping Linearization Matches Centered Differences", - tags::unit &tags::mapping -) { - constexpr double relative_tolerance = 2.0e-6; - constexpr double kelvin_difference_step = 2.0e-4; + tags::unit &tags::mapping) { + constexpr double relative_tolerance = 2.0e-6; + constexpr double kelvin_difference_step = 2.0e-4; - mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON, 2.98, 0.02, 0.02); + mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D( + 1, 1, 1, mfem::Element::HEXAHEDRON, 2.98, 0.02, 0.02); - for (int vertex_id = 0; vertex_id < mesh.GetNV(); ++vertex_id) { - double *vertex = mesh.GetVertex(vertex_id); - vertex[0] += 1.0; - vertex[1] -= 0.01; - vertex[2] -= 0.01; - } - - mesh.GetElement(0)->SetAttribute(3); - mesh.SetAttributes(); - - mfem::H1_FECollection displacement_collection(2, dimension); - mfem::FiniteElementSpace displacement_space(&mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); - - const mfem::FiniteElement &element = *displacement_space.GetFE(0); - mfem::ElementTransformation *transformation = mesh.GetElementTransformation(0); - - const mfem::Vector base_dofs = - make_function_element_dofs(element, *transformation, evaluate_quadratic_displacement, mfem::Ordering::byVDIM); - const mfem::Vector direction_dofs = - make_function_element_dofs(element, *transformation, evaluate_quadratic_direction, mfem::Ordering::byVDIM); - - mfem::Vector plus_dofs(base_dofs); - mfem::Vector minus_dofs(base_dofs); - plus_dofs.Add(kelvin_difference_step, direction_dofs); - minus_dofs.Add(-kelvin_difference_step, direction_dofs); - - const mapping::ElementDisplacementData base_displacement(element, base_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData direction(element, direction_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData plus_displacement(element, plus_dofs, mfem::Ordering::byVDIM); - const mapping::ElementDisplacementData minus_displacement(element, minus_dofs, mfem::Ordering::byVDIM); - - const mfem::Vector compactification_dofs = - make_compactification_element_dofs(element, *transformation, [](const mfem::Vector &reference_position) { - return (reference_position(0) - 1.0) / 3.0; - }); - - const ElementMappingDataOwner base_data(base_displacement, element, compactification_dofs); - const ElementMappingDataOwner plus_data(plus_displacement, element, compactification_dofs); - const ElementMappingDataOwner minus_data(minus_displacement, element, compactification_dofs); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - - auto vector_central_difference = [](const mfem::Vector &plus_value, const mfem::Vector &minus_value) { - mfem::Vector difference(plus_value); - difference -= minus_value; - difference *= 1.0 / (2.0 * kelvin_difference_step); - return difference; - }; - - auto matrix_central_difference = [](const mfem::DenseMatrix &plus_value, const mfem::DenseMatrix &minus_value) { - mfem::DenseMatrix difference(plus_value); - difference -= minus_value; - difference *= 1.0 / (2.0 * kelvin_difference_step); - return difference; - }; - - for (const double xi : std::array{0.0, 0.25, 0.75, 0.95, 0.99}) { - mfem::IntegrationPoint integration_point; - integration_point.x = 3.0 * xi / 2.98; - integration_point.y = 0.5; - integration_point.z = 0.5; - integration_point.weight = 0.73; - - mapping::VolumeMappingContext base_context; - mapping::VolumeMappingContext plus_context; - mapping::VolumeMappingContext minus_context; - mapping::VolumeMappingVariation variation; - - REQUIRE( - mapper.EvaluateVolume(base_data.Get(), *transformation, integration_point, workspace, base_context) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateVolumeVariation( - base_data.Get(), direction, *transformation, integration_point, base_context, workspace, variation - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateVolume(plus_data.Get(), *transformation, integration_point, workspace, plus_context) == - mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateVolume(minus_data.Get(), *transformation, integration_point, workspace, minus_context) == - mapping::MappingStatus::valid - ); - - CAPTURE(xi); - REQUIRE(base_context.mapping.compactified); - - const mfem::Vector physical_position_difference = - vector_central_difference(plus_context.mapping.physical_position, minus_context.mapping.physical_position); - const mfem::DenseMatrix mapping_jacobian_difference = - matrix_central_difference(plus_context.mapping.mapping_jacobian, minus_context.mapping.mapping_jacobian); - const mfem::DenseMatrix inverse_mapping_jacobian_difference = matrix_central_difference( - plus_context.mapping.inverse_mapping_jacobian, minus_context.mapping.inverse_mapping_jacobian - ); - const mfem::DenseMatrix inverse_element_jacobian_difference = - matrix_central_difference(plus_context.quadrature.J_inv, minus_context.quadrature.J_inv); - - CHECK_THAT( - relative_vector_difference(physical_position_difference, variation.mapping.physical_position_variation), - Catch::Matchers::WithinAbs(0.0, relative_tolerance) - ); - CHECK_THAT( - relative_matrix_difference(mapping_jacobian_difference, variation.mapping.mapping_jacobian_variation), - Catch::Matchers::WithinAbs(0.0, relative_tolerance) - ); - CHECK_THAT( - relative_matrix_difference( - inverse_mapping_jacobian_difference, variation.mapping.inverse_mapping_jacobian_variation - ), - Catch::Matchers::WithinAbs(0.0, relative_tolerance) - ); - CHECK_THAT( - relative_matrix_difference( - inverse_element_jacobian_difference, variation.inverse_element_jacobian_variation - ), - Catch::Matchers::WithinAbs(0.0, relative_tolerance) - ); - - const double determinant_difference = - (plus_context.mapping.mapping_determinant - minus_context.mapping.mapping_determinant) / - (2.0 * kelvin_difference_step); - const double weight_difference = - (plus_context.quadrature.weight - minus_context.quadrature.weight) / (2.0 * kelvin_difference_step); - - check_centered_difference( - variation.mapping.mapping_determinant_variation, determinant_difference, - plus_context.mapping.mapping_determinant, minus_context.mapping.mapping_determinant, kelvin_difference_step, - relative_tolerance - ); - check_centered_difference( - variation.weight_variation, weight_difference, plus_context.quadrature.weight, - minus_context.quadrature.weight, kelvin_difference_step, relative_tolerance - ); - } - - for (int boundary_element = 0; boundary_element < mesh.GetNBE(); ++boundary_element) { - mfem::FaceElementTransformations *face_transformation = mesh.GetBdrFaceTransformations(boundary_element); - REQUIRE(face_transformation != nullptr); - - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(face_transformation->GetGeometryType(), 4); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - - mapping::FaceMappingContext base_context; - mapping::FaceMappingContext plus_context; - mapping::FaceMappingContext minus_context; - mapping::FaceMappingVariation variation; - - REQUIRE( - mapper.EvaluateFace( - base_data.Get(), *face_transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, base_context - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateFaceVariation( - base_data.Get(), direction, *face_transformation, mapping::FaceElementSide::element_1, - integration_point, base_context, workspace, variation - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateFace( - plus_data.Get(), *face_transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, plus_context - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateFace( - minus_data.Get(), *face_transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, minus_context - ) == mapping::MappingStatus::valid - ); - - CAPTURE(boundary_element, q); - - const mfem::Vector normal_difference = - vector_central_difference(plus_context.quadrature.normal, minus_context.quadrature.normal); - - REQUIRE(normal_difference.Size() == variation.physical_normal_variation.Size()); - for (int i = 0; i < normal_difference.Size(); ++i) { - check_centered_difference( - variation.physical_normal_variation(i), normal_difference(i), plus_context.quadrature.normal(i), - minus_context.quadrature.normal(i), kelvin_difference_step, relative_tolerance - ); - } - - const double surface_weight_difference = - (plus_context.physical_surface_weight - minus_context.physical_surface_weight) / - (2.0 * kelvin_difference_step); - const double normal_scale_difference = - (plus_context.quadrature.v_dot_n_scale - minus_context.quadrature.v_dot_n_scale) / - (2.0 * kelvin_difference_step); - - check_centered_difference( - variation.physical_surface_weight_variation, surface_weight_difference, - plus_context.physical_surface_weight, minus_context.physical_surface_weight, kelvin_difference_step, - relative_tolerance - ); - check_centered_difference( - variation.normal_flux_scale_variation, normal_scale_difference, plus_context.quadrature.v_dot_n_scale, - minus_context.quadrature.v_dot_n_scale, kelvin_difference_step, relative_tolerance - ); - } + for (int vertex_id = 0; vertex_id < mesh.GetNV(); ++vertex_id) { + double *vertex = mesh.GetVertex(vertex_id); + vertex[0] += 1.0; + vertex[1] -= 0.01; + vertex[2] -= 0.01; + } + + mesh.GetElement(0)->SetAttribute(3); + mesh.SetAttributes(); + + mfem::H1_FECollection displacement_collection(2, dimension); + mfem::FiniteElementSpace displacement_space( + &mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); + + const mfem::FiniteElement &element = *displacement_space.GetFE(0); + mfem::ElementTransformation *transformation = + mesh.GetElementTransformation(0); + + const mfem::Vector base_dofs = make_function_element_dofs( + element, *transformation, evaluate_quadratic_displacement, + mfem::Ordering::byVDIM); + const mfem::Vector direction_dofs = make_function_element_dofs( + element, *transformation, evaluate_quadratic_direction, + mfem::Ordering::byVDIM); + + mfem::Vector plus_dofs(base_dofs); + mfem::Vector minus_dofs(base_dofs); + plus_dofs.Add(kelvin_difference_step, direction_dofs); + minus_dofs.Add(-kelvin_difference_step, direction_dofs); + + const mapping::ElementDisplacementData base_displacement( + element, base_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData direction(element, direction_dofs, + mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData plus_displacement( + element, plus_dofs, mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData minus_displacement( + element, minus_dofs, mfem::Ordering::byVDIM); + + const mfem::Vector compactification_dofs = make_compactification_element_dofs( + element, *transformation, [](const mfem::Vector &reference_position) { + return (reference_position(0) - 1.0) / 3.0; + }); + + const ElementMappingDataOwner base_data(base_displacement, element, + compactification_dofs); + const ElementMappingDataOwner plus_data(plus_displacement, element, + compactification_dofs); + const ElementMappingDataOwner minus_data(minus_displacement, element, + compactification_dofs); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + + auto vector_central_difference = [](const mfem::Vector &plus_value, + const mfem::Vector &minus_value) { + mfem::Vector difference(plus_value); + difference -= minus_value; + difference *= 1.0 / (2.0 * kelvin_difference_step); + return difference; + }; + + auto matrix_central_difference = [](const mfem::DenseMatrix &plus_value, + const mfem::DenseMatrix &minus_value) { + mfem::DenseMatrix difference(plus_value); + difference -= minus_value; + difference *= 1.0 / (2.0 * kelvin_difference_step); + return difference; + }; + + for (const double xi : std::array{0.0, 0.25, 0.75, 0.95, 0.99}) { + mfem::IntegrationPoint integration_point; + integration_point.x = 3.0 * xi / 2.98; + integration_point.y = 0.5; + integration_point.z = 0.5; + integration_point.weight = 0.73; + + mapping::VolumeMappingContext base_context; + mapping::VolumeMappingContext plus_context; + mapping::VolumeMappingContext minus_context; + mapping::VolumeMappingVariation variation; + + REQUIRE(mapper.EvaluateVolume(base_data.Get(), *transformation, + integration_point, workspace, base_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateVolumeVariation( + base_data.Get(), direction, *transformation, integration_point, + base_context, workspace, + variation) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateVolume(plus_data.Get(), *transformation, + integration_point, workspace, plus_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateVolume( + minus_data.Get(), *transformation, integration_point, workspace, + minus_context) == mapping::MappingStatus::valid); + + CAPTURE(xi); + REQUIRE(base_context.mapping.compactified); + + const mfem::Vector physical_position_difference = + vector_central_difference(plus_context.mapping.physical_position, + minus_context.mapping.physical_position); + const mfem::DenseMatrix mapping_jacobian_difference = + matrix_central_difference(plus_context.mapping.mapping_jacobian, + minus_context.mapping.mapping_jacobian); + const mfem::DenseMatrix inverse_mapping_jacobian_difference = + matrix_central_difference( + plus_context.mapping.inverse_mapping_jacobian, + minus_context.mapping.inverse_mapping_jacobian); + const mfem::DenseMatrix inverse_element_jacobian_difference = + matrix_central_difference(plus_context.quadrature.J_inv, + minus_context.quadrature.J_inv); + + CHECK_THAT(relative_vector_difference( + physical_position_difference, + variation.mapping.physical_position_variation), + Catch::Matchers::WithinAbs(0.0, relative_tolerance)); + CHECK_THAT(relative_matrix_difference( + mapping_jacobian_difference, + variation.mapping.mapping_jacobian_variation), + Catch::Matchers::WithinAbs(0.0, relative_tolerance)); + CHECK_THAT(relative_matrix_difference( + inverse_mapping_jacobian_difference, + variation.mapping.inverse_mapping_jacobian_variation), + Catch::Matchers::WithinAbs(0.0, relative_tolerance)); + CHECK_THAT(relative_matrix_difference( + inverse_element_jacobian_difference, + variation.inverse_element_jacobian_variation), + Catch::Matchers::WithinAbs(0.0, relative_tolerance)); + + const double determinant_difference = + (plus_context.mapping.mapping_determinant - + minus_context.mapping.mapping_determinant) / + (2.0 * kelvin_difference_step); + const double weight_difference = + (plus_context.quadrature.weight - minus_context.quadrature.weight) / + (2.0 * kelvin_difference_step); + + check_centered_difference(variation.mapping.mapping_determinant_variation, + determinant_difference, + plus_context.mapping.mapping_determinant, + minus_context.mapping.mapping_determinant, + kelvin_difference_step, relative_tolerance); + check_centered_difference(variation.weight_variation, weight_difference, + plus_context.quadrature.weight, + minus_context.quadrature.weight, + kelvin_difference_step, relative_tolerance); + } + + for (int boundary_element = 0; boundary_element < mesh.GetNBE(); + ++boundary_element) { + mfem::FaceElementTransformations *face_transformation = + mesh.GetBdrFaceTransformations(boundary_element); + REQUIRE(face_transformation != nullptr); + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(face_transformation->GetGeometryType(), 4); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + + mapping::FaceMappingContext base_context; + mapping::FaceMappingContext plus_context; + mapping::FaceMappingContext minus_context; + mapping::FaceMappingVariation variation; + + REQUIRE(mapper.EvaluateFace(base_data.Get(), *face_transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, base_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFaceVariation( + base_data.Get(), direction, *face_transformation, + mapping::FaceElementSide::element_1, integration_point, + base_context, workspace, + variation) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFace(plus_data.Get(), *face_transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, plus_context) == + mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFace( + minus_data.Get(), *face_transformation, + mapping::FaceElementSide::element_1, integration_point, + workspace, minus_context) == mapping::MappingStatus::valid); + + CAPTURE(boundary_element, q); + + const mfem::Vector normal_difference = vector_central_difference( + plus_context.quadrature.normal, minus_context.quadrature.normal); + + REQUIRE(normal_difference.Size() == + variation.physical_normal_variation.Size()); + for (int i = 0; i < normal_difference.Size(); ++i) { + check_centered_difference(variation.physical_normal_variation(i), + normal_difference(i), + plus_context.quadrature.normal(i), + minus_context.quadrature.normal(i), + kelvin_difference_step, relative_tolerance); + } + + const double surface_weight_difference = + (plus_context.physical_surface_weight - + minus_context.physical_surface_weight) / + (2.0 * kelvin_difference_step); + const double normal_scale_difference = + (plus_context.quadrature.v_dot_n_scale - + minus_context.quadrature.v_dot_n_scale) / + (2.0 * kelvin_difference_step); + + check_centered_difference(variation.physical_surface_weight_variation, + surface_weight_difference, + plus_context.physical_surface_weight, + minus_context.physical_surface_weight, + kelvin_difference_step, relative_tolerance); + check_centered_difference(variation.normal_flux_scale_variation, + normal_scale_difference, + plus_context.quadrature.v_dot_n_scale, + minus_context.quadrature.v_dot_n_scale, + kelvin_difference_step, relative_tolerance); } + } } TEST_CASE( "Stateless Domain Mapper Produces Consistent Two Sided Interface Geometry", - tags::integration &tags::mapping -) { - constexpr double r_star = 1.0; - constexpr double r_infinity = 4.0; - constexpr double interface_tolerance = 2.0e-7; + tags::integration &tags::mapping) { + constexpr double r_star = 1.0; + constexpr double r_infinity = 4.0; + constexpr double interface_tolerance = 2.0e-7; - stroid::config::MeshConfig mesh_config; - mesh_config.refinement_levels = 0; - mesh_config.order = 2; - mesh_config.include_external_domain = true; - mesh_config.r_core = 0.25; - mesh_config.r_star = r_star; - mesh_config.r_infinity = r_infinity; - mesh_config.flattening = 0.0; - mesh_config.optimization_methods = stroid::config::OptimizationMethods{false, true}; + stroid::config::MeshConfig mesh_config; + mesh_config.refinement_levels = 0; + mesh_config.order = 2; + mesh_config.include_external_domain = true; + mesh_config.r_core = 0.25; + mesh_config.r_star = r_star; + mesh_config.r_infinity = r_infinity; + mesh_config.flattening = 0.0; + mesh_config.optimization_methods = + stroid::config::OptimizationMethods{false, true}; - stroid::StroidMesh stroid_mesh = stroid::GenerateMesh(mesh_config); - mfem::Mesh &mesh = *stroid_mesh.mesh; - REQUIRE(stroid_mesh.exterior_coordinate != nullptr); - REQUIRE(stroid_mesh.exterior_coordinate->space != nullptr); - REQUIRE(stroid_mesh.exterior_coordinate->values != nullptr); + stroid::StroidMesh stroid_mesh = stroid::GenerateMesh(mesh_config); + mfem::Mesh &mesh = *stroid_mesh.mesh; + REQUIRE(stroid_mesh.exterior_coordinate != nullptr); + REQUIRE(stroid_mesh.exterior_coordinate->space != nullptr); + REQUIRE(stroid_mesh.exterior_coordinate->values != nullptr); - mfem::FiniteElementSpace &compactification_space = *stroid_mesh.exterior_coordinate->space; - mfem::GridFunction &compactification_coordinate = *stroid_mesh.exterior_coordinate->values; + mfem::FiniteElementSpace &compactification_space = + *stroid_mesh.exterior_coordinate->space; + mfem::GridFunction &compactification_coordinate = + *stroid_mesh.exterior_coordinate->values; - mfem::H1_FECollection displacement_collection(2, dimension); - mfem::FiniteElementSpace displacement_space(&mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); + mfem::H1_FECollection displacement_collection(2, dimension); + mfem::FiniteElementSpace displacement_space( + &mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); - auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.08 * position(0) + 0.02 * position(1); - value(1) = -0.03 * position(0) - 0.02 * position(1); - value(2) = 0.04 * position(2); - }; + auto displacement_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = 0.08 * position(0) + 0.02 * position(1); + value(1) = -0.03 * position(0) - 0.02 * position(1); + value(2) = 0.04 * position(2); + }; - auto direction_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.02 * position(0); - value(1) = -0.01 * position(1) + 0.005 * position(2); - value(2) = 0.015 * position(2); - }; + auto direction_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = 0.02 * position(0); + value(1) = -0.01 * position(1) + 0.005 * position(2); + value(2) = 0.015 * position(2); + }; - mfem::VectorFunctionCoefficient displacement_coefficient(dimension, displacement_function); - mfem::VectorFunctionCoefficient direction_coefficient(dimension, direction_function); + mfem::VectorFunctionCoefficient displacement_coefficient( + dimension, displacement_function); + mfem::VectorFunctionCoefficient direction_coefficient(dimension, + direction_function); + + mfem::GridFunction displacement(&displacement_space); + mfem::GridFunction direction(&displacement_space); + displacement.ProjectCoefficient(displacement_coefficient); + direction.ProjectCoefficient(direction_coefficient); + + std::unique_ptr + exterior_map = + std::make_unique( + mapping::compactification::options::KelvinCompactificationOptions{ + .r_star_ref = r_star, .r_inf_ref = r_infinity}); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + std::move(exterior_map)); + mapping::DomainMapper::Workspace workspace(dimension); + + int core_envelope_faces = 0; + int stellar_vacuum_faces = 0; + + for (int face_id = 0; face_id < mesh.GetNumFaces(); ++face_id) { + mfem::FaceElementTransformations *transformation = + mesh.GetFaceElementTransformations(face_id); + if (transformation == nullptr || transformation->Elem1 == nullptr || + transformation->Elem2 == nullptr) + continue; + + const int attribute_1 = transformation->Elem1->Attribute; + const int attribute_2 = transformation->Elem2->Attribute; + const bool core_envelope_interface = + (attribute_1 == 1 && attribute_2 == 2) || + (attribute_1 == 2 && attribute_2 == 1); + const bool stellar_vacuum_interface = + (attribute_1 == 3) != (attribute_2 == 3); + + if (!core_envelope_interface && !stellar_vacuum_interface) + continue; + + if (core_envelope_interface) + ++core_envelope_faces; + if (stellar_vacuum_interface) + ++stellar_vacuum_faces; + + const int element_1 = transformation->Elem1->ElementNo; + const int element_2 = transformation->Elem2->ElementNo; + + mfem::Array vdofs_1; + mfem::Array vdofs_2; + mfem::Vector displacement_dofs_1; + mfem::Vector displacement_dofs_2; + mfem::Vector direction_dofs_1; + mfem::Vector direction_dofs_2; + mfem::Array compactification_dof_indices_1; + mfem::Array compactification_dof_indices_2; + mfem::Vector compactification_dofs_1; + mfem::Vector compactification_dofs_2; + + displacement_space.GetElementVDofs(element_1, vdofs_1); + displacement_space.GetElementVDofs(element_2, vdofs_2); + displacement.GetSubVector(vdofs_1, displacement_dofs_1); + displacement.GetSubVector(vdofs_2, displacement_dofs_2); + direction.GetSubVector(vdofs_1, direction_dofs_1); + direction.GetSubVector(vdofs_2, direction_dofs_2); + compactification_space.GetElementDofs(element_1, + compactification_dof_indices_1); + compactification_space.GetElementDofs(element_2, + compactification_dof_indices_2); + compactification_coordinate.GetSubVector(compactification_dof_indices_1, + compactification_dofs_1); + compactification_coordinate.GetSubVector(compactification_dof_indices_2, + compactification_dofs_2); + + const mapping::ElementDisplacementData displacement_1 = + mapping::ElementDisplacementDataFromElementVDofs( + *displacement_space.GetFE(element_1), displacement_dofs_1); + const mapping::ElementDisplacementData displacement_2 = + mapping::ElementDisplacementDataFromElementVDofs( + *displacement_space.GetFE(element_2), displacement_dofs_2); + const mapping::ElementDisplacementData direction_1 = + mapping::ElementDisplacementDataFromElementVDofs( + *displacement_space.GetFE(element_1), direction_dofs_1); + const mapping::ElementDisplacementData direction_2 = + mapping::ElementDisplacementDataFromElementVDofs( + *displacement_space.GetFE(element_2), direction_dofs_2); + + const ElementMappingDataOwner element_data_1( + displacement_1, *compactification_space.GetFE(element_1), + compactification_dofs_1); + const ElementMappingDataOwner element_data_2( + displacement_2, *compactification_space.GetFE(element_2), + compactification_dofs_2); + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 6); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + + mapping::FaceMappingContext context_1; + mapping::FaceMappingContext context_2; + mapping::FaceMappingVariation variation_1; + mapping::FaceMappingVariation variation_2; + + auto status_1 = + mapper.EvaluateFace(element_data_1.Get(), *transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, context_1); + auto status_2 = + mapper.EvaluateFace(element_data_2.Get(), *transformation, + mapping::FaceElementSide::element_2, + integration_point, workspace, context_2); + INFO("Element 1 ID = " << transformation->Elem1No); + INFO("Element 2 ID = " << transformation->Elem2No); + INFO("Element 1 attribute = " << transformation->Elem1->Attribute); + INFO("Element 2 attribute = " << transformation->Elem2->Attribute); + INFO("Element 2 status = " << static_cast(status_2)); + + mfem::Vector reference_position(dimension); + transformation->Elem2->Transform(transformation->Elem2->GetIntPoint(), + reference_position); + INFO(std::format("Reference position = <{},{},{}>", reference_position(0), + reference_position(1), reference_position(2))); + INFO(std::format("Reference radius = {}", reference_position.Norml2())); + INFO(std::format("Reference radius minus r_star = {}", + reference_position.Norml2() - r_star)); + REQUIRE(status_1 == mapping::MappingStatus::valid); + REQUIRE(status_2 == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFaceVariation( + element_data_1.Get(), direction_1, *transformation, + mapping::FaceElementSide::element_1, integration_point, + context_1, workspace, + variation_1) == mapping::MappingStatus::valid); + REQUIRE(mapper.EvaluateFaceVariation( + element_data_2.Get(), direction_2, *transformation, + mapping::FaceElementSide::element_2, integration_point, + context_2, workspace, + variation_2) == mapping::MappingStatus::valid); + + CAPTURE(face_id, q, attribute_1, attribute_2); + check_vector(context_1.mapping.physical_position, + context_2.mapping.physical_position, interface_tolerance); + check_scalar_relative(context_1.physical_surface_weight, + context_2.physical_surface_weight, + interface_tolerance); + + mfem::Vector normal_sum(context_1.quadrature.normal); + normal_sum += context_2.quadrature.normal; + CHECK(normal_sum.Norml2() < interface_tolerance); + + check_vector(variation_1.mapping.physical_position_variation, + variation_2.mapping.physical_position_variation, + interface_tolerance); + check_scalar_relative(variation_1.physical_surface_weight_variation, + variation_2.physical_surface_weight_variation, + interface_tolerance); + + mfem::Vector normal_variation_sum(variation_1.physical_normal_variation); + normal_variation_sum += variation_2.physical_normal_variation; + CHECK(normal_variation_sum.Norml2() < interface_tolerance); + + const mfem::Vector physical_flux = make_vector(0.7, -0.4, 0.9); + mfem::Vector reference_flux_1; + mfem::Vector reference_flux_2; + + mapping::MapPhysicalFluxToHDivReference(context_1.mapping, physical_flux, + reference_flux_1); + mapping::MapPhysicalFluxToHDivReference(context_2.mapping, physical_flux, + reference_flux_2); + + const double flux_1 = (reference_flux_1 * context_1.reference_normal) * + context_1.reference_surface_weight; + const double flux_2 = (reference_flux_2 * context_2.reference_normal) * + context_2.reference_surface_weight; + + CHECK_THAT(flux_1 + flux_2, WithinAbs(0.0, interface_tolerance)); + } + } + + INFO("Core-envelope interface faces = " << core_envelope_faces); + INFO("Stellar-vacuum interface faces = " << stellar_vacuum_faces); + + REQUIRE(core_envelope_faces > 0); + REQUIRE(stellar_vacuum_faces > 0); +} + +TEST_CASE("Mapped Hdiv Field Satisfies The Divergence Theorem", + tags::unit &tags::mapping) { + constexpr double divergence_tolerance = 2.0e-9; + + QuadraticElementFixture fixture; + + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *element_transformation = + fixture.mesh.GetElementTransformation(0); + + const mfem::Vector displacement_dofs = make_function_element_dofs( + element, *element_transformation, evaluate_quadratic_displacement, + mfem::Ordering::byVDIM); + const mapping::ElementDisplacementData displacement( + element, displacement_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::DomainMapper::Workspace workspace(dimension); + + double reference_divergence_integral = 0.0; + double physical_divergence_integral = 0.0; + double physical_boundary_flux = 0.0; + + const mfem::IntegrationRule &volume_rule = + mfem::IntRules.Get(element_transformation->GetGeometryType(), 10); + + for (int q = 0; q < volume_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = volume_rule.IntPoint(q); + mapping::VolumeMappingContext context; + + REQUIRE(mapper.EvaluateVolume(element_data.Get(), *element_transformation, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + element_transformation->SetIntPoint(&integration_point); + + const double reference_divergence = + evaluate_reference_hdiv_divergence(context.mapping.reference_position); + const double physical_divergence = mapping::MapHDivDivergenceToPhysical( + context.mapping, reference_divergence); + const double reference_weight = + integration_point.weight * element_transformation->Weight(); + + reference_divergence_integral += reference_divergence * reference_weight; + physical_divergence_integral += + physical_divergence * context.quadrature.weight; + } + + for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); + ++boundary_element) { + mfem::FaceElementTransformations *transformation = + fixture.mesh.GetBdrFaceTransformations(boundary_element); + REQUIRE(transformation != nullptr); + + const mfem::IntegrationRule &face_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 10); + + for (int q = 0; q < face_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = face_rule.IntPoint(q); + mapping::FaceMappingContext context; + + REQUIRE(mapper.EvaluateFace(element_data.Get(), *transformation, + mapping::FaceElementSide::element_1, + integration_point, workspace, + context) == mapping::MappingStatus::valid); + + const mfem::Vector reference_flux = + evaluate_reference_hdiv_field(context.mapping.reference_position); + mfem::Vector physical_flux; + mapping::MapHDivFluxToPhysical(context.mapping, reference_flux, + physical_flux); + + physical_boundary_flux += (physical_flux * context.quadrature.normal) * + context.physical_surface_weight; + } + } + + constexpr double analytic_reference_integral = 216.0; + + INFO("Analytic reference divergence integral = " + << analytic_reference_integral); + INFO("Computed reference divergence integral = " + << reference_divergence_integral); + INFO("Computed physical divergence integral = " + << physical_divergence_integral); + INFO("Computed physical boundary flux = " << physical_boundary_flux); + + CHECK_THAT(reference_divergence_integral, + WithinAbs(analytic_reference_integral, divergence_tolerance)); + CHECK_THAT(physical_divergence_integral, + WithinAbs(analytic_reference_integral, divergence_tolerance)); + CHECK_THAT(physical_boundary_flux, + WithinAbs(analytic_reference_integral, divergence_tolerance)); +} + +TEST_CASE("Mapped Hcurl Fields Preserve Covariant Piola Identities", + tags::unit &tags::mapping) { + constexpr double curl_tolerance = 2.0e-12; + + mapping::MappingPointContext context; + context.mapping_jacobian.SetSize(3); + context.mapping_jacobian(0, 0) = 1.20; + context.mapping_jacobian(0, 1) = 0.15; + context.mapping_jacobian(0, 2) = -0.05; + context.mapping_jacobian(1, 0) = -0.08; + context.mapping_jacobian(1, 1) = 0.95; + context.mapping_jacobian(1, 2) = 0.12; + context.mapping_jacobian(2, 0) = 0.04; + context.mapping_jacobian(2, 1) = -0.10; + context.mapping_jacobian(2, 2) = 1.10; + + context.mapping_determinant = context.mapping_jacobian.Det(); + REQUIRE(context.mapping_determinant > 0.0); + + context.inverse_mapping_jacobian.SetSize(3); + mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian); + + const mfem::Vector reference_field = make_vector(0.7, -0.4, 1.1); + const mfem::Vector reference_test_field = make_vector(-0.2, 0.9, 0.5); + + mfem::DenseMatrix reference_gradient(3); + reference_gradient(0, 0) = 0.20; + reference_gradient(0, 1) = -0.10; + reference_gradient(0, 2) = 0.04; + reference_gradient(1, 0) = 0.03; + reference_gradient(1, 1) = 0.15; + reference_gradient(1, 2) = -0.08; + reference_gradient(2, 0) = -0.05; + reference_gradient(2, 1) = 0.02; + reference_gradient(2, 2) = 0.11; + + const mfem::Vector reference_curl = matrix_curl(reference_gradient); + + mfem::Vector physical_field; + mfem::Vector recovered_field; + mfem::Vector physical_test_field; + mfem::Vector physical_curl; + mfem::Vector recovered_curl; + + mapping::MapHCurlFieldToPhysical(context, reference_field, physical_field); + mapping::MapPhysicalFieldToHCurlReference(context, physical_field, + recovered_field); + mapping::MapHCurlFieldToPhysical(context, reference_test_field, + physical_test_field); + mapping::MapHCurlCurlToPhysical(context, reference_curl, physical_curl); + mapping::MapPhysicalCurlToHCurlReference(context, physical_curl, + recovered_curl); + + check_vector(recovered_field, reference_field, curl_tolerance); + check_vector(recovered_curl, reference_curl, curl_tolerance); + + mfem::DenseMatrix temporary(3); + mfem::DenseMatrix physical_gradient(3); + mfem::MultAtB(context.inverse_mapping_jacobian, reference_gradient, + temporary); + mfem::Mult(temporary, context.inverse_mapping_jacobian, physical_gradient); + + const mfem::Vector directly_computed_physical_curl = + matrix_curl(physical_gradient); + check_vector(directly_computed_physical_curl, physical_curl, curl_tolerance); + + mfem::DenseMatrix mass_tensor; + mfem::DenseMatrix curl_tensor; + mapping::ComputeHCurlMassTensor(context, mass_tensor); + mapping::ComputeHCurlCurlTensor(context, curl_tensor); + + mfem::Vector mass_action(3); + mass_tensor.Mult(reference_test_field, mass_action); + + const double physical_mass_inner_product = + context.mapping_determinant * (physical_field * physical_test_field); + const double reference_mass_inner_product = reference_field * mass_action; + + CHECK_THAT(physical_mass_inner_product, + WithinAbs(reference_mass_inner_product, curl_tolerance)); + + const mfem::Vector reference_test_curl = make_vector(-0.3, 0.6, 0.2); + mfem::Vector physical_test_curl; + mfem::Vector curl_action(3); + + mapping::MapHCurlCurlToPhysical(context, reference_test_curl, + physical_test_curl); + curl_tensor.Mult(reference_test_curl, curl_action); + + const double physical_curl_inner_product = + context.mapping_determinant * (physical_curl * physical_test_curl); + const double reference_curl_inner_product = reference_curl * curl_action; + + CHECK_THAT(physical_curl_inner_product, + WithinAbs(reference_curl_inner_product, curl_tolerance)); +} + +TEST_CASE("Element Displacement Data Matches MFEM GridFunction Evaluation", + tags::unit &tags::mapping) { + constexpr int displacement_order = 2; + constexpr int quadrature_order = 6; + constexpr double value_tolerance = 5.0e-13; + constexpr double gradient_tolerance = 2.0e-12; + constexpr double mapping_tolerance = 3.0e-12; + + auto check_space_ordering = [](const mfem::Ordering::Type space_ordering) { + mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D( + 2, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 1.5, 1.25); + + mfem::H1_FECollection displacement_collection(displacement_order, + dimension); + + mfem::FiniteElementSpace displacement_space(&mesh, &displacement_collection, + dimension, space_ordering); mfem::GridFunction displacement(&displacement_space); - mfem::GridFunction direction(&displacement_space); - displacement.ProjectCoefficient(displacement_coefficient); - direction.ProjectCoefficient(direction_coefficient); - std::unique_ptr exterior_map = - std::make_unique( - mapping::compactification::options::KelvinCompactificationOptions{ - .r_star_ref = r_star, .r_inf_ref = r_infinity - } - ); + auto displacement_function = [](const mfem::Vector &position, + mfem::Vector &value) { + const double x = position(0); + const double y = position(1); + const double z = position(2); - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, std::move(exterior_map) - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); + value.SetSize(dimension); - int core_envelope_faces = 0; - int stellar_vacuum_faces = 0; + value(0) = 0.17 + 0.11 * x - 0.07 * y + 0.03 * y * z; - for (int face_id = 0; face_id < mesh.GetNumFaces(); ++face_id) { - mfem::FaceElementTransformations *transformation = mesh.GetFaceElementTransformations(face_id); - if (transformation == nullptr || transformation->Elem1 == nullptr || transformation->Elem2 == nullptr) - continue; + value(1) = -0.23 + 0.05 * y + 0.09 * z + 0.02 * x * z; - const int attribute_1 = transformation->Elem1->Attribute; - const int attribute_2 = transformation->Elem2->Attribute; - const bool core_envelope_interface = - (attribute_1 == 1 && attribute_2 == 2) || (attribute_1 == 2 && attribute_2 == 1); - const bool stellar_vacuum_interface = (attribute_1 == 3) != (attribute_2 == 3); - - if (!core_envelope_interface && !stellar_vacuum_interface) - continue; - - if (core_envelope_interface) - ++core_envelope_faces; - if (stellar_vacuum_interface) - ++stellar_vacuum_faces; - - const int element_1 = transformation->Elem1->ElementNo; - const int element_2 = transformation->Elem2->ElementNo; - - mfem::Array vdofs_1; - mfem::Array vdofs_2; - mfem::Vector displacement_dofs_1; - mfem::Vector displacement_dofs_2; - mfem::Vector direction_dofs_1; - mfem::Vector direction_dofs_2; - mfem::Array compactification_dof_indices_1; - mfem::Array compactification_dof_indices_2; - mfem::Vector compactification_dofs_1; - mfem::Vector compactification_dofs_2; - - displacement_space.GetElementVDofs(element_1, vdofs_1); - displacement_space.GetElementVDofs(element_2, vdofs_2); - displacement.GetSubVector(vdofs_1, displacement_dofs_1); - displacement.GetSubVector(vdofs_2, displacement_dofs_2); - direction.GetSubVector(vdofs_1, direction_dofs_1); - direction.GetSubVector(vdofs_2, direction_dofs_2); - compactification_space.GetElementDofs(element_1, compactification_dof_indices_1); - compactification_space.GetElementDofs(element_2, compactification_dof_indices_2); - compactification_coordinate.GetSubVector(compactification_dof_indices_1, compactification_dofs_1); - compactification_coordinate.GetSubVector(compactification_dof_indices_2, compactification_dofs_2); - - const mapping::ElementDisplacementData displacement_1 = - mapping::ElementDisplacementDataFromElementVDofs(*displacement_space.GetFE(element_1), displacement_dofs_1); - const mapping::ElementDisplacementData displacement_2 = - mapping::ElementDisplacementDataFromElementVDofs(*displacement_space.GetFE(element_2), displacement_dofs_2); - const mapping::ElementDisplacementData direction_1 = - mapping::ElementDisplacementDataFromElementVDofs(*displacement_space.GetFE(element_1), direction_dofs_1); - const mapping::ElementDisplacementData direction_2 = - mapping::ElementDisplacementDataFromElementVDofs(*displacement_space.GetFE(element_2), direction_dofs_2); - - const ElementMappingDataOwner element_data_1( - displacement_1, *compactification_space.GetFE(element_1), compactification_dofs_1 - ); - const ElementMappingDataOwner element_data_2( - displacement_2, *compactification_space.GetFE(element_2), compactification_dofs_2 - ); - - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - - mapping::FaceMappingContext context_1; - mapping::FaceMappingContext context_2; - mapping::FaceMappingVariation variation_1; - mapping::FaceMappingVariation variation_2; - - auto status_1 = mapper.EvaluateFace( - element_data_1.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, context_1 - ); - auto status_2 = mapper.EvaluateFace( - element_data_2.Get(), *transformation, mapping::FaceElementSide::element_2, integration_point, - workspace, context_2 - ); - INFO("Element 1 ID = " << transformation->Elem1No); - INFO("Element 2 ID = " << transformation->Elem2No); - INFO("Element 1 attribute = " << transformation->Elem1->Attribute); - INFO("Element 2 attribute = " << transformation->Elem2->Attribute); - INFO("Element 2 status = " << static_cast(status_2)); - - mfem::Vector reference_position(dimension); - transformation->Elem2->Transform(transformation->Elem2->GetIntPoint(), reference_position); - INFO( - std::format( - "Reference position = <{},{},{}>", reference_position(0), reference_position(1), - reference_position(2) - ) - ); - INFO(std::format("Reference radius = {}", reference_position.Norml2())); - INFO(std::format("Reference radius minus r_star = {}", reference_position.Norml2() - r_star)); - REQUIRE(status_1 == mapping::MappingStatus::valid); - REQUIRE(status_2 == mapping::MappingStatus::valid); - REQUIRE( - mapper.EvaluateFaceVariation( - element_data_1.Get(), direction_1, *transformation, mapping::FaceElementSide::element_1, - integration_point, context_1, workspace, variation_1 - ) == mapping::MappingStatus::valid - ); - REQUIRE( - mapper.EvaluateFaceVariation( - element_data_2.Get(), direction_2, *transformation, mapping::FaceElementSide::element_2, - integration_point, context_2, workspace, variation_2 - ) == mapping::MappingStatus::valid - ); - - CAPTURE(face_id, q, attribute_1, attribute_2); - check_vector(context_1.mapping.physical_position, context_2.mapping.physical_position, interface_tolerance); - check_scalar_relative( - context_1.physical_surface_weight, context_2.physical_surface_weight, interface_tolerance - ); - - mfem::Vector normal_sum(context_1.quadrature.normal); - normal_sum += context_2.quadrature.normal; - CHECK(normal_sum.Norml2() < interface_tolerance); - - check_vector( - variation_1.mapping.physical_position_variation, variation_2.mapping.physical_position_variation, - interface_tolerance - ); - check_scalar_relative( - variation_1.physical_surface_weight_variation, variation_2.physical_surface_weight_variation, - interface_tolerance - ); - - mfem::Vector normal_variation_sum(variation_1.physical_normal_variation); - normal_variation_sum += variation_2.physical_normal_variation; - CHECK(normal_variation_sum.Norml2() < interface_tolerance); - - const mfem::Vector physical_flux = make_vector(0.7, -0.4, 0.9); - mfem::Vector reference_flux_1; - mfem::Vector reference_flux_2; - - mapping::MapPhysicalFluxToHDivReference(context_1.mapping, physical_flux, reference_flux_1); - mapping::MapPhysicalFluxToHDivReference(context_2.mapping, physical_flux, reference_flux_2); - - const double flux_1 = (reference_flux_1 * context_1.reference_normal) * context_1.reference_surface_weight; - const double flux_2 = (reference_flux_2 * context_2.reference_normal) * context_2.reference_surface_weight; - - CHECK_THAT(flux_1 + flux_2, WithinAbs(0.0, interface_tolerance)); - } - } - - INFO("Core-envelope interface faces = " << core_envelope_faces); - INFO("Stellar-vacuum interface faces = " << stellar_vacuum_faces); - - REQUIRE(core_envelope_faces > 0); - REQUIRE(stellar_vacuum_faces > 0); -} - -TEST_CASE( - "Mapped Hdiv Field Satisfies The Divergence Theorem", - tags::unit &tags::mapping -) { - constexpr double divergence_tolerance = 2.0e-9; - - QuadraticElementFixture fixture; - - const mfem::FiniteElement &element = fixture.GetElement(); - mfem::ElementTransformation *element_transformation = fixture.mesh.GetElementTransformation(0); - - const mfem::Vector displacement_dofs = make_function_element_dofs( - element, *element_transformation, evaluate_quadratic_displacement, mfem::Ordering::byVDIM - ); - const mapping::ElementDisplacementData displacement(element, displacement_dofs, mfem::Ordering::byVDIM); - const ElementMappingDataOwner element_data(displacement); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - mapping::DomainMapperStateless::Workspace workspace(dimension); - - double reference_divergence_integral = 0.0; - double physical_divergence_integral = 0.0; - double physical_boundary_flux = 0.0; - - const mfem::IntegrationRule &volume_rule = mfem::IntRules.Get(element_transformation->GetGeometryType(), 10); - - for (int q = 0; q < volume_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = volume_rule.IntPoint(q); - mapping::VolumeMappingContext context; - - REQUIRE( - mapper.EvaluateVolume(element_data.Get(), *element_transformation, integration_point, workspace, context) == - mapping::MappingStatus::valid - ); - - element_transformation->SetIntPoint(&integration_point); - - const double reference_divergence = evaluate_reference_hdiv_divergence(context.mapping.reference_position); - const double physical_divergence = mapping::MapHDivDivergenceToPhysical(context.mapping, reference_divergence); - const double reference_weight = integration_point.weight * element_transformation->Weight(); - - reference_divergence_integral += reference_divergence * reference_weight; - physical_divergence_integral += physical_divergence * context.quadrature.weight; - } - - for (int boundary_element = 0; boundary_element < fixture.mesh.GetNBE(); ++boundary_element) { - mfem::FaceElementTransformations *transformation = fixture.mesh.GetBdrFaceTransformations(boundary_element); - REQUIRE(transformation != nullptr); - - const mfem::IntegrationRule &face_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 10); - - for (int q = 0; q < face_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = face_rule.IntPoint(q); - mapping::FaceMappingContext context; - - REQUIRE( - mapper.EvaluateFace( - element_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, - workspace, context - ) == mapping::MappingStatus::valid - ); - - const mfem::Vector reference_flux = evaluate_reference_hdiv_field(context.mapping.reference_position); - mfem::Vector physical_flux; - mapping::MapHDivFluxToPhysical(context.mapping, reference_flux, physical_flux); - - physical_boundary_flux += (physical_flux * context.quadrature.normal) * context.physical_surface_weight; - } - } - - constexpr double analytic_reference_integral = 216.0; - - INFO("Analytic reference divergence integral = " << analytic_reference_integral); - INFO("Computed reference divergence integral = " << reference_divergence_integral); - INFO("Computed physical divergence integral = " << physical_divergence_integral); - INFO("Computed physical boundary flux = " << physical_boundary_flux); - - CHECK_THAT(reference_divergence_integral, WithinAbs(analytic_reference_integral, divergence_tolerance)); - CHECK_THAT(physical_divergence_integral, WithinAbs(analytic_reference_integral, divergence_tolerance)); - CHECK_THAT(physical_boundary_flux, WithinAbs(analytic_reference_integral, divergence_tolerance)); -} - -TEST_CASE( - "Mapped Hcurl Fields Preserve Covariant Piola Identities", - tags::unit &tags::mapping -) { - constexpr double curl_tolerance = 2.0e-12; - - mapping::MappingPointContext context; - context.mapping_jacobian.SetSize(3); - context.mapping_jacobian(0, 0) = 1.20; - context.mapping_jacobian(0, 1) = 0.15; - context.mapping_jacobian(0, 2) = -0.05; - context.mapping_jacobian(1, 0) = -0.08; - context.mapping_jacobian(1, 1) = 0.95; - context.mapping_jacobian(1, 2) = 0.12; - context.mapping_jacobian(2, 0) = 0.04; - context.mapping_jacobian(2, 1) = -0.10; - context.mapping_jacobian(2, 2) = 1.10; - - context.mapping_determinant = context.mapping_jacobian.Det(); - REQUIRE(context.mapping_determinant > 0.0); - - context.inverse_mapping_jacobian.SetSize(3); - mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian); - - const mfem::Vector reference_field = make_vector(0.7, -0.4, 1.1); - const mfem::Vector reference_test_field = make_vector(-0.2, 0.9, 0.5); - - mfem::DenseMatrix reference_gradient(3); - reference_gradient(0, 0) = 0.20; - reference_gradient(0, 1) = -0.10; - reference_gradient(0, 2) = 0.04; - reference_gradient(1, 0) = 0.03; - reference_gradient(1, 1) = 0.15; - reference_gradient(1, 2) = -0.08; - reference_gradient(2, 0) = -0.05; - reference_gradient(2, 1) = 0.02; - reference_gradient(2, 2) = 0.11; - - const mfem::Vector reference_curl = matrix_curl(reference_gradient); - - mfem::Vector physical_field; - mfem::Vector recovered_field; - mfem::Vector physical_test_field; - mfem::Vector physical_curl; - mfem::Vector recovered_curl; - - mapping::MapHCurlFieldToPhysical(context, reference_field, physical_field); - mapping::MapPhysicalFieldToHCurlReference(context, physical_field, recovered_field); - mapping::MapHCurlFieldToPhysical(context, reference_test_field, physical_test_field); - mapping::MapHCurlCurlToPhysical(context, reference_curl, physical_curl); - mapping::MapPhysicalCurlToHCurlReference(context, physical_curl, recovered_curl); - - check_vector(recovered_field, reference_field, curl_tolerance); - check_vector(recovered_curl, reference_curl, curl_tolerance); - - mfem::DenseMatrix temporary(3); - mfem::DenseMatrix physical_gradient(3); - mfem::MultAtB(context.inverse_mapping_jacobian, reference_gradient, temporary); - mfem::Mult(temporary, context.inverse_mapping_jacobian, physical_gradient); - - const mfem::Vector directly_computed_physical_curl = matrix_curl(physical_gradient); - check_vector(directly_computed_physical_curl, physical_curl, curl_tolerance); - - mfem::DenseMatrix mass_tensor; - mfem::DenseMatrix curl_tensor; - mapping::ComputeHCurlMassTensor(context, mass_tensor); - mapping::ComputeHCurlCurlTensor(context, curl_tensor); - - mfem::Vector mass_action(3); - mass_tensor.Mult(reference_test_field, mass_action); - - const double physical_mass_inner_product = context.mapping_determinant * (physical_field * physical_test_field); - const double reference_mass_inner_product = reference_field * mass_action; - - CHECK_THAT(physical_mass_inner_product, WithinAbs(reference_mass_inner_product, curl_tolerance)); - - const mfem::Vector reference_test_curl = make_vector(-0.3, 0.6, 0.2); - mfem::Vector physical_test_curl; - mfem::Vector curl_action(3); - - mapping::MapHCurlCurlToPhysical(context, reference_test_curl, physical_test_curl); - curl_tensor.Mult(reference_test_curl, curl_action); - - const double physical_curl_inner_product = context.mapping_determinant * (physical_curl * physical_test_curl); - const double reference_curl_inner_product = reference_curl * curl_action; - - CHECK_THAT(physical_curl_inner_product, WithinAbs(reference_curl_inner_product, curl_tolerance)); -} - -TEST_CASE( - "Element Displacement Data Matches MFEM GridFunction Evaluation", - tags::unit &tags::mapping -) { - constexpr int displacement_order = 2; - constexpr int quadrature_order = 6; - constexpr double value_tolerance = 5.0e-13; - constexpr double gradient_tolerance = 2.0e-12; - constexpr double mapping_tolerance = 3.0e-12; - - auto check_space_ordering = [](const mfem::Ordering::Type space_ordering) { - mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 1.5, 1.25); - - mfem::H1_FECollection displacement_collection(displacement_order, dimension); - - mfem::FiniteElementSpace displacement_space(&mesh, &displacement_collection, dimension, space_ordering); - - mfem::GridFunction displacement(&displacement_space); - - auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { - const double x = position(0); - const double y = position(1); - const double z = position(2); - - value.SetSize(dimension); - - value(0) = 0.17 + 0.11 * x - 0.07 * y + 0.03 * y * z; - - value(1) = -0.23 + 0.05 * y + 0.09 * z + 0.02 * x * z; - - value(2) = 0.31 - 0.04 * x + 0.08 * z - 0.015 * x * y; - }; - - mfem::VectorFunctionCoefficient displacement_coefficient(dimension, displacement_function); - - displacement.ProjectCoefficient(displacement_coefficient); - - mapping::DomainMapperStateless mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() - ); - - mapping::DomainMapperStateless::Workspace workspace(dimension); - - REQUIRE(displacement_space.GetOrdering() == space_ordering); - REQUIRE(displacement.VectorDim() == dimension); - - for (int element_id = 0; element_id < mesh.GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = mesh.GetElementTransformation(element_id); - - REQUIRE(transformation != nullptr); - - const mfem::FiniteElement &displacement_element = *displacement_space.GetFE(element_id); - - mfem::Array element_vdofs; - - mfem::DofTransformation *dof_transformation = displacement_space.GetElementVDofs(element_id, element_vdofs); - - mfem::Vector element_displacement; - - displacement.GetSubVector(element_vdofs, element_displacement); - - if (dof_transformation != nullptr) { - dof_transformation->InvTransformPrimal(element_displacement); - } - - const mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); - - REQUIRE(displacement_data.GetOrdering() == mfem::Ordering::byNODES); - - const ElementMappingDataOwner element_data(displacement_data); - - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - - mfem::Vector shape(displacement_element.GetDof()); - - mfem::DenseMatrix physical_dshape(displacement_element.GetDof(), dimension); - - mfem::Vector computed_value(dimension); - mfem::Vector expected_value(dimension); - - mfem::DenseMatrix computed_gradient(dimension, dimension); - - mfem::DenseMatrix expected_gradient(dimension, dimension); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - - CAPTURE(static_cast(space_ordering), element_id, q); - - transformation->SetIntPoint(&integration_point); - - displacement_element.CalcShape(integration_point, shape); - - displacement_element.CalcPhysDShape(*transformation, physical_dshape); - - displacement_data.GetDofMatrix().MultTranspose(shape, computed_value); - - mfem::MultAtB(displacement_data.GetDofMatrix(), physical_dshape, computed_gradient); - - /* - * Use MFEM's native evaluation as the authoritative - * interpretation of the GridFunction. - */ - transformation->SetIntPoint(&integration_point); - - displacement.GetVectorValue(*transformation, integration_point, expected_value); - - transformation->SetIntPoint(&integration_point); - - displacement.GetVectorGradient(*transformation, expected_gradient); - - check_vector(computed_value, expected_value, value_tolerance); - - check_matrix(computed_gradient, expected_gradient, gradient_tolerance); - - /* - * Also exercise the complete stateless-mapper path. - * The Cartesian elements are stellar-domain elements, - * so the mapper should produce x + u and I + grad(u). - */ - mapping::MappingPointContext context; - - const mapping::MappingStatus status = - mapper.EvaluatePoint(element_data.Get(), *transformation, integration_point, workspace, context); - - REQUIRE(status == mean_field::mapping::MappingStatus::valid); - - REQUIRE_FALSE(context.compactified); - - mfem::Vector expected_displaced_position(context.reference_position); - - expected_displaced_position += expected_value; - - mfem::DenseMatrix expected_displacement_jacobian(expected_gradient); - - for (int d = 0; d < dimension; ++d) { - expected_displacement_jacobian(d, d) += 1.0; - } - - check_vector(context.displaced_position, expected_displaced_position, mapping_tolerance); - - check_vector(context.physical_position, expected_displaced_position, mapping_tolerance); - - check_matrix(context.displacement_jacobian, expected_displacement_jacobian, mapping_tolerance); - - check_matrix(context.mapping_jacobian, expected_displacement_jacobian, mapping_tolerance); - - const double expected_determinant = expected_displacement_jacobian.Det(); - - CHECK_THAT( - context.mapping_determinant, Catch::Matchers::WithinAbs(expected_determinant, mapping_tolerance) - ); - - REQUIRE(context.mapping_determinant > 0.0); - } - } + value(2) = 0.31 - 0.04 * x + 0.08 * z - 0.015 * x * y; }; - SECTION("Global finite-element-space ordering is byNODES") { - check_space_ordering(mfem::Ordering::byNODES); - } + mfem::VectorFunctionCoefficient displacement_coefficient( + dimension, displacement_function); - SECTION("Global finite-element-space ordering is byVDIM") { - check_space_ordering(mfem::Ordering::byVDIM); + displacement.ProjectCoefficient(displacement_coefficient); + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + + mapping::DomainMapper::Workspace workspace(dimension); + + REQUIRE(displacement_space.GetOrdering() == space_ordering); + REQUIRE(displacement.VectorDim() == dimension); + + for (int element_id = 0; element_id < mesh.GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + mesh.GetElementTransformation(element_id); + + REQUIRE(transformation != nullptr); + + const mfem::FiniteElement &displacement_element = + *displacement_space.GetFE(element_id); + + mfem::Array element_vdofs; + + mfem::DofTransformation *dof_transformation = + displacement_space.GetElementVDofs(element_id, element_vdofs); + + mfem::Vector element_displacement; + + displacement.GetSubVector(element_vdofs, element_displacement); + + if (dof_transformation != nullptr) { + dof_transformation->InvTransformPrimal(element_displacement); + } + + const mapping::ElementDisplacementData displacement_data = + mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement); + + REQUIRE(displacement_data.GetOrdering() == mfem::Ordering::byNODES); + + const ElementMappingDataOwner element_data(displacement_data); + + const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get( + transformation->GetGeometryType(), quadrature_order); + + mfem::Vector shape(displacement_element.GetDof()); + + mfem::DenseMatrix physical_dshape(displacement_element.GetDof(), + dimension); + + mfem::Vector computed_value(dimension); + mfem::Vector expected_value(dimension); + + mfem::DenseMatrix computed_gradient(dimension, dimension); + + mfem::DenseMatrix expected_gradient(dimension, dimension); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + + CAPTURE(static_cast(space_ordering), element_id, q); + + transformation->SetIntPoint(&integration_point); + + displacement_element.CalcShape(integration_point, shape); + + displacement_element.CalcPhysDShape(*transformation, physical_dshape); + + displacement_data.GetDofMatrix().MultTranspose(shape, computed_value); + + mfem::MultAtB(displacement_data.GetDofMatrix(), physical_dshape, + computed_gradient); + + /* + * Use MFEM's native evaluation as the authoritative + * interpretation of the GridFunction. + */ + transformation->SetIntPoint(&integration_point); + + displacement.GetVectorValue(*transformation, integration_point, + expected_value); + + transformation->SetIntPoint(&integration_point); + + displacement.GetVectorGradient(*transformation, expected_gradient); + + check_vector(computed_value, expected_value, value_tolerance); + + check_matrix(computed_gradient, expected_gradient, gradient_tolerance); + + /* + * Also exercise the complete stateless-mapper path. + * The Cartesian elements are stellar-domain elements, + * so the mapper should produce x + u and I + grad(u). + */ + mapping::MappingPointContext context; + + const mapping::MappingStatus status = + mapper.EvaluatePoint(element_data.Get(), *transformation, + integration_point, workspace, context); + + REQUIRE(status == mean_field::mapping::MappingStatus::valid); + + REQUIRE_FALSE(context.compactified); + + mfem::Vector expected_displaced_position(context.reference_position); + + expected_displaced_position += expected_value; + + mfem::DenseMatrix expected_displacement_jacobian(expected_gradient); + + for (int d = 0; d < dimension; ++d) { + expected_displacement_jacobian(d, d) += 1.0; + } + + check_vector(context.displaced_position, expected_displaced_position, + mapping_tolerance); + + check_vector(context.physical_position, expected_displaced_position, + mapping_tolerance); + + check_matrix(context.displacement_jacobian, + expected_displacement_jacobian, mapping_tolerance); + + check_matrix(context.mapping_jacobian, expected_displacement_jacobian, + mapping_tolerance); + + const double expected_determinant = + expected_displacement_jacobian.Det(); + + CHECK_THAT(context.mapping_determinant, + Catch::Matchers::WithinAbs(expected_determinant, + mapping_tolerance)); + + REQUIRE(context.mapping_determinant > 0.0); + } } -} \ No newline at end of file + }; + + SECTION("Global finite-element-space ordering is byNODES") { + check_space_ordering(mfem::Ordering::byNODES); + } + + SECTION("Global finite-element-space ordering is byVDIM") { + check_space_ordering(mfem::Ordering::byVDIM); + } +} + +TEST_CASE( + "Grid Function Mapping Evaluator Refreshes A Cached Displacement Element", + tags::mapping_evaluator_unit) { + mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D( + 1, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 3.0, 4.0); + + mfem::H1_FECollection displacement_collection(1, dimension); + mfem::H1_FECollection compactification_collection(1, dimension); + mfem::FiniteElementSpace displacement_space( + &mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); + mfem::FiniteElementSpace compactification_space( + &mesh, &compactification_collection); + + mfem::GridFunction displacement(&displacement_space); + mfem::GridFunction compactification_coordinate(&compactification_space); + displacement = 0.0; + compactification_coordinate = 0.0; + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::GridFunctionMappingEvaluator evaluator( + mapper, displacement, compactification_coordinate); + + /* Refreshing before the first evaluation is a validated no-op. */ + evaluator.Refresh(); + + mfem::ElementTransformation *transformation = + mesh.GetElementTransformation(0); + REQUIRE(transformation != nullptr); + + mfem::IntegrationPoint integration_point; + integration_point.Set3(0.31, 0.43, 0.57); + + mapping::MappingPointContext initial_context; + REQUIRE(evaluator.EvaluatePoint(*transformation, integration_point, + initial_context) == + mapping::MappingStatus::valid); + + mfem::Vector reference_position(dimension); + transformation->Transform(integration_point, reference_position); + check_vector(initial_context.physical_position, reference_position); + + const mfem::Vector displacement_offset = make_vector(0.17, -0.09, 0.045); + mfem::VectorFunctionCoefficient displacement_coefficient( + dimension, + [&displacement_offset](const mfem::Vector &, mfem::Vector &value) { + value = displacement_offset; + }); + displacement.ProjectCoefficient(displacement_coefficient); + + /* + * The same element ID is still cached. Refresh must eagerly rebuild it from + * the mutated grid function rather than retaining the old element data. + */ + evaluator.Refresh(); + + mapping::MappingPointContext refreshed_context; + REQUIRE(evaluator.EvaluatePoint(*transformation, integration_point, + refreshed_context) == + mapping::MappingStatus::valid); + + mfem::Vector expected_position(reference_position); + expected_position += displacement_offset; + + check_vector(refreshed_context.physical_position, expected_position); + check_vector(refreshed_context.displaced_position, expected_position); + + mfem::Vector physical_position; + evaluator.GetPhysicalPoint(*transformation, integration_point, + physical_position); + check_vector(physical_position, expected_position); +} + +TEST_CASE( + "Grid Function Mapping Evaluator Invalidates A Cached Compactification Element", + tags::mapping_evaluator_unit) { + mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D( + 1, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 3.0, 4.0); + mesh.GetElement(0)->SetAttribute(3); + mesh.SetAttributes(); + + mfem::H1_FECollection displacement_collection(1, dimension); + mfem::H1_FECollection compactification_collection(1, dimension); + mfem::FiniteElementSpace displacement_space( + &mesh, &displacement_collection, dimension, mfem::Ordering::byVDIM); + mfem::FiniteElementSpace compactification_space( + &mesh, &compactification_collection); + + mfem::GridFunction displacement(&displacement_space); + mfem::GridFunction compactification_coordinate(&compactification_space); + displacement = 0.0; + compactification_coordinate = 0.20; + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + mapping::GridFunctionMappingEvaluator evaluator( + mapper, displacement, compactification_coordinate); + + mfem::ElementTransformation *transformation = + mesh.GetElementTransformation(0); + REQUIRE(transformation != nullptr); + + mfem::IntegrationPoint integration_point; + integration_point.Set3(0.29, 0.37, 0.61); + + mapping::MappingPointContext initial_context; + REQUIRE(evaluator.EvaluatePoint(*transformation, integration_point, + initial_context) == + mapping::MappingStatus::valid); + REQUIRE(initial_context.compactified); + + compactification_coordinate = 0.40; + + /* Lazy invalidation is idempotent and reloads on the next evaluation. */ + evaluator.InvalidateCache(); + evaluator.InvalidateCache(); + + mapping::MappingPointContext refreshed_context; + REQUIRE(evaluator.EvaluatePoint(*transformation, integration_point, + refreshed_context) == + mapping::MappingStatus::valid); + REQUIRE(refreshed_context.compactified); + + constexpr double coordinate = 0.40; + constexpr double computational_radius = 1.0 + 3.0 * coordinate; + constexpr double expected_scale = + 1.0 / (computational_radius * (1.0 - coordinate)); + + mfem::Vector expected_position(refreshed_context.reference_position); + expected_position *= expected_scale; + + check_vector(refreshed_context.physical_position, expected_position); + CHECK(relative_vector_difference(initial_context.physical_position, + refreshed_context.physical_position) > + 1.0e-3); +} + +TEST_CASE( + "Grid Function Mapping Evaluator Rejects Invalid Fields And Rebinding", + tags::mapping_evaluator_unit) { + mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D( + 1, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 3.0, 4.0); + mfem::Mesh other_mesh = mfem::Mesh::MakeCartesian3D( + 1, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 3.0, 4.0); + + mfem::H1_FECollection collection(1, dimension); + mfem::FiniteElementSpace displacement_space( + &mesh, &collection, dimension, mfem::Ordering::byVDIM); + mfem::FiniteElementSpace alternate_displacement_space( + &mesh, &collection, dimension, mfem::Ordering::byVDIM); + mfem::FiniteElementSpace scalar_space(&mesh, &collection); + mfem::FiniteElementSpace other_scalar_space(&other_mesh, &collection); + mfem::FiniteElementSpace vector_compactification_space( + &mesh, &collection, dimension, mfem::Ordering::byVDIM); + + mfem::GridFunction displacement(&displacement_space); + mfem::GridFunction scalar_displacement(&scalar_space); + mfem::GridFunction compactification_coordinate(&scalar_space); + mfem::GridFunction other_compactification_coordinate(&other_scalar_space); + mfem::GridFunction vector_compactification_coordinate( + &vector_compactification_space); + mfem::GridFunction detached; + + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, + make_kelvin_compactification()); + + CHECK_THROWS_AS( + (mapping::GridFunctionMappingEvaluator( + mapper, detached, compactification_coordinate)), + std::invalid_argument); + CHECK_THROWS_AS( + (mapping::GridFunctionMappingEvaluator(mapper, displacement, detached)), + std::invalid_argument); + CHECK_THROWS_AS( + (mapping::GridFunctionMappingEvaluator( + mapper, scalar_displacement, compactification_coordinate)), + std::invalid_argument); + CHECK_THROWS_AS( + (mapping::GridFunctionMappingEvaluator( + mapper, displacement, vector_compactification_coordinate)), + std::invalid_argument); + CHECK_THROWS_AS( + (mapping::GridFunctionMappingEvaluator( + mapper, displacement, other_compactification_coordinate)), + std::invalid_argument); + + mapping::GridFunctionMappingEvaluator evaluator( + mapper, displacement, compactification_coordinate); + + mfem::ElementTransformation *transformation = + mesh.GetElementTransformation(0); + REQUIRE(transformation != nullptr); + + mfem::IntegrationPoint integration_point; + integration_point.Set3(0.5, 0.5, 0.5); + + mapping::MappingPointContext context; + REQUIRE(evaluator.EvaluatePoint(*transformation, integration_point, + context) == + mapping::MappingStatus::valid); + + displacement.SetSpace(&alternate_displacement_space); + + CHECK_THROWS_AS(evaluator.Refresh(), std::invalid_argument); + CHECK_THROWS_AS( + evaluator.EvaluatePoint(*transformation, integration_point, context), + std::invalid_argument); +} diff --git a/tests/operators/contexts/gravity_field_context.cpp b/tests/operators/contexts/gravity_field_context.cpp index 65f7d87..4908390 100644 --- a/tests/operators/contexts/gravity_field_context.cpp +++ b/tests/operators/contexts/gravity_field_context.cpp @@ -43,11 +43,18 @@ TEST_CASE( CHECK(initial_report.geometry.reconstructed_operators); CHECK(initial_report.geometry.rebuilt_mass_operator); CHECK(initial_report.geometry.rebuilt_source_operator); + CHECK(initial_report.geometry.rebuilt_divergence_operator); CHECK(initial_report.geometry.refreshed_variation_state); CHECK(initial_report.updated_density); CHECK(initial_report.updated_gravity_gradient); CHECK(initial_report.DidAnyWork()); + const auto &geometry_context = context.GetGeometryContext(); + CHECK(geometry_context.GetDivergenceOperator().Width() == f.gravityFluxFes->GetTrueVSize()); + CHECK(geometry_context.GetDivergenceOperator().Height() == f.gravityPotentialFes->GetTrueVSize()); + CHECK(geometry_context.GetTransposeDivergenceOperator().Width() == f.gravityPotentialFes->GetTrueVSize()); + CHECK(geometry_context.GetTransposeDivergenceOperator().Height() == f.gravityFluxFes->GetTrueVSize()); + const auto initial_mass_preparations = context.GetGeometryContext().GetMassOperator().GetPreparationCount(); const auto initial_source_preparations = context.GetGeometryContext().GetSourceOperator().GetPreparationCount(); @@ -93,6 +100,7 @@ TEST_CASE( CHECK_FALSE(displacement_report.geometry.reconstructed_operators); CHECK(displacement_report.geometry.rebuilt_mass_operator); CHECK(displacement_report.geometry.rebuilt_source_operator); + CHECK_FALSE(displacement_report.geometry.rebuilt_divergence_operator); CHECK(displacement_report.geometry.refreshed_variation_state); CHECK_FALSE(displacement_report.updated_density); CHECK_FALSE(displacement_report.updated_gravity_gradient); @@ -107,6 +115,7 @@ TEST_CASE( CHECK(discretization_report.geometry.reconstructed_operators); CHECK(discretization_report.geometry.rebuilt_mass_operator); CHECK(discretization_report.geometry.rebuilt_source_operator); + CHECK(discretization_report.geometry.rebuilt_divergence_operator); CHECK(discretization_report.updated_density); CHECK(discretization_report.updated_gravity_gradient); CHECK(context.GetGeometryContext().GetMassOperator().GetPreparationCount() == 1); diff --git a/tests/operators/gravity_displacement_force.cpp b/tests/operators/gravity_displacement_force.cpp index 5600c90..e350db3 100644 --- a/tests/operators/gravity_displacement_force.cpp +++ b/tests/operators/gravity_displacement_force.cpp @@ -11,729 +11,763 @@ import mean_field; import test_helpers; namespace gravity_displacement_force_test_utils { - using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; +using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; - constexpr auto densityValue = - mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::density_field.mass_term); +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 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 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 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 enthalpyValue = + mean_field::utils::blocks::get_value_block( + mean_field::utils::blocks::enthalpy_field.specific_term); - constexpr auto barotropicConstantValue = mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term - ); +constexpr auto barotropicConstantValue = + 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 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 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 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 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 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 - ); +constexpr auto massResidual = + mean_field::utils::blocks::get_residual_block( + mean_field::utils::blocks::barotropic_constant_field + .mass_normalization_term); - [[nodiscard]] mean_field::operators::GravityDisplacementForceLayout make_layout(const mean_field::fem::FEM &f) { - using DomainSchema = gravity_prepared_test_utils::DomainSchema; +[[nodiscard]] mean_field::operators::GravityDisplacementForceLayout +make_layout(const mean_field::fem::FEM &f) { + using DomainSchema = gravity_prepared_test_utils::DomainSchema; - const auto densityMap = gravity_prepared_test_utils::make_field_map(f); - const auto displacementMap = gravity_prepared_test_utils::make_field_map(f); - const auto gravityFluxMap = - mean_field::field::make_field_dof_map(*f.gravityFluxFes); - const auto gravityPotentialMap = - mean_field::field::make_field_dof_map(*f.gravityPotentialFes); - const auto enthalpyMap = - mean_field::field::make_field_dof_map(*f.enthalpyFes); + const auto densityMap = + gravity_prepared_test_utils::make_field_map( + f); + const auto displacementMap = gravity_prepared_test_utils::make_field_map< + mean_field::field::Displacement>(f); + const auto gravityFluxMap = + mean_field::field::make_field_dof_map(*f.gravityFluxFes); + const auto gravityPotentialMap = mean_field::field::make_field_dof_map< + mean_field::field::Gravity, DomainSchema>(*f.gravityPotentialFes); + const auto enthalpyMap = + mean_field::field::make_field_dof_map(*f.enthalpyFes); - const std::array valueSizes{ - densityMap.reduced_size(), displacementMap.reduced_size(), gravityFluxMap.reduced_size(), - gravityPotentialMap.reduced_size(), enthalpyMap.reduced_size(), 1 - }; + const std::array valueSizes{ + densityMap.reduced_size(), displacementMap.reduced_size(), + gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), + enthalpyMap.reduced_size(), 1}; - const std::array residualSizes{ - gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(), - displacementMap.reduced_size(), enthalpyMap.reduced_size(), 1 - }; + const std::array residualSizes{ + gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), + densityMap.reduced_size(), displacementMap.reduced_size(), + enthalpyMap.reduced_size(), 1}; - return {valueSizes, residualSizes}; + return {valueSizes, residualSizes}; +} + +[[nodiscard]] mfem::Vector make_density(const mean_field::fem::FEM &f, + const double phase) { + mfem::ParGridFunction densityField(f.densityFes.get()); + + mfem::FunctionCoefficient densityCoefficient( + [phase](const mfem::Vector &position) { + return 0.82 + 0.07 * std::sin(0.8 * position(0) + phase) + + 0.05 * std::cos(0.6 * position(1) - 0.3 * phase) + + 0.03 * position(2) * position(2); + }); + + densityField.ProjectCoefficient(densityCoefficient); + + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; +} + +[[nodiscard]] mfem::Vector make_density_direction(const mean_field::fem::FEM &f, + const double phase) { + mfem::ParGridFunction densityField(f.densityFes.get()); + + mfem::FunctionCoefficient densityCoefficient( + [phase](const mfem::Vector &position) { + return 0.19 * std::sin(0.9 * position(0) + phase) - + 0.13 * std::cos(0.7 * position(1) - phase) + 0.08 * position(2); + }); + + densityField.ProjectCoefficient(densityCoefficient); + + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; +} + +[[nodiscard]] mfem::Vector make_gravity_gradient(const mean_field::fem::FEM &f, + const double phase) { + mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); + + auto gravityFunction = [phase](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + + value(0) = 0.31 + 0.08 * position(0) + 0.03 * phase * position(1); + + value(1) = -0.17 + 0.06 * position(1) - 0.02 * phase * position(2); + + value(2) = 0.23 - 0.05 * position(2) + 0.025 * phase * position(0); + }; + + mfem::VectorFunctionCoefficient gravityCoefficient(3, gravityFunction); + + gravityField.ProjectCoefficient(gravityCoefficient); + + mfem::Vector gravityTrue; + gravityField.GetTrueDofs(gravityTrue); + return gravityTrue; +} + +[[nodiscard]] mfem::Vector +make_gravity_gradient_direction(const mean_field::fem::FEM &f, + const double phase) { + mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); + + auto gravityFunction = [phase](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + + value(0) = 0.14 * std::sin(position(0) + phase) + 0.03 * position(1); + + value(1) = -0.11 * std::cos(position(1) - phase) + 0.04 * position(2); + + value(2) = 0.09 * std::sin(position(2) + 0.5 * phase) - 0.02 * position(0); + }; + + mfem::VectorFunctionCoefficient gravityCoefficient(3, gravityFunction); + + gravityField.ProjectCoefficient(gravityCoefficient); + + mfem::Vector gravityTrue; + gravityField.GetTrueDofs(gravityTrue); + return gravityTrue; +} + +[[nodiscard]] mfem::Vector +make_displacement_direction(const mean_field::fem::FEM &f) { + mfem::Vector direction = + gravity_prepared_test_utils::make_displacement(f, 0.83); + + const mfem::Vector second = + gravity_prepared_test_utils::make_displacement(f, 0.29); + + direction -= second; + return direction; +} + +[[nodiscard]] mfem::Vector +make_vacuum_only_density(const mean_field::fem::FEM &f) { + mfem::ParGridFunction densityField(f.densityFes.get()); + densityField = 0.0; + + const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; + + mfem::Array densityDofs; + int localVacuumElements = 0; + + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elementId); + + REQUIRE(transformation != nullptr); + + if (transformation->Attribute != vacuumAttribute) { + continue; } - [[nodiscard]] mfem::Vector make_density( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::ParGridFunction densityField(f.densityFes.get()); + f.densityFes->GetElementDofs(elementId, densityDofs); - mfem::FunctionCoefficient densityCoefficient([phase](const mfem::Vector &position) { - return 0.82 + 0.07 * std::sin(0.8 * position(0) + phase) + - 0.05 * std::cos(0.6 * position(1) - 0.3 * phase) + 0.03 * position(2) * position(2); - }); + mfem::Vector elementDensity(densityDofs.Size()); + elementDensity = 1.0; + densityField.SetSubVector(densityDofs, elementDensity); + ++localVacuumElements; + } - densityField.ProjectCoefficient(densityCoefficient); + int globalVacuumElements = 0; + MPI_Allreduce(&localVacuumElements, &globalVacuumElements, 1, MPI_INT, + MPI_SUM, f.mesh->GetComm()); - mfem::Vector densityTrue; - densityField.GetTrueDofs(densityTrue); - return densityTrue; - } + REQUIRE(globalVacuumElements > 0); - [[nodiscard]] mfem::Vector make_density_direction( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::ParGridFunction densityField(f.densityFes.get()); + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; +} - mfem::FunctionCoefficient densityCoefficient([phase](const mfem::Vector &position) { - return 0.19 * std::sin(0.9 * position(0) + phase) - 0.13 * std::cos(0.7 * position(1) - phase) + - 0.08 * position(2); - }); +[[nodiscard]] mean_field::operators::context::gravity_field:: + GravityFieldRevisions + make_revisions() { + return {.discretization = {.value = 3}, + .displacement = {.value = 5}, + .density = {.value = 7}, + .gravity_gradient = {.value = 11}, + .gravity_potential = {.value = 13}}; +} - densityField.ProjectCoefficient(densityCoefficient); +void prepare_gravity_context( + mean_field::operators::context::gravity_field:: + GravityFieldLinearizationContext &context, + const mfem::Vector &density, const mfem::Vector &displacement, + const mfem::Vector &gravityGradient, const mfem::Vector &gravityPotential, + const mean_field::operators::context::gravity_field::GravityFieldRevisions + &revisions) { + context.Prepare( + {.density = context.GetDensityMap().gather(density), + .displacement = context.GetDisplacementMap().gather(displacement), + .gravity_gradient = + context.GetGravityGradientMap().gather(gravityGradient), + .gravity_potential = + context.GetGravityPotentialMap().gather(gravityPotential)}, + revisions); +} - mfem::Vector densityTrue; - densityField.GetTrueDofs(densityTrue); - return densityTrue; - } +[[nodiscard]] double relative_difference(const mfem::Vector &left, + const mfem::Vector &right, + const MPI_Comm communicator) { + MFEM_VERIFY(left.Size() == right.Size(), + "Cannot compare gravity-displacement-force vectors with " + "different sizes."); - [[nodiscard]] mfem::Vector make_gravity_gradient( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); + mfem::Vector difference(left); + difference -= right; - auto gravityFunction = [phase](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); + const double scale = + std::max({gravity_prepared_test_utils::global_norm(left, communicator), + gravity_prepared_test_utils::global_norm(right, communicator), + 100.0 * std::numeric_limits::epsilon()}); - value(0) = 0.31 + 0.08 * position(0) + 0.03 * phase * position(1); + return gravity_prepared_test_utils::global_norm(difference, communicator) / + scale; +} - value(1) = -0.17 + 0.06 * position(1) - 0.02 * phase * position(2); +[[nodiscard]] mfem::Vector centered_difference( + const mean_field::fem::FEM &f, const mfem::Vector &baseDensity, + const mfem::Vector &densityDirection, + const mfem::Vector &baseGravityGradient, + const mfem::Vector &gravityGradientDirection, + const mfem::Vector &baseDisplacement, + const mfem::Vector &displacementDirection, const double step) { + mfem::Vector plusDensity(baseDensity); + plusDensity.Add(step, densityDirection); - value(2) = 0.23 - 0.05 * position(2) + 0.025 * phase * position(0); - }; + mfem::Vector minusDensity(baseDensity); + minusDensity.Add(-step, densityDirection); - mfem::VectorFunctionCoefficient gravityCoefficient(3, gravityFunction); + mfem::Vector plusGravity(baseGravityGradient); + plusGravity.Add(step, gravityGradientDirection); - gravityField.ProjectCoefficient(gravityCoefficient); + mfem::Vector minusGravity(baseGravityGradient); + minusGravity.Add(-step, gravityGradientDirection); - mfem::Vector gravityTrue; - gravityField.GetTrueDofs(gravityTrue); - return gravityTrue; - } + mfem::Vector plusDisplacement(baseDisplacement); + plusDisplacement.Add(step, displacementDirection); - [[nodiscard]] mfem::Vector make_gravity_gradient_direction( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); + mfem::Vector minusDisplacement(baseDisplacement); + minusDisplacement.Add(-step, displacementDirection); - auto gravityFunction = [phase](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); + mfem::Vector plusResidual; + mfem::Vector minusResidual; - value(0) = 0.14 * std::sin(position(0) + phase) + 0.03 * position(1); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, plusDensity, plusGravity, plusDisplacement, + plusResidual); - value(1) = -0.11 * std::cos(position(1) - phase) + 0.04 * position(2); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, minusDensity, minusGravity, + minusDisplacement, minusResidual); - value(2) = 0.09 * std::sin(position(2) + 0.5 * phase) - 0.02 * position(0); - }; + plusResidual -= minusResidual; + plusResidual /= 2.0 * step; + return plusResidual; +} - mfem::VectorFunctionCoefficient gravityCoefficient(3, gravityFunction); +template +[[nodiscard]] mfem::Vector copy_residual_block( + const mfem::Vector &action, + const mean_field::operators::GravityDisplacementForceLayout &layout, + const mean_field::utils::blocks::residual_block block) { + mfem::Vector result(layout.size(block)); + const int offset = layout.offset(block); - gravityField.ProjectCoefficient(gravityCoefficient); + for (int entry = 0; entry < result.Size(); ++entry) { + result(entry) = action(offset + entry); + } - mfem::Vector gravityTrue; - gravityField.GetTrueDofs(gravityTrue); - return gravityTrue; - } - - [[nodiscard]] mfem::Vector make_displacement_direction(const mean_field::fem::FEM &f) { - mfem::Vector direction = gravity_prepared_test_utils::make_displacement(f, 0.83); - - const mfem::Vector second = gravity_prepared_test_utils::make_displacement(f, 0.29); - - direction -= second; - return direction; - } - - [[nodiscard]] mfem::Vector make_vacuum_only_density(const mean_field::fem::FEM &f) { - mfem::ParGridFunction densityField(f.densityFes.get()); - densityField = 0.0; - - const int vacuumAttribute = f.domainMapperStateless->GetVacuumElementAttribute(); - - mfem::Array densityDofs; - int localVacuumElements = 0; - - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); - - REQUIRE(transformation != nullptr); - - if (transformation->Attribute != vacuumAttribute) { - continue; - } - - f.densityFes->GetElementDofs(elementId, densityDofs); - - mfem::Vector elementDensity(densityDofs.Size()); - elementDensity = 1.0; - densityField.SetSubVector(densityDofs, elementDensity); - ++localVacuumElements; - } - - int globalVacuumElements = 0; - MPI_Allreduce(&localVacuumElements, &globalVacuumElements, 1, MPI_INT, MPI_SUM, f.mesh->GetComm()); - - REQUIRE(globalVacuumElements > 0); - - mfem::Vector densityTrue; - densityField.GetTrueDofs(densityTrue); - return densityTrue; - } - - [[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions make_revisions() { - return { - .discretization = {.value = 3}, - .displacement = {.value = 5}, - .density = {.value = 7}, - .gravity_gradient = {.value = 11}, - .gravity_potential = {.value = 13} - }; - } - - void prepare_gravity_context( - mean_field::operators::context::gravity_field::GravityFieldLinearizationContext &context, - const mfem::Vector &density, - const mfem::Vector &displacement, - const mfem::Vector &gravityGradient, - const mfem::Vector &gravityPotential, - const mean_field::operators::context::gravity_field::GravityFieldRevisions &revisions - ) { - context.Prepare( - {.density = context.GetDensityMap().gather(density), - .displacement = context.GetDisplacementMap().gather(displacement), - .gravity_gradient = context.GetGravityGradientMap().gather(gravityGradient), - .gravity_potential = context.GetGravityPotentialMap().gather(gravityPotential)}, - revisions - ); - } - - [[nodiscard]] double relative_difference( - const mfem::Vector &left, - const mfem::Vector &right, - const MPI_Comm communicator - ) { - MFEM_VERIFY( - left.Size() == right.Size(), "Cannot compare gravity-displacement-force vectors with " - "different sizes." - ); - - mfem::Vector difference(left); - difference -= right; - - const double scale = std::max( - {gravity_prepared_test_utils::global_norm(left, communicator), - gravity_prepared_test_utils::global_norm(right, communicator), - 100.0 * std::numeric_limits::epsilon()} - ); - - return gravity_prepared_test_utils::global_norm(difference, communicator) / scale; - } - - [[nodiscard]] mfem::Vector centered_difference( - const mean_field::fem::FEM &f, - const mfem::Vector &baseDensity, - const mfem::Vector &densityDirection, - const mfem::Vector &baseGravityGradient, - const mfem::Vector &gravityGradientDirection, - const mfem::Vector &baseDisplacement, - const mfem::Vector &displacementDirection, - const double step - ) { - mfem::Vector plusDensity(baseDensity); - plusDensity.Add(step, densityDirection); - - mfem::Vector minusDensity(baseDensity); - minusDensity.Add(-step, densityDirection); - - mfem::Vector plusGravity(baseGravityGradient); - plusGravity.Add(step, gravityGradientDirection); - - mfem::Vector minusGravity(baseGravityGradient); - minusGravity.Add(-step, gravityGradientDirection); - - mfem::Vector plusDisplacement(baseDisplacement); - plusDisplacement.Add(step, displacementDirection); - - mfem::Vector minusDisplacement(baseDisplacement); - minusDisplacement.Add(-step, displacementDirection); - - mfem::Vector plusResidual; - mfem::Vector minusResidual; - - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, plusDensity, plusGravity, plusDisplacement, plusResidual - ); - - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, minusDensity, minusGravity, minusDisplacement, minusResidual - ); - - plusResidual -= minusResidual; - plusResidual /= 2.0 * step; - return plusResidual; - } - - template - [[nodiscard]] mfem::Vector copy_residual_block( - const mfem::Vector &action, - const mean_field::operators::GravityDisplacementForceLayout &layout, - const mean_field::utils::blocks::residual_block block - ) { - mfem::Vector result(layout.size(block)); - const int offset = layout.offset(block); - - for (int entry = 0; entry < result.Size(); ++entry) { - result(entry) = action(offset + entry); - } - - return result; - } + return result; +} } // namespace gravity_displacement_force_test_utils -TEST_CASE( - "Gravity Displacement Force Query Includes Every Registered Operand", - tags::gravity_unit -) { - using DisplacementField = mean_field::field::Field; +TEST_CASE("Gravity Displacement Force Query Includes Every Registered Operand", + tags::gravity_unit) { + using DisplacementField = + mean_field::field::Field; - constexpr int geometryWeightOrder = 4; + constexpr int geometryWeightOrder = 4; - constexpr mean_field::quadrature::Query query = - DisplacementField::make_query( - mean_field::quadrature::QuadratureRole::discretization, geometryWeightOrder, {}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + constexpr mean_field::quadrature::Query query = DisplacementField::make_query< + mean_field::field::Displacement::Form::GravityForce>( + mean_field::quadrature::QuadratureRole::discretization, + geometryWeightOrder, {}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - /* - * rho: 2 - * RT value: family order 2 + 1 = 3 - * geometry displacement gradient: 3 - 1 = 2 - * displacement test value: 3 - * reference-element geometry weight: 4 - */ - constexpr int expectedBaseOrder = 2 + 3 + 2 + 3 + 4; + /* + * rho: 2 + * RT value: family order 2 + 1 = 3 + * geometry displacement gradient: 3 - 1 = 2 + * displacement test value: 3 + * reference-element geometry weight: 4 + */ + constexpr int expectedBaseOrder = 2 + 3 + 2 + 3 + 4; - STATIC_REQUIRE(query.term == mean_field::quadrature::Term::gravity_force); + STATIC_REQUIRE(query.term == mean_field::quadrature::Term::gravity_force); - STATIC_REQUIRE(query.role == mean_field::quadrature::QuadratureRole::discretization); + STATIC_REQUIRE(query.role == + mean_field::quadrature::QuadratureRole::discretization); - STATIC_REQUIRE(query.domain == mean_field::utils::DOMAINS::STELLAR); + STATIC_REQUIRE(query.domain == mean_field::utils::DOMAINS::STELLAR); - STATIC_REQUIRE(query.mapping == mean_field::quadrature::MappingKind::general); + STATIC_REQUIRE(query.mapping == mean_field::quadrature::MappingKind::general); - STATIC_REQUIRE(query.base_order.has_value()); - STATIC_REQUIRE(*query.base_order == expectedBaseOrder); + STATIC_REQUIRE(query.base_order.has_value()); + STATIC_REQUIRE(*query.base_order == expectedBaseOrder); } -TEST_CASE( - "Gravity Displacement Force Uses Positive Grad-Phi Sign And Excludes " - "Vacuum", - tags::gravity_kernel_accuracy -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Gravity Displacement Force Uses Positive Grad-Phi Sign And Excludes " + "Vacuum", + tags::gravity_kernel_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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::ConstantCoefficient densityCoefficient(1.0); - densityField.ProjectCoefficient(densityCoefficient); + mfem::ParGridFunction densityField(f.densityFes.get()); + mfem::ConstantCoefficient densityCoefficient(1.0); + densityField.ProjectCoefficient(densityCoefficient); - mfem::Vector density; - densityField.GetTrueDofs(density); + mfem::Vector density; + densityField.GetTrueDofs(density); - mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); + mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); - auto constantGravityFunction = [](const mfem::Vector &, mfem::Vector &value) { - value.SetSize(3); - value = 0.0; - value(0) = 1.0; - }; + auto constantGravityFunction = [](const mfem::Vector &, mfem::Vector &value) { + value.SetSize(3); + value = 0.0; + value(0) = 1.0; + }; - mfem::VectorFunctionCoefficient gravityCoefficient(3, constantGravityFunction); + mfem::VectorFunctionCoefficient gravityCoefficient(3, + constantGravityFunction); - gravityField.ProjectCoefficient(gravityCoefficient); + gravityField.ProjectCoefficient(gravityCoefficient); - mfem::Vector gravityGradient; - gravityField.GetTrueDofs(gravityGradient); + mfem::Vector gravityGradient; + gravityField.GetTrueDofs(gravityGradient); - mfem::Vector displacement(f.displacementFes->GetTrueVSize()); - displacement = 0.0; + mfem::Vector displacement(f.displacementFes->GetTrueVSize()); + displacement = 0.0; - mfem::Vector residual; + mfem::Vector residual; - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, density, gravityGradient, displacement, residual - ); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, density, gravityGradient, displacement, + residual); - mfem::ParGridFunction testField(f.displacementFes.get()); - testField.ProjectCoefficient(gravityCoefficient); + mfem::ParGridFunction testField(f.displacementFes.get()); + testField.ProjectCoefficient(gravityCoefficient); - mfem::Vector testDirection; - testField.GetTrueDofs(testDirection); + mfem::Vector testDirection; + testField.GetTrueDofs(testDirection); - const double signedWork = gravity_prepared_test_utils::global_dot(residual, testDirection, f.mesh->GetComm()); + const double signedWork = gravity_prepared_test_utils::global_dot( + residual, testDirection, f.mesh->GetComm()); - INFO("Constant +x gravity-force work = " << signedWork); - CHECK(signedWork > 0.0); + INFO("Constant +x gravity-force work = " << signedWork); + CHECK(signedWork > 0.0); - const mfem::Vector vacuumDensity = gravity_displacement_force_test_utils::make_vacuum_only_density(f); + const mfem::Vector vacuumDensity = + gravity_displacement_force_test_utils::make_vacuum_only_density(f); - mfem::Vector vacuumResidual; + mfem::Vector vacuumResidual; - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, vacuumDensity, gravityGradient, displacement, vacuumResidual - ); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, vacuumDensity, gravityGradient, displacement, + vacuumResidual); - CHECK(gravity_prepared_test_utils::global_norm(vacuumResidual, f.mesh->GetComm()) == 0.0); + CHECK(gravity_prepared_test_utils::global_norm(vacuumResidual, + f.mesh->GetComm()) == 0.0); } -TEST_CASE( - "Prepared Gravity Displacement Force Reuses Shared Gravity Revisions", - tags::gravity_prepared -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Prepared Gravity Displacement Force Reuses Shared Gravity Revisions", + tags::gravity_prepared) { + mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - mfem::Vector density = gravity_displacement_force_test_utils::make_density(f, 0.31); + mfem::Vector density = + gravity_displacement_force_test_utils::make_density(f, 0.31); - const mfem::Vector gravityGradient = gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.47); + const mfem::Vector gravityGradient = + gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.47); - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 0.61); + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, 0.61); - mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize()); - gravityPotential = 0.0; + mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize()); + gravityPotential = 0.0; - auto revisions = gravity_displacement_force_test_utils::make_revisions(); + auto revisions = gravity_displacement_force_test_utils::make_revisions(); - mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( - f, *f.domainMapperStateless - ); + mean_field::operators::context::gravity_field:: + GravityFieldLinearizationContext gravityContext(f, + *f.domainMapperStateless); - gravity_displacement_force_test_utils::prepare_gravity_context( - gravityContext, density, displacement, gravityGradient, gravityPotential, revisions - ); + gravity_displacement_force_test_utils::prepare_gravity_context( + gravityContext, density, displacement, gravityGradient, gravityPotential, + revisions); - mean_field::operators::PreparedGravityDisplacementForceOperator preparedOperator( - f, *f.domainMapperStateless, gravityContext - ); + mean_field::operators::PreparedGravityDisplacementForceOperator + preparedOperator(f, *f.domainMapperStateless, gravityContext); - const auto initialReport = preparedOperator.Prepare(); - REQUIRE(initialReport.DidAnyWork()); - REQUIRE(preparedOperator.IsPrepared()); + const auto initialReport = preparedOperator.Prepare(); + REQUIRE(initialReport.DidAnyWork()); + REQUIRE(preparedOperator.IsPrepared()); - mfem::Vector preparedResidual; - mfem::Vector kernelResidual; + mfem::Vector preparedResidual; + mfem::Vector kernelResidual; - preparedOperator.BuildResidual(preparedResidual); + preparedOperator.BuildResidual(preparedResidual); - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, density, gravityGradient, displacement, kernelResidual - ); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, density, gravityGradient, displacement, + kernelResidual); - const mfem::Vector kernelResidualReduced = gravityContext.GetDisplacementMap().gather(kernelResidual); + const mfem::Vector kernelResidualReduced = + gravityContext.GetDisplacementMap().gather(kernelResidual); - CHECK( - gravity_displacement_force_test_utils::relative_difference( - preparedResidual, kernelResidualReduced, f.mesh->GetComm() - ) < 2.0e-12 - ); + CHECK(gravity_displacement_force_test_utils::relative_difference( + preparedResidual, kernelResidualReduced, f.mesh->GetComm()) < + 2.0e-12); - CHECK_FALSE(preparedOperator.Prepare().DidAnyWork()); + CHECK_FALSE(preparedOperator.Prepare().DidAnyWork()); - ++revisions.gravity_potential.value; + ++revisions.gravity_potential.value; - gravity_displacement_force_test_utils::prepare_gravity_context( - gravityContext, density, displacement, gravityGradient, gravityPotential, revisions - ); + gravity_displacement_force_test_utils::prepare_gravity_context( + gravityContext, density, displacement, gravityGradient, gravityPotential, + revisions); - CHECK(preparedOperator.IsPrepared()); - CHECK_FALSE(preparedOperator.Prepare().DidAnyWork()); + CHECK(preparedOperator.IsPrepared()); + CHECK_FALSE(preparedOperator.Prepare().DidAnyWork()); - density = gravity_displacement_force_test_utils::make_density(f, 0.79); - ++revisions.density.value; + density = gravity_displacement_force_test_utils::make_density(f, 0.79); + ++revisions.density.value; - gravity_displacement_force_test_utils::prepare_gravity_context( - gravityContext, density, displacement, gravityGradient, gravityPotential, revisions - ); + gravity_displacement_force_test_utils::prepare_gravity_context( + gravityContext, density, displacement, gravityGradient, gravityPotential, + revisions); - CHECK_FALSE(preparedOperator.IsPrepared()); + CHECK_FALSE(preparedOperator.IsPrepared()); - const auto densityReport = preparedOperator.Prepare(); - CHECK(densityReport.DidAnyWork()); - CHECK(preparedOperator.IsPrepared()); - CHECK(preparedOperator.GetResidualPreparationCount() == 2); - CHECK(preparedOperator.GetResidualApplicationCount() == 1); + const auto densityReport = preparedOperator.Prepare(); + CHECK(densityReport.DidAnyWork()); + CHECK(preparedOperator.IsPrepared()); + CHECK(preparedOperator.GetResidualPreparationCount() == 2); + CHECK(preparedOperator.GetResidualApplicationCount() == 1); } TEST_CASE( "Gravity Displacement Force Jacobian Matches All Columns And Centered " "Differences", - tags::gravity_prepared_jacobian_accuracy -) { - mean_field::utils::Args args = test_utils::setup_args(); + tags::gravity_prepared_jacobian_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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mfem::Vector density = gravity_displacement_force_test_utils::make_density(f, 0.37); + const mfem::Vector density = + gravity_displacement_force_test_utils::make_density(f, 0.37); - const mfem::Vector densityDirection = gravity_displacement_force_test_utils::make_density_direction(f, 0.53); + const mfem::Vector densityDirection = + gravity_displacement_force_test_utils::make_density_direction(f, 0.53); - const mfem::Vector gravityGradient = gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.67); + const mfem::Vector gravityGradient = + gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.67); - const mfem::Vector gravityGradientDirection = - gravity_displacement_force_test_utils::make_gravity_gradient_direction(f, 0.71); + const mfem::Vector gravityGradientDirection = + gravity_displacement_force_test_utils::make_gravity_gradient_direction( + f, 0.71); - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 0.59); + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, 0.59); - const mfem::Vector displacementDirection = gravity_displacement_force_test_utils::make_displacement_direction(f); + const mfem::Vector displacementDirection = + gravity_displacement_force_test_utils::make_displacement_direction(f); - mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize()); - gravityPotential = 0.0; + mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize()); + gravityPotential = 0.0; - mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( - f, *f.domainMapperStateless - ); + mean_field::operators::context::gravity_field:: + GravityFieldLinearizationContext gravityContext(f, + *f.domainMapperStateless); - gravity_displacement_force_test_utils::prepare_gravity_context( - gravityContext, density, displacement, gravityGradient, gravityPotential, - gravity_displacement_force_test_utils::make_revisions() - ); + gravity_displacement_force_test_utils::prepare_gravity_context( + gravityContext, density, displacement, gravityGradient, gravityPotential, + gravity_displacement_force_test_utils::make_revisions()); - mean_field::operators::PreparedGravityDisplacementForceOperator preparedOperator( - f, *f.domainMapperStateless, gravityContext - ); + mean_field::operators::PreparedGravityDisplacementForceOperator + preparedOperator(f, *f.domainMapperStateless, gravityContext); - preparedOperator.Prepare(); + preparedOperator.Prepare(); - const mfem::Vector densityDirectionReduced = gravityContext.GetDensityMap().gather(densityDirection); - const mfem::Vector gravityGradientDirectionReduced = - gravityContext.GetGravityGradientMap().gather(gravityGradientDirection); - const mfem::Vector displacementDirectionReduced = gravityContext.GetDisplacementMap().gather(displacementDirection); + const mfem::Vector densityDirectionReduced = + gravityContext.GetDensityMap().gather(densityDirection); + const mfem::Vector gravityGradientDirectionReduced = + gravityContext.GetGravityGradientMap().gather(gravityGradientDirection); + const mfem::Vector displacementDirectionReduced = + gravityContext.GetDisplacementMap().gather(displacementDirection); - mfem::Vector densityAction; - mfem::Vector gravityAction; - mfem::Vector displacementAction; - mfem::Vector completeAction; + mfem::Vector densityAction; + mfem::Vector gravityAction; + mfem::Vector displacementAction; + mfem::Vector completeAction; - preparedOperator.ApplyDensityJacobianAction(densityDirectionReduced, densityAction); + preparedOperator.ApplyDensityJacobianAction(densityDirectionReduced, + densityAction); - preparedOperator.ApplyGravityGradientJacobianAction(gravityGradientDirectionReduced, gravityAction); + preparedOperator.ApplyGravityGradientJacobianAction( + gravityGradientDirectionReduced, gravityAction); - preparedOperator.ApplyDisplacementJacobianAction(displacementDirectionReduced, displacementAction); + preparedOperator.ApplyDisplacementJacobianAction(displacementDirectionReduced, + displacementAction); - preparedOperator.ApplyCompleteJacobianAction( - densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, completeAction - ); + preparedOperator.ApplyCompleteJacobianAction( + densityDirectionReduced, displacementDirectionReduced, + gravityGradientDirectionReduced, completeAction); - mfem::Vector summedColumns(densityAction); - summedColumns += gravityAction; - summedColumns += displacementAction; + mfem::Vector summedColumns(densityAction); + summedColumns += gravityAction; + summedColumns += displacementAction; - CHECK( - gravity_displacement_force_test_utils::relative_difference(completeAction, summedColumns, f.mesh->GetComm()) < - 2.0e-12 - ); + CHECK(gravity_displacement_force_test_utils::relative_difference( + completeAction, summedColumns, f.mesh->GetComm()) < 2.0e-12); - mfem::Vector zeroDensity(densityDirection.Size()); - mfem::Vector zeroGravity(gravityGradientDirection.Size()); - mfem::Vector zeroDisplacement(displacementDirection.Size()); - zeroDensity = 0.0; - zeroGravity = 0.0; - zeroDisplacement = 0.0; + mfem::Vector zeroDensity(densityDirection.Size()); + mfem::Vector zeroGravity(gravityGradientDirection.Size()); + mfem::Vector zeroDisplacement(displacementDirection.Size()); + zeroDensity = 0.0; + zeroGravity = 0.0; + zeroDisplacement = 0.0; - constexpr double step = 1.0e-5; + constexpr double step = 1.0e-5; - const mfem::Vector densityDifferenceTrue = gravity_displacement_force_test_utils::centered_difference( - f, density, densityDirection, gravityGradient, zeroGravity, displacement, zeroDisplacement, step - ); + const mfem::Vector densityDifferenceTrue = + gravity_displacement_force_test_utils::centered_difference( + f, density, densityDirection, gravityGradient, zeroGravity, + displacement, zeroDisplacement, step); - const mfem::Vector gravityDifferenceTrue = gravity_displacement_force_test_utils::centered_difference( - f, density, zeroDensity, gravityGradient, gravityGradientDirection, displacement, zeroDisplacement, step - ); + const mfem::Vector gravityDifferenceTrue = + gravity_displacement_force_test_utils::centered_difference( + f, density, zeroDensity, gravityGradient, gravityGradientDirection, + displacement, zeroDisplacement, step); - const mfem::Vector displacementDifferenceTrue = gravity_displacement_force_test_utils::centered_difference( - f, density, zeroDensity, gravityGradient, zeroGravity, displacement, displacementDirection, step - ); + const mfem::Vector displacementDifferenceTrue = + gravity_displacement_force_test_utils::centered_difference( + f, density, zeroDensity, gravityGradient, zeroGravity, displacement, + displacementDirection, step); - const mfem::Vector completeDifferenceTrue = gravity_displacement_force_test_utils::centered_difference( - f, density, densityDirection, gravityGradient, gravityGradientDirection, displacement, displacementDirection, - step - ); + const mfem::Vector completeDifferenceTrue = + gravity_displacement_force_test_utils::centered_difference( + f, density, densityDirection, gravityGradient, + gravityGradientDirection, displacement, displacementDirection, step); - const mfem::Vector densityDifference = gravityContext.GetDisplacementMap().gather(densityDifferenceTrue); - const mfem::Vector gravityDifference = gravityContext.GetDisplacementMap().gather(gravityDifferenceTrue); - const mfem::Vector displacementDifference = gravityContext.GetDisplacementMap().gather(displacementDifferenceTrue); - const mfem::Vector completeDifference = gravityContext.GetDisplacementMap().gather(completeDifferenceTrue); + const mfem::Vector densityDifference = + gravityContext.GetDisplacementMap().gather(densityDifferenceTrue); + const mfem::Vector gravityDifference = + gravityContext.GetDisplacementMap().gather(gravityDifferenceTrue); + const mfem::Vector displacementDifference = + gravityContext.GetDisplacementMap().gather(displacementDifferenceTrue); + const mfem::Vector completeDifference = + gravityContext.GetDisplacementMap().gather(completeDifferenceTrue); - const double densityError = - gravity_displacement_force_test_utils::relative_difference(densityAction, densityDifference, f.mesh->GetComm()); + const double densityError = + gravity_displacement_force_test_utils::relative_difference( + densityAction, densityDifference, f.mesh->GetComm()); - const double gravityError = - gravity_displacement_force_test_utils::relative_difference(gravityAction, gravityDifference, f.mesh->GetComm()); + const double gravityError = + gravity_displacement_force_test_utils::relative_difference( + gravityAction, gravityDifference, f.mesh->GetComm()); - const double displacementError = gravity_displacement_force_test_utils::relative_difference( - displacementAction, displacementDifference, f.mesh->GetComm() - ); + const double displacementError = + gravity_displacement_force_test_utils::relative_difference( + displacementAction, displacementDifference, f.mesh->GetComm()); - const double completeError = gravity_displacement_force_test_utils::relative_difference( - completeAction, completeDifference, f.mesh->GetComm() - ); + const double completeError = + gravity_displacement_force_test_utils::relative_difference( + completeAction, completeDifference, f.mesh->GetComm()); - INFO("Density-column centered-difference error = " << densityError); - INFO("Gravity-column centered-difference error = " << gravityError); - INFO("Displacement-column centered-difference error = " << displacementError); - INFO("Complete centered-difference error = " << completeError); + INFO("Density-column centered-difference error = " << densityError); + INFO("Gravity-column centered-difference error = " << gravityError); + INFO("Displacement-column centered-difference error = " << displacementError); + INFO("Complete centered-difference error = " << completeError); - CHECK(densityError < 2.0e-9); - CHECK(gravityError < 2.0e-9); - CHECK(displacementError < 2.0e-8); - CHECK(completeError < 3.0e-8); + CHECK(densityError < 2.0e-9); + CHECK(gravityError < 2.0e-9); + CHECK(displacementError < 2.0e-8); + CHECK(completeError < 3.0e-8); } -TEST_CASE( - "Prepared Gravity Displacement Force MFEM Adapter Routes Only R-d", - tags::gravity_prepared_unit -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Prepared Gravity Displacement Force MFEM Adapter Routes Only R-d", + tags::gravity_prepared_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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mfem::Vector density = gravity_displacement_force_test_utils::make_density(f, 0.41); + const mfem::Vector density = + gravity_displacement_force_test_utils::make_density(f, 0.41); - const mfem::Vector densityDirection = gravity_displacement_force_test_utils::make_density_direction(f, 0.57); + const mfem::Vector densityDirection = + gravity_displacement_force_test_utils::make_density_direction(f, 0.57); - const mfem::Vector gravityGradient = gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.63); + const mfem::Vector gravityGradient = + gravity_displacement_force_test_utils::make_gravity_gradient(f, 0.63); - const mfem::Vector gravityGradientDirection = - gravity_displacement_force_test_utils::make_gravity_gradient_direction(f, 0.77); + const mfem::Vector gravityGradientDirection = + gravity_displacement_force_test_utils::make_gravity_gradient_direction( + f, 0.77); - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 0.51); + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, 0.51); - const mfem::Vector displacementDirection = gravity_displacement_force_test_utils::make_displacement_direction(f); + const mfem::Vector displacementDirection = + gravity_displacement_force_test_utils::make_displacement_direction(f); - mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize()); - gravityPotential = 0.0; + mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize()); + gravityPotential = 0.0; - mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( - f, *f.domainMapperStateless - ); + mean_field::operators::context::gravity_field:: + GravityFieldLinearizationContext gravityContext(f, + *f.domainMapperStateless); - gravity_displacement_force_test_utils::prepare_gravity_context( - gravityContext, density, displacement, gravityGradient, gravityPotential, - gravity_displacement_force_test_utils::make_revisions() - ); + gravity_displacement_force_test_utils::prepare_gravity_context( + gravityContext, density, displacement, gravityGradient, gravityPotential, + gravity_displacement_force_test_utils::make_revisions()); - mean_field::operators::PreparedGravityDisplacementForceOperator preparedOperator( - f, *f.domainMapperStateless, gravityContext - ); + mean_field::operators::PreparedGravityDisplacementForceOperator + preparedOperator(f, *f.domainMapperStateless, gravityContext); - preparedOperator.Prepare(); + preparedOperator.Prepare(); - const mfem::Vector densityDirectionReduced = gravityContext.GetDensityMap().gather(densityDirection); - const mfem::Vector gravityGradientDirectionReduced = - gravityContext.GetGravityGradientMap().gather(gravityGradientDirection); - const mfem::Vector displacementDirectionReduced = gravityContext.GetDisplacementMap().gather(displacementDirection); + const mfem::Vector densityDirectionReduced = + gravityContext.GetDensityMap().gather(densityDirection); + const mfem::Vector gravityGradientDirectionReduced = + gravityContext.GetGravityGradientMap().gather(gravityGradientDirection); + const mfem::Vector displacementDirectionReduced = + gravityContext.GetDisplacementMap().gather(displacementDirection); - const auto layout = gravity_displacement_force_test_utils::make_layout(f); + const auto layout = gravity_displacement_force_test_utils::make_layout(f); - mean_field::operators::PreparedGravityDisplacementForceJacobianOperator adapter(layout, preparedOperator); + mean_field::operators::PreparedGravityDisplacementForceJacobianOperator + adapter(layout, preparedOperator); - mfem::BlockVector direction(layout.value_offsets()); - direction = 0.0; + mfem::BlockVector direction(layout.value_offsets()); + direction = 0.0; - direction.GetBlock(gravity_displacement_force_test_utils::densityValue) = densityDirectionReduced; + direction.GetBlock(gravity_displacement_force_test_utils::densityValue) = + densityDirectionReduced; - direction.GetBlock(gravity_displacement_force_test_utils::displacementValue) = displacementDirectionReduced; + direction.GetBlock(gravity_displacement_force_test_utils::displacementValue) = + displacementDirectionReduced; - direction.GetBlock(gravity_displacement_force_test_utils::gravityGradientValue) = gravityGradientDirectionReduced; + direction.GetBlock( + gravity_displacement_force_test_utils::gravityGradientValue) = + gravityGradientDirectionReduced; - direction.GetBlock(gravity_displacement_force_test_utils::gravityPotentialValue) = 0.29; + direction.GetBlock( + gravity_displacement_force_test_utils::gravityPotentialValue) = 0.29; - direction.GetBlock(gravity_displacement_force_test_utils::enthalpyValue) = -0.37; + direction.GetBlock(gravity_displacement_force_test_utils::enthalpyValue) = + -0.37; - direction.GetBlock(gravity_displacement_force_test_utils::barotropicConstantValue) = 0.43; + direction.GetBlock( + gravity_displacement_force_test_utils::barotropicConstantValue) = 0.43; - mfem::Vector action; - adapter.Mult(direction, action); + mfem::Vector action; + adapter.Mult(direction, action); - mfem::Vector expectedDisplacementAction; + mfem::Vector expectedDisplacementAction; - preparedOperator.ApplyCompleteJacobianAction( - densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, - expectedDisplacementAction - ); + preparedOperator.ApplyCompleteJacobianAction( + densityDirectionReduced, displacementDirectionReduced, + gravityGradientDirectionReduced, expectedDisplacementAction); - const mfem::Vector actualDisplacementAction = gravity_displacement_force_test_utils::copy_residual_block( - action, layout, gravity_displacement_force_test_utils::displacementResidual - ); + const mfem::Vector actualDisplacementAction = + gravity_displacement_force_test_utils::copy_residual_block( + action, layout, + gravity_displacement_force_test_utils::displacementResidual); - CHECK( - gravity_displacement_force_test_utils::relative_difference( - actualDisplacementAction, expectedDisplacementAction, f.mesh->GetComm() - ) < 2.0e-12 - ); + CHECK(gravity_displacement_force_test_utils::relative_difference( + actualDisplacementAction, expectedDisplacementAction, + f.mesh->GetComm()) < 2.0e-12); - const std::array zeroRows{ - gravity_displacement_force_test_utils::copy_residual_block( - action, layout, gravity_displacement_force_test_utils::gravityGradientResidual - ), - gravity_displacement_force_test_utils::copy_residual_block( - action, layout, gravity_displacement_force_test_utils::gravityPotentialResidual - ), - gravity_displacement_force_test_utils::copy_residual_block( - action, layout, gravity_displacement_force_test_utils::densityResidual - ), - gravity_displacement_force_test_utils::copy_residual_block( - action, layout, gravity_displacement_force_test_utils::enthalpyResidual - ), - gravity_displacement_force_test_utils::copy_residual_block( - action, layout, gravity_displacement_force_test_utils::massResidual - ) - }; + const std::array zeroRows{ + gravity_displacement_force_test_utils::copy_residual_block( + action, layout, + gravity_displacement_force_test_utils::gravityGradientResidual), + gravity_displacement_force_test_utils::copy_residual_block( + action, layout, + gravity_displacement_force_test_utils::gravityPotentialResidual), + gravity_displacement_force_test_utils::copy_residual_block( + action, layout, + gravity_displacement_force_test_utils::densityResidual), + gravity_displacement_force_test_utils::copy_residual_block( + action, layout, + gravity_displacement_force_test_utils::enthalpyResidual), + gravity_displacement_force_test_utils::copy_residual_block( + action, layout, gravity_displacement_force_test_utils::massResidual)}; - for (const mfem::Vector &row : zeroRows) { - CHECK(gravity_prepared_test_utils::global_norm(row, f.mesh->GetComm()) == 0.0); - } + for (const mfem::Vector &row : zeroRows) { + CHECK(gravity_prepared_test_utils::global_norm(row, f.mesh->GetComm()) == + 0.0); + } } diff --git a/tests/operators/gravity_displacement_force_analytic_comparisons.cpp b/tests/operators/gravity_displacement_force_analytic_comparisons.cpp index 1727e36..e010d45 100644 --- a/tests/operators/gravity_displacement_force_analytic_comparisons.cpp +++ b/tests/operators/gravity_displacement_force_analytic_comparisons.cpp @@ -11,371 +11,386 @@ import mean_field; import test_helpers; namespace gravity_displacement_force_analytic_test_utils { - struct AffineCase { - const char *name; - std::array scales; - }; +struct AffineCase { + const char *name; + std::array scales; +}; - [[nodiscard]] double analytic_sphere_volume(const double radius) { - return (4.0 / 3.0) * std::numbers::pi * radius * radius * radius; - } +[[nodiscard]] double analytic_sphere_volume(const double radius) { + return (4.0 / 3.0) * std::numbers::pi * radius * radius * radius; +} - [[nodiscard]] double determinant( - const std::array< - double, - 3> &scales - ) { - return scales[0] * scales[1] * scales[2]; - } +[[nodiscard]] double determinant(const std::array &scales) { + return scales[0] * scales[1] * scales[2]; +} - [[nodiscard]] double relative_scalar_error( - const double computed, - const double expected - ) { - return std::abs(computed - expected) / std::max(std::abs(expected), 1.0e-30); - } +[[nodiscard]] double relative_scalar_error(const double computed, + const double expected) { + return std::abs(computed - expected) / std::max(std::abs(expected), 1.0e-30); +} - [[nodiscard]] mfem::Vector make_constant_density( - const mean_field::fem::FEM &f, - const double densityValue - ) { - mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::ConstantCoefficient densityCoefficient(densityValue); - densityField.ProjectCoefficient(densityCoefficient); +[[nodiscard]] mfem::Vector make_constant_density(const mean_field::fem::FEM &f, + const double densityValue) { + mfem::ParGridFunction densityField(f.densityFes.get()); + mfem::ConstantCoefficient densityCoefficient(densityValue); + densityField.ProjectCoefficient(densityCoefficient); - mfem::Vector densityTrue; - densityField.GetTrueDofs(densityTrue); - return densityTrue; - } + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; +} - [[nodiscard]] mfem::Vector make_reference_gravity( - const mean_field::fem::FEM &f, - const std::array< - double, - 3> &referenceGravity - ) { - mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); +[[nodiscard]] mfem::Vector +make_reference_gravity(const mean_field::fem::FEM &f, + const std::array &referenceGravity) { + mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); - mfem::VectorFunctionCoefficient gravityCoefficient( - f.mesh->Dimension(), [referenceGravity](const mfem::Vector &, mfem::Vector &value) { - value.SetSize(3); + mfem::VectorFunctionCoefficient gravityCoefficient( + f.mesh->Dimension(), + [referenceGravity](const mfem::Vector &, mfem::Vector &value) { + value.SetSize(3); - for (int component = 0; component < 3; ++component) { - value(component) = referenceGravity[static_cast(component)]; - } - } - ); + for (int component = 0; component < 3; ++component) { + value(component) = + referenceGravity[static_cast(component)]; + } + }); - gravityField.ProjectCoefficient(gravityCoefficient); + gravityField.ProjectCoefficient(gravityCoefficient); - mfem::Vector gravityTrue; - gravityField.GetTrueDofs(gravityTrue); - return gravityTrue; - } + mfem::Vector gravityTrue; + gravityField.GetTrueDofs(gravityTrue); + return gravityTrue; +} - [[nodiscard]] mfem::Vector make_radial_gravity( - const mean_field::fem::FEM &f, - const double radialCoefficient - ) { - mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); +[[nodiscard]] mfem::Vector make_radial_gravity(const mean_field::fem::FEM &f, + const double radialCoefficient) { + mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); - mfem::VectorFunctionCoefficient gravityCoefficient( - f.mesh->Dimension(), [radialCoefficient](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(position.Size()); + mfem::VectorFunctionCoefficient gravityCoefficient( + f.mesh->Dimension(), + [radialCoefficient](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(position.Size()); - for (int component = 0; component < position.Size(); ++component) { - value(component) = radialCoefficient * position(component); - } - } - ); + for (int component = 0; component < position.Size(); ++component) { + value(component) = radialCoefficient * position(component); + } + }); - gravityField.ProjectCoefficient(gravityCoefficient); + gravityField.ProjectCoefficient(gravityCoefficient); - mfem::Vector gravityTrue; - gravityField.GetTrueDofs(gravityTrue); - return gravityTrue; - } + mfem::Vector gravityTrue; + gravityField.GetTrueDofs(gravityTrue); + return gravityTrue; +} - [[nodiscard]] mfem::Vector make_affine_displacement( - const mean_field::fem::FEM &f, - const std::array< - double, - 3> &scales - ) { - mfem::ParGridFunction displacementField(f.displacementFes.get()); +[[nodiscard]] mfem::Vector +make_affine_displacement(const mean_field::fem::FEM &f, + const std::array &scales) { + mfem::ParGridFunction displacementField(f.displacementFes.get()); - mfem::VectorFunctionCoefficient displacementCoefficient( - f.mesh->Dimension(), [scales](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(position.Size()); + mfem::VectorFunctionCoefficient displacementCoefficient( + f.mesh->Dimension(), + [scales](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(position.Size()); - for (int component = 0; component < position.Size(); ++component) { - value(component) = (scales[static_cast(component)] - 1.0) * position(component); - } - } - ); + for (int component = 0; component < position.Size(); ++component) { + value(component) = + (scales[static_cast(component)] - 1.0) * + position(component); + } + }); - displacementField.ProjectCoefficient(displacementCoefficient); + displacementField.ProjectCoefficient(displacementCoefficient); - mfem::Vector displacementTrue; - displacementField.GetTrueDofs(displacementTrue); - return displacementTrue; - } + mfem::Vector displacementTrue; + displacementField.GetTrueDofs(displacementTrue); + return displacementTrue; +} - [[nodiscard]] mfem::Vector make_constant_test_direction( - const mean_field::fem::FEM &f, - const int selectedComponent - ) { - mfem::ParGridFunction testField(f.displacementFes.get()); +[[nodiscard]] mfem::Vector +make_constant_test_direction(const mean_field::fem::FEM &f, + const int selectedComponent) { + mfem::ParGridFunction testField(f.displacementFes.get()); - mfem::VectorFunctionCoefficient testCoefficient( - f.mesh->Dimension(), [selectedComponent](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(position.Size()); - value = 0.0; - value(selectedComponent) = 1.0; - } - ); + mfem::VectorFunctionCoefficient testCoefficient( + f.mesh->Dimension(), + [selectedComponent](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(position.Size()); + value = 0.0; + value(selectedComponent) = 1.0; + }); - testField.ProjectCoefficient(testCoefficient); + testField.ProjectCoefficient(testCoefficient); - mfem::Vector testTrue; - testField.GetTrueDofs(testTrue); - return testTrue; - } + mfem::Vector testTrue; + testField.GetTrueDofs(testTrue); + return testTrue; +} - [[nodiscard]] mfem::Vector make_dilation_test_direction(const mean_field::fem::FEM &f) { - mfem::ParGridFunction testField(f.displacementFes.get()); +[[nodiscard]] mfem::Vector +make_dilation_test_direction(const mean_field::fem::FEM &f) { + mfem::ParGridFunction testField(f.displacementFes.get()); - mfem::VectorFunctionCoefficient testCoefficient( - f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) { value = position; } - ); + mfem::VectorFunctionCoefficient testCoefficient( + f.mesh->Dimension(), [](const mfem::Vector &position, + mfem::Vector &value) { value = position; }); - testField.ProjectCoefficient(testCoefficient); + testField.ProjectCoefficient(testCoefficient); - mfem::Vector testTrue; - testField.GetTrueDofs(testTrue); - return testTrue; - } + mfem::Vector testTrue; + testField.GetTrueDofs(testTrue); + return testTrue; +} - void set_mass_normalized_density( - mean_field::fem::FEM &f, - const double targetMass, - mfem::ParGridFunction &densityField - ) { - const mfem::Vector stellarDensityTrue = gravity_prepared_test_utils::make_domain_supported_density(f, true); +void set_mass_normalized_density(mean_field::fem::FEM &f, + const double targetMass, + mfem::ParGridFunction &densityField) { + const mfem::Vector stellarDensityTrue = + gravity_prepared_test_utils::make_domain_supported_density(f, true); - densityField.SetFromTrueDofs(stellarDensityTrue); + densityField.SetFromTrueDofs(stellarDensityTrue); - const double unnormalizedMass = - mean_field::analysis::domain_integrate_grid_function(f, densityField, mean_field::utils::DOMAINS::STELLAR); + const double unnormalizedMass = + mean_field::analysis::domain_integrate_grid_function( + f, densityField, mean_field::utils::DOMAINS::STELLAR); - MFEM_VERIFY(unnormalizedMass > 0.0, "The analytic gravity-force test obtained non-positive mass."); + MFEM_VERIFY(unnormalizedMass > 0.0, + "The analytic gravity-force test obtained non-positive mass."); - densityField *= targetMass / unnormalizedMass; - } + densityField *= targetMass / unnormalizedMass; +} } // namespace gravity_displacement_force_analytic_test_utils -TEST_CASE( - "Gravity Displacement Force Matches Analytic Affine Resultants", - tags::gravity &tags::accuracy &tags::analytic_comparison &tags::integration -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Gravity Displacement Force Matches Analytic Affine Resultants", + tags::gravity &tags::accuracy &tags::analytic_comparison + &tags::integration) { + mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.mapping != nullptr); + REQUIRE(f.okay()); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - constexpr double densityValue = 1.37; + constexpr double densityValue = 1.37; - constexpr std::array physicalGravity{0.31, -0.47, 0.22}; + constexpr std::array physicalGravity{0.31, -0.47, 0.22}; - constexpr std::array affineCases{ - {{.name = "identity geometry", .scales = {1.0, 1.0, 1.0}}, - {.name = "volume-preserving affine geometry", .scales = {1.14, 0.93, 1.0 / (1.14 * 0.93)}}, - {.name = "volume-changing affine geometry", .scales = {1.11, 0.96, 1.07}}} - }; + constexpr std::array< + gravity_displacement_force_analytic_test_utils::AffineCase, 3> + affineCases{{{.name = "identity geometry", .scales = {1.0, 1.0, 1.0}}, + {.name = "volume-preserving affine geometry", + .scales = {1.14, 0.93, 1.0 / (1.14 * 0.93)}}, + {.name = "volume-changing affine geometry", + .scales = {1.11, 0.96, 1.07}}}}; - const mfem::Vector density = gravity_displacement_force_analytic_test_utils::make_constant_density(f, densityValue); + const mfem::Vector density = + gravity_displacement_force_analytic_test_utils::make_constant_density( + f, densityValue); - const double referenceVolume = - gravity_displacement_force_analytic_test_utils::analytic_sphere_volume(mean_field::utils::RADIUS); + const double referenceVolume = + gravity_displacement_force_analytic_test_utils::analytic_sphere_volume( + mean_field::utils::RADIUS); - constexpr double relativeTolerance = 5.0e-6; + constexpr double relativeTolerance = 5.0e-6; - for (const gravity_displacement_force_analytic_test_utils::AffineCase &affineCase : affineCases) { - DYNAMIC_SECTION(affineCase.name) { - const double mapDeterminant = - gravity_displacement_force_analytic_test_utils::determinant(affineCase.scales); + for (const gravity_displacement_force_analytic_test_utils::AffineCase + &affineCase : affineCases) { + DYNAMIC_SECTION(affineCase.name) { + const double mapDeterminant = + gravity_displacement_force_analytic_test_utils::determinant( + affineCase.scales); - REQUIRE(mapDeterminant > 0.0); + REQUIRE(mapDeterminant > 0.0); - std::array referenceGravity{}; + std::array referenceGravity{}; - /* - * For x = A X, the H(div) Piola relation is - * - * g_phys = A g_ref / det(A). - * - * Prescribe the RT pullback that represents the requested - * constant physical gravity field exactly. - */ - for (int component = 0; component < 3; ++component) { - referenceGravity[static_cast(component)] = - mapDeterminant * physicalGravity[static_cast(component)] / - affineCase.scales[static_cast(component)]; - } + /* + * For x = A X, the H(div) Piola relation is + * + * g_phys = A g_ref / det(A). + * + * Prescribe the RT pullback that represents the requested + * constant physical gravity field exactly. + */ + for (int component = 0; component < 3; ++component) { + referenceGravity[static_cast(component)] = + mapDeterminant * + physicalGravity[static_cast(component)] / + affineCase.scales[static_cast(component)]; + } - const mfem::Vector gravityGradient = - gravity_displacement_force_analytic_test_utils::make_reference_gravity(f, referenceGravity); + const mfem::Vector gravityGradient = + gravity_displacement_force_analytic_test_utils:: + make_reference_gravity(f, referenceGravity); - const mfem::Vector displacement = - gravity_displacement_force_analytic_test_utils::make_affine_displacement(f, affineCase.scales); + const mfem::Vector displacement = + gravity_displacement_force_analytic_test_utils:: + make_affine_displacement(f, affineCase.scales); - mfem::Vector residual; + mfem::Vector residual; - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, density, gravityGradient, displacement, residual - ); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, density, gravityGradient, displacement, + residual); - for (int component = 0; component < 3; ++component) { - const mfem::Vector testDirection = - gravity_displacement_force_analytic_test_utils::make_constant_test_direction(f, component); + for (int component = 0; component < 3; ++component) { + const mfem::Vector testDirection = + gravity_displacement_force_analytic_test_utils:: + make_constant_test_direction(f, component); - const double computedResultant = - gravity_prepared_test_utils::global_dot(residual, testDirection, f.mesh->GetComm()); + const double computedResultant = + gravity_prepared_test_utils::global_dot(residual, testDirection, + f.mesh->GetComm()); - const double expectedResultant = densityValue * physicalGravity[static_cast(component)] * - mapDeterminant * referenceVolume; + const double expectedResultant = + densityValue * + physicalGravity[static_cast(component)] * + mapDeterminant * referenceVolume; - const double relativeError = gravity_displacement_force_analytic_test_utils::relative_scalar_error( - computedResultant, expectedResultant - ); + const double relativeError = + gravity_displacement_force_analytic_test_utils:: + relative_scalar_error(computedResultant, expectedResultant); - CAPTURE(component); - INFO("Map determinant = " << mapDeterminant); - INFO("Computed resultant = " << computedResultant); - INFO("Analytic resultant = " << expectedResultant); - INFO("Relative resultant error = " << relativeError); + CAPTURE(component); + INFO("Map determinant = " << mapDeterminant); + INFO("Computed resultant = " << computedResultant); + INFO("Analytic resultant = " << expectedResultant); + INFO("Relative resultant error = " << relativeError); - CHECK(relativeError < relativeTolerance); - } - } + CHECK(relativeError < relativeTolerance); + } } + } } TEST_CASE( "Gravity Displacement Force Reproduces Analytic Homogeneous Sphere Work", - tags::gravity &tags::accuracy &tags::analytic_comparison &tags::integration -) { - mean_field::utils::Args args = test_utils::setup_args(); + tags::gravity &tags::accuracy &tags::analytic_comparison + &tags::integration) { + mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.okay()); + REQUIRE(f.domainMapperStateless != nullptr); - const double radius = mean_field::utils::RADIUS; - const double mass = mean_field::utils::MASS; - const double volume = gravity_displacement_force_analytic_test_utils::analytic_sphere_volume(radius); + const double radius = mean_field::utils::RADIUS; + const double mass = mean_field::utils::MASS; + const double volume = + gravity_displacement_force_analytic_test_utils::analytic_sphere_volume( + radius); - const double densityValue = mass / volume; - const double radialGravityCoefficient = mean_field::utils::G * mass / (radius * radius * radius); + const double densityValue = mass / volume; + const double radialGravityCoefficient = + mean_field::utils::G * mass / (radius * radius * radius); - const mfem::Vector density = gravity_displacement_force_analytic_test_utils::make_constant_density(f, densityValue); + const mfem::Vector density = + gravity_displacement_force_analytic_test_utils::make_constant_density( + f, densityValue); - const mfem::Vector gravityGradient = - gravity_displacement_force_analytic_test_utils::make_radial_gravity(f, radialGravityCoefficient); + const mfem::Vector gravityGradient = + gravity_displacement_force_analytic_test_utils::make_radial_gravity( + f, radialGravityCoefficient); - mfem::Vector displacement(f.displacementFes->GetTrueVSize()); - displacement = 0.0; + mfem::Vector displacement(f.displacementFes->GetTrueVSize()); + displacement = 0.0; - mfem::Vector residual; + mfem::Vector residual; - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, density, gravityGradient, displacement, residual - ); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, density, gravityGradient, displacement, + residual); - const mfem::Vector dilationDirection = - gravity_displacement_force_analytic_test_utils::make_dilation_test_direction(f); + const mfem::Vector dilationDirection = + gravity_displacement_force_analytic_test_utils:: + make_dilation_test_direction(f); - const double computedWork = gravity_prepared_test_utils::global_dot(residual, dilationDirection, f.mesh->GetComm()); + const double computedWork = gravity_prepared_test_utils::global_dot( + residual, dilationDirection, f.mesh->GetComm()); - const double analyticWork = (3.0 / 5.0) * mean_field::utils::G * mass * mass / radius; + const double analyticWork = + (3.0 / 5.0) * mean_field::utils::G * mass * mass / radius; - const double relativeError = - gravity_displacement_force_analytic_test_utils::relative_scalar_error(computedWork, analyticWork); + const double relativeError = + gravity_displacement_force_analytic_test_utils::relative_scalar_error( + computedWork, analyticWork); - INFO("Computed positive gravity work = " << computedWork); - INFO("Analytic positive gravity work = " << analyticWork); - INFO("Computed gravitational virial = " << -computedWork); - INFO("Analytic binding energy = " << -analyticWork); - INFO("Relative analytic work error = " << relativeError); + INFO("Computed positive gravity work = " << computedWork); + INFO("Analytic positive gravity work = " << analyticWork); + INFO("Computed gravitational virial = " << -computedWork); + INFO("Analytic binding energy = " << -analyticWork); + INFO("Relative analytic work error = " << relativeError); - REQUIRE(computedWork > 0.0); - CHECK(relativeError < 1.0e-5); + REQUIRE(computedWork > 0.0); + CHECK(relativeError < 1.0e-5); } -TEST_CASE( - "Solved Homogeneous Sphere Gravity Force Matches Analytic Virial", - tags::gravity &tags::accuracy &tags::analytic_comparison &tags::integration &tags::initialization -) { - mean_field::utils::Args args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); +TEST_CASE("Solved Homogeneous Sphere Gravity Force Matches Analytic Virial", + tags::gravity &tags::accuracy &tags::analytic_comparison + &tags::integration &tags::initialization) { + mean_field::utils::Args args = test_utils::setup_args(); + args.p.rtol = 1.0e-13; + 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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.okay()); + REQUIRE(f.domainMapperStateless != nullptr); - mfem::ParGridFunction displacementField(f.displacementFes.get()); - displacementField = 0.0; + mfem::ParGridFunction displacementField(f.displacementFes.get()); + displacementField = 0.0; - REQUIRE(f.mapping != nullptr); - f.mapping->ResetDisplacement(); - mean_field::physics::update_stiffness_matrix(f); + REQUIRE(f.domainMapperStateless != nullptr); + *f.displacement = 0.0; - const double radius = mean_field::utils::RADIUS; - const double mass = mean_field::utils::MASS; + const double radius = mean_field::utils::RADIUS; + const double mass = mean_field::utils::MASS; - mfem::ParGridFunction densityField(f.densityFes.get()); + mfem::ParGridFunction densityField(f.densityFes.get()); - gravity_displacement_force_analytic_test_utils::set_mass_normalized_density(f, mass, densityField); + gravity_displacement_force_analytic_test_utils::set_mass_normalized_density( + f, mass, densityField); - const mean_field::physics::GravitySolution gravitySolution = - mean_field::physics::grav_potential_new(f, args, densityField, displacementField); + const mean_field::physics::GravitySolution gravitySolution = + mean_field::physics::solve_gravity_field(f, args, densityField, + displacementField); - mfem::Vector densityTrue; - mfem::Vector gravityGradientTrue; - mfem::Vector displacementTrue; + mfem::Vector densityTrue; + mfem::Vector gravityGradientTrue; + mfem::Vector displacementTrue; - densityField.GetTrueDofs(densityTrue); - gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue); - displacementField.GetTrueDofs(displacementTrue); + densityField.GetTrueDofs(densityTrue); + gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue); + displacementField.GetTrueDofs(displacementTrue); - mfem::Vector residual; + mfem::Vector residual; - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, densityTrue, gravityGradientTrue, displacementTrue, residual - ); + mean_field::operators::kernels::apply_gravity_displacement_force_residual( + f, *f.domainMapperStateless, densityTrue, gravityGradientTrue, + displacementTrue, residual); - const mfem::Vector dilationDirection = - gravity_displacement_force_analytic_test_utils::make_dilation_test_direction(f); + const mfem::Vector dilationDirection = + gravity_displacement_force_analytic_test_utils:: + make_dilation_test_direction(f); - const double computedWork = gravity_prepared_test_utils::global_dot(residual, dilationDirection, f.mesh->GetComm()); + const double computedWork = gravity_prepared_test_utils::global_dot( + residual, dilationDirection, f.mesh->GetComm()); - const double analyticWork = (3.0 / 5.0) * mean_field::utils::G * mass * mass / radius; + const double analyticWork = + (3.0 / 5.0) * mean_field::utils::G * mass * mass / radius; - const double relativeError = - gravity_displacement_force_analytic_test_utils::relative_scalar_error(computedWork, analyticWork); + const double relativeError = + gravity_displacement_force_analytic_test_utils::relative_scalar_error( + computedWork, analyticWork); - INFO("Solved-field positive gravity work = " << computedWork); - INFO("Analytic positive gravity work = " << analyticWork); - INFO("Solved-field gravitational virial = " << -computedWork); - INFO("Analytic homogeneous-sphere binding energy = " << -analyticWork); - INFO("Relative solved-field virial error = " << relativeError); + INFO("Solved-field positive gravity work = " << computedWork); + INFO("Analytic positive gravity work = " << analyticWork); + INFO("Solved-field gravitational virial = " << -computedWork); + INFO("Analytic homogeneous-sphere binding energy = " << -analyticWork); + INFO("Relative solved-field virial error = " << relativeError); - REQUIRE(computedWork > 0.0); - CHECK(relativeError < 1.0e-5); -} \ No newline at end of file + REQUIRE(computedWork > 0.0); + CHECK(relativeError < 1.0e-5); +} diff --git a/tests/operators/gravity_field.cpp b/tests/operators/gravity_field.cpp index be2ae4f..0f701de 100644 --- a/tests/operators/gravity_field.cpp +++ b/tests/operators/gravity_field.cpp @@ -12,2899 +12,2761 @@ using namespace mean_field; using Catch::Matchers::WithinAbs; namespace { - namespace blocks = utils::blocks; - using form = blocks::gravity_field_form; +namespace blocks = utils::blocks; +using form = blocks::gravity_field_form; - constexpr auto density_block = blocks::get_value_block(blocks::density_field.mass_term); - constexpr auto displacement_block = blocks::get_value_block(blocks::displacement_field.geometry_term); - constexpr auto gravity_gradient_block = blocks::get_value_block(blocks::gravity_field.gradient_term); - constexpr auto gravity_potential_block = blocks::get_value_block(blocks::gravity_field.poisson_term); - constexpr auto gravity_gradient_residual_block = - blocks::get_residual_block(blocks::gravity_field.gradient_term); - constexpr auto gravity_poisson_residual_block = - blocks::get_residual_block(blocks::gravity_field.poisson_term); +constexpr auto density_block = + blocks::get_value_block(blocks::density_field.mass_term); +constexpr auto displacement_block = + blocks::get_value_block(blocks::displacement_field.geometry_term); +constexpr auto gravity_gradient_block = + blocks::get_value_block(blocks::gravity_field.gradient_term); +constexpr auto gravity_potential_block = + blocks::get_value_block(blocks::gravity_field.poisson_term); +constexpr auto gravity_gradient_residual_block = + blocks::get_residual_block(blocks::gravity_field.gradient_term); +constexpr auto gravity_poisson_residual_block = + blocks::get_residual_block(blocks::gravity_field.poisson_term); - blocks::form_layout make_gravity_layout(const fem::FEM &f) { - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - const auto density_map = field::make_field_dof_map(*f.densityFes); - const auto displacement_map = field::make_field_dof_map(*f.displacementFes); - const auto flux_map = field::make_field_dof_map(*f.gravityFluxFes); - const auto potential_map = field::make_field_dof_map(*f.gravityPotentialFes); - const std::array value_sizes{ - density_map.reduced_size(), displacement_map.reduced_size(), flux_map.reduced_size(), - potential_map.reduced_size() - }; +blocks::form_layout make_gravity_layout(const fem::FEM &f) { + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + const auto density_map = + field::make_field_dof_map(*f.densityFes); + const auto displacement_map = + field::make_field_dof_map( + *f.displacementFes); + const auto flux_map = field::make_field_dof_map( + *f.gravityFluxFes); + const auto potential_map = + field::make_field_dof_map( + *f.gravityPotentialFes); + const std::array value_sizes{ + density_map.reduced_size(), displacement_map.reduced_size(), + flux_map.reduced_size(), potential_map.reduced_size()}; - const std::array residual_sizes{ - flux_map.reduced_size(), potential_map.reduced_size() - }; + const std::array residual_sizes{ + flux_map.reduced_size(), potential_map.reduced_size()}; - return blocks::form_layout(value_sizes, residual_sizes); - } + return blocks::form_layout(value_sizes, residual_sizes); +} - template - void set_block( - mfem::Vector &vector, - const mfem::Array &offsets, - const Block block, - const mfem::Vector &values - ) { - const int block_id = block; - const int begin = offsets[block_id]; - const int size = offsets[block_id + 1] - begin; +template +void set_block(mfem::Vector &vector, const mfem::Array &offsets, + const Block block, const mfem::Vector &values) { + const int block_id = block; + const int begin = offsets[block_id]; + const int size = offsets[block_id + 1] - begin; - REQUIRE(values.Size() == size); - for (int i = 0; i < size; ++i) - vector(begin + i) = values(i); - } + REQUIRE(values.Size() == size); + for (int i = 0; i < size; ++i) + vector(begin + i) = values(i); +} - template - mfem::Vector get_block( - const mfem::Vector &vector, - const mfem::Array &offsets, - const Block block - ) { - const int block_id = block; - const int begin = offsets[block_id]; - const int size = offsets[block_id + 1] - begin; +template +mfem::Vector get_block(const mfem::Vector &vector, + const mfem::Array &offsets, const Block block) { + const int block_id = block; + const int begin = offsets[block_id]; + const int size = offsets[block_id + 1] - begin; - mfem::Vector result(size); - for (int i = 0; i < size; ++i) - result(i) = vector(begin + i); - return result; - } + mfem::Vector result(size); + for (int i = 0; i < size; ++i) + result(i) = vector(begin + i); + return result; +} - mfem::Vector make_test_vector( - const int size, - const double phase - ) { - mfem::Vector vector(size); - for (int i = 0; i < size; ++i) - vector(i) = 0.4 * std::sin(0.37 * static_cast(i + 1) + phase) + - 0.2 * std::cos(0.19 * static_cast(i + 1) - phase); - return vector; - } +mfem::Vector make_test_vector(const int size, const double phase) { + mfem::Vector vector(size); + for (int i = 0; i < size; ++i) + vector(i) = 0.4 * std::sin(0.37 * static_cast(i + 1) + phase) + + 0.2 * std::cos(0.19 * static_cast(i + 1) - phase); + return vector; +} - mfem::Vector make_displacement(const fem::FEM &f) { - auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.015 * position(0) + 0.004 * position(1); - value(1) = -0.003 * position(0) + 0.012 * position(1); - value(2) = -0.008 * position(2); - }; +mfem::Vector make_displacement(const fem::FEM &f) { + auto displacement_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = 0.015 * position(0) + 0.004 * position(1); + value(1) = -0.003 * position(0) + 0.012 * position(1); + value(2) = -0.008 * position(2); + }; - mfem::VectorFunctionCoefficient coefficient(3, displacement_function); - mfem::ParGridFunction displacement(f.displacementFes.get()); - mfem::Vector displacement_true; + mfem::VectorFunctionCoefficient coefficient(3, displacement_function); + mfem::ParGridFunction displacement(f.displacementFes.get()); + mfem::Vector displacement_true; - displacement.ProjectCoefficient(coefficient); - displacement.GetTrueDofs(displacement_true); - return displacement_true; - } + displacement.ProjectCoefficient(coefficient); + displacement.GetTrueDofs(displacement_true); + return displacement_true; +} - mfem::Vector make_constant_density( - const fem::FEM &f, - const double value - ) { - mfem::ConstantCoefficient coefficient(value); - mfem::ParGridFunction density(f.densityFes.get()); - mfem::Vector density_true; +mfem::Vector make_constant_density(const fem::FEM &f, const double value) { + mfem::ConstantCoefficient coefficient(value); + mfem::ParGridFunction density(f.densityFes.get()); + mfem::Vector density_true; - density.ProjectCoefficient(coefficient); - density.GetTrueDofs(density_true); - return density_true; - } + density.ProjectCoefficient(coefficient); + density.GetTrueDofs(density_true); + return density_true; +} - mfem::Vector make_vacuum_density( - const fem::FEM &f, - const double value - ) { - mfem::ParGridFunction density(f.densityFes.get()); - density = 0.0; +mfem::Vector make_vacuum_density(const fem::FEM &f, const double value) { + mfem::ParGridFunction density(f.densityFes.get()); + density = 0.0; - mfem::Array element_dofs; - mfem::Vector element_values; + mfem::Array element_dofs; + mfem::Vector element_values; - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - if (f.mesh->GetAttribute(element_id) != f.domainMapperStateless->GetVacuumElementAttribute()) - continue; + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + if (f.mesh->GetAttribute(element_id) != + field_dof_test_utils::vacuum_material_attribute) + continue; - f.densityFes->GetElementDofs(element_id, element_dofs); - element_values.SetSize(element_dofs.Size()); - element_values = value; - density.SetSubVector(element_dofs, element_values); - } + f.densityFes->GetElementDofs(element_id, element_dofs); + element_values.SetSize(element_dofs.Size()); + element_values = value; + density.SetSubVector(element_dofs, element_values); + } - mfem::Vector density_true; - density.GetTrueDofs(density_true); - return density_true; - } + mfem::Vector density_true; + density.GetTrueDofs(density_true); + return density_true; +} - double relative_difference( - const mfem::Vector &lhs, - const mfem::Vector &rhs - ) { - REQUIRE(lhs.Size() == rhs.Size()); +double relative_difference(const mfem::Vector &lhs, const mfem::Vector &rhs) { + REQUIRE(lhs.Size() == rhs.Size()); - mfem::Vector difference(lhs); - difference -= rhs; + mfem::Vector difference(lhs); + difference -= rhs; - return difference.Norml2() / std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-14}); - } + return difference.Norml2() / std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-14}); +} - mfem::Vector make_core_supported_gravity_gradient(const fem::FEM &f) { - constexpr double support_radius = 0.15 * utils::RADIUS; - constexpr double support_radius_squared = support_radius * support_radius; +mfem::Vector make_core_supported_gravity_gradient(const fem::FEM &f) { + constexpr double support_radius = 0.15 * utils::RADIUS; + constexpr double support_radius_squared = support_radius * support_radius; - auto field_function = [](const mfem::Vector &position, mfem::Vector &value) { - const double radius_squared = position * position; + auto field_function = [](const mfem::Vector &position, mfem::Vector &value) { + const double radius_squared = position * position; - value.SetSize(3); - value = 0.0; + value.SetSize(3); + value = 0.0; - if (radius_squared >= support_radius_squared) - return; + if (radius_squared >= support_radius_squared) + return; - const double normalized_radius_squared = radius_squared / support_radius_squared; - const double envelope = std::pow(1.0 - normalized_radius_squared, 3.0); + const double normalized_radius_squared = + radius_squared / support_radius_squared; + const double envelope = std::pow(1.0 - normalized_radius_squared, 3.0); - value(0) = envelope; - value(1) = -0.4 * envelope; - value(2) = 0.7 * envelope; - }; + value(0) = envelope; + value(1) = -0.4 * envelope; + value(2) = 0.7 * envelope; + }; - mfem::VectorFunctionCoefficient coefficient(3, field_function); - mfem::ParGridFunction gravity_gradient(f.gravityFluxFes.get()); - mfem::Vector gravity_gradient_true; + mfem::VectorFunctionCoefficient coefficient(3, field_function); + mfem::ParGridFunction gravity_gradient(f.gravityFluxFes.get()); + mfem::Vector gravity_gradient_true; - gravity_gradient.ProjectCoefficient(coefficient); - gravity_gradient.GetTrueDofs(gravity_gradient_true); - return gravity_gradient_true; - } + gravity_gradient.ProjectCoefficient(coefficient); + gravity_gradient.GetTrueDofs(gravity_gradient_true); + return gravity_gradient_true; +} - double global_norm( - const mfem::Vector &vector, - MPI_Comm communicator - ) { - REQUIRE(vector.Size() > 0); - const double local_norm_squared = vector * vector; - double global_norm_squared = 0.0; +double global_norm(const mfem::Vector &vector, MPI_Comm communicator) { + REQUIRE(vector.Size() > 0); + 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); - } + MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, + MPI_SUM, communicator); + return std::sqrt(global_norm_squared); +} - double global_relative_difference( - const mfem::Vector &lhs, - const mfem::Vector &rhs, - MPI_Comm communicator - ) { - REQUIRE(lhs.Size() == rhs.Size()); +double global_relative_difference(const mfem::Vector &lhs, + const mfem::Vector &rhs, + MPI_Comm communicator) { + REQUIRE(lhs.Size() == rhs.Size()); - mfem::Vector difference(lhs); - difference -= rhs; + mfem::Vector difference(lhs); + difference -= rhs; - const double difference_norm = global_norm(difference, communicator); - const double lhs_norm = global_norm(lhs, communicator); - const double rhs_norm = global_norm(rhs, communicator); + const double difference_norm = global_norm(difference, communicator); + const double lhs_norm = global_norm(lhs, communicator); + const double rhs_norm = global_norm(rhs, communicator); - return difference_norm / std::max({lhs_norm, rhs_norm, 1.0e-14}); - } + return difference_norm / std::max({lhs_norm, rhs_norm, 1.0e-14}); +} - struct StatelessHDivMassReference { - mfem::Vector total_action; - mfem::Vector stellar_action; - mfem::Vector vacuum_action; - long long stellar_elements{0}; - long long vacuum_elements{0}; - long long stellar_quadrature_points{0}; - long long vacuum_quadrature_points{0}; - double minimum_stellar_determinant{std::numeric_limits::infinity()}; - double maximum_stellar_determinant{0.0}; - double minimum_vacuum_determinant{std::numeric_limits::infinity()}; - double maximum_vacuum_determinant{0.0}; +struct StatelessHDivMassReference { + mfem::Vector total_action; + mfem::Vector stellar_action; + mfem::Vector vacuum_action; + long long stellar_elements{0}; + long long vacuum_elements{0}; + long long stellar_quadrature_points{0}; + long long vacuum_quadrature_points{0}; + double minimum_stellar_determinant{std::numeric_limits::infinity()}; + double maximum_stellar_determinant{0.0}; + double minimum_vacuum_determinant{std::numeric_limits::infinity()}; + double maximum_vacuum_determinant{0.0}; +}; + +void reference_true_to_local(const mfem::ParFiniteElementSpace &fes, + const mfem::Vector &true_vector, + mfem::Vector &local_vector) { + local_vector.SetSize(fes.GetVSize()); + + const mfem::Operator *prolongation = fes.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->Mult(true_vector, local_vector); + } else { + local_vector = true_vector; + } +} + +void reference_local_to_true(const mfem::ParFiniteElementSpace &fes, + const mfem::Vector &local_vector, + mfem::Vector &true_vector) { + true_vector.SetSize(fes.GetTrueVSize()); + true_vector = 0.0; + + const mfem::Operator *prolongation = fes.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->MultTranspose(local_vector, true_vector); + } else { + true_vector = local_vector; + } +} + +double global_vector_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); +} + +double global_vector_dot(const mfem::Vector &lhs, const mfem::Vector &rhs, + MPI_Comm communicator) { + const double local_dot = lhs * rhs; + double global_dot = 0.0; + MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator); + return global_dot; +} + +double global_relative_vector_error(const mfem::Vector &computed, + const mfem::Vector &reference, + MPI_Comm communicator) { + mfem::Vector difference(computed); + difference -= reference; + return global_vector_norm(difference, communicator) / + std::max(global_vector_norm(reference, communicator), + std::numeric_limits::epsilon()); +} + +const mfem::IntegrationRule &get_stateless_hdiv_reference_rule( + const fem::FEM &f, const mfem::FiniteElement &element, + const mfem::ElementTransformation &transformation) { + const bool is_vacuum = transformation.Attribute == + field_dof_test_utils::vacuum_material_attribute; + + const quadrature::Query query{ + .term = quadrature::Term::gravity_hdiv_mass, + .role = quadrature::QuadratureRole::discretization, + .domain = utils::DOMAINS::ALL, + .mapping = is_vacuum ? quadrature::MappingKind::kelvin + : quadrature::MappingKind::general, + .trial_order = element.GetOrder(), + .test_order = element.GetOrder(), + .coefficient_order = 0, + .geometry_weight_order = transformation.OrderW()}; + + return *f.quadratureFactory->get(query, transformation.GetGeometryType()) + .integration_rule; +} + +mfem::Vector make_full_support_gravity_gradient(const fem::FEM &f) { + mfem::Vector gravity_gradient(f.gravityFluxFes->GetTrueVSize()); + + for (int i = 0; i < gravity_gradient.Size(); ++i) { + const double index = static_cast(i + 1); + gravity_gradient(i) = + std::sin(0.37 * index) + 0.31 * std::cos(0.19 * index); + } + + return gravity_gradient; +} + +mfem::Vector make_stateless_reference_displacement(const fem::FEM &f, + const bool deformed) { + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; + + if (deformed) { + auto displacement_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = 0.04 * position(0) + 0.01 * position(1) * position(2); + value(1) = -0.03 * position(1) + 0.008 * position(0) * position(2); + value(2) = 0.02 * position(2) - 0.006 * position(0) * position(1); }; - void reference_true_to_local( - const mfem::ParFiniteElementSpace &fes, - const mfem::Vector &true_vector, - mfem::Vector &local_vector - ) { - local_vector.SetSize(fes.GetVSize()); + mfem::VectorFunctionCoefficient displacement_coefficient( + 3, displacement_function); + displacement.ProjectCoefficient(displacement_coefficient); + } - const mfem::Operator *prolongation = fes.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->Mult(true_vector, local_vector); - } else { - local_vector = true_vector; - } + mfem::Vector displacement_true; + displacement.GetTrueDofs(displacement_true); + return displacement_true; +} + +std::string_view mapping_status_name(const mapping::MappingStatus status) { + switch (status) { + case mapping::MappingStatus::valid: + return "valid"; + case mapping::MappingStatus::invalid_dimension: + return "invalid_dimension"; + case mapping::MappingStatus::non_finite_input: + return "non_finite_input"; + case mapping::MappingStatus::invalid_reference_radius: + return "invalid_reference_radius"; + case mapping::MappingStatus::at_compactified_infinity: + return "at_compactified_infinity"; + case mapping::MappingStatus::outside_reference_domain: + return "outside_reference_domain"; + case mapping::MappingStatus::non_finite_result: + return "non_finite_result"; + case mapping::MappingStatus::non_positive_determinant: + return "non_positive_determinant"; + } + + return "unknown"; +} + +StatelessHDivMassReference evaluate_stateless_hdiv_mass_quadrature_reference( + const fem::FEM &fem, const mfem::Vector &gravity_gradient_true, + const mfem::Vector &displacement_true) { + MFEM_VERIFY(fem.domainMapperStateless != nullptr, + "The stateless domain mapper is unavailable."); + + MFEM_VERIFY(fem.compactificationFes != nullptr, + "The compactification finite-element space is unavailable."); + + MFEM_VERIFY(fem.compactificationCoordinate != nullptr, + "The compactification coordinate is unavailable."); + + MFEM_VERIFY(gravity_gradient_true.Size() == + fem.gravityFluxFes->GetTrueVSize(), + "The gravity-gradient vector has the wrong size."); + + MFEM_VERIFY(displacement_true.Size() == fem.displacementFes->GetTrueVSize(), + "The displacement vector has the wrong size."); + + mfem::Vector gravity_gradient_local; + mfem::Vector displacement_local; + + reference_true_to_local(*fem.gravityFluxFes, gravity_gradient_true, + gravity_gradient_local); + + reference_true_to_local(*fem.displacementFes, displacement_true, + displacement_local); + + mfem::Vector total_local(fem.gravityFluxFes->GetVSize()); + + mfem::Vector stellar_local(fem.gravityFluxFes->GetVSize()); + + mfem::Vector vacuum_local(fem.gravityFluxFes->GetVSize()); + + total_local = 0.0; + stellar_local = 0.0; + vacuum_local = 0.0; + + StatelessHDivMassReference reference; + + mapping::DomainMapper::Workspace workspace(fem.mesh->Dimension()); + + mapping::VolumeMappingContext mapping_context; + + mfem::Array gravity_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; + + mfem::Vector element_gravity_gradient; + mfem::Vector element_displacement; + mfem::Vector element_compactification; + mfem::Vector element_action; + mfem::Vector quadrature_flux; + mfem::Vector mapped_quadrature_flux; + + mfem::DenseMatrix vector_shape; + mfem::DenseMatrix mapped_mass_tensor; + + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + + for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { + const mfem::FiniteElement &gravity_element = + *fem.gravityFluxFes->GetFE(element_id); + + const mfem::FiniteElement &displacement_element = + *fem.displacementFes->GetFE(element_id); + + const mfem::FiniteElement &compactification_element = + *fem.compactificationFes->GetFE(element_id); + + mfem::ElementTransformation &transformation = + *fem.mesh->GetElementTransformation(element_id); + + const bool is_vacuum = transformation.Attribute == vacuum_attribute; + + mfem::DofTransformation *gravity_dof_transformation = + fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); + + mfem::DofTransformation *displacement_dof_transformation = + fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); + + mfem::DofTransformation *compactification_dof_transformation = + fem.compactificationFes->GetElementDofs(element_id, + compactification_dofs); + + gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); + + displacement_local.GetSubVector(displacement_dofs, element_displacement); + + fem.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); + + if (gravity_dof_transformation != nullptr) { + gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); } - void reference_local_to_true( - const mfem::ParFiniteElementSpace &fes, - const mfem::Vector &local_vector, - mfem::Vector &true_vector - ) { - true_vector.SetSize(fes.GetTrueVSize()); - true_vector = 0.0; - - const mfem::Operator *prolongation = fes.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->MultTranspose(local_vector, true_vector); - } else { - true_vector = local_vector; - } + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal(element_displacement); } - double global_vector_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); + if (compactification_dof_transformation != nullptr) { + compactification_dof_transformation->InvTransformPrimal( + element_compactification); } - double global_vector_dot( - const mfem::Vector &lhs, - const mfem::Vector &rhs, - MPI_Comm communicator - ) { - const double local_dot = lhs * rhs; - double global_dot = 0.0; - MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator); - return global_dot; - } + const mapping::ElementDisplacementData displacement_data = + mapping::ElementDisplacementDataFromElementVDofs(displacement_element, + element_displacement); - double global_relative_vector_error( - const mfem::Vector &computed, - const mfem::Vector &reference, - MPI_Comm communicator - ) { - mfem::Vector difference(computed); - difference -= reference; - return global_vector_norm(difference, communicator) / - std::max(global_vector_norm(reference, communicator), std::numeric_limits::epsilon()); - } + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification); - const mfem::IntegrationRule &get_stateless_hdiv_reference_rule( - const fem::FEM &f, - const mfem::FiniteElement &element, - const mfem::ElementTransformation &transformation - ) { - const bool is_vacuum = transformation.Attribute == f.domainMapperStateless->GetVacuumElementAttribute(); + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; - const quadrature::Query query{ - .term = quadrature::Term::gravity_hdiv_mass, - .role = quadrature::QuadratureRole::discretization, - .domain = utils::DOMAINS::ALL, - .mapping = is_vacuum ? quadrature::MappingKind::kelvin : quadrature::MappingKind::general, - .trial_order = element.GetOrder(), - .test_order = element.GetOrder(), - .coefficient_order = 0, - .geometry_weight_order = transformation.OrderW() - }; + const int gravity_dof_count = gravity_element.GetDof(); - return *f.quadratureFactory->get(query, transformation.GetGeometryType()).integration_rule; - } + const int dimension = transformation.GetSpaceDim(); - mfem::Vector make_full_support_gravity_gradient(const fem::FEM &f) { - mfem::Vector gravity_gradient(f.gravityFluxFes->GetTrueVSize()); + element_action.SetSize(gravity_dof_count); + element_action = 0.0; - for (int i = 0; i < gravity_gradient.Size(); ++i) { - const double index = static_cast(i + 1); - gravity_gradient(i) = std::sin(0.37 * index) + 0.31 * std::cos(0.19 * index); + quadrature_flux.SetSize(dimension); + mapped_quadrature_flux.SetSize(dimension); + + vector_shape.SetSize(gravity_dof_count, dimension); + + mapped_mass_tensor.SetSize(dimension, dimension); + + const mfem::IntegrationRule &integration_rule = + get_stateless_hdiv_reference_rule(fem, gravity_element, transformation); + + for (int quadrature_point = 0; + quadrature_point < integration_rule.GetNPoints(); ++quadrature_point) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(quadrature_point); + + transformation.SetIntPoint(&integration_point); + + const mapping::MappingStatus status = + fem.domainMapperStateless->EvaluateVolume( + mapping_data, transformation, integration_point, workspace, + mapping_context); + + MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, + "Stateless mapping failed while evaluating the H(div) " + "quadrature reference." + << "\nElement ID = " << element_id + << "\nElement attribute = " << transformation.Attribute + << "\nQuadrature point = " << quadrature_point + << "\nMapping status = " << mapping_status_name(status)); + + gravity_element.CalcVShape(transformation, vector_shape); + + mean_field::mapping::ComputeHDivMassTensor(mapping_context.mapping, + mapped_mass_tensor); + + // Evaluate B*x at this quadrature point. + vector_shape.MultTranspose(element_gravity_gradient, quadrature_flux); + + // Apply the mapped H(div) mass tensor. + mapped_mass_tensor.Mult(quadrature_flux, mapped_quadrature_flux); + + const double weight = integration_point.weight * transformation.Weight(); + + // Accumulate B^T*D*B*x directly. + for (int dof = 0; dof < gravity_dof_count; ++dof) { + double contribution = 0.0; + + for (int component = 0; component < dimension; ++component) { + contribution += + vector_shape(dof, component) * mapped_quadrature_flux(component); } - return gravity_gradient; + element_action(dof) += weight * contribution; + } + + const double mapping_determinant = + mapping_context.mapping.mapping_determinant; + + MFEM_VERIFY(std::isfinite(mapping_determinant) && + mapping_determinant > 0.0, + "The quadrature reference encountered an invalid mapping " + "determinant."); + + if (is_vacuum) { + reference.minimum_vacuum_determinant = + std::min(reference.minimum_vacuum_determinant, mapping_determinant); + + reference.maximum_vacuum_determinant = + std::max(reference.maximum_vacuum_determinant, mapping_determinant); + + ++reference.vacuum_quadrature_points; + } else { + reference.minimum_stellar_determinant = std::min( + reference.minimum_stellar_determinant, mapping_determinant); + + reference.maximum_stellar_determinant = std::max( + reference.maximum_stellar_determinant, mapping_determinant); + + ++reference.stellar_quadrature_points; + } } - mfem::Vector make_stateless_reference_displacement( - const fem::FEM &f, - const bool deformed - ) { - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement = 0.0; - - if (deformed) { - auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.04 * position(0) + 0.01 * position(1) * position(2); - value(1) = -0.03 * position(1) + 0.008 * position(0) * position(2); - value(2) = 0.02 * position(2) - 0.006 * position(0) * position(1); - }; - - mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); - displacement.ProjectCoefficient(displacement_coefficient); - } - - mfem::Vector displacement_true; - displacement.GetTrueDofs(displacement_true); - return displacement_true; + if (gravity_dof_transformation != nullptr) { + gravity_dof_transformation->TransformDual(element_action); } - std::string_view mapping_status_name(const mapping::MappingStatus status) { - switch (status) { - case mapping::MappingStatus::valid: - return "valid"; - case mapping::MappingStatus::invalid_dimension: - return "invalid_dimension"; - case mapping::MappingStatus::non_finite_input: - return "non_finite_input"; - case mapping::MappingStatus::invalid_reference_radius: - return "invalid_reference_radius"; - case mapping::MappingStatus::at_compactified_infinity: - return "at_compactified_infinity"; - case mapping::MappingStatus::outside_reference_domain: - return "outside_reference_domain"; - case mapping::MappingStatus::non_finite_result: - return "non_finite_result"; - case mapping::MappingStatus::non_positive_determinant: - return "non_positive_determinant"; - } + total_local.AddElementVector(gravity_dofs, element_action); - return "unknown"; + if (is_vacuum) { + vacuum_local.AddElementVector(gravity_dofs, element_action); + + ++reference.vacuum_elements; + } else { + stellar_local.AddElementVector(gravity_dofs, element_action); + + ++reference.stellar_elements; } + } - StatelessHDivMassReference evaluate_stateless_hdiv_mass_quadrature_reference( - const fem::FEM &fem, - const mfem::Vector &gravity_gradient_true, - const mfem::Vector &displacement_true - ) { - MFEM_VERIFY(fem.domainMapperStateless != nullptr, "The stateless domain mapper is unavailable."); + reference_local_to_true(*fem.gravityFluxFes, total_local, + reference.total_action); - MFEM_VERIFY(fem.compactificationFes != nullptr, "The compactification finite-element space is unavailable."); + reference_local_to_true(*fem.gravityFluxFes, stellar_local, + reference.stellar_action); - MFEM_VERIFY(fem.compactificationCoordinate != nullptr, "The compactification coordinate is unavailable."); + reference_local_to_true(*fem.gravityFluxFes, vacuum_local, + reference.vacuum_action); - MFEM_VERIFY( - gravity_gradient_true.Size() == fem.gravityFluxFes->GetTrueVSize(), - "The gravity-gradient vector has the wrong size." - ); + const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); - MFEM_VERIFY( - displacement_true.Size() == fem.displacementFes->GetTrueVSize(), - "The displacement vector has the wrong size." - ); + const long long local_counts[4]{ + reference.stellar_elements, reference.vacuum_elements, + reference.stellar_quadrature_points, reference.vacuum_quadrature_points}; - mfem::Vector gravity_gradient_local; - mfem::Vector displacement_local; + long long global_counts[4]{}; - reference_true_to_local(*fem.gravityFluxFes, gravity_gradient_true, gravity_gradient_local); + MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, + communicator); - reference_true_to_local(*fem.displacementFes, displacement_true, displacement_local); + reference.stellar_elements = global_counts[0]; - mfem::Vector total_local(fem.gravityFluxFes->GetVSize()); + reference.vacuum_elements = global_counts[1]; - mfem::Vector stellar_local(fem.gravityFluxFes->GetVSize()); + reference.stellar_quadrature_points = global_counts[2]; + + reference.vacuum_quadrature_points = global_counts[3]; + + const double local_minimums[2]{reference.minimum_stellar_determinant, + reference.minimum_vacuum_determinant}; + + const double local_maximums[2]{reference.maximum_stellar_determinant, + reference.maximum_vacuum_determinant}; + + double global_minimums[2]{}; + double global_maximums[2]{}; + + MPI_Allreduce(local_minimums, global_minimums, 2, MPI_DOUBLE, MPI_MIN, + communicator); + + MPI_Allreduce(local_maximums, global_maximums, 2, MPI_DOUBLE, MPI_MAX, + communicator); + + reference.minimum_stellar_determinant = global_minimums[0]; + + reference.minimum_vacuum_determinant = global_minimums[1]; + + reference.maximum_stellar_determinant = global_maximums[0]; + + reference.maximum_vacuum_determinant = global_maximums[1]; + + return reference; +} + +using gravity_form = blocks::gravity_field_form; +using gravity_layout = blocks::form_layout; +gravity_layout make_gravity_jacobian_layout(const fem::FEM &f) { + return make_gravity_layout(f); +} + +template +void set_value_block(mfem::Vector &vector, const gravity_layout &layout, + const blocks::value_block block, + const mfem::Vector &values) { + REQUIRE(values.Size() == layout.size(block)); + for (int i = 0; i < values.Size(); ++i) + vector(layout.offset(block) + i) = values(i); +} + +template +mfem::Vector get_value_block(const mfem::Vector &vector, + const gravity_layout &layout, + const blocks::value_block block) { + mfem::Vector values(layout.size(block)); + for (int i = 0; i < values.Size(); ++i) + values(i) = vector(layout.offset(block) + i); + return values; +} + +template +mfem::Vector get_residual_block(const mfem::Vector &vector, + const gravity_layout &layout, + const blocks::residual_block block) { + mfem::Vector values(layout.size(block)); + for (int i = 0; i < values.Size(); ++i) + values(i) = vector(layout.offset(block) + i); + return values; +} + +template +void fill_value_block(mfem::Vector &vector, const gravity_layout &layout, + const blocks::value_block block, + const double scale, const double phase) { + for (int i = 0; i < layout.size(block); ++i) { + const double index_value = static_cast(i + 1); + vector(layout.offset(block) + i) = + scale * (std::sin(0.31 * index_value + phase) + + 0.37 * std::cos(0.17 * index_value - phase)); + } +} + +mfem::Vector make_gravity_jacobian_state(const fem::FEM &f, + const gravity_layout &layout, + const bool deformed) { + mfem::Vector state(layout.value_offsets().Last()); + state = 0.0; + + fill_value_block(state, layout, density_block, 0.7, 0.11); + fill_value_block(state, layout, gravity_gradient_block, 0.4, 0.23); + fill_value_block(state, layout, gravity_potential_block, 0.3, 0.37); + + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + const auto displacement_map = + field::make_field_dof_map( + *f.displacementFes); + const mfem::Vector displacement = displacement_map.gather( + make_stateless_reference_displacement(f, deformed)); + set_value_block(state, layout, displacement_block, displacement); + + return state; +} + +mfem::Vector make_density_direction(const gravity_layout &layout) { + mfem::Vector direction(layout.value_offsets().Last()); + direction = 0.0; + fill_value_block(direction, layout, density_block, 0.13, 0.19); + return direction; +} + +mfem::Vector make_gravity_gradient_direction(const gravity_layout &layout) { + mfem::Vector direction(layout.value_offsets().Last()); + direction = 0.0; + fill_value_block(direction, layout, gravity_gradient_block, 0.11, 0.29); + return direction; +} + +mfem::Vector make_gravity_potential_direction(const gravity_layout &layout) { + mfem::Vector direction(layout.value_offsets().Last()); + direction = 0.0; + fill_value_block(direction, layout, gravity_potential_block, 0.09, 0.41); + return direction; +} + +mfem::Vector +make_combined_fixed_geometry_direction(const gravity_layout &layout) { + mfem::Vector direction = make_density_direction(layout); + const mfem::Vector gravity_gradient_direction = + make_gravity_gradient_direction(layout); + const mfem::Vector gravity_potential_direction = + make_gravity_potential_direction(layout); + direction += gravity_gradient_direction; + direction += gravity_potential_direction; + return direction; +} + +mfem::Vector +make_displacement_direction(const gravity_layout &layout, + const mfem::Vector &displacement_direction) { + mfem::Vector direction(layout.value_offsets().Last()); + direction = 0.0; + set_value_block(direction, layout, displacement_block, + displacement_direction); + return direction; +} + +mfem::Vector +evaluate_centered_difference(operators::GravityFieldOperator &gravity_operator, + const mfem::Vector &state, + const mfem::Vector &direction, const double step) { + mfem::Vector plus_state(state); + mfem::Vector minus_state(state); + plus_state.Add(step, direction); + minus_state.Add(-step, direction); + + mfem::Vector plus_residual; + mfem::Vector minus_residual; + gravity_operator.Mult(plus_state, plus_residual); + gravity_operator.Mult(minus_state, minus_residual); + + mfem::Vector difference(plus_residual); + difference -= minus_residual; + difference /= 2.0 * step; + return difference; +} + +struct HdivMassVariationTestFields { + mfem::Vector gravity_gradient; + mfem::Vector displacement; + mfem::Vector displacement_direction_1; + mfem::Vector displacement_direction_2; +}; + +double global_relative_error(const mfem::Vector &computed, + const mfem::Vector &reference, + MPI_Comm communicator) { + REQUIRE(computed.Size() == reference.Size()); + + mfem::Vector difference(computed); + difference -= reference; + + return global_norm(difference, communicator) / + std::max(global_norm(reference, communicator), 1.0e-14); +} + +HdivMassVariationTestFields +make_hdiv_mass_variation_test_fields(const fem::FEM &f) { + const int dimension = f.mesh->Dimension(); + + auto gravity_gradient_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = 0.4 + 0.18 * position(0) - 0.07 * position(1) * position(2); + value(1) = -0.3 + 0.11 * position(1) + 0.05 * position(0) * position(2); + value(2) = 0.2 - 0.09 * position(2) + 0.04 * position(0) * position(1); + }; + + auto displacement_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = 0.025 * position(0) + 0.006 * position(1) * position(2); + value(1) = -0.018 * position(1) + 0.005 * position(0) * position(2); + value(2) = 0.014 * position(2) + 0.004 * position(0) * position(1); + }; + + auto displacement_direction_1_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = 0.16 * position(0) + 0.03 * position(1); + value(1) = -0.11 * position(1) + 0.02 * position(2); + value(2) = 0.13 * position(2) - 0.025 * position(0); + }; + + auto displacement_direction_2_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = -0.07 * position(1) + 0.025 * position(2); + value(1) = 0.09 * position(0) + 0.04 * position(2); + value(2) = -0.08 * position(2) + 0.03 * position(0) * position(1); + }; + + mfem::VectorFunctionCoefficient gravity_gradient_coefficient( + dimension, gravity_gradient_function); + mfem::VectorFunctionCoefficient displacement_coefficient( + dimension, displacement_function); + mfem::VectorFunctionCoefficient displacement_direction_1_coefficient( + dimension, displacement_direction_1_function); + mfem::VectorFunctionCoefficient displacement_direction_2_coefficient( + dimension, displacement_direction_2_function); + + mfem::ParGridFunction gravity_gradient_grid(f.gravityFluxFes.get()); + mfem::ParGridFunction displacement_grid(f.displacementFes.get()); + mfem::ParGridFunction displacement_direction_1_grid(f.displacementFes.get()); + mfem::ParGridFunction displacement_direction_2_grid(f.displacementFes.get()); + + gravity_gradient_grid.ProjectCoefficient(gravity_gradient_coefficient); + displacement_grid.ProjectCoefficient(displacement_coefficient); + displacement_direction_1_grid.ProjectCoefficient( + displacement_direction_1_coefficient); + displacement_direction_2_grid.ProjectCoefficient( + displacement_direction_2_coefficient); + + HdivMassVariationTestFields fields; + gravity_gradient_grid.GetTrueDofs(fields.gravity_gradient); + displacement_grid.GetTrueDofs(fields.displacement); + displacement_direction_1_grid.GetTrueDofs(fields.displacement_direction_1); + displacement_direction_2_grid.GetTrueDofs(fields.displacement_direction_2); + + return fields; +} + +mfem::Vector centered_hdiv_mass_geometry_difference( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper, + const mfem::Vector &gravity_gradient, const mfem::Vector &displacement, + const mfem::Vector &displacement_direction, const double difference_step) { + mfem::Vector plus_displacement(displacement); + mfem::Vector minus_displacement(displacement); + plus_displacement.Add(difference_step, displacement_direction); + minus_displacement.Add(-difference_step, displacement_direction); + + mfem::Vector plus_action; + mfem::Vector minus_action; + + operators::kernels::apply_mapped_hdiv_mass(f, domain_mapper, gravity_gradient, + plus_displacement, plus_action); + operators::kernels::apply_mapped_hdiv_mass(f, domain_mapper, gravity_gradient, + minus_displacement, minus_action); + + plus_action -= minus_action; + plus_action /= 2.0 * difference_step; + + return plus_action; +} + +mfem::Vector make_source_variation_density(const fem::FEM &f) { + auto density_function = [](const mfem::Vector &position) { + return 1.2 + 0.16 * position(0) - 0.09 * position(1) + 0.07 * position(2) + + 0.04 * position(0) * position(1); + }; + + mfem::FunctionCoefficient density_coefficient(density_function); + mfem::ParGridFunction density_grid(f.densityFes.get()); + mfem::Vector density_true; + + density_grid.ProjectCoefficient(density_coefficient); + density_grid.GetTrueDofs(density_true); + + return density_true; +} + +mfem::Vector centered_source_geometry_difference( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper, + const mfem::Vector &density, const mfem::Vector &displacement, + const mfem::Vector &displacement_direction, const double difference_step) { + mfem::Vector plus_displacement(displacement); + mfem::Vector minus_displacement(displacement); + plus_displacement.Add(difference_step, displacement_direction); + minus_displacement.Add(-difference_step, displacement_direction); + + mfem::Vector plus_action; + mfem::Vector minus_action; + + operators::kernels::apply_mapped_source(f, domain_mapper, density, + plus_displacement, plus_action); + operators::kernels::apply_mapped_source(f, domain_mapper, density, + minus_displacement, minus_action); + + plus_action -= minus_action; + plus_action /= 2.0 * difference_step; + + return plus_action; +} +double global_dot(const mfem::Vector &left, const mfem::Vector &right, + MPI_Comm communicator) { + REQUIRE(left.Size() > 0); + REQUIRE(left.Size() == right.Size()); + + const double local_dot = left * right; + double global_dot_value = 0.0; + MPI_Allreduce(&local_dot, &global_dot_value, 1, MPI_DOUBLE, MPI_SUM, + communicator); + + return global_dot_value; +} + +mfem::Vector make_secondary_gravity_gradient(const fem::FEM &f) { + auto gravity_gradient_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = -0.17 + 0.09 * position(1) + 0.03 * position(0) * position(2); + value(1) = 0.31 - 0.14 * position(0) + 0.05 * position(1) * position(2); + value(2) = -0.22 + 0.12 * position(2) - 0.04 * position(0) * position(1); + }; + + mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), + gravity_gradient_function); + mfem::ParGridFunction grid_function(f.gravityFluxFes.get()); + mfem::Vector true_dofs; + + grid_function.ProjectCoefficient(coefficient); + grid_function.GetTrueDofs(true_dofs); + + return true_dofs; +} + +mfem::Vector make_secondary_source_density(const fem::FEM &f) { + auto density_function = [](const mfem::Vector &position) { + return 0.8 - 0.11 * position(0) + 0.13 * position(1) - 0.06 * position(2) + + 0.03 * position(1) * position(2); + }; + + mfem::FunctionCoefficient coefficient(density_function); + mfem::ParGridFunction grid_function(f.densityFes.get()); + mfem::Vector true_dofs; + + grid_function.ProjectCoefficient(coefficient); + grid_function.GetTrueDofs(true_dofs); + + return true_dofs; +} + +mfem::Vector make_vacuum_only_density(const fem::FEM &f, + const int vacuum_attribute) { + mfem::ParGridFunction grid_function(f.densityFes.get()); + grid_function = 0.0; + + mfem::Array element_dofs; + mfem::Vector element_values; + + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); + REQUIRE(transformation != nullptr); + + if (transformation->Attribute != vacuum_attribute) + continue; + + const mfem::FiniteElement &element = *f.densityFes->GetFE(element_id); + mfem::DofTransformation *dof_transformation = + f.densityFes->GetElementDofs(element_id, element_dofs); + + element_values.SetSize(element.GetDof()); + for (int i = 0; i < element_values.Size(); ++i) + element_values(i) = 0.9 + 0.01 * static_cast(i); + + if (dof_transformation != nullptr) + dof_transformation->TransformPrimal(element_values); + grid_function.SetSubVector(element_dofs, element_values); + } + + mfem::Vector true_dofs; + grid_function.GetTrueDofs(true_dofs); + + return true_dofs; +} + +template +mfem::Vector make_read_only_value_view(const mfem::Vector &vector, + const mfem::Array &offsets, + const blocks::value_block) { + const int offset = offsets[index]; + const int size = offsets[index + 1] - offset; + return mfem::Vector(const_cast(vector.GetData()) + offset, + size); +} + +void check_linearization_context_matches_state( + const operators::context::gravity_field::GravityFieldLinearizationContext + &context, + const mfem::Vector &state, const mfem::Array &state_offsets, + MPI_Comm communicator) { + using form = blocks::gravity_field_form; + + constexpr auto density_block = + utils::blocks::get_value_block(blocks::density_field.mass_term); + constexpr auto displacement_block = utils::blocks::get_value_block( + blocks::displacement_field.geometry_term); + constexpr auto gravity_gradient_block = + utils::blocks::get_value_block(blocks::gravity_field.gradient_term); + + const mfem::Vector density = + make_read_only_value_view(state, state_offsets, density_block); + const mfem::Vector displacement = + make_read_only_value_view(state, state_offsets, displacement_block); + const mfem::Vector gravity_gradient = + make_read_only_value_view(state, state_offsets, gravity_gradient_block); + + CHECK_THAT(global_relative_vector_error( + context.GetDensityTrue(), + context.GetDensityMap().scatter(density), communicator), + Catch::Matchers::WithinAbs(0.0, 0.0)); + CHECK_THAT(global_relative_vector_error( + context.GetGeometryContext().GetDisplacementTrue(), + context.GetDisplacementMap().scatter(displacement), + communicator), + Catch::Matchers::WithinAbs(0.0, 0.0)); + CHECK_THAT(global_relative_vector_error( + context.GetGravityGradientTrue(), + context.GetGravityGradientMap().scatter(gravity_gradient), + communicator), + Catch::Matchers::WithinAbs(0.0, 0.0)); +} - mfem::Vector vacuum_local(fem.gravityFluxFes->GetVSize()); - - total_local = 0.0; - stellar_local = 0.0; - vacuum_local = 0.0; - - StatelessHDivMassReference reference; - - mapping::DomainMapperStateless::Workspace workspace(fem.mesh->Dimension()); - - mapping::VolumeMappingContext mapping_context; - - mfem::Array gravity_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; - - mfem::Vector element_gravity_gradient; - mfem::Vector element_displacement; - mfem::Vector element_compactification; - mfem::Vector element_action; - mfem::Vector quadrature_flux; - mfem::Vector mapped_quadrature_flux; - - mfem::DenseMatrix vector_shape; - mfem::DenseMatrix mapped_mass_tensor; - - const int vacuum_attribute = fem.domainMapperStateless->GetVacuumElementAttribute(); - - for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { - const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id); - - const mfem::FiniteElement &displacement_element = *fem.displacementFes->GetFE(element_id); - - const mfem::FiniteElement &compactification_element = *fem.compactificationFes->GetFE(element_id); - - mfem::ElementTransformation &transformation = *fem.mesh->GetElementTransformation(element_id); - - const bool is_vacuum = transformation.Attribute == vacuum_attribute; - - mfem::DofTransformation *gravity_dof_transformation = - fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); - - mfem::DofTransformation *displacement_dof_transformation = - fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); - - mfem::DofTransformation *compactification_dof_transformation = - fem.compactificationFes->GetElementDofs(element_id, compactification_dofs); - - gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); - - displacement_local.GetSubVector(displacement_dofs, element_displacement); - - fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - - if (gravity_dof_transformation != nullptr) { - gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); - } - - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(element_displacement); - } - - if (compactification_dof_transformation != nullptr) { - compactification_dof_transformation->InvTransformPrimal(element_compactification); - } - - const mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); - - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - const int gravity_dof_count = gravity_element.GetDof(); - - const int dimension = transformation.GetSpaceDim(); - - element_action.SetSize(gravity_dof_count); - element_action = 0.0; - - quadrature_flux.SetSize(dimension); - mapped_quadrature_flux.SetSize(dimension); - - vector_shape.SetSize(gravity_dof_count, dimension); - - mapped_mass_tensor.SetSize(dimension, dimension); - - const mfem::IntegrationRule &integration_rule = - get_stateless_hdiv_reference_rule(fem, gravity_element, transformation); - - for (int quadrature_point = 0; quadrature_point < integration_rule.GetNPoints(); ++quadrature_point) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(quadrature_point); - - transformation.SetIntPoint(&integration_point); - - const mapping::MappingStatus status = fem.domainMapperStateless->EvaluateVolume( - mapping_data, transformation, integration_point, workspace, mapping_context - ); - - MFEM_VERIFY( - status == mean_field::mapping::MappingStatus::valid, - "Stateless mapping failed while evaluating the H(div) " - "quadrature reference." - << "\nElement ID = " << element_id << "\nElement attribute = " << transformation.Attribute - << "\nQuadrature point = " << quadrature_point - << "\nMapping status = " << mapping_status_name(status) - ); - - gravity_element.CalcVShape(transformation, vector_shape); - - mean_field::mapping::ComputeHDivMassTensor(mapping_context.mapping, mapped_mass_tensor); - - // Evaluate B*x at this quadrature point. - vector_shape.MultTranspose(element_gravity_gradient, quadrature_flux); - - // Apply the mapped H(div) mass tensor. - mapped_mass_tensor.Mult(quadrature_flux, mapped_quadrature_flux); - - const double weight = integration_point.weight * transformation.Weight(); - - // Accumulate B^T*D*B*x directly. - for (int dof = 0; dof < gravity_dof_count; ++dof) { - double contribution = 0.0; - - for (int component = 0; component < dimension; ++component) { - contribution += vector_shape(dof, component) * mapped_quadrature_flux(component); - } - - element_action(dof) += weight * contribution; - } - - const double mapping_determinant = mapping_context.mapping.mapping_determinant; - - MFEM_VERIFY( - std::isfinite(mapping_determinant) && mapping_determinant > 0.0, - "The quadrature reference encountered an invalid mapping " - "determinant." - ); - - if (is_vacuum) { - reference.minimum_vacuum_determinant = - std::min(reference.minimum_vacuum_determinant, mapping_determinant); - - reference.maximum_vacuum_determinant = - std::max(reference.maximum_vacuum_determinant, mapping_determinant); - - ++reference.vacuum_quadrature_points; - } else { - reference.minimum_stellar_determinant = - std::min(reference.minimum_stellar_determinant, mapping_determinant); - - reference.maximum_stellar_determinant = - std::max(reference.maximum_stellar_determinant, mapping_determinant); - - ++reference.stellar_quadrature_points; - } - } - - if (gravity_dof_transformation != nullptr) { - gravity_dof_transformation->TransformDual(element_action); - } - - total_local.AddElementVector(gravity_dofs, element_action); - - if (is_vacuum) { - vacuum_local.AddElementVector(gravity_dofs, element_action); - - ++reference.vacuum_elements; - } else { - stellar_local.AddElementVector(gravity_dofs, element_action); - - ++reference.stellar_elements; - } - } - - reference_local_to_true(*fem.gravityFluxFes, total_local, reference.total_action); - - reference_local_to_true(*fem.gravityFluxFes, stellar_local, reference.stellar_action); - - reference_local_to_true(*fem.gravityFluxFes, vacuum_local, reference.vacuum_action); - - const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); - - const long long local_counts[4]{ - reference.stellar_elements, reference.vacuum_elements, reference.stellar_quadrature_points, - reference.vacuum_quadrature_points - }; - - long long global_counts[4]{}; - - MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, communicator); - - reference.stellar_elements = global_counts[0]; - - reference.vacuum_elements = global_counts[1]; - - reference.stellar_quadrature_points = global_counts[2]; - - reference.vacuum_quadrature_points = global_counts[3]; - - const double local_minimums[2]{reference.minimum_stellar_determinant, reference.minimum_vacuum_determinant}; - - const double local_maximums[2]{reference.maximum_stellar_determinant, reference.maximum_vacuum_determinant}; - - double global_minimums[2]{}; - double global_maximums[2]{}; - - MPI_Allreduce(local_minimums, global_minimums, 2, MPI_DOUBLE, MPI_MIN, communicator); - - MPI_Allreduce(local_maximums, global_maximums, 2, MPI_DOUBLE, MPI_MAX, communicator); - - reference.minimum_stellar_determinant = global_minimums[0]; - - reference.minimum_vacuum_determinant = global_minimums[1]; - - reference.maximum_stellar_determinant = global_maximums[0]; - - reference.maximum_vacuum_determinant = global_maximums[1]; - - return reference; - } - - using gravity_form = blocks::gravity_field_form; - using gravity_layout = blocks::form_layout; - gravity_layout make_gravity_jacobian_layout(const fem::FEM &f) { - return make_gravity_layout(f); - } - - template - void set_value_block( - mfem::Vector &vector, - const gravity_layout &layout, - const blocks::value_block block, - const mfem::Vector &values - ) { - REQUIRE(values.Size() == layout.size(block)); - for (int i = 0; i < values.Size(); ++i) - vector(layout.offset(block) + i) = values(i); - } - - template - mfem::Vector get_value_block( - const mfem::Vector &vector, - const gravity_layout &layout, - const blocks::value_block block - ) { - mfem::Vector values(layout.size(block)); - for (int i = 0; i < values.Size(); ++i) - values(i) = vector(layout.offset(block) + i); - return values; - } - - template - mfem::Vector get_residual_block( - const mfem::Vector &vector, - const gravity_layout &layout, - const blocks::residual_block block - ) { - mfem::Vector values(layout.size(block)); - for (int i = 0; i < values.Size(); ++i) - values(i) = vector(layout.offset(block) + i); - return values; - } - - template - void fill_value_block( - mfem::Vector &vector, - const gravity_layout &layout, - const blocks::value_block block, - const double scale, - const double phase - ) { - for (int i = 0; i < layout.size(block); ++i) { - const double index_value = static_cast(i + 1); - vector(layout.offset(block) + i) = - scale * (std::sin(0.31 * index_value + phase) + 0.37 * std::cos(0.17 * index_value - phase)); - } - } - - mfem::Vector make_gravity_jacobian_state( - const fem::FEM &f, - const gravity_layout &layout, - const bool deformed - ) { - mfem::Vector state(layout.value_offsets().Last()); - state = 0.0; - - fill_value_block(state, layout, density_block, 0.7, 0.11); - fill_value_block(state, layout, gravity_gradient_block, 0.4, 0.23); - fill_value_block(state, layout, gravity_potential_block, 0.3, 0.37); - - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - const auto displacement_map = field::make_field_dof_map(*f.displacementFes); - const mfem::Vector displacement = displacement_map.gather(make_stateless_reference_displacement(f, deformed)); - set_value_block(state, layout, displacement_block, displacement); - - return state; - } - - mfem::Vector make_density_direction(const gravity_layout &layout) { - mfem::Vector direction(layout.value_offsets().Last()); - direction = 0.0; - fill_value_block(direction, layout, density_block, 0.13, 0.19); - return direction; - } - - mfem::Vector make_gravity_gradient_direction(const gravity_layout &layout) { - mfem::Vector direction(layout.value_offsets().Last()); - direction = 0.0; - fill_value_block(direction, layout, gravity_gradient_block, 0.11, 0.29); - return direction; - } - - mfem::Vector make_gravity_potential_direction(const gravity_layout &layout) { - mfem::Vector direction(layout.value_offsets().Last()); - direction = 0.0; - fill_value_block(direction, layout, gravity_potential_block, 0.09, 0.41); - return direction; - } - - mfem::Vector make_combined_fixed_geometry_direction(const gravity_layout &layout) { - mfem::Vector direction = make_density_direction(layout); - const mfem::Vector gravity_gradient_direction = make_gravity_gradient_direction(layout); - const mfem::Vector gravity_potential_direction = make_gravity_potential_direction(layout); - direction += gravity_gradient_direction; - direction += gravity_potential_direction; - return direction; - } - - mfem::Vector make_displacement_direction( - const gravity_layout &layout, - const mfem::Vector &displacement_direction - ) { - mfem::Vector direction(layout.value_offsets().Last()); - direction = 0.0; - set_value_block(direction, layout, displacement_block, displacement_direction); - return direction; - } - - mfem::Vector evaluate_centered_difference( - operators::GravityFieldOperator &gravity_operator, - const mfem::Vector &state, - const mfem::Vector &direction, - const double step - ) { - mfem::Vector plus_state(state); - mfem::Vector minus_state(state); - plus_state.Add(step, direction); - minus_state.Add(-step, direction); - - mfem::Vector plus_residual; - mfem::Vector minus_residual; - gravity_operator.Mult(plus_state, plus_residual); - gravity_operator.Mult(minus_state, minus_residual); - - mfem::Vector difference(plus_residual); - difference -= minus_residual; - difference /= 2.0 * step; - return difference; - } - - struct HdivMassVariationTestFields { - mfem::Vector gravity_gradient; - mfem::Vector displacement; - mfem::Vector displacement_direction_1; - mfem::Vector displacement_direction_2; - }; - - double global_relative_error( - const mfem::Vector &computed, - const mfem::Vector &reference, - MPI_Comm communicator - ) { - REQUIRE(computed.Size() == reference.Size()); - - mfem::Vector difference(computed); - difference -= reference; - - return global_norm(difference, communicator) / std::max(global_norm(reference, communicator), 1.0e-14); - } - - HdivMassVariationTestFields make_hdiv_mass_variation_test_fields(const fem::FEM &f) { - const int dimension = f.mesh->Dimension(); - - auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.4 + 0.18 * position(0) - 0.07 * position(1) * position(2); - value(1) = -0.3 + 0.11 * position(1) + 0.05 * position(0) * position(2); - value(2) = 0.2 - 0.09 * position(2) + 0.04 * position(0) * position(1); - }; - - auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.025 * position(0) + 0.006 * position(1) * position(2); - value(1) = -0.018 * position(1) + 0.005 * position(0) * position(2); - value(2) = 0.014 * position(2) + 0.004 * position(0) * position(1); - }; - - auto displacement_direction_1_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.16 * position(0) + 0.03 * position(1); - value(1) = -0.11 * position(1) + 0.02 * position(2); - value(2) = 0.13 * position(2) - 0.025 * position(0); - }; - - auto displacement_direction_2_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = -0.07 * position(1) + 0.025 * position(2); - value(1) = 0.09 * position(0) + 0.04 * position(2); - value(2) = -0.08 * position(2) + 0.03 * position(0) * position(1); - }; - - mfem::VectorFunctionCoefficient gravity_gradient_coefficient(dimension, gravity_gradient_function); - mfem::VectorFunctionCoefficient displacement_coefficient(dimension, displacement_function); - mfem::VectorFunctionCoefficient displacement_direction_1_coefficient( - dimension, displacement_direction_1_function - ); - mfem::VectorFunctionCoefficient displacement_direction_2_coefficient( - dimension, displacement_direction_2_function - ); - - mfem::ParGridFunction gravity_gradient_grid(f.gravityFluxFes.get()); - mfem::ParGridFunction displacement_grid(f.displacementFes.get()); - mfem::ParGridFunction displacement_direction_1_grid(f.displacementFes.get()); - mfem::ParGridFunction displacement_direction_2_grid(f.displacementFes.get()); - - gravity_gradient_grid.ProjectCoefficient(gravity_gradient_coefficient); - displacement_grid.ProjectCoefficient(displacement_coefficient); - displacement_direction_1_grid.ProjectCoefficient(displacement_direction_1_coefficient); - displacement_direction_2_grid.ProjectCoefficient(displacement_direction_2_coefficient); - - HdivMassVariationTestFields fields; - gravity_gradient_grid.GetTrueDofs(fields.gravity_gradient); - displacement_grid.GetTrueDofs(fields.displacement); - displacement_direction_1_grid.GetTrueDofs(fields.displacement_direction_1); - displacement_direction_2_grid.GetTrueDofs(fields.displacement_direction_2); - - return fields; - } - - mfem::Vector centered_hdiv_mass_geometry_difference( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &gravity_gradient, - const mfem::Vector &displacement, - const mfem::Vector &displacement_direction, - const double difference_step - ) { - mfem::Vector plus_displacement(displacement); - mfem::Vector minus_displacement(displacement); - plus_displacement.Add(difference_step, displacement_direction); - minus_displacement.Add(-difference_step, displacement_direction); - - mfem::Vector plus_action; - mfem::Vector minus_action; - - operators::kernels::apply_mapped_hdiv_mass(f, domain_mapper, gravity_gradient, plus_displacement, plus_action); - operators::kernels::apply_mapped_hdiv_mass( - f, domain_mapper, gravity_gradient, minus_displacement, minus_action - ); - - plus_action -= minus_action; - plus_action /= 2.0 * difference_step; - - return plus_action; - } - - mfem::Vector make_source_variation_density(const fem::FEM &f) { - auto density_function = [](const mfem::Vector &position) { - return 1.2 + 0.16 * position(0) - 0.09 * position(1) + 0.07 * position(2) + - 0.04 * position(0) * position(1); - }; - - mfem::FunctionCoefficient density_coefficient(density_function); - mfem::ParGridFunction density_grid(f.densityFes.get()); - mfem::Vector density_true; - - density_grid.ProjectCoefficient(density_coefficient); - density_grid.GetTrueDofs(density_true); - - return density_true; - } - - mfem::Vector centered_source_geometry_difference( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::Vector &density, - const mfem::Vector &displacement, - const mfem::Vector &displacement_direction, - const double difference_step - ) { - mfem::Vector plus_displacement(displacement); - mfem::Vector minus_displacement(displacement); - plus_displacement.Add(difference_step, displacement_direction); - minus_displacement.Add(-difference_step, displacement_direction); - - mfem::Vector plus_action; - mfem::Vector minus_action; - - operators::kernels::apply_mapped_source(f, domain_mapper, density, plus_displacement, plus_action); - operators::kernels::apply_mapped_source(f, domain_mapper, density, minus_displacement, minus_action); - - plus_action -= minus_action; - plus_action /= 2.0 * difference_step; - - return plus_action; - } - double global_dot( - const mfem::Vector &left, - const mfem::Vector &right, - MPI_Comm communicator - ) { - REQUIRE(left.Size() > 0); - REQUIRE(left.Size() == right.Size()); - - const double local_dot = left * right; - double global_dot_value = 0.0; - MPI_Allreduce(&local_dot, &global_dot_value, 1, MPI_DOUBLE, MPI_SUM, communicator); - - return global_dot_value; - } - - mfem::Vector make_secondary_gravity_gradient(const fem::FEM &f) { - auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = -0.17 + 0.09 * position(1) + 0.03 * position(0) * position(2); - value(1) = 0.31 - 0.14 * position(0) + 0.05 * position(1) * position(2); - value(2) = -0.22 + 0.12 * position(2) - 0.04 * position(0) * position(1); - }; - - mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), gravity_gradient_function); - mfem::ParGridFunction grid_function(f.gravityFluxFes.get()); - mfem::Vector true_dofs; - - grid_function.ProjectCoefficient(coefficient); - grid_function.GetTrueDofs(true_dofs); - - return true_dofs; - } - - mfem::Vector make_secondary_source_density(const fem::FEM &f) { - auto density_function = [](const mfem::Vector &position) { - return 0.8 - 0.11 * position(0) + 0.13 * position(1) - 0.06 * position(2) + - 0.03 * position(1) * position(2); - }; - - mfem::FunctionCoefficient coefficient(density_function); - mfem::ParGridFunction grid_function(f.densityFes.get()); - mfem::Vector true_dofs; - - grid_function.ProjectCoefficient(coefficient); - grid_function.GetTrueDofs(true_dofs); - - return true_dofs; - } - - mfem::Vector make_vacuum_only_density( - const fem::FEM &f, - const int vacuum_attribute - ) { - mfem::ParGridFunction grid_function(f.densityFes.get()); - grid_function = 0.0; - - mfem::Array element_dofs; - mfem::Vector element_values; - - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - REQUIRE(transformation != nullptr); - - if (transformation->Attribute != vacuum_attribute) - continue; - - const mfem::FiniteElement &element = *f.densityFes->GetFE(element_id); - mfem::DofTransformation *dof_transformation = f.densityFes->GetElementDofs(element_id, element_dofs); - - element_values.SetSize(element.GetDof()); - for (int i = 0; i < element_values.Size(); ++i) - element_values(i) = 0.9 + 0.01 * static_cast(i); - - if (dof_transformation != nullptr) - dof_transformation->TransformPrimal(element_values); - grid_function.SetSubVector(element_dofs, element_values); - } - - mfem::Vector true_dofs; - grid_function.GetTrueDofs(true_dofs); - - return true_dofs; - } - - template - mfem::Vector make_read_only_value_view( - const mfem::Vector &vector, - const mfem::Array &offsets, - const blocks::value_block - ) { - const int offset = offsets[index]; - const int size = offsets[index + 1] - offset; - return mfem::Vector(const_cast(vector.GetData()) + offset, size); - } - - void check_linearization_context_matches_state( - const operators::context::gravity_field::GravityFieldLinearizationContext &context, - const mfem::Vector &state, - const mfem::Array &state_offsets, - MPI_Comm communicator - ) { - using form = blocks::gravity_field_form; - - constexpr auto density_block = utils::blocks::get_value_block(blocks::density_field.mass_term); - constexpr auto displacement_block = - utils::blocks::get_value_block(blocks::displacement_field.geometry_term); - constexpr auto gravity_gradient_block = - utils::blocks::get_value_block(blocks::gravity_field.gradient_term); - - const mfem::Vector density = make_read_only_value_view(state, state_offsets, density_block); - const mfem::Vector displacement = make_read_only_value_view(state, state_offsets, displacement_block); - const mfem::Vector gravity_gradient = make_read_only_value_view(state, state_offsets, gravity_gradient_block); - - CHECK_THAT( - global_relative_vector_error( - context.GetDensityTrue(), context.GetDensityMap().scatter(density), communicator - ), - Catch::Matchers::WithinAbs(0.0, 0.0) - ); - CHECK_THAT( - global_relative_vector_error( - context.GetGeometryContext().GetDisplacementTrue(), context.GetDisplacementMap().scatter(displacement), - communicator - ), - Catch::Matchers::WithinAbs(0.0, 0.0) - ); - CHECK_THAT( - global_relative_vector_error( - context.GetGravityGradientTrue(), context.GetGravityGradientMap().scatter(gravity_gradient), - communicator - ), - Catch::Matchers::WithinAbs(0.0, 0.0) - ); - } - - StatelessHDivMassReference assemble_legacy_hdiv_mass_reference( - const fem::FEM &f, - const mfem::Vector &gravity_gradient_true - ) { - MFEM_VERIFY(f.mesh != nullptr, "The legacy H(div) reference requires a mesh."); - MFEM_VERIFY(f.gravityFluxFes != nullptr, "The legacy H(div) reference requires the RT finite-element space."); - MFEM_VERIFY(f.mapping != nullptr, "The legacy H(div) reference requires the legacy domain mapper."); - MFEM_VERIFY(f.domainMapperStateless != nullptr, "The vacuum attribute is unavailable."); - - mfem::Vector gravity_gradient_local; - reference_true_to_local(*f.gravityFluxFes, gravity_gradient_true, gravity_gradient_local); - - mfem::Vector total_local(f.gravityFluxFes->GetVSize()); - mfem::Vector stellar_local(f.gravityFluxFes->GetVSize()); - mfem::Vector vacuum_local(f.gravityFluxFes->GetVSize()); - - total_local = 0.0; - stellar_local = 0.0; - vacuum_local = 0.0; - - StatelessHDivMassReference reference; - const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute(); - - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - const mfem::FiniteElement &gravity_element = *f.gravityFluxFes->GetFE(element_id); - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - - MFEM_VERIFY( - transformation != nullptr, "The legacy H(div) reference received a null element " - "transformation." - ); - - const bool is_vacuum = transformation->Attribute == vacuum_attribute; - - mfem::Array gravity_dofs; - f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); - - mfem::Vector element_gravity_gradient; - gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); - - const int gravity_dof_count = gravity_element.GetDof(); - const int dimension = transformation->GetSpaceDim(); - - mfem::DenseMatrix element_matrix(gravity_dof_count); - mfem::DenseMatrix vector_shape(gravity_dof_count, dimension); - mfem::DenseMatrix mapping_jacobian(dimension); - mfem::DenseMatrix mapped_mass_tensor(dimension); - - element_matrix = 0.0; - - const mfem::IntegrationRule &integration_rule = - get_stateless_hdiv_reference_rule(f, gravity_element, *transformation); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); - - /* - * This reproduces the legacy MappedHDivMassCoefficient. - * - * In particular, it does not evaluate or otherwise use - * f.compactificationCoordinate. - */ - f.mapping->ComputeJacobian(*transformation, mapping_jacobian); - - const double mapping_determinant = mapping_jacobian.Det(); - - CAPTURE(element_id, q, transformation->Attribute); - REQUIRE(std::isfinite(mapping_determinant)); - REQUIRE(mapping_determinant > 0.0); - - mfem::MultAtB(mapping_jacobian, mapping_jacobian, mapped_mass_tensor); - mapped_mass_tensor *= 1.0 / std::abs(mapping_determinant); - - gravity_element.CalcVShape(*transformation, vector_shape); - - const double weight = integration_point.weight * transformation->Weight(); - - for (int i = 0; i < gravity_dof_count; ++i) { - for (int j = 0; j < gravity_dof_count; ++j) { - double entry = 0.0; - - for (int row = 0; row < dimension; ++row) { - for (int column = 0; column < dimension; ++column) { - entry += - vector_shape(i, row) * mapped_mass_tensor(row, column) * vector_shape(j, column); - } - } - - element_matrix(i, j) += weight * entry; - } - } - - if (is_vacuum) { - ++reference.vacuum_quadrature_points; - } else { - ++reference.stellar_quadrature_points; - } - } - - mfem::Vector element_action(gravity_dof_count); - element_matrix.Mult(element_gravity_gradient, element_action); - - total_local.AddElementVector(gravity_dofs, element_action); - - if (is_vacuum) { - vacuum_local.AddElementVector(gravity_dofs, element_action); - ++reference.vacuum_elements; - } else { - stellar_local.AddElementVector(gravity_dofs, element_action); - ++reference.stellar_elements; - } - } - - reference_local_to_true(*f.gravityFluxFes, total_local, reference.total_action); - reference_local_to_true(*f.gravityFluxFes, stellar_local, reference.stellar_action); - reference_local_to_true(*f.gravityFluxFes, vacuum_local, reference.vacuum_action); - - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - const long long local_counts[4]{ - reference.stellar_elements, reference.vacuum_elements, reference.stellar_quadrature_points, - reference.vacuum_quadrature_points - }; - - long long global_counts[4]{}; - - MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, communicator); - - reference.stellar_elements = global_counts[0]; - reference.vacuum_elements = global_counts[1]; - reference.stellar_quadrature_points = global_counts[2]; - reference.vacuum_quadrature_points = global_counts[3]; - - return reference; - } } // namespace -TEST_CASE( - "Gravity Field Operator Mult Preserves Zero State And Static Blocks", - tags::gravity_operator_unit -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Gravity Field Operator Mult Preserves Zero State And Static Blocks", + tags::gravity_operator_unit) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - physics::update_stiffness_matrix(f); + REQUIRE(f.domainMapperStateless != nullptr); + const blocks::form_layout layout = make_gravity_layout(f); - REQUIRE(f.domainMapperStateless != nullptr); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - const blocks::form_layout layout = make_gravity_layout(f); + mfem::Vector state(layout.value_offsets().Last()); + mfem::Vector residual; + state = 0.0; + constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + gravity_operator.Prepare(state, revisions); + gravity_operator.Mult(state, residual); - mfem::Vector state(layout.value_offsets().Last()); - mfem::Vector residual; - state = 0.0; - constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + REQUIRE(residual.Size() == layout.residual_offsets().Last()); + CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-14)); - gravity_operator.Prepare(state, revisions); - gravity_operator.Mult(state, residual); + const mfem::Vector gravity_potential = + make_test_vector(layout.size(gravity_potential_block), 0.31); + set_block(state, layout.value_offsets(), gravity_potential_block, + gravity_potential); - REQUIRE(residual.Size() == layout.residual_offsets().Last()); - CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-14)); + gravity_operator.Mult(state, residual); - const mfem::Vector gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.31); - set_block(state, layout.value_offsets(), gravity_potential_block, gravity_potential); + const mfem::Vector gradient_residual = get_block( + residual, layout.residual_offsets(), gravity_gradient_residual_block); + const mfem::Vector poisson_residual = get_block( + residual, layout.residual_offsets(), gravity_poisson_residual_block); - gravity_operator.Mult(state, residual); + const auto &geometry_context = linearization_context.GetGeometryContext(); + const auto &flux_map = linearization_context.GetGravityGradientMap(); + const auto &potential_map = linearization_context.GetGravityPotentialMap(); - const mfem::Vector gradient_residual = - get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block); - const mfem::Vector poisson_residual = - get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block); + const mfem::Vector gravity_potential_true = + potential_map.scatter(gravity_potential); + mfem::Vector expected_gradient_residual_true(flux_map.full_size()); + geometry_context.GetTransposeDivergenceOperator().Mult( + gravity_potential_true, expected_gradient_residual_true); + const mfem::Vector expected_gradient_residual = + flux_map.gather(expected_gradient_residual_true); - mfem::Vector expected_gradient_residual(layout.size(gravity_gradient_residual_block)); - f.gravityContext.BT->Mult(gravity_potential, expected_gradient_residual); + CHECK_THAT(relative_difference(gradient_residual, expected_gradient_residual), + WithinAbs(0.0, 1.0e-13)); + CHECK_THAT(poisson_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); - CHECK_THAT(relative_difference(gradient_residual, expected_gradient_residual), WithinAbs(0.0, 1.0e-13)); - CHECK_THAT(poisson_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); + state = 0.0; - state = 0.0; + const mfem::Vector gravity_gradient = + make_test_vector(layout.size(gravity_gradient_block), 0.73); + set_block(state, layout.value_offsets(), gravity_gradient_block, + gravity_gradient); - const mfem::Vector gravity_gradient = make_test_vector(layout.size(gravity_gradient_block), 0.73); - set_block(state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); + gravity_operator.Mult(state, residual); - gravity_operator.Mult(state, residual); + const mfem::Vector gradient_only_residual = get_block( + residual, layout.residual_offsets(), gravity_gradient_residual_block); + const mfem::Vector poisson_only_residual = get_block( + residual, layout.residual_offsets(), gravity_poisson_residual_block); - const mfem::Vector gradient_only_residual = - get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block); - const mfem::Vector poisson_only_residual = - get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block); + const mfem::Vector gravity_gradient_true = flux_map.scatter(gravity_gradient); + mfem::Vector expected_poisson_residual_true(potential_map.full_size()); + geometry_context.GetDivergenceOperator().Mult(gravity_gradient_true, + expected_poisson_residual_true); + const mfem::Vector expected_poisson_residual = + potential_map.gather(expected_poisson_residual_true); - mfem::Vector expected_poisson_residual(layout.size(gravity_poisson_residual_block)); - f.gravityContext.b_form->Mult(gravity_gradient, expected_poisson_residual); + CHECK(gradient_only_residual.Norml2() > 0.0); + CHECK_THAT( + relative_difference(poisson_only_residual, expected_poisson_residual), + WithinAbs(0.0, 1.0e-13)); - CHECK(gradient_only_residual.Norml2() > 0.0); - CHECK_THAT(relative_difference(poisson_only_residual, expected_poisson_residual), WithinAbs(0.0, 1.0e-13)); + const double adjoint_lhs = gravity_gradient * expected_gradient_residual; + const double adjoint_rhs = gravity_potential * expected_poisson_residual; + const double adjoint_scale = + std::max({std::abs(adjoint_lhs), std::abs(adjoint_rhs), 1.0e-14}); - const double adjoint_lhs = gravity_gradient * expected_gradient_residual; - const double adjoint_rhs = gravity_potential * expected_poisson_residual; - const double adjoint_scale = std::max({std::abs(adjoint_lhs), std::abs(adjoint_rhs), 1.0e-14}); - - CHECK_THAT(std::abs(adjoint_lhs - adjoint_rhs) / adjoint_scale, WithinAbs(0.0, 1.0e-12)); + CHECK_THAT(std::abs(adjoint_lhs - adjoint_rhs) / adjoint_scale, + WithinAbs(0.0, 1.0e-12)); } - TEST_CASE( "Gravity Field Operator Mult Applies Stellar Source With Correct Sign", - tags::gravity_operator_unit -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + tags::gravity_operator_unit) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - physics::update_stiffness_matrix(f); + REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + const blocks::form_layout layout = make_gravity_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - const blocks::form_layout layout = make_gravity_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + mfem::Vector state(layout.value_offsets().Last()); + mfem::Vector residual; + state = 0.0; + constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - mfem::Vector state(layout.value_offsets().Last()); - mfem::Vector residual; - state = 0.0; - constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + const mfem::Vector density = linearization_context.GetDensityMap().gather( + make_constant_density(f, 1.0)); + set_block(state, layout.value_offsets(), density_block, density); - const mfem::Vector density = linearization_context.GetDensityMap().gather(make_constant_density(f, 1.0)); - set_block(state, layout.value_offsets(), density_block, density); + gravity_operator.Prepare(state, revisions); + gravity_operator.Mult(state, residual); - gravity_operator.Prepare(state, revisions); - gravity_operator.Mult(state, residual); + const mfem::Vector gradient_residual = get_block( + residual, layout.residual_offsets(), gravity_gradient_residual_block); + const mfem::Vector poisson_residual = get_block( + residual, layout.residual_offsets(), gravity_poisson_residual_block); - const mfem::Vector gradient_residual = - get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block); - const mfem::Vector poisson_residual = - get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block); + CHECK_THAT(gradient_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); + CHECK(poisson_residual.Norml2() > 0.0); - CHECK_THAT(gradient_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); - CHECK(poisson_residual.Norml2() > 0.0); + const mfem::Vector constant_test = make_constant_density(f, 1.0); + const double integrated_source_residual = constant_test * poisson_residual; - const mfem::Vector constant_test = make_constant_density(f, 1.0); - const double integrated_source_residual = constant_test * poisson_residual; + INFO("Integrated Poisson source residual = " << integrated_source_residual); + CHECK(integrated_source_residual < 0.0); - INFO("Integrated Poisson source residual = " << integrated_source_residual); - CHECK(integrated_source_residual < 0.0); + mfem::Vector doubled_state(state); + mfem::Vector doubled_density(density); + doubled_density *= 2.0; + set_block(doubled_state, layout.value_offsets(), density_block, + doubled_density); - mfem::Vector doubled_state(state); - mfem::Vector doubled_density(density); - doubled_density *= 2.0; - set_block(doubled_state, layout.value_offsets(), density_block, doubled_density); + mfem::Vector doubled_residual; + gravity_operator.Mult(doubled_state, doubled_residual); - mfem::Vector doubled_residual; - gravity_operator.Mult(doubled_state, doubled_residual); + mfem::Vector expected_doubled_residual(residual); + expected_doubled_residual *= 2.0; - mfem::Vector expected_doubled_residual(residual); - expected_doubled_residual *= 2.0; - - CHECK_THAT(relative_difference(doubled_residual, expected_doubled_residual), WithinAbs(0.0, 1.0e-12)); + CHECK_THAT(relative_difference(doubled_residual, expected_doubled_residual), + WithinAbs(0.0, 1.0e-12)); } -TEST_CASE( - "Gravity Field Operator Mult Ignores Vacuum Density", - tags::gravity_operator_unit -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Gravity Field Operator Mult Ignores Vacuum Density", + tags::gravity_operator_unit) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - physics::update_stiffness_matrix(f); + REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + const blocks::form_layout layout = make_gravity_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - const blocks::form_layout layout = make_gravity_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + mfem::Vector state(layout.value_offsets().Last()); + mfem::Vector residual; + state = 0.0; - mfem::Vector state(layout.value_offsets().Last()); - mfem::Vector residual; - state = 0.0; + const operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + const mfem::Vector vacuum_density_true = make_vacuum_density(f, 1.0); + REQUIRE(vacuum_density_true.Norml2() > 0.0); + const mfem::Vector vacuum_density = + linearization_context.GetDensityMap().gather(vacuum_density_true); - const mfem::Vector vacuum_density_true = make_vacuum_density(f, 1.0); - REQUIRE(vacuum_density_true.Norml2() > 0.0); - const mfem::Vector vacuum_density = linearization_context.GetDensityMap().gather(vacuum_density_true); + set_block(state, layout.value_offsets(), density_block, vacuum_density); - set_block(state, layout.value_offsets(), density_block, vacuum_density); + gravity_operator.Prepare(state, revisions); + gravity_operator.Mult(state, residual); - gravity_operator.Prepare(state, revisions); - gravity_operator.Mult(state, residual); - - INFO("Vacuum-source residual norm = " << residual.Norml2()); - CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-13)); + INFO("Vacuum-source residual norm = " << residual.Norml2()); + CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-13)); } -TEST_CASE( - "Gravity Field Operator Mult Produces A Symmetric Positive Hdiv Mass " - "Action", - tags::gravity_operator_unit -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Gravity Field Operator Mult Produces A Symmetric Positive Hdiv Mass " + "Action", + tags::gravity_operator_unit) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - physics::update_stiffness_matrix(f); + REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + const blocks::form_layout layout = make_gravity_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - const blocks::form_layout layout = make_gravity_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + const mfem::Vector displacement = + linearization_context.GetDisplacementMap().gather(make_displacement(f)); + const mfem::Vector gravity_gradient_a = + make_test_vector(layout.size(gravity_gradient_block), 0.27); + const mfem::Vector gravity_gradient_b = + make_test_vector(layout.size(gravity_gradient_block), 1.13); - const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(make_displacement(f)); - const mfem::Vector gravity_gradient_a = make_test_vector(layout.size(gravity_gradient_block), 0.27); - const mfem::Vector gravity_gradient_b = make_test_vector(layout.size(gravity_gradient_block), 1.13); + mfem::Vector state_a(layout.value_offsets().Last()); + mfem::Vector state_b(layout.value_offsets().Last()); + mfem::Vector residual_a; + mfem::Vector residual_b; - mfem::Vector state_a(layout.value_offsets().Last()); - mfem::Vector state_b(layout.value_offsets().Last()); - mfem::Vector residual_a; - mfem::Vector residual_b; + state_a = 0.0; + state_b = 0.0; - state_a = 0.0; - state_b = 0.0; + set_block(state_a, layout.value_offsets(), displacement_block, displacement); + set_block(state_a, layout.value_offsets(), gravity_gradient_block, + gravity_gradient_a); + set_block(state_b, layout.value_offsets(), displacement_block, displacement); + set_block(state_b, layout.value_offsets(), gravity_gradient_block, + gravity_gradient_b); - set_block(state_a, layout.value_offsets(), displacement_block, displacement); - set_block(state_a, layout.value_offsets(), gravity_gradient_block, gravity_gradient_a); - set_block(state_b, layout.value_offsets(), displacement_block, displacement); - set_block(state_b, layout.value_offsets(), gravity_gradient_block, gravity_gradient_b); + const operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + gravity_operator.Prepare(state_a, revisions); - gravity_operator.Prepare(state_a, revisions); + gravity_operator.Mult(state_a, residual_a); + gravity_operator.Mult(state_b, residual_b); - gravity_operator.Mult(state_a, residual_a); - gravity_operator.Mult(state_b, residual_b); + const mfem::Vector mass_action_a = get_block( + residual_a, layout.residual_offsets(), gravity_gradient_residual_block); + const mfem::Vector mass_action_b = get_block( + residual_b, layout.residual_offsets(), gravity_gradient_residual_block); - const mfem::Vector mass_action_a = - get_block(residual_a, layout.residual_offsets(), gravity_gradient_residual_block); - const mfem::Vector mass_action_b = - get_block(residual_b, layout.residual_offsets(), gravity_gradient_residual_block); + const double energy_a = gravity_gradient_a * mass_action_a; + const double energy_b = gravity_gradient_b * mass_action_b; + const double cross_ab = gravity_gradient_a * mass_action_b; + const double cross_ba = gravity_gradient_b * mass_action_a; + const double symmetry_scale = + std::max({std::abs(cross_ab), std::abs(cross_ba), 1.0e-14}); - const double energy_a = gravity_gradient_a * mass_action_a; - const double energy_b = gravity_gradient_b * mass_action_b; - const double cross_ab = gravity_gradient_a * mass_action_b; - const double cross_ba = gravity_gradient_b * mass_action_a; - const double symmetry_scale = std::max({std::abs(cross_ab), std::abs(cross_ba), 1.0e-14}); + INFO("Mapped H(div) energy A = " << energy_a); + INFO("Mapped H(div) energy B = " << energy_b); + INFO("Mapped H(div) cross action A-M-B = " << cross_ab); + INFO("Mapped H(div) cross action B-M-A = " << cross_ba); - INFO("Mapped H(div) energy A = " << energy_a); - INFO("Mapped H(div) energy B = " << energy_b); - INFO("Mapped H(div) cross action A-M-B = " << cross_ab); - INFO("Mapped H(div) cross action B-M-A = " << cross_ba); - - CHECK(std::isfinite(energy_a)); - CHECK(std::isfinite(energy_b)); - CHECK(energy_a > 0.0); - CHECK(energy_b > 0.0); - CHECK_THAT(std::abs(cross_ab - cross_ba) / symmetry_scale, WithinAbs(0.0, 1.0e-11)); + CHECK(std::isfinite(energy_a)); + CHECK(std::isfinite(energy_b)); + CHECK(energy_a > 0.0); + CHECK(energy_b > 0.0); + CHECK_THAT(std::abs(cross_ab - cross_ba) / symmetry_scale, + WithinAbs(0.0, 1.0e-11)); } TEST_CASE( "Gravity Field Operator Mult Is Additive In Fields At Fixed Displacement", - tags::gravity_operator_unit -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + tags::gravity_operator_unit) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - physics::update_stiffness_matrix(f); + REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + const blocks::form_layout layout = make_gravity_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - const blocks::form_layout layout = make_gravity_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + const mfem::Vector displacement = + linearization_context.GetDisplacementMap().gather(make_displacement(f)); + const mfem::Vector density = linearization_context.GetDensityMap().gather( + make_constant_density(f, 0.73)); + const mfem::Vector gravity_gradient = + make_test_vector(layout.size(gravity_gradient_block), 0.41); + const mfem::Vector gravity_potential = + make_test_vector(layout.size(gravity_potential_block), 0.89); - const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(make_displacement(f)); - const mfem::Vector density = linearization_context.GetDensityMap().gather(make_constant_density(f, 0.73)); - const mfem::Vector gravity_gradient = make_test_vector(layout.size(gravity_gradient_block), 0.41); - const mfem::Vector gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.89); + mfem::Vector displacement_state(layout.value_offsets().Last()); + mfem::Vector density_state(layout.value_offsets().Last()); + mfem::Vector gradient_state(layout.value_offsets().Last()); + mfem::Vector potential_state(layout.value_offsets().Last()); + mfem::Vector complete_state(layout.value_offsets().Last()); - mfem::Vector displacement_state(layout.value_offsets().Last()); - mfem::Vector density_state(layout.value_offsets().Last()); - mfem::Vector gradient_state(layout.value_offsets().Last()); - mfem::Vector potential_state(layout.value_offsets().Last()); - mfem::Vector complete_state(layout.value_offsets().Last()); + displacement_state = 0.0; + density_state = 0.0; + gradient_state = 0.0; + potential_state = 0.0; + complete_state = 0.0; - displacement_state = 0.0; - density_state = 0.0; - gradient_state = 0.0; - potential_state = 0.0; - complete_state = 0.0; + set_block(displacement_state, layout.value_offsets(), displacement_block, + displacement); - set_block(displacement_state, layout.value_offsets(), displacement_block, displacement); + set_block(density_state, layout.value_offsets(), displacement_block, + displacement); + set_block(density_state, layout.value_offsets(), density_block, density); - set_block(density_state, layout.value_offsets(), displacement_block, displacement); - set_block(density_state, layout.value_offsets(), density_block, density); + set_block(gradient_state, layout.value_offsets(), displacement_block, + displacement); + set_block(gradient_state, layout.value_offsets(), gravity_gradient_block, + gravity_gradient); - set_block(gradient_state, layout.value_offsets(), displacement_block, displacement); - set_block(gradient_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); + set_block(potential_state, layout.value_offsets(), displacement_block, + displacement); + set_block(potential_state, layout.value_offsets(), gravity_potential_block, + gravity_potential); - set_block(potential_state, layout.value_offsets(), displacement_block, displacement); - set_block(potential_state, layout.value_offsets(), gravity_potential_block, gravity_potential); + set_block(complete_state, layout.value_offsets(), displacement_block, + displacement); + set_block(complete_state, layout.value_offsets(), density_block, density); + set_block(complete_state, layout.value_offsets(), gravity_gradient_block, + gravity_gradient); + set_block(complete_state, layout.value_offsets(), gravity_potential_block, + gravity_potential); - set_block(complete_state, layout.value_offsets(), displacement_block, displacement); - set_block(complete_state, layout.value_offsets(), density_block, density); - set_block(complete_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); - set_block(complete_state, layout.value_offsets(), gravity_potential_block, gravity_potential); + mfem::Vector displacement_residual; + mfem::Vector density_residual; + mfem::Vector gradient_residual; + mfem::Vector potential_residual; + mfem::Vector complete_residual; + const operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - mfem::Vector displacement_residual; - mfem::Vector density_residual; - mfem::Vector gradient_residual; - mfem::Vector potential_residual; - mfem::Vector complete_residual; - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + gravity_operator.Prepare(complete_state, revisions); - gravity_operator.Prepare(complete_state, revisions); + gravity_operator.Mult(displacement_state, displacement_residual); + gravity_operator.Mult(density_state, density_residual); + gravity_operator.Mult(gradient_state, gradient_residual); + gravity_operator.Mult(potential_state, potential_residual); + gravity_operator.Mult(complete_state, complete_residual); - gravity_operator.Mult(displacement_state, displacement_residual); - gravity_operator.Mult(density_state, density_residual); - gravity_operator.Mult(gradient_state, gradient_residual); - gravity_operator.Mult(potential_state, potential_residual); - gravity_operator.Mult(complete_state, complete_residual); + CHECK_THAT(displacement_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); - CHECK_THAT(displacement_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); + mfem::Vector additive_residual(density_residual); + additive_residual += gradient_residual; + additive_residual += potential_residual; - mfem::Vector additive_residual(density_residual); - additive_residual += gradient_residual; - additive_residual += potential_residual; - - CHECK_THAT(relative_difference(complete_residual, additive_residual), WithinAbs(0.0, 1.0e-11)); -} - -TEST_CASE( - "Gravity Field Operator Stellar Hdiv Mass Matches Legacy Assembled Mass", - tags::gravity_legacy -) { - constexpr double parity_tolerance = 1.0e-10; - constexpr double vacuum_tolerance = 1.0e-13; - - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); - - const blocks::form_layout layout = make_gravity_layout(f); - const mfem::Vector gravity_gradient = make_core_supported_gravity_gradient(f); - const mfem::Vector deformed_displacement = make_displacement(f); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - REQUIRE(global_norm(gravity_gradient, communicator) > 0.0); - - mfem::ParGridFunction local_gravity_gradient(f.gravityFluxFes.get()); - local_gravity_gradient.SetFromTrueDofs(gravity_gradient); - - double local_maximum_vacuum_dof = 0.0; - mfem::Array element_vdofs; - mfem::Vector element_values; - - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - if (f.mesh->GetAttribute(element_id) != f.domainMapperStateless->GetVacuumElementAttribute()) - continue; - - f.gravityFluxFes->GetElementVDofs(element_id, element_vdofs); - local_gravity_gradient.GetSubVector(element_vdofs, element_values); - - if (element_values.Size() > 0) - local_maximum_vacuum_dof = std::max(local_maximum_vacuum_dof, element_values.Normlinf()); - } - - double global_maximum_vacuum_dof = 0.0; - MPI_Allreduce(&local_maximum_vacuum_dof, &global_maximum_vacuum_dof, 1, MPI_DOUBLE, MPI_MAX, communicator); - - INFO("Maximum gravity-gradient DOF on vacuum elements = " << global_maximum_vacuum_dof); - REQUIRE_THAT(global_maximum_vacuum_dof, WithinAbs(0.0, vacuum_tolerance)); - - for (const bool use_deformation : std::array{false, true}) { - DYNAMIC_SECTION("Geometry = " << (use_deformation ? "deformed" : "identity")) { - mfem::Vector displacement(layout.size(displacement_block)); - displacement = 0.0; - if (use_deformation) - displacement = deformed_displacement; - - mfem::ParGridFunction legacy_displacement(f.displacementFes.get()); - legacy_displacement.SetFromTrueDofs(displacement); - f.mapping->SetDisplacement(legacy_displacement); - - physics::update_stiffness_matrix(f); - - REQUIRE(f.gravityContext.m_form != nullptr); - REQUIRE(f.gravityContext.b_form != nullptr); - REQUIRE(f.gravityContext.BT != nullptr); - - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); - - mfem::Vector state(layout.value_offsets().Last()); - mfem::Vector matrix_free_residual; - state = 0.0; - - set_block(state, layout.value_offsets(), displacement_block, displacement); - set_block(state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); - - constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, - .displacement = {1}, - .density = {1}, - .gravity_gradient = {1}, - .gravity_potential = {1} - }; - - gravity_operator.Prepare(state, revisions); - gravity_operator.Mult(state, matrix_free_residual); - - const mfem::Vector matrix_free_mass_action = - get_block(matrix_free_residual, layout.residual_offsets(), gravity_gradient_residual_block); - - mfem::Vector legacy_mass_action(f.gravityFluxFes->GetTrueVSize()); - f.gravityContext.m_form->Mult(gravity_gradient, legacy_mass_action); - - const double matrix_free_mass_norm = global_norm(matrix_free_mass_action, communicator); - const double legacy_mass_norm = global_norm(legacy_mass_action, communicator); - const double mass_error = - global_relative_difference(matrix_free_mass_action, legacy_mass_action, communicator); - - mfem::Vector mass_difference(matrix_free_mass_action); - mass_difference -= legacy_mass_action; - - INFO("Geometry = " << (use_deformation ? "deformed" : "identity")); - INFO("Matrix-free stellar mass norm = " << matrix_free_mass_norm); - INFO("Legacy stellar mass norm = " << legacy_mass_norm); - INFO("Absolute stellar mass difference norm = " << global_norm(mass_difference, communicator)); - INFO("Relative stellar mass-action error = " << mass_error); - - REQUIRE(matrix_free_mass_norm > 0.0); - REQUIRE(legacy_mass_norm > 0.0); - CHECK_THAT(mass_error, WithinAbs(0.0, parity_tolerance)); - } - } + CHECK_THAT(relative_difference(complete_residual, additive_residual), + WithinAbs(0.0, 1.0e-11)); } TEST_CASE( "Gravity Field Operator Hdiv Mass Action Matches Stateless Quadrature " "Reference", - tags::gravity_operator_integration -) { - const utils::Args args = test_utils::setup_args(); + tags::gravity_operator_integration) { + const utils::Args args = test_utils::setup_args(); - fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); + fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); - fem.mapping->ResetDisplacement(); + *fem.displacement = 0.0; - physics::update_stiffness_matrix(fem); + constexpr auto displacement_block = + mean_field::utils::blocks::get_value_block( + blocks::displacement_field.geometry_term); - constexpr auto displacement_block = mean_field::utils::blocks::get_value_block( - blocks::displacement_field.geometry_term - ); + constexpr auto gravity_gradient_block = + mean_field::utils::blocks::get_value_block( + blocks::gravity_field.gradient_term); - constexpr auto gravity_gradient_block = - mean_field::utils::blocks::get_value_block(blocks::gravity_field.gradient_term); + constexpr auto gravity_gradient_residual_block = + mean_field::utils::blocks::get_residual_block( + blocks::gravity_field.gradient_term); - constexpr auto gravity_gradient_residual_block = - mean_field::utils::blocks::get_residual_block(blocks::gravity_field.gradient_term); + const blocks::form_layout layout = + make_gravity_layout(fem); - const blocks::form_layout layout = make_gravity_layout(fem); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(fem, *fem.domainMapperStateless); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - fem, *fem.domainMapperStateless - ); + operators::GravityFieldJacobianOperator gravity_jacobian( + fem, *fem.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); - operators::GravityFieldJacobianOperator gravity_jacobian( - fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); + operators::GravityFieldOperator gravity_operator( + fem, *fem.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - operators::GravityFieldOperator gravity_operator( - fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); - const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); + const mfem::Vector gravity_gradient_true = + make_full_support_gravity_gradient(fem); - const mfem::Vector gravity_gradient_true = make_full_support_gravity_gradient(fem); + for (const bool deformed : {false, true}) { + DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { + CAPTURE(deformed); - for (const bool deformed : {false, true}) { - DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { - CAPTURE(deformed); + const mfem::Vector displacement_true = + make_stateless_reference_displacement(fem, deformed); - const mfem::Vector displacement_true = make_stateless_reference_displacement(fem, deformed); + mfem::Vector state(layout.value_offsets().Last()); + state = 0.0; - mfem::Vector state(layout.value_offsets().Last()); - state = 0.0; + for (int i = 0; i < gravity_gradient_true.Size(); ++i) { + state(layout.offset(gravity_gradient_block) + i) = + gravity_gradient_true(i); + } - for (int i = 0; i < gravity_gradient_true.Size(); ++i) { - state(layout.offset(gravity_gradient_block) + i) = gravity_gradient_true(i); - } + for (int i = 0; i < displacement_true.Size(); ++i) { + state(layout.offset(displacement_block) + i) = displacement_true(i); + } - for (int i = 0; i < displacement_true.Size(); ++i) { - state(layout.offset(displacement_block) + i) = displacement_true(i); - } + const operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, - .displacement = {1}, - .density = {1}, - .gravity_gradient = {1}, - .gravity_potential = {1} - }; + gravity_operator.Prepare(state, revisions); - gravity_operator.Prepare(state, revisions); + mfem::Vector residual; - mfem::Vector residual; + gravity_operator.Mult(state, residual); - gravity_operator.Mult(state, residual); + mfem::Vector operator_action( + layout.size(gravity_gradient_residual_block)); - mfem::Vector operator_action(layout.size(gravity_gradient_residual_block)); + for (int i = 0; i < operator_action.Size(); ++i) { + operator_action(i) = + residual(layout.offset(gravity_gradient_residual_block) + i); + } - for (int i = 0; i < operator_action.Size(); ++i) { - operator_action(i) = residual(layout.offset(gravity_gradient_residual_block) + i); - } + const StatelessHDivMassReference reference = + evaluate_stateless_hdiv_mass_quadrature_reference( + fem, gravity_gradient_true, displacement_true); - const StatelessHDivMassReference reference = - evaluate_stateless_hdiv_mass_quadrature_reference(fem, gravity_gradient_true, displacement_true); + mfem::Vector decomposed_reference(reference.stellar_action); - mfem::Vector decomposed_reference(reference.stellar_action); + decomposed_reference += reference.vacuum_action; - decomposed_reference += reference.vacuum_action; + mfem::Vector action_difference(operator_action); - mfem::Vector action_difference(operator_action); + action_difference -= reference.total_action; - action_difference -= reference.total_action; + const double operator_norm = + global_vector_norm(operator_action, communicator); - const double operator_norm = global_vector_norm(operator_action, communicator); + const double reference_norm = + global_vector_norm(reference.total_action, communicator); - const double reference_norm = global_vector_norm(reference.total_action, communicator); + const double stellar_action_norm = + global_vector_norm(reference.stellar_action, communicator); - const double stellar_action_norm = global_vector_norm(reference.stellar_action, communicator); + const double vacuum_action_norm = + global_vector_norm(reference.vacuum_action, communicator); - const double vacuum_action_norm = global_vector_norm(reference.vacuum_action, communicator); + const double absolute_action_error = + global_vector_norm(action_difference, communicator); - const double absolute_action_error = global_vector_norm(action_difference, communicator); + const double relative_action_error = global_relative_vector_error( + operator_action, reference.total_action, communicator); - const double relative_action_error = - global_relative_vector_error(operator_action, reference.total_action, communicator); + const double decomposition_error = global_relative_vector_error( + reference.total_action, decomposed_reference, communicator); - const double decomposition_error = - global_relative_vector_error(reference.total_action, decomposed_reference, communicator); + const double operator_energy = global_vector_dot( + gravity_gradient_true, operator_action, communicator); - const double operator_energy = global_vector_dot(gravity_gradient_true, operator_action, communicator); + const double reference_energy = global_vector_dot( + gravity_gradient_true, reference.total_action, communicator); - const double reference_energy = - global_vector_dot(gravity_gradient_true, reference.total_action, communicator); + const double stellar_energy = global_vector_dot( + gravity_gradient_true, reference.stellar_action, communicator); - const double stellar_energy = - global_vector_dot(gravity_gradient_true, reference.stellar_action, communicator); + const double vacuum_energy = global_vector_dot( + gravity_gradient_true, reference.vacuum_action, communicator); - const double vacuum_energy = - global_vector_dot(gravity_gradient_true, reference.vacuum_action, communicator); + const double relative_energy_error = + std::abs(operator_energy - reference_energy) / + std::max(std::abs(reference_energy), + std::numeric_limits::epsilon()); - const double relative_energy_error = - std::abs(operator_energy - reference_energy) / - std::max(std::abs(reference_energy), std::numeric_limits::epsilon()); + const double vacuum_action_fraction = + vacuum_action_norm / + std::max(reference_norm, std::numeric_limits::epsilon()); - const double vacuum_action_fraction = - vacuum_action_norm / std::max(reference_norm, std::numeric_limits::epsilon()); + const double vacuum_energy_fraction = vacuum_energy / reference_energy; - const double vacuum_energy_fraction = vacuum_energy / reference_energy; + const double stellar_vacuum_dot = global_vector_dot( + reference.stellar_action, reference.vacuum_action, communicator); - const double stellar_vacuum_dot = - global_vector_dot(reference.stellar_action, reference.vacuum_action, communicator); + const double stellar_vacuum_alignment = + stellar_vacuum_dot / + std::max(stellar_action_norm * vacuum_action_norm, + std::numeric_limits::epsilon()); - const double stellar_vacuum_alignment = - stellar_vacuum_dot / - std::max(stellar_action_norm * vacuum_action_norm, std::numeric_limits::epsilon()); + INFO("Geometry = " << (deformed ? "deformed" : "identity")); - INFO("Geometry = " << (deformed ? "deformed" : "identity")); + INFO("Stellar elements = " << reference.stellar_elements); - INFO("Stellar elements = " << reference.stellar_elements); + INFO("Vacuum elements = " << reference.vacuum_elements); - INFO("Vacuum elements = " << reference.vacuum_elements); + INFO("Stellar quadrature points = " + << reference.stellar_quadrature_points); - INFO("Stellar quadrature points = " << reference.stellar_quadrature_points); + INFO("Vacuum quadrature points = " << reference.vacuum_quadrature_points); - INFO("Vacuum quadrature points = " << reference.vacuum_quadrature_points); + INFO("Stellar mapping determinant range = [" + << reference.minimum_stellar_determinant << ", " + << reference.maximum_stellar_determinant << "]"); - INFO( - "Stellar mapping determinant range = [" << reference.minimum_stellar_determinant << ", " - << reference.maximum_stellar_determinant << "]" - ); + INFO("Vacuum mapping determinant range = [" + << reference.minimum_vacuum_determinant << ", " + << reference.maximum_vacuum_determinant << "]"); - INFO( - "Vacuum mapping determinant range = [" << reference.minimum_vacuum_determinant << ", " - << reference.maximum_vacuum_determinant << "]" - ); + INFO("Operator mass-action norm = " << operator_norm); - INFO("Operator mass-action norm = " << operator_norm); + INFO("Stateless quadrature-reference norm = " << reference_norm); - INFO("Stateless quadrature-reference norm = " << reference_norm); + INFO("Stellar action norm = " << stellar_action_norm); - INFO("Stellar action norm = " << stellar_action_norm); + INFO("Vacuum action norm = " << vacuum_action_norm); - INFO("Vacuum action norm = " << vacuum_action_norm); + INFO("Vacuum action fraction = " << vacuum_action_fraction); - INFO("Vacuum action fraction = " << vacuum_action_fraction); + INFO("Stellar-vacuum action alignment = " << stellar_vacuum_alignment); - INFO("Stellar-vacuum action alignment = " << stellar_vacuum_alignment); + INFO("Absolute action error = " << absolute_action_error); - INFO("Absolute action error = " << absolute_action_error); + INFO("Relative action error = " << relative_action_error); - INFO("Relative action error = " << relative_action_error); + INFO("Domain-decomposition error = " << decomposition_error); - INFO("Domain-decomposition error = " << decomposition_error); + INFO("Operator quadratic energy = " << operator_energy); - INFO("Operator quadratic energy = " << operator_energy); + INFO("Quadrature-reference energy = " << reference_energy); - INFO("Quadrature-reference energy = " << reference_energy); + INFO("Stellar energy = " << stellar_energy); - INFO("Stellar energy = " << stellar_energy); + INFO("Vacuum energy = " << vacuum_energy); - INFO("Vacuum energy = " << vacuum_energy); + INFO("Vacuum energy fraction = " << vacuum_energy_fraction); - INFO("Vacuum energy fraction = " << vacuum_energy_fraction); + INFO("Relative energy error = " << relative_energy_error); - INFO("Relative energy error = " << relative_energy_error); + REQUIRE(reference.stellar_elements > 0); - REQUIRE(reference.stellar_elements > 0); + REQUIRE(reference.vacuum_elements > 0); - REQUIRE(reference.vacuum_elements > 0); + REQUIRE(reference.stellar_quadrature_points > 0); - REQUIRE(reference.stellar_quadrature_points > 0); + REQUIRE(reference.vacuum_quadrature_points > 0); - REQUIRE(reference.vacuum_quadrature_points > 0); + REQUIRE(reference.minimum_stellar_determinant > 0.0); - REQUIRE(reference.minimum_stellar_determinant > 0.0); + REQUIRE(reference.minimum_vacuum_determinant > 0.0); - REQUIRE(reference.minimum_vacuum_determinant > 0.0); + REQUIRE(reference_energy > 0.0); - REQUIRE(reference_energy > 0.0); + REQUIRE(stellar_energy > 0.0); - REQUIRE(stellar_energy > 0.0); + REQUIRE(vacuum_energy > 0.0); - REQUIRE(vacuum_energy > 0.0); + constexpr double decomposition_tolerance = 5.0e-14; - constexpr double decomposition_tolerance = 5.0e-14; + constexpr double action_tolerance = 1.0e-11; - constexpr double action_tolerance = 1.0e-11; + constexpr double energy_tolerance = 1.0e-11; - constexpr double energy_tolerance = 1.0e-11; + CHECK_THAT(decomposition_error, + Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); - CHECK_THAT(decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); + CHECK_THAT(relative_action_error, + Catch::Matchers::WithinAbs(0.0, action_tolerance)); - CHECK_THAT(relative_action_error, Catch::Matchers::WithinAbs(0.0, action_tolerance)); - - CHECK_THAT(relative_energy_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); - } + CHECK_THAT(relative_energy_error, + Catch::Matchers::WithinAbs(0.0, energy_tolerance)); } + } } TEST_CASE( "Gravity Field Jacobian Fixed Geometry Blocks Match Centered Differences", - tags::gravity_operator_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + tags::gravity_operator_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - const gravity_layout layout = make_gravity_jacobian_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + const gravity_layout layout = make_gravity_jacobian_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - mfem::Vector state(layout.value_offsets().Last()); - state = 0.0; + mfem::Vector state(layout.value_offsets().Last()); + state = 0.0; - const mfem::Vector displacement = - linearization_context.GetDisplacementMap().gather(make_stateless_reference_displacement(f, true)); - set_value_block(state, layout, displacement_block, displacement); + const mfem::Vector displacement = + linearization_context.GetDisplacementMap().gather( + make_stateless_reference_displacement(f, true)); + set_value_block(state, layout, displacement_block, displacement); - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + const operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - gravity_operator.Prepare(state, revisions); - mfem::Operator &gradient = gravity_operator.GetGradient(state); + gravity_operator.Prepare(state, revisions); + mfem::Operator &gradient = gravity_operator.GetGradient(state); - MPI_Comm communicator = f.mesh->GetComm(); + MPI_Comm communicator = f.mesh->GetComm(); - struct DirectionCase { - std::string name; - mfem::Vector direction; - bool expect_gradient_residual; - bool expect_poisson_residual; - }; + struct DirectionCase { + std::string name; + mfem::Vector direction; + bool expect_gradient_residual; + bool expect_poisson_residual; + }; - std::vector cases; - cases.push_back({"density", make_density_direction(layout), false, true}); - cases.push_back({"gravity gradient", make_gravity_gradient_direction(layout), true, true}); - cases.push_back({"gravity potential", make_gravity_potential_direction(layout), true, false}); + std::vector cases; + cases.push_back({"density", make_density_direction(layout), false, true}); + cases.push_back({"gravity gradient", make_gravity_gradient_direction(layout), + true, true}); + cases.push_back({"gravity potential", + make_gravity_potential_direction(layout), true, false}); - for (const DirectionCase &direction_case : cases) { - DYNAMIC_SECTION("Direction = " << direction_case.name) { - constexpr double difference_step = 1.0e-6; + for (const DirectionCase &direction_case : cases) { + DYNAMIC_SECTION("Direction = " << direction_case.name) { + constexpr double difference_step = 1.0e-6; - mfem::Vector jacobian_action; - gradient.Mult(direction_case.direction, jacobian_action); + mfem::Vector jacobian_action; + gradient.Mult(direction_case.direction, jacobian_action); - const mfem::Vector finite_difference = - evaluate_centered_difference(gravity_operator, state, direction_case.direction, difference_step); + const mfem::Vector finite_difference = evaluate_centered_difference( + gravity_operator, state, direction_case.direction, difference_step); - const mfem::Vector gradient_action = - get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); - const mfem::Vector poisson_action = - get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); + const mfem::Vector gradient_action = get_residual_block( + jacobian_action, layout, gravity_gradient_residual_block); + const mfem::Vector poisson_action = get_residual_block( + jacobian_action, layout, gravity_poisson_residual_block); - const double relative_error = - global_relative_vector_error(jacobian_action, finite_difference, communicator); - const double gradient_norm = global_vector_norm(gradient_action, communicator); - const double poisson_norm = global_vector_norm(poisson_action, communicator); + const double relative_error = global_relative_vector_error( + jacobian_action, finite_difference, communicator); + const double gradient_norm = + global_vector_norm(gradient_action, communicator); + const double poisson_norm = + global_vector_norm(poisson_action, communicator); - INFO("Direction = " << direction_case.name); - INFO("Jacobian action norm = " << global_vector_norm(jacobian_action, communicator)); - INFO("Finite-difference action norm = " << global_vector_norm(finite_difference, communicator)); - INFO("Gradient residual action norm = " << gradient_norm); - INFO("Poisson residual action norm = " << poisson_norm); - INFO("Relative Jacobian error = " << relative_error); + INFO("Direction = " << direction_case.name); + INFO("Jacobian action norm = " << global_vector_norm(jacobian_action, + communicator)); + INFO("Finite-difference action norm = " + << global_vector_norm(finite_difference, communicator)); + INFO("Gradient residual action norm = " << gradient_norm); + INFO("Poisson residual action norm = " << poisson_norm); + INFO("Relative Jacobian error = " << relative_error); - constexpr double jacobian_tolerance = 2.0e-8; - constexpr double zero_tolerance = 1.0e-13; + constexpr double jacobian_tolerance = 2.0e-8; + constexpr double zero_tolerance = 1.0e-13; - CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, jacobian_tolerance)); + CHECK_THAT(relative_error, + Catch::Matchers::WithinAbs(0.0, jacobian_tolerance)); - if (direction_case.expect_gradient_residual) { - CHECK(gradient_norm > zero_tolerance); - } else { - CHECK_THAT(gradient_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance)); - } + if (direction_case.expect_gradient_residual) { + CHECK(gradient_norm > zero_tolerance); + } else { + CHECK_THAT(gradient_norm, + Catch::Matchers::WithinAbs(0.0, zero_tolerance)); + } - if (direction_case.expect_poisson_residual) { - CHECK(poisson_norm > zero_tolerance); - } else { - CHECK_THAT(poisson_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance)); - } - } + if (direction_case.expect_poisson_residual) { + CHECK(poisson_norm > zero_tolerance); + } else { + CHECK_THAT(poisson_norm, + Catch::Matchers::WithinAbs(0.0, zero_tolerance)); + } } + } } TEST_CASE( "Gravity Field Jacobian Combined Fixed Geometry Direction Matches Centered " "Differences", - tags::gravity_operator_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + tags::gravity_operator_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - const gravity_layout layout = make_gravity_jacobian_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + const gravity_layout layout = make_gravity_jacobian_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); - const mfem::Vector direction = make_combined_fixed_geometry_direction(layout); + const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); + const mfem::Vector direction = make_combined_fixed_geometry_direction(layout); - operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; - gravity_operator.Prepare(state, revisions); + operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; + gravity_operator.Prepare(state, revisions); - mfem::Operator &gradient = gravity_operator.GetGradient(state); - mfem::Vector jacobian_action; - gradient.Mult(direction, jacobian_action); + mfem::Operator &gradient = gravity_operator.GetGradient(state); + mfem::Vector jacobian_action; + gradient.Mult(direction, jacobian_action); - MPI_Comm communicator = f.mesh->GetComm(); - double best_error = std::numeric_limits::infinity(); + MPI_Comm communicator = f.mesh->GetComm(); + double best_error = std::numeric_limits::infinity(); - for (const double step : std::array{1.0e-4, 1.0e-6, 1.0e-7}) { - const mfem::Vector finite_difference = evaluate_centered_difference(gravity_operator, state, direction, step); - const double relative_error = global_relative_vector_error(jacobian_action, finite_difference, communicator); - best_error = std::min(best_error, relative_error); + for (const double step : std::array{1.0e-4, 1.0e-6, 1.0e-7}) { + const mfem::Vector finite_difference = + evaluate_centered_difference(gravity_operator, state, direction, step); + const double relative_error = global_relative_vector_error( + jacobian_action, finite_difference, communicator); + best_error = std::min(best_error, relative_error); - INFO("Centered-difference step = " << step); - INFO("Relative Jacobian error = " << relative_error); + INFO("Centered-difference step = " << step); + INFO("Relative Jacobian error = " << relative_error); - CHECK(relative_error < 2.0e-7); - } + CHECK(relative_error < 2.0e-7); + } - INFO("Best relative Jacobian error = " << best_error); - CHECK(best_error < 2.0e-8); + INFO("Best relative Jacobian error = " << best_error); + CHECK(best_error < 2.0e-8); } -TEST_CASE( - "Gravity Field Jacobian Action Is Linear At Fixed Geometry", - tags::gravity_operator_unit -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Gravity Field Jacobian Action Is Linear At Fixed Geometry", + tags::gravity_operator_unit) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - const gravity_layout layout = make_gravity_jacobian_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + const gravity_layout layout = make_gravity_jacobian_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - const mfem::Vector state = make_gravity_jacobian_state(f, layout, false); - const mfem::Vector direction_a = make_density_direction(layout); - const mfem::Vector direction_b = make_gravity_gradient_direction(layout); + const mfem::Vector state = make_gravity_jacobian_state(f, layout, false); + const mfem::Vector direction_a = make_density_direction(layout); + const mfem::Vector direction_b = make_gravity_gradient_direction(layout); - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + const operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - gravity_operator.Prepare(state, revisions); - mfem::Operator &gradient = gravity_operator.GetGradient(state); + gravity_operator.Prepare(state, revisions); + mfem::Operator &gradient = gravity_operator.GetGradient(state); - constexpr double scale_a = 1.7; - constexpr double scale_b = -0.6; + constexpr double scale_a = 1.7; + constexpr double scale_b = -0.6; - mfem::Vector combined_direction(direction_a); - combined_direction *= scale_a; - combined_direction.Add(scale_b, direction_b); + mfem::Vector combined_direction(direction_a); + combined_direction *= scale_a; + combined_direction.Add(scale_b, direction_b); - mfem::Vector action_a; - mfem::Vector action_b; - mfem::Vector combined_action; + mfem::Vector action_a; + mfem::Vector action_b; + mfem::Vector combined_action; - gradient.Mult(direction_a, action_a); - gradient.Mult(direction_b, action_b); - gradient.Mult(combined_direction, combined_action); + gradient.Mult(direction_a, action_a); + gradient.Mult(direction_b, action_b); + gradient.Mult(combined_direction, combined_action); - mfem::Vector expected_action(action_a); - expected_action *= scale_a; - expected_action.Add(scale_b, action_b); + mfem::Vector expected_action(action_a); + expected_action *= scale_a; + expected_action.Add(scale_b, action_b); - MPI_Comm communicator = f.mesh->GetComm(); - const double relative_error = global_relative_vector_error(combined_action, expected_action, communicator); + MPI_Comm communicator = f.mesh->GetComm(); + const double relative_error = global_relative_vector_error( + combined_action, expected_action, communicator); - INFO("Relative Jacobian linearity error = " << relative_error); - CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, 2.0e-13)); + INFO("Relative Jacobian linearity error = " << relative_error); + CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, 2.0e-13)); } -TEST_CASE( - "Gravity Field Prepare Updates Shared Linearization Context", - tags::gravity_operator_unit -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Gravity Field Prepare Updates Shared Linearization Context", + tags::gravity_operator_unit) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - const gravity_layout layout = make_gravity_jacobian_layout(f); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + const gravity_layout layout = make_gravity_jacobian_layout(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - mfem::Vector state_a = make_gravity_jacobian_state(f, layout, false); - const mfem::Vector state_b = make_gravity_jacobian_state(f, layout, true); + mfem::Vector state_a = make_gravity_jacobian_state(f, layout, false); + const mfem::Vector state_b = make_gravity_jacobian_state(f, layout, true); - const operators::context::gravity_field::GravityFieldRevisions revisions_a{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + const operators::context::gravity_field::GravityFieldRevisions revisions_a{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - const operators::context::gravity_field::GravityFieldRevisions revisions_b{ - .discretization = {1}, .displacement = {2}, .density = {2}, .gravity_gradient = {2}, .gravity_potential = {2} - }; + const operators::context::gravity_field::GravityFieldRevisions revisions_b{ + .discretization = {1}, + .displacement = {2}, + .density = {2}, + .gravity_gradient = {2}, + .gravity_potential = {2}}; - MPI_Comm communicator = f.mesh->GetComm(); + MPI_Comm communicator = f.mesh->GetComm(); - gravity_operator.Prepare(state_a, revisions_a); + gravity_operator.Prepare(state_a, revisions_a); - mfem::Operator &returned_gradient_a = gravity_operator.GetGradient(state_a); - REQUIRE(&returned_gradient_a == static_cast(&gravity_jacobian)); + mfem::Operator &returned_gradient_a = gravity_operator.GetGradient(state_a); + REQUIRE(&returned_gradient_a == + static_cast(&gravity_jacobian)); - check_linearization_context_matches_state(linearization_context, state_a, layout.value_offsets(), communicator); + check_linearization_context_matches_state( + linearization_context, state_a, layout.value_offsets(), communicator); - state_a = 0.0; + state_a = 0.0; - CHECK(linearization_context.IsPrepared()); + CHECK(linearization_context.IsPrepared()); - const double stored_state_norm = - global_vector_norm(linearization_context.GetDensityTrue(), communicator) + - global_vector_norm(linearization_context.GetGeometryContext().GetDisplacementTrue(), communicator) + - global_vector_norm(linearization_context.GetGravityGradientTrue(), communicator); + const double stored_state_norm = + global_vector_norm(linearization_context.GetDensityTrue(), communicator) + + global_vector_norm( + linearization_context.GetGeometryContext().GetDisplacementTrue(), + communicator) + + global_vector_norm(linearization_context.GetGravityGradientTrue(), + communicator); - CHECK(stored_state_norm > 0.0); + CHECK(stored_state_norm > 0.0); - gravity_operator.Prepare(state_b, revisions_b); + gravity_operator.Prepare(state_b, revisions_b); - mfem::Operator &returned_gradient_b = gravity_operator.GetGradient(state_b); - REQUIRE(&returned_gradient_b == static_cast(&gravity_jacobian)); + mfem::Operator &returned_gradient_b = gravity_operator.GetGradient(state_b); + REQUIRE(&returned_gradient_b == + static_cast(&gravity_jacobian)); - check_linearization_context_matches_state(linearization_context, state_b, layout.value_offsets(), communicator); + check_linearization_context_matches_state( + linearization_context, state_b, layout.value_offsets(), communicator); } -TEST_CASE( - "Mapped Hdiv Mass Variation Is Linear In Displacement Direction", - tags::gravity_kernel_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Mapped Hdiv Mass Variation Is Linear In Displacement Direction", + tags::gravity_kernel_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); - mfem::Vector zero_gravity_gradient(f.gravityFluxFes->GetTrueVSize()); - zero_direction = 0.0; - zero_gravity_gradient = 0.0; + mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); + mfem::Vector zero_gravity_gradient(f.gravityFluxFes->GetTrueVSize()); + zero_direction = 0.0; + zero_gravity_gradient = 0.0; - mfem::Vector zero_direction_action; - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, fields.displacement, zero_direction, zero_direction_action - ); + mfem::Vector zero_direction_action; + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, fields.displacement, + zero_direction, zero_direction_action); - INFO("Zero-direction action norm = " << global_norm(zero_direction_action, communicator)); - CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); + INFO("Zero-direction action norm = " << global_norm(zero_direction_action, + communicator)); + CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); - mfem::Vector zero_gravity_action; - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, zero_gravity_gradient, fields.displacement, fields.displacement_direction_1, - zero_gravity_action - ); + mfem::Vector zero_gravity_action; + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, zero_gravity_gradient, fields.displacement, + fields.displacement_direction_1, zero_gravity_action); - INFO("Zero-gravity action norm = " << global_norm(zero_gravity_action, communicator)); - CHECK(global_norm(zero_gravity_action, communicator) < 1.0e-13); + INFO("Zero-gravity action norm = " << global_norm(zero_gravity_action, + communicator)); + CHECK(global_norm(zero_gravity_action, communicator) < 1.0e-13); - constexpr double scale_1 = 1.7; - constexpr double scale_2 = -0.6; + constexpr double scale_1 = 1.7; + constexpr double scale_2 = -0.6; - mfem::Vector combined_direction(fields.displacement_direction_1); - combined_direction *= scale_1; - combined_direction.Add(scale_2, fields.displacement_direction_2); + mfem::Vector combined_direction(fields.displacement_direction_1); + combined_direction *= scale_1; + combined_direction.Add(scale_2, fields.displacement_direction_2); - mfem::Vector action_1; - mfem::Vector action_2; - mfem::Vector combined_action; + mfem::Vector action_1; + mfem::Vector action_2; + mfem::Vector combined_action; - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, action_1 - ); - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_2, action_2 - ); - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, fields.displacement, combined_direction, combined_action - ); + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, fields.displacement, + fields.displacement_direction_1, action_1); + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, fields.displacement, + fields.displacement_direction_2, action_2); + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, fields.displacement, + combined_direction, combined_action); - mfem::Vector expected_action(action_1); - expected_action *= scale_1; - expected_action.Add(scale_2, action_2); + mfem::Vector expected_action(action_1); + expected_action *= scale_1; + expected_action.Add(scale_2, action_2); - const double linearity_error = global_relative_error(combined_action, expected_action, communicator); + const double linearity_error = + global_relative_error(combined_action, expected_action, communicator); - INFO("Combined action norm = " << global_norm(combined_action, communicator)); - INFO("Expected action norm = " << global_norm(expected_action, communicator)); - INFO("Displacement-direction linearity error = " << linearity_error); + INFO("Combined action norm = " << global_norm(combined_action, communicator)); + INFO("Expected action norm = " << global_norm(expected_action, communicator)); + INFO("Displacement-direction linearity error = " << linearity_error); - CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); + CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); } -TEST_CASE( - "Mapped Hdiv Mass Variation Matches Centered Geometry Differences", - tags::gravity_kernel_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Mapped Hdiv Mass Variation Matches Centered Geometry Differences", + tags::gravity_kernel_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - constexpr double difference_step = 1.0e-5; - constexpr double comparison_tolerance = 2.0e-7; + constexpr double difference_step = 1.0e-5; + constexpr double comparison_tolerance = 2.0e-7; - mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); - identity_displacement = 0.0; + mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); + identity_displacement = 0.0; - for (const bool deformed : std::array{false, true}) { - const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; + for (const bool deformed : std::array{false, true}) { + const mfem::Vector &base_displacement = + deformed ? fields.displacement : identity_displacement; - mfem::Vector analytic_action; - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, - analytic_action - ); + mfem::Vector analytic_action; + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, base_displacement, + fields.displacement_direction_1, analytic_action); - const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference( - f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, - difference_step - ); + const mfem::Vector finite_difference_action = + centered_hdiv_mass_geometry_difference( + f, domain_mapper, fields.gravity_gradient, base_displacement, + fields.displacement_direction_1, difference_step); - const double relative_error = global_relative_error(analytic_action, finite_difference_action, communicator); + const double relative_error = global_relative_error( + analytic_action, finite_difference_action, communicator); - INFO("Geometry = " << (deformed ? "deformed" : "identity")); - INFO("Analytic variation norm = " << global_norm(analytic_action, communicator)); - INFO("Finite-difference variation norm = " << global_norm(finite_difference_action, communicator)); - INFO("Relative H(div) mass-variation error = " << relative_error); + INFO("Geometry = " << (deformed ? "deformed" : "identity")); + INFO("Analytic variation norm = " << global_norm(analytic_action, + communicator)); + INFO("Finite-difference variation norm = " + << global_norm(finite_difference_action, communicator)); + INFO("Relative H(div) mass-variation error = " << relative_error); - CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); - } + CHECK_THAT(relative_error, + Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); + } } TEST_CASE( "Mapped Hdiv Mass Geometry Difference Converges To Analytic Variation", - tags::gravity_kernel_convergence -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + tags::gravity_kernel_convergence) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - mfem::Vector analytic_action; - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, analytic_action - ); + mfem::Vector analytic_action; + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, fields.displacement, + fields.displacement_direction_1, analytic_action); - constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; - std::array errors{}; + constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; + std::array errors{}; - for (std::size_t i = 0; i < difference_steps.size(); ++i) { - const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference( - f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, - difference_steps[i] - ); + for (std::size_t i = 0; i < difference_steps.size(); ++i) { + const mfem::Vector finite_difference_action = + centered_hdiv_mass_geometry_difference( + f, domain_mapper, fields.gravity_gradient, fields.displacement, + fields.displacement_direction_1, difference_steps[i]); - errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator); + errors[i] = global_relative_error(finite_difference_action, analytic_action, + communicator); - INFO("Difference step = " << difference_steps[i]); - INFO("Relative error = " << errors[i]); - } + INFO("Difference step = " << difference_steps[i]); + INFO("Relative error = " << errors[i]); + } - const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); - const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); + const double first_observed_order = + std::log(errors[0] / errors[1]) / std::log(2.0); + const double second_observed_order = + std::log(errors[1] / errors[2]) / std::log(2.0); - INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); - INFO("First observed convergence order = " << first_observed_order); - INFO("Second observed convergence order = " << second_observed_order); + INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] + << "]"); + INFO("First observed convergence order = " << first_observed_order); + INFO("Second observed convergence order = " << second_observed_order); - CHECK(errors[1] < errors[0]); - CHECK(errors[2] < errors[1]); - CHECK(first_observed_order > 1.8); - CHECK(second_observed_order > 1.8); + CHECK(errors[1] < errors[0]); + CHECK(errors[2] < errors[1]); + CHECK(first_observed_order > 1.8); + CHECK(second_observed_order > 1.8); } -TEST_CASE( - "Mapped Source Variation Is Linear In Displacement Direction", - tags::gravity_kernel_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Mapped Source Variation Is Linear In Displacement Direction", + tags::gravity_kernel_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector density = make_source_variation_density(f); - MPI_Comm communicator = f.mesh->GetComm(); + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector density = make_source_variation_density(f); + MPI_Comm communicator = f.mesh->GetComm(); - mfem::Vector zero_density(f.densityFes->GetTrueVSize()); - mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); - zero_density = 0.0; - zero_direction = 0.0; + mfem::Vector zero_density(f.densityFes->GetTrueVSize()); + mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); + zero_density = 0.0; + zero_direction = 0.0; - mfem::Vector zero_direction_action; + mfem::Vector zero_direction_action; + operators::kernels::apply_mapped_source_variation( + f, domain_mapper, density, fields.displacement, zero_direction, + zero_direction_action); + + REQUIRE(zero_direction_action.Size() == + f.gravityPotentialFes->GetTrueVSize()); + CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); + + mfem::Vector zero_density_action; + operators::kernels::apply_mapped_source_variation( + f, domain_mapper, zero_density, fields.displacement, + fields.displacement_direction_1, zero_density_action); + + REQUIRE(zero_density_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + CHECK(global_norm(zero_density_action, communicator) < 1.0e-13); + + constexpr double scale_1 = 1.4; + constexpr double scale_2 = -0.7; + + mfem::Vector combined_direction(fields.displacement_direction_1); + combined_direction *= scale_1; + combined_direction.Add(scale_2, fields.displacement_direction_2); + + mfem::Vector action_1; + mfem::Vector action_2; + mfem::Vector combined_action; + + operators::kernels::apply_mapped_source_variation( + f, domain_mapper, density, fields.displacement, + fields.displacement_direction_1, action_1); + operators::kernels::apply_mapped_source_variation( + f, domain_mapper, density, fields.displacement, + fields.displacement_direction_2, action_2); + operators::kernels::apply_mapped_source_variation( + f, domain_mapper, density, fields.displacement, combined_direction, + combined_action); + + REQUIRE(action_1.Size() == f.gravityPotentialFes->GetTrueVSize()); + REQUIRE(action_2.Size() == f.gravityPotentialFes->GetTrueVSize()); + REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + + mfem::Vector expected_action(action_1); + expected_action *= scale_1; + expected_action.Add(scale_2, action_2); + + const double linearity_error = + global_relative_error(combined_action, expected_action, communicator); + + INFO("Combined source-variation norm = " << global_norm(combined_action, + communicator)); + INFO("Expected source-variation norm = " << global_norm(expected_action, + communicator)); + INFO("Source-variation linearity error = " << linearity_error); + + CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); +} + +TEST_CASE("Mapped Source Variation Matches Centered Geometry Differences", + tags::gravity_kernel_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + + REQUIRE(f.domainMapperStateless != nullptr); + + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector density = make_source_variation_density(f); + MPI_Comm communicator = f.mesh->GetComm(); + + constexpr double difference_step = 1.0e-5; + constexpr double comparison_tolerance = 2.0e-7; + + mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); + identity_displacement = 0.0; + + for (const bool deformed : std::array{false, true}) { + const mfem::Vector &base_displacement = + deformed ? fields.displacement : identity_displacement; + + mfem::Vector analytic_action; operators::kernels::apply_mapped_source_variation( - f, domain_mapper, density, fields.displacement, zero_direction, zero_direction_action - ); + f, domain_mapper, density, base_displacement, + fields.displacement_direction_1, analytic_action); - REQUIRE(zero_direction_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); + const mfem::Vector finite_difference_action = + centered_source_geometry_difference( + f, domain_mapper, density, base_displacement, + fields.displacement_direction_1, difference_step); - mfem::Vector zero_density_action; - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, zero_density, fields.displacement, fields.displacement_direction_1, zero_density_action - ); + REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + REQUIRE(finite_difference_action.Size() == + f.gravityPotentialFes->GetTrueVSize()); - REQUIRE(zero_density_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - CHECK(global_norm(zero_density_action, communicator) < 1.0e-13); + const double relative_error = global_relative_error( + analytic_action, finite_difference_action, communicator); - constexpr double scale_1 = 1.4; - constexpr double scale_2 = -0.7; + INFO("Geometry = " << (deformed ? "deformed" : "identity")); + INFO("Analytic source-variation norm = " << global_norm(analytic_action, + communicator)); + INFO("Finite-difference source-variation norm = " + << global_norm(finite_difference_action, communicator)); + INFO("Relative source-variation error = " << relative_error); - mfem::Vector combined_direction(fields.displacement_direction_1); - combined_direction *= scale_1; - combined_direction.Add(scale_2, fields.displacement_direction_2); + CHECK_THAT(relative_error, + Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); + } +} - mfem::Vector action_1; - mfem::Vector action_2; +TEST_CASE("Mapped Source Geometry Difference Converges To Analytic Variation", + tags::gravity_kernel_convergence) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + + REQUIRE(f.domainMapperStateless != nullptr); + + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector density = make_source_variation_density(f); + MPI_Comm communicator = f.mesh->GetComm(); + + mfem::Vector analytic_action; + operators::kernels::apply_mapped_source_variation( + f, domain_mapper, density, fields.displacement, + fields.displacement_direction_1, analytic_action); + + REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + + constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; + std::array errors{}; + + for (std::size_t i = 0; i < difference_steps.size(); ++i) { + const mfem::Vector finite_difference_action = + centered_source_geometry_difference( + f, domain_mapper, density, fields.displacement, + fields.displacement_direction_1, difference_steps[i]); + + REQUIRE(finite_difference_action.Size() == + f.gravityPotentialFes->GetTrueVSize()); + errors[i] = global_relative_error(finite_difference_action, analytic_action, + communicator); + } + + const double first_observed_order = + std::log(errors[0] / errors[1]) / std::log(2.0); + const double second_observed_order = + std::log(errors[1] / errors[2]) / std::log(2.0); + + INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] + << "]"); + INFO("First observed convergence order = " << first_observed_order); + INFO("Second observed convergence order = " << second_observed_order); + + CHECK(errors[1] < errors[0]); + CHECK(errors[2] < errors[1]); + CHECK(first_observed_order > 1.8); + CHECK(second_observed_order > 1.8); +} + +TEST_CASE("Mapped Hdiv Mass Variation Is Symmetric", + tags::gravity_kernel_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + + REQUIRE(f.domainMapperStateless != nullptr); + + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector second_gravity_gradient = + make_secondary_gravity_gradient(f); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); + + mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); + identity_displacement = 0.0; + + for (const bool deformed : std::array{false, true}) { + const mfem::Vector &base_displacement = + deformed ? fields.displacement : identity_displacement; + + mfem::Vector first_action; + mfem::Vector second_action; + + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, base_displacement, + fields.displacement_direction_1, first_action); + + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, second_gravity_gradient, base_displacement, + fields.displacement_direction_1, second_action); + + REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); + REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); + + const double left_pairing = + global_dot(fields.gravity_gradient, second_action, communicator); + const double right_pairing = + global_dot(second_gravity_gradient, first_action, communicator); + const double symmetry_error = + std::abs(left_pairing - right_pairing) / + std::max({std::abs(left_pairing), std::abs(right_pairing), 1.0e-14}); + + INFO("Geometry = " << (deformed ? "deformed" : "identity")); + INFO("g1^T delta_M g2 = " << left_pairing); + INFO("g2^T delta_M g1 = " << right_pairing); + INFO("Relative symmetry error = " << symmetry_error); + + CHECK_THAT(symmetry_error, Catch::Matchers::WithinAbs(0.0, 1.0e-11)); + } +} + +TEST_CASE("Mapped Geometry Variations Are Linear In Base Fields", + tags::gravity_kernel_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + + REQUIRE(f.domainMapperStateless != nullptr); + + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector second_gravity_gradient = + make_secondary_gravity_gradient(f); + const mfem::Vector first_density = make_source_variation_density(f); + const mfem::Vector second_density = make_secondary_source_density(f); + + constexpr double scale_1 = 1.3; + constexpr double scale_2 = -0.4; + + { + MPI_Comm communicator = f.gravityFluxFes->GetComm(); + + mfem::Vector combined_gravity_gradient(fields.gravity_gradient); + combined_gravity_gradient *= scale_1; + combined_gravity_gradient.Add(scale_2, second_gravity_gradient); + + mfem::Vector first_action; + mfem::Vector second_action; + mfem::Vector combined_action; + + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, fields.gravity_gradient, fields.displacement, + fields.displacement_direction_1, first_action); + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, second_gravity_gradient, fields.displacement, + fields.displacement_direction_1, second_action); + operators::kernels::apply_mapped_hdiv_mass_variation( + f, domain_mapper, combined_gravity_gradient, fields.displacement, + fields.displacement_direction_1, combined_action); + + REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); + REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); + REQUIRE(combined_action.Size() == f.gravityFluxFes->GetTrueVSize()); + + mfem::Vector expected_action(first_action); + expected_action *= scale_1; + expected_action.Add(scale_2, second_action); + + const double linearity_error = + global_relative_error(combined_action, expected_action, communicator); + + INFO("H(div) base-field linearity error = " << linearity_error); + CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); + } + + { + MPI_Comm communicator = f.mesh->GetComm(); + + mfem::Vector combined_density(first_density); + combined_density *= scale_1; + combined_density.Add(scale_2, second_density); + + mfem::Vector first_action; + mfem::Vector second_action; mfem::Vector combined_action; operators::kernels::apply_mapped_source_variation( - f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, action_1 - ); + f, domain_mapper, first_density, fields.displacement, + fields.displacement_direction_1, first_action); operators::kernels::apply_mapped_source_variation( - f, domain_mapper, density, fields.displacement, fields.displacement_direction_2, action_2 - ); + f, domain_mapper, second_density, fields.displacement, + fields.displacement_direction_1, second_action); operators::kernels::apply_mapped_source_variation( - f, domain_mapper, density, fields.displacement, combined_direction, combined_action - ); + f, domain_mapper, combined_density, fields.displacement, + fields.displacement_direction_1, combined_action); - REQUIRE(action_1.Size() == f.gravityPotentialFes->GetTrueVSize()); - REQUIRE(action_2.Size() == f.gravityPotentialFes->GetTrueVSize()); + REQUIRE(first_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + REQUIRE(second_action.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - mfem::Vector expected_action(action_1); + mfem::Vector expected_action(first_action); expected_action *= scale_1; - expected_action.Add(scale_2, action_2); + expected_action.Add(scale_2, second_action); - const double linearity_error = global_relative_error(combined_action, expected_action, communicator); + const double linearity_error = + global_relative_error(combined_action, expected_action, communicator); - INFO("Combined source-variation norm = " << global_norm(combined_action, communicator)); - INFO("Expected source-variation norm = " << global_norm(expected_action, communicator)); - INFO("Source-variation linearity error = " << linearity_error); - - CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); + INFO("Source base-field linearity error = " << linearity_error); + CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); + } } -TEST_CASE( - "Mapped Source Variation Matches Centered Geometry Differences", - tags::gravity_kernel_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Mapped Source Variation Ignores Vacuum Density", + tags::gravity_kernel_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector density = make_source_variation_density(f); - MPI_Comm communicator = f.mesh->GetComm(); + const mapping::DomainMapper &domain_mapper = + *f.domainMapperStateless; + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + const mfem::Vector vacuum_density = + make_vacuum_only_density(f, vacuum_attribute); + MPI_Comm communicator = f.mesh->GetComm(); - constexpr double difference_step = 1.0e-5; - constexpr double comparison_tolerance = 2.0e-7; + const double vacuum_density_norm = global_norm(vacuum_density, communicator); + REQUIRE(vacuum_density_norm > 0.0); - mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); - identity_displacement = 0.0; + mfem::Vector action; + operators::kernels::apply_mapped_source_variation( + f, domain_mapper, vacuum_density, fields.displacement, + fields.displacement_direction_1, action); - for (const bool deformed : std::array{false, true}) { - const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; + REQUIRE(action.Size() == f.gravityPotentialFes->GetTrueVSize()); - mfem::Vector analytic_action; - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, density, base_displacement, fields.displacement_direction_1, analytic_action - ); + const double action_norm = global_norm(action, communicator); - const mfem::Vector finite_difference_action = centered_source_geometry_difference( - f, domain_mapper, density, base_displacement, fields.displacement_direction_1, difference_step - ); + INFO("Vacuum density norm = " << vacuum_density_norm); + INFO("Source-variation action norm = " << action_norm); - REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + CHECK_THAT(action_norm, Catch::Matchers::WithinAbs(0.0, 1.0e-14)); +} - const double relative_error = global_relative_error(analytic_action, finite_difference_action, communicator); +TEST_CASE("Gravity Field Jacobian Displacement Blocks Match Geometry Kernels", + tags::gravity_operator_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - INFO("Geometry = " << (deformed ? "deformed" : "identity")); - INFO("Analytic source-variation norm = " << global_norm(analytic_action, communicator)); - INFO("Finite-difference source-variation norm = " << global_norm(finite_difference_action, communicator)); - INFO("Relative source-variation error = " << relative_error); + *f.displacement = 0.0; - CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); - } -} - -TEST_CASE( - "Mapped Source Geometry Difference Converges To Analytic Variation", - tags::gravity_kernel_convergence -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(f.domainMapperStateless != nullptr); - - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector density = make_source_variation_density(f); - MPI_Comm communicator = f.mesh->GetComm(); - - mfem::Vector analytic_action; - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, analytic_action - ); - - REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - - constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; - std::array errors{}; - - for (std::size_t i = 0; i < difference_steps.size(); ++i) { - const mfem::Vector finite_difference_action = centered_source_geometry_difference( - f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, difference_steps[i] - ); - - REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator); - } - - const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); - const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); - - INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); - INFO("First observed convergence order = " << first_observed_order); - INFO("Second observed convergence order = " << second_observed_order); - - CHECK(errors[1] < errors[0]); - CHECK(errors[2] < errors[1]); - CHECK(first_observed_order > 1.8); - CHECK(second_observed_order > 1.8); -} - -TEST_CASE( - "Mapped Hdiv Mass Variation Is Symmetric", - tags::gravity_kernel_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(f.domainMapperStateless != nullptr); - - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); - identity_displacement = 0.0; - - for (const bool deformed : std::array{false, true}) { - const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; - - mfem::Vector first_action; - mfem::Vector second_action; - - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, first_action - ); - - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, second_gravity_gradient, base_displacement, fields.displacement_direction_1, second_action - ); - - REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); - REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); - - const double left_pairing = global_dot(fields.gravity_gradient, second_action, communicator); - const double right_pairing = global_dot(second_gravity_gradient, first_action, communicator); - const double symmetry_error = std::abs(left_pairing - right_pairing) / - std::max({std::abs(left_pairing), std::abs(right_pairing), 1.0e-14}); - - INFO("Geometry = " << (deformed ? "deformed" : "identity")); - INFO("g1^T delta_M g2 = " << left_pairing); - INFO("g2^T delta_M g1 = " << right_pairing); - INFO("Relative symmetry error = " << symmetry_error); - - CHECK_THAT(symmetry_error, Catch::Matchers::WithinAbs(0.0, 1.0e-11)); - } -} - -TEST_CASE( - "Mapped Geometry Variations Are Linear In Base Fields", - tags::gravity_kernel_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(f.domainMapperStateless != nullptr); - - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f); - const mfem::Vector first_density = make_source_variation_density(f); - const mfem::Vector second_density = make_secondary_source_density(f); - - constexpr double scale_1 = 1.3; - constexpr double scale_2 = -0.4; - - { - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - mfem::Vector combined_gravity_gradient(fields.gravity_gradient); - combined_gravity_gradient *= scale_1; - combined_gravity_gradient.Add(scale_2, second_gravity_gradient); - - mfem::Vector first_action; - mfem::Vector second_action; - mfem::Vector combined_action; - - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, - first_action - ); - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, second_gravity_gradient, fields.displacement, fields.displacement_direction_1, - second_action - ); - operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, combined_gravity_gradient, fields.displacement, fields.displacement_direction_1, - combined_action - ); - - REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); - REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); - REQUIRE(combined_action.Size() == f.gravityFluxFes->GetTrueVSize()); - - mfem::Vector expected_action(first_action); - expected_action *= scale_1; - expected_action.Add(scale_2, second_action); - - const double linearity_error = global_relative_error(combined_action, expected_action, communicator); - - INFO("H(div) base-field linearity error = " << linearity_error); - CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); - } - - { - MPI_Comm communicator = f.mesh->GetComm(); - - mfem::Vector combined_density(first_density); - combined_density *= scale_1; - combined_density.Add(scale_2, second_density); - - mfem::Vector first_action; - mfem::Vector second_action; - mfem::Vector combined_action; - - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, first_density, fields.displacement, fields.displacement_direction_1, first_action - ); - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, second_density, fields.displacement, fields.displacement_direction_1, second_action - ); - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, combined_density, fields.displacement, fields.displacement_direction_1, combined_action - ); - - REQUIRE(first_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - REQUIRE(second_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - - mfem::Vector expected_action(first_action); - expected_action *= scale_1; - expected_action.Add(scale_2, second_action); - - const double linearity_error = global_relative_error(combined_action, expected_action, communicator); - - INFO("Source base-field linearity error = " << linearity_error); - CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); - } -} - -TEST_CASE( - "Mapped Source Variation Ignores Vacuum Density", - tags::gravity_kernel_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(f.domainMapperStateless != nullptr); - - const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const int vacuum_attribute = domain_mapper.GetVacuumElementAttribute(); - const mfem::Vector vacuum_density = make_vacuum_only_density(f, vacuum_attribute); - MPI_Comm communicator = f.mesh->GetComm(); - - const double vacuum_density_norm = global_norm(vacuum_density, communicator); - REQUIRE(vacuum_density_norm > 0.0); - - mfem::Vector action; - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, vacuum_density, fields.displacement, fields.displacement_direction_1, action - ); - - REQUIRE(action.Size() == f.gravityPotentialFes->GetTrueVSize()); - - const double action_norm = global_norm(action, communicator); - - INFO("Vacuum density norm = " << vacuum_density_norm); - INFO("Source-variation action norm = " << action_norm); - - CHECK_THAT(action_norm, Catch::Matchers::WithinAbs(0.0, 1.0e-14)); -} - -TEST_CASE( - "Gravity Field Jacobian Displacement Blocks Match Geometry Kernels", - tags::gravity_operator_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); - - REQUIRE(f.domainMapperStateless != nullptr); - - const gravity_layout layout = make_gravity_jacobian_layout(f); - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); - const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); - - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); - - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; - - gravity_operator.Prepare(state, revisions); - - mfem::Vector jacobian_action; - gravity_jacobian.Mult(direction, jacobian_action); - - REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); - - const mfem::Vector &density_true = linearization_context.GetDensityTrue(); - const mfem::Vector &displacement_true = - linearization_context.GetGeometryContext().GetDisplacementTrue(); - const mfem::Vector &gravity_gradient_true = linearization_context.GetGravityGradientTrue(); - - const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); - const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); - - mfem::Vector expected_gradient_action_true; - mfem::Vector expected_poisson_action_true; - operators::kernels::apply_mapped_hdiv_mass_variation( - f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, fields.displacement_direction_1, - expected_gradient_action_true - ); - operators::kernels::apply_mapped_source_variation( - f, *f.domainMapperStateless, density_true, displacement_true, fields.displacement_direction_1, - expected_poisson_action_true - ); - expected_poisson_action_true *= -1.0; - - const mfem::Vector expected_gradient_action = - linearization_context.GetGravityGradientMap().gather(expected_gradient_action_true); - const mfem::Vector expected_poisson_action = - linearization_context.GetGravityPotentialMap().gather(expected_poisson_action_true); - - MPI_Comm communicator = f.mesh->GetComm(); - const double gradient_error = global_relative_error(gradient_action, expected_gradient_action, communicator); - const double poisson_error = global_relative_error(poisson_action, expected_poisson_action, communicator); - - INFO("Gradient displacement action norm = " << global_norm(gradient_action, communicator)); - INFO("Poisson displacement action norm = " << global_norm(poisson_action, communicator)); - INFO("Gradient displacement-block error = " << gradient_error); - INFO("Poisson displacement-block error = " << poisson_error); - - REQUIRE(global_norm(gradient_action, communicator) > 1.0e-12); - REQUIRE(global_norm(poisson_action, communicator) > 1.0e-12); - CHECK_THAT(gradient_error, WithinAbs(0.0, 2.0e-12)); - CHECK_THAT(poisson_error, WithinAbs(0.0, 2.0e-12)); -} - -TEST_CASE( - "Gravity Field Jacobian Displacement Direction Matches Centered " - "Differences", - tags::gravity_operator_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); - - REQUIRE(f.domainMapperStateless != nullptr); - - const gravity_layout layout = make_gravity_jacobian_layout(f); - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); - - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); - - constexpr double difference_step = 1.0e-5; - constexpr double comparison_tolerance = 3.0e-7; - constexpr double nonzero_tolerance = 1.0e-12; - - MPI_Comm communicator = f.mesh->GetComm(); - - REQUIRE(direction.Size() == layout.value_offsets().Last()); - REQUIRE(global_vector_norm(direction, communicator) > nonzero_tolerance); - - for (const bool deformed : std::array{false, true}) { - DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { - const mfem::Vector state = make_gravity_jacobian_state(f, layout, deformed); - - REQUIRE(state.Size() == layout.value_offsets().Last()); - - const operators::context::gravity_field::GravityFieldRevisions base_revisions{ - .discretization = {1}, - .displacement = {1}, - .density = {1}, - .gravity_gradient = {1}, - .gravity_potential = {1} - }; - - gravity_operator.Prepare(state, base_revisions); - - REQUIRE(linearization_context.IsPrepared()); - - check_linearization_context_matches_state( - linearization_context, state, layout.value_offsets(), communicator - ); - - const double base_density_norm = global_vector_norm(linearization_context.GetDensityTrue(), communicator); - const double base_gravity_gradient_norm = - global_vector_norm(linearization_context.GetGravityGradientTrue(), communicator); - - INFO("Base density norm = " << base_density_norm); - INFO("Base gravity-gradient norm = " << base_gravity_gradient_norm); - - REQUIRE(base_density_norm > nonzero_tolerance); - REQUIRE(base_gravity_gradient_norm > nonzero_tolerance); - - mfem::Operator &gradient = gravity_operator.GetGradient(state); - REQUIRE(&gradient == static_cast(&gravity_jacobian)); - - mfem::Vector jacobian_action; - gradient.Mult(direction, jacobian_action); - - REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); - - mfem::Vector plus_state(state); - mfem::Vector minus_state(state); - plus_state.Add(difference_step, direction); - minus_state.Add(-difference_step, direction); - - auto plus_revisions = base_revisions; - plus_revisions.displacement.value++; - - mfem::Vector plus_residual; - gravity_operator.Prepare(plus_state, plus_revisions); - gravity_operator.Mult(plus_state, plus_residual); - - REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); - - auto minus_revisions = plus_revisions; - minus_revisions.displacement.value++; - - mfem::Vector minus_residual; - gravity_operator.Prepare(minus_state, minus_revisions); - gravity_operator.Mult(minus_state, minus_residual); - - REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); - - mfem::Vector finite_difference(plus_residual); - finite_difference -= minus_residual; - finite_difference /= 2.0 * difference_step; - - const mfem::Vector gradient_action = - get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); - const mfem::Vector poisson_action = - get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); - const mfem::Vector finite_difference_gradient = - get_residual_block(finite_difference, layout, gravity_gradient_residual_block); - const mfem::Vector finite_difference_poisson = - get_residual_block(finite_difference, layout, gravity_poisson_residual_block); - - const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator); - const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator); - const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator); - const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator); - - REQUIRE(jacobian_gradient_norm > nonzero_tolerance); - REQUIRE(jacobian_poisson_norm > nonzero_tolerance); - REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); - REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); - - mfem::Vector gradient_difference(gradient_action); - gradient_difference -= finite_difference_gradient; - - mfem::Vector poisson_difference(poisson_action); - poisson_difference -= finite_difference_poisson; - - mfem::Vector combined_difference(jacobian_action); - combined_difference -= finite_difference; - - const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator); - const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator); - const double absolute_combined_error = global_vector_norm(combined_difference, communicator); - - const double gradient_error = - global_relative_error(gradient_action, finite_difference_gradient, communicator); - const double poisson_error = global_relative_error(poisson_action, finite_difference_poisson, communicator); - const double combined_error = global_relative_error(jacobian_action, finite_difference, communicator); - - INFO("Geometry = " << (deformed ? "deformed" : "identity")); - INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); - INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm); - INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); - INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm); - INFO("Absolute gradient residual error = " << absolute_gradient_error); - INFO("Absolute Poisson residual error = " << absolute_poisson_error); - INFO("Absolute combined residual error = " << absolute_combined_error); - INFO("Gradient residual relative error = " << gradient_error); - INFO("Poisson residual relative error = " << poisson_error); - INFO("Combined residual relative error = " << combined_error); - - REQUIRE(std::isfinite(gradient_error)); - REQUIRE(std::isfinite(poisson_error)); - REQUIRE(std::isfinite(combined_error)); - - CHECK_THAT(gradient_error, WithinAbs(0.0, comparison_tolerance)); - CHECK_THAT(poisson_error, WithinAbs(0.0, comparison_tolerance)); - CHECK_THAT(combined_error, WithinAbs(0.0, comparison_tolerance)); - } + REQUIRE(f.domainMapperStateless != nullptr); + + const gravity_layout layout = make_gravity_jacobian_layout(f); + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); + const mfem::Vector direction = + make_displacement_direction(layout, fields.displacement_direction_1); + + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); + + const operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; + + gravity_operator.Prepare(state, revisions); + + mfem::Vector jacobian_action; + gravity_jacobian.Mult(direction, jacobian_action); + + REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); + + const mfem::Vector &density_true = linearization_context.GetDensityTrue(); + const mfem::Vector &displacement_true = + linearization_context.GetGeometryContext().GetDisplacementTrue(); + const mfem::Vector &gravity_gradient_true = + linearization_context.GetGravityGradientTrue(); + + const mfem::Vector gradient_action = get_residual_block( + jacobian_action, layout, gravity_gradient_residual_block); + const mfem::Vector poisson_action = get_residual_block( + jacobian_action, layout, gravity_poisson_residual_block); + + mfem::Vector expected_gradient_action_true; + mfem::Vector expected_poisson_action_true; + operators::kernels::apply_mapped_hdiv_mass_variation( + f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, + fields.displacement_direction_1, expected_gradient_action_true); + operators::kernels::apply_mapped_source_variation( + f, *f.domainMapperStateless, density_true, displacement_true, + fields.displacement_direction_1, expected_poisson_action_true); + expected_poisson_action_true *= -1.0; + + const mfem::Vector expected_gradient_action = + linearization_context.GetGravityGradientMap().gather( + expected_gradient_action_true); + const mfem::Vector expected_poisson_action = + linearization_context.GetGravityPotentialMap().gather( + expected_poisson_action_true); + + MPI_Comm communicator = f.mesh->GetComm(); + const double gradient_error = global_relative_error( + gradient_action, expected_gradient_action, communicator); + const double poisson_error = global_relative_error( + poisson_action, expected_poisson_action, communicator); + + INFO("Gradient displacement action norm = " << global_norm(gradient_action, + communicator)); + INFO("Poisson displacement action norm = " << global_norm(poisson_action, + communicator)); + INFO("Gradient displacement-block error = " << gradient_error); + INFO("Poisson displacement-block error = " << poisson_error); + + REQUIRE(global_norm(gradient_action, communicator) > 1.0e-12); + REQUIRE(global_norm(poisson_action, communicator) > 1.0e-12); + CHECK_THAT(gradient_error, WithinAbs(0.0, 2.0e-12)); + CHECK_THAT(poisson_error, WithinAbs(0.0, 2.0e-12)); +} + +TEST_CASE("Gravity Field Jacobian Displacement Direction Matches Centered " + "Differences", + tags::gravity_operator_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + + *f.displacement = 0.0; + + REQUIRE(f.domainMapperStateless != nullptr); + + const gravity_layout layout = make_gravity_jacobian_layout(f); + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector direction = + make_displacement_direction(layout, fields.displacement_direction_1); + + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); + + constexpr double difference_step = 1.0e-5; + constexpr double comparison_tolerance = 3.0e-7; + constexpr double nonzero_tolerance = 1.0e-12; + + MPI_Comm communicator = f.mesh->GetComm(); + + REQUIRE(direction.Size() == layout.value_offsets().Last()); + REQUIRE(global_vector_norm(direction, communicator) > nonzero_tolerance); + + for (const bool deformed : std::array{false, true}) { + DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { + const mfem::Vector state = + make_gravity_jacobian_state(f, layout, deformed); + + REQUIRE(state.Size() == layout.value_offsets().Last()); + + const operators::context::gravity_field::GravityFieldRevisions + base_revisions{.discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; + + gravity_operator.Prepare(state, base_revisions); + + REQUIRE(linearization_context.IsPrepared()); + + check_linearization_context_matches_state( + linearization_context, state, layout.value_offsets(), communicator); + + const double base_density_norm = global_vector_norm( + linearization_context.GetDensityTrue(), communicator); + const double base_gravity_gradient_norm = global_vector_norm( + linearization_context.GetGravityGradientTrue(), communicator); + + INFO("Base density norm = " << base_density_norm); + INFO("Base gravity-gradient norm = " << base_gravity_gradient_norm); + + REQUIRE(base_density_norm > nonzero_tolerance); + REQUIRE(base_gravity_gradient_norm > nonzero_tolerance); + + mfem::Operator &gradient = gravity_operator.GetGradient(state); + REQUIRE(&gradient == static_cast(&gravity_jacobian)); + + mfem::Vector jacobian_action; + gradient.Mult(direction, jacobian_action); + + REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); + + mfem::Vector plus_state(state); + mfem::Vector minus_state(state); + plus_state.Add(difference_step, direction); + minus_state.Add(-difference_step, direction); + + auto plus_revisions = base_revisions; + plus_revisions.displacement.value++; + + mfem::Vector plus_residual; + gravity_operator.Prepare(plus_state, plus_revisions); + gravity_operator.Mult(plus_state, plus_residual); + + REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); + + auto minus_revisions = plus_revisions; + minus_revisions.displacement.value++; + + mfem::Vector minus_residual; + gravity_operator.Prepare(minus_state, minus_revisions); + gravity_operator.Mult(minus_state, minus_residual); + + REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); + + mfem::Vector finite_difference(plus_residual); + finite_difference -= minus_residual; + finite_difference /= 2.0 * difference_step; + + const mfem::Vector gradient_action = get_residual_block( + jacobian_action, layout, gravity_gradient_residual_block); + const mfem::Vector poisson_action = get_residual_block( + jacobian_action, layout, gravity_poisson_residual_block); + const mfem::Vector finite_difference_gradient = get_residual_block( + finite_difference, layout, gravity_gradient_residual_block); + const mfem::Vector finite_difference_poisson = get_residual_block( + finite_difference, layout, gravity_poisson_residual_block); + + const double jacobian_gradient_norm = + global_vector_norm(gradient_action, communicator); + const double jacobian_poisson_norm = + global_vector_norm(poisson_action, communicator); + const double finite_difference_gradient_norm = + global_vector_norm(finite_difference_gradient, communicator); + const double finite_difference_poisson_norm = + global_vector_norm(finite_difference_poisson, communicator); + + REQUIRE(jacobian_gradient_norm > nonzero_tolerance); + REQUIRE(jacobian_poisson_norm > nonzero_tolerance); + REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); + REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); + + mfem::Vector gradient_difference(gradient_action); + gradient_difference -= finite_difference_gradient; + + mfem::Vector poisson_difference(poisson_action); + poisson_difference -= finite_difference_poisson; + + mfem::Vector combined_difference(jacobian_action); + combined_difference -= finite_difference; + + const double absolute_gradient_error = + global_vector_norm(gradient_difference, communicator); + const double absolute_poisson_error = + global_vector_norm(poisson_difference, communicator); + const double absolute_combined_error = + global_vector_norm(combined_difference, communicator); + + const double gradient_error = global_relative_error( + gradient_action, finite_difference_gradient, communicator); + const double poisson_error = global_relative_error( + poisson_action, finite_difference_poisson, communicator); + const double combined_error = global_relative_error( + jacobian_action, finite_difference, communicator); + + INFO("Geometry = " << (deformed ? "deformed" : "identity")); + INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); + INFO("Finite-difference gradient norm = " + << finite_difference_gradient_norm); + INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); + INFO("Finite-difference Poisson norm = " + << finite_difference_poisson_norm); + INFO("Absolute gradient residual error = " << absolute_gradient_error); + INFO("Absolute Poisson residual error = " << absolute_poisson_error); + INFO("Absolute combined residual error = " << absolute_combined_error); + INFO("Gradient residual relative error = " << gradient_error); + INFO("Poisson residual relative error = " << poisson_error); + INFO("Combined residual relative error = " << combined_error); + + REQUIRE(std::isfinite(gradient_error)); + REQUIRE(std::isfinite(poisson_error)); + REQUIRE(std::isfinite(combined_error)); + + CHECK_THAT(gradient_error, WithinAbs(0.0, comparison_tolerance)); + CHECK_THAT(poisson_error, WithinAbs(0.0, comparison_tolerance)); + CHECK_THAT(combined_error, WithinAbs(0.0, comparison_tolerance)); } + } } TEST_CASE( "Gravity Field Jacobian Displacement Difference Converges At Second Order", - tags::gravity_operator_convergence -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + tags::gravity_operator_convergence) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const gravity_layout layout = make_gravity_jacobian_layout(f); - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); - const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); + const gravity_layout layout = make_gravity_jacobian_layout(f); + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); + const mfem::Vector direction = + make_displacement_direction(layout, fields.displacement_direction_1); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + operators::context::gravity_field::GravityFieldRevisions revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - MPI_Comm communicator = f.mesh->GetComm(); + MPI_Comm communicator = f.mesh->GetComm(); - gravity_operator.Prepare(state, revisions); + gravity_operator.Prepare(state, revisions); - REQUIRE(linearization_context.IsPrepared()); + REQUIRE(linearization_context.IsPrepared()); - check_linearization_context_matches_state(linearization_context, state, layout.value_offsets(), communicator); + check_linearization_context_matches_state( + linearization_context, state, layout.value_offsets(), communicator); - mfem::Operator &gradient = gravity_operator.GetGradient(state); - REQUIRE(&gradient == static_cast(&gravity_jacobian)); + mfem::Operator &gradient = gravity_operator.GetGradient(state); + REQUIRE(&gradient == static_cast(&gravity_jacobian)); - mfem::Vector jacobian_action; - gradient.Mult(direction, jacobian_action); + mfem::Vector jacobian_action; + gradient.Mult(direction, jacobian_action); - REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); + REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); - const double jacobian_action_norm = global_vector_norm(jacobian_action, communicator); + const double jacobian_action_norm = + global_vector_norm(jacobian_action, communicator); - INFO("Jacobian action norm = " << jacobian_action_norm); - REQUIRE(jacobian_action_norm > 1.0e-12); + INFO("Jacobian action norm = " << jacobian_action_norm); + REQUIRE(jacobian_action_norm > 1.0e-12); - auto evaluate_geometry_centered_difference = [&](const double difference_step) { + auto evaluate_geometry_centered_difference = + [&](const double difference_step) { mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(difference_step, direction); @@ -2930,639 +2792,418 @@ TEST_CASE( finite_difference /= 2.0 * difference_step; return finite_difference; - }; + }; - constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; + constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; - std::array errors{}; - std::array finite_difference_norms{}; + std::array errors{}; + std::array finite_difference_norms{}; - for (std::size_t i = 0; i < difference_steps.size(); ++i) { - const mfem::Vector finite_difference = evaluate_geometry_centered_difference(difference_steps[i]); + for (std::size_t i = 0; i < difference_steps.size(); ++i) { + const mfem::Vector finite_difference = + evaluate_geometry_centered_difference(difference_steps[i]); - finite_difference_norms[i] = global_vector_norm(finite_difference, communicator); - errors[i] = global_relative_error(finite_difference, jacobian_action, communicator); + finite_difference_norms[i] = + global_vector_norm(finite_difference, communicator); + errors[i] = + global_relative_error(finite_difference, jacobian_action, communicator); - INFO( - "Step = " << difference_steps[i] << ", finite-difference norm = " << finite_difference_norms[i] - << ", relative error = " << errors[i] - ); + INFO("Step = " << difference_steps[i] << ", finite-difference norm = " + << finite_difference_norms[i] + << ", relative error = " << errors[i]); - REQUIRE(finite_difference_norms[i] > 1.0e-12); - REQUIRE(std::isfinite(errors[i])); - REQUIRE(errors[i] > 0.0); - } + REQUIRE(finite_difference_norms[i] > 1.0e-12); + REQUIRE(std::isfinite(errors[i])); + REQUIRE(errors[i] > 0.0); + } - const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); - const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); + const double first_observed_order = + std::log(errors[0] / errors[1]) / std::log(2.0); + const double second_observed_order = + std::log(errors[1] / errors[2]) / std::log(2.0); - INFO( - "Difference steps = [" << difference_steps[0] << ", " << difference_steps[1] << ", " << difference_steps[2] - << "]" - ); - INFO( - "Finite-difference norms = [" << finite_difference_norms[0] << ", " << finite_difference_norms[1] << ", " - << finite_difference_norms[2] << "]" - ); - INFO("Relative errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); - INFO("First observed convergence order = " << first_observed_order); - INFO("Second observed convergence order = " << second_observed_order); + INFO("Difference steps = [" << difference_steps[0] << ", " + << difference_steps[1] << ", " + << difference_steps[2] << "]"); + INFO("Finite-difference norms = [" << finite_difference_norms[0] << ", " + << finite_difference_norms[1] << ", " + << finite_difference_norms[2] << "]"); + INFO("Relative errors = [" << errors[0] << ", " << errors[1] << ", " + << errors[2] << "]"); + INFO("First observed convergence order = " << first_observed_order); + INFO("Second observed convergence order = " << second_observed_order); - REQUIRE(std::isfinite(first_observed_order)); - REQUIRE(std::isfinite(second_observed_order)); + REQUIRE(std::isfinite(first_observed_order)); + REQUIRE(std::isfinite(second_observed_order)); - CHECK(errors[1] < errors[0]); - CHECK(errors[2] < errors[1]); - CHECK(first_observed_order > 1.8); - CHECK(second_observed_order > 1.8); + CHECK(errors[1] < errors[0]); + CHECK(errors[2] < errors[1]); + CHECK(first_observed_order > 1.8); + CHECK(second_observed_order > 1.8); } -TEST_CASE( - "Gravity Field Jacobian Complete Coupled Direction Matches Centered " - "Differences", - tags::gravity_operator_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Gravity Field Jacobian Complete Coupled Direction Matches Centered " + "Differences", + tags::gravity_operator_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const gravity_layout layout = make_gravity_jacobian_layout(f); - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); + const gravity_layout layout = make_gravity_jacobian_layout(f); + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); - mfem::Vector direction = make_combined_fixed_geometry_direction(layout); - set_value_block(direction, layout, displacement_block, fields.displacement_direction_2); + mfem::Vector direction = make_combined_fixed_geometry_direction(layout); + set_value_block(direction, layout, displacement_block, + fields.displacement_direction_2); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - operators::context::gravity_field::GravityFieldRevisions base_revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + operators::context::gravity_field::GravityFieldRevisions base_revisions{ + .discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - MPI_Comm communicator = f.mesh->GetComm(); + MPI_Comm communicator = f.mesh->GetComm(); - REQUIRE(state.Size() == layout.value_offsets().Last()); - REQUIRE(direction.Size() == layout.value_offsets().Last()); - REQUIRE(global_vector_norm(direction, communicator) > 1.0e-12); + REQUIRE(state.Size() == layout.value_offsets().Last()); + REQUIRE(direction.Size() == layout.value_offsets().Last()); + REQUIRE(global_vector_norm(direction, communicator) > 1.0e-12); - gravity_operator.Prepare(state, base_revisions); + gravity_operator.Prepare(state, base_revisions); - REQUIRE(linearization_context.IsPrepared()); + REQUIRE(linearization_context.IsPrepared()); - check_linearization_context_matches_state(linearization_context, state, layout.value_offsets(), communicator); + check_linearization_context_matches_state( + linearization_context, state, layout.value_offsets(), communicator); - mfem::Operator &gradient = gravity_operator.GetGradient(state); - REQUIRE(&gradient == static_cast(&gravity_jacobian)); + mfem::Operator &gradient = gravity_operator.GetGradient(state); + REQUIRE(&gradient == static_cast(&gravity_jacobian)); - mfem::Vector jacobian_action; - gradient.Mult(direction, jacobian_action); + mfem::Vector jacobian_action; + gradient.Mult(direction, jacobian_action); - REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); + REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); - constexpr double difference_step = 1.0e-5; + constexpr double difference_step = 1.0e-5; - mfem::Vector plus_state(state); - mfem::Vector minus_state(state); - plus_state.Add(difference_step, direction); - minus_state.Add(-difference_step, direction); + mfem::Vector plus_state(state); + mfem::Vector minus_state(state); + plus_state.Add(difference_step, direction); + minus_state.Add(-difference_step, direction); - auto plus_revisions = base_revisions; - plus_revisions.displacement.value++; - plus_revisions.density.value++; - plus_revisions.gravity_gradient.value++; - plus_revisions.gravity_potential.value++; + auto plus_revisions = base_revisions; + plus_revisions.displacement.value++; + plus_revisions.density.value++; + plus_revisions.gravity_gradient.value++; + plus_revisions.gravity_potential.value++; - mfem::Vector plus_residual; - gravity_operator.Prepare(plus_state, plus_revisions); - gravity_operator.Mult(plus_state, plus_residual); + mfem::Vector plus_residual; + gravity_operator.Prepare(plus_state, plus_revisions); + gravity_operator.Mult(plus_state, plus_residual); - REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); + REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); - auto minus_revisions = plus_revisions; - minus_revisions.displacement.value++; - minus_revisions.density.value++; - minus_revisions.gravity_gradient.value++; - minus_revisions.gravity_potential.value++; + auto minus_revisions = plus_revisions; + minus_revisions.displacement.value++; + minus_revisions.density.value++; + minus_revisions.gravity_gradient.value++; + minus_revisions.gravity_potential.value++; - mfem::Vector minus_residual; - gravity_operator.Prepare(minus_state, minus_revisions); - gravity_operator.Mult(minus_state, minus_residual); + mfem::Vector minus_residual; + gravity_operator.Prepare(minus_state, minus_revisions); + gravity_operator.Mult(minus_state, minus_residual); - REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); + REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); - mfem::Vector finite_difference(plus_residual); - finite_difference -= minus_residual; - finite_difference /= 2.0 * difference_step; + mfem::Vector finite_difference(plus_residual); + finite_difference -= minus_residual; + finite_difference /= 2.0 * difference_step; - const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); - const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); - const mfem::Vector finite_difference_gradient = - get_residual_block(finite_difference, layout, gravity_gradient_residual_block); - const mfem::Vector finite_difference_poisson = - get_residual_block(finite_difference, layout, gravity_poisson_residual_block); + const mfem::Vector gradient_action = get_residual_block( + jacobian_action, layout, gravity_gradient_residual_block); + const mfem::Vector poisson_action = get_residual_block( + jacobian_action, layout, gravity_poisson_residual_block); + const mfem::Vector finite_difference_gradient = get_residual_block( + finite_difference, layout, gravity_gradient_residual_block); + const mfem::Vector finite_difference_poisson = get_residual_block( + finite_difference, layout, gravity_poisson_residual_block); - const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator); - const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator); - const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator); - const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator); + const double jacobian_gradient_norm = + global_vector_norm(gradient_action, communicator); + const double jacobian_poisson_norm = + global_vector_norm(poisson_action, communicator); + const double finite_difference_gradient_norm = + global_vector_norm(finite_difference_gradient, communicator); + const double finite_difference_poisson_norm = + global_vector_norm(finite_difference_poisson, communicator); - constexpr double nonzero_tolerance = 1.0e-12; + constexpr double nonzero_tolerance = 1.0e-12; - REQUIRE(jacobian_gradient_norm > nonzero_tolerance); - REQUIRE(jacobian_poisson_norm > nonzero_tolerance); - REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); - REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); + REQUIRE(jacobian_gradient_norm > nonzero_tolerance); + REQUIRE(jacobian_poisson_norm > nonzero_tolerance); + REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); + REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); - mfem::Vector gradient_difference(gradient_action); - gradient_difference -= finite_difference_gradient; + mfem::Vector gradient_difference(gradient_action); + gradient_difference -= finite_difference_gradient; - mfem::Vector poisson_difference(poisson_action); - poisson_difference -= finite_difference_poisson; + mfem::Vector poisson_difference(poisson_action); + poisson_difference -= finite_difference_poisson; - mfem::Vector combined_difference(jacobian_action); - combined_difference -= finite_difference; + mfem::Vector combined_difference(jacobian_action); + combined_difference -= finite_difference; - const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator); - const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator); - const double absolute_combined_error = global_vector_norm(combined_difference, communicator); + const double absolute_gradient_error = + global_vector_norm(gradient_difference, communicator); + const double absolute_poisson_error = + global_vector_norm(poisson_difference, communicator); + const double absolute_combined_error = + global_vector_norm(combined_difference, communicator); - const double gradient_error = global_relative_error(gradient_action, finite_difference_gradient, communicator); - const double poisson_error = global_relative_error(poisson_action, finite_difference_poisson, communicator); - const double combined_error = global_relative_error(jacobian_action, finite_difference, communicator); + const double gradient_error = global_relative_error( + gradient_action, finite_difference_gradient, communicator); + const double poisson_error = global_relative_error( + poisson_action, finite_difference_poisson, communicator); + const double combined_error = + global_relative_error(jacobian_action, finite_difference, communicator); - INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); - INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm); - INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); - INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm); - INFO("Absolute gradient residual error = " << absolute_gradient_error); - INFO("Absolute Poisson residual error = " << absolute_poisson_error); - INFO("Absolute combined residual error = " << absolute_combined_error); - INFO("Gradient residual relative error = " << gradient_error); - INFO("Poisson residual relative error = " << poisson_error); - INFO("Combined residual relative error = " << combined_error); + INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); + INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm); + INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); + INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm); + INFO("Absolute gradient residual error = " << absolute_gradient_error); + INFO("Absolute Poisson residual error = " << absolute_poisson_error); + INFO("Absolute combined residual error = " << absolute_combined_error); + INFO("Gradient residual relative error = " << gradient_error); + INFO("Poisson residual relative error = " << poisson_error); + INFO("Combined residual relative error = " << combined_error); - REQUIRE(std::isfinite(gradient_error)); - REQUIRE(std::isfinite(poisson_error)); - REQUIRE(std::isfinite(combined_error)); + REQUIRE(std::isfinite(gradient_error)); + REQUIRE(std::isfinite(poisson_error)); + REQUIRE(std::isfinite(combined_error)); - CHECK_THAT(gradient_error, WithinAbs(0.0, 5.0e-7)); - CHECK_THAT(poisson_error, WithinAbs(0.0, 5.0e-7)); - CHECK_THAT(combined_error, WithinAbs(0.0, 5.0e-7)); + CHECK_THAT(gradient_error, WithinAbs(0.0, 5.0e-7)); + CHECK_THAT(poisson_error, WithinAbs(0.0, 5.0e-7)); + CHECK_THAT(combined_error, WithinAbs(0.0, 5.0e-7)); } TEST_CASE( "Reduced Gravity Field Operator Solves Deformed Gravity System With MINRES", - tags::gravity_operator_integration -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + tags::gravity_operator_integration) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const gravity_layout layout = make_gravity_jacobian_layout(f); - const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); - const mfem::Vector density_true = make_source_variation_density(f); - const mfem::Vector displacement_true = fields.displacement; + const gravity_layout layout = make_gravity_jacobian_layout(f); + const HdivMassVariationTestFields fields = + make_hdiv_mass_variation_test_fields(f); + const mfem::Vector density_true = make_source_variation_density(f); + const mfem::Vector displacement_true = fields.displacement; - mfem::ParGridFunction legacy_displacement(f.displacementFes.get()); - legacy_displacement.SetFromTrueDofs(displacement_true); - f.mapping->SetDisplacement(legacy_displacement); - physics::update_stiffness_matrix(f); + operators::context::gravity_field::GravityFieldLinearizationContext + linearization_context(f, *f.domainMapperStateless); + const mfem::Vector density = + linearization_context.GetDensityMap().gather(density_true); + const mfem::Vector displacement = + linearization_context.GetDisplacementMap().gather(displacement_true); + operators::GravityFieldJacobianOperator gravity_jacobian( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), layout.residual_offsets()); + operators::GravityFieldOperator gravity_operator( + f, *f.domainMapperStateless, linearization_context, + layout.value_offsets(), gravity_jacobian); - REQUIRE(f.gravityContext.b_form != nullptr); - REQUIRE(f.gravityContext.BT != nullptr); - REQUIRE(f.gravityContext.block_prec != nullptr); + operators::context::gravity_field::GravityFieldGeometryContext + reduced_geometry_context(f, *f.domainMapperStateless); + operators::ReducedGravityFieldOperator reduced_operator( + gravity_operator, reduced_geometry_context, displacement); + operators::ReducedGravityFieldPreconditioner reduced_preconditioner( + f, reduced_geometry_context); - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - const mfem::Vector density = linearization_context.GetDensityMap().gather(density_true); - const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(displacement_true); - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); + REQUIRE(reduced_geometry_context.IsPrepared()); + REQUIRE(reduced_operator.Width() == layout.residual_offsets().Last()); + REQUIRE(reduced_operator.Height() == layout.residual_offsets().Last()); + REQUIRE(reduced_operator.Width() == reduced_operator.Height()); + REQUIRE(reduced_preconditioner.Width() == reduced_operator.Width()); + REQUIRE(reduced_preconditioner.Height() == reduced_operator.Height()); + REQUIRE(reduced_preconditioner.GetOffsets().Size() == + reduced_operator.GetGravityOffsets().Size()); + for (int i = 0; i < reduced_preconditioner.GetOffsets().Size(); ++i) { + CHECK(reduced_preconditioner.GetOffsets()[i] == + reduced_operator.GetGravityOffsets()[i]); + } - operators::context::gravity_field::GravityFieldGeometryContext reduced_geometry_context( - f, *f.domainMapperStateless - ); - operators::ReducedGravityFieldOperator reduced_operator(gravity_operator, reduced_geometry_context, displacement); + const std::uint64_t mass_preparations_before_solve = + reduced_geometry_context.GetMassOperator().GetPreparationCount(); + const std::uint64_t source_preparations_before_solve = + reduced_geometry_context.GetSourceOperator().GetPreparationCount(); - REQUIRE(reduced_geometry_context.IsPrepared()); - REQUIRE(reduced_operator.Width() == layout.residual_offsets().Last()); - REQUIRE(reduced_operator.Height() == layout.residual_offsets().Last()); - REQUIRE(reduced_operator.Width() == reduced_operator.Height()); + REQUIRE(mass_preparations_before_solve > 0); + REQUIRE(source_preparations_before_solve > 0); - const std::uint64_t mass_preparations_before_solve = - reduced_geometry_context.GetMassOperator().GetPreparationCount(); - const std::uint64_t source_preparations_before_solve = - reduced_geometry_context.GetSourceOperator().GetPreparationCount(); + mfem::Vector right_hand_side; + reduced_operator.BuildRightHandSide(density, right_hand_side); - REQUIRE(mass_preparations_before_solve > 0); - REQUIRE(source_preparations_before_solve > 0); + REQUIRE(right_hand_side.Size() == reduced_operator.Height()); - mfem::Vector right_hand_side; - reduced_operator.BuildRightHandSide(density, right_hand_side); + MPI_Comm communicator = f.mesh->GetComm(); + const double right_hand_side_norm = + global_norm(right_hand_side, communicator); - REQUIRE(right_hand_side.Size() == reduced_operator.Height()); + INFO("Reduced gravity-system size = " << reduced_operator.Height()); + INFO("Gravity right-hand-side norm = " << right_hand_side_norm); + INFO("Mass preparations before solve = " << mass_preparations_before_solve); + INFO("Source preparations before solve = " + << source_preparations_before_solve); - MPI_Comm communicator = f.mesh->GetComm(); - const double right_hand_side_norm = global_norm(right_hand_side, communicator); + REQUIRE(right_hand_side_norm > 0.0); - INFO("Reduced gravity-system size = " << reduced_operator.Height()); - INFO("Gravity right-hand-side norm = " << right_hand_side_norm); - INFO("Mass preparations before solve = " << mass_preparations_before_solve); - INFO("Source preparations before solve = " << source_preparations_before_solve); + mfem::Vector gravity_state(reduced_operator.Width()); + gravity_state = 0.0; - REQUIRE(right_hand_side_norm > 0.0); + constexpr double relative_solver_tolerance = 1.0e-14; + constexpr double absolute_solver_tolerance = 1.0e-14; + const int maximum_iterations = 2000; - mfem::Vector gravity_state(reduced_operator.Width()); - gravity_state = 0.0; + mfem::MINRESSolver minres(communicator); + minres.SetOperator(reduced_operator); + minres.SetPreconditioner(reduced_preconditioner); + minres.SetRelTol(relative_solver_tolerance); + minres.SetAbsTol(absolute_solver_tolerance); + minres.SetMaxIter(maximum_iterations); + minres.SetPrintLevel(0); - constexpr double relative_solver_tolerance = 1.0e-14; - constexpr double absolute_solver_tolerance = 1.0e-14; - const int maximum_iterations = 2000; + MEAN_FIELD_PROFILE_RESET(); + MEAN_FIELD_PROFILE_CALL_WARMUP("MINRES total", 0, + minres.Mult(right_hand_side, gravity_state)); + MEAN_FIELD_PROFILE_PRINT(communicator); - mfem::MINRESSolver minres(communicator); - minres.SetOperator(reduced_operator); - minres.SetPreconditioner(*f.gravityContext.block_prec); - minres.SetRelTol(relative_solver_tolerance); - minres.SetAbsTol(absolute_solver_tolerance); - minres.SetMaxIter(maximum_iterations); - minres.SetPrintLevel(1); + REQUIRE(gravity_state.Size() == reduced_operator.Width()); + const std::uint64_t mass_preparations_after_solve = + reduced_geometry_context.GetMassOperator().GetPreparationCount(); + const std::uint64_t source_preparations_after_solve = + reduced_geometry_context.GetSourceOperator().GetPreparationCount(); - MEAN_FIELD_PROFILE_RESET(); - MEAN_FIELD_PROFILE_CALL_WARMUP("MINRES total", 0, minres.Mult(right_hand_side, gravity_state)); - MEAN_FIELD_PROFILE_PRINT(communicator); + INFO("Mass preparations after solve = " << mass_preparations_after_solve); + INFO("Source preparations after solve = " << source_preparations_after_solve); - REQUIRE(gravity_state.Size() == reduced_operator.Width()); + CHECK(mass_preparations_after_solve == mass_preparations_before_solve); + CHECK(source_preparations_after_solve == source_preparations_before_solve); - const std::uint64_t mass_preparations_after_solve = - reduced_geometry_context.GetMassOperator().GetPreparationCount(); - const std::uint64_t source_preparations_after_solve = - reduced_geometry_context.GetSourceOperator().GetPreparationCount(); + mfem::Vector operator_action; + reduced_operator.Mult(gravity_state, operator_action); - INFO("Mass preparations after solve = " << mass_preparations_after_solve); - INFO("Source preparations after solve = " << source_preparations_after_solve); + mfem::Vector reduced_residual(operator_action); + reduced_residual -= right_hand_side; - CHECK(mass_preparations_after_solve == mass_preparations_before_solve); - CHECK(source_preparations_after_solve == source_preparations_before_solve); + const mfem::Vector gradient_residual = get_residual_block( + reduced_residual, layout, gravity_gradient_residual_block); + const mfem::Vector poisson_residual = get_residual_block( + reduced_residual, layout, gravity_poisson_residual_block); + const mfem::Vector solved_gravity_gradient = get_residual_block( + gravity_state, layout, gravity_gradient_residual_block); + const mfem::Vector solved_gravity_potential = + get_residual_block(gravity_state, layout, gravity_poisson_residual_block); - mfem::Vector operator_action; - reduced_operator.Mult(gravity_state, operator_action); + const double gravity_state_norm = global_norm(gravity_state, communicator); + const double gravity_gradient_norm = + global_norm(solved_gravity_gradient, communicator); + const double gravity_potential_norm = + global_norm(solved_gravity_potential, communicator); + const double residual_norm = global_norm(reduced_residual, communicator); + const double gradient_residual_norm = + global_norm(gradient_residual, communicator); + const double poisson_residual_norm = + global_norm(poisson_residual, communicator); + const double relative_residual = residual_norm / right_hand_side_norm; + const double relative_gradient_residual = + gradient_residual_norm / right_hand_side_norm; + const double relative_poisson_residual = + poisson_residual_norm / right_hand_side_norm; - mfem::Vector reduced_residual(operator_action); - reduced_residual -= right_hand_side; + INFO("MINRES converged = " << minres.GetConverged()); + INFO("MINRES iterations = " << minres.GetNumIterations()); + INFO("MINRES reported final norm = " << minres.GetFinalNorm()); + INFO("Gravity-state norm = " << gravity_state_norm); + INFO("Solved gravity-gradient norm = " << gravity_gradient_norm); + INFO("Solved gravity-potential norm = " << gravity_potential_norm); + INFO("Direct residual norm = " << residual_norm); + INFO("Direct relative residual = " << relative_residual); + INFO("Relative gradient-equation residual = " << relative_gradient_residual); + INFO("Relative Poisson-equation residual = " << relative_poisson_residual); - const mfem::Vector gradient_residual = - get_residual_block(reduced_residual, layout, gravity_gradient_residual_block); - const mfem::Vector poisson_residual = get_residual_block(reduced_residual, layout, gravity_poisson_residual_block); - const mfem::Vector solved_gravity_gradient = - get_residual_block(gravity_state, layout, gravity_gradient_residual_block); - const mfem::Vector solved_gravity_potential = - get_residual_block(gravity_state, layout, gravity_poisson_residual_block); + CHECK(minres.GetConverged()); + CHECK(minres.GetNumIterations() <= maximum_iterations); + REQUIRE(gravity_state_norm > 0.0); + REQUIRE(gravity_gradient_norm > 0.0); + REQUIRE(gravity_potential_norm > 0.0); - const double gravity_state_norm = global_norm(gravity_state, communicator); - const double gravity_gradient_norm = global_norm(solved_gravity_gradient, communicator); - const double gravity_potential_norm = global_norm(solved_gravity_potential, communicator); - const double residual_norm = global_norm(reduced_residual, communicator); - const double gradient_residual_norm = global_norm(gradient_residual, communicator); - const double poisson_residual_norm = global_norm(poisson_residual, communicator); - const double relative_residual = residual_norm / right_hand_side_norm; - const double relative_gradient_residual = gradient_residual_norm / right_hand_side_norm; - const double relative_poisson_residual = poisson_residual_norm / right_hand_side_norm; + constexpr double direct_residual_tolerance = 1.0e-11; - INFO("MINRES converged = " << minres.GetConverged()); - INFO("MINRES iterations = " << minres.GetNumIterations()); - INFO("MINRES reported final norm = " << minres.GetFinalNorm()); - INFO("Gravity-state norm = " << gravity_state_norm); - INFO("Solved gravity-gradient norm = " << gravity_gradient_norm); - INFO("Solved gravity-potential norm = " << gravity_potential_norm); - INFO("Direct residual norm = " << residual_norm); - INFO("Direct relative residual = " << relative_residual); - INFO("Relative gradient-equation residual = " << relative_gradient_residual); - INFO("Relative Poisson-equation residual = " << relative_poisson_residual); + CHECK_THAT(relative_residual, WithinAbs(0.0, direct_residual_tolerance)); + CHECK_THAT(relative_gradient_residual, + WithinAbs(0.0, direct_residual_tolerance)); + CHECK_THAT(relative_poisson_residual, + WithinAbs(0.0, direct_residual_tolerance)); - CHECK(minres.GetConverged()); - CHECK(minres.GetNumIterations() <= maximum_iterations); - REQUIRE(gravity_state_norm > 0.0); - REQUIRE(gravity_gradient_norm > 0.0); - REQUIRE(gravity_potential_norm > 0.0); + mfem::Vector full_state(layout.value_offsets().Last()); + full_state = 0.0; - constexpr double direct_residual_tolerance = 1.0e-11; + set_value_block(full_state, layout, density_block, density); + set_value_block(full_state, layout, displacement_block, displacement); + set_value_block(full_state, layout, gravity_gradient_block, + solved_gravity_gradient); + set_value_block(full_state, layout, gravity_potential_block, + solved_gravity_potential); - CHECK_THAT(relative_residual, WithinAbs(0.0, direct_residual_tolerance)); - CHECK_THAT(relative_gradient_residual, WithinAbs(0.0, direct_residual_tolerance)); - CHECK_THAT(relative_poisson_residual, WithinAbs(0.0, direct_residual_tolerance)); + const operators::context::gravity_field::GravityFieldRevisions + full_state_revisions{.discretization = {1}, + .displacement = {1}, + .density = {1}, + .gravity_gradient = {1}, + .gravity_potential = {1}}; - mfem::Vector full_state(layout.value_offsets().Last()); - full_state = 0.0; + gravity_operator.Prepare(full_state, full_state_revisions); - set_value_block(full_state, layout, density_block, density); - set_value_block(full_state, layout, displacement_block, displacement); - set_value_block(full_state, layout, gravity_gradient_block, solved_gravity_gradient); - set_value_block(full_state, layout, gravity_potential_block, solved_gravity_potential); + REQUIRE(linearization_context.IsPrepared()); - const operators::context::gravity_field::GravityFieldRevisions full_state_revisions{ - .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} - }; + check_linearization_context_matches_state( + linearization_context, full_state, layout.value_offsets(), communicator); - gravity_operator.Prepare(full_state, full_state_revisions); + mfem::Vector full_residual; + gravity_operator.Mult(full_state, full_residual); - REQUIRE(linearization_context.IsPrepared()); + REQUIRE(full_residual.Size() == layout.residual_offsets().Last()); - check_linearization_context_matches_state(linearization_context, full_state, layout.value_offsets(), communicator); + mfem::Vector residual_representation_difference(full_residual); + residual_representation_difference -= reduced_residual; - mfem::Vector full_residual; - gravity_operator.Mult(full_state, full_residual); + const double residual_representation_error = + global_norm(residual_representation_difference, communicator) / + right_hand_side_norm; + const double full_relative_residual = + global_norm(full_residual, communicator) / right_hand_side_norm; - REQUIRE(full_residual.Size() == layout.residual_offsets().Last()); + INFO("Full gravity residual relative norm = " << full_relative_residual); + INFO("Reduced/full residual representation error = " + << residual_representation_error); - mfem::Vector residual_representation_difference(full_residual); - residual_representation_difference -= reduced_residual; - - const double residual_representation_error = - global_norm(residual_representation_difference, communicator) / right_hand_side_norm; - const double full_relative_residual = global_norm(full_residual, communicator) / right_hand_side_norm; - - INFO("Full gravity residual relative norm = " << full_relative_residual); - INFO("Reduced/full residual representation error = " << residual_representation_error); - - CHECK_THAT(full_relative_residual, WithinAbs(0.0, direct_residual_tolerance)); - CHECK_THAT(residual_representation_error, WithinAbs(0.0, 1.0e-12)); -} - -TEST_CASE( - "Gravity Hdiv Operator Difference Is Localized To Vacuum Compactification", - tags::gravity_legacy -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.compactificationCoordinate != nullptr); - - using form = blocks::gravity_field_form; - - constexpr auto displacement_block = - mean_field::utils::blocks::get_value_block(blocks::displacement_field.geometry_term); - - constexpr auto gravity_gradient_block = - mean_field::utils::blocks::get_value_block(blocks::gravity_field.gradient_term); - - constexpr auto gravity_gradient_residual_block = - mean_field::utils::blocks::get_residual_block(blocks::gravity_field.gradient_term); - - const blocks::form_layout layout = make_gravity_layout(f); - - const mfem::Vector gravity_gradient_true = make_full_support_gravity_gradient(f); - - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - REQUIRE(global_vector_norm(gravity_gradient_true, communicator) > 0.0); - - for (const bool deformed : std::array{false, true}) { - DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { - const mfem::Vector displacement_true = make_stateless_reference_displacement(f, deformed); - - /* - * Prepare the legacy geometry and assemble its PA operator. - * The legacy mapper performs its vacuum map without consulting - * the exterior-coordinate GridFunction. - */ - mfem::ParGridFunction legacy_displacement(f.displacementFes.get()); - legacy_displacement.SetFromTrueDofs(displacement_true); - f.mapping->SetDisplacement(legacy_displacement); - - physics::update_stiffness_matrix(f); - - REQUIRE(f.gravityContext.m_form != nullptr); - - operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( - f, *f.domainMapperStateless - ); - - operators::GravityFieldJacobianOperator gravity_jacobian( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() - ); - - operators::GravityFieldOperator gravity_operator( - f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian - ); - - mfem::Vector state(layout.value_offsets().Last()); - state = 0.0; - - for (int i = 0; i < displacement_true.Size(); ++i) { - state(layout.offset(displacement_block) + i) = displacement_true(i); - } - - for (int i = 0; i < gravity_gradient_true.Size(); ++i) { - state(layout.offset(gravity_gradient_block) + i) = gravity_gradient_true(i); - } - - const operators::context::gravity_field::GravityFieldRevisions revisions{ - .discretization = {1}, - .displacement = {1}, - .density = {1}, - .gravity_gradient = {1}, - .gravity_potential = {1} - }; - - gravity_operator.Prepare(state, revisions); - - mfem::Vector new_residual; - gravity_operator.Mult(state, new_residual); - - mfem::Vector new_operator_mass_action(layout.size(gravity_gradient_residual_block)); - - for (int i = 0; i < new_operator_mass_action.Size(); ++i) { - new_operator_mass_action(i) = new_residual(layout.offset(gravity_gradient_residual_block) + i); - } - - mfem::Vector legacy_operator_mass_action(f.gravityFluxFes->GetTrueVSize()); - - legacy_operator_mass_action = 0.0; - f.gravityContext.m_form->Mult(gravity_gradient_true, legacy_operator_mass_action); - - const StatelessHDivMassReference new_reference = - evaluate_stateless_hdiv_mass_quadrature_reference(f, gravity_gradient_true, displacement_true); - const StatelessHDivMassReference legacy_reference = - assemble_legacy_hdiv_mass_reference(f, gravity_gradient_true); - - mfem::Vector new_decomposed_action(new_reference.stellar_action); - new_decomposed_action += new_reference.vacuum_action; - - mfem::Vector legacy_decomposed_action(legacy_reference.stellar_action); - legacy_decomposed_action += legacy_reference.vacuum_action; - - mfem::Vector total_gap(new_reference.total_action); - total_gap -= legacy_reference.total_action; - - mfem::Vector stellar_gap(new_reference.stellar_action); - stellar_gap -= legacy_reference.stellar_action; - - mfem::Vector vacuum_gap(new_reference.vacuum_action); - vacuum_gap -= legacy_reference.vacuum_action; - - mfem::Vector decomposed_gap(stellar_gap); - decomposed_gap += vacuum_gap; - - mfem::Vector unexplained_by_vacuum(total_gap); - unexplained_by_vacuum -= vacuum_gap; - - const double new_operator_reference_error = - global_relative_difference(new_operator_mass_action, new_reference.total_action, communicator); - - const double legacy_operator_reference_error = - global_relative_difference(legacy_operator_mass_action, legacy_reference.total_action, communicator); - - const double new_decomposition_error = - global_relative_difference(new_reference.total_action, new_decomposed_action, communicator); - - const double legacy_decomposition_error = - global_relative_difference(legacy_reference.total_action, legacy_decomposed_action, communicator); - - const double gap_decomposition_error = global_relative_difference(total_gap, decomposed_gap, communicator); - - const double stellar_relative_difference = - global_relative_difference(new_reference.stellar_action, legacy_reference.stellar_action, communicator); - - const double vacuum_relative_difference = - global_relative_difference(new_reference.vacuum_action, legacy_reference.vacuum_action, communicator); - - const double total_relative_difference = - global_relative_difference(new_reference.total_action, legacy_reference.total_action, communicator); - - const double total_gap_norm = global_vector_norm(total_gap, communicator); - - const double stellar_gap_norm = global_vector_norm(stellar_gap, communicator); - - const double vacuum_gap_norm = global_vector_norm(vacuum_gap, communicator); - - const double unexplained_gap_norm = global_vector_norm(unexplained_by_vacuum, communicator); - - const double stellar_fraction_of_gap = - stellar_gap_norm / std::max(total_gap_norm, std::numeric_limits::epsilon()); - - const double unexplained_fraction_of_gap = - unexplained_gap_norm / std::max(total_gap_norm, std::numeric_limits::epsilon()); - - const double new_total_energy = - global_vector_dot(gravity_gradient_true, new_reference.total_action, communicator); - - const double legacy_total_energy = - global_vector_dot(gravity_gradient_true, legacy_reference.total_action, communicator); - - const double new_stellar_energy = - global_vector_dot(gravity_gradient_true, new_reference.stellar_action, communicator); - - const double legacy_stellar_energy = - global_vector_dot(gravity_gradient_true, legacy_reference.stellar_action, communicator); - - const double new_vacuum_energy = - global_vector_dot(gravity_gradient_true, new_reference.vacuum_action, communicator); - - const double legacy_vacuum_energy = - global_vector_dot(gravity_gradient_true, legacy_reference.vacuum_action, communicator); - - const double total_energy_gap = new_total_energy - legacy_total_energy; - - const double stellar_energy_gap = new_stellar_energy - legacy_stellar_energy; - - const double vacuum_energy_gap = new_vacuum_energy - legacy_vacuum_energy; - - const double energy_gap_decomposition_error = - std::abs(total_energy_gap - stellar_energy_gap - vacuum_energy_gap) / - std::max(std::abs(total_energy_gap), std::numeric_limits::epsilon()); - - INFO("Geometry = " << (deformed ? "deformed" : "identity")); - INFO("New operator/reference error = " << new_operator_reference_error); - INFO("Legacy operator/reference error = " << legacy_operator_reference_error); - INFO("New regional decomposition error = " << new_decomposition_error); - INFO("Legacy regional decomposition error = " << legacy_decomposition_error); - INFO("Gap decomposition error = " << gap_decomposition_error); - INFO("New/legacy total mass-action difference = " << total_relative_difference); - INFO("New/legacy stellar mass-action difference = " << stellar_relative_difference); - INFO("New/legacy vacuum mass-action difference = " << vacuum_relative_difference); - INFO("Total operator-gap norm = " << total_gap_norm); - INFO("Stellar operator-gap norm = " << stellar_gap_norm); - INFO("Vacuum operator-gap norm = " << vacuum_gap_norm); - INFO("Stellar fraction of operator gap = " << stellar_fraction_of_gap); - INFO("Operator gap unexplained by vacuum = " << unexplained_fraction_of_gap); - INFO("New total H(div) energy = " << new_total_energy); - INFO("Legacy total H(div) energy = " << legacy_total_energy); - INFO("New stellar H(div) energy = " << new_stellar_energy); - INFO("Legacy stellar H(div) energy = " << legacy_stellar_energy); - INFO("New vacuum H(div) energy = " << new_vacuum_energy); - INFO("Legacy vacuum H(div) energy = " << legacy_vacuum_energy); - INFO("Total H(div) energy gap = " << total_energy_gap); - INFO("Stellar H(div) energy gap = " << stellar_energy_gap); - INFO("Vacuum H(div) energy gap = " << vacuum_energy_gap); - INFO("Energy-gap decomposition error = " << energy_gap_decomposition_error); - - REQUIRE(new_reference.stellar_elements > 0); - REQUIRE(new_reference.vacuum_elements > 0); - REQUIRE(legacy_reference.stellar_elements > 0); - REQUIRE(legacy_reference.vacuum_elements > 0); - - REQUIRE(new_total_energy > 0.0); - REQUIRE(legacy_total_energy > 0.0); - REQUIRE(new_stellar_energy > 0.0); - REQUIRE(legacy_stellar_energy > 0.0); - REQUIRE(new_vacuum_energy > 0.0); - REQUIRE(legacy_vacuum_energy > 0.0); - - constexpr double operator_reference_tolerance = 1.0e-11; - constexpr double decomposition_tolerance = 5.0e-13; - constexpr double stellar_parity_tolerance = 1.0e-10; - constexpr double gap_localization_tolerance = 1.0e-9; - - CHECK_THAT(new_operator_reference_error, Catch::Matchers::WithinAbs(0.0, operator_reference_tolerance)); - - CHECK_THAT(legacy_operator_reference_error, Catch::Matchers::WithinAbs(0.0, operator_reference_tolerance)); - - CHECK_THAT(new_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); - - CHECK_THAT(legacy_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); - - CHECK_THAT(gap_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); - - CHECK_THAT(stellar_relative_difference, Catch::Matchers::WithinAbs(0.0, stellar_parity_tolerance)); - - /* - * The previous global comparison already showed a material - * difference. Confirm that this test continues to exercise it. - */ - REQUIRE(total_gap_norm > 1.0e-12); - REQUIRE(vacuum_gap_norm > 1.0e-12); - REQUIRE(vacuum_relative_difference > 1.0e-8); - - /* - * If these pass, the old/new difference has been directly - * localized to the vacuum compactification prescription. - */ - CHECK_THAT(stellar_fraction_of_gap, Catch::Matchers::WithinAbs(0.0, gap_localization_tolerance)); - - CHECK_THAT(unexplained_fraction_of_gap, Catch::Matchers::WithinAbs(0.0, gap_localization_tolerance)); - - CHECK_THAT(energy_gap_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); - } - } + CHECK_THAT(full_relative_residual, WithinAbs(0.0, direct_residual_tolerance)); + CHECK_THAT(residual_representation_error, WithinAbs(0.0, 1.0e-12)); } diff --git a/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp b/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp index 493c868..60674f1 100644 --- a/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp +++ b/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp @@ -9,848 +9,900 @@ import mean_field; import test_helpers; namespace hydrostatic_kernel_test_utils { - mfem::Vector project_scalar( - mfem::ParFiniteElementSpace &finiteElementSpace, - mfem::Coefficient &coefficient - ) { - mfem::ParGridFunction field(&finiteElementSpace); +mfem::Vector project_scalar(mfem::ParFiniteElementSpace &finiteElementSpace, + mfem::Coefficient &coefficient) { + mfem::ParGridFunction field(&finiteElementSpace); - field.ProjectCoefficient(coefficient); + field.ProjectCoefficient(coefficient); - mfem::Vector trueVector; - field.GetTrueDofs(trueVector); + mfem::Vector trueVector; + field.GetTrueDofs(trueVector); - return trueVector; - } - - mfem::Vector make_constant_field( - mfem::ParFiniteElementSpace &finiteElementSpace, - const double value - ) { - mfem::ConstantCoefficient coefficient(value); - - return project_scalar(finiteElementSpace, coefficient); - } - - mfem::Vector make_enthalpy(const mean_field::fem::FEM &f) { - mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { - return 1.10 + 0.035 * position(0) - 0.021 * position(1) + 0.014 * position(2); - }); - - return project_scalar(*f.enthalpyFes, coefficient); - } - - mfem::Vector make_potential(const mean_field::fem::FEM &f) { - mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { - return -0.72 + 0.018 * position(0) + 0.011 * position(1) - 0.025 * position(2); - }); - - return project_scalar(*f.gravityPotentialFes, coefficient); - } - - mean_field::physics::RigidRotation make_rotation() { - mfem::Vector angularVelocity(3); - - angularVelocity(0) = 0.21; - angularVelocity(1) = -0.13; - angularVelocity(2) = 0.48; - - mfem::Vector center(3); - - center(0) = 0.04; - center(1) = -0.03; - center(2) = 0.02; - - return mean_field::physics::RigidRotation(angularVelocity, center); - } - - mean_field::physics::RigidRotation make_zero_rotation() { - mfem::Vector angularVelocity(3); - mfem::Vector center(3); - - angularVelocity = 0.0; - center = 0.0; - - return mean_field::physics::RigidRotation(angularVelocity, center); - } - - mfem::Vector centered_difference( - const mfem::Vector &plusResidual, - const mfem::Vector &minusResidual, - const double epsilon - ) { - mfem::Vector difference(plusResidual); - - difference -= minusResidual; - difference *= 1.0 / (2.0 * epsilon); - - return difference; - } - - double sum_normalized_error( - const mfem::Vector &computed, - const mfem::Vector &reference, - const double normalization, - const MPI_Comm communicator - ) { - mfem::Vector difference(computed); - difference -= reference; - - return gravity_prepared_test_utils::global_norm(difference, communicator) / - std::max(normalization, std::numeric_limits::epsilon()); - } - - mfem::Vector make_vacuum_supported_potential(const mean_field::fem::FEM &f) { - mfem::Vector attributeValues(f.mesh->attributes.Max()); - - attributeValues = 0.0; - - const int vacuumAttribute = f.domainMapperStateless->GetVacuumElementAttribute(); - - for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size(); ++attributeIndex) { - const int attribute = f.mesh->attributes[attributeIndex]; - - if (attribute == vacuumAttribute) { - attributeValues(attribute - 1) = 1.0; - } - } - - mfem::PWConstCoefficient coefficient(attributeValues); - - return project_scalar(*f.gravityPotentialFes, coefficient); - } - - class HydrostaticEnthalpyMassOperator final : public mfem::Operator { - public: - HydrostaticEnthalpyMassOperator( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mfem::Vector &displacementTrue - ) - : mfem::Operator(f.enthalpyFes->GetTrueVSize()), - f_(f), - domainMapper_(domainMapper), - displacementTrue_(displacementTrue) { - } - - void Mult( - const mfem::Vector &input, - mfem::Vector &output - ) const override { - mean_field::operators::kernels::apply_hydrostatic_equilibrium_enthalpy_action( - f_, domainMapper_, input, displacementTrue_, output - ); - } - - private: - const mean_field::fem::FEM &f_; - - const mean_field::mapping::DomainMapperStateless &domainMapper_; - - const mfem::Vector &displacementTrue_; - }; -} // namespace hydrostatic_kernel_test_utils - -TEST_CASE( - "Rigid Rotation Potential Derivative Matches Centered Differences", - tags::barotrope &tags::hydro &tags::jacobian &tags::physics &tags::unit &tags::kernels -) { - const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); - - mfem::Vector position(3); - mfem::Vector direction(3); - - position(0) = 0.71; - position(1) = -0.42; - position(2) = 0.36; - - direction(0) = -0.17; - direction(1) = 0.29; - direction(2) = 0.11; - - constexpr double epsilon = 1.0e-7; - - mfem::Vector plusPosition(position); - mfem::Vector minusPosition(position); - - plusPosition.Add(epsilon, direction); - minusPosition.Add(-epsilon, direction); - - const double centeredDerivative = - (rotation.potential(plusPosition) - rotation.potential(minusPosition)) / (2.0 * epsilon); - - const double analyticDerivative = rotation.potential_directional_derivative(position, direction); - - const double relativeError = std::abs(centeredDerivative - analyticDerivative) / - std::max(std::abs(analyticDerivative), std::numeric_limits::epsilon()); - - INFO("Rigid-rotation derivative error = " << relativeError); - - CHECK(relativeError < 2.0e-9); + return trueVector; } -TEST_CASE( - "Hydrostatic Residual Vanishes For A Manufactured Rotating State", - tags::barotrope &tags::hydro &tags::integration &tags::kernels &tags::physics &tags::residuals -) { - auto args = test_utils::setup_args(); +mfem::Vector +make_constant_field(mfem::ParFiniteElementSpace &finiteElementSpace, + const double value) { + mfem::ConstantCoefficient coefficient(value); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + return project_scalar(finiteElementSpace, coefficient); +} - const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); +mfem::Vector make_enthalpy(const mean_field::fem::FEM &f) { + mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { + return 1.10 + 0.035 * position(0) - 0.021 * position(1) + + 0.014 * position(2); + }); - constexpr double bernoulliConstant = 0.73; - constexpr double potentialValue = -0.21; - constexpr double constantOffset = 0.40; + return project_scalar(*f.enthalpyFes, coefficient); +} - mfem::FunctionCoefficient enthalpyCoefficient([&rotation](const mfem::Vector &position) { - return bernoulliConstant - potentialValue + rotation.potential(position); - }); +mfem::Vector make_potential(const mean_field::fem::FEM &f) { + mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { + return -0.72 + 0.018 * position(0) + 0.011 * position(1) - + 0.025 * position(2); + }); - const mfem::Vector interpolatedEnthalpy = - hydrostatic_kernel_test_utils::project_scalar(*f.enthalpyFes, enthalpyCoefficient); + return project_scalar(*f.gravityPotentialFes, coefficient); +} - const mfem::Vector potential = - hydrostatic_kernel_test_utils::make_constant_field(*f.gravityPotentialFes, potentialValue); +mean_field::physics::RigidRotation make_rotation() { + mfem::Vector angularVelocity(3); - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 0.0); + angularVelocity(0) = 0.21; + angularVelocity(1) = -0.13; + angularVelocity(2) = 0.48; - const MPI_Comm communicator = f.mesh->GetComm(); + mfem::Vector center(3); - /* - * First measure the residual of the nodally interpolated - * analytic equilibrium. Because the order-3 enthalpy space - * cannot exactly represent the quadratic rotation potential - * on an order-4 curved mesh, this measures the representation - * floor rather than an algebraic residual. - */ - mfem::Vector interpolatedResidual; - mfem::Vector interpolatedReferenceResidual; + center(0) = 0.04; + center(1) = -0.03; + center(2) = 0.02; - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, interpolatedEnthalpy, potential, displacement, bernoulliConstant, - interpolatedResidual - ); + return mean_field::physics::RigidRotation(angularVelocity, center); +} - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, interpolatedEnthalpy, potential, displacement, - bernoulliConstant + constantOffset, interpolatedReferenceResidual - ); +mean_field::physics::RigidRotation make_zero_rotation() { + mfem::Vector angularVelocity(3); + mfem::Vector center(3); - const double interpolatedResidualNorm = - gravity_prepared_test_utils::global_norm(interpolatedResidual, communicator); + angularVelocity = 0.0; + center = 0.0; - const double interpolatedReferenceNorm = - gravity_prepared_test_utils::global_norm(interpolatedReferenceResidual, communicator); + return mean_field::physics::RigidRotation(angularVelocity, center); +} - REQUIRE(interpolatedReferenceNorm > 1.0e-12); +mfem::Vector centered_difference(const mfem::Vector &plusResidual, + const mfem::Vector &minusResidual, + const double epsilon) { + mfem::Vector difference(plusResidual); - const double representationFloor = interpolatedResidualNorm / interpolatedReferenceNorm; + difference -= minusResidual; + difference *= 1.0 / (2.0 * epsilon); - INFO("Interpolated rotating-state residual norm = " << interpolatedResidualNorm); + return difference; +} - INFO( - "Interpolated rotating-state relative " - "representation floor = " - << representationFloor - ); +double sum_normalized_error(const mfem::Vector &computed, + const mfem::Vector &reference, + const double normalization, + const MPI_Comm communicator) { + mfem::Vector difference(computed); + difference -= reference; - /* - * This remains an independent physical/sign check. A wrong - * rotation sign or coordinate convention would produce an - * order-unity error rather than the observed projection floor. - */ - CHECK(representationFloor < 2.0e-5); + return gravity_prepared_test_utils::global_norm(difference, communicator) / + std::max(normalization, std::numeric_limits::epsilon()); +} - /* - * Construct the weakly manufactured discrete equilibrium. - * - * If r_I is the residual of the nodal interpolant, solve - * - * M_h delta_h = -r_I, - * - * where M_h is exactly the stellar-domain enthalpy action. - * Then h_I + delta_h satisfies the discrete weak equilibrium. - * - * Vacuum-only enthalpy DOFs form a nullspace, but the right-hand - * side is in the range of M_h. Starting CG from zero keeps the - * iteration in the active stellar subspace. - */ - hydrostatic_kernel_test_utils::HydrostaticEnthalpyMassOperator enthalpyMassOperator( - f, *f.domainMapperStateless, displacement - ); +mfem::Vector make_vacuum_supported_potential(const mean_field::fem::FEM &f) { + mfem::Vector attributeValues(f.mesh->attributes.Max()); - mfem::Vector correctionRightHandSide(interpolatedResidual); + attributeValues = 0.0; - correctionRightHandSide *= -1.0; + const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; - mfem::Vector enthalpyCorrection(f.enthalpyFes->GetTrueVSize()); + for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size(); + ++attributeIndex) { + const int attribute = f.mesh->attributes[attributeIndex]; - enthalpyCorrection = 0.0; + if (attribute == vacuumAttribute) { + attributeValues(attribute - 1) = 1.0; + } + } - mfem::CGSolver projectionSolver(communicator); + mfem::PWConstCoefficient coefficient(attributeValues); - projectionSolver.SetOperator(enthalpyMassOperator); + return project_scalar(*f.gravityPotentialFes, coefficient); +} - projectionSolver.SetRelTol(1.0e-12); - projectionSolver.SetAbsTol(1.0e-15); - projectionSolver.SetMaxIter(1000); - projectionSolver.SetPrintLevel(0); +class HydrostaticEnthalpyMassOperator final : public mfem::Operator { +public: + HydrostaticEnthalpyMassOperator( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mfem::Vector &displacementTrue) + : mfem::Operator(f.enthalpyFes->GetTrueVSize()), f_(f), + domainMapper_(domainMapper), displacementTrue_(displacementTrue) {} - projectionSolver.Mult(correctionRightHandSide, enthalpyCorrection); + void Mult(const mfem::Vector &input, mfem::Vector &output) const override { + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_enthalpy_action( + f_, domainMapper_, input, displacementTrue_, output); + } - INFO("Discrete-equilibrium projection converged = " << projectionSolver.GetConverged()); +private: + const mean_field::fem::FEM &f_; - INFO("Discrete-equilibrium projection iterations = " << projectionSolver.GetNumIterations()); + const mean_field::mapping::DomainMapper &domainMapper_; - INFO("Discrete-equilibrium projection final norm = " << projectionSolver.GetFinalNorm()); + const mfem::Vector &displacementTrue_; +}; +} // namespace hydrostatic_kernel_test_utils - REQUIRE(projectionSolver.GetConverged()); +TEST_CASE("Rigid Rotation Potential Derivative Matches Centered Differences", + tags::barotrope &tags::hydro &tags::jacobian &tags::physics + &tags::unit &tags::kernels) { + const mean_field::physics::RigidRotation rotation = + hydrostatic_kernel_test_utils::make_rotation(); - mfem::Vector correctionEquationResidual; + mfem::Vector position(3); + mfem::Vector direction(3); - enthalpyMassOperator.Mult(enthalpyCorrection, correctionEquationResidual); + position(0) = 0.71; + position(1) = -0.42; + position(2) = 0.36; - correctionEquationResidual -= correctionRightHandSide; + direction(0) = -0.17; + direction(1) = 0.29; + direction(2) = 0.11; - const double correctionEquationNorm = - gravity_prepared_test_utils::global_norm(correctionEquationResidual, communicator); + constexpr double epsilon = 1.0e-7; - INFO( - "Discrete-equilibrium correction-equation " - "residual norm = " - << correctionEquationNorm - ); + mfem::Vector plusPosition(position); + mfem::Vector minusPosition(position); - CHECK(correctionEquationNorm <= std::max(5.0e-12 * interpolatedResidualNorm, 5.0e-15)); + plusPosition.Add(epsilon, direction); + minusPosition.Add(-epsilon, direction); - mfem::Vector discreteEnthalpy(interpolatedEnthalpy); + const double centeredDerivative = + (rotation.potential(plusPosition) - rotation.potential(minusPosition)) / + (2.0 * epsilon); - discreteEnthalpy += enthalpyCorrection; + const double analyticDerivative = + rotation.potential_directional_derivative(position, direction); - mfem::Vector exactResidual; - mfem::Vector referenceResidual; + const double relativeError = + std::abs(centeredDerivative - analyticDerivative) / + std::max(std::abs(analyticDerivative), + std::numeric_limits::epsilon()); - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, discreteEnthalpy, potential, displacement, bernoulliConstant, - exactResidual - ); + INFO("Rigid-rotation derivative error = " << relativeError); - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, discreteEnthalpy, potential, displacement, - bernoulliConstant + constantOffset, referenceResidual - ); + CHECK(relativeError < 2.0e-9); +} - const double exactNorm = gravity_prepared_test_utils::global_norm(exactResidual, communicator); +TEST_CASE("Hydrostatic Residual Vanishes For A Manufactured Rotating State", + tags::barotrope &tags::hydro &tags::integration &tags::kernels + &tags::physics &tags::residuals) { + auto args = test_utils::setup_args(); - const double referenceNorm = gravity_prepared_test_utils::global_norm(referenceResidual, communicator); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - const double correctionNorm = gravity_prepared_test_utils::global_norm(enthalpyCorrection, communicator); + const mean_field::physics::RigidRotation rotation = + hydrostatic_kernel_test_utils::make_rotation(); - INFO("Enthalpy representation correction norm = " << correctionNorm); + constexpr double bernoulliConstant = 0.73; + constexpr double potentialValue = -0.21; + constexpr double constantOffset = 0.40; - INFO("Discrete manufactured residual norm = " << exactNorm); + mfem::FunctionCoefficient enthalpyCoefficient( + [&rotation](const mfem::Vector &position) { + return bernoulliConstant - potentialValue + + rotation.potential(position); + }); - INFO("Discrete reference residual norm = " << referenceNorm); + const mfem::Vector interpolatedEnthalpy = + hydrostatic_kernel_test_utils::project_scalar(*f.enthalpyFes, + enthalpyCoefficient); - REQUIRE(referenceNorm > 1.0e-12); + const mfem::Vector potential = + hydrostatic_kernel_test_utils::make_constant_field(*f.gravityPotentialFes, + potentialValue); - CHECK(exactNorm <= 5.0e-12 * referenceNorm); + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, 0.0); + + const MPI_Comm communicator = f.mesh->GetComm(); + + /* + * First measure the residual of the nodally interpolated + * analytic equilibrium. Because the order-3 enthalpy space + * cannot exactly represent the quadratic rotation potential + * on an order-4 curved mesh, this measures the representation + * floor rather than an algebraic residual. + */ + mfem::Vector interpolatedResidual; + mfem::Vector interpolatedReferenceResidual; + + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, interpolatedEnthalpy, potential, + displacement, bernoulliConstant, interpolatedResidual); + + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, interpolatedEnthalpy, potential, + displacement, bernoulliConstant + constantOffset, + interpolatedReferenceResidual); + + const double interpolatedResidualNorm = + gravity_prepared_test_utils::global_norm(interpolatedResidual, + communicator); + + const double interpolatedReferenceNorm = + gravity_prepared_test_utils::global_norm(interpolatedReferenceResidual, + communicator); + + REQUIRE(interpolatedReferenceNorm > 1.0e-12); + + const double representationFloor = + interpolatedResidualNorm / interpolatedReferenceNorm; + + INFO("Interpolated rotating-state residual norm = " + << interpolatedResidualNorm); + + INFO("Interpolated rotating-state relative " + "representation floor = " + << representationFloor); + + /* + * This remains an independent physical/sign check. A wrong + * rotation sign or coordinate convention would produce an + * order-unity error rather than the observed projection floor. + */ + CHECK(representationFloor < 2.0e-5); + + /* + * Construct the weakly manufactured discrete equilibrium. + * + * If r_I is the residual of the nodal interpolant, solve + * + * M_h delta_h = -r_I, + * + * where M_h is exactly the stellar-domain enthalpy action. + * Then h_I + delta_h satisfies the discrete weak equilibrium. + * + * Vacuum-only enthalpy DOFs form a nullspace, but the right-hand + * side is in the range of M_h. Starting CG from zero keeps the + * iteration in the active stellar subspace. + */ + hydrostatic_kernel_test_utils::HydrostaticEnthalpyMassOperator + enthalpyMassOperator(f, *f.domainMapperStateless, displacement); + + mfem::Vector correctionRightHandSide(interpolatedResidual); + + correctionRightHandSide *= -1.0; + + mfem::Vector enthalpyCorrection(f.enthalpyFes->GetTrueVSize()); + + enthalpyCorrection = 0.0; + + mfem::CGSolver projectionSolver(communicator); + + projectionSolver.SetOperator(enthalpyMassOperator); + + projectionSolver.SetRelTol(1.0e-12); + projectionSolver.SetAbsTol(1.0e-15); + projectionSolver.SetMaxIter(1000); + projectionSolver.SetPrintLevel(0); + + projectionSolver.Mult(correctionRightHandSide, enthalpyCorrection); + + INFO("Discrete-equilibrium projection converged = " + << projectionSolver.GetConverged()); + + INFO("Discrete-equilibrium projection iterations = " + << projectionSolver.GetNumIterations()); + + INFO("Discrete-equilibrium projection final norm = " + << projectionSolver.GetFinalNorm()); + + REQUIRE(projectionSolver.GetConverged()); + + mfem::Vector correctionEquationResidual; + + enthalpyMassOperator.Mult(enthalpyCorrection, correctionEquationResidual); + + correctionEquationResidual -= correctionRightHandSide; + + const double correctionEquationNorm = + gravity_prepared_test_utils::global_norm(correctionEquationResidual, + communicator); + + INFO("Discrete-equilibrium correction-equation " + "residual norm = " + << correctionEquationNorm); + + CHECK(correctionEquationNorm <= + std::max(5.0e-12 * interpolatedResidualNorm, 5.0e-15)); + + mfem::Vector discreteEnthalpy(interpolatedEnthalpy); + + discreteEnthalpy += enthalpyCorrection; + + mfem::Vector exactResidual; + mfem::Vector referenceResidual; + + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, discreteEnthalpy, potential, + displacement, bernoulliConstant, exactResidual); + + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, discreteEnthalpy, potential, + displacement, bernoulliConstant + constantOffset, referenceResidual); + + const double exactNorm = + gravity_prepared_test_utils::global_norm(exactResidual, communicator); + + const double referenceNorm = + gravity_prepared_test_utils::global_norm(referenceResidual, communicator); + + const double correctionNorm = gravity_prepared_test_utils::global_norm( + enthalpyCorrection, communicator); + + INFO("Enthalpy representation correction norm = " << correctionNorm); + + INFO("Discrete manufactured residual norm = " << exactNorm); + + INFO("Discrete reference residual norm = " << referenceNorm); + + REQUIRE(referenceNorm > 1.0e-12); + + CHECK(exactNorm <= 5.0e-12 * referenceNorm); } TEST_CASE( "Exact Constant Hydrostatic Equilibrium Remains Zero Under Deformation", - tags::barotrope &tags::hydro &tags::integration &tags::jacobian &tags::kernels &tags::mapping &tags::physics -) { - auto args = test_utils::setup_args(); + tags::barotrope &tags::hydro &tags::integration &tags::jacobian + &tags::kernels &tags::mapping &tags::physics) { + auto args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_zero_rotation(); + const mean_field::physics::RigidRotation rotation = + hydrostatic_kernel_test_utils::make_zero_rotation(); - constexpr double enthalpyValue = 1.20; - constexpr double potentialValue = -0.35; + constexpr double enthalpyValue = 1.20; + constexpr double potentialValue = -0.35; - constexpr double bernoulliConstant = enthalpyValue + potentialValue; + constexpr double bernoulliConstant = enthalpyValue + potentialValue; - const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_constant_field(*f.enthalpyFes, enthalpyValue); + const mfem::Vector enthalpy = + hydrostatic_kernel_test_utils::make_constant_field(*f.enthalpyFes, + enthalpyValue); - const mfem::Vector potential = - hydrostatic_kernel_test_utils::make_constant_field(*f.gravityPotentialFes, potentialValue); + const mfem::Vector potential = + hydrostatic_kernel_test_utils::make_constant_field(*f.gravityPotentialFes, + potentialValue); - const mfem::Vector displacementVariation = gravity_prepared_test_utils::make_displacement(f, 0.67); + const mfem::Vector displacementVariation = + gravity_prepared_test_utils::make_displacement(f, 0.67); - const MPI_Comm communicator = f.mesh->GetComm(); + const MPI_Comm communicator = f.mesh->GetComm(); - for (const double deformationScale : {0.0, 0.5, 1.0}) { - DYNAMIC_SECTION("Deformation scale = " << deformationScale) { - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, deformationScale); + for (const double deformationScale : {0.0, 0.5, 1.0}) { + DYNAMIC_SECTION("Deformation scale = " << deformationScale) { + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, deformationScale); - mfem::Vector exactResidual; - mfem::Vector referenceResidual; - mfem::Vector exactGeometryAction; - mfem::Vector referenceGeometryAction; + mfem::Vector exactResidual; + mfem::Vector referenceResidual; + mfem::Vector exactGeometryAction; + mfem::Vector referenceGeometryAction; - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, - exactResidual - ); + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant, exactResidual); - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant + 0.50, - referenceResidual - ); + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant + 0.50, referenceResidual); - mean_field::operators::kernels::apply_hydrostatic_equilibrium_displacement_action( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, - displacementVariation, exactGeometryAction - ); + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_displacement_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant, displacementVariation, + exactGeometryAction); - mean_field::operators::kernels::apply_hydrostatic_equilibrium_displacement_action( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant + 0.50, - displacementVariation, referenceGeometryAction - ); + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_displacement_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant + 0.50, displacementVariation, + referenceGeometryAction); - const double exactResidualNorm = gravity_prepared_test_utils::global_norm(exactResidual, communicator); + const double exactResidualNorm = + gravity_prepared_test_utils::global_norm(exactResidual, communicator); - const double referenceResidualNorm = - gravity_prepared_test_utils::global_norm(referenceResidual, communicator); + const double referenceResidualNorm = + gravity_prepared_test_utils::global_norm(referenceResidual, + communicator); - const double exactGeometryNorm = - gravity_prepared_test_utils::global_norm(exactGeometryAction, communicator); + const double exactGeometryNorm = gravity_prepared_test_utils::global_norm( + exactGeometryAction, communicator); - const double referenceGeometryNorm = - gravity_prepared_test_utils::global_norm(referenceGeometryAction, communicator); + const double referenceGeometryNorm = + gravity_prepared_test_utils::global_norm(referenceGeometryAction, + communicator); - REQUIRE(referenceResidualNorm > 1.0e-12); + REQUIRE(referenceResidualNorm > 1.0e-12); - REQUIRE(referenceGeometryNorm > 1.0e-14); + REQUIRE(referenceGeometryNorm > 1.0e-14); - CHECK(exactResidualNorm <= 5.0e-12 * referenceResidualNorm); + CHECK(exactResidualNorm <= 5.0e-12 * referenceResidualNorm); - CHECK(exactGeometryNorm <= 5.0e-12 * referenceGeometryNorm); - } + CHECK(exactGeometryNorm <= 5.0e-12 * referenceGeometryNorm); } + } } -TEST_CASE( - "Hydrostatic Equilibrium Excludes Vacuum Elements", - tags::barotrope &tags::hydro &tags::kernels &tags::mapping &tags::physics &tags::unit -) { - auto args = test_utils::setup_args(); +TEST_CASE("Hydrostatic Equilibrium Excludes Vacuum Elements", + tags::barotrope &tags::hydro &tags::kernels &tags::mapping + &tags::physics &tags::unit) { + auto args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_zero_rotation(); + const mean_field::physics::RigidRotation rotation = + hydrostatic_kernel_test_utils::make_zero_rotation(); - const mfem::Vector zeroEnthalpy(f.enthalpyFes->GetTrueVSize()); + const mfem::Vector zeroEnthalpy(f.enthalpyFes->GetTrueVSize()); - mfem::Vector enthalpy(zeroEnthalpy); - enthalpy = 0.0; + mfem::Vector enthalpy(zeroEnthalpy); + enthalpy = 0.0; - const mfem::Vector vacuumPotential = hydrostatic_kernel_test_utils::make_vacuum_supported_potential(f); + const mfem::Vector vacuumPotential = + hydrostatic_kernel_test_utils::make_vacuum_supported_potential(f); - const mfem::Vector stellarPotential = - hydrostatic_kernel_test_utils::make_constant_field(*f.gravityPotentialFes, 1.0); + const mfem::Vector stellarPotential = + hydrostatic_kernel_test_utils::make_constant_field(*f.gravityPotentialFes, + 1.0); - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 1.0); + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, 1.0); + mfem::Vector residual; + mfem::Vector vacuumAction; + mfem::Vector stellarAction; + + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, enthalpy, vacuumPotential, + displacement, 0.0, residual); + + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_potential_action( + f, *f.domainMapperStateless, vacuumPotential, displacement, + vacuumAction); + + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_potential_action( + f, *f.domainMapperStateless, stellarPotential, displacement, + stellarAction); + + const MPI_Comm communicator = f.mesh->GetComm(); + + const double residualNorm = + gravity_prepared_test_utils::global_norm(residual, communicator); + + const double vacuumActionNorm = + gravity_prepared_test_utils::global_norm(vacuumAction, communicator); + + const double stellarActionNorm = + gravity_prepared_test_utils::global_norm(stellarAction, communicator); + + REQUIRE(stellarActionNorm > 1.0e-12); + + CHECK(residualNorm <= 5.0e-13 * stellarActionNorm); + + CHECK(vacuumActionNorm <= 5.0e-13 * stellarActionNorm); +} + +TEST_CASE("Hydrostatic Jacobian Matches Blocks And Centered Differences", + tags::barotrope &tags::hydro &tags::integration &tags::jacobian + &tags::kernels &tags::mapping &tags::physics) { + auto args = test_utils::setup_args(); + + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + + const mean_field::physics::RigidRotation rotation = + hydrostatic_kernel_test_utils::make_rotation(); + + const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); + + const mfem::Vector potential = + hydrostatic_kernel_test_utils::make_potential(f); + + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, 1.0); + + const mfem::Vector enthalpyVariation = + gravity_prepared_test_utils::make_deterministic_vector( + f.enthalpyFes->GetTrueVSize(), 0.23); + + const mfem::Vector potentialVariation = + gravity_prepared_test_utils::make_deterministic_vector( + f.gravityPotentialFes->GetTrueVSize(), 0.47); + + const mfem::Vector displacementVariation = + gravity_prepared_test_utils::make_deterministic_vector( + f.displacementFes->GetTrueVSize(), 0.71); + + constexpr double bernoulliConstant = 0.41; + constexpr double constantVariation = -0.37; + constexpr double epsilon = 1.0e-7; + + mfem::Vector enthalpyAction; + mfem::Vector potentialAction; + mfem::Vector constantAction; + mfem::Vector displacementAction; + mfem::Vector completeAction; + + mean_field::operators::kernels::apply_hydrostatic_equilibrium_enthalpy_action( + f, *f.domainMapperStateless, enthalpyVariation, displacement, + enthalpyAction); + + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_potential_action( + f, *f.domainMapperStateless, potentialVariation, displacement, + potentialAction); + + mean_field::operators::kernels::apply_hydrostatic_equilibrium_constant_action( + f, *f.domainMapperStateless, constantVariation, displacement, + constantAction); + + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_displacement_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant, displacementVariation, + displacementAction); + + mean_field::operators::kernels::apply_hydrostatic_equilibrium_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, + bernoulliConstant, enthalpyVariation, potentialVariation, + constantVariation, displacementVariation, completeAction); + + mfem::Vector blockAction(enthalpyAction); + blockAction += potentialAction; + blockAction += constantAction; + blockAction += displacementAction; + + const MPI_Comm communicator = f.mesh->GetComm(); + + const double blockError = gravity_prepared_test_utils::relative_error( + completeAction, blockAction, communicator); + + INFO("Hydrostatic block reconstruction error = " << blockError); + + CHECK(blockError < 5.0e-13); + + auto evaluate_residual = [&f, + &rotation](const mfem::Vector &trialEnthalpy, + const mfem::Vector &trialPotential, + const mfem::Vector &trialDisplacement, + const double trialConstant) { mfem::Vector residual; - mfem::Vector vacuumAction; - mfem::Vector stellarAction; mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, enthalpy, vacuumPotential, displacement, 0.0, residual - ); + f, *f.domainMapperStateless, rotation, trialEnthalpy, trialPotential, + trialDisplacement, trialConstant, residual); - mean_field::operators::kernels::apply_hydrostatic_equilibrium_potential_action( - f, *f.domainMapperStateless, vacuumPotential, displacement, vacuumAction - ); + return residual; + }; - mean_field::operators::kernels::apply_hydrostatic_equilibrium_potential_action( - f, *f.domainMapperStateless, stellarPotential, displacement, stellarAction - ); + mfem::Vector plusEnthalpy(enthalpy); + mfem::Vector minusEnthalpy(enthalpy); - const MPI_Comm communicator = f.mesh->GetComm(); + plusEnthalpy.Add(epsilon, enthalpyVariation); - const double residualNorm = gravity_prepared_test_utils::global_norm(residual, communicator); + minusEnthalpy.Add(-epsilon, enthalpyVariation); - const double vacuumActionNorm = gravity_prepared_test_utils::global_norm(vacuumAction, communicator); + const mfem::Vector enthalpyDifference = + hydrostatic_kernel_test_utils::centered_difference( + evaluate_residual(plusEnthalpy, potential, displacement, + bernoulliConstant), + evaluate_residual(minusEnthalpy, potential, displacement, + bernoulliConstant), + epsilon); - const double stellarActionNorm = gravity_prepared_test_utils::global_norm(stellarAction, communicator); + mfem::Vector plusPotential(potential); + mfem::Vector minusPotential(potential); - REQUIRE(stellarActionNorm > 1.0e-12); + plusPotential.Add(epsilon, potentialVariation); - CHECK(residualNorm <= 5.0e-13 * stellarActionNorm); + minusPotential.Add(-epsilon, potentialVariation); - CHECK(vacuumActionNorm <= 5.0e-13 * stellarActionNorm); + const mfem::Vector potentialDifference = + hydrostatic_kernel_test_utils::centered_difference( + evaluate_residual(enthalpy, plusPotential, displacement, + bernoulliConstant), + evaluate_residual(enthalpy, minusPotential, displacement, + bernoulliConstant), + epsilon); + + const mfem::Vector constantDifference = + hydrostatic_kernel_test_utils::centered_difference( + evaluate_residual(enthalpy, potential, displacement, + bernoulliConstant + epsilon * constantVariation), + evaluate_residual(enthalpy, potential, displacement, + bernoulliConstant - epsilon * constantVariation), + epsilon); + + mfem::Vector plusDisplacement(displacement); + mfem::Vector minusDisplacement(displacement); + + plusDisplacement.Add(epsilon, displacementVariation); + + minusDisplacement.Add(-epsilon, displacementVariation); + + const mfem::Vector displacementDifference = + hydrostatic_kernel_test_utils::centered_difference( + evaluate_residual(enthalpy, potential, plusDisplacement, + bernoulliConstant), + evaluate_residual(enthalpy, potential, minusDisplacement, + bernoulliConstant), + epsilon); + + const double enthalpyError = gravity_prepared_test_utils::relative_error( + enthalpyAction, enthalpyDifference, communicator); + + const double potentialError = gravity_prepared_test_utils::relative_error( + potentialAction, potentialDifference, communicator); + + const double constantError = gravity_prepared_test_utils::relative_error( + constantAction, constantDifference, communicator); + + const double displacementError = gravity_prepared_test_utils::relative_error( + displacementAction, displacementDifference, communicator); + + INFO("Hydrostatic enthalpy-block error = " << enthalpyError); + + INFO("Hydrostatic potential-block error = " << potentialError); + + INFO("Hydrostatic constant-block error = " << constantError); + + INFO("Hydrostatic displacement-block error = " << displacementError); + + CHECK(enthalpyError < 2.0e-8); + CHECK(potentialError < 2.0e-8); + CHECK(constantError < 2.0e-8); + CHECK(displacementError < 2.0e-7); + + mfem::Vector combinedPlusEnthalpy(enthalpy); + mfem::Vector combinedMinusEnthalpy(enthalpy); + mfem::Vector combinedPlusPotential(potential); + mfem::Vector combinedMinusPotential(potential); + mfem::Vector combinedPlusDisplacement(displacement); + mfem::Vector combinedMinusDisplacement(displacement); + + combinedPlusEnthalpy.Add(epsilon, enthalpyVariation); + + combinedMinusEnthalpy.Add(-epsilon, enthalpyVariation); + + combinedPlusPotential.Add(epsilon, potentialVariation); + + combinedMinusPotential.Add(-epsilon, potentialVariation); + + combinedPlusDisplacement.Add(epsilon, displacementVariation); + + combinedMinusDisplacement.Add(-epsilon, displacementVariation); + + const mfem::Vector combinedDifference = + hydrostatic_kernel_test_utils::centered_difference( + evaluate_residual(combinedPlusEnthalpy, combinedPlusPotential, + combinedPlusDisplacement, + bernoulliConstant + epsilon * constantVariation), + evaluate_residual(combinedMinusEnthalpy, combinedMinusPotential, + combinedMinusDisplacement, + bernoulliConstant - epsilon * constantVariation), + epsilon); + + const double blockNormSum = + gravity_prepared_test_utils::global_norm(enthalpyAction, communicator) + + gravity_prepared_test_utils::global_norm(potentialAction, communicator) + + gravity_prepared_test_utils::global_norm(constantAction, communicator) + + gravity_prepared_test_utils::global_norm(displacementAction, + communicator); + + const double simultaneousError = + hydrostatic_kernel_test_utils::sum_normalized_error( + completeAction, combinedDifference, blockNormSum, communicator); + + INFO("Hydrostatic simultaneous Jacobian error = " << simultaneousError); + + CHECK(simultaneousError < 2.0e-7); } -TEST_CASE( - "Hydrostatic Jacobian Matches Blocks And Centered Differences", - tags::barotrope &tags::hydro &tags::integration &tags::jacobian &tags::kernels &tags::mapping &tags::physics -) { - auto args = test_utils::setup_args(); +TEST_CASE("Hydrostatic Displacement Action Is Linear In Its Direction", + tags::barotrope &tags::hydro &tags::integration &tags::jacobian + &tags::mapping &tags::physics &tags::unit &tags::kernels) { + auto args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); + const mean_field::physics::RigidRotation rotation = + hydrostatic_kernel_test_utils::make_rotation(); - const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); + const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); - const mfem::Vector potential = hydrostatic_kernel_test_utils::make_potential(f); + const mfem::Vector potential = + hydrostatic_kernel_test_utils::make_potential(f); - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 1.0); + const mfem::Vector displacement = + gravity_prepared_test_utils::make_displacement(f, 1.0); - const mfem::Vector enthalpyVariation = - gravity_prepared_test_utils::make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.23); + const mfem::Vector firstDirection = + gravity_prepared_test_utils::make_deterministic_vector( + f.displacementFes->GetTrueVSize(), 0.31); - const mfem::Vector potentialVariation = - gravity_prepared_test_utils::make_deterministic_vector(f.gravityPotentialFes->GetTrueVSize(), 0.47); + const mfem::Vector secondDirection = + gravity_prepared_test_utils::make_deterministic_vector( + f.displacementFes->GetTrueVSize(), 0.83); - const mfem::Vector displacementVariation = - gravity_prepared_test_utils::make_deterministic_vector(f.displacementFes->GetTrueVSize(), 0.71); + constexpr double firstScale = 0.43; + constexpr double secondScale = -0.29; + constexpr double bernoulliConstant = 0.41; - constexpr double bernoulliConstant = 0.41; - constexpr double constantVariation = -0.37; - constexpr double epsilon = 1.0e-7; + const mfem::Vector combinedDirection = + gravity_prepared_test_utils::linear_combination( + firstDirection, firstScale, secondDirection, secondScale); - mfem::Vector enthalpyAction; - mfem::Vector potentialAction; - mfem::Vector constantAction; - mfem::Vector displacementAction; - mfem::Vector completeAction; + mfem::Vector firstAction; + mfem::Vector secondAction; + mfem::Vector combinedAction; - mean_field::operators::kernels::apply_hydrostatic_equilibrium_enthalpy_action( - f, *f.domainMapperStateless, enthalpyVariation, displacement, enthalpyAction - ); + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_displacement_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant, firstDirection, firstAction); - mean_field::operators::kernels::apply_hydrostatic_equilibrium_potential_action( - f, *f.domainMapperStateless, potentialVariation, displacement, potentialAction - ); + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_displacement_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant, secondDirection, secondAction); - mean_field::operators::kernels::apply_hydrostatic_equilibrium_constant_action( - f, *f.domainMapperStateless, constantVariation, displacement, constantAction - ); + mean_field::operators::kernels:: + apply_hydrostatic_equilibrium_displacement_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, + displacement, bernoulliConstant, combinedDirection, combinedAction); - mean_field::operators::kernels::apply_hydrostatic_equilibrium_displacement_action( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, - displacementVariation, displacementAction - ); + const mfem::Vector expectedAction = + gravity_prepared_test_utils::linear_combination( + firstAction, firstScale, secondAction, secondScale); - mean_field::operators::kernels::apply_hydrostatic_equilibrium_action( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, enthalpyVariation, - potentialVariation, constantVariation, displacementVariation, completeAction - ); + const double linearityError = gravity_prepared_test_utils::relative_error( + combinedAction, expectedAction, f.mesh->GetComm()); - mfem::Vector blockAction(enthalpyAction); - blockAction += potentialAction; - blockAction += constantAction; - blockAction += displacementAction; + INFO("Hydrostatic displacement-linearity error = " << linearityError); - const MPI_Comm communicator = f.mesh->GetComm(); - - const double blockError = gravity_prepared_test_utils::relative_error(completeAction, blockAction, communicator); - - INFO("Hydrostatic block reconstruction error = " << blockError); - - CHECK(blockError < 5.0e-13); - - auto evaluate_residual = [&f, &rotation]( - const mfem::Vector &trialEnthalpy, const mfem::Vector &trialPotential, - const mfem::Vector &trialDisplacement, const double trialConstant - ) { - mfem::Vector residual; - - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, trialEnthalpy, trialPotential, trialDisplacement, trialConstant, - residual - ); - - return residual; - }; - - mfem::Vector plusEnthalpy(enthalpy); - mfem::Vector minusEnthalpy(enthalpy); - - plusEnthalpy.Add(epsilon, enthalpyVariation); - - minusEnthalpy.Add(-epsilon, enthalpyVariation); - - const mfem::Vector enthalpyDifference = hydrostatic_kernel_test_utils::centered_difference( - evaluate_residual(plusEnthalpy, potential, displacement, bernoulliConstant), - evaluate_residual(minusEnthalpy, potential, displacement, bernoulliConstant), epsilon - ); - - mfem::Vector plusPotential(potential); - mfem::Vector minusPotential(potential); - - plusPotential.Add(epsilon, potentialVariation); - - minusPotential.Add(-epsilon, potentialVariation); - - const mfem::Vector potentialDifference = hydrostatic_kernel_test_utils::centered_difference( - evaluate_residual(enthalpy, plusPotential, displacement, bernoulliConstant), - evaluate_residual(enthalpy, minusPotential, displacement, bernoulliConstant), epsilon - ); - - const mfem::Vector constantDifference = hydrostatic_kernel_test_utils::centered_difference( - evaluate_residual(enthalpy, potential, displacement, bernoulliConstant + epsilon * constantVariation), - evaluate_residual(enthalpy, potential, displacement, bernoulliConstant - epsilon * constantVariation), epsilon - ); - - mfem::Vector plusDisplacement(displacement); - mfem::Vector minusDisplacement(displacement); - - plusDisplacement.Add(epsilon, displacementVariation); - - minusDisplacement.Add(-epsilon, displacementVariation); - - const mfem::Vector displacementDifference = hydrostatic_kernel_test_utils::centered_difference( - evaluate_residual(enthalpy, potential, plusDisplacement, bernoulliConstant), - evaluate_residual(enthalpy, potential, minusDisplacement, bernoulliConstant), epsilon - ); - - const double enthalpyError = - gravity_prepared_test_utils::relative_error(enthalpyAction, enthalpyDifference, communicator); - - const double potentialError = - gravity_prepared_test_utils::relative_error(potentialAction, potentialDifference, communicator); - - const double constantError = - gravity_prepared_test_utils::relative_error(constantAction, constantDifference, communicator); - - const double displacementError = - gravity_prepared_test_utils::relative_error(displacementAction, displacementDifference, communicator); - - INFO("Hydrostatic enthalpy-block error = " << enthalpyError); - - INFO("Hydrostatic potential-block error = " << potentialError); - - INFO("Hydrostatic constant-block error = " << constantError); - - INFO("Hydrostatic displacement-block error = " << displacementError); - - CHECK(enthalpyError < 2.0e-8); - CHECK(potentialError < 2.0e-8); - CHECK(constantError < 2.0e-8); - CHECK(displacementError < 2.0e-7); - - mfem::Vector combinedPlusEnthalpy(enthalpy); - mfem::Vector combinedMinusEnthalpy(enthalpy); - mfem::Vector combinedPlusPotential(potential); - mfem::Vector combinedMinusPotential(potential); - mfem::Vector combinedPlusDisplacement(displacement); - mfem::Vector combinedMinusDisplacement(displacement); - - combinedPlusEnthalpy.Add(epsilon, enthalpyVariation); - - combinedMinusEnthalpy.Add(-epsilon, enthalpyVariation); - - combinedPlusPotential.Add(epsilon, potentialVariation); - - combinedMinusPotential.Add(-epsilon, potentialVariation); - - combinedPlusDisplacement.Add(epsilon, displacementVariation); - - combinedMinusDisplacement.Add(-epsilon, displacementVariation); - - const mfem::Vector combinedDifference = hydrostatic_kernel_test_utils::centered_difference( - evaluate_residual( - combinedPlusEnthalpy, combinedPlusPotential, combinedPlusDisplacement, - bernoulliConstant + epsilon * constantVariation - ), - evaluate_residual( - combinedMinusEnthalpy, combinedMinusPotential, combinedMinusDisplacement, - bernoulliConstant - epsilon * constantVariation - ), - epsilon - ); - - const double blockNormSum = gravity_prepared_test_utils::global_norm(enthalpyAction, communicator) + - gravity_prepared_test_utils::global_norm(potentialAction, communicator) + - gravity_prepared_test_utils::global_norm(constantAction, communicator) + - gravity_prepared_test_utils::global_norm(displacementAction, communicator); - - const double simultaneousError = hydrostatic_kernel_test_utils::sum_normalized_error( - completeAction, combinedDifference, blockNormSum, communicator - ); - - INFO("Hydrostatic simultaneous Jacobian error = " << simultaneousError); - - CHECK(simultaneousError < 2.0e-7); -} - -TEST_CASE( - "Hydrostatic Displacement Action Is Linear In Its Direction", - tags::barotrope &tags::hydro &tags::integration &tags::jacobian &tags::mapping &tags::physics &tags::unit - &tags::kernels -) { - auto args = test_utils::setup_args(); - - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - - const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); - - const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); - - const mfem::Vector potential = hydrostatic_kernel_test_utils::make_potential(f); - - const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 1.0); - - const mfem::Vector firstDirection = - gravity_prepared_test_utils::make_deterministic_vector(f.displacementFes->GetTrueVSize(), 0.31); - - const mfem::Vector secondDirection = - gravity_prepared_test_utils::make_deterministic_vector(f.displacementFes->GetTrueVSize(), 0.83); - - constexpr double firstScale = 0.43; - constexpr double secondScale = -0.29; - constexpr double bernoulliConstant = 0.41; - - const mfem::Vector combinedDirection = - gravity_prepared_test_utils::linear_combination(firstDirection, firstScale, secondDirection, secondScale); - - mfem::Vector firstAction; - mfem::Vector secondAction; - mfem::Vector combinedAction; - - mean_field::operators::kernels::apply_hydrostatic_equilibrium_displacement_action( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, firstDirection, - firstAction - ); - - mean_field::operators::kernels::apply_hydrostatic_equilibrium_displacement_action( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, secondDirection, - secondAction - ); - - mean_field::operators::kernels::apply_hydrostatic_equilibrium_displacement_action( - f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, combinedDirection, - combinedAction - ); - - const mfem::Vector expectedAction = - gravity_prepared_test_utils::linear_combination(firstAction, firstScale, secondAction, secondScale); - - const double linearityError = - gravity_prepared_test_utils::relative_error(combinedAction, expectedAction, f.mesh->GetComm()); - - INFO("Hydrostatic displacement-linearity error = " << linearityError); - - CHECK(linearityError < 5.0e-12); + CHECK(linearityError < 5.0e-12); } TEST_CASE( "Hydrostatic Residual Is Translationally Invariant On Deformed Geometry", - tags::barotrope &tags::hydro &tags::integration &tags::kernels &tags::mapping &tags::physics &tags::residuals -) { - auto args = test_utils::setup_args(); + tags::barotrope &tags::hydro &tags::integration &tags::kernels + &tags::mapping &tags::physics &tags::residuals) { + auto args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - mfem::Vector angularVelocity(3); + mfem::Vector angularVelocity(3); - angularVelocity(0) = 0.21; - angularVelocity(1) = -0.13; - angularVelocity(2) = 0.48; + angularVelocity(0) = 0.21; + angularVelocity(1) = -0.13; + angularVelocity(2) = 0.48; - mfem::Vector center(3); + mfem::Vector center(3); - center(0) = 0.04; - center(1) = -0.03; - center(2) = 0.02; + center(0) = 0.04; + center(1) = -0.03; + center(2) = 0.02; - mfem::Vector translation(3); + mfem::Vector translation(3); - translation(0) = 0.071; - translation(1) = -0.053; - translation(2) = 0.037; + translation(0) = 0.071; + translation(1) = -0.053; + translation(2) = 0.037; - mfem::Vector translatedCenter(center); - translatedCenter += translation; + mfem::Vector translatedCenter(center); + translatedCenter += translation; - const mean_field::physics::RigidRotation baseRotation(angularVelocity, center); + const mean_field::physics::RigidRotation baseRotation(angularVelocity, + center); - const mean_field::physics::RigidRotation translatedRotation(angularVelocity, translatedCenter); + const mean_field::physics::RigidRotation translatedRotation(angularVelocity, + translatedCenter); - const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); + const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); - const mfem::Vector potential = hydrostatic_kernel_test_utils::make_potential(f); + const mfem::Vector potential = + hydrostatic_kernel_test_utils::make_potential(f); - /* - * Use a nontrivially deformed base state so this checks rotation - * and mapped geometry simultaneously. The comparison state adds - * an exactly representable rigid translation to that deformation. - */ - const mfem::Vector baseDisplacement = gravity_prepared_test_utils::make_displacement(f, 0.73); + /* + * Use a nontrivially deformed base state so this checks rotation + * and mapped geometry simultaneously. The comparison state adds + * an exactly representable rigid translation to that deformation. + */ + const mfem::Vector baseDisplacement = + gravity_prepared_test_utils::make_displacement(f, 0.73); - mfem::ParGridFunction translationField(f.displacementFes.get()); + mfem::ParGridFunction translationField(f.displacementFes.get()); - mfem::VectorFunctionCoefficient translationCoefficient( - f.mesh->Dimension(), [&translation](const mfem::Vector &, mfem::Vector &value) { - value.SetSize(translation.Size()); - value = translation; - } - ); + mfem::VectorFunctionCoefficient translationCoefficient( + f.mesh->Dimension(), + [&translation](const mfem::Vector &, mfem::Vector &value) { + value.SetSize(translation.Size()); + value = translation; + }); - translationField.ProjectCoefficient(translationCoefficient); + translationField.ProjectCoefficient(translationCoefficient); - mfem::Vector translationTrue; - translationField.GetTrueDofs(translationTrue); + mfem::Vector translationTrue; + translationField.GetTrueDofs(translationTrue); - mfem::Vector translatedDisplacement(baseDisplacement); + mfem::Vector translatedDisplacement(baseDisplacement); - translatedDisplacement += translationTrue; + translatedDisplacement += translationTrue; - constexpr double bernoulliConstant = 0.41; + constexpr double bernoulliConstant = 0.41; - mfem::Vector baseResidual; - mfem::Vector translatedResidual; - mfem::Vector untranslatedCenterResidual; + mfem::Vector baseResidual; + mfem::Vector translatedResidual; + mfem::Vector untranslatedCenterResidual; - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, baseRotation, enthalpy, potential, baseDisplacement, bernoulliConstant, - baseResidual - ); + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, baseRotation, enthalpy, potential, + baseDisplacement, bernoulliConstant, baseResidual); - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, translatedRotation, enthalpy, potential, translatedDisplacement, bernoulliConstant, - translatedResidual - ); + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, translatedRotation, enthalpy, potential, + translatedDisplacement, bernoulliConstant, translatedResidual); - /* - * Negative control: translate the geometry but leave the rotation - * center fixed. This must not agree with the covariant result. - */ - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, baseRotation, enthalpy, potential, translatedDisplacement, bernoulliConstant, - untranslatedCenterResidual - ); + /* + * Negative control: translate the geometry but leave the rotation + * center fixed. This must not agree with the covariant result. + */ + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, baseRotation, enthalpy, potential, + translatedDisplacement, bernoulliConstant, untranslatedCenterResidual); - const MPI_Comm communicator = f.mesh->GetComm(); + const MPI_Comm communicator = f.mesh->GetComm(); - const double baseResidualNorm = gravity_prepared_test_utils::global_norm(baseResidual, communicator); + const double baseResidualNorm = + gravity_prepared_test_utils::global_norm(baseResidual, communicator); - const double translatedResidualNorm = gravity_prepared_test_utils::global_norm(translatedResidual, communicator); + const double translatedResidualNorm = + gravity_prepared_test_utils::global_norm(translatedResidual, + communicator); - const double translationInvarianceError = - gravity_prepared_test_utils::relative_error(translatedResidual, baseResidual, communicator); + const double translationInvarianceError = + gravity_prepared_test_utils::relative_error(translatedResidual, + baseResidual, communicator); - const double fixedCenterDifference = - gravity_prepared_test_utils::relative_error(untranslatedCenterResidual, translatedResidual, communicator); + const double fixedCenterDifference = + gravity_prepared_test_utils::relative_error( + untranslatedCenterResidual, translatedResidual, communicator); - INFO("Base deformed hydrostatic residual norm = " << baseResidualNorm); + INFO("Base deformed hydrostatic residual norm = " << baseResidualNorm); - INFO("Translated hydrostatic residual norm = " << translatedResidualNorm); + INFO("Translated hydrostatic residual norm = " << translatedResidualNorm); - INFO("Mapped-rotation translation invariance error = " << translationInvarianceError); + INFO("Mapped-rotation translation invariance error = " + << translationInvarianceError); - INFO("Relative change with untranslated rotation center = " << fixedCenterDifference); + INFO("Relative change with untranslated rotation center = " + << fixedCenterDifference); - REQUIRE(baseResidualNorm > 1.0e-12); - REQUIRE(translatedResidualNorm > 1.0e-12); - REQUIRE(fixedCenterDifference > 1.0e-5); + REQUIRE(baseResidualNorm > 1.0e-12); + REQUIRE(translatedResidualNorm > 1.0e-12); + REQUIRE(fixedCenterDifference > 1.0e-5); - CHECK(translationInvarianceError < 5.0e-12); + CHECK(translationInvarianceError < 5.0e-12); } \ No newline at end of file diff --git a/tests/operators/kernels/pressure_force_kernels.cpp b/tests/operators/kernels/pressure_force_kernels.cpp index 35c4b54..b0865fe 100644 --- a/tests/operators/kernels/pressure_force_kernels.cpp +++ b/tests/operators/kernels/pressure_force_kernels.cpp @@ -10,670 +10,704 @@ import mean_field; import test_helpers; namespace pressure_force_kernel_test_utils { - [[nodiscard]] mfem::Vector make_deterministic_vector( - const int size, - const double phase - ) { - mfem::Vector vector(size); +[[nodiscard]] mfem::Vector make_deterministic_vector(const int size, + const double phase) { + mfem::Vector vector(size); - for (int index = 0; index < size; ++index) { - const double position = static_cast(index + 1); + for (int index = 0; index < size; ++index) { + const double position = static_cast(index + 1); - vector(index) = - 0.71 + 0.19 * std::sin(0.31 * position + phase) + 0.08 * std::cos(0.17 * position - 0.5 * phase); - } + vector(index) = 0.71 + 0.19 * std::sin(0.31 * position + phase) + + 0.08 * std::cos(0.17 * position - 0.5 * phase); + } - return vector; + return vector; +} + +[[nodiscard]] mfem::Vector +make_zero_displacement(const mean_field::fem::FEM &f) { + mfem::Vector displacementTrue(f.displacementFes->GetTrueVSize()); + displacementTrue = 0.0; + return displacementTrue; +} + +[[nodiscard]] mfem::Vector +make_vacuum_only_enthalpy(const mean_field::fem::FEM &f) { + mfem::Vector enthalpyTrue = + make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.43); + + using DomainSchema = + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + const mean_field::field::FieldDofMap enthalpyMap = + mean_field::field::make_field_dof_map(*f.enthalpyFes); + + for (int reducedDof = 0; reducedDof < enthalpyMap.reduced_size(); + ++reducedDof) { + enthalpyTrue(enthalpyMap.true_dof(reducedDof)) = 0.0; + } + + return enthalpyTrue; +} + +[[nodiscard]] mfem::Vector +make_positive_asymmetric_enthalpy(const mean_field::fem::FEM &f) { + mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { + return 1.10 + 0.07 * position(0) - 0.04 * position(1) + 0.03 * position(2); + }); + + mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); + + enthalpyField.ProjectCoefficient(coefficient); + + mfem::Vector enthalpyTrue; + enthalpyField.GetTrueDofs(enthalpyTrue); + + return enthalpyTrue; +} + +[[nodiscard]] mfem::Vector +make_component_test_field(const mean_field::fem::FEM &f, const int component, + const int coordinate) { + const int dimension = f.mesh->Dimension(); + + MFEM_VERIFY(component >= 0 && component < dimension, + "The requested vector component is invalid."); + + MFEM_VERIFY(coordinate >= -1 && coordinate < dimension, + "The requested coordinate is invalid."); + + /* + * coordinate == -1 gives the rigid translation e_component. + * + * Otherwise this gives + * + * w = x_coordinate e_component. + */ + mfem::VectorFunctionCoefficient coefficient( + dimension, [component, coordinate, dimension]( + const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(dimension); + value = 0.0; + + value(component) = coordinate < 0 ? 1.0 : position(coordinate); + }); + + mfem::ParGridFunction field(f.displacementFes.get()); + + field.ProjectCoefficient(coefficient); + + mfem::Vector fieldTrue; + field.GetTrueDofs(fieldTrue); + + return fieldTrue; +} + +[[nodiscard]] double global_dot(const mfem::Vector &left, + const mfem::Vector &right, + MPI_Comm communicator) { + MFEM_VERIFY(left.Size() == right.Size(), + "The global dot-product vectors have different sizes."); + + const double localDot = left * right; + double globalDot = 0.0; + + MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator); + + return globalDot; +} + +[[nodiscard]] double integrate_pressure( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domainMapper, + const mean_field::eos::Polytrope &barotrope, + const mfem::Vector &enthalpyTrue, const mfem::Vector &displacementTrue) { + MFEM_VERIFY(enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), + "The pressure-integral enthalpy vector has the wrong size."); + + MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The pressure-integral displacement vector has the wrong size."); + + mfem::Vector enthalpyLocal(f.enthalpyFes->GetVSize()); + + const mfem::Operator *enthalpyProlongation = + f.enthalpyFes->GetProlongationMatrix(); + + if (enthalpyProlongation != nullptr) { + enthalpyProlongation->Mult(enthalpyTrue, enthalpyLocal); + } else { + enthalpyLocal = enthalpyTrue; + } + + mfem::Vector displacementLocal(f.displacementFes->GetVSize()); + + const mfem::Operator *displacementProlongation = + f.displacementFes->GetProlongationMatrix(); + + if (displacementProlongation != nullptr) { + displacementProlongation->Mult(displacementTrue, displacementLocal); + } else { + displacementLocal = displacementTrue; + } + + const double pressureExtraOrderValue = + barotrope.polytropic_index() * + static_cast(mean_field::field::Enthalpy::Scalar::familyOrder); + + MFEM_VERIFY(std::isfinite(pressureExtraOrderValue) && + pressureExtraOrderValue >= 0.0 && + pressureExtraOrderValue <= + static_cast(std::numeric_limits::max()), + "The pressure-integral EOS order is invalid."); + + const int pressureExtraOrder = + static_cast(std::ceil(pressureExtraOrderValue)); + + using EnthalpyField = mean_field::field::Field; + + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); + + mean_field::mapping::VolumeMappingContext mappingContext; + + mfem::Array enthalpyDofs; + mfem::Array displacementDofs; + mfem::Array compactificationDofs; + + mfem::Vector elementEnthalpy; + mfem::Vector elementDisplacement; + mfem::Vector elementCompactification; + mfem::Vector enthalpyShape; + + double localPressureIntegral = 0.0; + + const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; + + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elementId); + + MFEM_VERIFY(transformation != nullptr, + "The pressure-integral reference received a null " + "element transformation."); + + if (transformation->Attribute == vacuumAttribute) { + continue; } - [[nodiscard]] mfem::Vector make_zero_displacement(const mean_field::fem::FEM &f) { - mfem::Vector displacementTrue(f.displacementFes->GetTrueVSize()); - displacementTrue = 0.0; - return displacementTrue; + const mfem::FiniteElement &enthalpyElement = + *f.enthalpyFes->GetFE(elementId); + + const mfem::FiniteElement &displacementElement = + *f.displacementFes->GetFE(elementId); + + const mfem::FiniteElement &compactificationElement = + *f.compactificationFes->GetFE(elementId); + + mfem::DofTransformation *enthalpyDofTransformation = + f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); + + mfem::DofTransformation *displacementDofTransformation = + f.displacementFes->GetElementVDofs(elementId, displacementDofs); + + mfem::DofTransformation *compactificationDofTransformation = + f.compactificationFes->GetElementDofs(elementId, compactificationDofs); + + enthalpyLocal.GetSubVector(enthalpyDofs, elementEnthalpy); + + displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + + f.compactificationCoordinate->GetSubVector(compactificationDofs, + elementCompactification); + + if (enthalpyDofTransformation != nullptr) { + enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy); } - [[nodiscard]] mfem::Vector make_vacuum_only_enthalpy(const mean_field::fem::FEM &f) { - mfem::Vector enthalpyTrue = make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.43); - - mfem::Array stellarElementMask; - mean_field::utils::populate_element_mask(f.mesh.get(), mean_field::utils::DOMAINS::STELLAR, stellarElementMask); - - mfem::Array stellarEnthalpyTrueDofs; - mean_field::utils::populate_domain_tdofs(f.enthalpyFes.get(), stellarElementMask, stellarEnthalpyTrueDofs); - - for (int listIndex = 0; listIndex < stellarEnthalpyTrueDofs.Size(); ++listIndex) { - const int trueDof = stellarEnthalpyTrueDofs[listIndex]; - - MFEM_VERIFY( - trueDof >= 0 && trueDof < enthalpyTrue.Size(), "The stellar enthalpy true-DOF mask contains an " - "invalid index." - ); - - enthalpyTrue(trueDof) = 0.0; - } - - return enthalpyTrue; + if (displacementDofTransformation != nullptr) { + displacementDofTransformation->InvTransformPrimal(elementDisplacement); } - [[nodiscard]] mfem::Vector make_positive_asymmetric_enthalpy(const mean_field::fem::FEM &f) { - mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { - return 1.10 + 0.07 * position(0) - 0.04 * position(1) + 0.03 * position(2); - }); - - mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); - - enthalpyField.ProjectCoefficient(coefficient); - - mfem::Vector enthalpyTrue; - enthalpyField.GetTrueDofs(enthalpyTrue); - - return enthalpyTrue; + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal( + elementCompactification); } - [[nodiscard]] mfem::Vector make_component_test_field( - const mean_field::fem::FEM &f, - const int component, - const int coordinate - ) { - const int dimension = f.mesh->Dimension(); + const mean_field::mapping::ElementDisplacementData displacementData = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacementElement, elementDisplacement); - MFEM_VERIFY(component >= 0 && component < dimension, "The requested vector component is invalid."); + const mean_field::mapping::ElementCompactificationData compactificationData( + compactificationElement, elementCompactification); - MFEM_VERIFY(coordinate >= -1 && coordinate < dimension, "The requested coordinate is invalid."); + const mean_field::mapping::ElementMappingData mappingData{ + .displacement = displacementData, + .compactification = compactificationData}; - /* - * coordinate == -1 gives the rigid translation e_component. - * - * Otherwise this gives - * - * w = x_coordinate e_component. - */ - mfem::VectorFunctionCoefficient coefficient( - dimension, [component, coordinate, dimension](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(dimension); - value = 0.0; + const mean_field::quadrature::Query query = EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::PressureIntegral>( + mean_field::quadrature::QuadratureRole::diagnostic, + transformation->OrderW(), std::array{pressureExtraOrder}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - value(component) = coordinate < 0 ? 1.0 : position(coordinate); - } - ); + const mean_field::quadrature::MfemRule rule = + f.quadratureFactory->get(query, transformation->GetGeometryType()); - mfem::ParGridFunction field(f.displacementFes.get()); + MFEM_VERIFY(rule.integration_rule != nullptr, + "The pressure-integral quadrature rule is null."); - field.ProjectCoefficient(coefficient); + enthalpyShape.SetSize(enthalpyElement.GetDof()); - mfem::Vector fieldTrue; - field.GetTrueDofs(fieldTrue); + for (int quadratureIndex = 0; + quadratureIndex < rule.integration_rule->GetNPoints(); + ++quadratureIndex) { + const mfem::IntegrationPoint &integrationPoint = + rule.integration_rule->IntPoint(quadratureIndex); - return fieldTrue; + transformation->SetIntPoint(&integrationPoint); + + const mean_field::mapping::MappingStatus mappingStatus = + domainMapper.EvaluateVolume(mappingData, *transformation, + integrationPoint, workspace, + mappingContext); + + MFEM_VERIFY(mappingStatus == mean_field::mapping::MappingStatus::valid, + "Stateless mapping failed in the " + "independent pressure integral. Element: " + << elementId + << ", attribute: " << transformation->Attribute + << ", quadrature point: " << quadratureIndex + << ", status: " << static_cast(mappingStatus)); + + enthalpyElement.CalcShape(integrationPoint, enthalpyShape); + + const double enthalpyValue = elementEnthalpy * enthalpyShape; + + const double pressureValue = + barotrope.pressure_from_enthalpy(enthalpyValue); + + const double contribution = + pressureValue * mappingContext.quadrature.weight; + + MFEM_VERIFY(std::isfinite(pressureValue) && std::isfinite(contribution), + "The independent pressure integral " + "encountered a non-finite value."); + + localPressureIntegral += contribution; } + } - [[nodiscard]] double global_dot( - const mfem::Vector &left, - const mfem::Vector &right, - MPI_Comm communicator - ) { - MFEM_VERIFY(left.Size() == right.Size(), "The global dot-product vectors have different sizes."); + double globalPressureIntegral = 0.0; - const double localDot = left * right; - double globalDot = 0.0; + MPI_Allreduce(&localPressureIntegral, &globalPressureIntegral, 1, MPI_DOUBLE, + MPI_SUM, f.mesh->GetComm()); - MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator); - - return globalDot; - } - - [[nodiscard]] double integrate_pressure( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &domainMapper, - const mean_field::eos::Polytrope &barotrope, - const mfem::Vector &enthalpyTrue, - const mfem::Vector &displacementTrue - ) { - MFEM_VERIFY( - enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), - "The pressure-integral enthalpy vector has the wrong size." - ); - - MFEM_VERIFY( - displacementTrue.Size() == f.displacementFes->GetTrueVSize(), - "The pressure-integral displacement vector has the wrong size." - ); - - mfem::Vector enthalpyLocal(f.enthalpyFes->GetVSize()); - - const mfem::Operator *enthalpyProlongation = f.enthalpyFes->GetProlongationMatrix(); - - if (enthalpyProlongation != nullptr) { - enthalpyProlongation->Mult(enthalpyTrue, enthalpyLocal); - } else { - enthalpyLocal = enthalpyTrue; - } - - mfem::Vector displacementLocal(f.displacementFes->GetVSize()); - - const mfem::Operator *displacementProlongation = f.displacementFes->GetProlongationMatrix(); - - if (displacementProlongation != nullptr) { - displacementProlongation->Mult(displacementTrue, displacementLocal); - } else { - displacementLocal = displacementTrue; - } - - const double pressureExtraOrderValue = - barotrope.polytropic_index() * static_cast(mean_field::field::Enthalpy::Scalar::familyOrder); - - MFEM_VERIFY( - std::isfinite(pressureExtraOrderValue) && pressureExtraOrderValue >= 0.0 && - pressureExtraOrderValue <= static_cast(std::numeric_limits::max()), - "The pressure-integral EOS order is invalid." - ); - - const int pressureExtraOrder = static_cast(std::ceil(pressureExtraOrderValue)); - - using EnthalpyField = mean_field::field::Field; - - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); - - mean_field::mapping::VolumeMappingContext mappingContext; - - mfem::Array enthalpyDofs; - mfem::Array displacementDofs; - mfem::Array compactificationDofs; - - mfem::Vector elementEnthalpy; - mfem::Vector elementDisplacement; - mfem::Vector elementCompactification; - mfem::Vector enthalpyShape; - - double localPressureIntegral = 0.0; - - const int vacuumAttribute = domainMapper.GetVacuumElementAttribute(); - - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); - - MFEM_VERIFY( - transformation != nullptr, "The pressure-integral reference received a null " - "element transformation." - ); - - if (transformation->Attribute == vacuumAttribute) { - continue; - } - - const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId); - - const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId); - - const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId); - - mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); - - mfem::DofTransformation *displacementDofTransformation = - f.displacementFes->GetElementVDofs(elementId, displacementDofs); - - mfem::DofTransformation *compactificationDofTransformation = - f.compactificationFes->GetElementDofs(elementId, compactificationDofs); - - enthalpyLocal.GetSubVector(enthalpyDofs, elementEnthalpy); - - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); - - f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); - - if (enthalpyDofTransformation != nullptr) { - enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy); - } - - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacement); - } - - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal(elementCompactification); - } - - const mean_field::mapping::ElementDisplacementData displacementData = - mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement); - - const mean_field::mapping::ElementCompactificationData compactificationData( - compactificationElement, elementCompactification - ); - - const mean_field::mapping::ElementMappingData mappingData{ - .displacement = displacementData, .compactification = compactificationData - }; - - const mean_field::quadrature::Query query = - EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::diagnostic, transformation->OrderW(), - std::array{pressureExtraOrder}, mean_field::utils::DOMAINS::STELLAR, - mean_field::quadrature::MappingKind::general - ); - - const mean_field::quadrature::MfemRule rule = - f.quadratureFactory->get(query, transformation->GetGeometryType()); - - MFEM_VERIFY(rule.integration_rule != nullptr, "The pressure-integral quadrature rule is null."); - - enthalpyShape.SetSize(enthalpyElement.GetDof()); - - for (int quadratureIndex = 0; quadratureIndex < rule.integration_rule->GetNPoints(); ++quadratureIndex) { - const mfem::IntegrationPoint &integrationPoint = rule.integration_rule->IntPoint(quadratureIndex); - - transformation->SetIntPoint(&integrationPoint); - - const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume( - mappingData, *transformation, integrationPoint, workspace, mappingContext - ); - - MFEM_VERIFY( - mappingStatus == mean_field::mapping::MappingStatus::valid, - "Stateless mapping failed in the " - "independent pressure integral. Element: " - << elementId << ", attribute: " << transformation->Attribute - << ", quadrature point: " << quadratureIndex << ", status: " << static_cast(mappingStatus) - ); - - enthalpyElement.CalcShape(integrationPoint, enthalpyShape); - - const double enthalpyValue = elementEnthalpy * enthalpyShape; - - const double pressureValue = barotrope.pressure_from_enthalpy(enthalpyValue); - - const double contribution = pressureValue * mappingContext.quadrature.weight; - - MFEM_VERIFY( - std::isfinite(pressureValue) && std::isfinite(contribution), "The independent pressure integral " - "encountered a non-finite value." - ); - - localPressureIntegral += contribution; - } - } - - double globalPressureIntegral = 0.0; - - MPI_Allreduce(&localPressureIntegral, &globalPressureIntegral, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm()); - - return globalPressureIntegral; - } + return globalPressureIntegral; +} } // namespace pressure_force_kernel_test_utils -TEST_CASE( - "Pressure Force Residual Vanishes For Zero Enthalpy", - tags::barotrope &tags::pressure &tags::kernels &tags::integration -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Pressure Force Residual Vanishes For Zero Enthalpy", + tags::barotrope &tags::pressure &tags::kernels &tags::integration) { + mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mean_field::eos::Polytrope barotrope(3.0, 0.25); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); - mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize()); - enthalpyTrue = 0.0; + mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize()); + enthalpyTrue = 0.0; - const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); + const mfem::Vector displacementTrue = + pressure_force_kernel_test_utils::make_zero_displacement(f); - mfem::Vector residualTrue; + mfem::Vector residualTrue; - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue - ); + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, + residualTrue); - REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); + REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); - const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); + const double residualNorm = + gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); - CHECK(residualNorm == 0.0); + CHECK(residualNorm == 0.0); } -TEST_CASE( - "Pressure Force Residual Excludes Vacuum Enthalpy Exactly", - tags::barotrope &tags::pressure &tags::kernels &tags::integration -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Pressure Force Residual Excludes Vacuum Enthalpy Exactly", + tags::barotrope &tags::pressure &tags::kernels &tags::integration) { + mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mean_field::eos::Polytrope barotrope(3.0, 0.25); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); - const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_vacuum_only_enthalpy(f); + const mfem::Vector enthalpyTrue = + pressure_force_kernel_test_utils::make_vacuum_only_enthalpy(f); - const double enthalpyNorm = gravity_prepared_test_utils::global_norm(enthalpyTrue, f.mesh->GetComm()); + const double enthalpyNorm = + gravity_prepared_test_utils::global_norm(enthalpyTrue, f.mesh->GetComm()); - /* - * Ensure this is a real exclusion test rather than another - * all-zero-input test. - */ - REQUIRE(enthalpyNorm > 0.0); + /* + * Ensure this is a real exclusion test rather than another + * all-zero-input test. + */ + REQUIRE(enthalpyNorm > 0.0); - const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); + const mfem::Vector displacementTrue = + pressure_force_kernel_test_utils::make_zero_displacement(f); - mfem::Vector residualTrue; + mfem::Vector residualTrue; - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue - ); + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, + residualTrue); - REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); + REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); - const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); + const double residualNorm = + gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); - CHECK(residualNorm == 0.0); + CHECK(residualNorm == 0.0); } -TEST_CASE( - "Pressure Force Residual Is Nonzero For Positive Stellar Pressure", - tags::barotrope &tags::pressure &tags::kernels &tags::integration -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Pressure Force Residual Is Nonzero For Positive Stellar Pressure", + tags::barotrope &tags::pressure &tags::kernels &tags::integration) { + mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mean_field::eos::Polytrope barotrope(3.0, 0.25); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); - /* - * With n = 3 and K = 1/4: - * - * P(1) = 1/4. - */ - mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize()); - enthalpyTrue = 1.0; + /* + * With n = 3 and K = 1/4: + * + * P(1) = 1/4. + */ + mfem::Vector enthalpyTrue(f.enthalpyFes->GetTrueVSize()); + enthalpyTrue = 1.0; - const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); + const mfem::Vector displacementTrue = + pressure_force_kernel_test_utils::make_zero_displacement(f); - mfem::Vector residualTrue; + mfem::Vector residualTrue; - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue - ); + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, + residualTrue); - const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); + const double residualNorm = + gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); - INFO("Positive-pressure residual norm = " << residualNorm); + INFO("Positive-pressure residual norm = " << residualNorm); - CHECK(std::isfinite(residualNorm)); + CHECK(std::isfinite(residualNorm)); - CHECK(residualNorm > 100.0 * std::numeric_limits::epsilon()); + CHECK(residualNorm > 100.0 * std::numeric_limits::epsilon()); } -TEST_CASE( - "Pressure Force Residual Does No Work Against Rigid Translations", - tags::barotrope &tags::pressure &tags::kernels &tags::integration &tags::accuracy -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Pressure Force Residual Does No Work Against Rigid Translations", + tags::barotrope &tags::pressure &tags::kernels &tags::integration + &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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES); + REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES); - const mean_field::eos::Polytrope barotrope(3.0, 0.25); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); - const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f); + const mfem::Vector enthalpyTrue = + pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f); - const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); + const mfem::Vector displacementTrue = + pressure_force_kernel_test_utils::make_zero_displacement(f); - mfem::Vector residualTrue; + mfem::Vector residualTrue; - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue - ); + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, + residualTrue); - const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); + const double residualNorm = + gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); - REQUIRE(residualNorm > 0.0); + REQUIRE(residualNorm > 0.0); - const int dimension = f.mesh->Dimension(); + const int dimension = f.mesh->Dimension(); - for (int component = 0; component < dimension; ++component) { - const mfem::Vector translationTrue = - pressure_force_kernel_test_utils::make_component_test_field(f, component, -1); + for (int component = 0; component < dimension; ++component) { + const mfem::Vector translationTrue = + pressure_force_kernel_test_utils::make_component_test_field( + f, component, -1); - const double translationNorm = gravity_prepared_test_utils::global_norm(translationTrue, f.mesh->GetComm()); + const double translationNorm = gravity_prepared_test_utils::global_norm( + translationTrue, f.mesh->GetComm()); - const double translationWork = - pressure_force_kernel_test_utils::global_dot(translationTrue, residualTrue, f.mesh->GetComm()); + const double translationWork = pressure_force_kernel_test_utils::global_dot( + translationTrue, residualTrue, f.mesh->GetComm()); - const double dotProductScale = std::fmax(residualNorm * translationNorm, 1.0); + const double dotProductScale = + std::fmax(residualNorm * translationNorm, 1.0); - CAPTURE(component, translationWork, dotProductScale); + CAPTURE(component, translationWork, dotProductScale); - CHECK(std::abs(translationWork) <= 5.0e-12 * dotProductScale); + CHECK(std::abs(translationWork) <= 5.0e-12 * dotProductScale); + } +} + +TEST_CASE("Pressure Force Residual Matches Independent Pressure Integral", + tags::barotrope &tags::pressure &tags::kernels &tags::integration + &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()); + + REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES); + + const mean_field::eos::Polytrope barotrope(3.0, 0.25); + + const mfem::Vector enthalpyTrue = + pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f); + + const mfem::Vector displacementTrue = + pressure_force_kernel_test_utils::make_zero_displacement(f); + + mfem::Vector residualTrue; + + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, + residualTrue); + + const int dimension = f.mesh->Dimension(); + + REQUIRE(dimension == 3); + + mfem::DenseMatrix virtualWork(dimension, dimension); + + for (int component = 0; component < dimension; ++component) { + for (int coordinate = 0; coordinate < dimension; ++coordinate) { + const mfem::Vector affineTestTrue = + pressure_force_kernel_test_utils::make_component_test_field( + f, component, coordinate); + + virtualWork(component, coordinate) = + pressure_force_kernel_test_utils::global_dot( + affineTestTrue, residualTrue, f.mesh->GetComm()); } + } + + const double pressureIntegral = + pressure_force_kernel_test_utils::integrate_pressure( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, + displacementTrue); + + REQUIRE(std::isfinite(pressureIntegral)); + + REQUIRE(pressureIntegral > 100.0 * std::numeric_limits::epsilon()); + + double meanDiagonalWork = 0.0; + + for (int component = 0; component < dimension; ++component) { + meanDiagonalWork += virtualWork(component, component); + } + + meanDiagonalWork /= static_cast(dimension); + + const double comparisonTolerance = 1.0e-6 * std::abs(pressureIntegral); + + INFO("Independent pressure integral = " << pressureIntegral); + + INFO("Expected diagonal virtual work = " << -pressureIntegral); + + INFO("Mean diagonal virtual work = " << meanDiagonalWork); + + INFO("Comparison tolerance = " << comparisonTolerance); + + /* + * This separate mean check gives a compact diagnostic if all three + * diagonal components drift together. + */ + CHECK(std::abs(meanDiagonalWork + pressureIntegral) <= comparisonTolerance); + + for (int component = 0; component < dimension; ++component) { + for (int coordinate = 0; coordinate < dimension; ++coordinate) { + const double computedWork = virtualWork(component, coordinate); + + const double expectedWork = + component == coordinate ? -pressureIntegral : 0.0; + + CAPTURE(component, coordinate, computedWork, expectedWork, + pressureIntegral, comparisonTolerance); + + CHECK(std::abs(computedWork - expectedWork) <= comparisonTolerance); + } + } + + const double relativeMeanError = + std::abs(meanDiagonalWork + pressureIntegral) / + std::abs(pressureIntegral); + + INFO("Relative mean diagonal error = " << relativeMeanError); + + CHECK(relativeMeanError <= 1.0e-6); } -TEST_CASE( - "Pressure Force Residual Matches Independent Pressure Integral", - tags::barotrope &tags::pressure &tags::kernels &tags::integration &tags::accuracy -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Pressure Force Residual Matches Deformed Pressure Volume Variation", + tags::barotrope &tags::pressure &tags::kernels &tags::integration + &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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - REQUIRE(f.displacementFes->GetOrdering() == mfem::Ordering::byNODES); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); - const mean_field::eos::Polytrope barotrope(3.0, 0.25); + /* + * This field is positive but spatially nonuniform, so the test + * exercises a genuinely nonuniform pressure distribution. + */ + const mfem::Vector enthalpyTrue = + pressure_force_kernel_test_utils::make_deterministic_vector( + f.enthalpyFes->GetTrueVSize(), 0.37); - const mfem::Vector enthalpyTrue = pressure_force_kernel_test_utils::make_positive_asymmetric_enthalpy(f); + /* + * make_displacement() contains anisotropic diagonal terms and + * quadratic cross terms. A scale of 0.67 therefore provides a + * nonzero, nonspherical, valid base geometry. + */ + const mfem::Vector baseDisplacementTrue = + gravity_prepared_test_utils::make_displacement(f, 0.67); - const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); + /* + * Differentiate along the same smooth deformation family. Thus + * + * d(epsilon) = (0.67 + epsilon) d_shape. + * + * This gives a controlled geometry path while still evaluating + * the derivative at a genuinely deformed base state. + */ + const mfem::Vector displacementVariationTrue = + gravity_prepared_test_utils::make_displacement(f, 1.0); - mfem::Vector residualTrue; + const double baseDisplacementNorm = gravity_prepared_test_utils::global_norm( + baseDisplacementTrue, f.mesh->GetComm()); - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue - ); + const double variationNorm = gravity_prepared_test_utils::global_norm( + displacementVariationTrue, f.mesh->GetComm()); - const int dimension = f.mesh->Dimension(); + REQUIRE(baseDisplacementNorm > + 100.0 * std::numeric_limits::epsilon()); - REQUIRE(dimension == 3); + REQUIRE(variationNorm > 100.0 * std::numeric_limits::epsilon()); - mfem::DenseMatrix virtualWork(dimension, dimension); + mfem::Vector residualTrue; - for (int component = 0; component < dimension; ++component) { - for (int coordinate = 0; coordinate < dimension; ++coordinate) { - const mfem::Vector affineTestTrue = - pressure_force_kernel_test_utils::make_component_test_field(f, component, coordinate); + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, + baseDisplacementTrue, residualTrue); - virtualWork(component, coordinate) = - pressure_force_kernel_test_utils::global_dot(affineTestTrue, residualTrue, f.mesh->GetComm()); - } + REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); + + const double residualWork = pressure_force_kernel_test_utils::global_dot( + displacementVariationTrue, residualTrue, f.mesh->GetComm()); + + REQUIRE(std::isfinite(residualWork)); + + REQUIRE(std::abs(residualWork) > + 100.0 * std::numeric_limits::epsilon()); + + /* + * The relatively broad initial sweep lets us see the expected + * centered-difference convergence before reaching the quadrature + * and representation plateau. + */ + constexpr std::array differenceSteps{1.0e-2, 5.0e-3, 2.5e-3, + 1.25e-3}; + + double bestRelativeDiscrepancy = std::numeric_limits::infinity(); + + for (const double differenceStep : differenceSteps) { + mfem::Vector displacementPlus(baseDisplacementTrue); + + mfem::Vector displacementMinus(baseDisplacementTrue); + + displacementPlus.Add(differenceStep, displacementVariationTrue); + + displacementMinus.Add(-differenceStep, displacementVariationTrue); + + const double pressureIntegralPlus = + pressure_force_kernel_test_utils::integrate_pressure( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, + displacementPlus); + + const double pressureIntegralMinus = + pressure_force_kernel_test_utils::integrate_pressure( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, + displacementMinus); + + REQUIRE(std::isfinite(pressureIntegralPlus)); + REQUIRE(std::isfinite(pressureIntegralMinus)); + + const double pressureVolumeDerivative = + (pressureIntegralPlus - pressureIntegralMinus) / (2.0 * differenceStep); + + REQUIRE(std::isfinite(pressureVolumeDerivative)); + + double comparisonScale = std::abs(residualWork); + + if (std::abs(pressureVolumeDerivative) > comparisonScale) { + comparisonScale = std::abs(pressureVolumeDerivative); } - const double pressureIntegral = pressure_force_kernel_test_utils::integrate_pressure( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue - ); + REQUIRE(comparisonScale > 100.0 * std::numeric_limits::epsilon()); - REQUIRE(std::isfinite(pressureIntegral)); + const double absoluteDiscrepancy = + std::abs(residualWork + pressureVolumeDerivative); - REQUIRE(pressureIntegral > 100.0 * std::numeric_limits::epsilon()); + const double relativeDiscrepancy = absoluteDiscrepancy / comparisonScale; - double meanDiagonalWork = 0.0; - - for (int component = 0; component < dimension; ++component) { - meanDiagonalWork += virtualWork(component, component); + if (relativeDiscrepancy < bestRelativeDiscrepancy) { + bestRelativeDiscrepancy = relativeDiscrepancy; } - meanDiagonalWork /= static_cast(dimension); + INFO("Difference step = " << differenceStep); - const double comparisonTolerance = 1.0e-6 * std::abs(pressureIntegral); + INFO("Pressure residual work = " << residualWork); - INFO("Independent pressure integral = " << pressureIntegral); + INFO("Pressure-volume derivative = " << pressureVolumeDerivative); - INFO("Expected diagonal virtual work = " << -pressureIntegral); + INFO("Residual work plus derivative = " << residualWork + + pressureVolumeDerivative); - INFO("Mean diagonal virtual work = " << meanDiagonalWork); - - INFO("Comparison tolerance = " << comparisonTolerance); + INFO("Relative discrepancy = " << relativeDiscrepancy); /* - * This separate mean check gives a compact diagnostic if all three - * diagonal components drift together. + * The signs must be opposite because the implemented pressure + * force is the negative variation of the pressure-volume + * functional. */ - CHECK(std::abs(meanDiagonalWork + pressureIntegral) <= comparisonTolerance); + CHECK(residualWork * pressureVolumeDerivative < 0.0); + } - for (int component = 0; component < dimension; ++component) { - for (int coordinate = 0; coordinate < dimension; ++coordinate) { - const double computedWork = virtualWork(component, coordinate); + INFO("Best pressure-volume relative discrepancy = " + << bestRelativeDiscrepancy); - const double expectedWork = component == coordinate ? -pressureIntegral : 0.0; - - CAPTURE(component, coordinate, computedWork, expectedWork, pressureIntegral, comparisonTolerance); - - CHECK(std::abs(computedWork - expectedWork) <= comparisonTolerance); - } - } - - const double relativeMeanError = std::abs(meanDiagonalWork + pressureIntegral) / std::abs(pressureIntegral); - - INFO("Relative mean diagonal error = " << relativeMeanError); - - CHECK(relativeMeanError <= 1.0e-6); -} - -TEST_CASE( - "Pressure Force Residual Matches Deformed Pressure Volume Variation", - tags::barotrope &tags::pressure &tags::kernels &tags::integration &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(3.0, 0.25); - - /* - * This field is positive but spatially nonuniform, so the test - * exercises a genuinely nonuniform pressure distribution. - */ - const mfem::Vector enthalpyTrue = - pressure_force_kernel_test_utils::make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.37); - - /* - * make_displacement() contains anisotropic diagonal terms and - * quadratic cross terms. A scale of 0.67 therefore provides a - * nonzero, nonspherical, valid base geometry. - */ - const mfem::Vector baseDisplacementTrue = gravity_prepared_test_utils::make_displacement(f, 0.67); - - /* - * Differentiate along the same smooth deformation family. Thus - * - * d(epsilon) = (0.67 + epsilon) d_shape. - * - * This gives a controlled geometry path while still evaluating - * the derivative at a genuinely deformed base state. - */ - const mfem::Vector displacementVariationTrue = gravity_prepared_test_utils::make_displacement(f, 1.0); - - const double baseDisplacementNorm = - gravity_prepared_test_utils::global_norm(baseDisplacementTrue, f.mesh->GetComm()); - - const double variationNorm = gravity_prepared_test_utils::global_norm(displacementVariationTrue, f.mesh->GetComm()); - - REQUIRE(baseDisplacementNorm > 100.0 * std::numeric_limits::epsilon()); - - REQUIRE(variationNorm > 100.0 * std::numeric_limits::epsilon()); - - mfem::Vector residualTrue; - - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, baseDisplacementTrue, residualTrue - ); - - REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); - - const double residualWork = - pressure_force_kernel_test_utils::global_dot(displacementVariationTrue, residualTrue, f.mesh->GetComm()); - - REQUIRE(std::isfinite(residualWork)); - - REQUIRE(std::abs(residualWork) > 100.0 * std::numeric_limits::epsilon()); - - /* - * The relatively broad initial sweep lets us see the expected - * centered-difference convergence before reaching the quadrature - * and representation plateau. - */ - constexpr std::array differenceSteps{1.0e-2, 5.0e-3, 2.5e-3, 1.25e-3}; - - double bestRelativeDiscrepancy = std::numeric_limits::infinity(); - - for (const double differenceStep : differenceSteps) { - mfem::Vector displacementPlus(baseDisplacementTrue); - - mfem::Vector displacementMinus(baseDisplacementTrue); - - displacementPlus.Add(differenceStep, displacementVariationTrue); - - displacementMinus.Add(-differenceStep, displacementVariationTrue); - - const double pressureIntegralPlus = pressure_force_kernel_test_utils::integrate_pressure( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementPlus - ); - - const double pressureIntegralMinus = pressure_force_kernel_test_utils::integrate_pressure( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementMinus - ); - - REQUIRE(std::isfinite(pressureIntegralPlus)); - REQUIRE(std::isfinite(pressureIntegralMinus)); - - const double pressureVolumeDerivative = (pressureIntegralPlus - pressureIntegralMinus) / (2.0 * differenceStep); - - REQUIRE(std::isfinite(pressureVolumeDerivative)); - - double comparisonScale = std::abs(residualWork); - - if (std::abs(pressureVolumeDerivative) > comparisonScale) { - comparisonScale = std::abs(pressureVolumeDerivative); - } - - REQUIRE(comparisonScale > 100.0 * std::numeric_limits::epsilon()); - - const double absoluteDiscrepancy = std::abs(residualWork + pressureVolumeDerivative); - - const double relativeDiscrepancy = absoluteDiscrepancy / comparisonScale; - - if (relativeDiscrepancy < bestRelativeDiscrepancy) { - bestRelativeDiscrepancy = relativeDiscrepancy; - } - - INFO("Difference step = " << differenceStep); - - INFO("Pressure residual work = " << residualWork); - - INFO("Pressure-volume derivative = " << pressureVolumeDerivative); - - INFO("Residual work plus derivative = " << residualWork + pressureVolumeDerivative); - - INFO("Relative discrepancy = " << relativeDiscrepancy); - - /* - * The signs must be opposite because the implemented pressure - * force is the negative variation of the pressure-volume - * functional. - */ - CHECK(residualWork * pressureVolumeDerivative < 0.0); - } - - INFO("Best pressure-volume relative discrepancy = " << bestRelativeDiscrepancy); - - /* - * This is intentionally a provisional but meaningful threshold. - * We will tighten it after measuring the convergence plateau. - */ - CHECK(bestRelativeDiscrepancy < 1.0e-8); + /* + * This is intentionally a provisional but meaningful threshold. + * We will tighten it after measuring the convergence plateau. + */ + CHECK(bestRelativeDiscrepancy < 1.0e-8); } diff --git a/tests/operators/prepared_hdiv_mass.cpp b/tests/operators/prepared_hdiv_mass.cpp index ee8b586..5129c81 100644 --- a/tests/operators/prepared_hdiv_mass.cpp +++ b/tests/operators/prepared_hdiv_mass.cpp @@ -1,5 +1,6 @@ #include #include +#include #include import mean_field; @@ -9,128 +10,181 @@ using namespace mean_field; using Catch::Matchers::WithinAbs; namespace prepared_test = gravity_prepared_test_utils; -TEST_CASE( - "Prepared Mapped Hdiv Mass Matches Stateless Kernel", - tags::gravity_prepared -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Prepared Mapped Hdiv Mass Matches Stateless Kernel", + tags::gravity_prepared) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - operators::PreparedMappedHDivMassOperator prepared_operator(f, *f.domainMapperStateless); - REQUIRE(prepared_operator.Width() == prepared_operator.GetFluxMap().reduced_size()); - REQUIRE(prepared_operator.Height() == prepared_operator.GetFluxMap().reduced_size()); + operators::PreparedMappedHDivMassOperator prepared_operator( + f, *f.domainMapperStateless); + REQUIRE(prepared_operator.Width() == + prepared_operator.GetFluxMap().reduced_size()); + REQUIRE(prepared_operator.Height() == + prepared_operator.GetFluxMap().reduced_size()); - const mfem::Vector gravity_gradient_true = - prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.21); - const mfem::Vector gravity_gradient = prepared_operator.GetFluxMap().gather(gravity_gradient_true); - const MPI_Comm communicator = f.gravityFluxFes->GetComm(); + const mfem::Vector gravity_gradient_true = + prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), + 0.21); + const mfem::Vector gravity_gradient = + prepared_operator.GetFluxMap().gather(gravity_gradient_true); + const MPI_Comm communicator = f.gravityFluxFes->GetComm(); - mfem::Vector identity_action; - mfem::Vector deformed_action; + mfem::Vector identity_action; + mfem::Vector deformed_action; - for (const double deformation_scale : {0.0, 1.0}) { - const mfem::Vector displacement_true = prepared_test::make_displacement(f, deformation_scale); - const mfem::Vector displacement = prepared_operator.GetDisplacementMap().gather(displacement_true); - - prepared_operator.Prepare(displacement); - - mfem::Vector prepared_action; - - prepared_operator.Mult(gravity_gradient, prepared_action); - mfem::Vector reference_action_true; - operators::kernels::apply_mapped_hdiv_mass( - f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, reference_action_true - ); - const mfem::Vector reference_action = prepared_operator.GetFluxMap().gather(reference_action_true); - - const double relative_error = prepared_test::relative_error(prepared_action, reference_action, communicator); - - INFO("Deformation scale = " << deformation_scale); - INFO("Prepared action norm = " << prepared_test::global_norm(prepared_action, communicator)); - INFO("Reference action norm = " << prepared_test::global_norm(reference_action, communicator)); - INFO("Relative prepared-operator error = " << relative_error); - - REQUIRE(prepared_operator.IsPrepared()); - CHECK_THAT(relative_error, WithinAbs(0.0, 2.0e-11)); - - if (deformation_scale == 0.0) { - identity_action = prepared_action; - } else { - deformed_action = prepared_action; - } - } - - const double geometry_change = prepared_test::relative_error(deformed_action, identity_action, communicator); - - INFO("Relative action change under deformation = " << geometry_change); - - CHECK(prepared_operator.GetPreparationCount() == 2); - CHECK(geometry_change > 1.0e-5); -} - -TEST_CASE( - "Prepared Mapped Hdiv Mass Preserves Operator Identities", - tags::gravity_prepared -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - operators::PreparedMappedHDivMassOperator prepared_operator(f, *f.domainMapperStateless); - REQUIRE(prepared_operator.Width() == prepared_operator.GetFluxMap().reduced_size()); - REQUIRE(prepared_operator.Height() == prepared_operator.GetFluxMap().reduced_size()); + for (const double deformation_scale : {0.0, 1.0}) { + const mfem::Vector displacement_true = + prepared_test::make_displacement(f, deformation_scale); const mfem::Vector displacement = - prepared_operator.GetDisplacementMap().gather(prepared_test::make_displacement(f, 1.0)); + prepared_operator.GetDisplacementMap().gather(displacement_true); + prepared_operator.Prepare(displacement); - const mfem::Vector first = prepared_operator.GetFluxMap().gather( - prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.17) - ); - const mfem::Vector second = prepared_operator.GetFluxMap().gather( - prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.83) - ); - const mfem::Vector combination = prepared_test::linear_combination(first, 1.7, second, -0.4); + mfem::Vector prepared_action; - mfem::Vector first_action; - mfem::Vector second_action; - mfem::Vector combination_action; - mfem::Vector zero_action; + prepared_operator.Mult(gravity_gradient, prepared_action); + mfem::Vector reference_action_true; + operators::kernels::apply_mapped_hdiv_mass( + f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, + reference_action_true); + const mfem::Vector reference_action = + prepared_operator.GetFluxMap().gather(reference_action_true); - prepared_operator.Mult(first, first_action); - prepared_operator.Mult(second, second_action); - prepared_operator.Mult(combination, combination_action); + const double relative_error = prepared_test::relative_error( + prepared_action, reference_action, communicator); - mfem::Vector expected_combination = prepared_test::linear_combination(first_action, 1.7, second_action, -0.4); + INFO("Deformation scale = " << deformation_scale); + INFO("Prepared action norm = " + << prepared_test::global_norm(prepared_action, communicator)); + INFO("Reference action norm = " + << prepared_test::global_norm(reference_action, communicator)); + INFO("Relative prepared-operator error = " << relative_error); - mfem::Vector zero(first.Size()); - zero = 0.0; - prepared_operator.Mult(zero, zero_action); + REQUIRE(prepared_operator.IsPrepared()); + CHECK_THAT(relative_error, WithinAbs(0.0, 2.0e-11)); - const MPI_Comm communicator = f.gravityFluxFes->GetComm(); + if (deformation_scale == 0.0) { + identity_action = prepared_action; + } else { + deformed_action = prepared_action; + } + } - const double first_second_product = prepared_test::global_dot(first, second_action, communicator); - const double second_first_product = prepared_test::global_dot(second, first_action, communicator); - const double symmetry_error = prepared_test::relative_scalar_error(first_second_product, second_first_product); - const double linearity_error = - prepared_test::relative_error(combination_action, expected_combination, communicator); - const double first_energy = prepared_test::global_dot(first, first_action, communicator); - const double second_energy = prepared_test::global_dot(second, second_action, communicator); - const std::uint64_t preparation_count = prepared_operator.GetPreparationCount(); + const double geometry_change = prepared_test::relative_error( + deformed_action, identity_action, communicator); - mfem::Vector repeated_action; - prepared_operator.Mult(first, repeated_action); + INFO("Relative action change under deformation = " << geometry_change); - INFO("u^T M v = " << first_second_product); - INFO("v^T M u = " << second_first_product); - INFO("Relative symmetry error = " << symmetry_error); - INFO("Relative linearity error = " << linearity_error); - INFO("u^T M u = " << first_energy); - INFO("v^T M v = " << second_energy); - - CHECK_THAT(symmetry_error, WithinAbs(0.0, 2.0e-12)); - CHECK_THAT(linearity_error, WithinAbs(0.0, 2.0e-12)); - CHECK_THAT(prepared_test::global_norm(zero_action, communicator), WithinAbs(0.0, 1.0e-14)); - CHECK(first_energy > 0.0); - CHECK(second_energy > 0.0); - CHECK(prepared_test::relative_error(repeated_action, first_action, communicator) < 2.0e-14); - CHECK(prepared_operator.GetPreparationCount() == preparation_count); + CHECK(prepared_operator.GetPreparationCount() == 2); + CHECK(geometry_change > 1.0e-5); +} + +TEST_CASE("Prepared Mapped Hdiv Mass Preserves Operator Identities", + tags::gravity_prepared) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + + operators::PreparedMappedHDivMassOperator prepared_operator( + f, *f.domainMapperStateless); + REQUIRE(prepared_operator.Width() == + prepared_operator.GetFluxMap().reduced_size()); + REQUIRE(prepared_operator.Height() == + prepared_operator.GetFluxMap().reduced_size()); + const mfem::Vector displacement = + prepared_operator.GetDisplacementMap().gather( + prepared_test::make_displacement(f, 1.0)); + prepared_operator.Prepare(displacement); + + const mfem::Vector first = prepared_operator.GetFluxMap().gather( + prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), + 0.17)); + const mfem::Vector second = prepared_operator.GetFluxMap().gather( + prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), + 0.83)); + const mfem::Vector combination = + prepared_test::linear_combination(first, 1.7, second, -0.4); + + mfem::Vector first_action; + mfem::Vector second_action; + mfem::Vector combination_action; + mfem::Vector zero_action; + + prepared_operator.Mult(first, first_action); + prepared_operator.Mult(second, second_action); + prepared_operator.Mult(combination, combination_action); + + mfem::Vector expected_combination = + prepared_test::linear_combination(first_action, 1.7, second_action, -0.4); + + mfem::Vector zero(first.Size()); + zero = 0.0; + prepared_operator.Mult(zero, zero_action); + + const MPI_Comm communicator = f.gravityFluxFes->GetComm(); + + const double first_second_product = + prepared_test::global_dot(first, second_action, communicator); + const double second_first_product = + prepared_test::global_dot(second, first_action, communicator); + const double symmetry_error = prepared_test::relative_scalar_error( + first_second_product, second_first_product); + const double linearity_error = prepared_test::relative_error( + combination_action, expected_combination, communicator); + const double first_energy = + prepared_test::global_dot(first, first_action, communicator); + const double second_energy = + prepared_test::global_dot(second, second_action, communicator); + const std::uint64_t preparation_count = + prepared_operator.GetPreparationCount(); + + mfem::Vector repeated_action; + prepared_operator.Mult(first, repeated_action); + + INFO("u^T M v = " << first_second_product); + INFO("v^T M u = " << second_first_product); + INFO("Relative symmetry error = " << symmetry_error); + INFO("Relative linearity error = " << linearity_error); + INFO("u^T M u = " << first_energy); + INFO("v^T M v = " << second_energy); + + CHECK_THAT(symmetry_error, WithinAbs(0.0, 2.0e-12)); + CHECK_THAT(linearity_error, WithinAbs(0.0, 2.0e-12)); + CHECK_THAT(prepared_test::global_norm(zero_action, communicator), + WithinAbs(0.0, 1.0e-14)); + CHECK(first_energy > 0.0); + CHECK(second_energy > 0.0); + CHECK(prepared_test::relative_error(repeated_action, first_action, + communicator) < 2.0e-14); + CHECK(prepared_operator.GetPreparationCount() == preparation_count); +} + +TEST_CASE("Prepared Mapped Hdiv Mass Diagonal Is Positive Across Both Domains", + tags::gravity_prepared) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + + operators::PreparedMappedHDivMassOperator prepared_operator( + f, *f.domainMapperStateless); + const mfem::Vector displacement = + prepared_operator.GetDisplacementMap().gather( + prepared_test::make_displacement(f, 1.0)); + prepared_operator.Prepare(displacement); + + mfem::Vector diagonal; + mfem::Vector true_diagonal; + prepared_operator.AssembleDiagonal(diagonal); + prepared_operator.AssembleTrueDiagonal(true_diagonal); + + REQUIRE(diagonal.Size() == prepared_operator.Height()); + REQUIRE(true_diagonal.Size() == prepared_operator.GetFluxMap().full_size()); + + const mfem::Vector gathered_true_diagonal = + prepared_operator.GetFluxMap().gather(true_diagonal); + + for (int i = 0; i < diagonal.Size(); ++i) { + REQUIRE(std::isfinite(diagonal(i))); + CHECK(diagonal(i) > 0.0); + CHECK_THAT(diagonal(i), WithinAbs(gathered_true_diagonal(i), + 1.0e-14 * std::abs(diagonal(i)))); + } } diff --git a/tests/operators/prepared_hydrostatic_equilibrium_analytic_accuracy.cpp b/tests/operators/prepared_hydrostatic_equilibrium_analytic_accuracy.cpp index 0f9d4a5..e652ff1 100644 --- a/tests/operators/prepared_hydrostatic_equilibrium_analytic_accuracy.cpp +++ b/tests/operators/prepared_hydrostatic_equilibrium_analytic_accuracy.cpp @@ -9,423 +9,430 @@ import mean_field; import test_helpers; namespace prepared_hydrostatic_analytic_solve_test_utils { - constexpr double bernoulliConstant = 0.83; - constexpr double enthalpyAmplitude = 0.61; +constexpr double bernoulliConstant = 0.83; +constexpr double enthalpyAmplitude = 0.61; - struct AnalyticCase { - const char *name; +struct AnalyticCase { + const char *name; - std::array deformationScale; - std::array angularVelocity; - std::array rotationCenter; - }; + std::array deformationScale; + std::array angularVelocity; + std::array rotationCenter; +}; - class EnthalpyJacobianOperator final : public mfem::Operator { - public: - EnthalpyJacobianOperator( - const int enthalpySize, - const mean_field::operators::PreparedHydrostaticEquilibriumOperator &preparedOperator - ) - : mfem::Operator(enthalpySize), - m_preparedOperator(preparedOperator) { - } +class EnthalpyJacobianOperator final : public mfem::Operator { +public: + EnthalpyJacobianOperator( + const int enthalpySize, + const mean_field::operators::PreparedHydrostaticEquilibriumOperator + &preparedOperator) + : mfem::Operator(enthalpySize), m_preparedOperator(preparedOperator) {} - void Mult( - const mfem::Vector &direction, - mfem::Vector &action - ) const override { - m_preparedOperator.ApplyEnthalpyJacobianAction(direction, action); - } + void Mult(const mfem::Vector &direction, + mfem::Vector &action) const override { + m_preparedOperator.ApplyEnthalpyJacobianAction(direction, action); + } - private: - const mean_field::operators::PreparedHydrostaticEquilibriumOperator &m_preparedOperator; - }; +private: + const mean_field::operators::PreparedHydrostaticEquilibriumOperator + &m_preparedOperator; +}; - mean_field::operators::context::hydrostatic::HydrostaticEquilibriumDependencies make_dependencies() { - return { - .discretization = {.identity = 701, .revision = 2}, - .enthalpy = {.identity = 709, .revision = 3}, - .gravityPotential = {.identity = 719, .revision = 5}, - .displacement = {.identity = 727, .revision = 7}, - .rotation = {.identity = 733, .revision = 11}, - .bernoulliConstant = {.identity = 739, .revision = 13} - }; - } +mean_field::operators::context::hydrostatic::HydrostaticEquilibriumDependencies +make_dependencies() { + return {.discretization = {.identity = 701, .revision = 2}, + .enthalpy = {.identity = 709, .revision = 3}, + .gravityPotential = {.identity = 719, .revision = 5}, + .displacement = {.identity = 727, .revision = 7}, + .rotation = {.identity = 733, .revision = 11}, + .bernoulliConstant = {.identity = 739, .revision = 13}}; +} - mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView make_state( - const mfem::Vector &enthalpy, - const mfem::Vector &gravityPotential, - const mfem::Vector &displacement - ) { - return { - .enthalpy = enthalpy, - .gravityPotential = gravityPotential, - .displacement = displacement, - .bernoulliConstant = bernoulliConstant - }; - } +mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView +make_state(const mfem::Vector &enthalpy, const mfem::Vector &gravityPotential, + const mfem::Vector &displacement) { + return {.enthalpy = enthalpy, + .gravityPotential = gravityPotential, + .displacement = displacement, + .bernoulliConstant = bernoulliConstant}; +} - mfem::Vector make_vector( - const std::array< - double, - 3> &values - ) { - mfem::Vector vector(3); +mfem::Vector make_vector(const std::array &values) { + mfem::Vector vector(3); - for (int component = 0; component < 3; ++component) { - vector(component) = values[static_cast(component)]; - } + for (int component = 0; component < 3; ++component) { + vector(component) = values[static_cast(component)]; + } - return vector; - } + return vector; +} - mean_field::physics::RigidRotation make_rotation(const AnalyticCase &analyticCase) { - return mean_field::physics::RigidRotation( - make_vector(analyticCase.angularVelocity), make_vector(analyticCase.rotationCenter) - ); - } +mean_field::physics::RigidRotation +make_rotation(const AnalyticCase &analyticCase) { + return mean_field::physics::RigidRotation( + make_vector(analyticCase.angularVelocity), + make_vector(analyticCase.rotationCenter)); +} - void map_to_physical( - const mfem::Vector &referencePosition, - const AnalyticCase &analyticCase, - mfem::Vector &physicalPosition - ) { - physicalPosition.SetSize(3); +void map_to_physical(const mfem::Vector &referencePosition, + const AnalyticCase &analyticCase, + mfem::Vector &physicalPosition) { + physicalPosition.SetSize(3); - for (int component = 0; component < 3; ++component) { - physicalPosition(component) = - analyticCase.deformationScale[static_cast(component)] * referencePosition(component); - } - } + for (int component = 0; component < 3; ++component) { + physicalPosition(component) = + analyticCase.deformationScale[static_cast(component)] * + referencePosition(component); + } +} - double exact_enthalpy_value(const mfem::Vector &referencePosition) { - double normalizedRadiusSquared = 0.0; +double exact_enthalpy_value(const mfem::Vector &referencePosition) { + double normalizedRadiusSquared = 0.0; - for (int component = 0; component < 3; ++component) { - const double normalizedCoordinate = referencePosition(component) / mean_field::utils::RADIUS; + for (int component = 0; component < 3; ++component) { + const double normalizedCoordinate = + referencePosition(component) / mean_field::utils::RADIUS; - normalizedRadiusSquared += normalizedCoordinate * normalizedCoordinate; - } + normalizedRadiusSquared += normalizedCoordinate * normalizedCoordinate; + } - return enthalpyAmplitude * std::max(0.0, 1.0 - normalizedRadiusSquared); - } + return enthalpyAmplitude * std::max(0.0, 1.0 - normalizedRadiusSquared); +} - double exact_potential_value( - const mfem::Vector &referencePosition, - const AnalyticCase &analyticCase, - const mean_field::physics::RigidRotation &rotation - ) { - mfem::Vector physicalPosition; +double +exact_potential_value(const mfem::Vector &referencePosition, + const AnalyticCase &analyticCase, + const mean_field::physics::RigidRotation &rotation) { + mfem::Vector physicalPosition; - map_to_physical(referencePosition, analyticCase, physicalPosition); + map_to_physical(referencePosition, analyticCase, physicalPosition); - /* - * Construct Phi so that - * - * h + Phi - Psi_rotation - C = 0 - * - * analytically. - */ - return bernoulliConstant + rotation.potential(physicalPosition) - exact_enthalpy_value(referencePosition); - } + /* + * Construct Phi so that + * + * h + Phi - Psi_rotation - C = 0 + * + * analytically. + */ + return bernoulliConstant + rotation.potential(physicalPosition) - + exact_enthalpy_value(referencePosition); +} - mfem::Array make_stellar_element_marker(const mean_field::fem::FEM &f) { - mfem::Array stellarElementMarker(f.mesh->GetNE()); +mfem::Array make_stellar_element_marker(const mean_field::fem::FEM &f) { + mfem::Array stellarElementMarker(f.mesh->GetNE()); - const int vacuumAttribute = f.domainMapperStateless->GetVacuumElementAttribute(); + const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - stellarElementMarker[elementId] = f.mesh->GetAttribute(elementId) != vacuumAttribute; - } + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + stellarElementMarker[elementId] = + f.mesh->GetAttribute(elementId) != vacuumAttribute; + } - return stellarElementMarker; - } + return stellarElementMarker; +} } // namespace prepared_hydrostatic_analytic_solve_test_utils -TEST_CASE( - "Prepared Hydrostatic Operator Solves Analytic Bernoulli Equilibria", - tags::barotrope_hydrostatic_prepared_analytic &tags::convergence &tags::accuracy -) { - using prepared_hydrostatic_analytic_solve_test_utils::AnalyticCase; +TEST_CASE("Prepared Hydrostatic Operator Solves Analytic Bernoulli Equilibria", + tags::barotrope_hydrostatic_prepared_analytic &tags::convergence + &tags::accuracy) { + using prepared_hydrostatic_analytic_solve_test_utils::AnalyticCase; + + constexpr double deformationX = 1.08; + constexpr double deformationY = 0.96; + + /* + * The third scale makes the affine deformation + * volume-preserving: + * + * det(F) = sx * sy * sz = 1. + */ + constexpr double deformationZ = 1.0 / (deformationX * deformationY); + + const std::array analyticCases{ + {{.name = "spherical nonrotating equilibrium", + .deformationScale = {1.0, 1.0, 1.0}, + .angularVelocity = {0.0, 0.0, 0.0}, + .rotationCenter = {0.0, 0.0, 0.0}}, + {.name = "spherical rotating equilibrium", + .deformationScale = {1.0, 1.0, 1.0}, + .angularVelocity = {0.13, -0.09, 0.31}, + .rotationCenter = {0.04, -0.03, 0.02}}, + {.name = "volume-preserving deformed rotating equilibrium", + .deformationScale = {deformationX, deformationY, deformationZ}, + .angularVelocity = {0.17, -0.12, 0.43}, + .rotationCenter = {0.031, -0.024, 0.018}}}}; - constexpr double deformationX = 1.08; - constexpr double deformationY = 0.96; + auto args = test_utils::setup_args(); - /* - * The third scale makes the affine deformation - * volume-preserving: - * - * det(F) = sx * sy * sz = 1. - */ - constexpr double deformationZ = 1.0 / (deformationX * deformationY); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - const std::array analyticCases{ - {{.name = "spherical nonrotating equilibrium", - .deformationScale = {1.0, 1.0, 1.0}, - .angularVelocity = {0.0, 0.0, 0.0}, - .rotationCenter = {0.0, 0.0, 0.0}}, - {.name = "spherical rotating equilibrium", - .deformationScale = {1.0, 1.0, 1.0}, - .angularVelocity = {0.13, -0.09, 0.31}, - .rotationCenter = {0.04, -0.03, 0.02}}, - {.name = "volume-preserving deformed rotating equilibrium", - .deformationScale = {deformationX, deformationY, deformationZ}, - .angularVelocity = {0.17, -0.12, 0.43}, - .rotationCenter = {0.031, -0.024, 0.018}}} - }; + const MPI_Comm communicator = f.mesh->GetComm(); - auto args = test_utils::setup_args(); + const mean_field::field::FieldDofMap enthalpyMap = + field_dof_test_utils::make_map( + *f.enthalpyFes); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + const mean_field::field::FieldDofMap gravityPotentialMap = + field_dof_test_utils::make_map( + *f.gravityPotentialFes); - const MPI_Comm communicator = f.mesh->GetComm(); + const mean_field::field::FieldDofMap displacementMap = + field_dof_test_utils::make_map( + *f.displacementFes); - const mean_field::field::FieldDofMap enthalpyMap = - field_dof_test_utils::make_map(*f.enthalpyFes); + const mfem::Array stellarElementMarker = + prepared_hydrostatic_analytic_solve_test_utils:: + make_stellar_element_marker(f); - const mean_field::field::FieldDofMap gravityPotentialMap = - field_dof_test_utils::make_map(*f.gravityPotentialFes); + for (const AnalyticCase &analyticCase : analyticCases) { + DYNAMIC_SECTION(analyticCase.name) { + const double deformationDeterminant = analyticCase.deformationScale[0] * + analyticCase.deformationScale[1] * + analyticCase.deformationScale[2]; - const mean_field::field::FieldDofMap displacementMap = - field_dof_test_utils::make_map(*f.displacementFes); + REQUIRE(std::abs(deformationDeterminant - 1.0) < 2.0e-14); - const mfem::Array stellarElementMarker = - prepared_hydrostatic_analytic_solve_test_utils::make_stellar_element_marker(f); + const mean_field::physics::RigidRotation rotation = + prepared_hydrostatic_analytic_solve_test_utils::make_rotation( + analyticCase); - for (const AnalyticCase &analyticCase : analyticCases) { - DYNAMIC_SECTION(analyticCase.name) { - const double deformationDeterminant = - analyticCase.deformationScale[0] * analyticCase.deformationScale[1] * analyticCase.deformationScale[2]; + auto displacementFunction = + [&analyticCase](const mfem::Vector &referencePosition, + mfem::Vector &displacementValue) { + mfem::Vector physicalPosition; - REQUIRE(std::abs(deformationDeterminant - 1.0) < 2.0e-14); + prepared_hydrostatic_analytic_solve_test_utils::map_to_physical( + referencePosition, analyticCase, physicalPosition); - const mean_field::physics::RigidRotation rotation = - prepared_hydrostatic_analytic_solve_test_utils::make_rotation(analyticCase); + displacementValue.SetSize(3); + displacementValue = physicalPosition; + displacementValue -= referencePosition; + }; - auto displacementFunction = - [&analyticCase](const mfem::Vector &referencePosition, mfem::Vector &displacementValue) { - mfem::Vector physicalPosition; + auto potentialFunction = [&analyticCase, &rotation]( + const mfem::Vector &referencePosition) { + return prepared_hydrostatic_analytic_solve_test_utils:: + exact_potential_value(referencePosition, analyticCase, rotation); + }; - prepared_hydrostatic_analytic_solve_test_utils::map_to_physical( - referencePosition, analyticCase, physicalPosition - ); + auto enthalpyFunction = [](const mfem::Vector &referencePosition) { + return prepared_hydrostatic_analytic_solve_test_utils:: + exact_enthalpy_value(referencePosition); + }; - displacementValue.SetSize(3); - displacementValue = physicalPosition; - displacementValue -= referencePosition; - }; + mfem::VectorFunctionCoefficient displacementCoefficient( + f.mesh->Dimension(), displacementFunction); - auto potentialFunction = [&analyticCase, &rotation](const mfem::Vector &referencePosition) { - return prepared_hydrostatic_analytic_solve_test_utils::exact_potential_value( - referencePosition, analyticCase, rotation - ); - }; + mfem::FunctionCoefficient potentialCoefficient(potentialFunction); - auto enthalpyFunction = [](const mfem::Vector &referencePosition) { - return prepared_hydrostatic_analytic_solve_test_utils::exact_enthalpy_value(referencePosition); - }; + mfem::FunctionCoefficient exactEnthalpyCoefficient(enthalpyFunction); - mfem::VectorFunctionCoefficient displacementCoefficient(f.mesh->Dimension(), displacementFunction); + /* + * Project the prescribed geometry and potential. + */ + mfem::ParGridFunction displacementField(f.displacementFes.get()); - mfem::FunctionCoefficient potentialCoefficient(potentialFunction); + mfem::ParGridFunction potentialField(f.gravityPotentialFes.get()); - mfem::FunctionCoefficient exactEnthalpyCoefficient(enthalpyFunction); + displacementField.ProjectCoefficient(displacementCoefficient); - /* - * Project the prescribed geometry and potential. - */ - mfem::ParGridFunction displacementField(f.displacementFes.get()); + potentialField.ProjectCoefficient(potentialCoefficient); - mfem::ParGridFunction potentialField(f.gravityPotentialFes.get()); + mfem::Vector displacementTrue; + mfem::Vector gravityPotentialTrue; - displacementField.ProjectCoefficient(displacementCoefficient); + displacementField.GetTrueDofs(displacementTrue); + potentialField.GetTrueDofs(gravityPotentialTrue); - potentialField.ProjectCoefficient(potentialCoefficient); + const mfem::Vector displacement = + displacementMap.gather(displacementTrue); + const mfem::Vector gravityPotential = + gravityPotentialMap.gather(gravityPotentialTrue); - mfem::Vector displacementTrue; - mfem::Vector gravityPotentialTrue; + /* + * This projection is not used as the solution. It gives + * the best directly available representation baseline + * against which the solved field can be compared. + */ + mfem::ParGridFunction projectedEnthalpyField(f.enthalpyFes.get()); - displacementField.GetTrueDofs(displacementTrue); - potentialField.GetTrueDofs(gravityPotentialTrue); + projectedEnthalpyField.ProjectCoefficient(exactEnthalpyCoefficient); - const mfem::Vector displacement = displacementMap.gather(displacementTrue); - const mfem::Vector gravityPotential = gravityPotentialMap.gather(gravityPotentialTrue); + mfem::ParGridFunction zeroEnthalpyField(f.enthalpyFes.get()); - /* - * This projection is not used as the solution. It gives - * the best directly available representation baseline - * against which the solved field can be compared. - */ - mfem::ParGridFunction projectedEnthalpyField(f.enthalpyFes.get()); + zeroEnthalpyField = 0.0; - projectedEnthalpyField.ProjectCoefficient(exactEnthalpyCoefficient); + const double exactEnthalpyNorm = zeroEnthalpyField.ComputeL2Error( + exactEnthalpyCoefficient, nullptr, &stellarElementMarker); - mfem::ParGridFunction zeroEnthalpyField(f.enthalpyFes.get()); + const double projectionError = projectedEnthalpyField.ComputeL2Error( + exactEnthalpyCoefficient, nullptr, &stellarElementMarker); - zeroEnthalpyField = 0.0; + REQUIRE(exactEnthalpyNorm > 0.0); - const double exactEnthalpyNorm = - zeroEnthalpyField.ComputeL2Error(exactEnthalpyCoefficient, nullptr, &stellarElementMarker); + const double relativeProjectionError = + projectionError / exactEnthalpyNorm; - const double projectionError = - projectedEnthalpyField.ComputeL2Error(exactEnthalpyCoefficient, nullptr, &stellarElementMarker); + /* + * Begin deliberately far from equilibrium. + */ + mfem::Vector enthalpy(enthalpyMap.reduced_size()); - REQUIRE(exactEnthalpyNorm > 0.0); + enthalpy = 0.0; - const double relativeProjectionError = projectionError / exactEnthalpyNorm; + auto dependencies = + prepared_hydrostatic_analytic_solve_test_utils::make_dependencies(); - /* - * Begin deliberately far from equilibrium. - */ - mfem::Vector enthalpy(enthalpyMap.reduced_size()); + mean_field::operators::PreparedHydrostaticEquilibriumOperator + preparedOperator(f, *f.domainMapperStateless); - enthalpy = 0.0; + const auto initialReport = preparedOperator.Prepare( + prepared_hydrostatic_analytic_solve_test_utils::make_state( + enthalpy, gravityPotential, displacement), + dependencies, rotation); - auto dependencies = prepared_hydrostatic_analytic_solve_test_utils::make_dependencies(); + REQUIRE(initialReport.preparedResidual); + REQUIRE(initialReport.preparedAlgebraicJacobianBlocks); - mean_field::operators::PreparedHydrostaticEquilibriumOperator preparedOperator(f, *f.domainMapperStateless); + mfem::Vector initialResidual; - const auto initialReport = preparedOperator.Prepare( - prepared_hydrostatic_analytic_solve_test_utils::make_state(enthalpy, gravityPotential, displacement), - dependencies, rotation - ); + preparedOperator.BuildResidual(initialResidual); - REQUIRE(initialReport.preparedResidual); - REQUIRE(initialReport.preparedAlgebraicJacobianBlocks); + const double initialResidualNorm = + gravity_prepared_test_utils::global_norm(initialResidual, + communicator); - mfem::Vector initialResidual; + REQUIRE(initialResidualNorm > 1.0e-12); - preparedOperator.BuildResidual(initialResidual); + /* + * One discrete Newton step: + * + * M_h delta_h = -R_h. + * + * The full four-block Bernoulli Jacobian is rectangular + * and underdetermined in isolation. Freezing Phi, C, + * rotation, and displacement makes this a well-defined + * enthalpy solve. + */ + prepared_hydrostatic_analytic_solve_test_utils::EnthalpyJacobianOperator + enthalpyJacobian(enthalpyMap.reduced_size(), preparedOperator); - const double initialResidualNorm = gravity_prepared_test_utils::global_norm(initialResidual, communicator); + mfem::Vector rightHandSide(initialResidual); + rightHandSide *= -1.0; - REQUIRE(initialResidualNorm > 1.0e-12); + mfem::Vector enthalpyCorrection(enthalpyMap.reduced_size()); - /* - * One discrete Newton step: - * - * M_h delta_h = -R_h. - * - * The full four-block Bernoulli Jacobian is rectangular - * and underdetermined in isolation. Freezing Phi, C, - * rotation, and displacement makes this a well-defined - * enthalpy solve. - */ - prepared_hydrostatic_analytic_solve_test_utils::EnthalpyJacobianOperator enthalpyJacobian( - enthalpyMap.reduced_size(), preparedOperator - ); + enthalpyCorrection = 0.0; - mfem::Vector rightHandSide(initialResidual); - rightHandSide *= -1.0; + /* + * The reduced operator contains only stellar-supported + * enthalpy DOFs and is positive definite. MINRES remains + * appropriate for this symmetric system. + */ + mfem::MINRESSolver linearSolver(communicator); - mfem::Vector enthalpyCorrection(enthalpyMap.reduced_size()); + linearSolver.SetOperator(enthalpyJacobian); - enthalpyCorrection = 0.0; + linearSolver.SetRelTol(1.0e-13); + linearSolver.SetAbsTol(1.0e-14); + linearSolver.SetMaxIter(2000); + linearSolver.SetPrintLevel(0); - /* - * The reduced operator contains only stellar-supported - * enthalpy DOFs and is positive definite. MINRES remains - * appropriate for this symmetric system. - */ - mfem::MINRESSolver linearSolver(communicator); + linearSolver.Mult(rightHandSide, enthalpyCorrection); - linearSolver.SetOperator(enthalpyJacobian); + INFO("Linear solver converged = " << linearSolver.GetConverged()); - linearSolver.SetRelTol(1.0e-13); - linearSolver.SetAbsTol(1.0e-14); - linearSolver.SetMaxIter(2000); - linearSolver.SetPrintLevel(0); + INFO("Linear solver iterations = " << linearSolver.GetNumIterations()); - linearSolver.Mult(rightHandSide, enthalpyCorrection); + INFO("Linear solver final norm = " << linearSolver.GetFinalNorm()); - INFO("Linear solver converged = " << linearSolver.GetConverged()); + REQUIRE(linearSolver.GetConverged()); - INFO("Linear solver iterations = " << linearSolver.GetNumIterations()); + enthalpy += enthalpyCorrection; - INFO("Linear solver final norm = " << linearSolver.GetFinalNorm()); + /* + * Only the enthalpy state changed. Geometry, rotation, + * and algebraic Jacobian data must remain reusable. + */ + ++dependencies.enthalpy.revision; - REQUIRE(linearSolver.GetConverged()); + const auto solvedReport = preparedOperator.Prepare( + prepared_hydrostatic_analytic_solve_test_utils::make_state( + enthalpy, gravityPotential, displacement), + dependencies, rotation); - enthalpy += enthalpyCorrection; + CHECK(solvedReport.contextReport.updatedEnthalpy); - /* - * Only the enthalpy state changed. Geometry, rotation, - * and algebraic Jacobian data must remain reusable. - */ - ++dependencies.enthalpy.revision; + CHECK(solvedReport.contextReport.preparedBaseState); - const auto solvedReport = preparedOperator.Prepare( - prepared_hydrostatic_analytic_solve_test_utils::make_state(enthalpy, gravityPotential, displacement), - dependencies, rotation - ); + CHECK_FALSE(solvedReport.contextReport.preparedGeometryState); - CHECK(solvedReport.contextReport.updatedEnthalpy); + CHECK_FALSE(solvedReport.preparedAlgebraicJacobianBlocks); - CHECK(solvedReport.contextReport.preparedBaseState); + mfem::Vector solvedResidual; - CHECK_FALSE(solvedReport.contextReport.preparedGeometryState); + preparedOperator.BuildResidual(solvedResidual); - CHECK_FALSE(solvedReport.preparedAlgebraicJacobianBlocks); + const double solvedResidualNorm = + gravity_prepared_test_utils::global_norm(solvedResidual, + communicator); - mfem::Vector solvedResidual; + const double residualReduction = solvedResidualNorm / initialResidualNorm; - preparedOperator.BuildResidual(solvedResidual); + /* + * Compare the solved field with the continuum analytic + * enthalpy over stellar elements only. + * + * All three mappings have determinant one, so this + * normalized L2 error is also unchanged by the physical + * volume transformation. + */ + mfem::ParGridFunction solvedEnthalpyField(f.enthalpyFes.get()); - const double solvedResidualNorm = gravity_prepared_test_utils::global_norm(solvedResidual, communicator); + mfem::Vector enthalpyTrue(enthalpyMap.full_size()); + enthalpyMap.scatter(enthalpy, enthalpyTrue); + solvedEnthalpyField.SetFromTrueDofs(enthalpyTrue); - const double residualReduction = solvedResidualNorm / initialResidualNorm; + const double solvedAnalyticError = solvedEnthalpyField.ComputeL2Error( + exactEnthalpyCoefficient, nullptr, &stellarElementMarker); - /* - * Compare the solved field with the continuum analytic - * enthalpy over stellar elements only. - * - * All three mappings have determinant one, so this - * normalized L2 error is also unchanged by the physical - * volume transformation. - */ - mfem::ParGridFunction solvedEnthalpyField(f.enthalpyFes.get()); + const double relativeSolvedAnalyticError = + solvedAnalyticError / exactEnthalpyNorm; - mfem::Vector enthalpyTrue(enthalpyMap.full_size()); - enthalpyMap.scatter(enthalpy, enthalpyTrue); - solvedEnthalpyField.SetFromTrueDofs(enthalpyTrue); + INFO("Deformation determinant = " << deformationDeterminant); - const double solvedAnalyticError = - solvedEnthalpyField.ComputeL2Error(exactEnthalpyCoefficient, nullptr, &stellarElementMarker); + INFO("Initial weak residual norm = " << initialResidualNorm); - const double relativeSolvedAnalyticError = solvedAnalyticError / exactEnthalpyNorm; + INFO("Solved weak residual norm = " << solvedResidualNorm); - INFO("Deformation determinant = " << deformationDeterminant); + INFO("Weak residual reduction = " << residualReduction); - INFO("Initial weak residual norm = " << initialResidualNorm); + INFO("Relative analytic projection floor = " << relativeProjectionError); - INFO("Solved weak residual norm = " << solvedResidualNorm); + INFO("Relative solved analytic L2 error = " + << relativeSolvedAnalyticError); - INFO("Weak residual reduction = " << residualReduction); + /* + * The discrete Bernoulli equation must be solved essentially + * to the linear-solver floor. + */ + CHECK(residualReduction < 1.0e-10); - INFO("Relative analytic projection floor = " << relativeProjectionError); + /* + * The directly projected analytic enthalpy provides a lower + * representation bound, but it is not the expected solution + * of the cross-space discrete Bernoulli equation. The latter + * also contains potential-projection and mapped-space + * compatibility errors. + */ + CHECK(relativeSolvedAnalyticError < + std::max(5.0 * relativeProjectionError, 1.25e-4)); - INFO("Relative solved analytic L2 error = " << relativeSolvedAnalyticError); - - /* - * The discrete Bernoulli equation must be solved essentially - * to the linear-solver floor. - */ - CHECK(residualReduction < 1.0e-10); - - /* - * The directly projected analytic enthalpy provides a lower - * representation bound, but it is not the expected solution - * of the cross-space discrete Bernoulli equation. The latter - * also contains potential-projection and mapped-space - * compatibility errors. - */ - CHECK(relativeSolvedAnalyticError < std::max(5.0 * relativeProjectionError, 1.25e-4)); - - /* - * Record that the analytic error remains within one order of - * magnitude of the direct enthalpy projection floor. - */ - CHECK(relativeSolvedAnalyticError / relativeProjectionError < 5.0); - } + /* + * Record that the analytic error remains within one order of + * magnitude of the direct enthalpy projection floor. + */ + CHECK(relativeSolvedAnalyticError / relativeProjectionError < 5.0); } + } } diff --git a/tests/operators/prepared_pressure_force.cpp b/tests/operators/prepared_pressure_force.cpp index eee9f7d..de8e068 100644 --- a/tests/operators/prepared_pressure_force.cpp +++ b/tests/operators/prepared_pressure_force.cpp @@ -11,696 +11,711 @@ import mean_field; import test_helpers; namespace prepared_pressure_force_test_utils { - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; +using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; - struct Maps final { - mean_field::field::FieldDofMap density; - mean_field::field::FieldDofMap displacement; - mean_field::field::FieldDofMap gravityFlux; - mean_field::field::FieldDofMap gravityPotential; - mean_field::field::FieldDofMap enthalpy; +struct Maps final { + mean_field::field::FieldDofMap density; + mean_field::field::FieldDofMap displacement; + mean_field::field::FieldDofMap gravityFlux; + mean_field::field::FieldDofMap gravityPotential; + mean_field::field::FieldDofMap enthalpy; - explicit Maps(const mean_field::fem::FEM &f) - : density( - mean_field::field::make_field_dof_map< - mean_field::field::Density, - DomainSchema>(*f.densityFes) - ), - displacement( - mean_field::field::make_field_dof_map< - mean_field::field::Displacement, - DomainSchema>(*f.displacementFes) - ), - gravityFlux( - mean_field::field::make_field_dof_map< - mean_field::field::Gravity, - DomainSchema>(*f.gravityFluxFes) - ), - gravityPotential( - mean_field::field::make_field_dof_map< - mean_field::field::Gravity, - DomainSchema>(*f.gravityPotentialFes) - ), - enthalpy( - mean_field::field::make_field_dof_map< - mean_field::field::Enthalpy, - DomainSchema>(*f.enthalpyFes) - ) { - } - }; + explicit Maps(const mean_field::fem::FEM &f) + : density( + mean_field::field::make_field_dof_map(*f.densityFes)), + displacement(mean_field::field::make_field_dof_map< + mean_field::field::Displacement, DomainSchema>( + *f.displacementFes)), + gravityFlux( + mean_field::field::make_field_dof_map( + *f.gravityFluxFes)), + gravityPotential( + mean_field::field::make_field_dof_map( + *f.gravityPotentialFes)), + enthalpy(mean_field::field::make_field_dof_map< + mean_field::field::Enthalpy, DomainSchema>(*f.enthalpyFes)) {} +}; - [[nodiscard]] - mfem::Vector make_positive_enthalpy_true( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::Vector enthalpy(f.enthalpyFes->GetTrueVSize()); +[[nodiscard]] +mfem::Vector make_positive_enthalpy_true(const mean_field::fem::FEM &f, + const double phase) { + mfem::Vector enthalpy(f.enthalpyFes->GetTrueVSize()); - for (int index = 0; index < enthalpy.Size(); ++index) { - const double position = static_cast(index + 1); + for (int index = 0; index < enthalpy.Size(); ++index) { + const double position = static_cast(index + 1); - enthalpy(index) = - 0.93 + 0.09 * std::sin(0.23 * position + phase) + 0.04 * std::cos(0.17 * position - 0.5 * phase); - } + enthalpy(index) = 0.93 + 0.09 * std::sin(0.23 * position + phase) + + 0.04 * std::cos(0.17 * position - 0.5 * phase); + } - return enthalpy; - } + return enthalpy; +} - [[nodiscard]] - mfem::Vector make_enthalpy_direction_true( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::Vector direction(f.enthalpyFes->GetTrueVSize()); +[[nodiscard]] +mfem::Vector make_enthalpy_direction_true(const mean_field::fem::FEM &f, + const double phase) { + mfem::Vector direction(f.enthalpyFes->GetTrueVSize()); - for (int index = 0; index < direction.Size(); ++index) { - const double position = static_cast(index + 1); + for (int index = 0; index < direction.Size(); ++index) { + const double position = static_cast(index + 1); - direction(index) = - 0.27 * std::sin(0.19 * position + phase) + 0.14 * std::cos(0.13 * position - 0.5 * phase); - } + direction(index) = 0.27 * std::sin(0.19 * position + phase) + + 0.14 * std::cos(0.13 * position - 0.5 * phase); + } - return direction; - } + return direction; +} - [[nodiscard]] - mfem::Vector make_displacement_direction_true( - const mean_field::fem::FEM &f, - const double phase - ) { - MFEM_VERIFY( - f.mesh->Dimension() == 3, "The prepared pressure-force test requires a " - "three-dimensional mesh." - ); +[[nodiscard]] +mfem::Vector make_displacement_direction_true(const mean_field::fem::FEM &f, + const double phase) { + MFEM_VERIFY(f.mesh->Dimension() == 3, + "The prepared pressure-force test requires a " + "three-dimensional mesh."); - mfem::ParGridFunction directionField(f.displacementFes.get()); + mfem::ParGridFunction directionField(f.displacementFes.get()); - mfem::VectorFunctionCoefficient directionCoefficient( - 3, [phase](const mfem::Vector &position, mfem::Vector &value) { - const double x = position(0); + mfem::VectorFunctionCoefficient directionCoefficient( + 3, [phase](const mfem::Vector &position, mfem::Vector &value) { + const double x = position(0); - const double y = position(1); + const double y = position(1); - const double z = position(2); + const double z = position(2); - value.SetSize(3); + value.SetSize(3); - value(0) = 0.019 * x + 0.011 * y * z - 0.006 * z * z + 0.004 * phase * y; + value(0) = + 0.019 * x + 0.011 * y * z - 0.006 * z * z + 0.004 * phase * y; - value(1) = -0.016 * y + 0.008 * x * z + 0.005 * x * x - 0.003 * phase * z; + value(1) = + -0.016 * y + 0.008 * x * z + 0.005 * x * x - 0.003 * phase * z; - value(2) = 0.013 * z - 0.010 * x * y + 0.006 * y * y + 0.004 * phase * x; - } - ); + value(2) = + 0.013 * z - 0.010 * x * y + 0.006 * y * y + 0.004 * phase * x; + }); - directionField.ProjectCoefficient(directionCoefficient); + directionField.ProjectCoefficient(directionCoefficient); - mfem::Vector directionTrue; + mfem::Vector directionTrue; - directionField.GetTrueDofs(directionTrue); + directionField.GetTrueDofs(directionTrue); - return directionTrue; - } + return directionTrue; +} - [[nodiscard]] - double relative_difference( - const mfem::Vector &left, - const mfem::Vector &right, - const MPI_Comm communicator - ) { - MFEM_VERIFY( - left.Size() == right.Size(), "Cannot compare prepared pressure-force vectors with " - "different sizes." - ); +[[nodiscard]] +double relative_difference(const mfem::Vector &left, const mfem::Vector &right, + const MPI_Comm communicator) { + MFEM_VERIFY(left.Size() == right.Size(), + "Cannot compare prepared pressure-force vectors with " + "different sizes."); - mfem::Vector difference(left); + mfem::Vector difference(left); - difference -= right; + difference -= right; - const double scale = std::max( - {gravity_prepared_test_utils::global_norm(left, communicator), - gravity_prepared_test_utils::global_norm(right, communicator), - 100.0 * std::numeric_limits::epsilon()} - ); + const double scale = + std::max({gravity_prepared_test_utils::global_norm(left, communicator), + gravity_prepared_test_utils::global_norm(right, communicator), + 100.0 * std::numeric_limits::epsilon()}); - return gravity_prepared_test_utils::global_norm(difference, communicator) / scale; - } + return gravity_prepared_test_utils::global_norm(difference, communicator) / + scale; +} - [[nodiscard]] - mean_field::operators::context::pressure_force::PressureForceDependencies make_dependencies() { - return { - .discretization = {.identity = 1201, .revision = 3}, - .enthalpy = {.identity = 1213, .revision = 5}, - .displacement = {.identity = 1217, .revision = 7} - }; - } +[[nodiscard]] +mean_field::operators::context::pressure_force::PressureForceDependencies +make_dependencies() { + return {.discretization = {.identity = 1201, .revision = 3}, + .enthalpy = {.identity = 1213, .revision = 5}, + .displacement = {.identity = 1217, .revision = 7}}; +} - constexpr auto densityValue = - mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::density_field.mass_term); +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 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 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 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 enthalpyValue = + mean_field::utils::blocks::get_value_block( + mean_field::utils::blocks::enthalpy_field.specific_term); - constexpr auto barotropicConstantValue = mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term - ); +constexpr auto barotropicConstantValue = + 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 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 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 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 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 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 - ); +constexpr auto massResidual = + mean_field::utils::blocks::get_residual_block( + mean_field::utils::blocks::barotropic_constant_field + .mass_normalization_term); - [[nodiscard]] - mean_field::operators::BarotropicEquilibriumLayout make_coupled_layout(const Maps &maps) { - const std::array valueSizes{ - maps.density.reduced_size(), maps.displacement.reduced_size(), maps.gravityFlux.reduced_size(), - maps.gravityPotential.reduced_size(), maps.enthalpy.reduced_size(), 1 - }; +[[nodiscard]] +mean_field::operators::BarotropicEquilibriumLayout +make_coupled_layout(const Maps &maps) { + const std::array valueSizes{ + maps.density.reduced_size(), maps.displacement.reduced_size(), + maps.gravityFlux.reduced_size(), maps.gravityPotential.reduced_size(), + maps.enthalpy.reduced_size(), 1}; - const std::array residualSizes{ - maps.gravityFlux.reduced_size(), maps.gravityPotential.reduced_size(), maps.density.reduced_size(), - maps.displacement.reduced_size(), maps.enthalpy.reduced_size(), 1 - }; + const std::array residualSizes{ + maps.gravityFlux.reduced_size(), maps.gravityPotential.reduced_size(), + maps.density.reduced_size(), maps.displacement.reduced_size(), + maps.enthalpy.reduced_size(), 1}; - return {valueSizes, residualSizes}; - } + return {valueSizes, residualSizes}; +} - template - [[nodiscard]] - mfem::Vector copy_residual_block( - const mfem::Vector &action, - const mean_field::operators::BarotropicEquilibriumLayout &layout, - const mean_field::utils::blocks::residual_block block - ) { - mfem::Vector result(layout.size(block)); +template +[[nodiscard]] +mfem::Vector copy_residual_block( + const mfem::Vector &action, + const mean_field::operators::BarotropicEquilibriumLayout &layout, + const mean_field::utils::blocks::residual_block block) { + mfem::Vector result(layout.size(block)); - const int offset = layout.offset(block); + const int offset = layout.offset(block); - for (int entry = 0; entry < result.Size(); ++entry) { - result(entry) = action(offset + entry); - } + for (int entry = 0; entry < result.Size(); ++entry) { + result(entry) = action(offset + entry); + } - return result; - } + return result; +} } // namespace prepared_pressure_force_test_utils TEST_CASE( - "Prepared Pressure Force Uses FieldDof Supported Dimensions And Owns Its Context", - tags::barotrope &tags::pressure &tags::prepared &tags::field &tags::unit -) { - mean_field::utils::Args args = test_utils::setup_args(); + "Prepared Pressure Force Uses FieldDof Supported Dimensions And Owns Its " + "Context", + tags::barotrope &tags::pressure &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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const prepared_pressure_force_test_utils::Maps maps(f); + const prepared_pressure_force_test_utils::Maps maps(f); - const mean_field::eos::Polytrope equationOfState(3.0, 0.25); + const mean_field::eos::Polytrope equationOfState(3.0, 0.25); - mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); + mean_field::operators::PreparedPressureForceOperator preparedOperator( + f, *f.domainMapperStateless, equationOfState); - REQUIRE(maps.enthalpy.reduced_size() < maps.enthalpy.full_size()); + REQUIRE(maps.enthalpy.reduced_size() < maps.enthalpy.full_size()); - CHECK(maps.displacement.is_identity()); + CHECK(maps.displacement.is_identity()); - CHECK(preparedOperator.GetEnthalpySize() == maps.enthalpy.reduced_size()); + CHECK(preparedOperator.GetEnthalpySize() == maps.enthalpy.reduced_size()); - CHECK(preparedOperator.GetDisplacementSize() == maps.displacement.reduced_size()); + CHECK(preparedOperator.GetDisplacementSize() == + maps.displacement.reduced_size()); - CHECK( - &preparedOperator.GetContext().GetPreparationStatistics() == &preparedOperator.GetContextPreparationStatistics() - ); + CHECK(&preparedOperator.GetContext().GetPreparationStatistics() == + &preparedOperator.GetContextPreparationStatistics()); +} + +TEST_CASE("Prepared Pressure Force Jacobian Matches Full Stateless Columns " + "Through FieldDof Restriction", + tags::barotrope &tags::pressure &tags::prepared &tags::field + &tags::integration &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 prepared_pressure_force_test_utils::Maps maps(f); + + const mean_field::eos::Polytrope equationOfState(3.0, 0.25); + + const mfem::Vector enthalpy = maps.enthalpy.gather( + prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.47)); + + const mfem::Vector displacement = maps.displacement.gather( + gravity_prepared_test_utils::make_displacement(f, 0.69)); + + const mfem::Vector enthalpyDirection = maps.enthalpy.gather( + prepared_pressure_force_test_utils::make_enthalpy_direction_true(f, + 0.73)); + + const mfem::Vector displacementDirection = maps.displacement.gather( + prepared_pressure_force_test_utils::make_displacement_direction_true( + f, 0.83)); + + mean_field::operators::PreparedPressureForceOperator preparedOperator( + f, *f.domainMapperStateless, equationOfState); + + preparedOperator.Prepare( + {.enthalpy = enthalpy, .displacement = displacement}, + prepared_pressure_force_test_utils::make_dependencies()); + + const mfem::Vector enthalpyTrue = maps.enthalpy.scatter(enthalpy); + + const mfem::Vector displacementTrue = maps.displacement.scatter(displacement); + + const mfem::Vector enthalpyDirectionTrue = + maps.enthalpy.scatter(enthalpyDirection); + + const mfem::Vector displacementDirectionTrue = + maps.displacement.scatter(displacementDirection); + + mfem::Vector preparedEnthalpyAction; + mfem::Vector kernelEnthalpyActionTrue; + + preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, + preparedEnthalpyAction); + + mean_field::operators::kernels::apply_pressure_force_enthalpy_action( + f, *f.domainMapperStateless, equationOfState, enthalpyTrue, + enthalpyDirectionTrue, displacementTrue, kernelEnthalpyActionTrue); + + const mfem::Vector kernelEnthalpyAction = + maps.displacement.gather(kernelEnthalpyActionTrue); + + CHECK(prepared_pressure_force_test_utils::relative_difference( + preparedEnthalpyAction, kernelEnthalpyAction, f.mesh->GetComm()) < + 2.0e-12); + + mfem::Vector preparedDisplacementAction; + mfem::Vector kernelDisplacementActionTrue; + + preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, + preparedDisplacementAction); + + mean_field::operators::kernels::apply_pressure_force_displacement_action( + f, *f.domainMapperStateless, equationOfState, enthalpyTrue, + displacementDirectionTrue, displacementTrue, + kernelDisplacementActionTrue); + + const mfem::Vector kernelDisplacementAction = + maps.displacement.gather(kernelDisplacementActionTrue); + + CHECK(prepared_pressure_force_test_utils::relative_difference( + preparedDisplacementAction, kernelDisplacementAction, + f.mesh->GetComm()) < 2.0e-12); + + mfem::Vector fusedAction; + + preparedOperator.ApplyCompleteJacobianAction( + enthalpyDirection, displacementDirection, fusedAction); + + mfem::Vector expectedFusedAction(kernelEnthalpyAction); + + expectedFusedAction += kernelDisplacementAction; + + CHECK(prepared_pressure_force_test_utils::relative_difference( + fusedAction, expectedFusedAction, f.mesh->GetComm()) < 2.0e-12); } TEST_CASE( - "Prepared Pressure Force Jacobian Matches Full Stateless Columns Through FieldDof Restriction", - tags::barotrope &tags::pressure &tags::prepared &tags::field &tags::integration &tags::accuracy -) { + "Prepared Pressure Force MFEM Adapter Routes Reduced Coupled FieldDof " + "Blocks", + tags::barotrope &tags::pressure &tags::prepared &tags::field + &tags::integration &tags::jacobian &tags::mfem_operators &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 prepared_pressure_force_test_utils::Maps maps(f); + + const mean_field::eos::Polytrope equationOfState(3.0, 0.25); + + const mfem::Vector enthalpy = maps.enthalpy.gather( + prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.53)); + + const mfem::Vector displacement = maps.displacement.gather( + gravity_prepared_test_utils::make_displacement(f, 0.71)); + + const mfem::Vector enthalpyDirection = maps.enthalpy.gather( + prepared_pressure_force_test_utils::make_enthalpy_direction_true(f, + 0.89)); + + const mfem::Vector displacementDirection = maps.displacement.gather( + prepared_pressure_force_test_utils::make_displacement_direction_true( + f, 0.97)); + + mean_field::operators::PreparedPressureForceOperator preparedOperator( + f, *f.domainMapperStateless, equationOfState); + + preparedOperator.Prepare( + {.enthalpy = enthalpy, .displacement = displacement}, + prepared_pressure_force_test_utils::make_dependencies()); + + const mean_field::operators::BarotropicEquilibriumLayout layout = + prepared_pressure_force_test_utils::make_coupled_layout(maps); + + mean_field::operators::PreparedPressureForceJacobianOperator adapter( + layout, preparedOperator); + + CHECK(layout.size(prepared_pressure_force_test_utils::enthalpyValue) == + maps.enthalpy.reduced_size()); + + CHECK(layout.size(prepared_pressure_force_test_utils::densityValue) == + maps.density.reduced_size()); + + mfem::BlockVector direction(layout.value_offsets()); + + direction = 0.0; + + /* + * Populate unrelated columns deliberately. + */ + direction.GetBlock(prepared_pressure_force_test_utils::densityValue) = 0.37; + + direction.GetBlock(prepared_pressure_force_test_utils::gravityGradientValue) = + -0.41; + + direction.GetBlock( + prepared_pressure_force_test_utils::gravityPotentialValue) = 0.59; + + direction.GetBlock( + prepared_pressure_force_test_utils::barotropicConstantValue) = -0.73; + + direction.GetBlock(prepared_pressure_force_test_utils::displacementValue) = + displacementDirection; + + direction.GetBlock(prepared_pressure_force_test_utils::enthalpyValue) = + enthalpyDirection; + + mfem::Vector expectedDisplacementAction; + + preparedOperator.ApplyCompleteJacobianAction( + enthalpyDirection, displacementDirection, expectedDisplacementAction); + + mfem::Vector action; + + adapter.Mult(direction, action); + + const mfem::Vector displacementResidualAction = + prepared_pressure_force_test_utils::copy_residual_block( + action, layout, + prepared_pressure_force_test_utils::displacementResidual); + + CHECK(prepared_pressure_force_test_utils::relative_difference( + displacementResidualAction, expectedDisplacementAction, + f.mesh->GetComm()) < 2.0e-14); + + CHECK(prepared_pressure_force_test_utils::copy_residual_block( + action, layout, + prepared_pressure_force_test_utils::gravityGradientResidual) + .Norml2() == 0.0); + + CHECK(prepared_pressure_force_test_utils::copy_residual_block( + action, layout, + prepared_pressure_force_test_utils::gravityPotentialResidual) + .Norml2() == 0.0); + + CHECK(prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::densityResidual) + .Norml2() == 0.0); + + CHECK( + prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::enthalpyResidual) + .Norml2() == 0.0); + + CHECK(prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::massResidual) + .Norml2() == 0.0); +} +TEST_CASE("Pressure Force Residual Converges To A Manufactured Analytic Force", + tags::barotrope &tags::pressure &tags::kernels &tags::integration + &tags::convergence &tags::h_refinement &tags::analytic_comparison + &tags::accuracy) { + constexpr int dimension = 3; + + constexpr std::array refinementLevels{0, 1}; + + constexpr double minimumObservedRate = 3.0; + constexpr double finestRelativeTolerance = 2.0e-3; + + constexpr double amplitude = 1.0; + constexpr double bumpSharpness = 0.25; + constexpr double supportRadiusFraction = 0.90; + + std::array relativeErrors{}; + + for (std::size_t levelIndex = 0; levelIndex < refinementLevels.size(); + ++levelIndex) { mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem( + args.mesh_file, args, refinementLevels[levelIndex]); REQUIRE(f.okay()); + REQUIRE(f.mesh->Dimension() == dimension); + REQUIRE(f.mesh->GetNE() > 0); - const prepared_pressure_force_test_utils::Maps maps(f); + const MPI_Comm communicator = f.mesh->GetComm(); - const mean_field::eos::Polytrope equationOfState(3.0, 0.25); + constexpr double supportRadius = + supportRadiusFraction * mean_field::utils::RADIUS; - const mfem::Vector enthalpy = - maps.enthalpy.gather(prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.47)); + constexpr double supportRadiusSquared = supportRadius * supportRadius; - const mfem::Vector displacement = maps.displacement.gather(gravity_prepared_test_utils::make_displacement(f, 0.69)); + auto analyticEnthalpyFunction = [supportRadiusSquared]( + const mfem::Vector &position) { + const double normalizedRadiusSquared = + (position * position) / supportRadiusSquared; - const mfem::Vector enthalpyDirection = - maps.enthalpy.gather(prepared_pressure_force_test_utils::make_enthalpy_direction_true(f, 0.73)); + if (normalizedRadiusSquared >= 1.0) { + return 0.0; + } - const mfem::Vector displacementDirection = - maps.displacement.gather(prepared_pressure_force_test_utils::make_displacement_direction_true(f, 0.83)); + const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; - mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); + return amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / + distanceToSupportBoundary); + }; - preparedOperator.Prepare( - {.enthalpy = enthalpy, .displacement = displacement}, prepared_pressure_force_test_utils::make_dependencies() - ); + auto analyticPressureForceFunction = [supportRadiusSquared]( + const mfem::Vector &position, + mfem::Vector &force) { + force.SetSize(dimension); + force = 0.0; - const mfem::Vector enthalpyTrue = maps.enthalpy.scatter(enthalpy); + const double normalizedRadiusSquared = + (position * position) / supportRadiusSquared; - const mfem::Vector displacementTrue = maps.displacement.scatter(displacement); + if (normalizedRadiusSquared >= 1.0) { + return; + } - const mfem::Vector enthalpyDirectionTrue = maps.enthalpy.scatter(enthalpyDirection); + const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; - const mfem::Vector displacementDirectionTrue = maps.displacement.scatter(displacementDirection); + const double enthalpy = + amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / + distanceToSupportBoundary); - mfem::Vector preparedEnthalpyAction; - mfem::Vector kernelEnthalpyActionTrue; + const double pressureGradientScale = + -2.0 * bumpSharpness * std::pow(enthalpy, 4.0) / + (supportRadiusSquared * distanceToSupportBoundary * + distanceToSupportBoundary); - preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, preparedEnthalpyAction); + for (int component = 0; component < dimension; ++component) { + force(component) = pressureGradientScale * position(component); + } + }; - mean_field::operators::kernels::apply_pressure_force_enthalpy_action( - f, *f.domainMapperStateless, equationOfState, enthalpyTrue, enthalpyDirectionTrue, displacementTrue, - kernelEnthalpyActionTrue - ); + mfem::FunctionCoefficient analyticEnthalpyCoefficient( + analyticEnthalpyFunction); - const mfem::Vector kernelEnthalpyAction = maps.displacement.gather(kernelEnthalpyActionTrue); + mfem::VectorFunctionCoefficient analyticPressureForceCoefficient( + dimension, analyticPressureForceFunction); - CHECK( - prepared_pressure_force_test_utils::relative_difference( - preparedEnthalpyAction, kernelEnthalpyAction, f.mesh->GetComm() - ) < 2.0e-12 - ); + mfem::ParGridFunction discreteEnthalpyField(f.enthalpyFes.get()); - mfem::Vector preparedDisplacementAction; - mfem::Vector kernelDisplacementActionTrue; + discreteEnthalpyField.ProjectCoefficient(analyticEnthalpyCoefficient); - preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, preparedDisplacementAction); + mfem::Vector discreteEnthalpyTrue; + discreteEnthalpyField.GetTrueDofs(discreteEnthalpyTrue); - mean_field::operators::kernels::apply_pressure_force_displacement_action( - f, *f.domainMapperStateless, equationOfState, enthalpyTrue, displacementDirectionTrue, displacementTrue, - kernelDisplacementActionTrue - ); + mfem::Vector zeroDisplacement(f.displacementFes->GetTrueVSize()); - const mfem::Vector kernelDisplacementAction = maps.displacement.gather(kernelDisplacementActionTrue); + zeroDisplacement = 0.0; - CHECK( - prepared_pressure_force_test_utils::relative_difference( - preparedDisplacementAction, kernelDisplacementAction, f.mesh->GetComm() - ) < 2.0e-12 - ); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); - mfem::Vector fusedAction; + mfem::Vector discreteResidual; - preparedOperator.ApplyCompleteJacobianAction(enthalpyDirection, displacementDirection, fusedAction); + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, discreteEnthalpyTrue, + zeroDisplacement, discreteResidual); - mfem::Vector expectedFusedAction(kernelEnthalpyAction); + REQUIRE(discreteResidual.Size() == f.displacementFes->GetTrueVSize()); - expectedFusedAction += kernelDisplacementAction; + mfem::Array stellarMarker(f.mesh->attributes.Max()); - CHECK( - prepared_pressure_force_test_utils::relative_difference(fusedAction, expectedFusedAction, f.mesh->GetComm()) < - 2.0e-12 - ); -} + stellarMarker = 0; -TEST_CASE( - "Prepared Pressure Force MFEM Adapter Routes Reduced Coupled FieldDof Blocks", - tags::barotrope &tags::pressure &tags::prepared &tags::field &tags::integration &tags::jacobian - &tags::mfem_operators &tags::unit -) { - mean_field::utils::Args args = test_utils::setup_args(); + const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size(); + ++attributeIndex) { + const int attribute = f.mesh->attributes[attributeIndex]; - REQUIRE(f.okay()); - - const prepared_pressure_force_test_utils::Maps maps(f); - - const mean_field::eos::Polytrope equationOfState(3.0, 0.25); - - const mfem::Vector enthalpy = - maps.enthalpy.gather(prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.53)); - - const mfem::Vector displacement = maps.displacement.gather(gravity_prepared_test_utils::make_displacement(f, 0.71)); - - const mfem::Vector enthalpyDirection = - maps.enthalpy.gather(prepared_pressure_force_test_utils::make_enthalpy_direction_true(f, 0.89)); - - const mfem::Vector displacementDirection = - maps.displacement.gather(prepared_pressure_force_test_utils::make_displacement_direction_true(f, 0.97)); - - mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); - - preparedOperator.Prepare( - {.enthalpy = enthalpy, .displacement = displacement}, prepared_pressure_force_test_utils::make_dependencies() - ); - - const mean_field::operators::BarotropicEquilibriumLayout layout = - prepared_pressure_force_test_utils::make_coupled_layout(maps); - - mean_field::operators::PreparedPressureForceJacobianOperator adapter(layout, preparedOperator); - - CHECK(layout.size(prepared_pressure_force_test_utils::enthalpyValue) == maps.enthalpy.reduced_size()); - - CHECK(layout.size(prepared_pressure_force_test_utils::densityValue) == maps.density.reduced_size()); - - mfem::BlockVector direction(layout.value_offsets()); - - direction = 0.0; - - /* - * Populate unrelated columns deliberately. - */ - direction.GetBlock(prepared_pressure_force_test_utils::densityValue) = 0.37; - - direction.GetBlock(prepared_pressure_force_test_utils::gravityGradientValue) = -0.41; - - direction.GetBlock(prepared_pressure_force_test_utils::gravityPotentialValue) = 0.59; - - direction.GetBlock(prepared_pressure_force_test_utils::barotropicConstantValue) = -0.73; - - direction.GetBlock(prepared_pressure_force_test_utils::displacementValue) = displacementDirection; - - direction.GetBlock(prepared_pressure_force_test_utils::enthalpyValue) = enthalpyDirection; - - mfem::Vector expectedDisplacementAction; - - preparedOperator.ApplyCompleteJacobianAction(enthalpyDirection, displacementDirection, expectedDisplacementAction); - - mfem::Vector action; - - adapter.Mult(direction, action); - - const mfem::Vector displacementResidualAction = prepared_pressure_force_test_utils::copy_residual_block( - action, layout, prepared_pressure_force_test_utils::displacementResidual - ); - - CHECK( - prepared_pressure_force_test_utils::relative_difference( - displacementResidualAction, expectedDisplacementAction, f.mesh->GetComm() - ) < 2.0e-14 - ); - - CHECK( - prepared_pressure_force_test_utils::copy_residual_block( - action, layout, prepared_pressure_force_test_utils::gravityGradientResidual - ) - .Norml2() == 0.0 - ); - - CHECK( - prepared_pressure_force_test_utils::copy_residual_block( - action, layout, prepared_pressure_force_test_utils::gravityPotentialResidual - ) - .Norml2() == 0.0 - ); - - CHECK( - prepared_pressure_force_test_utils::copy_residual_block( - action, layout, prepared_pressure_force_test_utils::densityResidual - ) - .Norml2() == 0.0 - ); - - CHECK( - prepared_pressure_force_test_utils::copy_residual_block( - action, layout, prepared_pressure_force_test_utils::enthalpyResidual - ) - .Norml2() == 0.0 - ); - - CHECK( - prepared_pressure_force_test_utils::copy_residual_block( - action, layout, prepared_pressure_force_test_utils::massResidual - ) - .Norml2() == 0.0 - ); -} -TEST_CASE( - "Pressure Force Residual Converges To A Manufactured Analytic Force", - tags::barotrope &tags::pressure &tags::kernels &tags::integration &tags::convergence &tags::h_refinement - &tags::analytic_comparison &tags::accuracy -) { - constexpr int dimension = 3; - - constexpr std::array refinementLevels{0, 1}; - - constexpr double minimumObservedRate = 3.0; - constexpr double finestRelativeTolerance = 2.0e-3; - - constexpr double amplitude = 1.0; - constexpr double bumpSharpness = 0.25; - constexpr double supportRadiusFraction = 0.90; - - std::array relativeErrors{}; - - for (std::size_t levelIndex = 0; levelIndex < refinementLevels.size(); ++levelIndex) { - mean_field::utils::Args args = test_utils::setup_args(); - - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, refinementLevels[levelIndex]); - - REQUIRE(f.okay()); - REQUIRE(f.mesh->Dimension() == dimension); - REQUIRE(f.mesh->GetNE() > 0); - - const MPI_Comm communicator = f.mesh->GetComm(); - - constexpr double supportRadius = supportRadiusFraction * mean_field::utils::RADIUS; - - constexpr double supportRadiusSquared = supportRadius * supportRadius; - - auto analyticEnthalpyFunction = [supportRadiusSquared](const mfem::Vector &position) { - const double normalizedRadiusSquared = (position * position) / supportRadiusSquared; - - if (normalizedRadiusSquared >= 1.0) { - return 0.0; - } - - const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; - - return amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / distanceToSupportBoundary); - }; - - auto analyticPressureForceFunction = [supportRadiusSquared](const mfem::Vector &position, mfem::Vector &force) { - force.SetSize(dimension); - force = 0.0; - - const double normalizedRadiusSquared = (position * position) / supportRadiusSquared; - - if (normalizedRadiusSquared >= 1.0) { - return; - } - - const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; - - const double enthalpy = - amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / distanceToSupportBoundary); - - const double pressureGradientScale = - -2.0 * bumpSharpness * std::pow(enthalpy, 4.0) / - (supportRadiusSquared * distanceToSupportBoundary * distanceToSupportBoundary); - - for (int component = 0; component < dimension; ++component) { - force(component) = pressureGradientScale * position(component); - } - }; - - mfem::FunctionCoefficient analyticEnthalpyCoefficient(analyticEnthalpyFunction); - - mfem::VectorFunctionCoefficient analyticPressureForceCoefficient(dimension, analyticPressureForceFunction); - - mfem::ParGridFunction discreteEnthalpyField(f.enthalpyFes.get()); - - discreteEnthalpyField.ProjectCoefficient(analyticEnthalpyCoefficient); - - mfem::Vector discreteEnthalpyTrue; - discreteEnthalpyField.GetTrueDofs(discreteEnthalpyTrue); - - mfem::Vector zeroDisplacement(f.displacementFes->GetTrueVSize()); - - zeroDisplacement = 0.0; - - const mean_field::eos::Polytrope barotrope(3.0, 0.25); - - mfem::Vector discreteResidual; - - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, discreteEnthalpyTrue, zeroDisplacement, discreteResidual - ); - - REQUIRE(discreteResidual.Size() == f.displacementFes->GetTrueVSize()); - - mfem::Array stellarMarker(f.mesh->attributes.Max()); - - stellarMarker = 0; - - const int vacuumAttribute = f.domainMapperStateless->GetVacuumElementAttribute(); - - for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size(); ++attributeIndex) { - const int attribute = f.mesh->attributes[attributeIndex]; - - if (attribute != vacuumAttribute) { - stellarMarker[attribute - 1] = 1; - } - } - - const mfem::Geometry::Type elementGeometry = f.displacementFes->GetFE(0)->GetGeomType(); - - for (int element = 1; element < f.mesh->GetNE(); ++element) { - REQUIRE(f.displacementFes->GetFE(element)->GetGeomType() == elementGeometry); - } - - const int referenceQuadratureOrder = 2 * f.displacementFes->GetMaxElementOrder() + 16; - - const mfem::IntegrationRule &referenceQuadrature = - mfem::IntRules.Get(elementGeometry, referenceQuadratureOrder); - - auto *analyticForceIntegrator = new mfem::VectorDomainLFIntegrator(analyticPressureForceCoefficient); - - analyticForceIntegrator->SetIntRule(&referenceQuadrature); - - mfem::ParLinearForm analyticForceLoad(f.displacementFes.get()); - - analyticForceLoad.AddDomainIntegrator(analyticForceIntegrator, stellarMarker); - - analyticForceLoad.Assemble(); - - std::unique_ptr analyticForceHypreVector(analyticForceLoad.ParallelAssemble()); - - REQUIRE(analyticForceHypreVector != nullptr); - - mfem::Vector analyticForceTrue(*analyticForceHypreVector); - - REQUIRE(analyticForceTrue.Size() == discreteResidual.Size()); - - const double analyticForceNorm = gravity_prepared_test_utils::global_norm(analyticForceTrue, communicator); - - REQUIRE(std::isfinite(analyticForceNorm)); - REQUIRE(analyticForceNorm > 0.0); - - mfem::Vector residualError(discreteResidual); - residualError -= analyticForceTrue; - - mfem::ParBilinearForm rieszForm(f.displacementFes.get()); - - rieszForm.AddDomainIntegrator(new mfem::VectorMassIntegrator()); - - rieszForm.AddDomainIntegrator(new mfem::VectorDiffusionIntegrator()); - - rieszForm.Assemble(); - rieszForm.Finalize(); - - std::unique_ptr rieszMatrix(rieszForm.ParallelAssemble()); - - REQUIRE(rieszMatrix != nullptr); - REQUIRE(rieszMatrix->Height() == discreteResidual.Size()); - REQUIRE(rieszMatrix->Width() == discreteResidual.Size()); - - mfem::HypreBoomerAMG rieszPreconditioner(*rieszMatrix); - - rieszPreconditioner.SetPrintLevel(0); - - mfem::CGSolver rieszSolver(communicator); - - rieszSolver.SetOperator(*rieszMatrix); - rieszSolver.SetPreconditioner(rieszPreconditioner); - rieszSolver.SetRelTol(1.0e-13); - rieszSolver.SetAbsTol(1.0e-15); - rieszSolver.SetMaxIter(5000); - rieszSolver.SetPrintLevel(1); - - auto calculateDualNorm = [&rieszSolver, communicator](const mfem::Vector &functional) { - mfem::Vector rieszRepresentative(functional.Size()); - - rieszRepresentative = 0.0; - - rieszSolver.Mult(functional, rieszRepresentative); - - MFEM_VERIFY( - rieszSolver.GetConverged(), "The pressure-force convergence-test Riesz solve " - "did not converge." - ); - - const double dualNormSquared = - gravity_prepared_test_utils::global_dot(functional, rieszRepresentative, communicator); - - MFEM_VERIFY(std::isfinite(dualNormSquared), "The pressure-force dual norm is not finite."); - - MFEM_VERIFY( - dualNormSquared >= -100.0 * std::numeric_limits::epsilon(), - "The pressure-force Riesz operator produced a " - "negative dual norm." - ); - - return std::sqrt(std::max(dualNormSquared, 0.0)); - }; - - const double errorDualNorm = calculateDualNorm(residualError); - - const double analyticDualNorm = calculateDualNorm(analyticForceTrue); - - REQUIRE(std::isfinite(errorDualNorm)); - REQUIRE(std::isfinite(analyticDualNorm)); - REQUIRE(errorDualNorm > 0.0); - REQUIRE(analyticDualNorm > 0.0); - - relativeErrors[levelIndex] = errorDualNorm / analyticDualNorm; - - INFO("Pressure-force refinement level = " << refinementLevels[levelIndex]); - - INFO("Pressure-force true DOFs = " << f.displacementFes->GlobalTrueVSize()); - - INFO("Pressure-force relative dual error = " << relativeErrors[levelIndex]); + if (attribute != vacuumAttribute) { + stellarMarker[attribute - 1] = 1; + } } - for (const double relativeError : relativeErrors) { - REQUIRE(std::isfinite(relativeError)); - REQUIRE(relativeError > 0.0); + const mfem::Geometry::Type elementGeometry = + f.displacementFes->GetFE(0)->GetGeomType(); + + for (int element = 1; element < f.mesh->GetNE(); ++element) { + REQUIRE(f.displacementFes->GetFE(element)->GetGeomType() == + elementGeometry); } - static_assert(refinementLevels.size() == 2, "This reduced convergence test expects exactly two refinement levels."); + const int referenceQuadratureOrder = + 2 * f.displacementFes->GetMaxElementOrder() + 16; - const double observedRate = std::log(relativeErrors[0] / relativeErrors[1]) / std::log(2.0); + const mfem::IntegrationRule &referenceQuadrature = + mfem::IntRules.Get(elementGeometry, referenceQuadratureOrder); - INFO("Level 0 pressure-force relative dual error = " << relativeErrors[0]); + auto *analyticForceIntegrator = + new mfem::VectorDomainLFIntegrator(analyticPressureForceCoefficient); - INFO("Level 1 pressure-force relative dual error = " << relativeErrors[1]); + analyticForceIntegrator->SetIntRule(&referenceQuadrature); - INFO("Level 0 to 1 pressure-force convergence rate = " << observedRate); + mfem::ParLinearForm analyticForceLoad(f.displacementFes.get()); - CHECK(relativeErrors[1] < relativeErrors[0]); + analyticForceLoad.AddDomainIntegrator(analyticForceIntegrator, + stellarMarker); - CHECK(observedRate > minimumObservedRate); + analyticForceLoad.Assemble(); - CHECK(relativeErrors[1] < finestRelativeTolerance); + std::unique_ptr analyticForceHypreVector( + analyticForceLoad.ParallelAssemble()); + + REQUIRE(analyticForceHypreVector != nullptr); + + mfem::Vector analyticForceTrue(*analyticForceHypreVector); + + REQUIRE(analyticForceTrue.Size() == discreteResidual.Size()); + + const double analyticForceNorm = gravity_prepared_test_utils::global_norm( + analyticForceTrue, communicator); + + REQUIRE(std::isfinite(analyticForceNorm)); + REQUIRE(analyticForceNorm > 0.0); + + mfem::Vector residualError(discreteResidual); + residualError -= analyticForceTrue; + + mfem::ParBilinearForm rieszForm(f.displacementFes.get()); + + rieszForm.AddDomainIntegrator(new mfem::VectorMassIntegrator()); + + rieszForm.AddDomainIntegrator(new mfem::VectorDiffusionIntegrator()); + + rieszForm.Assemble(); + rieszForm.Finalize(); + + std::unique_ptr rieszMatrix( + rieszForm.ParallelAssemble()); + + REQUIRE(rieszMatrix != nullptr); + REQUIRE(rieszMatrix->Height() == discreteResidual.Size()); + REQUIRE(rieszMatrix->Width() == discreteResidual.Size()); + + mfem::HypreBoomerAMG rieszPreconditioner(*rieszMatrix); + + rieszPreconditioner.SetPrintLevel(0); + + mfem::CGSolver rieszSolver(communicator); + + rieszSolver.SetOperator(*rieszMatrix); + rieszSolver.SetPreconditioner(rieszPreconditioner); + rieszSolver.SetRelTol(1.0e-13); + rieszSolver.SetAbsTol(1.0e-15); + rieszSolver.SetMaxIter(5000); + rieszSolver.SetPrintLevel(0); + + auto calculateDualNorm = [&rieszSolver, + communicator](const mfem::Vector &functional) { + mfem::Vector rieszRepresentative(functional.Size()); + + rieszRepresentative = 0.0; + + rieszSolver.Mult(functional, rieszRepresentative); + + MFEM_VERIFY(rieszSolver.GetConverged(), + "The pressure-force convergence-test Riesz solve " + "did not converge."); + + const double dualNormSquared = gravity_prepared_test_utils::global_dot( + functional, rieszRepresentative, communicator); + + MFEM_VERIFY(std::isfinite(dualNormSquared), + "The pressure-force dual norm is not finite."); + + MFEM_VERIFY(dualNormSquared >= + -100.0 * std::numeric_limits::epsilon(), + "The pressure-force Riesz operator produced a " + "negative dual norm."); + + return std::sqrt(std::max(dualNormSquared, 0.0)); + }; + + const double errorDualNorm = calculateDualNorm(residualError); + + const double analyticDualNorm = calculateDualNorm(analyticForceTrue); + + REQUIRE(std::isfinite(errorDualNorm)); + REQUIRE(std::isfinite(analyticDualNorm)); + REQUIRE(errorDualNorm > 0.0); + REQUIRE(analyticDualNorm > 0.0); + + relativeErrors[levelIndex] = errorDualNorm / analyticDualNorm; + + INFO("Pressure-force refinement level = " << refinementLevels[levelIndex]); + + INFO("Pressure-force true DOFs = " << f.displacementFes->GlobalTrueVSize()); + + INFO("Pressure-force relative dual error = " << relativeErrors[levelIndex]); + } + + for (const double relativeError : relativeErrors) { + REQUIRE(std::isfinite(relativeError)); + REQUIRE(relativeError > 0.0); + } + + static_assert( + refinementLevels.size() == 2, + "This reduced convergence test expects exactly two refinement levels."); + + const double observedRate = + std::log(relativeErrors[0] / relativeErrors[1]) / std::log(2.0); + + INFO("Level 0 pressure-force relative dual error = " << relativeErrors[0]); + + INFO("Level 1 pressure-force relative dual error = " << relativeErrors[1]); + + INFO("Level 0 to 1 pressure-force convergence rate = " << observedRate); + + CHECK(relativeErrors[1] < relativeErrors[0]); + + CHECK(observedRate > minimumObservedRate); + + CHECK(relativeErrors[1] < finestRelativeTolerance); } diff --git a/tests/operators/prepared_rotation_displacement_force.cpp b/tests/operators/prepared_rotation_displacement_force.cpp index 5c15a34..64191fd 100644 --- a/tests/operators/prepared_rotation_displacement_force.cpp +++ b/tests/operators/prepared_rotation_displacement_force.cpp @@ -11,635 +11,682 @@ import mean_field; import test_helpers; namespace rotational_displacement_force_test_utils { - using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; +using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; - constexpr auto densityValue = - mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::density_field.mass_term); +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 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 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 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 enthalpyValue = + mean_field::utils::blocks::get_value_block( + mean_field::utils::blocks::enthalpy_field.specific_term); - constexpr auto barotropicConstantValue = mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term - ); +constexpr auto barotropicConstantValue = + 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 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 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 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 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 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 - ); +constexpr auto massResidual = + mean_field::utils::blocks::get_residual_block( + mean_field::utils::blocks::barotropic_constant_field + .mass_normalization_term); - [[nodiscard]] mean_field::operators::RotationalDisplacementForceLayout make_layout(const mean_field::fem::FEM &f) { - using DomainSchema = gravity_prepared_test_utils::DomainSchema; +[[nodiscard]] mean_field::operators::RotationalDisplacementForceLayout +make_layout(const mean_field::fem::FEM &f) { + using DomainSchema = gravity_prepared_test_utils::DomainSchema; - const auto densityMap = gravity_prepared_test_utils::make_field_map(f); - const auto displacementMap = gravity_prepared_test_utils::make_field_map(f); - const auto gravityFluxMap = - mean_field::field::make_field_dof_map(*f.gravityFluxFes); - const auto gravityPotentialMap = - mean_field::field::make_field_dof_map(*f.gravityPotentialFes); - const auto enthalpyMap = - mean_field::field::make_field_dof_map(*f.enthalpyFes); + const auto densityMap = + gravity_prepared_test_utils::make_field_map( + f); + const auto displacementMap = gravity_prepared_test_utils::make_field_map< + mean_field::field::Displacement>(f); + const auto gravityFluxMap = + mean_field::field::make_field_dof_map(*f.gravityFluxFes); + const auto gravityPotentialMap = mean_field::field::make_field_dof_map< + mean_field::field::Gravity, DomainSchema>(*f.gravityPotentialFes); + const auto enthalpyMap = + mean_field::field::make_field_dof_map(*f.enthalpyFes); - const std::array valueSizes{ - densityMap.reduced_size(), displacementMap.reduced_size(), gravityFluxMap.reduced_size(), - gravityPotentialMap.reduced_size(), enthalpyMap.reduced_size(), 1 - }; + const std::array valueSizes{ + densityMap.reduced_size(), displacementMap.reduced_size(), + gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), + enthalpyMap.reduced_size(), 1}; - const std::array residualSizes{ - gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(), - displacementMap.reduced_size(), enthalpyMap.reduced_size(), 1 - }; + const std::array residualSizes{ + gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), + densityMap.reduced_size(), displacementMap.reduced_size(), + enthalpyMap.reduced_size(), 1}; - return {valueSizes, residualSizes}; - } + return {valueSizes, residualSizes}; +} - [[nodiscard]] mfem::Vector make_density( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::ParGridFunction densityField(f.densityFes.get()); +[[nodiscard]] mfem::Vector make_density(const mean_field::fem::FEM &f, + const double phase) { + mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::FunctionCoefficient densityCoefficient([phase](const mfem::Vector &position) { - return 0.88 + 0.06 * std::sin(0.7 * position(0) + phase) + 0.04 * std::cos(0.6 * position(1) - phase) + - 0.025 * position(2) * position(2); - }); + mfem::FunctionCoefficient densityCoefficient( + [phase](const mfem::Vector &position) { + return 0.88 + 0.06 * std::sin(0.7 * position(0) + phase) + + 0.04 * std::cos(0.6 * position(1) - phase) + + 0.025 * position(2) * position(2); + }); - densityField.ProjectCoefficient(densityCoefficient); + densityField.ProjectCoefficient(densityCoefficient); - mfem::Vector densityTrue; - densityField.GetTrueDofs(densityTrue); - return densityTrue; - } + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; +} - [[nodiscard]] mfem::Vector make_density_direction( - const mean_field::fem::FEM &f, - const double phase - ) { - mfem::ParGridFunction densityField(f.densityFes.get()); +[[nodiscard]] mfem::Vector make_density_direction(const mean_field::fem::FEM &f, + const double phase) { + mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::FunctionCoefficient densityCoefficient([phase](const mfem::Vector &position) { - return 0.17 * std::sin(0.9 * position(0) + phase) - 0.12 * std::cos(0.8 * position(1) - phase) + - 0.07 * position(2); - }); + mfem::FunctionCoefficient densityCoefficient( + [phase](const mfem::Vector &position) { + return 0.17 * std::sin(0.9 * position(0) + phase) - + 0.12 * std::cos(0.8 * position(1) - phase) + 0.07 * position(2); + }); - densityField.ProjectCoefficient(densityCoefficient); + densityField.ProjectCoefficient(densityCoefficient); - mfem::Vector densityTrue; - densityField.GetTrueDofs(densityTrue); - return densityTrue; - } + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; +} - [[nodiscard]] mfem::Vector make_displacement_direction(const mean_field::fem::FEM &f) { - mfem::Vector direction = gravity_prepared_test_utils::make_displacement(f, 0.91); +[[nodiscard]] mfem::Vector +make_displacement_direction(const mean_field::fem::FEM &f) { + mfem::Vector direction = + gravity_prepared_test_utils::make_displacement(f, 0.91); - const mfem::Vector second = gravity_prepared_test_utils::make_displacement(f, 0.27); + const mfem::Vector second = + gravity_prepared_test_utils::make_displacement(f, 0.27); - direction -= second; - return direction; - } + direction -= second; + return direction; +} - [[nodiscard]] mean_field::physics::RigidRotation make_rotation(const double scale = 1.0) { - mfem::Vector angularVelocity(3); - angularVelocity(0) = scale * 0.17; - angularVelocity(1) = scale * -0.09; - angularVelocity(2) = scale * 0.62; +[[nodiscard]] mean_field::physics::RigidRotation +make_rotation(const double scale = 1.0) { + mfem::Vector angularVelocity(3); + angularVelocity(0) = scale * 0.17; + angularVelocity(1) = scale * -0.09; + angularVelocity(2) = scale * 0.62; - mfem::Vector center(3); - center(0) = 0.04; - center(1) = -0.03; - center(2) = 0.02; + mfem::Vector center(3); + center(0) = 0.04; + center(1) = -0.03; + center(2) = 0.02; - return mean_field::physics::RigidRotation(angularVelocity, center); - } + return mean_field::physics::RigidRotation(angularVelocity, center); +} - [[nodiscard]] mean_field::operators::context::rotational_displacement_force::RotationalDisplacementForceDependencies +[[nodiscard]] mean_field::operators::context::rotational_displacement_force:: + RotationalDisplacementForceDependencies make_dependencies() { - return { - .discretization = {.identity = 211, .revision = 3}, - .density = {.identity = 223, .revision = 5}, - .displacement = {.identity = 227, .revision = 7}, - .rotation = {.identity = 229, .revision = 11} - }; + return {.discretization = {.identity = 211, .revision = 3}, + .density = {.identity = 223, .revision = 5}, + .displacement = {.identity = 227, .revision = 7}, + .rotation = {.identity = 229, .revision = 11}}; +} + +[[nodiscard]] mfem::Vector +make_vacuum_only_density(const mean_field::fem::FEM &f) { + mfem::ParGridFunction densityField(f.densityFes.get()); + densityField = 0.0; + + const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; + + mfem::Array densityDofs; + int localVacuumElements = 0; + + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elementId); + + REQUIRE(transformation != nullptr); + + if (transformation->Attribute != vacuumAttribute) { + continue; } - [[nodiscard]] mfem::Vector make_vacuum_only_density(const mean_field::fem::FEM &f) { - mfem::ParGridFunction densityField(f.densityFes.get()); - densityField = 0.0; + f.densityFes->GetElementDofs(elementId, densityDofs); - const int vacuumAttribute = f.domainMapperStateless->GetVacuumElementAttribute(); + mfem::Vector elementDensity(densityDofs.Size()); + elementDensity = 1.0; + densityField.SetSubVector(densityDofs, elementDensity); + ++localVacuumElements; + } - mfem::Array densityDofs; - int localVacuumElements = 0; + int globalVacuumElements = 0; - for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); + MPI_Allreduce(&localVacuumElements, &globalVacuumElements, 1, MPI_INT, + MPI_SUM, f.mesh->GetComm()); - REQUIRE(transformation != nullptr); + REQUIRE(globalVacuumElements > 0); - if (transformation->Attribute != vacuumAttribute) { - continue; - } + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; +} - f.densityFes->GetElementDofs(elementId, densityDofs); +[[nodiscard]] double global_norm(const mfem::Vector &vector, + MPI_Comm communicator) { + const double localSquaredNorm = vector * vector; + double globalSquaredNorm = 0.0; - mfem::Vector elementDensity(densityDofs.Size()); - elementDensity = 1.0; - densityField.SetSubVector(densityDofs, elementDensity); - ++localVacuumElements; - } + MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, + communicator); - int globalVacuumElements = 0; + return std::sqrt(globalSquaredNorm); +} - MPI_Allreduce(&localVacuumElements, &globalVacuumElements, 1, MPI_INT, MPI_SUM, f.mesh->GetComm()); +[[nodiscard]] double global_dot(const mfem::Vector &left, + const mfem::Vector &right, + MPI_Comm communicator) { + REQUIRE(left.Size() == right.Size()); - REQUIRE(globalVacuumElements > 0); + const double localDot = left * right; + double globalDot = 0.0; - mfem::Vector densityTrue; - densityField.GetTrueDofs(densityTrue); - return densityTrue; - } + MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator); - [[nodiscard]] double global_norm( - const mfem::Vector &vector, - MPI_Comm communicator - ) { - const double localSquaredNorm = vector * vector; - double globalSquaredNorm = 0.0; + return globalDot; +} - MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, communicator); +[[nodiscard]] double relative_difference(const mfem::Vector &computed, + const mfem::Vector &reference, + MPI_Comm communicator) { + REQUIRE(computed.Size() == reference.Size()); - return std::sqrt(globalSquaredNorm); - } + mfem::Vector difference(computed); + difference -= reference; - [[nodiscard]] double global_dot( - const mfem::Vector &left, - const mfem::Vector &right, - MPI_Comm communicator - ) { - REQUIRE(left.Size() == right.Size()); + return global_norm(difference, communicator) / + std::max(global_norm(reference, communicator), + std::numeric_limits::epsilon()); +} - const double localDot = left * right; - double globalDot = 0.0; +[[nodiscard]] mfem::Vector centered_difference( + const mean_field::fem::FEM &f, + const mean_field::physics::RigidRotation &rotation, + const mfem::Vector &baseDensity, const mfem::Vector &densityDirection, + const mfem::Vector &baseDisplacement, + const mfem::Vector &displacementDirection, const double step) { + mfem::Vector plusDensity(baseDensity); + plusDensity.Add(step, densityDirection); - MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator); + mfem::Vector minusDensity(baseDensity); + minusDensity.Add(-step, densityDirection); - return globalDot; - } + mfem::Vector plusDisplacement(baseDisplacement); + plusDisplacement.Add(step, displacementDirection); - [[nodiscard]] double relative_difference( - const mfem::Vector &computed, - const mfem::Vector &reference, - MPI_Comm communicator - ) { - REQUIRE(computed.Size() == reference.Size()); + mfem::Vector minusDisplacement(baseDisplacement); + minusDisplacement.Add(-step, displacementDirection); - mfem::Vector difference(computed); - difference -= reference; + mfem::Vector plusResidual; + mfem::Vector minusResidual; - return global_norm(difference, communicator) / - std::max(global_norm(reference, communicator), std::numeric_limits::epsilon()); - } + mean_field::operators::kernels::apply_rotational_displacement_force_residual( + f, *f.domainMapperStateless, rotation, plusDensity, plusDisplacement, + plusResidual); - [[nodiscard]] mfem::Vector centered_difference( - const mean_field::fem::FEM &f, - const mean_field::physics::RigidRotation &rotation, - const mfem::Vector &baseDensity, - const mfem::Vector &densityDirection, - const mfem::Vector &baseDisplacement, - const mfem::Vector &displacementDirection, - const double step - ) { - mfem::Vector plusDensity(baseDensity); - plusDensity.Add(step, densityDirection); + mean_field::operators::kernels::apply_rotational_displacement_force_residual( + f, *f.domainMapperStateless, rotation, minusDensity, minusDisplacement, + minusResidual); - mfem::Vector minusDensity(baseDensity); - minusDensity.Add(-step, densityDirection); + plusResidual -= minusResidual; + plusResidual /= 2.0 * step; + return plusResidual; +} - mfem::Vector plusDisplacement(baseDisplacement); - plusDisplacement.Add(step, displacementDirection); +template +[[nodiscard]] mfem::Vector copy_residual_block( + const mfem::Vector &action, + const mean_field::operators::RotationalDisplacementForceLayout &layout, + const mean_field::utils::blocks::residual_block block) { + mfem::Vector result(layout.size(block)); + const int offset = layout.offset(block); - mfem::Vector minusDisplacement(baseDisplacement); - minusDisplacement.Add(-step, displacementDirection); + for (int entry = 0; entry < result.Size(); ++entry) { + result(entry) = action(offset + entry); + } - mfem::Vector plusResidual; - mfem::Vector minusResidual; - - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, rotation, plusDensity, plusDisplacement, plusResidual - ); - - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, rotation, minusDensity, minusDisplacement, minusResidual - ); - - plusResidual -= minusResidual; - plusResidual /= 2.0 * step; - return plusResidual; - } - - template - [[nodiscard]] mfem::Vector copy_residual_block( - const mfem::Vector &action, - const mean_field::operators::RotationalDisplacementForceLayout &layout, - const mean_field::utils::blocks::residual_block block - ) { - mfem::Vector result(layout.size(block)); - const int offset = layout.offset(block); - - for (int entry = 0; entry < result.Size(); ++entry) { - result(entry) = action(offset + entry); - } - - return result; - } + return result; +} } // namespace rotational_displacement_force_test_utils TEST_CASE( "Rotational Displacement Force Query Includes Density Test And Linear " "Position", - tags::rotation_prepared_unit -) { - using DisplacementField = mean_field::field::Field; + tags::rotation_prepared_unit) { + using DisplacementField = + mean_field::field::Field; - constexpr int geometryWeightOrder = 4; + constexpr int geometryWeightOrder = 4; - constexpr mean_field::quadrature::Query query = - DisplacementField::make_query( - mean_field::quadrature::QuadratureRole::discretization, geometryWeightOrder, std::array{1}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general - ); + constexpr mean_field::quadrature::Query query = DisplacementField::make_query< + mean_field::field::Displacement::Form::CentrifugalForce>( + mean_field::quadrature::QuadratureRole::discretization, + geometryWeightOrder, std::array{1}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - /* density: 2, displacement test: 3, position: 1, geometry: 4 */ - constexpr int expectedBaseOrder = 2 + 3 + 1 + 4; + /* density: 2, displacement test: 3, position: 1, geometry: 4 */ + constexpr int expectedBaseOrder = 2 + 3 + 1 + 4; - STATIC_REQUIRE(query.term == mean_field::quadrature::Term::centrifugal); - STATIC_REQUIRE(query.domain == mean_field::utils::DOMAINS::STELLAR); - STATIC_REQUIRE(query.mapping == mean_field::quadrature::MappingKind::general); - STATIC_REQUIRE(query.base_order.has_value()); - STATIC_REQUIRE(*query.base_order == expectedBaseOrder); + STATIC_REQUIRE(query.term == mean_field::quadrature::Term::centrifugal); + STATIC_REQUIRE(query.domain == mean_field::utils::DOMAINS::STELLAR); + STATIC_REQUIRE(query.mapping == mean_field::quadrature::MappingKind::general); + STATIC_REQUIRE(query.base_order.has_value()); + STATIC_REQUIRE(*query.base_order == expectedBaseOrder); } -TEST_CASE( - "Rotational Displacement Force Uses Negative Rotation-Potential " - "Gradient And Excludes Vacuum", - tags::rotation_kernel_accuracy -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Rotational Displacement Force Uses Negative Rotation-Potential " + "Gradient And Excludes Vacuum", + tags::rotation_kernel_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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mfem::Vector density = rotational_displacement_force_test_utils::make_density(f, 0.31); + const mfem::Vector density = + rotational_displacement_force_test_utils::make_density(f, 0.31); - mfem::Vector displacement(f.displacementFes->GetTrueVSize()); - displacement = 0.0; + mfem::Vector displacement(f.displacementFes->GetTrueVSize()); + displacement = 0.0; - const mean_field::physics::RigidRotation rotation = rotational_displacement_force_test_utils::make_rotation(); + const mean_field::physics::RigidRotation rotation = + rotational_displacement_force_test_utils::make_rotation(); - mfem::Vector residual; + mfem::Vector residual; - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, rotation, density, displacement, residual - ); + mean_field::operators::kernels::apply_rotational_displacement_force_residual( + f, *f.domainMapperStateless, rotation, density, displacement, residual); - mfem::ParGridFunction gradientTestField(f.displacementFes.get()); + mfem::ParGridFunction gradientTestField(f.displacementFes.get()); - auto gradientFunction = [&rotation](const mfem::Vector &position, mfem::Vector &value) { - rotation.potential_gradient(position, value); - }; + auto gradientFunction = [&rotation](const mfem::Vector &position, + mfem::Vector &value) { + rotation.potential_gradient(position, value); + }; - mfem::VectorFunctionCoefficient gradientCoefficient(3, gradientFunction); + mfem::VectorFunctionCoefficient gradientCoefficient(3, gradientFunction); - gradientTestField.ProjectCoefficient(gradientCoefficient); + gradientTestField.ProjectCoefficient(gradientCoefficient); - mfem::Vector gradientTestDirection; - gradientTestField.GetTrueDofs(gradientTestDirection); + mfem::Vector gradientTestDirection; + gradientTestField.GetTrueDofs(gradientTestDirection); - const double signedWork = - rotational_displacement_force_test_utils::global_dot(residual, gradientTestDirection, f.mesh->GetComm()); + const double signedWork = + rotational_displacement_force_test_utils::global_dot( + residual, gradientTestDirection, f.mesh->GetComm()); - INFO("Rotation-force work against grad(Psi) = " << signedWork); - CHECK(signedWork < 0.0); + INFO("Rotation-force work against grad(Psi) = " << signedWork); + CHECK(signedWork < 0.0); - const mfem::Vector vacuumDensity = rotational_displacement_force_test_utils::make_vacuum_only_density(f); + const mfem::Vector vacuumDensity = + rotational_displacement_force_test_utils::make_vacuum_only_density(f); - mfem::Vector vacuumResidual; + mfem::Vector vacuumResidual; - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, rotation, vacuumDensity, displacement, vacuumResidual - ); + mean_field::operators::kernels::apply_rotational_displacement_force_residual( + f, *f.domainMapperStateless, rotation, vacuumDensity, displacement, + vacuumResidual); - CHECK(rotational_displacement_force_test_utils::global_norm(vacuumResidual, f.mesh->GetComm()) == 0.0); + CHECK(rotational_displacement_force_test_utils::global_norm( + vacuumResidual, f.mesh->GetComm()) == 0.0); - mfem::Vector zeroAngularVelocity(3); - mfem::Vector zeroCenter(3); - zeroAngularVelocity = 0.0; - zeroCenter = 0.0; + mfem::Vector zeroAngularVelocity(3); + mfem::Vector zeroCenter(3); + zeroAngularVelocity = 0.0; + zeroCenter = 0.0; - const mean_field::physics::RigidRotation zeroRotation(zeroAngularVelocity, zeroCenter); + const mean_field::physics::RigidRotation zeroRotation(zeroAngularVelocity, + zeroCenter); - mfem::Vector zeroRotationResidual; + mfem::Vector zeroRotationResidual; - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, zeroRotation, density, displacement, zeroRotationResidual - ); + mean_field::operators::kernels::apply_rotational_displacement_force_residual( + f, *f.domainMapperStateless, zeroRotation, density, displacement, + zeroRotationResidual); - CHECK(rotational_displacement_force_test_utils::global_norm(zeroRotationResidual, f.mesh->GetComm()) == 0.0); + CHECK(rotational_displacement_force_test_utils::global_norm( + zeroRotationResidual, f.mesh->GetComm()) == 0.0); } -TEST_CASE( - "Prepared Rotational Displacement Force Reprepares Selectively", - tags::rotation_prepared -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Prepared Rotational Displacement Force Reprepares Selectively", + tags::rotation_prepared) { + mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - mfem::Vector densityTrue = rotational_displacement_force_test_utils::make_density(f, 0.37); + mfem::Vector densityTrue = + rotational_displacement_force_test_utils::make_density(f, 0.37); - const mfem::Vector displacementTrue = gravity_prepared_test_utils::make_displacement(f, 0.53); + const mfem::Vector displacementTrue = + gravity_prepared_test_utils::make_displacement(f, 0.53); - mean_field::physics::RigidRotation rotation = rotational_displacement_force_test_utils::make_rotation(0.81); + mean_field::physics::RigidRotation rotation = + rotational_displacement_force_test_utils::make_rotation(0.81); - auto dependencies = rotational_displacement_force_test_utils::make_dependencies(); + auto dependencies = + rotational_displacement_force_test_utils::make_dependencies(); - mean_field::operators::PreparedRotationalDisplacementForceOperator preparedOperator(f, *f.domainMapperStateless); + mean_field::operators::PreparedRotationalDisplacementForceOperator + preparedOperator(f, *f.domainMapperStateless); - const auto &context = preparedOperator.GetContext(); - mfem::Vector density = context.GetDensityMap().gather(densityTrue); - const mfem::Vector displacement = context.GetDisplacementMap().gather(displacementTrue); + const auto &context = preparedOperator.GetContext(); + mfem::Vector density = context.GetDensityMap().gather(densityTrue); + const mfem::Vector displacement = + context.GetDisplacementMap().gather(displacementTrue); - const auto initialReport = - preparedOperator.Prepare({.density = density, .displacement = displacement}, dependencies, rotation); + const auto initialReport = preparedOperator.Prepare( + {.density = density, .displacement = displacement}, dependencies, + rotation); - REQUIRE(initialReport.DidAnyWork()); - REQUIRE(initialReport.updatedRotation); - REQUIRE(initialReport.preparedResidual); - REQUIRE(preparedOperator.IsPrepared()); + REQUIRE(initialReport.DidAnyWork()); + REQUIRE(initialReport.updatedRotation); + REQUIRE(initialReport.preparedResidual); + REQUIRE(preparedOperator.IsPrepared()); - mfem::Vector preparedResidual; - mfem::Vector kernelResidual; + mfem::Vector preparedResidual; + mfem::Vector kernelResidual; - preparedOperator.BuildResidual(preparedResidual); + preparedOperator.BuildResidual(preparedResidual); - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, rotation, densityTrue, displacementTrue, kernelResidual - ); + mean_field::operators::kernels::apply_rotational_displacement_force_residual( + f, *f.domainMapperStateless, rotation, densityTrue, displacementTrue, + kernelResidual); - const mfem::Vector kernelResidualReduced = context.GetDisplacementMap().gather(kernelResidual); + const mfem::Vector kernelResidualReduced = + context.GetDisplacementMap().gather(kernelResidual); - CHECK( - rotational_displacement_force_test_utils::relative_difference( - preparedResidual, kernelResidualReduced, f.mesh->GetComm() - ) < 2.0e-12 - ); + CHECK(rotational_displacement_force_test_utils::relative_difference( + preparedResidual, kernelResidualReduced, f.mesh->GetComm()) < + 2.0e-12); - CHECK_FALSE(preparedOperator.Prepare({.density = density, .displacement = displacement}, dependencies, rotation) - .DidAnyWork()); + CHECK_FALSE(preparedOperator + .Prepare({.density = density, .displacement = displacement}, + dependencies, rotation) + .DidAnyWork()); - densityTrue = rotational_displacement_force_test_utils::make_density(f, 0.79); - density = context.GetDensityMap().gather(densityTrue); + densityTrue = rotational_displacement_force_test_utils::make_density(f, 0.79); + density = context.GetDensityMap().gather(densityTrue); - ++dependencies.density.revision; + ++dependencies.density.revision; - const auto densityReport = - preparedOperator.Prepare({.density = density, .displacement = displacement}, dependencies, rotation); + const auto densityReport = preparedOperator.Prepare( + {.density = density, .displacement = displacement}, dependencies, + rotation); - CHECK(densityReport.preparedResidual); - CHECK_FALSE(densityReport.updatedRotation); + CHECK(densityReport.preparedResidual); + CHECK_FALSE(densityReport.updatedRotation); - rotation = rotational_displacement_force_test_utils::make_rotation(1.23); + rotation = rotational_displacement_force_test_utils::make_rotation(1.23); - ++dependencies.rotation.revision; + ++dependencies.rotation.revision; - const auto rotationReport = - preparedOperator.Prepare({.density = density, .displacement = displacement}, dependencies, rotation); + const auto rotationReport = preparedOperator.Prepare( + {.density = density, .displacement = displacement}, dependencies, + rotation); - CHECK(rotationReport.updatedRotation); - CHECK(rotationReport.preparedResidual); - CHECK(preparedOperator.GetResidualPreparationCount() == 3); - CHECK(preparedOperator.GetResidualApplicationCount() == 1); + CHECK(rotationReport.updatedRotation); + CHECK(rotationReport.preparedResidual); + CHECK(preparedOperator.GetResidualPreparationCount() == 3); + CHECK(preparedOperator.GetResidualApplicationCount() == 1); } -TEST_CASE( - "Rotational Displacement Force Jacobian Matches Both Columns And " - "Centered Differences", - tags::rotation_prepared_jacobian_accuracy -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Rotational Displacement Force Jacobian Matches Both Columns And " + "Centered Differences", + tags::rotation_prepared_jacobian_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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mfem::Vector densityTrue = rotational_displacement_force_test_utils::make_density(f, 0.43); + const mfem::Vector densityTrue = + rotational_displacement_force_test_utils::make_density(f, 0.43); - const mfem::Vector densityDirectionTrue = rotational_displacement_force_test_utils::make_density_direction(f, 0.59); + const mfem::Vector densityDirectionTrue = + rotational_displacement_force_test_utils::make_density_direction(f, 0.59); - const mfem::Vector displacementTrue = gravity_prepared_test_utils::make_displacement(f, 0.61); + const mfem::Vector displacementTrue = + gravity_prepared_test_utils::make_displacement(f, 0.61); - const mfem::Vector displacementDirectionTrue = - rotational_displacement_force_test_utils::make_displacement_direction(f); + const mfem::Vector displacementDirectionTrue = + rotational_displacement_force_test_utils::make_displacement_direction(f); - const mean_field::physics::RigidRotation rotation = rotational_displacement_force_test_utils::make_rotation(0.93); + const mean_field::physics::RigidRotation rotation = + rotational_displacement_force_test_utils::make_rotation(0.93); - mean_field::operators::PreparedRotationalDisplacementForceOperator preparedOperator(f, *f.domainMapperStateless); + mean_field::operators::PreparedRotationalDisplacementForceOperator + preparedOperator(f, *f.domainMapperStateless); - const auto &context = preparedOperator.GetContext(); - const mfem::Vector density = context.GetDensityMap().gather(densityTrue); - const mfem::Vector densityDirection = context.GetDensityMap().gather(densityDirectionTrue); - const mfem::Vector displacement = context.GetDisplacementMap().gather(displacementTrue); - const mfem::Vector displacementDirection = context.GetDisplacementMap().gather(displacementDirectionTrue); + const auto &context = preparedOperator.GetContext(); + const mfem::Vector density = context.GetDensityMap().gather(densityTrue); + const mfem::Vector densityDirection = + context.GetDensityMap().gather(densityDirectionTrue); + const mfem::Vector displacement = + context.GetDisplacementMap().gather(displacementTrue); + const mfem::Vector displacementDirection = + context.GetDisplacementMap().gather(displacementDirectionTrue); - preparedOperator.Prepare( - {.density = density, .displacement = displacement}, - rotational_displacement_force_test_utils::make_dependencies(), rotation - ); + preparedOperator.Prepare( + {.density = density, .displacement = displacement}, + rotational_displacement_force_test_utils::make_dependencies(), rotation); - mfem::Vector densityAction; - mfem::Vector displacementAction; - mfem::Vector completeAction; + mfem::Vector densityAction; + mfem::Vector displacementAction; + mfem::Vector completeAction; - preparedOperator.ApplyDensityJacobianAction(densityDirection, densityAction); + preparedOperator.ApplyDensityJacobianAction(densityDirection, densityAction); - preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, displacementAction); + preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, + displacementAction); - preparedOperator.ApplyCompleteJacobianAction(densityDirection, displacementDirection, completeAction); + preparedOperator.ApplyCompleteJacobianAction( + densityDirection, displacementDirection, completeAction); - mfem::Vector summedColumns(densityAction); - summedColumns += displacementAction; + mfem::Vector summedColumns(densityAction); + summedColumns += displacementAction; - CHECK( - rotational_displacement_force_test_utils::relative_difference( - completeAction, summedColumns, f.mesh->GetComm() - ) < 2.0e-12 - ); + CHECK(rotational_displacement_force_test_utils::relative_difference( + completeAction, summedColumns, f.mesh->GetComm()) < 2.0e-12); - mfem::Vector zeroDensityTrue(densityDirectionTrue.Size()); - mfem::Vector zeroDisplacementTrue(displacementDirectionTrue.Size()); - zeroDensityTrue = 0.0; - zeroDisplacementTrue = 0.0; + mfem::Vector zeroDensityTrue(densityDirectionTrue.Size()); + mfem::Vector zeroDisplacementTrue(displacementDirectionTrue.Size()); + zeroDensityTrue = 0.0; + zeroDisplacementTrue = 0.0; - constexpr double step = 1.0e-5; + constexpr double step = 1.0e-5; - const mfem::Vector densityDifferenceTrue = rotational_displacement_force_test_utils::centered_difference( - f, rotation, densityTrue, densityDirectionTrue, displacementTrue, zeroDisplacementTrue, step - ); + const mfem::Vector densityDifferenceTrue = + rotational_displacement_force_test_utils::centered_difference( + f, rotation, densityTrue, densityDirectionTrue, displacementTrue, + zeroDisplacementTrue, step); - const mfem::Vector displacementDifferenceTrue = rotational_displacement_force_test_utils::centered_difference( - f, rotation, densityTrue, zeroDensityTrue, displacementTrue, displacementDirectionTrue, step - ); + const mfem::Vector displacementDifferenceTrue = + rotational_displacement_force_test_utils::centered_difference( + f, rotation, densityTrue, zeroDensityTrue, displacementTrue, + displacementDirectionTrue, step); - const mfem::Vector completeDifferenceTrue = rotational_displacement_force_test_utils::centered_difference( - f, rotation, densityTrue, densityDirectionTrue, displacementTrue, displacementDirectionTrue, step - ); + const mfem::Vector completeDifferenceTrue = + rotational_displacement_force_test_utils::centered_difference( + f, rotation, densityTrue, densityDirectionTrue, displacementTrue, + displacementDirectionTrue, step); - const mfem::Vector densityDifference = context.GetDisplacementMap().gather(densityDifferenceTrue); - const mfem::Vector displacementDifference = context.GetDisplacementMap().gather(displacementDifferenceTrue); - const mfem::Vector completeDifference = context.GetDisplacementMap().gather(completeDifferenceTrue); + const mfem::Vector densityDifference = + context.GetDisplacementMap().gather(densityDifferenceTrue); + const mfem::Vector displacementDifference = + context.GetDisplacementMap().gather(displacementDifferenceTrue); + const mfem::Vector completeDifference = + context.GetDisplacementMap().gather(completeDifferenceTrue); - const double densityError = rotational_displacement_force_test_utils::relative_difference( - densityAction, densityDifference, f.mesh->GetComm() - ); + const double densityError = + rotational_displacement_force_test_utils::relative_difference( + densityAction, densityDifference, f.mesh->GetComm()); - const double displacementError = rotational_displacement_force_test_utils::relative_difference( - displacementAction, displacementDifference, f.mesh->GetComm() - ); + const double displacementError = + rotational_displacement_force_test_utils::relative_difference( + displacementAction, displacementDifference, f.mesh->GetComm()); - const double completeError = rotational_displacement_force_test_utils::relative_difference( - completeAction, completeDifference, f.mesh->GetComm() - ); + const double completeError = + rotational_displacement_force_test_utils::relative_difference( + completeAction, completeDifference, f.mesh->GetComm()); - INFO("Density-column centered-difference error = " << densityError); - INFO("Displacement-column centered-difference error = " << displacementError); - INFO("Complete centered-difference error = " << completeError); + INFO("Density-column centered-difference error = " << densityError); + INFO("Displacement-column centered-difference error = " << displacementError); + INFO("Complete centered-difference error = " << completeError); - CHECK(densityError < 2.0e-9); - CHECK(displacementError < 3.0e-8); - CHECK(completeError < 4.0e-8); + CHECK(densityError < 2.0e-9); + CHECK(displacementError < 3.0e-8); + CHECK(completeError < 4.0e-8); } -TEST_CASE( - "Prepared Rotational Displacement Force MFEM Adapter Routes Only R-d", - tags::rotation_prepared_unit -) { - mean_field::utils::Args args = test_utils::setup_args(); +TEST_CASE("Prepared Rotational Displacement Force MFEM Adapter Routes Only R-d", + tags::rotation_prepared_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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + REQUIRE(f.okay()); - const mfem::Vector densityTrue = rotational_displacement_force_test_utils::make_density(f, 0.47); + const mfem::Vector densityTrue = + rotational_displacement_force_test_utils::make_density(f, 0.47); - const mfem::Vector densityDirectionTrue = rotational_displacement_force_test_utils::make_density_direction(f, 0.63); + const mfem::Vector densityDirectionTrue = + rotational_displacement_force_test_utils::make_density_direction(f, 0.63); - const mfem::Vector displacementTrue = gravity_prepared_test_utils::make_displacement(f, 0.57); + const mfem::Vector displacementTrue = + gravity_prepared_test_utils::make_displacement(f, 0.57); - const mfem::Vector displacementDirectionTrue = - rotational_displacement_force_test_utils::make_displacement_direction(f); + const mfem::Vector displacementDirectionTrue = + rotational_displacement_force_test_utils::make_displacement_direction(f); - const mean_field::physics::RigidRotation rotation = rotational_displacement_force_test_utils::make_rotation(0.87); + const mean_field::physics::RigidRotation rotation = + rotational_displacement_force_test_utils::make_rotation(0.87); - mean_field::operators::PreparedRotationalDisplacementForceOperator preparedOperator(f, *f.domainMapperStateless); + mean_field::operators::PreparedRotationalDisplacementForceOperator + preparedOperator(f, *f.domainMapperStateless); - const auto &context = preparedOperator.GetContext(); - const mfem::Vector density = context.GetDensityMap().gather(densityTrue); - const mfem::Vector densityDirection = context.GetDensityMap().gather(densityDirectionTrue); - const mfem::Vector displacement = context.GetDisplacementMap().gather(displacementTrue); - const mfem::Vector displacementDirection = context.GetDisplacementMap().gather(displacementDirectionTrue); + const auto &context = preparedOperator.GetContext(); + const mfem::Vector density = context.GetDensityMap().gather(densityTrue); + const mfem::Vector densityDirection = + context.GetDensityMap().gather(densityDirectionTrue); + const mfem::Vector displacement = + context.GetDisplacementMap().gather(displacementTrue); + const mfem::Vector displacementDirection = + context.GetDisplacementMap().gather(displacementDirectionTrue); - preparedOperator.Prepare( - {.density = density, .displacement = displacement}, - rotational_displacement_force_test_utils::make_dependencies(), rotation - ); + preparedOperator.Prepare( + {.density = density, .displacement = displacement}, + rotational_displacement_force_test_utils::make_dependencies(), rotation); - const auto layout = rotational_displacement_force_test_utils::make_layout(f); + const auto layout = rotational_displacement_force_test_utils::make_layout(f); - mean_field::operators::PreparedRotationalDisplacementForceJacobianOperator adapter(layout, preparedOperator); + mean_field::operators::PreparedRotationalDisplacementForceJacobianOperator + adapter(layout, preparedOperator); - mfem::BlockVector direction(layout.value_offsets()); - direction = 0.0; + mfem::BlockVector direction(layout.value_offsets()); + direction = 0.0; - direction.GetBlock(rotational_displacement_force_test_utils::densityValue) = densityDirection; + direction.GetBlock(rotational_displacement_force_test_utils::densityValue) = + densityDirection; - direction.GetBlock(rotational_displacement_force_test_utils::displacementValue) = displacementDirection; + direction.GetBlock( + rotational_displacement_force_test_utils::displacementValue) = + displacementDirection; - direction.GetBlock(rotational_displacement_force_test_utils::gravityGradientValue) = 0.23; + direction.GetBlock( + rotational_displacement_force_test_utils::gravityGradientValue) = 0.23; - direction.GetBlock(rotational_displacement_force_test_utils::gravityPotentialValue) = -0.31; + direction.GetBlock( + rotational_displacement_force_test_utils::gravityPotentialValue) = -0.31; - direction.GetBlock(rotational_displacement_force_test_utils::enthalpyValue) = 0.37; + direction.GetBlock(rotational_displacement_force_test_utils::enthalpyValue) = + 0.37; - direction.GetBlock(rotational_displacement_force_test_utils::barotropicConstantValue) = -0.41; + direction.GetBlock( + rotational_displacement_force_test_utils::barotropicConstantValue) = + -0.41; - mfem::Vector action; - adapter.Mult(direction, action); + mfem::Vector action; + adapter.Mult(direction, action); - mfem::Vector expectedDisplacementAction; + mfem::Vector expectedDisplacementAction; - preparedOperator.ApplyCompleteJacobianAction(densityDirection, displacementDirection, expectedDisplacementAction); + preparedOperator.ApplyCompleteJacobianAction( + densityDirection, displacementDirection, expectedDisplacementAction); - const mfem::Vector actualDisplacementAction = rotational_displacement_force_test_utils::copy_residual_block( - action, layout, rotational_displacement_force_test_utils::displacementResidual - ); + const mfem::Vector actualDisplacementAction = + rotational_displacement_force_test_utils::copy_residual_block( + action, layout, + rotational_displacement_force_test_utils::displacementResidual); - CHECK( - rotational_displacement_force_test_utils::relative_difference( - actualDisplacementAction, expectedDisplacementAction, f.mesh->GetComm() - ) < 2.0e-12 - ); + CHECK(rotational_displacement_force_test_utils::relative_difference( + actualDisplacementAction, expectedDisplacementAction, + f.mesh->GetComm()) < 2.0e-12); - const std::array zeroRows{ - rotational_displacement_force_test_utils::copy_residual_block( - action, layout, rotational_displacement_force_test_utils::gravityGradientResidual - ), - rotational_displacement_force_test_utils::copy_residual_block( - action, layout, rotational_displacement_force_test_utils::gravityPotentialResidual - ), - rotational_displacement_force_test_utils::copy_residual_block( - action, layout, rotational_displacement_force_test_utils::densityResidual - ), - rotational_displacement_force_test_utils::copy_residual_block( - action, layout, rotational_displacement_force_test_utils::enthalpyResidual - ), - rotational_displacement_force_test_utils::copy_residual_block( - action, layout, rotational_displacement_force_test_utils::massResidual - ) - }; + const std::array zeroRows{ + rotational_displacement_force_test_utils::copy_residual_block( + action, layout, + rotational_displacement_force_test_utils::gravityGradientResidual), + rotational_displacement_force_test_utils::copy_residual_block( + action, layout, + rotational_displacement_force_test_utils::gravityPotentialResidual), + rotational_displacement_force_test_utils::copy_residual_block( + action, layout, + rotational_displacement_force_test_utils::densityResidual), + rotational_displacement_force_test_utils::copy_residual_block( + action, layout, + rotational_displacement_force_test_utils::enthalpyResidual), + rotational_displacement_force_test_utils::copy_residual_block( + action, layout, + rotational_displacement_force_test_utils::massResidual)}; - for (const mfem::Vector &row : zeroRows) { - CHECK(rotational_displacement_force_test_utils::global_norm(row, f.mesh->GetComm()) == 0.0); - } + for (const mfem::Vector &row : zeroRows) { + CHECK(rotational_displacement_force_test_utils::global_norm( + row, f.mesh->GetComm()) == 0.0); + } } diff --git a/tests/operators/prepared_stellar_equilibrium.cpp b/tests/operators/prepared_stellar_equilibrium.cpp index 1403c26..d40d920 100644 --- a/tests/operators/prepared_stellar_equilibrium.cpp +++ b/tests/operators/prepared_stellar_equilibrium.cpp @@ -13,2332 +13,2427 @@ 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< - field::Density, - DomainSchema>(*f.densityFes) - ), - displacement( - field::make_field_dof_map< - field::Displacement, - DomainSchema>(*f.displacementFes) - ), - gravityFlux( - field::make_field_dof_map< - field::Gravity, - DomainSchema>(*f.gravityFluxFes) - ), - gravityPotential( - field::make_field_dof_map< - field::Gravity, - DomainSchema>(*f.gravityPotentialFes) - ), - enthalpy( - field::make_field_dof_map< - field::Enthalpy, - DomainSchema>(*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) - ); +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; +} - 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 - ); - } +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()); + 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 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); + 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::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(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(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()); + CHECK(stellarOperator.Width() == layout.value_offsets().Last()); + CHECK(stellarOperator.Height() == layout.residual_offsets().Last()); - const MPI_Comm communicator = f.mesh->GetComm(); + 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 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); + 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); + 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); + REQUIRE(globalDensityFull > 0); + REQUIRE(globalEnthalpyFull > 0); - CHECK(globalDensityReduced > 0); - CHECK(globalDensityReduced < globalDensityFull); + CHECK(globalDensityReduced > 0); + CHECK(globalDensityReduced < globalDensityFull); - CHECK(globalEnthalpyReduced > 0); - CHECK(globalEnthalpyReduced < globalEnthalpyFull); + 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()); +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 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); + 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); + stellarOperator.Prepare(state, dependencies, rotation); - mfem::Vector rootAction; - stellarOperator.Mult(direction, rootAction); + mfem::Vector rootAction; + stellarOperator.Mult(direction, rootAction); - const mfem::Vector explicitAction = - stellar_equilibrium_test_utils::explicit_jacobian_action(stellarOperator, direction); + 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()); + 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); + INFO("Reduced root versus explicit R J P relative difference = " + << difference); - CHECK(difference < 2.0e-15); + 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; + 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); + 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()); + 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 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 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); + 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(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() - ); + 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); + 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(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()); + 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 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()); + "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(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::eos::Polytrope barotrope(3.0, 0.25); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, barotrope, 1.0); - const mean_field::mapping::COORDINATE_SPACE volumeCoordinates = - f.has_mapping() ? mean_field::mapping::COORDINATE_SPACE::PHYSICAL - : mean_field::mapping::COORDINATE_SPACE::REFERENCE; + const auto &layout = stellarOperator.GetLayout(); + mfem::Vector state(layout.value_offsets().Last()); + state = 0.0; - const double targetMass = - density * mean_field::analysis::get_mesh_volume(f, volumeCoordinates, mean_field::utils::DOMAINS::STELLAR); + 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()); - mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( - f, *f.domainMapperStateless, barotrope, targetMass - ); + mfem::Vector residual; + stellarOperator.BuildResidual(residual); - const auto &layout = stellarOperator.GetLayout(); - mfem::Vector state(layout.value_offsets().Last()); - state = 0.0; + 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()); - 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); + 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; + 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> - ); + 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()); + 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 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); + const mfem::Vector fullDirection = + stellar_equilibrium_test_utils::make_direction(f, layout); - struct ShapeCase final { - int activeColumn; - std::array allowedRows; - }; + 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 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() - }; + 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); + for (const ShapeCase &shapeCase : cases) { + CAPTURE(shapeCase.activeColumn); - mfem::Vector columnDirection(fullDirection.Size()); - columnDirection = 0.0; + 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); + for (int entry = valueOffsets[shapeCase.activeColumn]; + entry < valueOffsets[shapeCase.activeColumn + 1]; ++entry) { + columnDirection(entry) = fullDirection(entry); } -} -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); + stellarOperator.Mult(columnDirection, 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); + 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)}; - 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; + double allowedNormSquared = 0.0; - const auto potentialReport = stellarOperator.Prepare(state, dependencies, rotation); + for (int row = 0; row < 6; ++row) { + CAPTURE(row); - CHECK_FALSE(potentialReport.barotropicClosure.DidAnyWork()); - CHECK(potentialReport.hydrostatic.DidAnyWork()); - CHECK_FALSE(potentialReport.displacement.DidAnyWork()); - CHECK_FALSE(potentialReport.massNormalization.DidAnyWork()); - CHECK(potentialReport.assembledResidual); + const double rowNorm = stellar_equilibrium_test_utils::global_norm( + rowActions[row], f.mesh->GetComm()); - stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) += - 0.09; - ++dependencies.bernoulliConstant.revision; + if (shapeCase.allowedRows[row]) { + allowedNormSquared += rowNorm * rowNorm; + } else { + CHECK(rowNorm == 0.0); + } + } - const auto bernoulliReport = stellarOperator.Prepare(state, dependencies, rotation); + CHECK(allowedNormSquared > 0.0); + } +} - CHECK_FALSE(bernoulliReport.barotropicClosure.DidAnyWork()); - CHECK(bernoulliReport.hydrostatic.DidAnyWork()); - CHECK_FALSE(bernoulliReport.displacement.DidAnyWork()); - CHECK_FALSE(bernoulliReport.massNormalization.DidAnyWork()); - CHECK(bernoulliReport.assembledResidual); +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()); - ++dependencies.targetMass.revision; - const auto targetReport = stellarOperator.Prepare(state, dependencies, rotation); + 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); - CHECK_FALSE(targetReport.barotropicClosure.DidAnyWork()); - CHECK_FALSE(targetReport.hydrostatic.DidAnyWork()); - CHECK_FALSE(targetReport.displacement.DidAnyWork()); - CHECK(targetReport.massNormalization.DidAnyWork()); - CHECK(targetReport.assembledResidual); + 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 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) { - } + "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()); - void Eval( - mfem::Vector &value, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integrationPoint - ) override { - mfem::Vector computationalPosition; - transformation.Transform(integrationPoint, computationalPosition); + 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); - value.SetSize(vdim); - value = 0.0; + stellarOperator.Prepare(state, dependencies, rotation); - const double radius = computationalPosition.Norml2(); + 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; - if (!std::isfinite(radius) || radius <= 100.0 * std::numeric_limits::epsilon()) { - return; - } + const auto repeated = stellarOperator.Prepare(state, dependencies, rotation); + CHECK_FALSE(repeated.DidAnyWork()); + CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies); - double radialGradient = 0.0; + 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); - 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; + 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); - 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); - } - } + 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; - value = computationalPosition; - value *= radialGradient / radius; - } + const auto potentialReport = + stellarOperator.Prepare(state, dependencies, rotation); - private: - int m_vacuumAttribute; - double m_stellarRadius; - double m_centralDensity; - double m_targetMass; - double m_polytropicConstant; - }; + CHECK_FALSE(potentialReport.barotropicClosure.DidAnyWork()); + CHECK(potentialReport.hydrostatic.DidAnyWork()); + CHECK_FALSE(potentialReport.displacement.DidAnyWork()); + CHECK_FALSE(potentialReport.massNormalization.DidAnyWork()); + CHECK(potentialReport.assembledResidual); - mean_field::utils::Args args = test_utils::setup_args(); + stellar_equilibrium_test_utils::value_view( + state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) += 0.09; + ++dependencies.bernoulliConstant.revision; - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + const auto bernoulliReport = + stellarOperator.Prepare(state, dependencies, rotation); - REQUIRE(f.okay()); - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + CHECK_FALSE(bernoulliReport.barotropicClosure.DidAnyWork()); + CHECK(bernoulliReport.hydrostatic.DidAnyWork()); + CHECK_FALSE(bernoulliReport.displacement.DidAnyWork()); + CHECK_FALSE(bernoulliReport.massNormalization.DidAnyWork()); + CHECK(bernoulliReport.assembledResidual); - f.mapping->ResetDisplacement(); + ++dependencies.targetMass.revision; + const auto targetReport = + stellarOperator.Prepare(state, dependencies, rotation); - const double pi = std::acos(-1.0); - const double stellarRadius = mean_field::utils::RADIUS; - const double targetMass = mean_field::utils::MASS; + CHECK_FALSE(targetReport.barotropicClosure.DidAnyWork()); + CHECK_FALSE(targetReport.hydrostatic.DidAnyWork()); + CHECK_FALSE(targetReport.displacement.DidAnyWork()); + CHECK(targetReport.massNormalization.DidAnyWork()); + CHECK(targetReport.assembledResidual); +} - /* - * 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; +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) {} - /* - * The analytic n = 1 mass is - * - * M = 4 rho_c R^3 / pi. - */ - const double centralDensity = pi * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius); + void Eval(mfem::Vector &value, mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integrationPoint) override { + mfem::Vector computationalPosition; + transformation.Transform(integrationPoint, computationalPosition); - const double bernoulliConstant = -mean_field::utils::G * targetMass / stellarRadius; + value.SetSize(vdim); + value = 0.0; - const mean_field::eos::Polytrope barotrope(1.0, polytropicConstant); + const double radius = computationalPosition.Norml2(); - const auto densityFunction = [centralDensity, stellarRadius, pi](const mfem::Vector &position) { - const double radius = position.Norml2(); + if (!std::isfinite(radius) || + radius <= 100.0 * std::numeric_limits::epsilon()) { + return; + } - 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(); + double radialGradient = 0.0; + if (transformation.Attribute == m_vacuumAttribute) { /* - * Phi tends to zero at compactified infinity. + * In the compactified exterior, the three-dimensional H(div) + * Piola pullback of the inverse-square monopole field reduces + * to this finite computational-space expression. */ - if (!std::isfinite(radius)) { - return 0.0; + 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); } + } - if (radius >= stellarRadius) { - return radius > 0.0 ? -mean_field::utils::G * targetMass / radius : 0.0; - } + value = computationalPosition; + value *= radialGradient / radius; + } - const double xi = pi * radius / stellarRadius; + private: + int m_vacuumAttribute; + double m_stellarRadius; + double m_centralDensity; + double m_targetMass; + double m_polytropicConstant; + }; - const double density = std::abs(xi) < 100.0 * std::numeric_limits::epsilon() - ? centralDensity - : centralDensity * std::sin(xi) / xi; + mean_field::utils::Args args = test_utils::setup_args(); - const double enthalpy = 2.0 * polytropicConstant * density; + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - /* - * Hydrostatic equilibrium is h + Phi = C. - */ - return bernoulliConstant - enthalpy; - }; + REQUIRE(f.okay()); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - mfem::FunctionCoefficient densityCoefficient(densityFunction); - mfem::FunctionCoefficient enthalpyCoefficient(enthalpyFunction); + *f.displacement = 0.0; - mean_field::mapping::PhysicalPositionFunctionCoefficient potentialCoefficient(*f.mapping, potentialFunction); + const double pi = std::acos(-1.0); + const double stellarRadius = mean_field::utils::RADIUS; + const double targetMass = mean_field::utils::MASS; - LaneEmdenGravityGradientCoefficient gravityGradientCoefficient( - f.mesh->Dimension(), f.domainMapperStateless->GetVacuumElementAttribute(), stellarRadius, centralDensity, - targetMass, polytropicConstant - ); + /* + * 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; - mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); - mfem::ParGridFunction gravityPotentialField(f.gravityPotentialFes.get()); - mfem::ParGridFunction gravityGradientField(f.gravityFluxFes.get()); + /* + * The analytic n = 1 mass is + * + * M = 4 rho_c R^3 / pi. + */ + const double centralDensity = + pi * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius); - densityField = 0.0; - enthalpyField = 0.0; - gravityPotentialField = 0.0; - gravityGradientField = 0.0; + const double bernoulliConstant = + -mean_field::utils::G * targetMass / stellarRadius; - densityField.ProjectCoefficient(densityCoefficient); - enthalpyField.ProjectCoefficient(enthalpyCoefficient); - gravityPotentialField.ProjectCoefficient(potentialCoefficient); - gravityGradientField.ProjectCoefficient(gravityGradientCoefficient); + const mean_field::eos::Polytrope barotrope(1.0, polytropicConstant); - mfem::Vector densityTrue; - mfem::Vector enthalpyTrue; - mfem::Vector gravityPotentialTrue; - mfem::Vector gravityGradientTrue; + const auto densityFunction = [centralDensity, stellarRadius, + pi](const mfem::Vector &position) { + const double radius = position.Norml2(); - densityField.GetTrueDofs(densityTrue); - enthalpyField.GetTrueDofs(enthalpyTrue); - gravityPotentialField.GetTrueDofs(gravityPotentialTrue); - gravityGradientField.GetTrueDofs(gravityGradientTrue); + if (radius >= stellarRadius) { + return 0.0; + } - mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( - f, *f.domainMapperStateless, barotrope, targetMass - ); + const double xi = pi * radius / stellarRadius; - const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout(); + if (std::abs(xi) < 100.0 * std::numeric_limits::epsilon()) { + return centralDensity; + } - mfem::Vector analyticState(layout.value_offsets().Last()); - analyticState = 0.0; + return centralDensity * std::sin(xi) / xi; + }; - stellar_equilibrium_test_utils::assign_value_block( - analyticState, layout, stellar_equilibrium_test_utils::densityValue, - stellar_equilibrium_test_utils::reduce_density(f, densityTrue) - ); + const auto enthalpyFunction = [centralDensity, stellarRadius, + polytropicConstant, + pi](const mfem::Vector &position) { + const double radius = position.Norml2(); - stellar_equilibrium_test_utils::assign_value_block( - analyticState, layout, stellar_equilibrium_test_utils::gravityGradientValue, gravityGradientTrue - ); + if (radius >= stellarRadius) { + return 0.0; + } - stellar_equilibrium_test_utils::assign_value_block( - analyticState, layout, stellar_equilibrium_test_utils::gravityPotentialValue, gravityPotentialTrue - ); + const double xi = pi * radius / stellarRadius; - stellar_equilibrium_test_utils::assign_value_block( - analyticState, layout, stellar_equilibrium_test_utils::enthalpyValue, - stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyTrue) - ); + const double density = + std::abs(xi) < 100.0 * std::numeric_limits::epsilon() + ? centralDensity + : centralDensity * std::sin(xi) / xi; - stellar_equilibrium_test_utils::value_view(analyticState, layout, stellar_equilibrium_test_utils::bernoulliValue)( - 0 - ) = bernoulliConstant; + return 2.0 * polytropicConstant * density; + }; - 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) - ); + const auto potentialFunction = [centralDensity, stellarRadius, targetMass, + polytropicConstant, bernoulliConstant, + pi](const mfem::Vector &physicalPosition) { + const double radius = physicalPosition.Norml2(); /* - * Construct a deliberately inconsistent nearby state. The analytic - * projection should have a substantially smaller residual in every row. + * Phi tends to zero at compactified infinity. */ - mfem::Vector perturbedState(analyticState); - - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, stellar_equilibrium_test_utils::densityValue - ); - block *= 1.12; + if (!std::isfinite(radius)) { + return 0.0; } - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, stellar_equilibrium_test_utils::gravityGradientValue - ); - block *= 0.87; + if (radius >= stellarRadius) { + return radius > 0.0 ? -mean_field::utils::G * targetMass / radius : 0.0; } - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, stellar_equilibrium_test_utils::gravityPotentialValue - ); - block *= 1.08; - } + const double xi = pi * radius / stellarRadius; - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, stellar_equilibrium_test_utils::enthalpyValue - ); - block *= 0.91; - } + const double density = + std::abs(xi) < 100.0 * std::numeric_limits::epsilon() + ? centralDensity + : centralDensity * std::sin(xi) / xi; - 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); + const double enthalpy = 2.0 * polytropicConstant * density; /* - * Closure and hydrostatic balance are algebraically especially favorable - * for n = 1 because h = 2 K rho and h + Phi = C are linear relations. + * Hydrostatic equilibrium is h + Phi = C. */ - CHECK(analyticClosureNorm < 0.10 * perturbedClosureNorm); + return bernoulliConstant - enthalpy; + }; - CHECK(analyticHydrostaticNorm < 0.10 * perturbedHydrostaticNorm); + mfem::FunctionCoefficient densityCoefficient(densityFunction); + mfem::FunctionCoefficient enthalpyCoefficient(enthalpyFunction); - /* - * 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); + mean_field::mapping::PhysicalPositionFunctionCoefficient potentialCoefficient( + *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, + potentialFunction); - CHECK(analyticDisplacementNorm < 0.35 * perturbedDisplacementNorm); + LaneEmdenGravityGradientCoefficient gravityGradientCoefficient( + f.mesh->Dimension(), field_dof_test_utils::vacuum_material_attribute, + stellarRadius, centralDensity, targetMass, polytropicConstant); - CHECK(analyticMassError < 5.0e-5 * targetMass); + mfem::ParGridFunction densityField(f.densityFes.get()); + mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); + mfem::ParGridFunction gravityPotentialField(f.gravityPotentialFes.get()); + mfem::ParGridFunction gravityGradientField(f.gravityFluxFes.get()); - CHECK(analyticMassError < 0.10 * perturbedMassError); + 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(); + "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); + 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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + 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; + 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; + /* + * 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; + 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); + /* + * 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); + /* + * 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::eos::Polytrope equationOfState(3.0, polytropicConstant); - const mean_field::models::structure::PolytropicStructure structurePrescription(equationOfState, targetMass); + const mean_field::models::structure::PolytropicStructure + structurePrescription(equationOfState, targetMass); - const mean_field::models::structure::StructureSeed seed = - structurePrescription.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = 8192}); + 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); + 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); + 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); + 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."); + 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."); + MFEM_VERIFY(radiusSamples.Size() >= 2, + "The radial profile requires at least two samples."); - if (radius <= radiusSamples(0)) { - return valueSamples(0); - } + if (radius <= radiusSamples(0)) { + return valueSamples(0); + } - const int finalIndex = radiusSamples.Size() - 1; + const int finalIndex = radiusSamples.Size() - 1; - if (radius >= radiusSamples(finalIndex)) { - return valueSamples(finalIndex); - } + if (radius >= radiusSamples(finalIndex)) { + return valueSamples(finalIndex); + } - int lowerIndex = 0; - int upperIndex = finalIndex; + int lowerIndex = 0; + int upperIndex = finalIndex; - while (upperIndex - lowerIndex > 1) { - const int middleIndex = lowerIndex + (upperIndex - lowerIndex) / 2; + while (upperIndex - lowerIndex > 1) { + const int middleIndex = lowerIndex + (upperIndex - lowerIndex) / 2; - if (radiusSamples(middleIndex) <= radius) { - lowerIndex = middleIndex; - } else { - upperIndex = middleIndex; - } - } + if (radiusSamples(middleIndex) <= radius) { + lowerIndex = middleIndex; + } else { + upperIndex = middleIndex; + } + } - const double radialInterval = radiusSamples(upperIndex) - radiusSamples(lowerIndex); + const double radialInterval = + radiusSamples(upperIndex) - radiusSamples(lowerIndex); - MFEM_VERIFY(radialInterval > 0.0, "The radial profile is not strictly increasing."); + MFEM_VERIFY(radialInterval > 0.0, + "The radial profile is not strictly increasing."); - const double fraction = (radius - radiusSamples(lowerIndex)) / radialInterval; + const double fraction = + (radius - radiusSamples(lowerIndex)) / radialInterval; - return (1.0 - fraction) * valueSamples(lowerIndex) + fraction * valueSamples(upperIndex); - }; + return (1.0 - fraction) * valueSamples(lowerIndex) + + fraction * valueSamples(upperIndex); + }; - mfem::FunctionCoefficient densityCoefficient([&seed, &interpolateProfile](const mfem::Vector &position) { + mfem::FunctionCoefficient densityCoefficient( + [&seed, &interpolateProfile](const mfem::Vector &position) { const double radius = position.Norml2(); if (radius >= seed.stellarRadius) { - return 0.0; + return 0.0; } return interpolateProfile(seed.radius, seed.density, radius); - }); + }); - mfem::FunctionCoefficient enthalpyCoefficient([&seed, &interpolateProfile](const mfem::Vector &position) { + mfem::FunctionCoefficient enthalpyCoefficient( + [&seed, &interpolateProfile](const mfem::Vector &position) { const double radius = position.Norml2(); if (radius >= seed.stellarRadius) { - return 0.0; + 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()); + 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 = 0.0; + enthalpyField = 0.0; + displacementField = 0.0; - densityField.ProjectCoefficient(densityCoefficient); - enthalpyField.ProjectCoefficient(enthalpyCoefficient); + 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); /* - * Gravity initialization and the prepared root operator must see the - * same undeformed geometry. + * The Lane-Emden state must be closer to equilibrium than the nearby + * perturbed state in every residual row. */ - f.mapping->SetDisplacement(displacementField); - mean_field::physics::update_stiffness_matrix(f); - - const mean_field::physics::GravitySolution gravitySolution = - mean_field::physics::grav_potential_new(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); + CHECK(equilibriumRowNorms[row] < perturbedRowNorms[row]); /* - * Construct a physically safe perturbation direction. Density and - * enthalpy perturbations vanish at the surface because they are - * proportional to the equilibrium profiles. + * Require a substantial separation from the perturbed state while + * allowing the larger discrete projection floor in the force row. */ - mfem::Vector perturbationDirection(layout.value_offsets().Last()); - perturbationDirection = 0.0; + CHECK(equilibriumRowNorms[row] < + maximumEquilibriumFractions[row] * perturbedRowNorms[row]); + } - mfem::Vector densityDirection(densityTrue); - densityDirection *= 0.12; + /* + * Record an absolute regression bound for the current coarse-mesh + * displacement-force projection floor. + */ + CHECK(equilibriumRowNorms[3] < 1.0e-3); - mfem::Vector gravityGradientDirection(gravityGradientTrue); - gravityGradientDirection *= -0.09; + const double equilibriumMassError = + std::abs(stellar_equilibrium_test_utils::const_residual_view( + equilibriumResidual, layout, + stellar_equilibrium_test_utils::massResidual)(0)); - mfem::Vector gravityPotentialDirection(gravityPotentialTrue); - gravityPotentialDirection *= 0.07; + INFO("Equilibrium relative mass error = " << equilibriumMassError / + targetMass); - mfem::Vector enthalpyDirection(enthalpyTrue); - enthalpyDirection *= -0.11; + CHECK(equilibriumMassError < 5.0e-4 * targetMass); - const mfem::Vector displacementDirection = stellar_equilibrium_test_utils::project_displacement_direction(f, 0.15); + /* + * 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; - stellar_equilibrium_test_utils::assign_value_block( - perturbationDirection, layout, stellar_equilibrium_test_utils::densityValue, - stellar_equilibrium_test_utils::reduce_density(f, densityDirection) - ); + mfem::Vector linearizedDeparture(jacobianAction); + linearizedDeparture *= perturbationScale; - stellar_equilibrium_test_utils::assign_value_block( - perturbationDirection, layout, stellar_equilibrium_test_utils::displacementValue, displacementDirection - ); + mfem::Vector nonlinearRemainder(residualDeparture); + nonlinearRemainder -= linearizedDeparture; - stellar_equilibrium_test_utils::assign_value_block( - perturbationDirection, layout, stellar_equilibrium_test_utils::gravityGradientValue, gravityGradientDirection - ); + const double departureNorm = stellar_equilibrium_test_utils::global_norm( + residualDeparture, f.mesh->GetComm()); - stellar_equilibrium_test_utils::assign_value_block( - perturbationDirection, layout, stellar_equilibrium_test_utils::gravityPotentialValue, gravityPotentialDirection - ); + const double nonlinearRemainderNorm = + stellar_equilibrium_test_utils::global_norm(nonlinearRemainder, + f.mesh->GetComm()); - stellar_equilibrium_test_utils::assign_value_block( - perturbationDirection, layout, stellar_equilibrium_test_utils::enthalpyValue, - stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyDirection) - ); + /* + * 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); - stellar_equilibrium_test_utils:: - value_view(perturbationDirection, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = - 0.05 * bernoulliConstant; + 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); /* - * Evaluate J delta-x at the equilibrium state before changing the - * prepared base point. + * Return from the perturbed state used by the preceding Jacobian test to + * the spherical equilibrium state, while changing the rotation stream. */ - 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); + ++dependencies.rotation.revision; - stellarOperator.Prepare(perturbedState, dependencies, zeroRotation); + stellarOperator.Prepare(equilibriumState, dependencies, rotation); - mfem::Vector perturbedResidual; - stellarOperator.BuildResidual(perturbedResidual); + mfem::Vector rotatingSphericalResidual; + stellarOperator.BuildResidual(rotatingSphericalResidual); - 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() - ); - }; + mfem::ParGridFunction oblateDisplacementField(f.displacementFes.get()); - 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) - }; + mfem::VectorFunctionCoefficient oblateDisplacementCoefficient( + f.mesh->Dimension(), + [](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(3); - 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) - }; + /* + * 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); + }); - constexpr std::array rowNames{"gravity-gradient", "Poisson", "closure", - "displacement", "hydrostatic", "mass"}; + 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); /* - * 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. + * Subtract the nonrotating force-balance projection floor. The remainder + * is the displacement residual introduced by rotation. */ - 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 - }; + mfem::Vector rotationInducedResidual(rotatingDisplacementResidual); + rotationInducedResidual -= equilibriumDisplacementResidual; - for (int row = 0; row < 6; ++row) { - CAPTURE(row); - CAPTURE(rowNames[row]); - CAPTURE(equilibriumRowNorms[row]); - CAPTURE(perturbedRowNorms[row]); - CAPTURE(maximumEquilibriumFractions[row]); + const double rotationInducedWork = gravity_prepared_test_utils::global_dot( + rotationInducedResidual, oblateDisplacement, f.mesh->GetComm()); - REQUIRE(std::isfinite(equilibriumRowNorms[row])); - REQUIRE(std::isfinite(perturbedRowNorms[row])); - REQUIRE(perturbedRowNorms[row] > 0.0); + const double rotationInducedNorm = + stellar_equilibrium_test_utils::global_norm(rotationInducedResidual, + f.mesh->GetComm()); - /* - * The Lane-Emden state must be closer to equilibrium than the nearby - * perturbed state in every residual row. - */ - CHECK(equilibriumRowNorms[row] < perturbedRowNorms[row]); + const double oblateDirectionNorm = + stellar_equilibrium_test_utils::global_norm(oblateDisplacement, + f.mesh->GetComm()); - /* - * 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]); - } + 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); /* - * Record an absolute regression bound for the current coarse-mesh - * displacement-force projection floor. + * 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(equilibriumRowNorms[3] < 1.0e-3); + CHECK(rotationInducedWork < 0.0); - 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); + CHECK(rotationInducedWork < -1.0e-3 * workScale); /* - * The nonlinear residual departure should be - * - * R(x + epsilon p) - R(x) - * = epsilon J(x) p + O(epsilon^2). + * Evaluate the displacement column of the complete coupled Jacobian at + * the rotating spherical state. */ - mfem::Vector residualDeparture(perturbedResidual); - residualDeparture -= equilibriumResidual; + mfem::Vector oblateJacobianAction; + stellarOperator.Mult(oblateDirection, oblateJacobianAction); - mfem::Vector linearizedDeparture(jacobianAction); - linearizedDeparture *= perturbationScale; + const mfem::Vector oblateDisplacementJacobianAction = + stellar_equilibrium_test_utils::const_residual_view( + oblateJacobianAction, layout, + stellar_equilibrium_test_utils::displacementResidual); - mfem::Vector nonlinearRemainder(residualDeparture); - nonlinearRemainder -= linearizedDeparture; + const double residualDirectionalDerivative = + gravity_prepared_test_utils::global_dot( + rotatingDisplacementResidual, oblateDisplacementJacobianAction, + f.mesh->GetComm()); - const double departureNorm = stellar_equilibrium_test_utils::global_norm(residualDeparture, f.mesh->GetComm()); + const double jacobianDirectionNormSquared = + gravity_prepared_test_utils::global_dot( + oblateDisplacementJacobianAction, oblateDisplacementJacobianAction, + f.mesh->GetComm()); - const double nonlinearRemainderNorm = - stellar_equilibrium_test_utils::global_norm(nonlinearRemainder, f.mesh->GetComm()); + REQUIRE(std::isfinite(residualDirectionalDerivative)); + REQUIRE(std::isfinite(jacobianDirectionNormSquared)); + REQUIRE(jacobianDirectionNormSquared > 0.0); /* - * Apply the known restoring correction -epsilon p through the Jacobian. + * Minimize the linearized displacement-residual norm along the oblate + * direction: * - * This predicts the residual after returning to the equilibrium state: + * alpha_* = -(R_d, J_d p) / ||J_d p||^2. * - * R(x + epsilon p) - epsilon J(x)p approximately R(x). + * A positive alpha_* means that the operator selects equatorial expansion + * and polar contraction rather than the prolate direction. */ - mfem::Vector restoredResidualPrediction(perturbedResidual); - restoredResidualPrediction.Add(-perturbationScale, jacobianAction); + const double optimalLinearizedAmplitude = + -residualDirectionalDerivative / jacobianDirectionNormSquared; - restoredResidualPrediction -= equilibriumResidual; + INFO("Displacement-residual directional derivative = " + << residualDirectionalDerivative); + INFO( + "Optimal linearized oblate amplitude = " << optimalLinearizedAmplitude); - 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); + REQUIRE(std::isfinite(optimalLinearizedAmplitude)); + CHECK(residualDirectionalDerivative < 0.0); + REQUIRE(optimalLinearizedAmplitude > 0.0); /* - * 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. + * 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); + { - const double keplerianAngularSpeed = - std::sqrt(mean_field::utils::G * targetMass / (stellarRadius * stellarRadius * stellarRadius)); + mfem::Vector displacementBlock = + stellar_equilibrium_test_utils::value_view( + oblateState, layout, + stellar_equilibrium_test_utils::displacementValue); - 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); + 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); + } } diff --git a/tests/physics/barotrope.cpp b/tests/physics/barotrope.cpp index 5fc4d61..5ac6632 100644 --- a/tests/physics/barotrope.cpp +++ b/tests/physics/barotrope.cpp @@ -8,106 +8,115 @@ import mean_field; import test_helpers; -TEST_CASE( - "Polytropic Barotrope Satisfies Its Analytic Identities", - tags::hydro &tags::unit &tags::barotrope -) { - constexpr double polytropic_index = 3.0; - constexpr double polytropic_constant = 1.5; +TEST_CASE("Polytropic EOS Satisfies Its Analytic Identities", + tags::barotrope_eos_unit) { + constexpr double polytropic_index = 3.0; + constexpr double polytropic_constant = 1.5; - const mean_field::physics::PolytropicBarotrope barotrope(polytropic_index, polytropic_constant); + const mean_field::eos::Polytrope barotrope(polytropic_index, + polytropic_constant); - const std::array densities{1.0e-6, 1.0e-3, 0.1, 0.7, 2.0}; + const std::array densities{1.0e-6, 1.0e-3, 0.1, 0.7, 2.0}; - for (const double density : densities) { - const double pressure = barotrope.pressure_from_density(density); + for (const double density : densities) { + const double pressure = barotrope.pressure_from_density(density); - const double enthalpy = barotrope.enthalpy_from_density(density); + const double enthalpy = barotrope.enthalpy_from_density(density); - const double reconstructed_density = barotrope.density_from_enthalpy(enthalpy); + const double reconstructed_density = + barotrope.density_from_enthalpy(enthalpy); - const double reconstructed_pressure = barotrope.pressure_from_enthalpy(enthalpy); + const double reconstructed_pressure = + barotrope.pressure_from_enthalpy(enthalpy); - CHECK_THAT(reconstructed_density, Catch::Matchers::WithinRel(density, 2.0e-14)); + const double reconstructed_enthalpy = + barotrope.enthalpy_from_pressure(pressure); - CHECK_THAT(reconstructed_pressure, Catch::Matchers::WithinRel(pressure, 2.0e-14)); + CHECK_THAT(reconstructed_density, + Catch::Matchers::WithinRel(density, 2.0e-14)); - CHECK_THAT(pressure, Catch::Matchers::WithinRel(density * enthalpy / (polytropic_index + 1.0), 2.0e-14)); + CHECK_THAT(reconstructed_pressure, + Catch::Matchers::WithinRel(pressure, 2.0e-14)); - CHECK_THAT(barotrope.pressure_derivative_from_enthalpy(enthalpy), Catch::Matchers::WithinRel(density, 2.0e-14)); + CHECK_THAT(reconstructed_enthalpy, + Catch::Matchers::WithinRel(enthalpy, 2.0e-14)); - CHECK_THAT( - barotrope.pressure_derivative_from_density(density), - Catch::Matchers::WithinRel(enthalpy / polytropic_index, 2.0e-14) - ); - } + CHECK_THAT(pressure, + Catch::Matchers::WithinRel( + density * enthalpy / (polytropic_index + 1.0), 2.0e-14)); + + CHECK_THAT(barotrope.pressure_derivative_from_enthalpy(enthalpy), + Catch::Matchers::WithinRel(density, 2.0e-14)); + + CHECK_THAT( + barotrope.pressure_derivative_from_density(density), + Catch::Matchers::WithinRel(enthalpy / polytropic_index, 2.0e-14)); + } } -TEST_CASE( - "Polytropic Barotrope Derivatives Match Centered Differences", - tags::hydro &tags::jacobian &tags::unit &tags::barotrope -) { - const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5); +TEST_CASE("Polytropic EOS Derivatives Match Centered Differences", + tags::barotrope_eos_jacobian) { + const mean_field::eos::Polytrope barotrope(3.0, 1.5); - const std::array enthalpies{0.05, 0.2, 0.7, 1.4}; + const std::array enthalpies{0.05, 0.2, 0.7, 1.4}; - for (const double enthalpy : enthalpies) { - const double step = 1.0e-6 * std::max(1.0, enthalpy); + for (const double enthalpy : enthalpies) { + const double step = 1.0e-6 * std::max(1.0, enthalpy); - const double density_difference = - (barotrope.density_from_enthalpy(enthalpy + step) - barotrope.density_from_enthalpy(enthalpy - step)) / - (2.0 * step); + const double density_difference = + (barotrope.density_from_enthalpy(enthalpy + step) - + barotrope.density_from_enthalpy(enthalpy - step)) / + (2.0 * step); - const double pressure_difference = - (barotrope.pressure_from_enthalpy(enthalpy + step) - barotrope.pressure_from_enthalpy(enthalpy - step)) / - (2.0 * step); + const double pressure_difference = + (barotrope.pressure_from_enthalpy(enthalpy + step) - + barotrope.pressure_from_enthalpy(enthalpy - step)) / + (2.0 * step); - CHECK_THAT( - density_difference, - Catch::Matchers::WithinRel(barotrope.density_derivative_from_enthalpy(enthalpy), 5.0e-10) - ); + CHECK_THAT( + density_difference, + Catch::Matchers::WithinRel( + barotrope.density_derivative_from_enthalpy(enthalpy), 5.0e-10)); - CHECK_THAT( - pressure_difference, - Catch::Matchers::WithinRel(barotrope.pressure_derivative_from_enthalpy(enthalpy), 5.0e-10) - ); - } + CHECK_THAT( + pressure_difference, + Catch::Matchers::WithinRel( + barotrope.pressure_derivative_from_enthalpy(enthalpy), 5.0e-10)); + } } -TEST_CASE( - "Polytropic Barotrope Has An Exact Zero Density Surface", - tags::hydro &tags::unit &tags::barotrope -) { - const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.5); +TEST_CASE("Polytropic EOS Has An Exact Zero Density Surface", + tags::barotrope_eos_unit) { + const mean_field::eos::Polytrope barotrope(3.0, 1.5); - CHECK(barotrope.density_from_enthalpy(-1.0) == 0.0); - CHECK(barotrope.density_from_enthalpy(0.0) == 0.0); + CHECK(barotrope.density_from_enthalpy(-1.0) == 0.0); + CHECK(barotrope.density_from_enthalpy(0.0) == 0.0); - CHECK(barotrope.pressure_from_enthalpy(-1.0) == 0.0); - CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0); + CHECK(barotrope.pressure_from_enthalpy(-1.0) == 0.0); + CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0); - CHECK(barotrope.density_derivative_from_enthalpy(-1.0) == 0.0); + CHECK(barotrope.density_derivative_from_enthalpy(-1.0) == 0.0); - CHECK(barotrope.density_derivative_from_enthalpy(0.0) == 0.0); + CHECK(barotrope.density_derivative_from_enthalpy(0.0) == 0.0); - CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0); + CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0); } -TEST_CASE( - "Polytropic Barotrope Rejects Invalid Material Parameters", - tags::hydro &tags::unit -) { - CHECK_THROWS_AS(mean_field::physics::PolytropicBarotrope(0.5, 1.0), std::invalid_argument); +TEST_CASE("Polytropic EOS Rejects Invalid Material Parameters", + tags::barotrope_eos_unit) { + CHECK_THROWS_AS(mean_field::eos::Polytrope(0.5, 1.0), std::invalid_argument); - CHECK_THROWS_AS(mean_field::physics::PolytropicBarotrope(3.0, 0.0), std::invalid_argument); + CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, 0.0), std::invalid_argument); - CHECK_THROWS_AS( - mean_field::physics::PolytropicBarotrope(std::numeric_limits::infinity(), 1.0), std::invalid_argument - ); + CHECK_THROWS_AS( + mean_field::eos::Polytrope(std::numeric_limits::infinity(), 1.0), + std::invalid_argument); - const mean_field::physics::PolytropicBarotrope barotrope(3.0, 1.0); + const mean_field::eos::Polytrope barotrope(3.0, 1.0); - CHECK_THROWS_AS(barotrope.pressure_from_density(-1.0), std::domain_error); + CHECK_THROWS_AS(barotrope.pressure_from_density(-1.0), std::domain_error); - CHECK_THROWS_AS(barotrope.enthalpy_from_density(-1.0), std::domain_error); -} \ No newline at end of file + CHECK_THROWS_AS(barotrope.enthalpy_from_density(-1.0), std::domain_error); + + CHECK_THROWS_AS(barotrope.enthalpy_from_pressure(-1.0), std::domain_error); +} diff --git a/tests/physics/barotrope_pressure.cpp b/tests/physics/barotrope_pressure.cpp index 4a41231..7edfcf4 100644 --- a/tests/physics/barotrope_pressure.cpp +++ b/tests/physics/barotrope_pressure.cpp @@ -13,520 +13,561 @@ import mean_field; import test_helpers; -namespace polytropic_barotrope_test_utils { - template - double centered_derivative( - Function &&function, - const double position, - const double step - ) { - return (function(position + step) - function(position - step)) / (2.0 * step); - } - - template - double integrate_cube( - const mfem::IntegrationRule &integrationRule, - Integrand &&integrand - ) { - double integral = 0.0; - - for (int pointIndex = 0; pointIndex < integrationRule.GetNPoints(); ++pointIndex) { - const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(pointIndex); - - integral += integrationPoint.weight * integrand(integrationPoint); - } - - return integral; - } -} // namespace polytropic_barotrope_test_utils - -TEST_CASE( - "Polytropic Barotrope Satisfies Its Thermodynamic Identities", - tags::barotrope &tags::physics &tags::unit -) { - constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; - - constexpr std::array densities{1.0e-4, 0.02, 0.37, 2.4}; - - constexpr double polytropicConstant = 0.73; - - for (const double polytropicIndex : polytropicIndices) { - DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { - const mean_field::physics::PolytropicBarotrope barotrope(polytropicIndex, polytropicConstant); - - const double expectedEnthalpyScale = (polytropicIndex + 1.0) * polytropicConstant; - - CHECK(barotrope.polytropic_index() == polytropicIndex); - - CHECK(barotrope.polytropic_constant() == polytropicConstant); - - CHECK(barotrope.enthalpy_scale() == expectedEnthalpyScale); - - for (const double density : densities) { - CAPTURE(polytropicIndex, polytropicConstant, density); - - const double expectedPressure = polytropicConstant * std::pow(density, 1.0 + 1.0 / polytropicIndex); - - const double expectedEnthalpy = expectedEnthalpyScale * std::pow(density, 1.0 / polytropicIndex); - - const double pressureFromDensity = barotrope.pressure_from_density(density); - - const double enthalpyFromDensity = barotrope.enthalpy_from_density(density); - - const double recoveredDensity = barotrope.density_from_enthalpy(enthalpyFromDensity); - - const double pressureFromEnthalpy = barotrope.pressure_from_enthalpy(enthalpyFromDensity); - - CHECK_THAT(pressureFromDensity, Catch::Matchers::WithinRel(expectedPressure, 2.0e-13)); - - CHECK_THAT(enthalpyFromDensity, Catch::Matchers::WithinRel(expectedEnthalpy, 2.0e-13)); - - CHECK_THAT(recoveredDensity, Catch::Matchers::WithinRel(density, 5.0e-13)); - - CHECK_THAT(pressureFromEnthalpy, Catch::Matchers::WithinRel(expectedPressure, 5.0e-13)); - - /* - * Polytropic identity: - * - * P = rho h / (n + 1). - */ - CHECK_THAT( - pressureFromEnthalpy, - Catch::Matchers::WithinRel(density * enthalpyFromDensity / (polytropicIndex + 1.0), 5.0e-13) - ); - - /* - * Polytropic identity: - * - * dP / dh = rho. - * - * The implementation should return the same - * value as density_from_enthalpy(). - */ - CHECK( - barotrope.pressure_derivative_from_enthalpy(enthalpyFromDensity) == - barotrope.density_from_enthalpy(enthalpyFromDensity) - ); - - /* - * Since - * - * h = (n + 1) K rho^(1/n), - * - * it follows that - * - * dP / d rho = h / n. - */ - CHECK_THAT( - barotrope.pressure_derivative_from_density(density), - Catch::Matchers::WithinRel(enthalpyFromDensity / polytropicIndex, 5.0e-13) - ); - } - } - } +namespace polytropic_eos_test_utils { +template +double centered_derivative(Function &&function, const double position, + const double step) { + return (function(position + step) - function(position - step)) / (2.0 * step); } -TEST_CASE( - "Polytropic Barotrope Pressure Derivatives Match Centered Differences", - tags::barotrope &tags::physics &tags::unit &tags::jacobian &tags::pressure -) { - constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; +template +double integrate_cube(const mfem::IntegrationRule &integrationRule, + Integrand &&integrand) { + double integral = 0.0; - constexpr std::array positiveValues{0.2, 0.73, 1.8}; + for (int pointIndex = 0; pointIndex < integrationRule.GetNPoints(); + ++pointIndex) { + const mfem::IntegrationPoint &integrationPoint = + integrationRule.IntPoint(pointIndex); - constexpr double polytropicConstant = 0.61; + integral += integrationPoint.weight * integrand(integrationPoint); + } - for (const double polytropicIndex : polytropicIndices) { - const mean_field::physics::PolytropicBarotrope barotrope(polytropicIndex, polytropicConstant); + return integral; +} +} // namespace polytropic_eos_test_utils - DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { - for (const double enthalpy : positiveValues) { - const double step = 2.0e-6 * std::max(1.0, std::abs(enthalpy)); +TEST_CASE("Polytropic EOS Satisfies Its Thermodynamic Identities", + tags::barotrope_eos_unit) { + constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; - const double numericalDerivative = polytropic_barotrope_test_utils::centered_derivative( - [&barotrope](const double perturbedEnthalpy) { - return barotrope.pressure_from_enthalpy(perturbedEnthalpy); - }, - enthalpy, step - ); + constexpr std::array densities{1.0e-4, 0.02, 0.37, 2.4}; - const double analyticDerivative = barotrope.pressure_derivative_from_enthalpy(enthalpy); + constexpr double polytropicConstant = 0.73; - CAPTURE(polytropicIndex, enthalpy, step, numericalDerivative, analyticDerivative); + for (const double polytropicIndex : polytropicIndices) { + DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { + const mean_field::eos::Polytrope barotrope(polytropicIndex, + polytropicConstant); - CHECK_THAT(numericalDerivative, Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); - } + const double expectedEnthalpyScale = + (polytropicIndex + 1.0) * polytropicConstant; - for (const double density : positiveValues) { - const double step = 2.0e-6 * std::max(1.0, std::abs(density)); + CHECK(barotrope.polytropic_index() == polytropicIndex); - const double numericalDerivative = polytropic_barotrope_test_utils::centered_derivative( - [&barotrope](const double perturbedDensity) { - return barotrope.pressure_from_density(perturbedDensity); - }, - density, step - ); + CHECK(barotrope.polytropic_constant() == polytropicConstant); - const double analyticDerivative = barotrope.pressure_derivative_from_density(density); + CHECK(barotrope.enthalpy_scale() == expectedEnthalpyScale); - CAPTURE(polytropicIndex, density, step, numericalDerivative, analyticDerivative); + for (const double density : densities) { + CAPTURE(polytropicIndex, polytropicConstant, density); - CHECK_THAT(numericalDerivative, Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); - } - } + const double expectedPressure = + polytropicConstant * std::pow(density, 1.0 + 1.0 / polytropicIndex); + + const double expectedEnthalpy = + expectedEnthalpyScale * std::pow(density, 1.0 / polytropicIndex); + + const double pressureFromDensity = + barotrope.pressure_from_density(density); + + const double enthalpyFromDensity = + barotrope.enthalpy_from_density(density); + + const double recoveredDensity = + barotrope.density_from_enthalpy(enthalpyFromDensity); + + const double pressureFromEnthalpy = + barotrope.pressure_from_enthalpy(enthalpyFromDensity); + + CHECK_THAT(pressureFromDensity, + Catch::Matchers::WithinRel(expectedPressure, 2.0e-13)); + + CHECK_THAT(enthalpyFromDensity, + Catch::Matchers::WithinRel(expectedEnthalpy, 2.0e-13)); + + CHECK_THAT(recoveredDensity, + Catch::Matchers::WithinRel(density, 5.0e-13)); + + CHECK_THAT(pressureFromEnthalpy, + Catch::Matchers::WithinRel(expectedPressure, 5.0e-13)); + + /* + * Polytropic identity: + * + * P = rho h / (n + 1). + */ + CHECK_THAT(pressureFromEnthalpy, + Catch::Matchers::WithinRel(density * enthalpyFromDensity / + (polytropicIndex + 1.0), + 5.0e-13)); + + /* + * Polytropic identity: + * + * dP / dh = rho. + * + * The implementation should return the same + * value as density_from_enthalpy(). + */ + CHECK( + barotrope.pressure_derivative_from_enthalpy(enthalpyFromDensity) == + barotrope.density_from_enthalpy(enthalpyFromDensity)); + + /* + * Since + * + * h = (n + 1) K rho^(1/n), + * + * it follows that + * + * dP / d rho = h / n. + */ + CHECK_THAT(barotrope.pressure_derivative_from_density(density), + Catch::Matchers::WithinRel( + enthalpyFromDensity / polytropicIndex, 5.0e-13)); + } } + } } -TEST_CASE( - "Polytropic Barotrope Density Derivative Matches Centered Differences", - tags::barotrope &tags::physics &tags::unit &tags::jacobian &tags::pressure -) { - constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; +TEST_CASE("Polytropic EOS Pressure Derivatives Match Centered Differences", + tags::barotrope_eos_jacobian) { + constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; - constexpr std::array enthalpies{0.2, 0.73, 1.8}; + constexpr std::array positiveValues{0.2, 0.73, 1.8}; - constexpr double polytropicConstant = 0.61; + constexpr double polytropicConstant = 0.61; - for (const double polytropicIndex : polytropicIndices) { - const mean_field::physics::PolytropicBarotrope barotrope(polytropicIndex, polytropicConstant); + for (const double polytropicIndex : polytropicIndices) { + const mean_field::eos::Polytrope barotrope(polytropicIndex, + polytropicConstant); - DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { - for (const double enthalpy : enthalpies) { - const double step = 2.0e-6 * std::max(1.0, std::abs(enthalpy)); + DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { + for (const double enthalpy : positiveValues) { + const double step = 2.0e-6 * std::max(1.0, std::abs(enthalpy)); - const double numericalDerivative = polytropic_barotrope_test_utils::centered_derivative( - [&barotrope](const double perturbedEnthalpy) { - return barotrope.density_from_enthalpy(perturbedEnthalpy); - }, - enthalpy, step - ); + const double numericalDerivative = + polytropic_eos_test_utils::centered_derivative( + [&barotrope](const double perturbedEnthalpy) { + return barotrope.pressure_from_enthalpy(perturbedEnthalpy); + }, + enthalpy, step); - const double analyticDerivative = barotrope.density_derivative_from_enthalpy(enthalpy); + const double analyticDerivative = + barotrope.pressure_derivative_from_enthalpy(enthalpy); - CAPTURE(polytropicIndex, enthalpy, step, numericalDerivative, analyticDerivative); + CAPTURE(polytropicIndex, enthalpy, step, numericalDerivative, + analyticDerivative); - CHECK_THAT(numericalDerivative, Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); - } - } + CHECK_THAT(numericalDerivative, + Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); + } + + for (const double density : positiveValues) { + const double step = 2.0e-6 * std::max(1.0, std::abs(density)); + + const double numericalDerivative = + polytropic_eos_test_utils::centered_derivative( + [&barotrope](const double perturbedDensity) { + return barotrope.pressure_from_density(perturbedDensity); + }, + density, step); + + const double analyticDerivative = + barotrope.pressure_derivative_from_density(density); + + CAPTURE(polytropicIndex, density, step, numericalDerivative, + analyticDerivative); + + CHECK_THAT(numericalDerivative, + Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); + } } + } } -TEST_CASE( - "Polytropic Barotrope Defines Consistent Surface And Exterior Behavior", - tags::barotrope &tags::physics &tags::unit &tags::pressure -) { - constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; +TEST_CASE("Polytropic EOS Density Derivative Matches Centered Differences", + tags::barotrope_eos_jacobian) { + constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; - constexpr double polytropicConstant = 0.47; - constexpr double exteriorEnthalpy = -0.3; + constexpr std::array enthalpies{0.2, 0.73, 1.8}; - for (const double polytropicIndex : polytropicIndices) { - const mean_field::physics::PolytropicBarotrope barotrope(polytropicIndex, polytropicConstant); + constexpr double polytropicConstant = 0.61; - DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { - /* - * Exact surface values. - */ - CHECK(barotrope.density_from_enthalpy(0.0) == 0.0); + for (const double polytropicIndex : polytropicIndices) { + const mean_field::eos::Polytrope barotrope(polytropicIndex, + polytropicConstant); - CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0); + DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { + for (const double enthalpy : enthalpies) { + const double step = 2.0e-6 * std::max(1.0, std::abs(enthalpy)); - CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0); + const double numericalDerivative = + polytropic_eos_test_utils::centered_derivative( + [&barotrope](const double perturbedEnthalpy) { + return barotrope.density_from_enthalpy(perturbedEnthalpy); + }, + enthalpy, step); - CHECK(barotrope.pressure_from_density(0.0) == 0.0); + const double analyticDerivative = + barotrope.density_derivative_from_enthalpy(enthalpy); - CHECK(barotrope.enthalpy_from_density(0.0) == 0.0); + CAPTURE(polytropicIndex, enthalpy, step, numericalDerivative, + analyticDerivative); - CHECK(barotrope.pressure_derivative_from_density(0.0) == 0.0); - - /* - * Positive-part extension into h < 0. - */ - CHECK(barotrope.density_from_enthalpy(exteriorEnthalpy) == 0.0); - - CHECK(barotrope.pressure_from_enthalpy(exteriorEnthalpy) == 0.0); - - CHECK(barotrope.density_derivative_from_enthalpy(exteriorEnthalpy) == 0.0); - - CHECK(barotrope.pressure_derivative_from_enthalpy(exteriorEnthalpy) == 0.0); - - /* - * At h = 0, rho(h) has a nonzero right - * derivative only for n = 1. - */ - const double expectedSurfaceDensityDerivative = - polytropicIndex == 1.0 ? 1.0 / barotrope.enthalpy_scale() : 0.0; - - CHECK(barotrope.density_derivative_from_enthalpy(0.0) == expectedSurfaceDensityDerivative); - } + CHECK_THAT(numericalDerivative, + Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); + } } + } } -TEST_CASE( - "Polytropic Barotrope Rejects Invalid Physical Inputs", - tags::barotrope &tags::physics &tags::unit &tags::pressure -) { - CHECK_THROWS_AS(mean_field::physics::PolytropicBarotrope(0.999, 1.0), std::invalid_argument); +TEST_CASE("Polytropic EOS Defines Consistent Surface And Exterior Behavior", + tags::barotrope_eos_unit) { + constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; - CHECK_THROWS_AS( - mean_field::physics::PolytropicBarotrope(std::numeric_limits::infinity(), 1.0), std::invalid_argument - ); + constexpr double polytropicConstant = 0.47; + constexpr double exteriorEnthalpy = -0.3; - CHECK_THROWS_AS(mean_field::physics::PolytropicBarotrope(3.0, 0.0), std::invalid_argument); + for (const double polytropicIndex : polytropicIndices) { + const mean_field::eos::Polytrope barotrope(polytropicIndex, + polytropicConstant); - CHECK_THROWS_AS(mean_field::physics::PolytropicBarotrope(3.0, -1.0), std::invalid_argument); + DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { + /* + * Exact surface values. + */ + CHECK(barotrope.density_from_enthalpy(0.0) == 0.0); - const mean_field::physics::PolytropicBarotrope barotrope(3.0, 0.75); + CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0); - CHECK_THROWS_AS(barotrope.pressure_from_density(-0.1), std::domain_error); + CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0); - CHECK_THROWS_AS(barotrope.enthalpy_from_density(-0.1), std::domain_error); + CHECK(barotrope.pressure_from_density(0.0) == 0.0); - CHECK_THROWS_AS(barotrope.pressure_derivative_from_density(-0.1), std::domain_error); + CHECK(barotrope.enthalpy_from_density(0.0) == 0.0); - constexpr std::array nonfiniteValues{ - std::numeric_limits::infinity(), -std::numeric_limits::infinity(), - std::numeric_limits::quiet_NaN() - }; + CHECK(barotrope.pressure_derivative_from_density(0.0) == 0.0); - for (const double nonfiniteValue : nonfiniteValues) { - CAPTURE(nonfiniteValue); + /* + * Positive-part extension into h < 0. + */ + CHECK(barotrope.density_from_enthalpy(exteriorEnthalpy) == 0.0); - CHECK_THROWS_AS(barotrope.density_from_enthalpy(nonfiniteValue), std::domain_error); + CHECK(barotrope.pressure_from_enthalpy(exteriorEnthalpy) == 0.0); - CHECK_THROWS_AS(barotrope.pressure_from_enthalpy(nonfiniteValue), std::domain_error); + CHECK(barotrope.density_derivative_from_enthalpy(exteriorEnthalpy) == + 0.0); - CHECK_THROWS_AS(barotrope.density_derivative_from_enthalpy(nonfiniteValue), std::domain_error); + CHECK(barotrope.pressure_derivative_from_enthalpy(exteriorEnthalpy) == + 0.0); - CHECK_THROWS_AS(barotrope.pressure_derivative_from_enthalpy(nonfiniteValue), std::domain_error); + /* + * At h = 0, rho(h) has a nonzero right + * derivative only for n = 1. + */ + const double expectedSurfaceDensityDerivative = + polytropicIndex == 1.0 ? 1.0 / barotrope.enthalpy_scale() : 0.0; + + CHECK(barotrope.density_derivative_from_enthalpy(0.0) == + expectedSurfaceDensityDerivative); } + } } -TEST_CASE( - "Pressure Force And Pressure Integral Have Distinct Registered Forms", - tags::barotrope &tags::pressure &tags::pressure_gradient &tags::quadrature &tags::unit -) { - using EnthalpyField = mean_field::field::Field; +TEST_CASE("Polytropic EOS Rejects Invalid Physical Inputs", + tags::barotrope_eos_unit) { + CHECK_THROWS_AS(mean_field::eos::Polytrope(0.999, 1.0), + std::invalid_argument); - /* - * For the registered H1 order p = 3 and n = 3: - * - * h has degree p, - * P(h) has degree 4p, - * - * so the nonlinear EOS contributes an additional - * - * 4p - p = 3p = 9 - * - * beyond the registered enthalpy operand. - */ - constexpr int enthalpyOrder = mean_field::field::Enthalpy::Scalar::familyOrder; + CHECK_THROWS_AS( + mean_field::eos::Polytrope(std::numeric_limits::infinity(), 1.0), + std::invalid_argument); - constexpr int pressureExtraOrder = 3 * enthalpyOrder; + CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, 0.0), std::invalid_argument); - constexpr int geometryWeightOrder = 2; + CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, -1.0), std::invalid_argument); - constexpr mean_field::quadrature::Query pressureIntegralQuery = - EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::diagnostic, geometryWeightOrder, - std::array{pressureExtraOrder}, mean_field::utils::DOMAINS::STELLAR, - mean_field::quadrature::MappingKind::general - ); + const mean_field::eos::Polytrope barotrope(3.0, 0.75); - constexpr mean_field::quadrature::Query pressureForceQuery = - EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, geometryWeightOrder, - std::array{pressureExtraOrder}, mean_field::utils::DOMAINS::STELLAR, - mean_field::quadrature::MappingKind::general - ); + CHECK_THROWS_AS(barotrope.pressure_from_density(-0.1), std::domain_error); - STATIC_CHECK(mean_field::field::Enthalpy::Form::PressureIntegral::dynamicOrderCount == 1); + CHECK_THROWS_AS(barotrope.enthalpy_from_density(-0.1), std::domain_error); - STATIC_CHECK(mean_field::field::Enthalpy::Form::PressureForce::dynamicOrderCount == 1); + CHECK_THROWS_AS(barotrope.pressure_derivative_from_density(-0.1), + std::domain_error); - STATIC_CHECK( - mean_field::field::Enthalpy::Form::PressureIntegral::policyKey != - mean_field::field::Enthalpy::Form::PressureForce::policyKey - ); + constexpr std::array nonfiniteValues{ + std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}; - REQUIRE(pressureIntegralQuery.base_order.has_value()); + for (const double nonfiniteValue : nonfiniteValues) { + CAPTURE(nonfiniteValue); - REQUIRE(pressureForceQuery.base_order.has_value()); + CHECK_THROWS_AS(barotrope.density_from_enthalpy(nonfiniteValue), + std::domain_error); - /* - * Pressure integral: - * - * degree(P) + degree(J) - * = 12 + 2 - * = 14. - */ - CHECK(*pressureIntegralQuery.base_order == 14); + CHECK_THROWS_AS(barotrope.pressure_from_enthalpy(nonfiniteValue), + std::domain_error); - /* - * Pressure force: - * - * degree(P) - * + degree(grad w) - * + degree(J) - * - * = 12 + 2 + 2 - * = 16. - */ - CHECK(*pressureForceQuery.base_order == 16); + CHECK_THROWS_AS(barotrope.density_derivative_from_enthalpy(nonfiniteValue), + std::domain_error); - CHECK(pressureIntegralQuery.term == mean_field::quadrature::Term::pressure_integral); - - CHECK(pressureForceQuery.term == mean_field::quadrature::Term::pressure_force); - - CHECK(pressureIntegralQuery.role == mean_field::quadrature::QuadratureRole::diagnostic); - - CHECK(pressureForceQuery.role == mean_field::quadrature::QuadratureRole::discretization); - - CHECK(pressureIntegralQuery.domain == mean_field::utils::DOMAINS::STELLAR); - - CHECK(pressureForceQuery.domain == mean_field::utils::DOMAINS::STELLAR); - - /* - * Verify that the two terms route to independent policy - * controls. - */ - mean_field::quadrature::RuleSet ruleSet = - mean_field::quadrature::make_rule_set(mean_field::quadrature::Mode::production); - - ruleSet.pressure_integral.boost = 3; - ruleSet.pressure_force.boost = 5; - - const mean_field::quadrature::Policy policy(std::move(ruleSet)); - - const mean_field::quadrature::Resolution pressureIntegralResolution = policy.resolve(pressureIntegralQuery); - - const mean_field::quadrature::Resolution pressureForceResolution = policy.resolve(pressureForceQuery); - - CHECK(pressureIntegralResolution.base_order == 14); - - CHECK(pressureIntegralResolution.boost == 3); - - CHECK(pressureIntegralResolution.order == 17); - - CHECK(pressureForceResolution.base_order == 16); - - CHECK(pressureForceResolution.boost == 5); - - CHECK(pressureForceResolution.order == 21); + CHECK_THROWS_AS(barotrope.pressure_derivative_from_enthalpy(nonfiniteValue), + std::domain_error); + } } -TEST_CASE( - "Pressure Quadrature Exactly Integrates An N Three Polynomial", - tags::barotrope &tags::pressure &tags::pressure_gradient &tags::quadrature &tags::accuracy -) { - using EnthalpyField = mean_field::field::Field; +TEST_CASE("Pressure Force And Pressure Integral Have Distinct Registered Forms", + tags::barotrope_pressure_quadrature_unit) { + using EnthalpyField = mean_field::field::Field; - constexpr int enthalpyOrder = mean_field::field::Enthalpy::Scalar::familyOrder; + /* + * For the registered H1 order p = 3 and n = 3: + * + * h has degree p, + * P(h) has degree 4p, + * + * so the nonlinear EOS contributes an additional + * + * 4p - p = 3p = 9 + * + * beyond the registered enthalpy operand. + */ + constexpr int enthalpyOrder = + mean_field::field::Enthalpy::Scalar::familyOrder; - constexpr int pressureExtraOrder = 3 * enthalpyOrder; + constexpr int pressureExtraOrder = 3 * enthalpyOrder; - /* - * K = 1/4 and n = 3 give - * - * (n + 1) K = 1, - * rho(h) = h^3, - * P(h) = h^4 / 4. - */ - const mean_field::physics::PolytropicBarotrope barotrope(3.0, 0.25); + constexpr int geometryWeightOrder = 2; - constexpr mean_field::quadrature::Query pressureIntegralQuery = - EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::diagnostic, 0, std::array{pressureExtraOrder}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::affine - ); + constexpr mean_field::quadrature::Query pressureIntegralQuery = + EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::PressureIntegral>( + mean_field::quadrature::QuadratureRole::diagnostic, + geometryWeightOrder, std::array{pressureExtraOrder}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - constexpr mean_field::quadrature::Query pressureForceQuery = - EnthalpyField::make_query( - mean_field::quadrature::QuadratureRole::discretization, 0, std::array{pressureExtraOrder}, - mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::affine - ); + constexpr mean_field::quadrature::Query pressureForceQuery = + EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::PressureForce>( + mean_field::quadrature::QuadratureRole::discretization, + geometryWeightOrder, std::array{pressureExtraOrder}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general); - const mean_field::quadrature::RuleFactory ruleFactory{ - mean_field::quadrature::Policy(mean_field::quadrature::make_rule_set(mean_field::quadrature::Mode::production)) - }; + STATIC_CHECK( + mean_field::field::Enthalpy::Form::PressureIntegral::dynamicOrderCount == + 1); - const mean_field::quadrature::MfemRule pressureIntegralRule = - ruleFactory.get(pressureIntegralQuery, mfem::Geometry::CUBE); + STATIC_CHECK( + mean_field::field::Enthalpy::Form::PressureForce::dynamicOrderCount == 1); - const mean_field::quadrature::MfemRule pressureForceRule = - ruleFactory.get(pressureForceQuery, mfem::Geometry::CUBE); + STATIC_CHECK(mean_field::field::Enthalpy::Form::PressureIntegral::policyKey != + mean_field::field::Enthalpy::Form::PressureForce::policyKey); - /* - * On the reference cube [0,1]^3 choose - * - * h = x^3 y^3 z^3. - * - * This is representable by the order-three H1 space. - * Then - * - * P = x^12 y^12 z^12 / 4. - */ - const double numericalPressureIntegral = polytropic_barotrope_test_utils::integrate_cube( - *pressureIntegralRule.integration_rule, [&barotrope](const mfem::IntegrationPoint &integrationPoint) { - const double coordinateProduct = integrationPoint.x * integrationPoint.y * integrationPoint.z; + REQUIRE(pressureIntegralQuery.base_order.has_value()); - const double enthalpy = std::pow(coordinateProduct, 3.0); + REQUIRE(pressureForceQuery.base_order.has_value()); + + /* + * Pressure integral: + * + * degree(P) + degree(J) + * = 12 + 2 + * = 14. + */ + CHECK(*pressureIntegralQuery.base_order == 14); + + /* + * Pressure force: + * + * degree(P) + * + degree(grad w) + * + degree(J) + * + * = 12 + 2 + 2 + * = 16. + */ + CHECK(*pressureForceQuery.base_order == 16); + + CHECK(pressureIntegralQuery.term == + mean_field::quadrature::Term::pressure_integral); + + CHECK(pressureForceQuery.term == + mean_field::quadrature::Term::pressure_force); + + CHECK(pressureIntegralQuery.role == + mean_field::quadrature::QuadratureRole::diagnostic); + + CHECK(pressureForceQuery.role == + mean_field::quadrature::QuadratureRole::discretization); + + CHECK(pressureIntegralQuery.domain == mean_field::utils::DOMAINS::STELLAR); + + CHECK(pressureForceQuery.domain == mean_field::utils::DOMAINS::STELLAR); + + /* + * Verify that the two terms route to independent policy + * controls. + */ + mean_field::quadrature::RuleSet ruleSet = + mean_field::quadrature::make_rule_set( + mean_field::quadrature::Mode::production); + + ruleSet.pressure_integral.boost = 3; + ruleSet.pressure_force.boost = 5; + + const mean_field::quadrature::Policy policy(std::move(ruleSet)); + + const mean_field::quadrature::Resolution pressureIntegralResolution = + policy.resolve(pressureIntegralQuery); + + const mean_field::quadrature::Resolution pressureForceResolution = + policy.resolve(pressureForceQuery); + + CHECK(pressureIntegralResolution.base_order == 14); + + CHECK(pressureIntegralResolution.boost == 3); + + CHECK(pressureIntegralResolution.order == 17); + + CHECK(pressureForceResolution.base_order == 16); + + CHECK(pressureForceResolution.boost == 5); + + CHECK(pressureForceResolution.order == 21); +} + +TEST_CASE("Pressure Quadrature Exactly Integrates An N Three Polynomial", + tags::barotrope_pressure_quadrature_accuracy) { + using EnthalpyField = mean_field::field::Field; + + constexpr int enthalpyOrder = + mean_field::field::Enthalpy::Scalar::familyOrder; + + constexpr int pressureExtraOrder = 3 * enthalpyOrder; + + /* + * K = 1/4 and n = 3 give + * + * (n + 1) K = 1, + * rho(h) = h^3, + * P(h) = h^4 / 4. + */ + const mean_field::eos::Polytrope barotrope(3.0, 0.25); + + constexpr mean_field::quadrature::Query pressureIntegralQuery = + EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::PressureIntegral>( + mean_field::quadrature::QuadratureRole::diagnostic, 0, + std::array{pressureExtraOrder}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::affine); + + constexpr mean_field::quadrature::Query pressureForceQuery = + EnthalpyField::make_query< + mean_field::field::Enthalpy::Form::PressureForce>( + mean_field::quadrature::QuadratureRole::discretization, 0, + std::array{pressureExtraOrder}, + mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::affine); + + const mean_field::quadrature::RuleFactory ruleFactory{ + mean_field::quadrature::Policy(mean_field::quadrature::make_rule_set( + mean_field::quadrature::Mode::production))}; + + const mean_field::quadrature::MfemRule pressureIntegralRule = + ruleFactory.get(pressureIntegralQuery, mfem::Geometry::CUBE); + + const mean_field::quadrature::MfemRule pressureForceRule = + ruleFactory.get(pressureForceQuery, mfem::Geometry::CUBE); + + /* + * On the reference cube [0,1]^3 choose + * + * h = x^3 y^3 z^3. + * + * This is representable by the order-three H1 space. + * Then + * + * P = x^12 y^12 z^12 / 4. + */ + const double numericalPressureIntegral = + polytropic_eos_test_utils::integrate_cube( + *pressureIntegralRule.integration_rule, + [&barotrope](const mfem::IntegrationPoint &integrationPoint) { + const double coordinateProduct = + integrationPoint.x * integrationPoint.y * integrationPoint.z; + + const double enthalpy = std::pow(coordinateProduct, 3.0); return barotrope.pressure_from_enthalpy(enthalpy); - } - ); + }); - const double analyticPressureIntegral = 0.25 / std::pow(13.0, 3.0); + const double analyticPressureIntegral = 0.25 / std::pow(13.0, 3.0); - /* - * Choose a representable vector test function whose - * divergence is - * - * div(w) = x^2 y^2 z^2. - * - * Therefore - * - * -P div(w) - * = -x^14 y^14 z^14 / 4. - */ - const double numericalPressureForceIntegral = polytropic_barotrope_test_utils::integrate_cube( - *pressureForceRule.integration_rule, [&barotrope](const mfem::IntegrationPoint &integrationPoint) { - const double coordinateProduct = integrationPoint.x * integrationPoint.y * integrationPoint.z; + /* + * Choose a representable vector test function whose + * divergence is + * + * div(w) = x^2 y^2 z^2. + * + * Therefore + * + * -P div(w) + * = -x^14 y^14 z^14 / 4. + */ + const double numericalPressureForceIntegral = + polytropic_eos_test_utils::integrate_cube( + *pressureForceRule.integration_rule, + [&barotrope](const mfem::IntegrationPoint &integrationPoint) { + const double coordinateProduct = + integrationPoint.x * integrationPoint.y * integrationPoint.z; - const double enthalpy = std::pow(coordinateProduct, 3.0); + const double enthalpy = std::pow(coordinateProduct, 3.0); - const double pressure = barotrope.pressure_from_enthalpy(enthalpy); + const double pressure = barotrope.pressure_from_enthalpy(enthalpy); - const double testDivergence = integrationPoint.x * integrationPoint.x * integrationPoint.y * - integrationPoint.y * integrationPoint.z * integrationPoint.z; + const double testDivergence = + integrationPoint.x * integrationPoint.x * integrationPoint.y * + integrationPoint.y * integrationPoint.z * integrationPoint.z; return -pressure * testDivergence; - } - ); + }); - const double analyticPressureForceIntegral = -0.25 / std::pow(15.0, 3.0); + const double analyticPressureForceIntegral = -0.25 / std::pow(15.0, 3.0); - INFO("Pressure-integral quadrature order = " << pressureIntegralRule.resolution.order); + INFO("Pressure-integral quadrature order = " + << pressureIntegralRule.resolution.order); - INFO("Pressure-force quadrature order = " << pressureForceRule.resolution.order); + INFO("Pressure-force quadrature order = " + << pressureForceRule.resolution.order); - INFO("Numerical pressure integral = " << numericalPressureIntegral); + INFO("Numerical pressure integral = " << numericalPressureIntegral); - INFO("Analytic pressure integral = " << analyticPressureIntegral); + INFO("Analytic pressure integral = " << analyticPressureIntegral); - INFO("Numerical pressure-force integral = " << numericalPressureForceIntegral); + INFO( + "Numerical pressure-force integral = " << numericalPressureForceIntegral); - INFO("Analytic pressure-force integral = " << analyticPressureForceIntegral); + INFO("Analytic pressure-force integral = " << analyticPressureForceIntegral); - CHECK(pressureIntegralRule.resolution.base_order == 12); + CHECK(pressureIntegralRule.resolution.base_order == 12); - CHECK(pressureIntegralRule.resolution.order == 12); + CHECK(pressureIntegralRule.resolution.order == 12); - CHECK(pressureForceRule.resolution.base_order == 14); + CHECK(pressureForceRule.resolution.base_order == 14); - CHECK(pressureForceRule.resolution.order == 14); + CHECK(pressureForceRule.resolution.order == 14); - CHECK_THAT(numericalPressureIntegral, Catch::Matchers::WithinAbs(analyticPressureIntegral, 5.0e-14)); + CHECK_THAT(numericalPressureIntegral, + Catch::Matchers::WithinAbs(analyticPressureIntegral, 5.0e-14)); - CHECK_THAT(numericalPressureForceIntegral, Catch::Matchers::WithinAbs(analyticPressureForceIntegral, 5.0e-14)); -} \ No newline at end of file + CHECK_THAT( + numericalPressureForceIntegral, + Catch::Matchers::WithinAbs(analyticPressureForceIntegral, 5.0e-14)); +} diff --git a/tests/physics/gravity.cpp b/tests/physics/gravity.cpp index a255001..1f7143f 100644 --- a/tests/physics/gravity.cpp +++ b/tests/physics/gravity.cpp @@ -18,3110 +18,2610 @@ import test_helpers; using namespace mean_field; namespace { - double global_vector_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); +// Concentration-16 rational profiles have a stable mixed-projection virial +// floor of approximately 1.53e-5 on the regression mesh. Increasing the +// diagnostic quadrature order changes each energy by only O(1e-13). +constexpr double rational_profile_virial_tolerance = 2.0e-5; + +double global_vector_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); +} + +double global_vector_dot(const mfem::Vector &lhs, const mfem::Vector &rhs, + MPI_Comm communicator) { + const double local_dot = lhs * rhs; + double global_dot = 0.0; + MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator); + return global_dot; +} + +double global_relative_vector_error(const mfem::Vector &computed, + const mfem::Vector &reference, + MPI_Comm communicator) { + mfem::Vector difference(computed); + difference -= reference; + return global_vector_norm(difference, communicator) / + std::max(global_vector_norm(reference, communicator), + std::numeric_limits::epsilon()); +} + +struct GravitationalEnergies { + double binding; + double virial; +}; + +struct HomogeneousEllipsoidAnalytic { + double coefficient_x; + double coefficient_y; + double coefficient_z; + double energy_kernel; +}; + +class HomogeneousEllipsoidHDivCoefficient : public mfem::VectorCoefficient { +public: + HomogeneousEllipsoidHDivCoefficient( + const mapping::DomainMapper &domain_mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate, + const double density, + const HomogeneousEllipsoidAnalytic &analytic) + : VectorCoefficient(3), + mapping_evaluator(domain_mapper, displacement, + compactification_coordinate), + density(density), + coefficient_x(analytic.coefficient_x), + coefficient_y(analytic.coefficient_y), + coefficient_z(analytic.coefficient_z) {} + + void Eval(mfem::Vector &value, mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) override { + transformation.SetIntPoint(&integration_point); + + mfem::Vector field_physical(3); + mapping::MappingPointContext context; + MFEM_VERIFY(mapping_evaluator.EvaluatePoint( + transformation, integration_point, context) == + mapping::MappingStatus::valid, + "Ellipsoid coefficient encountered an invalid mapping."); + const mfem::Vector &x_physical = context.physical_position; + + field_physical(0) = + 2.0 * M_PI * utils::G * density * coefficient_x * x_physical(0); + field_physical(1) = + 2.0 * M_PI * utils::G * density * coefficient_y * x_physical(1); + field_physical(2) = + 2.0 * M_PI * utils::G * density * coefficient_z * x_physical(2); + + mapping::MapPhysicalFluxToHDivReference(context, field_physical, value); + } + +private: + mapping::GridFunctionMappingEvaluator mapping_evaluator; + double density; + double coefficient_x; + double coefficient_y; + double coefficient_z; +}; + +template +GravitationalEnergies +compute_gravitational_energies(fem::FEM &f, const mfem::GridFunction &rho, + const GravitySolutionType &gravity_solution, + const int quadrature_order) { + const int dim = f.mesh->Dimension(); + double local_bind_integral = 0.0; + double local_virial_integral = 0.0; + + mfem::Vector x_physical(dim); + mfem::Vector grad_phi_element(dim); + mfem::Vector grad_phi_physical(dim); + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate); + + for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) { + if (!DomainSchema::template attribute_belongs_to( + f.mesh->GetAttribute(elem_id))) { + continue; } - double global_vector_dot( - const mfem::Vector &lhs, - const mfem::Vector &rhs, - MPI_Comm communicator - ) { - const double local_dot = lhs * rhs; - double global_dot = 0.0; - MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator); - return global_dot; + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elem_id); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); + + mapping::VolumeMappingContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluateVolume(*transformation, integration_point, + mapping_context) == + mapping::MappingStatus::valid, + "Gravity-energy integration encountered an invalid mapping."); + const double weight = mapping_context.quadrature.weight; + x_physical = mapping_context.mapping.physical_position; + gravity_solution.gradPhi.GetVectorValue(elem_id, integration_point, + grad_phi_element); + mapping::MapHDivFluxToPhysical(mapping_context.mapping, + grad_phi_element, grad_phi_physical); + + const double rho_value = rho.GetValue(elem_id, integration_point); + const double phi_value = + gravity_solution.phi.GetValue(elem_id, integration_point); + double radius_dot_gradient = 0.0; + + for (int d = 0; d < dim; ++d) { + radius_dot_gradient += + (x_physical(d) - f.com(d)) * grad_phi_physical(d); + } + + local_bind_integral += rho_value * phi_value * weight; + local_virial_integral += rho_value * radius_dot_gradient * weight; + } + } + + const double local_w_bind = 0.5 * local_bind_integral; + const double local_w_vir = -local_virial_integral; + double global_w_bind = 0.0; + double global_w_vir = 0.0; + MPI_Comm communicator = f.densityFes->GetComm(); + + MPI_Allreduce(&local_w_bind, &global_w_bind, 1, MPI_DOUBLE, MPI_SUM, + communicator); + MPI_Allreduce(&local_w_vir, &global_w_vir, 1, MPI_DOUBLE, MPI_SUM, + communicator); + + return {.binding = global_w_bind, .virial = global_w_vir}; +} + +void zero_vacuum_density(const fem::FEM &f, mfem::GridFunction &rho) { + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + + const field::FieldDofMap densityMap = + field::make_field_dof_map(*f.densityFes); + + mfem::Vector densityTrue; + rho.GetTrueDofs(densityTrue); + + const mfem::Vector supportedDensity = densityMap.gather(densityTrue); + densityMap.scatter(supportedDensity, densityTrue); + rho.SetFromTrueDofs(densityTrue); +} + +int get_gravity_quadrature_order(const fem::FEM &f) { + return 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), + f.gravityFluxFes->GetMaxElementOrder()) + + 8; +} + +double compute_ellipsoid_coefficient(const double normalized_axis_x, + const double normalized_axis_y, + const double normalized_axis_z, + const double target_axis_squared) { + auto integrand = [=](const double t) { + if (t <= 0.0 || t >= 1.0) { + return 0.0; } - double global_relative_vector_error( - const mfem::Vector &computed, - const mfem::Vector &reference, - MPI_Comm communicator - ) { - mfem::Vector difference(computed); - difference -= reference; - return global_vector_norm(difference, communicator) / - std::max(global_vector_norm(reference, communicator), std::numeric_limits::epsilon()); + const double one_minus_t = 1.0 - t; + const double s = t / one_minus_t; + const double s_squared = s * s; + const double ds_squared_dt = 2.0 * s / (one_minus_t * one_minus_t); + const double delta = + std::sqrt((normalized_axis_x * normalized_axis_x + s_squared) * + (normalized_axis_y * normalized_axis_y + s_squared) * + (normalized_axis_z * normalized_axis_z + s_squared)); + + return normalized_axis_x * normalized_axis_y * normalized_axis_z * + ds_squared_dt / ((target_axis_squared + s_squared) * delta); + }; + + double integration_error = 0.0; + return boost::math::quadrature::gauss_kronrod::integrate( + integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error); +} + +double compute_ellipsoid_energy_kernel(const double normalized_axis_x, + const double normalized_axis_y, + const double normalized_axis_z, + const double length_scale) { + auto integrand = [=](const double t) { + if (t <= 0.0) { + return 0.0; + } + if (t >= 1.0) { + return 2.0; } - struct GravitationalEnergies { - double binding; - double virial; - }; + const double one_minus_t = 1.0 - t; + const double s = t / one_minus_t; + const double s_squared = s * s; + const double ds_squared_dt = 2.0 * s / (one_minus_t * one_minus_t); + const double delta = + std::sqrt((normalized_axis_x * normalized_axis_x + s_squared) * + (normalized_axis_y * normalized_axis_y + s_squared) * + (normalized_axis_z * normalized_axis_z + s_squared)); - struct HomogeneousEllipsoidAnalytic { - double coefficient_x; - double coefficient_y; - double coefficient_z; - double energy_kernel; - }; + return ds_squared_dt / delta; + }; - class HomogeneousEllipsoidHDivCoefficient : public mfem::VectorCoefficient { - public: - HomogeneousEllipsoidHDivCoefficient( - const mapping::DomainMapper &domain_mapping, - const double density, - const HomogeneousEllipsoidAnalytic &analytic - ) - : VectorCoefficient(3), - domain_mapping(domain_mapping), - density(density), - coefficient_x(analytic.coefficient_x), - coefficient_y(analytic.coefficient_y), - coefficient_z(analytic.coefficient_z) { - } + double integration_error = 0.0; + const double dimensionless_integral = + boost::math::quadrature::gauss_kronrod::integrate( + integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error); - void Eval( - mfem::Vector &value, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point - ) override { - transformation.SetIntPoint(&integration_point); + return dimensionless_integral / length_scale; +} - mfem::Vector x_physical(3); - mfem::Vector field_physical(3); - mfem::DenseMatrix map_jacobian(3, 3); - mfem::DenseMatrix inverse_map_jacobian(3, 3); +HomogeneousEllipsoidAnalytic +compute_homogeneous_ellipsoid_analytic(const double semi_axis_x, + const double semi_axis_y, + const double semi_axis_z) { + const double length_scale = + std::cbrt(semi_axis_x * semi_axis_y * semi_axis_z); + const double normalized_axis_x = semi_axis_x / length_scale; + const double normalized_axis_y = semi_axis_y / length_scale; + const double normalized_axis_z = semi_axis_z / length_scale; + const double coefficient_x = compute_ellipsoid_coefficient( + normalized_axis_x, normalized_axis_y, normalized_axis_z, + normalized_axis_x * normalized_axis_x); + const double coefficient_y = compute_ellipsoid_coefficient( + normalized_axis_x, normalized_axis_y, normalized_axis_z, + normalized_axis_y * normalized_axis_y); + const double coefficient_z = compute_ellipsoid_coefficient( + normalized_axis_x, normalized_axis_y, normalized_axis_z, + normalized_axis_z * normalized_axis_z); + const double energy_kernel = compute_ellipsoid_energy_kernel( + normalized_axis_x, normalized_axis_y, normalized_axis_z, length_scale); - domain_mapping.GetPhysicalPoint(transformation, integration_point, x_physical); + return {.coefficient_x = coefficient_x, + .coefficient_y = coefficient_y, + .coefficient_z = coefficient_z, + .energy_kernel = energy_kernel}; +} - field_physical(0) = 2.0 * M_PI * utils::G * density * coefficient_x * x_physical(0); - field_physical(1) = 2.0 * M_PI * utils::G * density * coefficient_y * x_physical(1); - field_physical(2) = 2.0 * M_PI * utils::G * density * coefficient_z * x_physical(2); +struct ExteriorMonopoleShellMetrics { + long long quadrature_points{0}; + double minimum_radius{std::numeric_limits::infinity()}; + double maximum_radius{0.0}; + double potential_rms_error{0.0}; + double radial_field_rms_error{0.0}; + double tangential_field_rms{0.0}; +}; - domain_mapping.ComputeJacobian(transformation, map_jacobian); - const double map_determinant = map_jacobian.Det(); - MFEM_VERIFY(map_determinant > 0.0, "Domain mapping has a non-positive Jacobian determinant."); +struct ExteriorMonopoleShellAccumulator { + long long quadrature_points{0}; + double minimum_radius{std::numeric_limits::infinity()}; + double maximum_radius{0.0}; + double reference_weight{0.0}; + double potential_error_squared{0.0}; + double radial_field_error_squared{0.0}; + double tangential_field_squared{0.0}; +}; - mfem::CalcInverse(map_jacobian, inverse_map_jacobian); - inverse_map_jacobian.Mult(field_physical, value); - value *= map_determinant; - } +constexpr std::array exterior_shell_boundaries{0.0, 0.25, 0.50, + 0.75, 0.90, 1.0}; - private: - const mapping::DomainMapper &domain_mapping; - double density; - double coefficient_x; - double coefficient_y; - double coefficient_z; - }; +int get_exterior_shell(const double compactification_coordinate) { + REQUIRE(std::isfinite(compactification_coordinate)); + REQUIRE(compactification_coordinate >= -1.0e-12); + REQUIRE(compactification_coordinate <= 1.0 + 1.0e-12); - template - GravitationalEnergies compute_gravitational_energies( - fem::FEM &f, - const mfem::GridFunction &rho, - const GravitySolutionType &gravity_solution, - const int quadrature_order - ) { - const int dim = f.mesh->Dimension(); - double local_bind_integral = 0.0; - double local_virial_integral = 0.0; + const double coordinate = + std::clamp(compactification_coordinate, 0.0, std::nextafter(1.0, 0.0)); - mfem::Vector x_physical(dim); - mfem::Vector grad_phi_element(dim); - mfem::Vector grad_phi_physical(dim); - mfem::DenseMatrix map_jacobian(dim, dim); + for (int shell = 0; + shell < static_cast(exterior_shell_boundaries.size()) - 1; + ++shell) { + if (coordinate < exterior_shell_boundaries[shell + 1]) { + return shell; + } + } - for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) { - if (f.mesh->GetAttribute(elem_id) == 3) { - continue; - } + return static_cast(exterior_shell_boundaries.size()) - 2; +} - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elem_id); - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); +std::array measure_exterior_monopole_shells( + fem::FEM &f, const physics::GravitySolution &solution, + const mfem::GridFunction &displacement, const double mass) { + REQUIRE(f.mesh != nullptr); + REQUIRE(f.gravityFluxFes != nullptr); + REQUIRE(f.displacementFes != nullptr); + REQUIRE(f.compactificationFes != nullptr); + REQUIRE(f.compactificationCoordinate != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); + constexpr int shell_count = + static_cast(exterior_shell_boundaries.size()) - 1; - double weight = transformation->Weight() * integration_point.weight; + std::array local_shells{}; - if (f.has_mapping()) { - const double map_determinant = f.mapping->ComputeDetJ(*transformation, integration_point); - MFEM_VERIFY( - map_determinant > 0.0, "Domain mapping has a non-positive Jacobian " - "determinant." - ); + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - weight *= map_determinant; - f.mapping->GetPhysicalPoint(*transformation, integration_point, x_physical); - gravity_solution.gradPhi.GetVectorValue(elem_id, integration_point, grad_phi_element); - f.mapping->ComputeJacobian(*transformation, map_jacobian); - map_jacobian.Mult(grad_phi_element, grad_phi_physical); - grad_phi_physical /= map_determinant; - } else { - transformation->Transform(integration_point, x_physical); - gravity_solution.gradPhi.GetVectorValue(elem_id, integration_point, grad_phi_physical); - } + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; - const double rho_value = rho.GetValue(elem_id, integration_point); - const double phi_value = gravity_solution.phi.GetValue(elem_id, integration_point); - double radius_dot_gradient = 0.0; + const int quadrature_order = get_gravity_quadrature_order(f); - for (int d = 0; d < dim; ++d) { - radius_dot_gradient += (x_physical(d) - f.com(d)) * grad_phi_physical(d); - } + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); - local_bind_integral += rho_value * phi_value * weight; - local_virial_integral += rho_value * radius_dot_gradient * weight; - } - } + REQUIRE(transformation != nullptr); - const double local_w_bind = 0.5 * local_bind_integral; - const double local_w_vir = -local_virial_integral; - double global_w_bind = 0.0; - double global_w_vir = 0.0; - MPI_Comm communicator = f.densityFes->GetComm(); - - MPI_Allreduce(&local_w_bind, &global_w_bind, 1, MPI_DOUBLE, MPI_SUM, communicator); - MPI_Allreduce(&local_w_vir, &global_w_vir, 1, MPI_DOUBLE, MPI_SUM, communicator); - - return {.binding = global_w_bind, .virial = global_w_vir}; + if (transformation->Attribute != vacuum_attribute) { + continue; } - void zero_vacuum_density( - const fem::FEM &f, - mfem::GridFunction &rho - ) { - for (int i = 0; i < f.vacuumDensityTdofs.Size(); ++i) { - rho(f.vacuumDensityTdofs[i]) = 0.0; - } + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); + + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; + + mfem::DofTransformation *displacement_dof_transformation = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + + mfem::DofTransformation *compactification_dof_transformation = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); + + mfem::Vector element_displacement; + mfem::Vector element_compactification; + + displacement.GetSubVector(displacement_dofs, element_displacement); + + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); + + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal(element_displacement); } - int get_gravity_quadrature_order(const fem::FEM &f) { - return 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), f.gravityFluxFes->GetMaxElementOrder()) + 8; + if (compactification_dof_transformation != nullptr) { + compactification_dof_transformation->InvTransformPrimal( + element_compactification); } - double compute_ellipsoid_coefficient( - const double normalized_axis_x, - const double normalized_axis_y, - const double normalized_axis_z, - const double target_axis_squared - ) { - auto integrand = [=](const double t) { - if (t <= 0.0 || t >= 1.0) { - return 0.0; - } + const mapping::ElementDisplacementData displacement_data( + displacement_element, element_displacement, + f.displacementFes->GetOrdering()); - const double one_minus_t = 1.0 - t; - const double s = t / one_minus_t; - const double s_squared = s * s; - const double ds_squared_dt = 2.0 * s / (one_minus_t * one_minus_t); - const double delta = std::sqrt( - (normalized_axis_x * normalized_axis_x + s_squared) * - (normalized_axis_y * normalized_axis_y + s_squared) * - (normalized_axis_z * normalized_axis_z + s_squared) - ); + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification); - return normalized_axis_x * normalized_axis_y * normalized_axis_z * ds_squared_dt / - ((target_axis_squared + s_squared) * delta); - }; + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; - double integration_error = 0.0; - return boost::math::quadrature::gauss_kronrod::integrate( - integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error - ); + mfem::Vector compactification_shape(compactification_element.GetDof()); + + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + + transformation->SetIntPoint(&integration_point); + + compactification_element.CalcShape(integration_point, + compactification_shape); + + const double compactification_coordinate = + element_compactification * compactification_shape; + + const int shell = get_exterior_shell(compactification_coordinate); + + mfem::Vector reference_field(3); + mfem::Vector physical_field(3); + mfem::Vector physical_position(3); + + solution.gradPhi.GetVectorValue(element_id, integration_point, + reference_field); + + mapping::VolumeMappingContext mapping_context; + + const mapping::MappingStatus status = + f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation, + integration_point, workspace, + mapping_context); + + CAPTURE(element_id, q, compactification_coordinate, + static_cast(status)); + + REQUIRE(status == mean_field::mapping::MappingStatus::valid); + + physical_position = mapping_context.mapping.physical_position; + + mean_field::mapping::MapHDivFluxToPhysical( + mapping_context.mapping, reference_field, physical_field); + + const double radius = physical_position.Norml2(); + + CAPTURE(element_id, q, shell, compactification_coordinate, radius); + + REQUIRE(std::isfinite(radius)); + REQUIRE(radius > 0.0); + + mfem::Vector radial_unit_vector(physical_position); + radial_unit_vector /= radius; + + const double numerical_radial_field = physical_field * radial_unit_vector; + + mfem::Vector tangential_field(physical_field); + tangential_field.Add(-numerical_radial_field, radial_unit_vector); + + const double numerical_potential = + solution.phi.GetValue(element_id, integration_point); + + /* + * For an exterior monopole: + * + * phi = -GM/r + * grad(phi) = GM r_hat/r^2 + * + * These scaled quantities should therefore be one, one, and + * zero respectively. They remain well-conditioned as r -> inf. + */ + const double scaled_potential = + -radius * numerical_potential / (utils::G * mass); + + const double scaled_radial_field = + radius * radius * numerical_radial_field / (utils::G * mass); + + const double scaled_tangential_field = + radius * radius * tangential_field.Norml2() / (utils::G * mass); + + REQUIRE(std::isfinite(scaled_potential)); + REQUIRE(std::isfinite(scaled_radial_field)); + REQUIRE(std::isfinite(scaled_tangential_field)); + + /* + * Use the finite reference-domain measure for averaging. A + * physical L2 norm of phi over an infinite three-dimensional + * exterior domain is not finite. + */ + const double reference_weight = + integration_point.weight * transformation->Weight(); + + ExteriorMonopoleShellAccumulator &accumulator = local_shells[shell]; + + ++accumulator.quadrature_points; + + accumulator.minimum_radius = std::min(accumulator.minimum_radius, radius); + + accumulator.maximum_radius = std::max(accumulator.maximum_radius, radius); + + accumulator.reference_weight += reference_weight; + + accumulator.potential_error_squared += + reference_weight * std::pow(scaled_potential - 1.0, 2); + + accumulator.radial_field_error_squared += + reference_weight * std::pow(scaled_radial_field - 1.0, 2); + + accumulator.tangential_field_squared += + reference_weight * scaled_tangential_field * scaled_tangential_field; + } + } + + MPI_Comm communicator = f.gravityFluxFes->GetComm(); + + std::array metrics{}; + + for (int shell = 0; shell < shell_count; ++shell) { + long long global_points = 0; + + MPI_Allreduce(&local_shells[shell].quadrature_points, &global_points, 1, + MPI_LONG_LONG, MPI_SUM, communicator); + + double local_sums[4]{local_shells[shell].reference_weight, + local_shells[shell].potential_error_squared, + local_shells[shell].radial_field_error_squared, + local_shells[shell].tangential_field_squared}; + + double global_sums[4]{}; + + MPI_Allreduce(local_sums, global_sums, 4, MPI_DOUBLE, MPI_SUM, + communicator); + + double global_minimum_radius = 0.0; + double global_maximum_radius = 0.0; + + MPI_Allreduce(&local_shells[shell].minimum_radius, &global_minimum_radius, + 1, MPI_DOUBLE, MPI_MIN, communicator); + + MPI_Allreduce(&local_shells[shell].maximum_radius, &global_maximum_radius, + 1, MPI_DOUBLE, MPI_MAX, communicator); + + REQUIRE(global_points > 0); + REQUIRE(global_sums[0] > 0.0); + + metrics[shell] = { + .quadrature_points = global_points, + .minimum_radius = global_minimum_radius, + .maximum_radius = global_maximum_radius, + .potential_rms_error = std::sqrt(global_sums[1] / global_sums[0]), + .radial_field_rms_error = std::sqrt(global_sums[2] / global_sums[0]), + .tangential_field_rms = std::sqrt(global_sums[3] / global_sums[0])}; + } + + return metrics; +} + +class StatelessProjectionGeometry { +public: + StatelessProjectionGeometry( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper, + const mfem::GridFunction &displacement) + : m_fem(f), m_domain_mapper(domain_mapper), m_displacement(displacement), + m_workspace(f.mesh->Dimension()) {} + + mapping::MappingStatus + Evaluate(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + mapping::MappingPointContext &context, + const bool permit_infinity_limit) { + m_last_evaluation_used_infinity_limit = false; + + const int element_id = transformation.ElementNo; + + MFEM_VERIFY(element_id >= 0, + "Projection coefficient received an invalid element number."); + + const mfem::FiniteElement &displacement_element = + *m_fem.displacementFes->GetFE(element_id); + + const mfem::FiniteElement &compactification_element = + *m_fem.compactificationFes->GetFE(element_id); + + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; + + mfem::DofTransformation *displacement_dof_transformation = + m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); + + mfem::DofTransformation *compactification_dof_transformation = + m_fem.compactificationFes->GetElementDofs(element_id, + compactification_dofs); + + mfem::Vector element_displacement; + mfem::Vector element_compactification; + + m_displacement.GetSubVector(displacement_dofs, element_displacement); + + m_fem.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); + + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal(element_displacement); } - double compute_ellipsoid_energy_kernel( - const double normalized_axis_x, - const double normalized_axis_y, - const double normalized_axis_z, - const double length_scale - ) { - auto integrand = [=](const double t) { - if (t <= 0.0) { - return 0.0; - } - if (t >= 1.0) { - return 2.0; - } - - const double one_minus_t = 1.0 - t; - const double s = t / one_minus_t; - const double s_squared = s * s; - const double ds_squared_dt = 2.0 * s / (one_minus_t * one_minus_t); - const double delta = std::sqrt( - (normalized_axis_x * normalized_axis_x + s_squared) * - (normalized_axis_y * normalized_axis_y + s_squared) * - (normalized_axis_z * normalized_axis_z + s_squared) - ); - - return ds_squared_dt / delta; - }; - - double integration_error = 0.0; - const double dimensionless_integral = boost::math::quadrature::gauss_kronrod::integrate( - integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error - ); - - return dimensionless_integral / length_scale; + if (compactification_dof_transformation != nullptr) { + compactification_dof_transformation->InvTransformPrimal( + element_compactification); } - HomogeneousEllipsoidAnalytic compute_homogeneous_ellipsoid_analytic( - const double semi_axis_x, - const double semi_axis_y, - const double semi_axis_z - ) { - const double length_scale = std::cbrt(semi_axis_x * semi_axis_y * semi_axis_z); - const double normalized_axis_x = semi_axis_x / length_scale; - const double normalized_axis_y = semi_axis_y / length_scale; - const double normalized_axis_z = semi_axis_z / length_scale; - const double coefficient_x = compute_ellipsoid_coefficient( - normalized_axis_x, normalized_axis_y, normalized_axis_z, normalized_axis_x * normalized_axis_x - ); - const double coefficient_y = compute_ellipsoid_coefficient( - normalized_axis_x, normalized_axis_y, normalized_axis_z, normalized_axis_y * normalized_axis_y - ); - const double coefficient_z = compute_ellipsoid_coefficient( - normalized_axis_x, normalized_axis_y, normalized_axis_z, normalized_axis_z * normalized_axis_z - ); - const double energy_kernel = - compute_ellipsoid_energy_kernel(normalized_axis_x, normalized_axis_y, normalized_axis_z, length_scale); + const mapping::ElementDisplacementData displacement_data( + displacement_element, element_displacement, + m_fem.displacementFes->GetOrdering()); - return { - .coefficient_x = coefficient_x, - .coefficient_y = coefficient_y, - .coefficient_z = coefficient_z, - .energy_kernel = energy_kernel - }; + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification); + + mfem::Vector requested_compactification_shape( + compactification_element.GetDof()); + + compactification_element.CalcShape(integration_point, + requested_compactification_shape); + + const double requested_compactification_coordinate = + element_compactification * requested_compactification_shape; + + constexpr double infinity_candidate_tolerance = 1.0e-8; + + const bool requested_infinity_limit = + std::isfinite(requested_compactification_coordinate) && + requested_compactification_coordinate >= + 1.0 - infinity_candidate_tolerance; + + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; + + mapping::MappingStatus status = m_domain_mapper.EvaluatePoint( + mapping_data, transformation, integration_point, m_workspace, context); + + if (status == mapping::MappingStatus::valid) { + transformation.SetIntPoint(&integration_point); + return status; } - struct GravitySolutionComparison { - double relative_gradient_difference; - double relative_potential_difference; - }; - - template < - typename LeftGravitySolution, - typename RightGravitySolution> - GravitySolutionComparison compare_gravity_solutions( - fem::FEM &f, - const LeftGravitySolution &left_solution, - const RightGravitySolution &right_solution, - const int quadrature_order - ) { - const int dimension = f.mesh->Dimension(); - - double local_gradient_difference_squared = 0.0; - double local_left_gradient_norm_squared = 0.0; - double local_right_gradient_norm_squared = 0.0; - double local_potential_difference_squared = 0.0; - double local_left_potential_norm_squared = 0.0; - double local_right_potential_norm_squared = 0.0; - - mfem::Vector left_gradient_element(dimension); - mfem::Vector right_gradient_element(dimension); - mfem::Vector left_gradient_physical(dimension); - mfem::Vector right_gradient_physical(dimension); - mfem::Vector gradient_difference(dimension); - mfem::DenseMatrix map_jacobian(dimension, dimension); - - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - if (f.mesh->GetAttribute(element_id) == 3) { - continue; - } - - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); - - double weight = transformation->Weight() * integration_point.weight; - - left_solution.gradPhi.GetVectorValue(element_id, integration_point, left_gradient_element); - right_solution.gradPhi.GetVectorValue(element_id, integration_point, right_gradient_element); - - if (f.has_mapping()) { - const double map_determinant = f.mapping->ComputeDetJ(*transformation, integration_point); - MFEM_VERIFY( - map_determinant > 0.0, "Domain mapping has a non-positive Jacobian " - "determinant." - ); - - weight *= map_determinant; - f.mapping->ComputeJacobian(*transformation, map_jacobian); - - map_jacobian.Mult(left_gradient_element, left_gradient_physical); - map_jacobian.Mult(right_gradient_element, right_gradient_physical); - - left_gradient_physical /= map_determinant; - right_gradient_physical /= map_determinant; - } else { - left_gradient_physical = left_gradient_element; - right_gradient_physical = right_gradient_element; - } - - gradient_difference = left_gradient_physical; - gradient_difference -= right_gradient_physical; - - local_gradient_difference_squared += (gradient_difference * gradient_difference) * weight; - local_left_gradient_norm_squared += (left_gradient_physical * left_gradient_physical) * weight; - local_right_gradient_norm_squared += (right_gradient_physical * right_gradient_physical) * weight; - - const double left_potential = left_solution.phi.GetValue(element_id, integration_point); - const double right_potential = right_solution.phi.GetValue(element_id, integration_point); - const double potential_difference = left_potential - right_potential; - - local_potential_difference_squared += potential_difference * potential_difference * weight; - local_left_potential_norm_squared += left_potential * left_potential * weight; - local_right_potential_norm_squared += right_potential * right_potential * weight; - } - } - - const std::array local_values{local_gradient_difference_squared, local_left_gradient_norm_squared, - local_right_gradient_norm_squared, local_potential_difference_squared, - local_left_potential_norm_squared, local_right_potential_norm_squared}; - - std::array global_values{}; - - MPI_Allreduce( - local_values.data(), global_values.data(), static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, - f.densityFes->GetComm() - ); - - const double gradient_scale_squared = 0.5 * (global_values[1] + global_values[2]); - const double potential_scale_squared = 0.5 * (global_values[4] + global_values[5]); - - MFEM_VERIFY(gradient_scale_squared > 0.0, "Cannot compare gravity solutions with zero gradient norm."); - MFEM_VERIFY(potential_scale_squared > 0.0, "Cannot compare gravity solutions with zero potential norm."); - - return { - .relative_gradient_difference = std::sqrt(global_values[0] / gradient_scale_squared), - .relative_potential_difference = std::sqrt(global_values[3] / potential_scale_squared) - }; + if (!permit_infinity_limit || + !m_domain_mapper.IsCompactifiedElement(transformation)) { + transformation.SetIntPoint(&integration_point); + return status; } - struct GravityResidualMetrics { - double relative_total; - double relative_gradient; - double relative_poisson; - }; + const bool retryable_boundary_status = + status == mapping::MappingStatus::at_compactified_infinity || + status == mapping::MappingStatus::outside_reference_domain || + status == mapping::MappingStatus::non_finite_result || + (requested_infinity_limit && + status == mapping::MappingStatus::non_positive_determinant); - double gravity_test_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); + if (!retryable_boundary_status) { + transformation.SetIntPoint(&integration_point); + return status; } - void gravity_test_get_true_dofs( - const mfem::ParFiniteElementSpace &finite_element_space, - const mfem::GridFunction &grid_function, - mfem::Vector &true_dofs - ) { - MFEM_VERIFY(grid_function.Size() == finite_element_space.GetVSize(), "Grid function has the wrong local size."); + const mfem::IntegrationPoint &element_center = + mfem::Geometries.GetCenter(transformation.GetGeometryType()); - true_dofs.SetSize(finite_element_space.GetTrueVSize()); + /* + * Use the nearest admissible point. Starting extremely close to + * the requested point preserves the limiting RT trace, while the + * larger fallbacks accommodate the mapper's infinity guard. + */ + constexpr std::array inward_fractions{1.0e-12, 1.0e-11, 1.0e-10, + 1.0e-9, 1.0e-8, 1.0e-7, + 1.0e-6, 1.0e-5, 1.0e-4}; - const mfem::Operator *restriction = finite_element_space.GetRestrictionMatrix(); + for (const double inward_fraction : inward_fractions) { + mfem::IntegrationPoint inward_point; - if (restriction != nullptr) { - restriction->Mult(grid_function, true_dofs); - } else { - MFEM_VERIFY(grid_function.Size() == true_dofs.Size(), "Local and true sizes do not match."); - true_dofs = grid_function; - } + inward_point.x = (1.0 - inward_fraction) * integration_point.x + + inward_fraction * element_center.x; + + inward_point.y = (1.0 - inward_fraction) * integration_point.y + + inward_fraction * element_center.y; + + inward_point.z = (1.0 - inward_fraction) * integration_point.z + + inward_fraction * element_center.z; + + inward_point.weight = integration_point.weight; + + status = m_domain_mapper.EvaluatePoint( + mapping_data, transformation, inward_point, m_workspace, context); + + if (status == mapping::MappingStatus::valid) { + m_last_evaluation_used_infinity_limit = true; + transformation.SetIntPoint(&integration_point); + return status; + } + + const bool still_retryable = + status == mapping::MappingStatus::at_compactified_infinity || + status == mapping::MappingStatus::outside_reference_domain || + status == mapping::MappingStatus::non_finite_result || + (requested_infinity_limit && + status == mapping::MappingStatus::non_positive_determinant); + if (!still_retryable) { + break; + } } - GravityResidualMetrics measure_gravity_residual( - const mfem::Vector &residual, - const mfem::Array &offsets, - const double right_hand_side_norm, - MPI_Comm communicator - ) { - MFEM_VERIFY(offsets.Size() == 3, "Gravity residual must contain two blocks."); - MFEM_VERIFY(residual.Size() == offsets.Last(), "Gravity residual has the wrong size."); - MFEM_VERIFY(right_hand_side_norm > 0.0, "Gravity right-hand side must be nonzero."); - - mfem::Vector gradient_residual(offsets[1] - offsets[0]); - mfem::Vector poisson_residual(offsets[2] - offsets[1]); - - for (int i = 0; i < gradient_residual.Size(); ++i) { - gradient_residual(i) = residual(offsets[0] + i); - } - - for (int i = 0; i < poisson_residual.Size(); ++i) { - poisson_residual(i) = residual(offsets[1] + i); - } - - return { - .relative_total = gravity_test_global_norm(residual, communicator) / right_hand_side_norm, - .relative_gradient = gravity_test_global_norm(gradient_residual, communicator) / right_hand_side_norm, - .relative_poisson = gravity_test_global_norm(poisson_residual, communicator) / right_hand_side_norm - }; - } - - enum class ExteriorMonopoleMapping { legacy, stateless }; - - struct ExteriorMonopoleShellMetrics { - long long quadrature_points{0}; - double minimum_radius{std::numeric_limits::infinity()}; - double maximum_radius{0.0}; - double potential_rms_error{0.0}; - double radial_field_rms_error{0.0}; - double tangential_field_rms{0.0}; - }; - - struct ExteriorMonopoleShellAccumulator { - long long quadrature_points{0}; - double minimum_radius{std::numeric_limits::infinity()}; - double maximum_radius{0.0}; - double reference_weight{0.0}; - double potential_error_squared{0.0}; - double radial_field_error_squared{0.0}; - double tangential_field_squared{0.0}; - }; - - constexpr std::array exterior_shell_boundaries{0.0, 0.25, 0.50, 0.75, 0.90, 1.0}; - - int get_exterior_shell(const double compactification_coordinate) { - REQUIRE(std::isfinite(compactification_coordinate)); - REQUIRE(compactification_coordinate >= -1.0e-12); - REQUIRE(compactification_coordinate <= 1.0 + 1.0e-12); - - const double coordinate = std::clamp(compactification_coordinate, 0.0, std::nextafter(1.0, 0.0)); - - for (int shell = 0; shell < static_cast(exterior_shell_boundaries.size()) - 1; ++shell) { - if (coordinate < exterior_shell_boundaries[shell + 1]) { - return shell; - } - } - - return static_cast(exterior_shell_boundaries.size()) - 2; - } - - std::array< - ExteriorMonopoleShellMetrics, - 5> - measure_exterior_monopole_shells( - fem::FEM &f, - const physics::GravitySolution &solution, - const mfem::GridFunction &displacement, - const ExteriorMonopoleMapping mapping_path, - const double mass - ) { - REQUIRE(f.mesh != nullptr); - REQUIRE(f.gravityFluxFes != nullptr); - REQUIRE(f.displacementFes != nullptr); - REQUIRE(f.compactificationFes != nullptr); - REQUIRE(f.compactificationCoordinate != nullptr); - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); - - constexpr int shell_count = static_cast(exterior_shell_boundaries.size()) - 1; - - std::array local_shells{}; - - mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); - - const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute(); - - const int quadrature_order = get_gravity_quadrature_order(f); - - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - - REQUIRE(transformation != nullptr); - - if (transformation->Attribute != vacuum_attribute) { - continue; - } - - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); - - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; - - mfem::DofTransformation *displacement_dof_transformation = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - - mfem::DofTransformation *compactification_dof_transformation = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); - - mfem::Vector element_displacement; - mfem::Vector element_compactification; - - displacement.GetSubVector(displacement_dofs, element_displacement); - - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(element_displacement); - } - - if (compactification_dof_transformation != nullptr) { - compactification_dof_transformation->InvTransformPrimal(element_compactification); - } - - const mapping::ElementDisplacementData displacement_data( - displacement_element, element_displacement, f.displacementFes->GetOrdering() - ); - - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - mfem::Vector compactification_shape(compactification_element.GetDof()); - - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - - transformation->SetIntPoint(&integration_point); - - compactification_element.CalcShape(integration_point, compactification_shape); - - const double compactification_coordinate = element_compactification * compactification_shape; - - const int shell = get_exterior_shell(compactification_coordinate); - - mfem::Vector reference_field(3); - mfem::Vector physical_field(3); - mfem::Vector physical_position(3); - - solution.gradPhi.GetVectorValue(element_id, integration_point, reference_field); - - if (mapping_path == ExteriorMonopoleMapping::stateless) { - mapping::VolumeMappingContext mapping_context; - - const mapping::MappingStatus status = f.domainMapperStateless->EvaluateVolume( - mapping_data, *transformation, integration_point, workspace, mapping_context - ); - - CAPTURE(element_id, q, compactification_coordinate, static_cast(status)); - - REQUIRE(status == mean_field::mapping::MappingStatus::valid); - - physical_position = mapping_context.mapping.physical_position; - - mean_field::mapping::MapHDivFluxToPhysical( - mapping_context.mapping, reference_field, physical_field - ); - } else { - /* - * The legacy path intentionally does not use the exterior - * coordinate to construct its physical mapping. - */ - f.mapping->GetPhysicalPoint(*transformation, integration_point, physical_position); - - mfem::DenseMatrix mapping_jacobian(3); - f.mapping->ComputeJacobian(*transformation, mapping_jacobian); - - const double mapping_determinant = mapping_jacobian.Det(); - - REQUIRE(std::isfinite(mapping_determinant)); - REQUIRE(mapping_determinant > 0.0); - - mapping_jacobian.Mult(reference_field, physical_field); - - physical_field /= mapping_determinant; - } - - const double radius = physical_position.Norml2(); - - CAPTURE(element_id, q, shell, compactification_coordinate, radius); - - REQUIRE(std::isfinite(radius)); - REQUIRE(radius > 0.0); - - mfem::Vector radial_unit_vector(physical_position); - radial_unit_vector /= radius; - - const double numerical_radial_field = physical_field * radial_unit_vector; - - mfem::Vector tangential_field(physical_field); - tangential_field.Add(-numerical_radial_field, radial_unit_vector); - - const double numerical_potential = solution.phi.GetValue(element_id, integration_point); - - /* - * For an exterior monopole: - * - * phi = -GM/r - * grad(phi) = GM r_hat/r^2 - * - * These scaled quantities should therefore be one, one, and - * zero respectively. They remain well-conditioned as r -> inf. - */ - const double scaled_potential = -radius * numerical_potential / (utils::G * mass); - - const double scaled_radial_field = radius * radius * numerical_radial_field / (utils::G * mass); - - const double scaled_tangential_field = radius * radius * tangential_field.Norml2() / (utils::G * mass); - - REQUIRE(std::isfinite(scaled_potential)); - REQUIRE(std::isfinite(scaled_radial_field)); - REQUIRE(std::isfinite(scaled_tangential_field)); - - /* - * Use the finite reference-domain measure for averaging. A - * physical L2 norm of phi over an infinite three-dimensional - * exterior domain is not finite. - */ - const double reference_weight = integration_point.weight * transformation->Weight(); - - ExteriorMonopoleShellAccumulator &accumulator = local_shells[shell]; - - ++accumulator.quadrature_points; - - accumulator.minimum_radius = std::min(accumulator.minimum_radius, radius); - - accumulator.maximum_radius = std::max(accumulator.maximum_radius, radius); - - accumulator.reference_weight += reference_weight; - - accumulator.potential_error_squared += reference_weight * std::pow(scaled_potential - 1.0, 2); - - accumulator.radial_field_error_squared += reference_weight * std::pow(scaled_radial_field - 1.0, 2); - - accumulator.tangential_field_squared += - reference_weight * scaled_tangential_field * scaled_tangential_field; - } - } - - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - std::array metrics{}; - - for (int shell = 0; shell < shell_count; ++shell) { - long long global_points = 0; - - MPI_Allreduce( - &local_shells[shell].quadrature_points, &global_points, 1, MPI_LONG_LONG, MPI_SUM, communicator - ); - - double local_sums[4]{ - local_shells[shell].reference_weight, local_shells[shell].potential_error_squared, - local_shells[shell].radial_field_error_squared, local_shells[shell].tangential_field_squared - }; - - double global_sums[4]{}; - - MPI_Allreduce(local_sums, global_sums, 4, MPI_DOUBLE, MPI_SUM, communicator); - - double global_minimum_radius = 0.0; - double global_maximum_radius = 0.0; - - MPI_Allreduce( - &local_shells[shell].minimum_radius, &global_minimum_radius, 1, MPI_DOUBLE, MPI_MIN, communicator - ); - - MPI_Allreduce( - &local_shells[shell].maximum_radius, &global_maximum_radius, 1, MPI_DOUBLE, MPI_MAX, communicator - ); - - REQUIRE(global_points > 0); - REQUIRE(global_sums[0] > 0.0); - - metrics[shell] = { - .quadrature_points = global_points, - .minimum_radius = global_minimum_radius, - .maximum_radius = global_maximum_radius, - .potential_rms_error = std::sqrt(global_sums[1] / global_sums[0]), - .radial_field_rms_error = std::sqrt(global_sums[2] / global_sums[0]), - .tangential_field_rms = std::sqrt(global_sums[3] / global_sums[0]) - }; - } - - return metrics; - } - - class StatelessProjectionGeometry { - public: - StatelessProjectionGeometry( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::GridFunction &displacement - ) - : m_fem(f), - m_domain_mapper(domain_mapper), - m_displacement(displacement), - m_workspace(f.mesh->Dimension()) { - } - - mapping::MappingStatus Evaluate( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - mapping::MappingPointContext &context, - const bool permit_infinity_limit - ) { - m_last_evaluation_used_infinity_limit = false; - - const int element_id = transformation.ElementNo; - - MFEM_VERIFY(element_id >= 0, "Projection coefficient received an invalid element number."); - - const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id); - - const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id); - - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; - - mfem::DofTransformation *displacement_dof_transformation = - m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); - - mfem::DofTransformation *compactification_dof_transformation = - m_fem.compactificationFes->GetElementDofs(element_id, compactification_dofs); - - mfem::Vector element_displacement; - mfem::Vector element_compactification; - - m_displacement.GetSubVector(displacement_dofs, element_displacement); - - m_fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(element_displacement); - } - - if (compactification_dof_transformation != nullptr) { - compactification_dof_transformation->InvTransformPrimal(element_compactification); - } - - const mapping::ElementDisplacementData displacement_data( - displacement_element, element_displacement, m_fem.displacementFes->GetOrdering() - ); - - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - - mfem::Vector requested_compactification_shape(compactification_element.GetDof()); - - compactification_element.CalcShape(integration_point, requested_compactification_shape); - - const double requested_compactification_coordinate = - element_compactification * requested_compactification_shape; - - constexpr double infinity_candidate_tolerance = 1.0e-8; - - const bool requested_infinity_limit = - std::isfinite(requested_compactification_coordinate) && - requested_compactification_coordinate >= 1.0 - infinity_candidate_tolerance; - - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - mapping::MappingStatus status = - m_domain_mapper.EvaluatePoint(mapping_data, transformation, integration_point, m_workspace, context); - - if (status == mapping::MappingStatus::valid) { - transformation.SetIntPoint(&integration_point); - return status; - } - - if (!permit_infinity_limit || !m_domain_mapper.IsCompactifiedElement(transformation)) { - transformation.SetIntPoint(&integration_point); - return status; - } - - const bool retryable_boundary_status = - status == mapping::MappingStatus::at_compactified_infinity || - status == mapping::MappingStatus::outside_reference_domain || - status == mapping::MappingStatus::non_finite_result || - (requested_infinity_limit && status == mapping::MappingStatus::non_positive_determinant); - - if (!retryable_boundary_status) { - transformation.SetIntPoint(&integration_point); - return status; - } - - const mfem::IntegrationPoint &element_center = mfem::Geometries.GetCenter(transformation.GetGeometryType()); - - /* - * Use the nearest admissible point. Starting extremely close to - * the requested point preserves the limiting RT trace, while the - * larger fallbacks accommodate the mapper's infinity guard. - */ - constexpr std::array inward_fractions{1.0e-12, 1.0e-11, 1.0e-10, 1.0e-9, 1.0e-8, - 1.0e-7, 1.0e-6, 1.0e-5, 1.0e-4}; - - for (const double inward_fraction : inward_fractions) { - mfem::IntegrationPoint inward_point; - - inward_point.x = (1.0 - inward_fraction) * integration_point.x + inward_fraction * element_center.x; - - inward_point.y = (1.0 - inward_fraction) * integration_point.y + inward_fraction * element_center.y; - - inward_point.z = (1.0 - inward_fraction) * integration_point.z + inward_fraction * element_center.z; - - inward_point.weight = integration_point.weight; - - status = - m_domain_mapper.EvaluatePoint(mapping_data, transformation, inward_point, m_workspace, context); - - if (status == mapping::MappingStatus::valid) { - m_last_evaluation_used_infinity_limit = true; - transformation.SetIntPoint(&integration_point); - return status; - } - - const bool still_retryable = - status == mapping::MappingStatus::at_compactified_infinity || - status == mapping::MappingStatus::outside_reference_domain || - status == mapping::MappingStatus::non_finite_result || - (requested_infinity_limit && status == mapping::MappingStatus::non_positive_determinant); - if (!still_retryable) { - break; - } - } - - transformation.SetIntPoint(&integration_point); - return status; - } - - [[nodiscard]] - bool LastEvaluationUsedInfinityLimit() const noexcept { - return m_last_evaluation_used_infinity_limit; - } - - private: - const fem::FEM &m_fem; - const mapping::DomainMapperStateless &m_domain_mapper; - const mfem::GridFunction &m_displacement; - mapping::DomainMapperStateless::Workspace m_workspace; - bool m_last_evaluation_used_infinity_limit{false}; - }; - class StatelessMonopolePotentialCoefficient final : public mfem::Coefficient { - public: - StatelessMonopolePotentialCoefficient( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::GridFunction &displacement, - const double mass, - const double stellar_radius - ) - : m_geometry( - f, - domain_mapper, - displacement - ), - m_vacuum_attribute(domain_mapper.GetVacuumElementAttribute()), - m_mass(mass), - m_stellar_radius(stellar_radius) { - } - - double Eval( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point - ) override { - mapping::MappingPointContext context; - - const mapping::MappingStatus status = m_geometry.Evaluate(transformation, integration_point, context, true); - - MFEM_VERIFY( - status == mean_field::mapping::MappingStatus::valid, + transformation.SetIntPoint(&integration_point); + return status; + } + + [[nodiscard]] + bool LastEvaluationUsedInfinityLimit() const noexcept { + return m_last_evaluation_used_infinity_limit; + } + +private: + const fem::FEM &m_fem; + const mapping::DomainMapper &m_domain_mapper; + const mfem::GridFunction &m_displacement; + mapping::DomainMapper::Workspace m_workspace; + bool m_last_evaluation_used_infinity_limit{false}; +}; +class StatelessMonopolePotentialCoefficient final : public mfem::Coefficient { +public: + StatelessMonopolePotentialCoefficient( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper, + const mfem::GridFunction &displacement, const double mass, + const double stellar_radius) + : m_geometry(f, domain_mapper, displacement), + m_vacuum_attribute(field_dof_test_utils::vacuum_material_attribute), + m_mass(mass), m_stellar_radius(stellar_radius) {} + + double Eval(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) override { + mapping::MappingPointContext context; + + const mapping::MappingStatus status = + m_geometry.Evaluate(transformation, integration_point, context, true); + + MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, "Stateless monopole-potential projection failed." << "\nMapping status = " << static_cast(status) << "\nElement ID = " << transformation.ElementNo - << "\nElement attribute = " << transformation.Attribute << "\nIntegration point = <" - << integration_point.x << ", " << integration_point.y << ", " << integration_point.z << ">" - ); + << "\nElement attribute = " << transformation.Attribute + << "\nIntegration point = <" << integration_point.x << ", " + << integration_point.y << ", " << integration_point.z + << ">"); - if (m_geometry.LastEvaluationUsedInfinityLimit()) { - return 0.0; - } + if (m_geometry.LastEvaluationUsedInfinityLimit()) { + return 0.0; + } - const double radius = context.physical_position.Norml2(); + const double radius = context.physical_position.Norml2(); - MFEM_VERIFY( - std::isfinite(radius) && radius > 0.0, "Monopole projection encountered an invalid physical radius." - ); + MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, + "Monopole projection encountered an invalid physical radius."); - if (transformation.Attribute == m_vacuum_attribute) { - return -utils::G * m_mass / radius; - } + if (transformation.Attribute == m_vacuum_attribute) { + return -utils::G * m_mass / radius; + } - return -utils::G * m_mass / (2.0 * m_stellar_radius * m_stellar_radius * m_stellar_radius) * - (3.0 * m_stellar_radius * m_stellar_radius - radius * radius); - } + return -utils::G * m_mass / + (2.0 * m_stellar_radius * m_stellar_radius * m_stellar_radius) * + (3.0 * m_stellar_radius * m_stellar_radius - radius * radius); + } - private: - StatelessProjectionGeometry m_geometry; - int m_vacuum_attribute; - double m_mass; - double m_stellar_radius; - }; +private: + StatelessProjectionGeometry m_geometry; + int m_vacuum_attribute; + double m_mass; + double m_stellar_radius; +}; - class StatelessMonopoleHDivCoefficient final : public mfem::VectorCoefficient { - public: - StatelessMonopoleHDivCoefficient( - const fem::FEM &f, - const mapping::DomainMapperStateless &domain_mapper, - const mfem::GridFunction &displacement, - const double mass, - const double stellar_radius - ) - : VectorCoefficient(f.mesh->Dimension()), - m_geometry( - f, - domain_mapper, - displacement - ), - m_vacuum_attribute(domain_mapper.GetVacuumElementAttribute()), - m_mass(mass), - m_stellar_radius(stellar_radius) { - } +class StatelessMonopoleHDivCoefficient final : public mfem::VectorCoefficient { +public: + StatelessMonopoleHDivCoefficient( + const fem::FEM &f, const mapping::DomainMapper &domain_mapper, + const mfem::GridFunction &displacement, const double mass, + const double stellar_radius) + : VectorCoefficient(f.mesh->Dimension()), + m_geometry(f, domain_mapper, displacement), + m_vacuum_attribute(field_dof_test_utils::vacuum_material_attribute), + m_mass(mass), m_stellar_radius(stellar_radius) {} - void Eval( - mfem::Vector &value, - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point - ) override { - mapping::MappingPointContext context; + void Eval(mfem::Vector &value, mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) override { + mapping::MappingPointContext context; - const mapping::MappingStatus status = m_geometry.Evaluate(transformation, integration_point, context, true); + const mapping::MappingStatus status = + m_geometry.Evaluate(transformation, integration_point, context, true); - MFEM_VERIFY( - status == mean_field::mapping::MappingStatus::valid, + MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, "Stateless monopole H(div) projection failed." << "\nMapping status = " << static_cast(status) << "\nElement ID = " << transformation.ElementNo - << "\nElement attribute = " << transformation.Attribute << "\nIntegration point = <" - << integration_point.x << ", " << integration_point.y << ", " << integration_point.z << ">" - << "\nInfinity-limit evaluation attempted = " << m_geometry.LastEvaluationUsedInfinityLimit() - ); - const mfem::Vector &displaced_position = context.displaced_position; + << "\nElement attribute = " << transformation.Attribute + << "\nIntegration point = <" << integration_point.x << ", " + << integration_point.y << ", " << integration_point.z << ">" + << "\nInfinity-limit evaluation attempted = " + << m_geometry.LastEvaluationUsedInfinityLimit()); + const mfem::Vector &displaced_position = context.displaced_position; - const double computational_radius = displaced_position.Norml2(); + const double computational_radius = displaced_position.Norml2(); - MFEM_VERIFY( - std::isfinite(computational_radius) && computational_radius > 0.0, + MFEM_VERIFY(std::isfinite(computational_radius) && + computational_radius > 0.0, "Monopole H(div) projection encountered an invalid displaced " - "computational radius." - ); + "computational radius."); - const double displacement_determinant = context.displacement_jacobian.Det(); + const double displacement_determinant = context.displacement_jacobian.Det(); - MFEM_VERIFY( - std::isfinite(displacement_determinant) && displacement_determinant > 0.0, + MFEM_VERIFY(std::isfinite(displacement_determinant) && + displacement_determinant > 0.0, "Monopole H(div) projection encountered an invalid " "displacement " - "Jacobian determinant." - ); + "Jacobian determinant."); - mfem::DenseMatrix inverse_displacement_jacobian; + mfem::DenseMatrix inverse_displacement_jacobian; - inverse_displacement_jacobian.SetSize( - context.displacement_jacobian.Height(), context.displacement_jacobian.Width() - ); + inverse_displacement_jacobian.SetSize( + context.displacement_jacobian.Height(), + context.displacement_jacobian.Width()); - mfem::CalcInverse(context.displacement_jacobian, inverse_displacement_jacobian); + mfem::CalcInverse(context.displacement_jacobian, + inverse_displacement_jacobian); - /* - * Pull the radial field back only through the regular displacement - * map. - * - * In the compactified vacuum, the Kelvin scale and its radial - * derivative cancel exactly from the three-dimensional H(div) Piola - * pullback of the inverse-square monopole field: - * - * det(J) J^{-1} (GM x / |x|^3) - * = GM det(A) A^{-1} y / |y|^3. - * - * This is also the finite reference-space limit at compactified - * infinity. - */ - inverse_displacement_jacobian.Mult(displaced_position, value); + /* + * Pull the radial field back only through the regular displacement + * map. + * + * In the compactified vacuum, the Kelvin scale and its radial + * derivative cancel exactly from the three-dimensional H(div) Piola + * pullback of the inverse-square monopole field: + * + * det(J) J^{-1} (GM x / |x|^3) + * = GM det(A) A^{-1} y / |y|^3. + * + * This is also the finite reference-space limit at compactified + * infinity. + */ + inverse_displacement_jacobian.Mult(displaced_position, value); - double radial_denominator = 0.0; + double radial_denominator = 0.0; - if (transformation.Attribute == m_vacuum_attribute) { - radial_denominator = computational_radius * computational_radius * computational_radius; - } else { - radial_denominator = m_stellar_radius * m_stellar_radius * m_stellar_radius; - } + if (transformation.Attribute == m_vacuum_attribute) { + radial_denominator = + computational_radius * computational_radius * computational_radius; + } else { + radial_denominator = + m_stellar_radius * m_stellar_radius * m_stellar_radius; + } - value *= mean_field::utils::G * m_mass * displacement_determinant / radial_denominator; + value *= mean_field::utils::G * m_mass * displacement_determinant / + radial_denominator; - for (int component = 0; component < value.Size(); ++component) { - MFEM_VERIFY( - std::isfinite(value(component)), "Monopole H(div) projection produced a non-finite " - "reference flux." - ); - } - } + for (int component = 0; component < value.Size(); ++component) { + MFEM_VERIFY(std::isfinite(value(component)), + "Monopole H(div) projection produced a non-finite " + "reference flux."); + } + } - private: - StatelessProjectionGeometry m_geometry; - int m_vacuum_attribute; - double m_mass; - double m_stellar_radius; - }; +private: + StatelessProjectionGeometry m_geometry; + int m_vacuum_attribute; + double m_mass; + double m_stellar_radius; +}; } // namespace -TEST_CASE( - "Uniform Potential Matches Analytic", - tags::gravity_analytic_initialization -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); +TEST_CASE("Uniform Potential Matches Analytic", tags::gravity_analytic) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + *f.displacement = 0.0; - const double radius = utils::RADIUS; - const double mass = utils::MASS; - const double analytic_volume = (4.0 / 3.0) * M_PI * std::pow(radius, 3.0); - const double density = mass / analytic_volume; + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; - mfem::GridFunction rho_uniform(f.densityFes.get()); - rho_uniform = density; - zero_vacuum_density(f, rho_uniform); - analysis::conserve_mass(f, rho_uniform, mass); + const double radius = utils::RADIUS; + const double mass = utils::MASS; + const double analytic_volume = (4.0 / 3.0) * M_PI * std::pow(radius, 3.0); + const double density = mass / analytic_volume; - f.com = analysis::get_com(f, rho_uniform); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); + mfem::GridFunction rho_uniform(f.densityFes.get()); + rho_uniform = density; + zero_vacuum_density(f, rho_uniform); + analysis::conserve_mass(f, rho_uniform, mass); - const auto gravity_solution = physics::grav_potential(f, args, rho_uniform); - constexpr double potential_tolerance = utils::APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING; - double local_max_abs_error = 0.0; - double local_max_rel_error = 0.0; + f.com = analysis::get_com(f, rho_uniform); + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); - const int num_elements_to_test = std::min(30, f.mesh->GetNE()); - for (int elem_id = 0; elem_id < num_elements_to_test; ++elem_id) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elem_id); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 2); - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(0); - transformation->SetIntPoint(&integration_point); + const auto gravity_solution = + physics::solve_gravity_field(f, args, rho_uniform, displacement); + constexpr double potential_tolerance = + utils::APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING; + double local_max_abs_error = 0.0; + double local_max_rel_error = 0.0; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate); - mfem::Vector x_physical; - f.mapping->GetPhysicalPoint(*transformation, integration_point, x_physical); + const int num_elements_to_test = std::min(30, f.mesh->GetNE()); + for (int elem_id = 0; elem_id < num_elements_to_test; ++elem_id) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elem_id); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 2); + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(0); + transformation->SetIntPoint(&integration_point); - const double radial_coordinate = x_physical.Norml2(); - if (radial_coordinate < 1.0e-9) { - continue; - } + mfem::Vector x_physical; + mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, + x_physical); - const double phi_analytic = -(utils::G * mass / (2.0 * std::pow(radius, 3.0))) * - (3.0 * radius * radius - radial_coordinate * radial_coordinate); - const double phi_fem = gravity_solution.phi.GetValue(elem_id, integration_point); - const double absolute_error = std::abs(phi_fem - phi_analytic); - const double relative_error = absolute_error / std::abs(phi_analytic); - - local_max_abs_error = std::max(local_max_abs_error, absolute_error); - local_max_rel_error = std::max(local_max_rel_error, relative_error); - CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); + const double radial_coordinate = x_physical.Norml2(); + if (radial_coordinate < 1.0e-9) { + continue; } - double global_max_abs_error = 0.0; - double global_max_rel_error = 0.0; - MPI_Comm communicator = f.densityFes->GetComm(); - MPI_Allreduce(&local_max_abs_error, &global_max_abs_error, 1, MPI_DOUBLE, MPI_MAX, communicator); - MPI_Allreduce(&local_max_rel_error, &global_max_rel_error, 1, MPI_DOUBLE, MPI_MAX, communicator); + const double phi_analytic = + -(utils::G * mass / (2.0 * std::pow(radius, 3.0))) * + (3.0 * radius * radius - radial_coordinate * radial_coordinate); + const double phi_fem = + gravity_solution.phi.GetValue(elem_id, integration_point); + const double absolute_error = std::abs(phi_fem - phi_analytic); + const double relative_error = absolute_error / std::abs(phi_analytic); - const int quadrature_order = get_gravity_quadrature_order(f); - const GravitationalEnergies energies = - compute_gravitational_energies(f, rho_uniform, gravity_solution, quadrature_order); - const double analytic_binding_energy = -(3.0 / 5.0) * utils::G * mass * mass / radius; - const double relative_binding_error = - std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_virial_error = - std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + local_max_abs_error = std::max(local_max_abs_error, absolute_error); + local_max_rel_error = std::max(local_max_rel_error, relative_error); + CHECK_THAT(relative_error, + Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); + } - INFO("Analytic binding energy = " << analytic_binding_energy); - INFO("Computed binding energy = " << energies.binding); - INFO("Computed virial energy = " << energies.virial); - INFO("Relative virial consistency error = " << relative_consistency_error); + double global_max_abs_error = 0.0; + double global_max_rel_error = 0.0; + MPI_Comm communicator = f.densityFes->GetComm(); + MPI_Allreduce(&local_max_abs_error, &global_max_abs_error, 1, MPI_DOUBLE, + MPI_MAX, communicator); + MPI_Allreduce(&local_max_rel_error, &global_max_rel_error, 1, MPI_DOUBLE, + MPI_MAX, communicator); - constexpr double energy_tolerance = 1.0e-5; - constexpr double consistency_tolerance = 1.0e-6; + const int quadrature_order = get_gravity_quadrature_order(f); + const GravitationalEnergies energies = compute_gravitational_energies( + f, rho_uniform, gravity_solution, quadrature_order); + const double analytic_binding_energy = + -(3.0 / 5.0) * utils::G * mass * mass / radius; + const double relative_binding_error = + std::abs(energies.binding - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_virial_error = + std::abs(energies.virial - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_consistency_error = + std::abs(energies.binding - energies.virial) / std::abs(energies.binding); - CHECK_THAT(global_max_rel_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); - CHECK_THAT(global_max_abs_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); - CHECK_THAT(relative_binding_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); - CHECK_THAT(relative_virial_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); - CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); + INFO("Analytic binding energy = " << analytic_binding_energy); + INFO("Computed binding energy = " << energies.binding); + INFO("Computed virial energy = " << energies.virial); + INFO("Relative virial consistency error = " << relative_consistency_error); + + constexpr double energy_tolerance = 1.0e-5; + constexpr double consistency_tolerance = 1.0e-6; + + CHECK_THAT(global_max_rel_error, + Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); + CHECK_THAT(global_max_abs_error, + Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); + CHECK_THAT(relative_binding_error, + Catch::Matchers::WithinAbs(0.0, energy_tolerance)); + CHECK_THAT(relative_virial_error, + Catch::Matchers::WithinAbs(0.0, energy_tolerance)); + CHECK_THAT(relative_consistency_error, + Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); } -TEST_CASE( - "Parabolic Density Virial Self-Consistency", - tags::gravity_analytic_initialization -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); +TEST_CASE("Parabolic Density Virial Self-Consistency", tags::gravity_analytic) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + *f.displacement = 0.0; - const double radius = utils::RADIUS; - const double mass = utils::MASS; - const double central_density = (15.0 * mass) / (8.0 * M_PI * std::pow(radius, 3.0)); + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; - auto parabolic_rho = [central_density, radius](const mfem::Vector &x) { - const double radial_coordinate = x.Norml2(); - return central_density * (1.0 - radial_coordinate * radial_coordinate / (radius * radius)); - }; + const double radius = utils::RADIUS; + const double mass = utils::MASS; + const double central_density = + (15.0 * mass) / (8.0 * M_PI * std::pow(radius, 3.0)); - std::unique_ptr rho_coeff; - if (f.has_mapping()) { - rho_coeff = std::make_unique(*f.mapping, parabolic_rho); - } else { - rho_coeff = std::make_unique(parabolic_rho); - } + auto parabolic_rho = [central_density, radius](const mfem::Vector &x) { + const double radial_coordinate = x.Norml2(); + return central_density * + (1.0 - radial_coordinate * radial_coordinate / (radius * radius)); + }; - mfem::GridFunction rho_grid(f.densityFes.get()); - rho_grid.ProjectCoefficient(*rho_coeff); - zero_vacuum_density(f, rho_grid); - analysis::conserve_mass(f, rho_grid, mass); + std::unique_ptr rho_coeff; + if (f.has_mapping()) { + rho_coeff = std::make_unique( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate, parabolic_rho); + } else { + rho_coeff = std::make_unique(parabolic_rho); + } - f.com = analysis::get_com(f, rho_grid); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); + mfem::GridFunction rho_grid(f.densityFes.get()); + rho_grid.ProjectCoefficient(*rho_coeff); + zero_vacuum_density(f, rho_grid); + analysis::conserve_mass(f, rho_grid, mass); - const auto gravity_solution = physics::grav_potential(f, args, rho_grid); - const int quadrature_order = get_gravity_quadrature_order(f); - const GravitationalEnergies energies = - compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order); - const double analytic_binding_energy = -(5.0 / 7.0) * utils::G * mass * mass / radius; - const double relative_binding_error = - std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_virial_error = - std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + f.com = analysis::get_com(f, rho_grid); + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); - INFO("Analytic binding energy = " << analytic_binding_energy); - INFO("Computed binding energy = " << energies.binding); - INFO("Computed virial energy = " << energies.virial); - INFO("Relative virial consistency error = " << relative_consistency_error); + const auto gravity_solution = + physics::solve_gravity_field(f, args, rho_grid, displacement); + const int quadrature_order = get_gravity_quadrature_order(f); + const GravitationalEnergies energies = compute_gravitational_energies( + f, rho_grid, gravity_solution, quadrature_order); + const double analytic_binding_energy = + -(5.0 / 7.0) * utils::G * mass * mass / radius; + const double relative_binding_error = + std::abs(energies.binding - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_virial_error = + std::abs(energies.virial - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_consistency_error = + std::abs(energies.binding - energies.virial) / std::abs(energies.binding); - constexpr double analytic_tolerance = 1.0e-5; - constexpr double consistency_tolerance = 1.0e-6; + INFO("Analytic binding energy = " << analytic_binding_energy); + INFO("Computed binding energy = " << energies.binding); + INFO("Computed virial energy = " << energies.virial); + INFO("Relative virial consistency error = " << relative_consistency_error); - CHECK_THAT(relative_binding_error, Catch::Matchers::WithinAbs(0.0, analytic_tolerance)); - CHECK_THAT(relative_virial_error, Catch::Matchers::WithinAbs(0.0, analytic_tolerance)); - CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); + constexpr double analytic_tolerance = 1.0e-5; + constexpr double consistency_tolerance = 1.0e-6; + + CHECK_THAT(relative_binding_error, + Catch::Matchers::WithinAbs(0.0, analytic_tolerance)); + CHECK_THAT(relative_virial_error, + Catch::Matchers::WithinAbs(0.0, analytic_tolerance)); + CHECK_THAT(relative_consistency_error, + Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); } -TEST_CASE( - "Rational Density Virial Self-Consistency", - tags::gravity_consistency_initialization -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); +TEST_CASE("Rational Density Virial Self-Consistency", + tags::gravity_consistency) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + *f.displacement = 0.0; - const double radius = utils::RADIUS; - const double mass = utils::MASS; + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; - // Larger values are more centrally concentrated and generally harder for a - // polynomial to represent. A regression would be considered if this test - // does not pass for concentrations <= 16.0. - constexpr double concentration = 16.0; - const double density_scale = mass / std::pow(radius, 3.0); + const double radius = utils::RADIUS; + const double mass = utils::MASS; - auto rational_rho = [radius, density_scale](const mfem::Vector &x) { - const double normalized_radius_squared = (x * x) / (radius * radius); - if (normalized_radius_squared >= 1.0) { - return 0.0; - } + // Larger values are more centrally concentrated and generally harder for a + // polynomial to represent. A regression would be considered if this test + // does not pass for concentrations <= 16.0. + constexpr double concentration = 16.0; + const double density_scale = mass / std::pow(radius, 3.0); - const double denominator = 1.0 + concentration * normalized_radius_squared; - return density_scale * (1.0 - normalized_radius_squared) / (denominator * denominator); - }; - - std::unique_ptr rho_coeff; - if (f.has_mapping()) { - rho_coeff = std::make_unique(*f.mapping, rational_rho); - } else { - rho_coeff = std::make_unique(rational_rho); + auto rational_rho = [radius, density_scale](const mfem::Vector &x) { + const double normalized_radius_squared = (x * x) / (radius * radius); + if (normalized_radius_squared >= 1.0) { + return 0.0; } - mfem::GridFunction rho_grid(f.densityFes.get()); - rho_grid.ProjectCoefficient(*rho_coeff); - zero_vacuum_density(f, rho_grid); - analysis::conserve_mass(f, rho_grid, mass); + const double denominator = 1.0 + concentration * normalized_radius_squared; + return density_scale * (1.0 - normalized_radius_squared) / + (denominator * denominator); + }; - f.com = analysis::get_com(f, rho_grid); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); + std::unique_ptr rho_coeff; + if (f.has_mapping()) { + rho_coeff = std::make_unique( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate, rational_rho); + } else { + rho_coeff = std::make_unique(rational_rho); + } - const auto gravity_solution = physics::grav_potential(f, args, rho_grid); - const int quadrature_order = get_gravity_quadrature_order(f); - const GravitationalEnergies energies = - compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order); + mfem::GridFunction rho_grid(f.densityFes.get()); + rho_grid.ProjectCoefficient(*rho_coeff); + zero_vacuum_density(f, rho_grid); + analysis::conserve_mass(f, rho_grid, mass); - REQUIRE(energies.binding < 0.0); - REQUIRE(energies.virial < 0.0); + f.com = analysis::get_com(f, rho_grid); + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); - const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); - INFO("W_bind = " << energies.binding); - INFO("W_vir = " << energies.virial); - INFO("Relative virial consistency error = " << relative_consistency_error); + const auto gravity_solution = + physics::solve_gravity_field(f, args, rho_grid, displacement); + const int quadrature_order = get_gravity_quadrature_order(f); + const GravitationalEnergies energies = compute_gravitational_energies( + f, rho_grid, gravity_solution, quadrature_order); - constexpr double virial_tolerance = 1.0e-5; - CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, virial_tolerance)); + REQUIRE(energies.binding < 0.0); + REQUIRE(energies.virial < 0.0); + + const double relative_consistency_error = + std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + INFO("W_bind = " << energies.binding); + INFO("W_vir = " << energies.virial); + INFO("Relative virial consistency error = " << relative_consistency_error); + + CHECK_THAT( + relative_consistency_error, + Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance)); } -TEST_CASE( - "Homogeneous Ellipsoid Analytic Gravity", - tags::gravity_analytic_initialization -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Homogeneous Ellipsoid Analytic Gravity", tags::gravity_analytic) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - const double radius = utils::RADIUS; - const double mass = utils::MASS; - constexpr double x_scale = 1.15; - constexpr double y_scale = 0.95; - constexpr double z_scale = 1.0 / (x_scale * y_scale); - assert(std::abs(x_scale * y_scale * z_scale - 1.0) < 1.0e-14); + const double radius = utils::RADIUS; + const double mass = utils::MASS; + constexpr double x_scale = 1.15; + constexpr double y_scale = 0.95; + constexpr double z_scale = 1.0 / (x_scale * y_scale); + assert(std::abs(x_scale * y_scale * z_scale - 1.0) < 1.0e-14); - const double semi_axis_x = x_scale * radius; - const double semi_axis_y = y_scale * radius; - const double semi_axis_z = z_scale * radius; + const double semi_axis_x = x_scale * radius; + const double semi_axis_y = y_scale * radius; + const double semi_axis_z = z_scale * radius; - auto affine_displacement = [](const mfem::Vector &x, mfem::Vector &displacement_value) { - displacement_value.SetSize(3); - displacement_value(0) = (x_scale - 1.0) * x(0); - displacement_value(1) = (y_scale - 1.0) * x(1); - displacement_value(2) = (z_scale - 1.0) * x(2); - }; + auto affine_displacement = [](const mfem::Vector &x, + mfem::Vector &displacement_value) { + displacement_value.SetSize(3); + displacement_value(0) = (x_scale - 1.0) * x(0); + displacement_value(1) = (y_scale - 1.0) * x(1); + displacement_value(2) = (z_scale - 1.0) * x(2); + }; - mfem::VectorFunctionCoefficient displacement_coeff(3, affine_displacement); - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement.ProjectCoefficient(displacement_coeff); - f.mapping->SetDisplacement(displacement); - physics::update_stiffness_matrix(f); + mfem::VectorFunctionCoefficient displacement_coeff(3, affine_displacement); + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement.ProjectCoefficient(displacement_coeff); + *f.displacement = displacement; - const double analytic_volume = (4.0 / 3.0) * M_PI * semi_axis_x * semi_axis_y * semi_axis_z; - const double density = mass / analytic_volume; + const double analytic_volume = + (4.0 / 3.0) * M_PI * semi_axis_x * semi_axis_y * semi_axis_z; + const double density = mass / analytic_volume; - mfem::GridFunction rho_grid(f.densityFes.get()); - rho_grid = density; - zero_vacuum_density(f, rho_grid); + mfem::GridFunction rho_grid(f.densityFes.get()); + rho_grid = density; + zero_vacuum_density(f, rho_grid); - const double projected_mass = analysis::domain_integrate_grid_function(f, rho_grid, utils::DOMAINS::STELLAR); - const double numerical_density = density * mass / projected_mass; - analysis::conserve_mass(f, rho_grid, mass); + const double projected_mass = analysis::domain_integrate_grid_function( + f, rho_grid, utils::DOMAINS::STELLAR); + const double numerical_density = density * mass / projected_mass; + analysis::conserve_mass(f, rho_grid, mass); - f.com = analysis::get_com(f, rho_grid); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); + f.com = analysis::get_com(f, rho_grid); + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); - const HomogeneousEllipsoidAnalytic analytic = - compute_homogeneous_ellipsoid_analytic(semi_axis_x, semi_axis_y, semi_axis_z); - const double coefficient_sum = analytic.coefficient_x + analytic.coefficient_y + analytic.coefficient_z; + const HomogeneousEllipsoidAnalytic analytic = + compute_homogeneous_ellipsoid_analytic(semi_axis_x, semi_axis_y, + semi_axis_z); + const double coefficient_sum = + analytic.coefficient_x + analytic.coefficient_y + analytic.coefficient_z; - INFO("A_x = " << analytic.coefficient_x); - INFO("A_y = " << analytic.coefficient_y); - INFO("A_z = " << analytic.coefficient_z); - INFO("A_x + A_y + A_z = " << coefficient_sum); - REQUIRE_THAT(coefficient_sum, Catch::Matchers::WithinAbs(2.0, 1.0e-11)); + INFO("A_x = " << analytic.coefficient_x); + INFO("A_y = " << analytic.coefficient_y); + INFO("A_z = " << analytic.coefficient_z); + INFO("A_x + A_y + A_z = " << coefficient_sum); + REQUIRE_THAT(coefficient_sum, Catch::Matchers::WithinAbs(2.0, 1.0e-11)); - mfem::DenseMatrix analytic_quadrupole(3, 3); - analytic_quadrupole = 0.0; - analytic_quadrupole(0, 0) = - (mass / 5.0) * (2.0 * semi_axis_x * semi_axis_x - semi_axis_y * semi_axis_y - semi_axis_z * semi_axis_z); - analytic_quadrupole(1, 1) = - (mass / 5.0) * (2.0 * semi_axis_y * semi_axis_y - semi_axis_x * semi_axis_x - semi_axis_z * semi_axis_z); - analytic_quadrupole(2, 2) = - (mass / 5.0) * (2.0 * semi_axis_z * semi_axis_z - semi_axis_x * semi_axis_x - semi_axis_y * semi_axis_y); + mfem::DenseMatrix analytic_quadrupole(3, 3); + analytic_quadrupole = 0.0; + analytic_quadrupole(0, 0) = + (mass / 5.0) * (2.0 * semi_axis_x * semi_axis_x - + semi_axis_y * semi_axis_y - semi_axis_z * semi_axis_z); + analytic_quadrupole(1, 1) = + (mass / 5.0) * (2.0 * semi_axis_y * semi_axis_y - + semi_axis_x * semi_axis_x - semi_axis_z * semi_axis_z); + analytic_quadrupole(2, 2) = + (mass / 5.0) * (2.0 * semi_axis_z * semi_axis_z - + semi_axis_x * semi_axis_x - semi_axis_y * semi_axis_y); - mfem::DenseMatrix quadrupole_difference(f.Q); - quadrupole_difference -= analytic_quadrupole; - const double relative_quadrupole_error = quadrupole_difference.FNorm() / analytic_quadrupole.FNorm(); - INFO("Relative quadrupole error = " << relative_quadrupole_error); + mfem::DenseMatrix quadrupole_difference(f.Q); + quadrupole_difference -= analytic_quadrupole; + const double relative_quadrupole_error = + quadrupole_difference.FNorm() / analytic_quadrupole.FNorm(); + INFO("Relative quadrupole error = " << relative_quadrupole_error); - HomogeneousEllipsoidHDivCoefficient analytic_field_coefficient(*f.mapping, numerical_density, analytic); - mfem::ParGridFunction analytic_field_projection(f.gravityFluxFes.get()); - analytic_field_projection = 0.0; + HomogeneousEllipsoidHDivCoefficient analytic_field_coefficient( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate, numerical_density, analytic); + mfem::ParGridFunction analytic_field_projection(f.gravityFluxFes.get()); + analytic_field_projection = 0.0; - for (int i = 0; i < f.mesh->attributes.Size(); ++i) { - const int attribute = f.mesh->attributes[i]; + for (int i = 0; i < f.mesh->attributes.Size(); ++i) { + const int attribute = f.mesh->attributes[i]; - if (attribute != 3) { - analytic_field_projection.ProjectCoefficient(analytic_field_coefficient, attribute); - } + if (attribute != 3) { + analytic_field_projection.ProjectCoefficient(analytic_field_coefficient, + attribute); + } + } + + const auto gravity_solution = + physics::solve_gravity_field(f, args, rho_grid, displacement); + const int quadrature_order = get_gravity_quadrature_order(f); + double local_field_error_squared = 0.0; + double local_field_norm_squared = 0.0; + double local_projection_error_squared = 0.0; + double local_gravity_projection_difference_squared = 0.0; + + mfem::Vector x_physical(3); + mfem::Vector grad_phi_element(3); + mfem::Vector grad_phi_physical(3); + mfem::Vector grad_phi_analytic(3); + mfem::Vector grad_phi_difference(3); + mfem::Vector projected_field_element(3); + mfem::Vector projected_field_physical(3); + mfem::Vector projected_field_difference(3); + mfem::Vector gravity_projection_difference(3); + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate); + + for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) { + if (f.mesh->GetAttribute(elem_id) == 3) { + continue; } - const auto gravity_solution = physics::grav_potential(f, args, rho_grid); - const int quadrature_order = get_gravity_quadrature_order(f); - double local_field_error_squared = 0.0; - double local_field_norm_squared = 0.0; - double local_projection_error_squared = 0.0; - double local_gravity_projection_difference_squared = 0.0; + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(elem_id); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - mfem::Vector x_physical(3); - mfem::Vector grad_phi_element(3); - mfem::Vector grad_phi_physical(3); - mfem::Vector grad_phi_analytic(3); - mfem::Vector grad_phi_difference(3); - mfem::Vector projected_field_element(3); - mfem::Vector projected_field_physical(3); - mfem::Vector projected_field_difference(3); - mfem::DenseMatrix map_jacobian(3, 3); - mfem::Vector gravity_projection_difference(3); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); - for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) { - if (f.mesh->GetAttribute(elem_id) == 3) { - continue; - } + mapping::VolumeMappingContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluateVolume(*transformation, integration_point, + mapping_context) == + mapping::MappingStatus::valid, + "Ellipsoid field comparison encountered an invalid mapping."); + const double map_determinant = + mapping_context.mapping.mapping_determinant; + const double weight = mapping_context.quadrature.weight; + x_physical = mapping_context.mapping.physical_position; + gravity_solution.gradPhi.GetVectorValue(elem_id, integration_point, + grad_phi_element); + const mfem::DenseMatrix &map_jacobian = + mapping_context.mapping.mapping_jacobian; + map_jacobian.Mult(grad_phi_element, grad_phi_physical); + grad_phi_physical /= map_determinant; - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elem_id); - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + analytic_field_projection.GetVectorValue(elem_id, integration_point, + projected_field_element); + map_jacobian.Mult(projected_field_element, projected_field_physical); + projected_field_physical /= map_determinant; - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); + grad_phi_analytic(0) = 2.0 * M_PI * utils::G * numerical_density * + analytic.coefficient_x * x_physical(0); + grad_phi_analytic(1) = 2.0 * M_PI * utils::G * numerical_density * + analytic.coefficient_y * x_physical(1); + grad_phi_analytic(2) = 2.0 * M_PI * utils::G * numerical_density * + analytic.coefficient_z * x_physical(2); - const double map_determinant = f.mapping->ComputeDetJ(*transformation, integration_point); - MFEM_VERIFY(map_determinant > 0.0, "Domain mapping has a non-positive Jacobian determinant."); + projected_field_difference = projected_field_physical; + projected_field_difference -= grad_phi_analytic; + local_projection_error_squared += + (projected_field_difference * projected_field_difference) * weight; - const double weight = transformation->Weight() * integration_point.weight * map_determinant; - f.mapping->GetPhysicalPoint(*transformation, integration_point, x_physical); - gravity_solution.gradPhi.GetVectorValue(elem_id, integration_point, grad_phi_element); - f.mapping->ComputeJacobian(*transformation, map_jacobian); - map_jacobian.Mult(grad_phi_element, grad_phi_physical); - grad_phi_physical /= map_determinant; + gravity_projection_difference = grad_phi_physical; + gravity_projection_difference -= projected_field_physical; + local_gravity_projection_difference_squared += + (gravity_projection_difference * gravity_projection_difference) * + weight; - analytic_field_projection.GetVectorValue(elem_id, integration_point, projected_field_element); - map_jacobian.Mult(projected_field_element, projected_field_physical); - projected_field_physical /= map_determinant; - - grad_phi_analytic(0) = 2.0 * M_PI * utils::G * numerical_density * analytic.coefficient_x * x_physical(0); - grad_phi_analytic(1) = 2.0 * M_PI * utils::G * numerical_density * analytic.coefficient_y * x_physical(1); - grad_phi_analytic(2) = 2.0 * M_PI * utils::G * numerical_density * analytic.coefficient_z * x_physical(2); - - projected_field_difference = projected_field_physical; - projected_field_difference -= grad_phi_analytic; - local_projection_error_squared += (projected_field_difference * projected_field_difference) * weight; - - gravity_projection_difference = grad_phi_physical; - gravity_projection_difference -= projected_field_physical; - local_gravity_projection_difference_squared += - (gravity_projection_difference * gravity_projection_difference) * weight; - - grad_phi_difference = grad_phi_physical; - grad_phi_difference -= grad_phi_analytic; - local_field_error_squared += (grad_phi_difference * grad_phi_difference) * weight; - local_field_norm_squared += (grad_phi_analytic * grad_phi_analytic) * weight; - } + grad_phi_difference = grad_phi_physical; + grad_phi_difference -= grad_phi_analytic; + local_field_error_squared += + (grad_phi_difference * grad_phi_difference) * weight; + local_field_norm_squared += + (grad_phi_analytic * grad_phi_analytic) * weight; } + } - double global_field_error_squared = 0.0; - double global_field_norm_squared = 0.0; - double global_projection_error_squared = 0.0; - double global_gravity_projection_difference_squared = 0.0; - MPI_Comm communicator = f.densityFes->GetComm(); - MPI_Allreduce(&local_field_error_squared, &global_field_error_squared, 1, MPI_DOUBLE, MPI_SUM, communicator); - MPI_Allreduce(&local_field_norm_squared, &global_field_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator); - MPI_Allreduce( - &local_projection_error_squared, &global_projection_error_squared, 1, MPI_DOUBLE, MPI_SUM, communicator - ); - MPI_Allreduce( - &local_gravity_projection_difference_squared, &global_gravity_projection_difference_squared, 1, MPI_DOUBLE, - MPI_SUM, communicator - ); + double global_field_error_squared = 0.0; + double global_field_norm_squared = 0.0; + double global_projection_error_squared = 0.0; + double global_gravity_projection_difference_squared = 0.0; + MPI_Comm communicator = f.densityFes->GetComm(); + MPI_Allreduce(&local_field_error_squared, &global_field_error_squared, 1, + MPI_DOUBLE, MPI_SUM, communicator); + MPI_Allreduce(&local_field_norm_squared, &global_field_norm_squared, 1, + MPI_DOUBLE, MPI_SUM, communicator); + MPI_Allreduce(&local_projection_error_squared, + &global_projection_error_squared, 1, MPI_DOUBLE, MPI_SUM, + communicator); + MPI_Allreduce(&local_gravity_projection_difference_squared, + &global_gravity_projection_difference_squared, 1, MPI_DOUBLE, + MPI_SUM, communicator); - const double relative_field_error = std::sqrt(global_field_error_squared / global_field_norm_squared); - const GravitationalEnergies energies = - compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order); - const double analytic_binding_energy = -(3.0 / 10.0) * utils::G * mass * mass * analytic.energy_kernel; - const double relative_binding_energy_error = - std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_virial_energy_error = - std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); - const double relative_projection_error = std::sqrt(global_projection_error_squared / global_field_norm_squared); - const double gravity_to_projection_error_ratio = relative_projection_error > 0.0 - ? relative_field_error / relative_projection_error - : std::numeric_limits::infinity(); - const double relative_gravity_projection_difference = - std::sqrt(global_gravity_projection_difference_squared / global_field_norm_squared); - const double projection_gap_ratio = relative_gravity_projection_difference / relative_projection_error; + const double relative_field_error = + std::sqrt(global_field_error_squared / global_field_norm_squared); + const GravitationalEnergies energies = compute_gravitational_energies( + f, rho_grid, gravity_solution, quadrature_order); + const double analytic_binding_energy = + -(3.0 / 10.0) * utils::G * mass * mass * analytic.energy_kernel; + const double relative_binding_energy_error = + std::abs(energies.binding - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_virial_energy_error = + std::abs(energies.virial - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_consistency_error = + std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + const double relative_projection_error = + std::sqrt(global_projection_error_squared / global_field_norm_squared); + const double gravity_to_projection_error_ratio = + relative_projection_error > 0.0 + ? relative_field_error / relative_projection_error + : std::numeric_limits::infinity(); + const double relative_gravity_projection_difference = std::sqrt( + global_gravity_projection_difference_squared / global_field_norm_squared); + const double projection_gap_ratio = + relative_gravity_projection_difference / relative_projection_error; - INFO("Analytic binding energy = " << analytic_binding_energy); - INFO("Computed binding energy = " << energies.binding); - INFO("Computed virial energy = " << energies.virial); - INFO("Relative field L2 error = " << relative_field_error); - INFO("Relative binding energy error = " << relative_binding_energy_error); - INFO("Relative virial energy error = " << relative_virial_energy_error); - INFO("Relative virial consistency error = " << relative_consistency_error); - INFO("Relative gravity-to-RT-projection difference = " << relative_gravity_projection_difference); - INFO("Gravity-to-projection gap ratio = " << projection_gap_ratio); + INFO("Analytic binding energy = " << analytic_binding_energy); + INFO("Computed binding energy = " << energies.binding); + INFO("Computed virial energy = " << energies.virial); + INFO("Relative field L2 error = " << relative_field_error); + INFO("Relative binding energy error = " << relative_binding_energy_error); + INFO("Relative virial energy error = " << relative_virial_energy_error); + INFO("Relative virial consistency error = " << relative_consistency_error); + INFO("Relative gravity-to-RT-projection difference = " + << relative_gravity_projection_difference); + INFO("Gravity-to-projection gap ratio = " << projection_gap_ratio); - INFO("Relative RT projection L2 error = " << relative_projection_error); - INFO("Gravity-to-projection error ratio = " << gravity_to_projection_error_ratio); - REQUIRE(std::isfinite(relative_projection_error)); + INFO("Relative RT projection L2 error = " << relative_projection_error); + INFO("Gravity-to-projection error ratio = " + << gravity_to_projection_error_ratio); + REQUIRE(std::isfinite(relative_projection_error)); - constexpr double quadrupole_tolerance = 2.0e-4; - constexpr double field_tolerance = 1.0e-5; - constexpr double energy_tolerance = 1.0e-5; - constexpr double consistency_tolerance = 1.0e-5; + // The RT projection itself is accurate to approximately 8.8e-4 on this + // mesh. The solved field should remain close to that best representable + // field, while the integrated energies have a substantially lower floor. + constexpr double quadrupole_tolerance = 2.0e-4; + constexpr double field_tolerance = 1.0e-3; + constexpr double binding_energy_tolerance = 1.0e-5; + constexpr double virial_energy_tolerance = 5.0e-5; + constexpr double consistency_tolerance = 5.0e-5; + constexpr double projection_gap_tolerance = 0.3; + constexpr double projection_ratio_tolerance = 1.05; - CHECK_THAT(relative_quadrupole_error, Catch::Matchers::WithinAbs(0.0, quadrupole_tolerance)); - CHECK_THAT(relative_field_error, Catch::Matchers::WithinAbs(0.0, field_tolerance)); - CHECK_THAT(relative_binding_energy_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); - CHECK_THAT(relative_virial_energy_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); - CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); + CHECK_THAT(relative_quadrupole_error, + Catch::Matchers::WithinAbs(0.0, quadrupole_tolerance)); + CHECK_THAT(relative_field_error, + Catch::Matchers::WithinAbs(0.0, field_tolerance)); + CHECK_THAT(relative_binding_energy_error, + Catch::Matchers::WithinAbs(0.0, binding_energy_tolerance)); + CHECK_THAT(relative_virial_energy_error, + Catch::Matchers::WithinAbs(0.0, virial_energy_tolerance)); + CHECK_THAT(relative_consistency_error, + Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); + CHECK(projection_gap_ratio < projection_gap_tolerance); + CHECK(gravity_to_projection_error_ratio < projection_ratio_tolerance); } -TEST_CASE( - "Deformed Rational Density Virial Self-Consistency", - tags::gravity_consistency_initialization -) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); +TEST_CASE("Deformed Rational Density Virial Self-Consistency", + tags::gravity_consistency) { + auto args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - const double radius = utils::RADIUS; - const double mass = utils::MASS; - constexpr double x_scale = 1.15; - constexpr double y_scale = 0.95; - constexpr double z_scale = 1.0 / (x_scale * y_scale); - assert(std::abs(x_scale * y_scale * z_scale - 1.0) < 1.0e-14); + const double radius = utils::RADIUS; + const double mass = utils::MASS; + constexpr double x_scale = 1.15; + constexpr double y_scale = 0.95; + constexpr double z_scale = 1.0 / (x_scale * y_scale); + assert(std::abs(x_scale * y_scale * z_scale - 1.0) < 1.0e-14); - const double semi_axis_x = x_scale * radius; - const double semi_axis_y = y_scale * radius; - const double semi_axis_z = z_scale * radius; + const double semi_axis_x = x_scale * radius; + const double semi_axis_y = y_scale * radius; + const double semi_axis_z = z_scale * radius; - auto affine_displacement = [](const mfem::Vector &x, mfem::Vector &displacement_value) { - displacement_value.SetSize(3); - displacement_value(0) = (x_scale - 1.0) * x(0); - displacement_value(1) = (y_scale - 1.0) * x(1); - displacement_value(2) = (z_scale - 1.0) * x(2); - }; + auto affine_displacement = [](const mfem::Vector &x, + mfem::Vector &displacement_value) { + displacement_value.SetSize(3); + displacement_value(0) = (x_scale - 1.0) * x(0); + displacement_value(1) = (y_scale - 1.0) * x(1); + displacement_value(2) = (z_scale - 1.0) * x(2); + }; - mfem::VectorFunctionCoefficient displacement_coeff(3, affine_displacement); - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement.ProjectCoefficient(displacement_coeff); - f.mapping->SetDisplacement(displacement); - physics::update_stiffness_matrix(f); + mfem::VectorFunctionCoefficient displacement_coeff(3, affine_displacement); + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement.ProjectCoefficient(displacement_coeff); + *f.displacement = displacement; - constexpr double concentration = 16.0; - const double density_scale = mass / std::pow(radius, 3.0); + constexpr double concentration = 16.0; + const double density_scale = mass / std::pow(radius, 3.0); - auto ellipsoidal_rho = [semi_axis_x, semi_axis_y, semi_axis_z, density_scale](const mfem::Vector &x) { - const double ellipsoidal_radius_squared = x(0) * x(0) / (semi_axis_x * semi_axis_x) + - x(1) * x(1) / (semi_axis_y * semi_axis_y) + - x(2) * x(2) / (semi_axis_z * semi_axis_z); + auto ellipsoidal_rho = [semi_axis_x, semi_axis_y, semi_axis_z, + density_scale](const mfem::Vector &x) { + const double ellipsoidal_radius_squared = + x(0) * x(0) / (semi_axis_x * semi_axis_x) + + x(1) * x(1) / (semi_axis_y * semi_axis_y) + + x(2) * x(2) / (semi_axis_z * semi_axis_z); - if (ellipsoidal_radius_squared >= 1.0) { - return 0.0; - } - - const double denominator = 1.0 + concentration * ellipsoidal_radius_squared; - return density_scale * (1.0 - ellipsoidal_radius_squared) / (denominator * denominator); - }; - - std::unique_ptr rho_coeff; - if (f.has_mapping()) { - rho_coeff = std::make_unique(*f.mapping, ellipsoidal_rho); - } else { - rho_coeff = std::make_unique(ellipsoidal_rho); + if (ellipsoidal_radius_squared >= 1.0) { + return 0.0; } - mfem::GridFunction rho_grid(f.densityFes.get()); - rho_grid.ProjectCoefficient(*rho_coeff); - zero_vacuum_density(f, rho_grid); - analysis::conserve_mass(f, rho_grid, mass); + const double denominator = 1.0 + concentration * ellipsoidal_radius_squared; + return density_scale * (1.0 - ellipsoidal_radius_squared) / + (denominator * denominator); + }; - f.com = analysis::get_com(f, rho_grid); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); + std::unique_ptr rho_coeff; + if (f.has_mapping()) { + rho_coeff = std::make_unique( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate, ellipsoidal_rho); + } else { + rho_coeff = std::make_unique(ellipsoidal_rho); + } - const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); - INFO("Normalized quadrupole = " << normalized_quadrupole); - REQUIRE(normalized_quadrupole > 1.0e-3); + mfem::GridFunction rho_grid(f.densityFes.get()); + rho_grid.ProjectCoefficient(*rho_coeff); + zero_vacuum_density(f, rho_grid); + analysis::conserve_mass(f, rho_grid, mass); - const auto gravity_solution = physics::grav_potential(f, args, rho_grid); - const int quadrature_order = get_gravity_quadrature_order(f); - const GravitationalEnergies energies = - compute_gravitational_energies(f, rho_grid, gravity_solution, quadrature_order); + f.com = analysis::get_com(f, rho_grid); + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); - REQUIRE(energies.binding < 0.0); - REQUIRE(energies.virial < 0.0); + const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); + INFO("Normalized quadrupole = " << normalized_quadrupole); + REQUIRE(normalized_quadrupole > 1.0e-3); - const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); - INFO("W_bind = " << energies.binding); - INFO("W_vir = " << energies.virial); - INFO("Relative virial consistency error = " << relative_consistency_error); + const auto gravity_solution = + physics::solve_gravity_field(f, args, rho_grid, displacement); + const int quadrature_order = get_gravity_quadrature_order(f); + const GravitationalEnergies energies = compute_gravitational_energies( + f, rho_grid, gravity_solution, quadrature_order); - constexpr double virial_tolerance = 1.0e-5; - CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, virial_tolerance)); + REQUIRE(energies.binding < 0.0); + REQUIRE(energies.virial < 0.0); + + const double relative_consistency_error = + std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + INFO("W_bind = " << energies.binding); + INFO("W_vir = " << energies.virial); + INFO("Relative virial consistency error = " << relative_consistency_error); + + CHECK_THAT( + relative_consistency_error, + Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance)); } -TEST_CASE( - "New Gravity Potential Matches Uniform Sphere Analytic", - tags::gravity_analytic -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); +TEST_CASE("Gravity Field Matches Uniform Sphere Analytic", + tags::gravity_analytic) { + auto args = test_utils::setup_args(); + args.p.rtol = 1.0e-13; + args.p.max_iters = std::max(args.p.max_iters, 1000); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement = 0.0; + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; - const double radius = utils::RADIUS; - const double mass = utils::MASS; - const double analytic_volume = (4.0 / 3.0) * M_PI * std::pow(radius, 3.0); - const double density = mass / analytic_volume; + const double radius = utils::RADIUS; + const double mass = utils::MASS; + const double analytic_volume = (4.0 / 3.0) * M_PI * std::pow(radius, 3.0); + const double density = mass / analytic_volume; - mfem::GridFunction rho_uniform(f.densityFes.get()); - rho_uniform = density; - zero_vacuum_density(f, rho_uniform); - analysis::conserve_mass(f, rho_uniform, mass); + mfem::GridFunction rho_uniform(f.densityFes.get()); + rho_uniform = density; + zero_vacuum_density(f, rho_uniform); + analysis::conserve_mass(f, rho_uniform, mass); - f.com = analysis::get_com(f, rho_uniform); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); + f.com = analysis::get_com(f, rho_uniform); + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); - const physics::GravitySolution gravity_solution = physics::grav_potential_new(f, args, rho_uniform, displacement); + const physics::GravitySolution gravity_solution = + physics::solve_gravity_field(f, args, rho_uniform, displacement); - constexpr double potential_tolerance = utils::APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING; - double local_maximum_absolute_error = 0.0; - double local_maximum_relative_error = 0.0; + constexpr double potential_tolerance = + utils::APPROX_MAX_ACCEPTABLE_POTENTIAL_ERROR_SI_BURNING; + double local_maximum_absolute_error = 0.0; + double local_maximum_relative_error = 0.0; + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate); - const int elements_to_test = std::min(30, f.mesh->GetNE()); + const int elements_to_test = std::min(30, f.mesh->GetNE()); - for (int element_id = 0; element_id < elements_to_test; ++element_id) { - if (f.mesh->GetAttribute(element_id) == 3) { - continue; - } - - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 2); - const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(0); - transformation->SetIntPoint(&integration_point); - - mfem::Vector physical_position; - f.mapping->GetPhysicalPoint(*transformation, integration_point, physical_position); - - const double radial_coordinate = physical_position.Norml2(); - - if (radial_coordinate < 1.0e-9) { - continue; - } - - const double analytic_potential = -(utils::G * mass / (2.0 * std::pow(radius, 3.0))) * - (3.0 * radius * radius - radial_coordinate * radial_coordinate); - - const double computed_potential = gravity_solution.phi.GetValue(element_id, integration_point); - const double absolute_error = std::abs(computed_potential - analytic_potential); - const double relative_error = absolute_error / std::abs(analytic_potential); - - local_maximum_absolute_error = std::max(local_maximum_absolute_error, absolute_error); - local_maximum_relative_error = std::max(local_maximum_relative_error, relative_error); + for (int element_id = 0; element_id < elements_to_test; ++element_id) { + if (f.mesh->GetAttribute(element_id) == 3) { + continue; } - double global_maximum_absolute_error = 0.0; - double global_maximum_relative_error = 0.0; + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 2); + const mfem::IntegrationPoint &integration_point = + integration_rule.IntPoint(0); + transformation->SetIntPoint(&integration_point); - MPI_Allreduce( - &local_maximum_absolute_error, &global_maximum_absolute_error, 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm() - ); + mfem::Vector physical_position; + mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, + physical_position); - MPI_Allreduce( - &local_maximum_relative_error, &global_maximum_relative_error, 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm() - ); + const double radial_coordinate = physical_position.Norml2(); - const int quadrature_order = get_gravity_quadrature_order(f); - const GravitationalEnergies energies = - compute_gravitational_energies(f, rho_uniform, gravity_solution, quadrature_order); - const double analytic_binding_energy = -(3.0 / 5.0) * utils::G * mass * mass / radius; - const double relative_binding_error = - std::abs(energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_virial_error = - std::abs(energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy); - const double relative_consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + if (radial_coordinate < 1.0e-9) { + continue; + } - INFO("Global maximum absolute potential error = " << global_maximum_absolute_error); - INFO("Global maximum relative potential error = " << global_maximum_relative_error); - INFO("Analytic binding energy = " << analytic_binding_energy); - INFO("New-solver binding energy = " << energies.binding); - INFO("New-solver virial energy = " << energies.virial); - INFO("Relative binding-energy error = " << relative_binding_error); - INFO("Relative virial-energy error = " << relative_virial_error); - INFO("Relative virial consistency error = " << relative_consistency_error); + const double analytic_potential = + -(utils::G * mass / (2.0 * std::pow(radius, 3.0))) * + (3.0 * radius * radius - radial_coordinate * radial_coordinate); - REQUIRE(energies.binding < 0.0); - REQUIRE(energies.virial < 0.0); + const double computed_potential = + gravity_solution.phi.GetValue(element_id, integration_point); + const double absolute_error = + std::abs(computed_potential - analytic_potential); + const double relative_error = absolute_error / std::abs(analytic_potential); - constexpr double energy_tolerance = 1.0e-5; - constexpr double consistency_tolerance = 1.0e-6; + local_maximum_absolute_error = + std::max(local_maximum_absolute_error, absolute_error); + local_maximum_relative_error = + std::max(local_maximum_relative_error, relative_error); + } - CHECK_THAT(global_maximum_relative_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); - CHECK_THAT(global_maximum_absolute_error, Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); - CHECK_THAT(relative_binding_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); - CHECK_THAT(relative_virial_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); - CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); + double global_maximum_absolute_error = 0.0; + double global_maximum_relative_error = 0.0; + + MPI_Allreduce(&local_maximum_absolute_error, &global_maximum_absolute_error, + 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm()); + + MPI_Allreduce(&local_maximum_relative_error, &global_maximum_relative_error, + 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm()); + + const int quadrature_order = get_gravity_quadrature_order(f); + const GravitationalEnergies energies = compute_gravitational_energies( + f, rho_uniform, gravity_solution, quadrature_order); + const double analytic_binding_energy = + -(3.0 / 5.0) * utils::G * mass * mass / radius; + const double relative_binding_error = + std::abs(energies.binding - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_virial_error = + std::abs(energies.virial - analytic_binding_energy) / + std::abs(analytic_binding_energy); + const double relative_consistency_error = + std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + + INFO("Global maximum absolute potential error = " + << global_maximum_absolute_error); + INFO("Global maximum relative potential error = " + << global_maximum_relative_error); + INFO("Analytic binding energy = " << analytic_binding_energy); + INFO("New-solver binding energy = " << energies.binding); + INFO("New-solver virial energy = " << energies.virial); + INFO("Relative binding-energy error = " << relative_binding_error); + INFO("Relative virial-energy error = " << relative_virial_error); + INFO("Relative virial consistency error = " << relative_consistency_error); + + REQUIRE(energies.binding < 0.0); + REQUIRE(energies.virial < 0.0); + + constexpr double energy_tolerance = 1.0e-5; + constexpr double consistency_tolerance = 1.0e-6; + + CHECK_THAT(global_maximum_relative_error, + Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); + CHECK_THAT(global_maximum_absolute_error, + Catch::Matchers::WithinAbs(0.0, 0.1 * potential_tolerance)); + CHECK_THAT(relative_binding_error, + Catch::Matchers::WithinAbs(0.0, energy_tolerance)); + CHECK_THAT(relative_virial_error, + Catch::Matchers::WithinAbs(0.0, energy_tolerance)); + CHECK_THAT(relative_consistency_error, + Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); } -TEST_CASE( - "New Gravity Potential Matches Legacy Solver On Homogeneous Ellipsoid", - tags::gravity_legacy -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); +TEST_CASE("Gravity Field Deformed Rational Density Virial Self-Consistency", + tags::gravity_consistency) { + auto args = test_utils::setup_args(); + args.p.rtol = 1.0e-13; + args.p.max_iters = std::max(args.p.max_iters, 1000); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - const double radius = utils::RADIUS; - const double mass = utils::MASS; + const double radius = utils::RADIUS; + const double mass = utils::MASS; - constexpr double x_scale = 1.15; - constexpr double y_scale = 0.95; - constexpr double z_scale = 1.0 / (x_scale * y_scale); + constexpr double x_scale = 1.15; + constexpr double y_scale = 0.95; + constexpr double z_scale = 1.0 / (x_scale * y_scale); - REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14)); + REQUIRE_THAT(x_scale * y_scale * z_scale, + Catch::Matchers::WithinAbs(1.0, 1.0e-14)); - const double semi_axis_x = x_scale * radius; - const double semi_axis_y = y_scale * radius; - const double semi_axis_z = z_scale * radius; + const double semi_axis_x = x_scale * radius; + const double semi_axis_y = y_scale * radius; + const double semi_axis_z = z_scale * radius; - auto affine_displacement = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = (x_scale - 1.0) * position(0); - value(1) = (y_scale - 1.0) * position(1); - value(2) = (z_scale - 1.0) * position(2); - }; + auto affine_displacement = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = (x_scale - 1.0) * position(0); + value(1) = (y_scale - 1.0) * position(1); + value(2) = (z_scale - 1.0) * position(2); + }; - mfem::VectorFunctionCoefficient displacement_coefficient(3, affine_displacement); - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement.ProjectCoefficient(displacement_coefficient); + mfem::VectorFunctionCoefficient displacement_coefficient(3, + affine_displacement); - f.mapping->SetDisplacement(displacement); - physics::update_stiffness_matrix(f); + mfem::ParGridFunction displacement(f.displacementFes.get()); - REQUIRE(f.gravityContext.block_A != nullptr); - REQUIRE(f.gravityContext.source_form != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + displacement.ProjectCoefficient(displacement_coefficient); - const double analytic_volume = (4.0 / 3.0) * M_PI * semi_axis_x * semi_axis_y * semi_axis_z; - const double density = mass / analytic_volume; + *f.displacement = displacement; - mfem::GridFunction rho_grid(f.densityFes.get()); - rho_grid = density; - zero_vacuum_density(f, rho_grid); + constexpr double concentration = 16.0; + const double density_scale = mass / std::pow(radius, 3.0); - const double projected_mass = analysis::domain_integrate_grid_function(f, rho_grid, utils::DOMAINS::STELLAR); - const double numerical_density = density * mass / projected_mass; - analysis::conserve_mass(f, rho_grid, mass); + auto ellipsoidal_density = [semi_axis_x, semi_axis_y, semi_axis_z, + density_scale](const mfem::Vector &position) { + const double ellipsoidal_radius_squared = + position(0) * position(0) / (semi_axis_x * semi_axis_x) + + position(1) * position(1) / (semi_axis_y * semi_axis_y) + + position(2) * position(2) / (semi_axis_z * semi_axis_z); - f.com = analysis::get_com(f, rho_grid); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); - - const physics::GravitySolution legacy_solution = physics::grav_potential(f, args, rho_grid); - const physics::GravitySolution new_solution = physics::grav_potential_new(f, args, rho_grid, displacement); - - using gravity_form = utils::blocks::gravity_field_form; - - constexpr auto gravity_gradient_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); - - constexpr auto gravity_poisson_residual_block = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - const field::FieldDofMap density_map = field::make_field_dof_map(*f.densityFes); - const field::FieldDofMap displacement_map = - field::make_field_dof_map(*f.displacementFes); - const field::FieldDofMap gravity_flux_map = - field::make_field_dof_map(*f.gravityFluxFes); - const field::FieldDofMap gravity_potential_map = - field::make_field_dof_map(*f.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 utils::blocks::form_layout gravity_layout(value_sizes, residual_sizes); - const int gravity_system_size = gravity_layout.residual_offsets().Last(); - const int gravity_gradient_size = gravity_layout.size(gravity_gradient_residual_block); - const int gravity_poisson_size = gravity_layout.size(gravity_poisson_residual_block); - - mfem::Vector density_true; - mfem::Vector displacement_true; - gravity_test_get_true_dofs(*f.densityFes, rho_grid, density_true); - gravity_test_get_true_dofs(*f.displacementFes, displacement, displacement_true); - - REQUIRE(density_true.Size() == f.densityFes->GetTrueVSize()); - REQUIRE(displacement_true.Size() == f.displacementFes->GetTrueVSize()); - const mfem::Vector reduced_density = density_map.gather(density_true); - const mfem::Vector reduced_displacement = displacement_map.gather(displacement_true); - - operators::context::gravity_field::GravityFieldLinearizationContext residual_linearization_context( - f, *f.domainMapperStateless - ); - - operators::GravityFieldJacobianOperator residual_jacobian( - f, *f.domainMapperStateless, residual_linearization_context, gravity_layout.value_offsets(), - gravity_layout.residual_offsets() - ); - - operators::GravityFieldOperator residual_gravity_operator( - f, *f.domainMapperStateless, residual_linearization_context, gravity_layout.value_offsets(), residual_jacobian - ); - - operators::context::gravity_field::GravityFieldGeometryContext residual_geometry_context( - f, *f.domainMapperStateless - ); - - operators::ReducedGravityFieldOperator new_reduced_operator( - residual_gravity_operator, residual_geometry_context, reduced_displacement - ); - - REQUIRE(new_reduced_operator.Width() == gravity_system_size); - REQUIRE(new_reduced_operator.Height() == gravity_system_size); - - mfem::Vector new_right_hand_side; - new_reduced_operator.BuildRightHandSide(reduced_density, new_right_hand_side); - - REQUIRE(new_right_hand_side.Size() == gravity_system_size); - - REQUIRE(f.gravityContext.source_form->Width() == reduced_density.Size()); - REQUIRE(f.gravityContext.source_form->Height() == gravity_poisson_size); - - mfem::Vector legacy_source_action(f.gravityContext.source_form->Height()); - legacy_source_action = 0.0; - - f.gravityContext.source_form->Mult(reduced_density, legacy_source_action); - - REQUIRE(legacy_source_action.Size() == gravity_poisson_size); - - mfem::BlockVector legacy_right_hand_side(gravity_layout.residual_offsets()); - legacy_right_hand_side = 0.0; - legacy_right_hand_side.GetBlock(gravity_poisson_residual_block) = legacy_source_action; - - REQUIRE(legacy_right_hand_side.Size() == gravity_system_size); - - MPI_Comm communicator = f.densityFes->GetComm(); - - const double new_right_hand_side_norm = gravity_test_global_norm(new_right_hand_side, communicator); - const double legacy_right_hand_side_norm = gravity_test_global_norm(legacy_right_hand_side, communicator); - - REQUIRE(new_right_hand_side_norm > 0.0); - REQUIRE(legacy_right_hand_side_norm > 0.0); - - mfem::Vector right_hand_side_difference(new_right_hand_side); - right_hand_side_difference -= legacy_right_hand_side; - - const double right_hand_side_scale = - std::max(0.5 * (new_right_hand_side_norm + legacy_right_hand_side_norm), std::numeric_limits::min()); - - const double relative_right_hand_side_difference = - gravity_test_global_norm(right_hand_side_difference, communicator) / right_hand_side_scale; - - mfem::BlockVector legacy_gravity_state(gravity_layout.residual_offsets()); - mfem::BlockVector new_gravity_state(gravity_layout.residual_offsets()); - - legacy_gravity_state = 0.0; - new_gravity_state = 0.0; - - { - mfem::Vector gradient_true; - mfem::Vector potential_true; - - legacy_solution.gradPhi.GetTrueDofs(gradient_true); - legacy_solution.phi.GetTrueDofs(potential_true); - - REQUIRE(gradient_true.Size() == gravity_gradient_size); - REQUIRE(potential_true.Size() == gravity_poisson_size); - - legacy_gravity_state.GetBlock(gravity_gradient_residual_block) = gradient_true; - legacy_gravity_state.GetBlock(gravity_poisson_residual_block) = potential_true; - - new_solution.gradPhi.GetTrueDofs(gradient_true); - new_solution.phi.GetTrueDofs(potential_true); - - REQUIRE(gradient_true.Size() == gravity_gradient_size); - REQUIRE(potential_true.Size() == gravity_poisson_size); - - new_gravity_state.GetBlock(gravity_gradient_residual_block) = gradient_true; - new_gravity_state.GetBlock(gravity_poisson_residual_block) = potential_true; + if (ellipsoidal_radius_squared >= 1.0) { + return 0.0; } - REQUIRE(legacy_gravity_state.Size() == gravity_system_size); - REQUIRE(new_gravity_state.Size() == gravity_system_size); + const double denominator = 1.0 + concentration * ellipsoidal_radius_squared; - REQUIRE(f.gravityContext.block_A->Width() == gravity_system_size); - REQUIRE(f.gravityContext.block_A->Height() == gravity_system_size); + return density_scale * (1.0 - ellipsoidal_radius_squared) / + (denominator * denominator); + }; - mfem::Vector legacy_action_at_legacy_solution(gravity_system_size); - mfem::Vector legacy_action_at_new_solution(gravity_system_size); - mfem::Vector new_action_at_legacy_solution(gravity_system_size); - mfem::Vector new_action_at_new_solution(gravity_system_size); + mapping::PhysicalPositionFunctionCoefficient density_coefficient( + *f.domainMapperStateless, *f.displacement, + *f.compactificationCoordinate, ellipsoidal_density); - legacy_action_at_legacy_solution = 0.0; - legacy_action_at_new_solution = 0.0; - new_action_at_legacy_solution = 0.0; - new_action_at_new_solution = 0.0; + mfem::GridFunction density(f.densityFes.get()); - f.gravityContext.block_A->Mult(legacy_gravity_state, legacy_action_at_legacy_solution); + density.ProjectCoefficient(density_coefficient); - f.gravityContext.block_A->Mult(new_gravity_state, legacy_action_at_new_solution); + zero_vacuum_density(f, density); + analysis::conserve_mass(f, density, mass); - new_reduced_operator.Mult(legacy_gravity_state, new_action_at_legacy_solution); + f.com = analysis::get_com(f, density); + f.Q = physics::compute_quadrupole_moment_tensor(f, density, f.com); - new_reduced_operator.Mult(new_gravity_state, new_action_at_new_solution); + const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); - REQUIRE(legacy_action_at_legacy_solution.Size() == gravity_system_size); - REQUIRE(legacy_action_at_new_solution.Size() == gravity_system_size); - REQUIRE(new_action_at_legacy_solution.Size() == gravity_system_size); - REQUIRE(new_action_at_new_solution.Size() == gravity_system_size); + INFO("Normalized quadrupole = " << normalized_quadrupole); - mfem::Vector legacy_residual_at_legacy_solution(legacy_action_at_legacy_solution); - mfem::Vector legacy_residual_at_new_solution(legacy_action_at_new_solution); - mfem::Vector new_residual_at_legacy_solution(new_action_at_legacy_solution); - mfem::Vector new_residual_at_new_solution(new_action_at_new_solution); + REQUIRE(normalized_quadrupole > 1.0e-3); - legacy_residual_at_legacy_solution -= legacy_right_hand_side; - legacy_residual_at_new_solution -= legacy_right_hand_side; - new_residual_at_legacy_solution -= new_right_hand_side; - new_residual_at_new_solution -= new_right_hand_side; + const physics::GravitySolution gravity_solution = + physics::solve_gravity_field(f, args, density, displacement); - const GravityResidualMetrics legacy_at_legacy = measure_gravity_residual( - legacy_residual_at_legacy_solution, gravity_layout.residual_offsets(), legacy_right_hand_side_norm, communicator - ); + const int base_quadrature_order = get_gravity_quadrature_order(f); - const GravityResidualMetrics legacy_at_new = measure_gravity_residual( - legacy_residual_at_new_solution, gravity_layout.residual_offsets(), legacy_right_hand_side_norm, communicator - ); + const GravitationalEnergies base_energies = compute_gravitational_energies( + f, density, gravity_solution, base_quadrature_order); - const GravityResidualMetrics new_at_legacy = measure_gravity_residual( - new_residual_at_legacy_solution, gravity_layout.residual_offsets(), new_right_hand_side_norm, communicator - ); + const GravitationalEnergies medium_energies = compute_gravitational_energies( + f, density, gravity_solution, base_quadrature_order + 4); - const GravityResidualMetrics new_at_new = measure_gravity_residual( - new_residual_at_new_solution, gravity_layout.residual_offsets(), new_right_hand_side_norm, communicator - ); + const GravitationalEnergies fine_energies = compute_gravitational_energies( + f, density, gravity_solution, base_quadrature_order + 8); - mfem::Vector operator_gap_at_legacy_solution(new_residual_at_legacy_solution); - operator_gap_at_legacy_solution -= legacy_residual_at_legacy_solution; + REQUIRE(fine_energies.binding < 0.0); + REQUIRE(fine_energies.virial < 0.0); - mfem::Vector operator_gap_at_new_solution(new_residual_at_new_solution); - operator_gap_at_new_solution -= legacy_residual_at_new_solution; + const double base_consistency_error = + std::abs(base_energies.binding - base_energies.virial) / + std::abs(base_energies.binding); - const GravityResidualMetrics gap_at_legacy = measure_gravity_residual( - operator_gap_at_legacy_solution, gravity_layout.residual_offsets(), right_hand_side_scale, communicator - ); + const double medium_consistency_error = + std::abs(medium_energies.binding - medium_energies.virial) / + std::abs(medium_energies.binding); - const GravityResidualMetrics gap_at_new = measure_gravity_residual( - operator_gap_at_new_solution, gravity_layout.residual_offsets(), right_hand_side_scale, communicator - ); + const double fine_consistency_error = + std::abs(fine_energies.binding - fine_energies.virial) / + std::abs(fine_energies.binding); - INFO("New/legacy right-hand-side difference = " << relative_right_hand_side_difference); - INFO( - "Legacy operator at legacy solution: total = " << legacy_at_legacy.relative_total - << ", gradient = " << legacy_at_legacy.relative_gradient - << ", Poisson = " << legacy_at_legacy.relative_poisson - ); - INFO( - "Legacy operator at new solution: total = " << legacy_at_new.relative_total - << ", gradient = " << legacy_at_new.relative_gradient - << ", Poisson = " << legacy_at_new.relative_poisson - ); - INFO( - "New operator at legacy solution: total = " << new_at_legacy.relative_total - << ", gradient = " << new_at_legacy.relative_gradient - << ", Poisson = " << new_at_legacy.relative_poisson - ); - INFO( - "New operator at new solution: total = " << new_at_new.relative_total - << ", gradient = " << new_at_new.relative_gradient - << ", Poisson = " << new_at_new.relative_poisson - ); - INFO( - "Operator gap at legacy solution: total = " << gap_at_legacy.relative_total - << ", gradient = " << gap_at_legacy.relative_gradient - << ", Poisson = " << gap_at_legacy.relative_poisson - ); - INFO( - "Operator gap at new solution: total = " << gap_at_new.relative_total - << ", gradient = " << gap_at_new.relative_gradient - << ", Poisson = " << gap_at_new.relative_poisson - ); + const double binding_quadrature_change = + std::abs(fine_energies.binding - medium_energies.binding) / + std::abs(fine_energies.binding); - REQUIRE(std::isfinite(legacy_at_legacy.relative_total)); - REQUIRE(std::isfinite(legacy_at_new.relative_total)); - REQUIRE(std::isfinite(new_at_legacy.relative_total)); - REQUIRE(std::isfinite(new_at_new.relative_total)); - REQUIRE(std::isfinite(gap_at_legacy.relative_total)); - REQUIRE(std::isfinite(gap_at_new.relative_total)); + const double virial_quadrature_change = + std::abs(fine_energies.virial - medium_energies.virial) / + std::abs(fine_energies.virial); - constexpr double source_parity_tolerance = 1.0e-12; - constexpr double diagonal_residual_tolerance = 1.0e-8; - constexpr double poisson_gap_tolerance = 1.0e-11; + INFO("Base-order consistency error = " << base_consistency_error); - CHECK_THAT(relative_right_hand_side_difference, Catch::Matchers::WithinAbs(0.0, source_parity_tolerance)); + INFO("Medium-order consistency error = " << medium_consistency_error); - CHECK_THAT(legacy_at_legacy.relative_total, Catch::Matchers::WithinAbs(0.0, diagonal_residual_tolerance)); + INFO("Fine-order consistency error = " << fine_consistency_error); - CHECK_THAT(new_at_new.relative_total, Catch::Matchers::WithinAbs(0.0, diagonal_residual_tolerance)); + INFO("Medium-to-fine binding-energy change = " << binding_quadrature_change); - CHECK_THAT(gap_at_legacy.relative_poisson, Catch::Matchers::WithinAbs(0.0, poisson_gap_tolerance)); + INFO("Medium-to-fine virial-energy change = " << virial_quadrature_change); - CHECK_THAT(gap_at_new.relative_poisson, Catch::Matchers::WithinAbs(0.0, poisson_gap_tolerance)); + constexpr double diagnostic_quadrature_tolerance = 1.0e-7; - const int quadrature_order = get_gravity_quadrature_order(f); + CHECK_THAT( + fine_consistency_error, + Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance)); - const GravitySolutionComparison comparison = - compare_gravity_solutions(f, legacy_solution, new_solution, quadrature_order); + CHECK_THAT(binding_quadrature_change, + Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); - const GravitationalEnergies legacy_energies = - compute_gravitational_energies(f, rho_grid, legacy_solution, quadrature_order); - - const GravitationalEnergies new_energies = - compute_gravitational_energies(f, rho_grid, new_solution, quadrature_order); - - const HomogeneousEllipsoidAnalytic analytic = - compute_homogeneous_ellipsoid_analytic(semi_axis_x, semi_axis_y, semi_axis_z); - - const double analytic_binding_energy = -(3.0 / 10.0) * utils::G * mass * mass * analytic.energy_kernel; - - const double new_binding_analytic_error = - std::abs(new_energies.binding - analytic_binding_energy) / std::abs(analytic_binding_energy); - - const double new_virial_analytic_error = - std::abs(new_energies.virial - analytic_binding_energy) / std::abs(analytic_binding_energy); - - const double new_virial_consistency_error = - std::abs(new_energies.binding - new_energies.virial) / std::abs(new_energies.binding); - - const double relative_binding_difference = - std::abs(new_energies.binding - legacy_energies.binding) / - std::max( - 0.5 * (std::abs(new_energies.binding) + std::abs(legacy_energies.binding)), - std::numeric_limits::min() - ); - - const double relative_virial_difference = - std::abs(new_energies.virial - legacy_energies.virial) / - std::max( - 0.5 * (std::abs(new_energies.virial) + std::abs(legacy_energies.virial)), std::numeric_limits::min() - ); - - INFO("Numerical density = " << numerical_density); - INFO("Analytic binding energy = " << analytic_binding_energy); - INFO("Legacy binding energy = " << legacy_energies.binding); - INFO("New binding energy = " << new_energies.binding); - INFO("Legacy virial energy = " << legacy_energies.virial); - INFO("New virial energy = " << new_energies.virial); - INFO("New binding analytic error = " << new_binding_analytic_error); - INFO("New virial analytic error = " << new_virial_analytic_error); - INFO("New virial consistency error = " << new_virial_consistency_error); - INFO("New/legacy physical gradient difference = " << comparison.relative_gradient_difference); - INFO("New/legacy physical potential difference = " << comparison.relative_potential_difference); - INFO("New/legacy binding-energy difference = " << relative_binding_difference); - INFO("New/legacy virial-energy difference = " << relative_virial_difference); - - REQUIRE(legacy_energies.binding < 0.0); - REQUIRE(legacy_energies.virial < 0.0); - REQUIRE(new_energies.binding < 0.0); - REQUIRE(new_energies.virial < 0.0); - - REQUIRE(std::isfinite(comparison.relative_gradient_difference)); - REQUIRE(std::isfinite(comparison.relative_potential_difference)); - - constexpr double gradient_parity_tolerance = 5.0e-4; - constexpr double potential_parity_tolerance = 1.0e-4; - constexpr double energy_parity_tolerance = 1.0e-5; - constexpr double analytic_energy_tolerance = 1.0e-5; - constexpr double consistency_tolerance = 1.0e-5; - - CHECK_THAT(comparison.relative_gradient_difference, Catch::Matchers::WithinAbs(0.0, gradient_parity_tolerance)); - - CHECK_THAT(comparison.relative_potential_difference, Catch::Matchers::WithinAbs(0.0, potential_parity_tolerance)); - - CHECK_THAT(relative_binding_difference, Catch::Matchers::WithinAbs(0.0, energy_parity_tolerance)); - - CHECK_THAT(relative_virial_difference, Catch::Matchers::WithinAbs(0.0, energy_parity_tolerance)); - - CHECK_THAT(new_binding_analytic_error, Catch::Matchers::WithinAbs(0.0, analytic_energy_tolerance)); - - CHECK_THAT(new_virial_analytic_error, Catch::Matchers::WithinAbs(0.0, analytic_energy_tolerance)); - - CHECK_THAT(new_virial_consistency_error, Catch::Matchers::WithinAbs(0.0, consistency_tolerance)); + CHECK_THAT(virial_quadrature_change, + Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); } -TEST_CASE( - "New Gravity Potential Deformed Rational Density Virial Self-Consistency", - tags::gravity_consistency -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); +TEST_CASE("Exterior Monopole Error Is Separated From Finite Element Projection " + "Floor", + tags::gravity_analytic_accuracy) { + auto args = test_utils::setup_args(); + args.p.rtol = 1.0e-13; + args.p.max_iters = std::max(args.p.max_iters, 1000); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - const double radius = utils::RADIUS; - const double mass = utils::MASS; + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.compactificationCoordinate != nullptr); - constexpr double x_scale = 1.15; - constexpr double y_scale = 0.95; - constexpr double z_scale = 1.0 / (x_scale * y_scale); + const double stellar_radius = utils::RADIUS; - REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14)); + const double mass = utils::MASS; - const double semi_axis_x = x_scale * radius; - const double semi_axis_y = y_scale * radius; - const double semi_axis_z = z_scale * radius; + const double analytic_volume = + (4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius; - auto affine_displacement = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = (x_scale - 1.0) * position(0); - value(1) = (y_scale - 1.0) * position(1); - value(2) = (z_scale - 1.0) * position(2); - }; + const double density = mass / analytic_volume; - mfem::VectorFunctionCoefficient displacement_coefficient(3, affine_displacement); + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; - mfem::ParGridFunction displacement(f.displacementFes.get()); + *f.displacement = 0.0; - displacement.ProjectCoefficient(displacement_coefficient); + mfem::GridFunction rho_uniform(f.densityFes.get()); + rho_uniform = density; - f.mapping->SetDisplacement(displacement); - physics::update_stiffness_matrix(f); + zero_vacuum_density(f, rho_uniform); - constexpr double concentration = 16.0; - const double density_scale = mass / std::pow(radius, 3.0); + analysis::conserve_mass(f, rho_uniform, mass); - auto ellipsoidal_density = [semi_axis_x, semi_axis_y, semi_axis_z, density_scale](const mfem::Vector &position) { - const double ellipsoidal_radius_squared = position(0) * position(0) / (semi_axis_x * semi_axis_x) + - position(1) * position(1) / (semi_axis_y * semi_axis_y) + - position(2) * position(2) / (semi_axis_z * semi_axis_z); + f.com = analysis::get_com(f, rho_uniform); - if (ellipsoidal_radius_squared >= 1.0) { - return 0.0; - } + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); - const double denominator = 1.0 + concentration * ellipsoidal_radius_squared; + const physics::GravitySolution numerical_solution = + physics::solve_gravity_field(f, args, rho_uniform, displacement); - return density_scale * (1.0 - ellipsoidal_radius_squared) / (denominator * denominator); - }; + StatelessMonopolePotentialCoefficient analytic_potential_coefficient( + f, *f.domainMapperStateless, displacement, mass, stellar_radius); - mapping::PhysicalPositionFunctionCoefficient density_coefficient(*f.mapping, ellipsoidal_density); + StatelessMonopoleHDivCoefficient analytic_field_coefficient( + f, *f.domainMapperStateless, displacement, mass, stellar_radius); - mfem::GridFunction density(f.densityFes.get()); + physics::GravitySolution analytic_projection(f); + analytic_projection.phi = 0.0; + analytic_projection.gradPhi = 0.0; - density.ProjectCoefficient(density_coefficient); + analytic_projection.phi.ProjectCoefficient(analytic_potential_coefficient); - zero_vacuum_density(f, density); - analysis::conserve_mass(f, density, mass); + analytic_projection.gradPhi.ProjectCoefficient(analytic_field_coefficient); - f.com = analysis::get_com(f, density); - f.Q = physics::compute_quadrupole_moment_tensor(f, density, f.com); + const std::array numerical_metrics = + measure_exterior_monopole_shells(f, numerical_solution, displacement, + mass); - const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); + const std::array projection_metrics = + measure_exterior_monopole_shells(f, analytic_projection, displacement, + mass); - INFO("Normalized quadrupole = " << normalized_quadrupole); + mfem::Vector numerical_gradient_true; + mfem::Vector numerical_potential_true; + mfem::Vector projected_gradient_true; + mfem::Vector projected_potential_true; - REQUIRE(normalized_quadrupole > 1.0e-3); + numerical_solution.gradPhi.GetTrueDofs(numerical_gradient_true); - const physics::GravitySolution gravity_solution = physics::grav_potential_new(f, args, density, displacement); + numerical_solution.phi.GetTrueDofs(numerical_potential_true); - const int base_quadrature_order = get_gravity_quadrature_order(f); + analytic_projection.gradPhi.GetTrueDofs(projected_gradient_true); - const GravitationalEnergies base_energies = - compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order); + analytic_projection.phi.GetTrueDofs(projected_potential_true); - const GravitationalEnergies medium_energies = - compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order + 4); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - const GravitationalEnergies fine_energies = - compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order + 8); + const double gradient_projection_gap = global_relative_vector_error( + numerical_gradient_true, projected_gradient_true, communicator); - REQUIRE(fine_energies.binding < 0.0); - REQUIRE(fine_energies.virial < 0.0); + const double potential_projection_gap = global_relative_vector_error( + numerical_potential_true, projected_potential_true, communicator); - const double base_consistency_error = - std::abs(base_energies.binding - base_energies.virial) / std::abs(base_energies.binding); + double maximum_numerical_potential_error = 0.0; + double maximum_projected_potential_error = 0.0; + double maximum_numerical_radial_error = 0.0; + double maximum_projected_radial_error = 0.0; + double maximum_numerical_tangential_field = 0.0; + double maximum_projected_tangential_field = 0.0; - const double medium_consistency_error = - std::abs(medium_energies.binding - medium_energies.virial) / std::abs(medium_energies.binding); + std::ostringstream report; - const double fine_consistency_error = - std::abs(fine_energies.binding - fine_energies.virial) / std::abs(fine_energies.binding); + report << "Global numerical/projection gradient DOF gap = " + << gradient_projection_gap << '\n' + << "Global numerical/projection potential DOF gap = " + << potential_projection_gap << '\n'; - const double binding_quadrature_change = - std::abs(fine_energies.binding - medium_energies.binding) / std::abs(fine_energies.binding); + for (int shell = 0; shell < 5; ++shell) { + const ExteriorMonopoleShellMetrics &numerical = numerical_metrics[shell]; - const double virial_quadrature_change = - std::abs(fine_energies.virial - medium_energies.virial) / std::abs(fine_energies.virial); + const ExteriorMonopoleShellMetrics &projected = projection_metrics[shell]; - INFO("Base-order consistency error = " << base_consistency_error); + maximum_numerical_potential_error = std::max( + maximum_numerical_potential_error, numerical.potential_rms_error); - INFO("Medium-order consistency error = " << medium_consistency_error); + maximum_projected_potential_error = std::max( + maximum_projected_potential_error, projected.potential_rms_error); - INFO("Fine-order consistency error = " << fine_consistency_error); + maximum_numerical_radial_error = std::max(maximum_numerical_radial_error, + numerical.radial_field_rms_error); - INFO("Medium-to-fine binding-energy change = " << binding_quadrature_change); + maximum_projected_radial_error = std::max(maximum_projected_radial_error, + projected.radial_field_rms_error); - INFO("Medium-to-fine virial-energy change = " << virial_quadrature_change); + maximum_numerical_tangential_field = std::max( + maximum_numerical_tangential_field, numerical.tangential_field_rms); - constexpr double virial_tolerance = 1.0e-5; - constexpr double diagnostic_quadrature_tolerance = 1.0e-7; + maximum_projected_tangential_field = std::max( + maximum_projected_tangential_field, projected.tangential_field_rms); - CHECK_THAT(fine_consistency_error, Catch::Matchers::WithinAbs(0.0, virial_tolerance)); + report << "Shell " << shell << " xi=[" << exterior_shell_boundaries[shell] + << ", " << exterior_shell_boundaries[shell + 1] << "):\n" + << " radius range = [" << numerical.minimum_radius << ", " + << numerical.maximum_radius << "]\n" + << " numerical potential error = " << numerical.potential_rms_error + << '\n' + << " projected potential error = " << projected.potential_rms_error + << '\n' + << " numerical radial-field error = " + << numerical.radial_field_rms_error << '\n' + << " projected radial-field error = " + << projected.radial_field_rms_error << '\n' + << " numerical tangential field = " + << numerical.tangential_field_rms << '\n' + << " projected tangential field = " + << projected.tangential_field_rms << '\n'; + } - CHECK_THAT(binding_quadrature_change, Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); + INFO(report.str()); - CHECK_THAT(virial_quadrature_change, Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); + REQUIRE(std::isfinite(gradient_projection_gap)); + REQUIRE(std::isfinite(potential_projection_gap)); + + REQUIRE(maximum_numerical_potential_error > 0.0); + REQUIRE(maximum_projected_potential_error > 0.0); + REQUIRE(maximum_numerical_radial_error > 0.0); + REQUIRE(maximum_projected_radial_error > 0.0); + + /* + * Broad guards against a broken projection. These are not the final + * physical acceptance thresholds. + */ + CHECK(maximum_projected_potential_error < 5.0e-2); + CHECK(maximum_projected_radial_error < 5.0e-3); + CHECK(maximum_projected_tangential_field < 5.0e-3); + + CHECK(maximum_numerical_potential_error < 5.0e-2); + CHECK(maximum_numerical_radial_error < 5.0e-3); + CHECK(maximum_numerical_tangential_field < 5.0e-3); + + /* + * These are the decisive comparisons. If either fails, the solved + * field is farther from the direct FE representation than it is from + * the continuum monopole, indicating an operator-consistency issue + * rather than a simple approximation floor. + */ + CHECK(gradient_projection_gap < maximum_numerical_radial_error); + + CHECK(potential_projection_gap < maximum_numerical_potential_error); } -TEST_CASE( - "New Gravity Potential Resolves Exterior Monopole By Compactification " - "Shell", - tags::gravity_analytic -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); +TEST_CASE("Gravity Field Matches Analytic Interior Potential For A Deformed " + "Homogeneous Star", + tags::gravity_analytic_accuracy) { + auto args = test_utils::setup_args(); + args.p.rtol = 1.0e-13; + args.p.atol = 1.0e-14; + args.p.max_iters = std::max(args.p.max_iters, 2000); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.compactificationCoordinate != nullptr); + REQUIRE(fem.domainMapperStateless != nullptr); + REQUIRE(fem.domainMapperStateless != nullptr); - const double radius = utils::RADIUS; - const double mass = utils::MASS; - const double analytic_volume = (4.0 / 3.0) * M_PI * radius * radius * radius; - const double density = mass / analytic_volume; + const double radius = utils::RADIUS; + const double mass = utils::MASS; - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement = 0.0; + constexpr double x_scale = 1.15; + constexpr double y_scale = 0.95; + constexpr double z_scale = 1.0 / (x_scale * y_scale); - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); + const double semi_axis_x = x_scale * radius; + const double semi_axis_y = y_scale * radius; + const double semi_axis_z = z_scale * radius; - mfem::GridFunction rho_uniform(f.densityFes.get()); - rho_uniform = density; + REQUIRE_THAT(x_scale * y_scale * z_scale, + Catch::Matchers::WithinAbs(1.0, 1.0e-14)); - zero_vacuum_density(f, rho_uniform); - analysis::conserve_mass(f, rho_uniform, mass); + auto displacement_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = (x_scale - 1.0) * position(0); + value(1) = (y_scale - 1.0) * position(1); + value(2) = (z_scale - 1.0) * position(2); + }; - f.com = analysis::get_com(f, rho_uniform); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); + mfem::VectorFunctionCoefficient displacement_coefficient( + 3, displacement_function); + mfem::ParGridFunction displacement(fem.displacementFes.get()); + displacement.ProjectCoefficient(displacement_coefficient); - const physics::GravitySolution legacy_solution = physics::grav_potential(f, args, rho_uniform); + *fem.displacement = displacement; - const physics::GravitySolution new_solution = physics::grav_potential_new(f, args, rho_uniform, displacement); + const double analytic_volume = + (4.0 / 3.0) * M_PI * semi_axis_x * semi_axis_y * semi_axis_z; + const double density_value = mass / analytic_volume; - const std::array legacy_metrics = - measure_exterior_monopole_shells(f, legacy_solution, displacement, ExteriorMonopoleMapping::legacy, mass); + mfem::GridFunction density(fem.densityFes.get()); + density = density_value; + zero_vacuum_density(fem, density); - const std::array new_metrics = - measure_exterior_monopole_shells(f, new_solution, displacement, ExteriorMonopoleMapping::stateless, mass); + const double projected_mass = analysis::domain_integrate_grid_function( + fem, density, utils::DOMAINS::STELLAR); + const double numerical_density = density_value * mass / projected_mass; + analysis::conserve_mass(fem, density, mass); - double maximum_new_potential_error = 0.0; - double maximum_new_radial_field_error = 0.0; - double maximum_new_tangential_field = 0.0; + fem.com = analysis::get_com(fem, density); + fem.Q = physics::compute_quadrupole_moment_tensor(fem, density, fem.com); - for (int shell = 0; shell < 5; ++shell) { - DYNAMIC_SECTION( - "Exterior coordinate in [" << exterior_shell_boundaries[shell] << ", " - << exterior_shell_boundaries[shell + 1] << ")" - ) { - const ExteriorMonopoleShellMetrics &legacy = legacy_metrics[shell]; + const HomogeneousEllipsoidAnalytic analytic = + compute_homogeneous_ellipsoid_analytic(semi_axis_x, semi_axis_y, + semi_axis_z); - const ExteriorMonopoleShellMetrics ¤t = new_metrics[shell]; + auto analytic_potential = [numerical_density, semi_axis_x, semi_axis_y, + semi_axis_z, + analytic](const mfem::Vector &position) { + const double potential_kernel = + semi_axis_x * semi_axis_y * semi_axis_z * analytic.energy_kernel - + analytic.coefficient_x * position(0) * position(0) - + analytic.coefficient_y * position(1) * position(1) - + analytic.coefficient_z * position(2) * position(2); - maximum_new_potential_error = std::max(maximum_new_potential_error, current.potential_rms_error); + return -M_PI * utils::G * numerical_density * potential_kernel; + }; - maximum_new_radial_field_error = std::max(maximum_new_radial_field_error, current.radial_field_rms_error); + mapping::PhysicalPositionFunctionCoefficient analytic_potential_coefficient( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate, analytic_potential); + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get()); + projected_potential.ProjectCoefficient(analytic_potential_coefficient); + const physics::GravitySolution solution = + physics::solve_gravity_field(fem, args, density, displacement); - maximum_new_tangential_field = std::max(maximum_new_tangential_field, current.tangential_field_rms); + const int quadrature_order = get_gravity_quadrature_order(fem); + double local_solution_error_squared = 0.0; + double local_projection_error_squared = 0.0; + double local_solution_projection_gap_squared = 0.0; + double local_analytic_norm_squared = 0.0; + double local_projected_norm_squared = 0.0; + double local_maximum_relative_error = 0.0; - INFO("Shell = " << shell); - INFO( - "Exterior-coordinate interval = [" << exterior_shell_boundaries[shell] << ", " - << exterior_shell_boundaries[shell + 1] << ")" - ); + mfem::Vector physical_position(3); + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate); - INFO("Legacy physical-radius range = [" << legacy.minimum_radius << ", " << legacy.maximum_radius << "]"); - - INFO("New physical-radius range = [" << current.minimum_radius << ", " << current.maximum_radius << "]"); - - INFO("Legacy scaled-potential RMS error = " << legacy.potential_rms_error); - - INFO("New scaled-potential RMS error = " << current.potential_rms_error); - - INFO("Legacy scaled-radial-field RMS error = " << legacy.radial_field_rms_error); - - INFO("New scaled-radial-field RMS error = " << current.radial_field_rms_error); - - INFO("Legacy scaled-tangential-field RMS = " << legacy.tangential_field_rms); - - INFO("New scaled-tangential-field RMS = " << current.tangential_field_rms); - - REQUIRE(legacy.quadrature_points > 0); - REQUIRE(current.quadrature_points > 0); - - REQUIRE(std::isfinite(current.minimum_radius)); - REQUIRE(std::isfinite(current.maximum_radius)); - REQUIRE(std::isfinite(current.potential_rms_error)); - REQUIRE(std::isfinite(current.radial_field_rms_error)); - REQUIRE(std::isfinite(current.tangential_field_rms)); - - CHECK(current.minimum_radius > 0.0); - CHECK(current.maximum_radius > current.minimum_radius); - - /* - * These are broad regression bounds, not the virial target. - * The measured values will determine whether a monopole lift - * is warranted. - */ - CHECK(current.potential_rms_error < 1.0e-2); - CHECK(current.radial_field_rms_error < 1.0e-2); - CHECK(current.tangential_field_rms < 1.0e-2); - } + 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; } - for (int shell = 1; shell < 5; ++shell) { - CHECK(new_metrics[shell].minimum_radius >= new_metrics[shell - 1].minimum_radius); + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - CHECK(new_metrics[shell].maximum_radius > new_metrics[shell - 1].maximum_radius); + 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); + + mapping::MappingPointContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluatePoint(*transformation, point, + mapping_context) == + mapping::MappingStatus::valid, + "Deformed potential test encountered an invalid mapping."); + physical_position = mapping_context.physical_position; + const double mapping_determinant = + mapping_context.mapping_determinant; + MFEM_VERIFY(mapping_determinant > 0.0, + "Deformed potential test encountered a " + "non-positive mapping determinant."); + + const double expected_potential = analytic_potential(physical_position); + const double computed_potential = + solution.phi.GetValue(element_id, point); + const double projected_potential_value = + projected_potential.GetValue(element_id, point); + const double weight = + point.weight * transformation->Weight() * mapping_determinant; + + local_solution_error_squared += + weight * (computed_potential - expected_potential) * + (computed_potential - expected_potential); + local_projection_error_squared += + weight * (projected_potential_value - expected_potential) * + (projected_potential_value - expected_potential); + local_solution_projection_gap_squared += + weight * (computed_potential - projected_potential_value) * + (computed_potential - projected_potential_value); + local_analytic_norm_squared += + weight * expected_potential * expected_potential; + local_projected_norm_squared += + weight * projected_potential_value * projected_potential_value; + local_maximum_relative_error = + std::max(local_maximum_relative_error, + std::abs(computed_potential - expected_potential) / + std::max(std::abs(expected_potential), + std::numeric_limits::epsilon())); } - - INFO("Maximum new scaled-potential shell error = " << maximum_new_potential_error); - - INFO("Maximum new scaled-radial-field shell error = " << maximum_new_radial_field_error); - - INFO("Maximum new scaled-tangential-field shell amplitude = " << maximum_new_tangential_field); -} - -TEST_CASE( - "New Exterior Monopole Error Is Separated From Finite Element Projection " - "Floor", - tags::gravity_analytic_accuracy -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); - - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.compactificationCoordinate != nullptr); - - const double stellar_radius = utils::RADIUS; - - const double mass = utils::MASS; - - const double analytic_volume = (4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius; - - const double density = mass / analytic_volume; - - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement = 0.0; - - f.mapping->ResetDisplacement(); - physics::update_stiffness_matrix(f); - - mfem::GridFunction rho_uniform(f.densityFes.get()); - rho_uniform = density; - - zero_vacuum_density(f, rho_uniform); - - analysis::conserve_mass(f, rho_uniform, mass); - - f.com = analysis::get_com(f, rho_uniform); - - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); - - const physics::GravitySolution numerical_solution = physics::grav_potential_new(f, args, rho_uniform, displacement); - - StatelessMonopolePotentialCoefficient analytic_potential_coefficient( - f, *f.domainMapperStateless, displacement, mass, stellar_radius - ); - - StatelessMonopoleHDivCoefficient analytic_field_coefficient( - f, *f.domainMapperStateless, displacement, mass, stellar_radius - ); - - physics::GravitySolution analytic_projection(f); - analytic_projection.phi = 0.0; - analytic_projection.gradPhi = 0.0; - - analytic_projection.phi.ProjectCoefficient(analytic_potential_coefficient); - - analytic_projection.gradPhi.ProjectCoefficient(analytic_field_coefficient); - - const std::array numerical_metrics = - measure_exterior_monopole_shells(f, numerical_solution, displacement, ExteriorMonopoleMapping::stateless, mass); - - const std::array projection_metrics = measure_exterior_monopole_shells( - f, analytic_projection, displacement, ExteriorMonopoleMapping::stateless, mass - ); - - mfem::Vector numerical_gradient_true; - mfem::Vector numerical_potential_true; - mfem::Vector projected_gradient_true; - mfem::Vector projected_potential_true; - - numerical_solution.gradPhi.GetTrueDofs(numerical_gradient_true); - - numerical_solution.phi.GetTrueDofs(numerical_potential_true); - - analytic_projection.gradPhi.GetTrueDofs(projected_gradient_true); - - analytic_projection.phi.GetTrueDofs(projected_potential_true); - - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - const double gradient_projection_gap = - global_relative_vector_error(numerical_gradient_true, projected_gradient_true, communicator); - - const double potential_projection_gap = - global_relative_vector_error(numerical_potential_true, projected_potential_true, communicator); - - double maximum_numerical_potential_error = 0.0; - double maximum_projected_potential_error = 0.0; - double maximum_numerical_radial_error = 0.0; - double maximum_projected_radial_error = 0.0; - double maximum_numerical_tangential_field = 0.0; - double maximum_projected_tangential_field = 0.0; - - std::ostringstream report; - - report << "Global numerical/projection gradient DOF gap = " << gradient_projection_gap << '\n' - << "Global numerical/projection potential DOF gap = " << potential_projection_gap << '\n'; - - for (int shell = 0; shell < 5; ++shell) { - const ExteriorMonopoleShellMetrics &numerical = numerical_metrics[shell]; - - const ExteriorMonopoleShellMetrics &projected = projection_metrics[shell]; - - maximum_numerical_potential_error = std::max(maximum_numerical_potential_error, numerical.potential_rms_error); - - maximum_projected_potential_error = std::max(maximum_projected_potential_error, projected.potential_rms_error); - - maximum_numerical_radial_error = std::max(maximum_numerical_radial_error, numerical.radial_field_rms_error); - - maximum_projected_radial_error = std::max(maximum_projected_radial_error, projected.radial_field_rms_error); - - maximum_numerical_tangential_field = - std::max(maximum_numerical_tangential_field, numerical.tangential_field_rms); - - maximum_projected_tangential_field = - std::max(maximum_projected_tangential_field, projected.tangential_field_rms); - - report << "Shell " << shell << " xi=[" << exterior_shell_boundaries[shell] << ", " - << exterior_shell_boundaries[shell + 1] << "):\n" - << " radius range = [" << numerical.minimum_radius << ", " << numerical.maximum_radius << "]\n" - << " numerical potential error = " << numerical.potential_rms_error << '\n' - << " projected potential error = " << projected.potential_rms_error << '\n' - << " numerical radial-field error = " << numerical.radial_field_rms_error << '\n' - << " projected radial-field error = " << projected.radial_field_rms_error << '\n' - << " numerical tangential field = " << numerical.tangential_field_rms << '\n' - << " projected tangential field = " << projected.tangential_field_rms << '\n'; - } - - INFO(report.str()); - - REQUIRE(std::isfinite(gradient_projection_gap)); - REQUIRE(std::isfinite(potential_projection_gap)); - - REQUIRE(maximum_numerical_potential_error > 0.0); - REQUIRE(maximum_projected_potential_error > 0.0); - REQUIRE(maximum_numerical_radial_error > 0.0); - REQUIRE(maximum_projected_radial_error > 0.0); - - /* - * Broad guards against a broken projection. These are not the final - * physical acceptance thresholds. - */ - CHECK(maximum_projected_potential_error < 5.0e-2); - CHECK(maximum_projected_radial_error < 5.0e-3); - CHECK(maximum_projected_tangential_field < 5.0e-3); - - CHECK(maximum_numerical_potential_error < 5.0e-2); - CHECK(maximum_numerical_radial_error < 5.0e-3); - CHECK(maximum_numerical_tangential_field < 5.0e-3); - - /* - * These are the decisive comparisons. If either fails, the solved - * field is farther from the direct FE representation than it is from - * the continuum monopole, indicating an operator-consistency issue - * rather than a simple approximation floor. - */ - CHECK(gradient_projection_gap < maximum_numerical_radial_error); - - CHECK(potential_projection_gap < maximum_numerical_potential_error); -} - -TEST_CASE( - "New Gravity Potential Matches Analytic Interior Potential For A Deformed " - "Homogeneous Star", - tags::gravity_analytic_accuracy -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.atol = 1.0e-14; - args.p.max_iters = std::max(args.p.max_iters, 2000); - - fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); - - REQUIRE(fem.mapping != nullptr); - REQUIRE(fem.domainMapperStateless != nullptr); - - const double radius = utils::RADIUS; - const double mass = utils::MASS; - - constexpr double x_scale = 1.15; - constexpr double y_scale = 0.95; - constexpr double z_scale = 1.0 / (x_scale * y_scale); - - const double semi_axis_x = x_scale * radius; - const double semi_axis_y = y_scale * radius; - const double semi_axis_z = z_scale * radius; - - REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14)); - - auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = (x_scale - 1.0) * position(0); - value(1) = (y_scale - 1.0) * position(1); - value(2) = (z_scale - 1.0) * position(2); - }; - - mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); - mfem::ParGridFunction displacement(fem.displacementFes.get()); - displacement.ProjectCoefficient(displacement_coefficient); - - fem.mapping->SetDisplacement(displacement); - physics::update_stiffness_matrix(fem); - - const double analytic_volume = (4.0 / 3.0) * M_PI * semi_axis_x * semi_axis_y * semi_axis_z; - const double density_value = mass / analytic_volume; - - mfem::GridFunction density(fem.densityFes.get()); - density = density_value; - zero_vacuum_density(fem, density); - - const double projected_mass = analysis::domain_integrate_grid_function(fem, density, utils::DOMAINS::STELLAR); - const double numerical_density = density_value * mass / projected_mass; - analysis::conserve_mass(fem, density, mass); - - fem.com = analysis::get_com(fem, density); - fem.Q = physics::compute_quadrupole_moment_tensor(fem, density, fem.com); - - const HomogeneousEllipsoidAnalytic analytic = - compute_homogeneous_ellipsoid_analytic(semi_axis_x, semi_axis_y, semi_axis_z); - - auto analytic_potential = [numerical_density, semi_axis_x, semi_axis_y, semi_axis_z, - analytic](const mfem::Vector &position) { - const double potential_kernel = semi_axis_x * semi_axis_y * semi_axis_z * analytic.energy_kernel - - analytic.coefficient_x * position(0) * position(0) - - analytic.coefficient_y * position(1) * position(1) - - analytic.coefficient_z * position(2) * position(2); - - return -M_PI * utils::G * numerical_density * potential_kernel; - }; - - mapping::PhysicalPositionFunctionCoefficient analytic_potential_coefficient(*fem.mapping, analytic_potential); - const int vacuum_attribute = fem.domainMapperStateless->GetVacuumElementAttribute(); - mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get()); - projected_potential.ProjectCoefficient(analytic_potential_coefficient); - const physics::GravitySolution solution = physics::grav_potential_new(fem, args, density, displacement); - - const int quadrature_order = get_gravity_quadrature_order(fem); - double local_solution_error_squared = 0.0; - double local_projection_error_squared = 0.0; - double local_solution_projection_gap_squared = 0.0; - double local_analytic_norm_squared = 0.0; - double local_projected_norm_squared = 0.0; - double local_maximum_relative_error = 0.0; - - mfem::Vector physical_position(3); - mfem::DenseMatrix mapping_jacobian(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); - - fem.mapping->GetPhysicalPoint(*transformation, point, physical_position); - fem.mapping->ComputeJacobian(*transformation, mapping_jacobian); - const double mapping_determinant = mapping_jacobian.Det(); - MFEM_VERIFY( - mapping_determinant > 0.0, "Deformed potential test encountered a " - "non-positive mapping determinant." - ); - - const double expected_potential = analytic_potential(physical_position); - const double computed_potential = solution.phi.GetValue(element_id, point); - const double projected_potential_value = projected_potential.GetValue(element_id, point); - const double weight = point.weight * transformation->Weight() * mapping_determinant; - - local_solution_error_squared += - weight * (computed_potential - expected_potential) * (computed_potential - expected_potential); - local_projection_error_squared += weight * (projected_potential_value - expected_potential) * - (projected_potential_value - expected_potential); - local_solution_projection_gap_squared += weight * (computed_potential - projected_potential_value) * - (computed_potential - projected_potential_value); - local_analytic_norm_squared += weight * expected_potential * expected_potential; - local_projected_norm_squared += weight * projected_potential_value * projected_potential_value; - local_maximum_relative_error = std::max( - local_maximum_relative_error, - std::abs(computed_potential - expected_potential) / - std::max(std::abs(expected_potential), std::numeric_limits::epsilon()) - ); - } - } - - const std::array local_values{ - local_solution_error_squared, local_projection_error_squared, local_solution_projection_gap_squared, - local_analytic_norm_squared, local_projected_norm_squared - }; - std::array global_values{}; - MPI_Allreduce( - local_values.data(), global_values.data(), static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, - fem.densityFes->GetComm() - ); - - double maximum_relative_error = 0.0; - MPI_Allreduce( - &local_maximum_relative_error, &maximum_relative_error, 1, MPI_DOUBLE, MPI_MAX, fem.densityFes->GetComm() - ); - - const double solution_relative_error = std::sqrt(global_values[0] / global_values[3]); - const double projection_relative_error = std::sqrt(global_values[1] / global_values[3]); - const double solution_projection_gap = std::sqrt(global_values[2] / global_values[4]); - - INFO( - "Ellipsoid coefficients = (" << analytic.coefficient_x << ", " << analytic.coefficient_y << ", " - << analytic.coefficient_z << ")" - ); - INFO("Analytic interior-potential L2 relative error = " << solution_relative_error); - INFO("Analytic-potential FE projection L2 relative error = " << projection_relative_error); - INFO("New-solver / analytic-potential projection relative gap = " << solution_projection_gap); - INFO("Maximum interior pointwise relative potential error = " << maximum_relative_error); - - REQUIRE(std::isfinite(solution_relative_error)); - REQUIRE(std::isfinite(projection_relative_error)); - REQUIRE(std::isfinite(solution_projection_gap)); - REQUIRE(std::isfinite(maximum_relative_error)); - - /* - * The field test establishes an O(1e-3) representation floor on this - * mesh. These are intentionally accuracy-regression guards, not claims - * of analytic convergence of the unrefined RT/L2 representation. - */ - CHECK(solution_relative_error < 1.0e-5); - CHECK(projection_relative_error < 1.0e-5); - CHECK(solution_projection_gap < 1.0e-5); - CHECK(maximum_relative_error < 1.0e-5); + } + + const std::array local_values{ + local_solution_error_squared, local_projection_error_squared, + local_solution_projection_gap_squared, local_analytic_norm_squared, + local_projected_norm_squared}; + std::array global_values{}; + MPI_Allreduce(local_values.data(), global_values.data(), + static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, + fem.densityFes->GetComm()); + + double maximum_relative_error = 0.0; + MPI_Allreduce(&local_maximum_relative_error, &maximum_relative_error, 1, + MPI_DOUBLE, MPI_MAX, fem.densityFes->GetComm()); + + const double solution_relative_error = + std::sqrt(global_values[0] / global_values[3]); + const double projection_relative_error = + std::sqrt(global_values[1] / global_values[3]); + const double solution_projection_gap = + std::sqrt(global_values[2] / global_values[4]); + + INFO("Ellipsoid coefficients = (" << analytic.coefficient_x << ", " + << analytic.coefficient_y << ", " + << analytic.coefficient_z << ")"); + INFO("Analytic interior-potential L2 relative error = " + << solution_relative_error); + INFO("Analytic-potential FE projection L2 relative error = " + << projection_relative_error); + INFO("New-solver / analytic-potential projection relative gap = " + << solution_projection_gap); + INFO("Maximum interior pointwise relative potential error = " + << maximum_relative_error); + + REQUIRE(std::isfinite(solution_relative_error)); + REQUIRE(std::isfinite(projection_relative_error)); + REQUIRE(std::isfinite(solution_projection_gap)); + REQUIRE(std::isfinite(maximum_relative_error)); + + /* + * On the regression mesh, the direct L2 projection floor is about 6.7e-6, + * the mixed-solve/projection gap is about 3.3e-5, and the maximum + * pointwise error is about 2e-4. These bounds guard those independently. + */ + CHECK(solution_relative_error < 5.0e-5); + CHECK(projection_relative_error < 1.0e-5); + CHECK(solution_projection_gap < 5.0e-5); + CHECK(maximum_relative_error < 2.5e-4); } struct FerrersN1Analytic { - double potential_constant; - std::array first_coefficients; - std::array, 3> second_coefficients; + double potential_constant; + std::array first_coefficients; + std::array, 3> second_coefficients; }; class FerrersVacuumMaskedCoefficient final : public mfem::Coefficient { public: - FerrersVacuumMaskedCoefficient( - Coefficient &coefficient, - const int vacuum_attribute - ) - : m_coefficient(coefficient), - m_vacuum_attribute(vacuum_attribute) { + FerrersVacuumMaskedCoefficient(Coefficient &coefficient, + const int vacuum_attribute) + : m_coefficient(coefficient), m_vacuum_attribute(vacuum_attribute) {} + + double Eval(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) override { + if (transformation.Attribute == m_vacuum_attribute) { + return 0.0; } - double Eval( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point - ) override { - if (transformation.Attribute == m_vacuum_attribute) { - return 0.0; - } - - return m_coefficient.Eval(transformation, integration_point); - } + return m_coefficient.Eval(transformation, integration_point); + } private: - Coefficient &m_coefficient; - int m_vacuum_attribute; + Coefficient &m_coefficient; + int m_vacuum_attribute; }; -static double compute_ferrers_n1_coefficient( - const std::array< - double, - 3> &semi_axes, - const int first_denominator_axis, - const int second_denominator_axis -) { - const double axis_product = semi_axes[0] * semi_axes[1] * semi_axes[2]; +static double +compute_ferrers_n1_coefficient(const std::array &semi_axes, + const int first_denominator_axis, + const int second_denominator_axis) { + const double axis_product = semi_axes[0] * semi_axes[1] * semi_axes[2]; - const double length_scale = std::cbrt(axis_product); + const double length_scale = std::cbrt(axis_product); - auto integrand = [semi_axes, axis_product, length_scale, first_denominator_axis, - second_denominator_axis](const double t) { - if (t <= 0.0 || t >= 1.0) { - return 0.0; - } - - /* - * Map u in [0, infinity) to t in [0, 1]: - * - * u = L^2 [t / (1 - t)]^2. - */ - const double one_minus_t = 1.0 - t; - const double s = t / one_minus_t; - - const double u = length_scale * length_scale * s * s; - - const double du_dt = length_scale * length_scale * 2.0 * s / (one_minus_t * one_minus_t); - - const double delta = std::sqrt( - (semi_axes[0] * semi_axes[0] + u) * (semi_axes[1] * semi_axes[1] + u) * (semi_axes[2] * semi_axes[2] + u) - ); - - double value = axis_product * du_dt / delta; - - if (first_denominator_axis >= 0) { - value /= semi_axes[first_denominator_axis] * semi_axes[first_denominator_axis] + u; - } - - if (second_denominator_axis >= 0) { - value /= semi_axes[second_denominator_axis] * semi_axes[second_denominator_axis] + u; - } - - return value; - }; - - double integration_error = 0.0; - - return boost::math::quadrature::gauss_kronrod::integrate( - integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error - ); -} - -static FerrersN1Analytic compute_ferrers_n1_analytic( - const double semi_axis_x, - const double semi_axis_y, - const double semi_axis_z -) { - const std::array semi_axes{semi_axis_x, semi_axis_y, semi_axis_z}; - - FerrersN1Analytic analytic{ - .potential_constant = compute_ferrers_n1_coefficient(semi_axes, -1, -1), - .first_coefficients = {}, - .second_coefficients = {} - }; - - for (int axis = 0; axis < 3; ++axis) { - analytic.first_coefficients[axis] = compute_ferrers_n1_coefficient(semi_axes, axis, -1); + auto integrand = [semi_axes, axis_product, length_scale, + first_denominator_axis, + second_denominator_axis](const double t) { + if (t <= 0.0 || t >= 1.0) { + return 0.0; } - for (int first_axis = 0; first_axis < 3; ++first_axis) { - for (int second_axis = first_axis; second_axis < 3; ++second_axis) { - const double coefficient = compute_ferrers_n1_coefficient(semi_axes, first_axis, second_axis); - - analytic.second_coefficients[first_axis][second_axis] = coefficient; - - analytic.second_coefficients[second_axis][first_axis] = coefficient; - } - } - - return analytic; -} - -static double evaluate_ferrers_n1_potential( - const mfem::Vector &position, - const double central_density, - const FerrersN1Analytic &analytic -) { - const std::array coordinate_squared{ - position(0) * position(0), position(1) * position(1), position(2) * position(2) - }; - /* - * Expansion of + * Map u in [0, infinity) to t in [0, 1]: * - * -pi G rho_c abc / 2 - * integral [(1 - m^2(u))^2 / Delta(u)] du. + * u = L^2 [t / (1 - t)]^2. */ - double potential_kernel = analytic.potential_constant; + const double one_minus_t = 1.0 - t; + const double s = t / one_minus_t; - for (int first_axis = 0; first_axis < 3; ++first_axis) { - potential_kernel -= 2.0 * analytic.first_coefficients[first_axis] * coordinate_squared[first_axis]; + const double u = length_scale * length_scale * s * s; - for (int second_axis = 0; second_axis < 3; ++second_axis) { - potential_kernel += analytic.second_coefficients[first_axis][second_axis] * coordinate_squared[first_axis] * - coordinate_squared[second_axis]; - } + const double du_dt = + length_scale * length_scale * 2.0 * s / (one_minus_t * one_minus_t); + + const double delta = std::sqrt((semi_axes[0] * semi_axes[0] + u) * + (semi_axes[1] * semi_axes[1] + u) * + (semi_axes[2] * semi_axes[2] + u)); + + double value = axis_product * du_dt / delta; + + if (first_denominator_axis >= 0) { + value /= semi_axes[first_denominator_axis] * + semi_axes[first_denominator_axis] + + u; } - return -0.5 * M_PI * utils::G * central_density * potential_kernel; + if (second_denominator_axis >= 0) { + value /= semi_axes[second_denominator_axis] * + semi_axes[second_denominator_axis] + + u; + } + + return value; + }; + + double integration_error = 0.0; + + return boost::math::quadrature::gauss_kronrod::integrate( + integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error); } -static void evaluate_ferrers_n1_gradient( - const mfem::Vector &position, - const double central_density, - const FerrersN1Analytic &analytic, - mfem::Vector &gradient -) { - gradient.SetSize(3); +static FerrersN1Analytic compute_ferrers_n1_analytic(const double semi_axis_x, + const double semi_axis_y, + const double semi_axis_z) { + const std::array semi_axes{semi_axis_x, semi_axis_y, semi_axis_z}; - const std::array coordinate_squared{ - position(0) * position(0), position(1) * position(1), position(2) * position(2) - }; + FerrersN1Analytic analytic{ + .potential_constant = compute_ferrers_n1_coefficient(semi_axes, -1, -1), + .first_coefficients = {}, + .second_coefficients = {}}; - for (int axis = 0; axis < 3; ++axis) { - double coefficient = analytic.first_coefficients[axis]; + for (int axis = 0; axis < 3; ++axis) { + analytic.first_coefficients[axis] = + compute_ferrers_n1_coefficient(semi_axes, axis, -1); + } - for (int other_axis = 0; other_axis < 3; ++other_axis) { - coefficient -= analytic.second_coefficients[axis][other_axis] * coordinate_squared[other_axis]; - } + for (int first_axis = 0; first_axis < 3; ++first_axis) { + for (int second_axis = first_axis; second_axis < 3; ++second_axis) { + const double coefficient = + compute_ferrers_n1_coefficient(semi_axes, first_axis, second_axis); - /* - * The solver stores grad(Phi), which points outward for a - * negative gravitational potential. - */ - gradient(axis) = 2.0 * M_PI * utils::G * central_density * position(axis) * coefficient; + analytic.second_coefficients[first_axis][second_axis] = coefficient; + + analytic.second_coefficients[second_axis][first_axis] = coefficient; } + } + + return analytic; } -TEST_CASE( - "New Gravity Potential Matches Analytic Ferrers Ellipsoid", - tags::gravity_analytic_accuracy -) { - auto args = test_utils::setup_args(); +static double evaluate_ferrers_n1_potential(const mfem::Vector &position, + const double central_density, + const FerrersN1Analytic &analytic) { + const std::array coordinate_squared{position(0) * position(0), + position(1) * position(1), + position(2) * position(2)}; - args.p.rtol = 1.0e-13; - args.p.atol = 1.0e-14; - args.p.max_iters = std::max(args.p.max_iters, 2000); + /* + * Expansion of + * + * -pi G rho_c abc / 2 + * integral [(1 - m^2(u))^2 / Delta(u)] du. + */ + double potential_kernel = analytic.potential_constant; - fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); + for (int first_axis = 0; first_axis < 3; ++first_axis) { + potential_kernel -= 2.0 * analytic.first_coefficients[first_axis] * + coordinate_squared[first_axis]; - REQUIRE(fem.mapping != nullptr); - REQUIRE(fem.domainMapperStateless != nullptr); - - const double radius = utils::RADIUS; - - const double mass = utils::MASS; - - constexpr double x_scale = 1.0; - constexpr double y_scale = 1.0; - constexpr double z_scale = 1.0 / (x_scale * y_scale); - - const double semi_axis_x = x_scale * radius; - const double semi_axis_y = y_scale * radius; - const double semi_axis_z = z_scale * radius; - - REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14)); - - auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - - value(0) = (x_scale - 1.0) * position(0); - value(1) = (y_scale - 1.0) * position(1); - value(2) = (z_scale - 1.0) * position(2); - }; - - mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); - - mfem::ParGridFunction displacement(fem.displacementFes.get()); - - displacement.ProjectCoefficient(displacement_coefficient); - - fem.mapping->SetDisplacement(displacement); - - physics::update_stiffness_matrix(fem); - - const int vacuum_attribute = fem.domainMapperStateless->GetVacuumElementAttribute(); - - /* - * For rho = rho_c (1 - m^2), the exact mass is - * - * M = 8 pi a b c rho_c / 15. - */ - const double central_density = 15.0 * mass / (8.0 * M_PI * semi_axis_x * semi_axis_y * semi_axis_z); - - auto density_function = [central_density, semi_axis_x, semi_axis_y, semi_axis_z](const mfem::Vector &position) { - const double ellipsoidal_radius_squared = position(0) * position(0) / (semi_axis_x * semi_axis_x) + - position(1) * position(1) / (semi_axis_y * semi_axis_y) + - position(2) * position(2) / (semi_axis_z * semi_axis_z); - - return central_density * std::max(0.0, 1.0 - ellipsoidal_radius_squared); - }; - - mapping::PhysicalPositionFunctionCoefficient physical_density_coefficient(*fem.mapping, density_function); - - FerrersVacuumMaskedCoefficient stellar_density_coefficient(physical_density_coefficient, vacuum_attribute); - - mfem::GridFunction density(fem.densityFes.get()); - - density = 0.0; - - density.ProjectCoefficient(stellar_density_coefficient); - - const double projected_mass = analysis::domain_integrate_grid_function(fem, density, utils::DOMAINS::STELLAR); - - REQUIRE(std::isfinite(projected_mass)); - REQUIRE(projected_mass > 0.0); - - /* - * Keep the projected source at exactly the requested mass. Because - * projection and scaling are linear, this also gives the central - * density appropriate to the represented source. - */ - const double density_scale = mass / projected_mass; - - density *= density_scale; - - const double represented_central_density = central_density * density_scale; - - fem.com = analysis::get_com(fem, density); - - fem.Q = physics::compute_quadrupole_moment_tensor(fem, density, fem.com); - - const double normalized_quadrupole = fem.Q.FNorm() / (mass * radius * radius); - - // REQUIRE(normalized_quadrupole > 1.0e-3); - - const FerrersN1Analytic analytic = compute_ferrers_n1_analytic(semi_axis_x, semi_axis_y, semi_axis_z); - - /* - * Independent analytic consistency checks. - * - * Sum(A_i) = 2 supplies the constant part of Poisson's - * equation. The B_ij identities supply the -m^2 part. - */ - const double first_coefficient_sum = - analytic.first_coefficients[0] + analytic.first_coefficients[1] + analytic.first_coefficients[2]; - - REQUIRE_THAT(first_coefficient_sum, Catch::Matchers::WithinAbs(2.0, 1.0e-11)); - - const std::array semi_axes_squared{ - semi_axis_x * semi_axis_x, semi_axis_y * semi_axis_y, semi_axis_z * semi_axis_z - }; - - for (int axis = 0; axis < 3; ++axis) { - double poisson_coefficient = 3.0 * analytic.second_coefficients[axis][axis]; - - for (int other_axis = 0; other_axis < 3; ++other_axis) { - if (other_axis != axis) { - poisson_coefficient += analytic.second_coefficients[axis][other_axis]; - } - } - - REQUIRE_THAT(poisson_coefficient, Catch::Matchers::WithinRel(2.0 / semi_axes_squared[axis], 1.0e-10)); + for (int second_axis = 0; second_axis < 3; ++second_axis) { + potential_kernel += + analytic.second_coefficients[first_axis][second_axis] * + coordinate_squared[first_axis] * coordinate_squared[second_axis]; } + } - const physics::GravitySolution solution = physics::grav_potential_new(fem, args, density, displacement); - - auto analytic_potential_function = [represented_central_density, analytic](const mfem::Vector &position) { - return evaluate_ferrers_n1_potential(position, represented_central_density, analytic); - }; - - mapping::PhysicalPositionFunctionCoefficient physical_potential_coefficient( - *fem.mapping, analytic_potential_function - ); - - /* - * This wrapper is required because scalar ProjectCoefficient has no - * attribute overload. It also prevents evaluation of the quartic - * interior formula in the compactified vacuum. - */ - FerrersVacuumMaskedCoefficient stellar_potential_coefficient(physical_potential_coefficient, vacuum_attribute); - - mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get()); - - projected_potential = 0.0; - - projected_potential.ProjectCoefficient(stellar_potential_coefficient); - - const int quadrature_order = get_gravity_quadrature_order(fem) + 4; - - double local_potential_error_squared = 0.0; - double local_projection_error_squared = 0.0; - double local_solution_projection_gap_squared = 0.0; - double local_potential_norm_squared = 0.0; - double local_projection_norm_squared = 0.0; - double local_field_error_squared = 0.0; - double local_field_norm_squared = 0.0; - double local_maximum_potential_error = 0.0; - - mfem::Vector physical_position(3); - mfem::Vector reference_field(3); - mfem::Vector physical_field(3); - mfem::Vector analytic_field(3); - mfem::Vector field_difference(3); - - mfem::DenseMatrix mapping_jacobian(3, 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); - - fem.mapping->GetPhysicalPoint(*transformation, point, physical_position); - - fem.mapping->ComputeJacobian(*transformation, mapping_jacobian); - - const double mapping_determinant = mapping_jacobian.Det(); - - MFEM_VERIFY( - std::isfinite(mapping_determinant) && mapping_determinant > 0.0, - "Ferrers test encountered an invalid mapping determinant." - ); - - const double expected_potential = - evaluate_ferrers_n1_potential(physical_position, represented_central_density, analytic); - - evaluate_ferrers_n1_gradient(physical_position, represented_central_density, analytic, analytic_field); - - const double computed_potential = solution.phi.GetValue(element_id, point); - - const double projected_potential_value = projected_potential.GetValue(element_id, point); - - solution.gradPhi.GetVectorValue(element_id, point, reference_field); - - mapping_jacobian.Mult(reference_field, physical_field); - - physical_field /= mapping_determinant; - - field_difference = physical_field; - - field_difference -= analytic_field; - - const double weight = point.weight * transformation->Weight() * mapping_determinant; - - const double potential_error = computed_potential - expected_potential; - - const double projection_error = projected_potential_value - expected_potential; - - const double solution_projection_difference = computed_potential - projected_potential_value; - - local_potential_error_squared += weight * potential_error * potential_error; - - local_projection_error_squared += weight * projection_error * projection_error; - - local_solution_projection_gap_squared += - weight * solution_projection_difference * solution_projection_difference; - - local_potential_norm_squared += weight * expected_potential * expected_potential; - - local_projection_norm_squared += weight * projected_potential_value * projected_potential_value; - - local_field_error_squared += weight * (field_difference * field_difference); - - local_field_norm_squared += weight * (analytic_field * analytic_field); - - local_maximum_potential_error = std::max( - local_maximum_potential_error, - std::abs(potential_error) / - std::max(std::abs(expected_potential), std::numeric_limits::epsilon()) - ); - } - } - - const std::array local_values{ - local_potential_error_squared, local_projection_error_squared, local_solution_projection_gap_squared, - local_potential_norm_squared, local_projection_norm_squared, local_field_error_squared, - local_field_norm_squared - }; - - std::array global_values{}; - - MPI_Allreduce( - local_values.data(), global_values.data(), static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, - fem.densityFes->GetComm() - ); - - double maximum_potential_error = 0.0; - - MPI_Allreduce( - &local_maximum_potential_error, &maximum_potential_error, 1, MPI_DOUBLE, MPI_MAX, fem.densityFes->GetComm() - ); - - REQUIRE(global_values[3] > 0.0); - REQUIRE(global_values[4] > 0.0); - REQUIRE(global_values[6] > 0.0); - - const double potential_relative_error = std::sqrt(global_values[0] / global_values[3]); - - const double projection_relative_error = std::sqrt(global_values[1] / global_values[3]); - - const double solution_projection_gap = std::sqrt(global_values[2] / global_values[4]); - - const double field_relative_error = std::sqrt(global_values[5] / global_values[6]); - - INFO("Projected mass before normalization = " << projected_mass); - - INFO("Density normalization factor = " << density_scale); - - INFO("Normalized quadrupole = " << normalized_quadrupole); - - INFO("Ferrers potential L2 relative error = " << potential_relative_error); - - INFO("Ferrers potential FE-projection relative error = " << projection_relative_error); - - INFO("New-solver / Ferrers-potential projection gap = " << solution_projection_gap); - - INFO("Ferrers field L2 relative error = " << field_relative_error); - - INFO("Maximum interior pointwise potential relative error = " << maximum_potential_error); - - REQUIRE(std::isfinite(potential_relative_error)); - REQUIRE(std::isfinite(projection_relative_error)); - REQUIRE(std::isfinite(solution_projection_gap)); - REQUIRE(std::isfinite(field_relative_error)); - REQUIRE(std::isfinite(maximum_potential_error)); - - /* - * Initial characterization guards. Unlike the homogeneous case, - * this exact potential is quartic, so the FE representation floor - * will generally be higher. Record the values before deciding - * whether tighter regression thresholds are appropriate. - */ - CHECK(potential_relative_error < 1.0e-5); - CHECK(projection_relative_error < 1.0e-5); - CHECK(solution_projection_gap < 1.0e-5); - CHECK(field_relative_error < 1.0e-5); - CHECK(maximum_potential_error < 1.0e-5); + return -0.5 * M_PI * utils::G * central_density * potential_kernel; +} + +static void evaluate_ferrers_n1_gradient(const mfem::Vector &position, + const double central_density, + const FerrersN1Analytic &analytic, + mfem::Vector &gradient) { + gradient.SetSize(3); + + const std::array coordinate_squared{position(0) * position(0), + position(1) * position(1), + position(2) * position(2)}; + + for (int axis = 0; axis < 3; ++axis) { + double coefficient = analytic.first_coefficients[axis]; + + for (int other_axis = 0; other_axis < 3; ++other_axis) { + coefficient -= analytic.second_coefficients[axis][other_axis] * + coordinate_squared[other_axis]; + } + + /* + * The solver stores grad(Phi), which points outward for a + * negative gravitational potential. + */ + gradient(axis) = + 2.0 * M_PI * utils::G * central_density * position(axis) * coefficient; + } +} + +TEST_CASE("Gravity Field Matches Analytic Ferrers Ellipsoid", + tags::gravity_analytic_accuracy) { + auto args = test_utils::setup_args(); + + args.p.rtol = 1.0e-13; + args.p.atol = 1.0e-14; + args.p.max_iters = std::max(args.p.max_iters, 2000); + + fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); + + REQUIRE(fem.domainMapperStateless != nullptr); + REQUIRE(fem.domainMapperStateless != nullptr); + + const double radius = utils::RADIUS; + + const double mass = utils::MASS; + + constexpr double x_scale = 1.0; + constexpr double y_scale = 1.0; + constexpr double z_scale = 1.0 / (x_scale * y_scale); + + const double semi_axis_x = x_scale * radius; + const double semi_axis_y = y_scale * radius; + const double semi_axis_z = z_scale * radius; + + REQUIRE_THAT(x_scale * y_scale * z_scale, + Catch::Matchers::WithinAbs(1.0, 1.0e-14)); + + auto displacement_function = [](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + + value(0) = (x_scale - 1.0) * position(0); + value(1) = (y_scale - 1.0) * position(1); + value(2) = (z_scale - 1.0) * position(2); + }; + + mfem::VectorFunctionCoefficient displacement_coefficient( + 3, displacement_function); + + mfem::ParGridFunction displacement(fem.displacementFes.get()); + + displacement.ProjectCoefficient(displacement_coefficient); + + *fem.displacement = displacement; + + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + + /* + * For rho = rho_c (1 - m^2), the exact mass is + * + * M = 8 pi a b c rho_c / 15. + */ + const double central_density = + 15.0 * mass / (8.0 * M_PI * semi_axis_x * semi_axis_y * semi_axis_z); + + auto density_function = [central_density, semi_axis_x, semi_axis_y, + semi_axis_z](const mfem::Vector &position) { + const double ellipsoidal_radius_squared = + position(0) * position(0) / (semi_axis_x * semi_axis_x) + + position(1) * position(1) / (semi_axis_y * semi_axis_y) + + position(2) * position(2) / (semi_axis_z * semi_axis_z); + + return central_density * std::max(0.0, 1.0 - ellipsoidal_radius_squared); + }; + + mapping::PhysicalPositionFunctionCoefficient physical_density_coefficient( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate, density_function); + + FerrersVacuumMaskedCoefficient stellar_density_coefficient( + physical_density_coefficient, vacuum_attribute); + + mfem::GridFunction density(fem.densityFes.get()); + + density = 0.0; + + density.ProjectCoefficient(stellar_density_coefficient); + + const double projected_mass = analysis::domain_integrate_grid_function( + fem, density, utils::DOMAINS::STELLAR); + + REQUIRE(std::isfinite(projected_mass)); + REQUIRE(projected_mass > 0.0); + + /* + * Keep the projected source at exactly the requested mass. Because + * projection and scaling are linear, this also gives the central + * density appropriate to the represented source. + */ + const double density_scale = mass / projected_mass; + + density *= density_scale; + + const double represented_central_density = central_density * density_scale; + + fem.com = analysis::get_com(fem, density); + + fem.Q = physics::compute_quadrupole_moment_tensor(fem, density, fem.com); + + const double normalized_quadrupole = fem.Q.FNorm() / (mass * radius * radius); + + // REQUIRE(normalized_quadrupole > 1.0e-3); + + const FerrersN1Analytic analytic = + compute_ferrers_n1_analytic(semi_axis_x, semi_axis_y, semi_axis_z); + + /* + * Independent analytic consistency checks. + * + * Sum(A_i) = 2 supplies the constant part of Poisson's + * equation. The B_ij identities supply the -m^2 part. + */ + const double first_coefficient_sum = analytic.first_coefficients[0] + + analytic.first_coefficients[1] + + analytic.first_coefficients[2]; + + REQUIRE_THAT(first_coefficient_sum, Catch::Matchers::WithinAbs(2.0, 1.0e-11)); + + const std::array semi_axes_squared{semi_axis_x * semi_axis_x, + semi_axis_y * semi_axis_y, + semi_axis_z * semi_axis_z}; + + for (int axis = 0; axis < 3; ++axis) { + double poisson_coefficient = 3.0 * analytic.second_coefficients[axis][axis]; + + for (int other_axis = 0; other_axis < 3; ++other_axis) { + if (other_axis != axis) { + poisson_coefficient += analytic.second_coefficients[axis][other_axis]; + } + } + + REQUIRE_THAT( + poisson_coefficient, + Catch::Matchers::WithinRel(2.0 / semi_axes_squared[axis], 1.0e-10)); + } + + const physics::GravitySolution solution = + physics::solve_gravity_field(fem, args, density, displacement); + + auto analytic_potential_function = [represented_central_density, + analytic](const mfem::Vector &position) { + return evaluate_ferrers_n1_potential(position, represented_central_density, + analytic); + }; + + mapping::PhysicalPositionFunctionCoefficient physical_potential_coefficient( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate, analytic_potential_function); + + /* + * This wrapper is required because scalar ProjectCoefficient has no + * attribute overload. It also prevents evaluation of the quartic + * interior formula in the compactified vacuum. + */ + FerrersVacuumMaskedCoefficient stellar_potential_coefficient( + physical_potential_coefficient, vacuum_attribute); + + mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get()); + + projected_potential = 0.0; + + projected_potential.ProjectCoefficient(stellar_potential_coefficient); + + const int quadrature_order = get_gravity_quadrature_order(fem) + 4; + + double local_potential_error_squared = 0.0; + double local_projection_error_squared = 0.0; + double local_solution_projection_gap_squared = 0.0; + double local_potential_norm_squared = 0.0; + double local_projection_norm_squared = 0.0; + double local_field_error_squared = 0.0; + double local_field_norm_squared = 0.0; + double local_maximum_potential_error = 0.0; + + mfem::Vector physical_position(3); + mfem::Vector reference_field(3); + mfem::Vector physical_field(3); + mfem::Vector analytic_field(3); + mfem::Vector field_difference(3); + + mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, + *fem.compactificationCoordinate); + + 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); + + mapping::MappingPointContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluatePoint(*transformation, point, + mapping_context) == + mapping::MappingStatus::valid, + "Ferrers test encountered an invalid mapping."); + 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(std::isfinite(mapping_determinant) && + mapping_determinant > 0.0, + "Ferrers test encountered an invalid mapping determinant."); + + const double expected_potential = evaluate_ferrers_n1_potential( + physical_position, represented_central_density, analytic); + + evaluate_ferrers_n1_gradient(physical_position, + represented_central_density, analytic, + analytic_field); + + const double computed_potential = + solution.phi.GetValue(element_id, point); + + const double projected_potential_value = + projected_potential.GetValue(element_id, point); + + solution.gradPhi.GetVectorValue(element_id, point, reference_field); + + mapping_jacobian.Mult(reference_field, physical_field); + + physical_field /= mapping_determinant; + + field_difference = physical_field; + + field_difference -= analytic_field; + + const double weight = + point.weight * transformation->Weight() * mapping_determinant; + + const double potential_error = computed_potential - expected_potential; + + const double projection_error = + projected_potential_value - expected_potential; + + const double solution_projection_difference = + computed_potential - projected_potential_value; + + local_potential_error_squared += + weight * potential_error * potential_error; + + local_projection_error_squared += + weight * projection_error * projection_error; + + local_solution_projection_gap_squared += weight * + solution_projection_difference * + solution_projection_difference; + + local_potential_norm_squared += + weight * expected_potential * expected_potential; + + local_projection_norm_squared += + weight * projected_potential_value * projected_potential_value; + + local_field_error_squared += + weight * (field_difference * field_difference); + + local_field_norm_squared += weight * (analytic_field * analytic_field); + + local_maximum_potential_error = + std::max(local_maximum_potential_error, + std::abs(potential_error) / + std::max(std::abs(expected_potential), + std::numeric_limits::epsilon())); + } + } + + const std::array local_values{ + local_potential_error_squared, + local_projection_error_squared, + local_solution_projection_gap_squared, + local_potential_norm_squared, + local_projection_norm_squared, + local_field_error_squared, + local_field_norm_squared}; + + std::array global_values{}; + + MPI_Allreduce(local_values.data(), global_values.data(), + static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, + fem.densityFes->GetComm()); + + double maximum_potential_error = 0.0; + + MPI_Allreduce(&local_maximum_potential_error, &maximum_potential_error, 1, + MPI_DOUBLE, MPI_MAX, fem.densityFes->GetComm()); + + REQUIRE(global_values[3] > 0.0); + REQUIRE(global_values[4] > 0.0); + REQUIRE(global_values[6] > 0.0); + + const double potential_relative_error = + std::sqrt(global_values[0] / global_values[3]); + + const double projection_relative_error = + std::sqrt(global_values[1] / global_values[3]); + + const double solution_projection_gap = + std::sqrt(global_values[2] / global_values[4]); + + const double field_relative_error = + std::sqrt(global_values[5] / global_values[6]); + + INFO("Projected mass before normalization = " << projected_mass); + + INFO("Density normalization factor = " << density_scale); + + INFO("Normalized quadrupole = " << normalized_quadrupole); + + INFO("Ferrers potential L2 relative error = " << potential_relative_error); + + INFO("Ferrers potential FE-projection relative error = " + << projection_relative_error); + + INFO("New-solver / Ferrers-potential projection gap = " + << solution_projection_gap); + + INFO("Ferrers field L2 relative error = " << field_relative_error); + + INFO("Maximum interior pointwise potential relative error = " + << maximum_potential_error); + + REQUIRE(std::isfinite(potential_relative_error)); + REQUIRE(std::isfinite(projection_relative_error)); + REQUIRE(std::isfinite(solution_projection_gap)); + REQUIRE(std::isfinite(field_relative_error)); + REQUIRE(std::isfinite(maximum_potential_error)); + + /* + * On the regression mesh, the quartic analytic potential and its RT + * gradient have representation floors of about 1.1e-4 and 8.7e-4, + * respectively. The mixed solution remains much closer to the direct + * FE projection, with a solution/projection gap below 1e-5. + */ + CHECK(potential_relative_error < 1.5e-4); + CHECK(projection_relative_error < 1.5e-4); + CHECK(solution_projection_gap < 1.0e-5); + CHECK(field_relative_error < 1.0e-3); + CHECK(maximum_potential_error < 5.0e-4); } diff --git a/tests/physics/gravity_monopole_accuracy.cpp b/tests/physics/gravity_monopole_accuracy.cpp index 7679733..c6e9f51 100644 --- a/tests/physics/gravity_monopole_accuracy.cpp +++ b/tests/physics/gravity_monopole_accuracy.cpp @@ -14,1160 +14,1201 @@ import mean_field; import test_helpers; namespace { - constexpr std::array shell_boundaries{0.0, 0.25, 0.50, 0.75, 0.90, 1.0}; - constexpr int shell_count = static_cast(shell_boundaries.size()) - 1; +constexpr std::array shell_boundaries{0.0, 0.25, 0.50, + 0.75, 0.90, 1.0}; +constexpr int shell_count = static_cast(shell_boundaries.size()) - 1; - enum class MappingPath { legacy, stateless }; +struct ShellAccumulator { + long long points{0}; + double weight{0.0}; + double minimum_radius{std::numeric_limits::infinity()}; + double maximum_radius{0.0}; + double potential_error_squared{0.0}; + double radial_error_squared{0.0}; + double tangential_squared{0.0}; +}; - struct ShellAccumulator { - long long points{0}; - double weight{0.0}; - double minimum_radius{std::numeric_limits::infinity()}; - double maximum_radius{0.0}; - double potential_error_squared{0.0}; - double radial_error_squared{0.0}; - double tangential_squared{0.0}; - }; +struct ShellMetrics { + long long points{0}; + double minimum_radius{0.0}; + double maximum_radius{0.0}; + double potential_rms_error{0.0}; + double radial_rms_error{0.0}; + double tangential_rms{0.0}; +}; - struct ShellMetrics { - long long points{0}; - double minimum_radius{0.0}; - double maximum_radius{0.0}; - double potential_rms_error{0.0}; - double radial_rms_error{0.0}; - double tangential_rms{0.0}; - }; +struct ShellMeasurement { + std::array shells{}; + long long invalid_points{0}; +}; - struct ShellMeasurement { - std::array shells{}; - long long invalid_points{0}; - }; +constexpr int mapping_status_count = 8; - constexpr int mapping_status_count = 8; +struct GravitationalEnergies { + double binding{0.0}; + double virial{0.0}; + double minimum_mapping_determinant{std::numeric_limits::infinity()}; + double maximum_mapping_determinant{-std::numeric_limits::infinity()}; + long long invalid_points{0}; + std::array mapping_status_counts{}; + int first_invalid_element{-1}; + int first_invalid_attribute{-1}; + int first_invalid_quadrature_point{-1}; + double first_invalid_determinant{std::numeric_limits::quiet_NaN()}; +}; - struct GravitationalEnergies { - double binding{0.0}; - double virial{0.0}; - double minimum_mapping_determinant{std::numeric_limits::infinity()}; - double maximum_mapping_determinant{-std::numeric_limits::infinity()}; - long long invalid_points{0}; - std::array mapping_status_counts{}; - int first_invalid_element{-1}; - int first_invalid_attribute{-1}; - int first_invalid_quadrature_point{-1}; - double first_invalid_determinant{std::numeric_limits::quiet_NaN()}; - }; +constexpr int +mapping_status_index(const mean_field::mapping::MappingStatus status) { + return static_cast(status); +} - constexpr int mapping_status_index(const mean_field::mapping::MappingStatus status) { - return static_cast(status); +void zero_vacuum_density(const mean_field::fem::FEM &f, + mfem::GridFunction &density) { + using DomainSchema = + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + + const mean_field::field::FieldDofMap densityMap = + mean_field::field::make_field_dof_map(*f.densityFes); + + mfem::Vector densityTrue; + density.GetTrueDofs(densityTrue); + + const mfem::Vector supportedDensity = densityMap.gather(densityTrue); + densityMap.scatter(supportedDensity, densityTrue); + density.SetFromTrueDofs(densityTrue); +} + +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); +} + +double global_dot(const mfem::Vector &lhs, const mfem::Vector &rhs, + MPI_Comm communicator) { + const double local_dot = lhs * rhs; + double result = 0.0; + MPI_Allreduce(&local_dot, &result, 1, MPI_DOUBLE, MPI_SUM, communicator); + return result; +} + +double global_relative_error(const mfem::Vector &computed, + const mfem::Vector &reference, + MPI_Comm communicator) { + REQUIRE(computed.Size() == reference.Size()); + + mfem::Vector difference(computed); + difference -= reference; + + return global_norm(difference, communicator) / + std::max(global_norm(reference, communicator), + std::numeric_limits::epsilon()); +} + +int get_shell(const double coordinate) { + const double clamped = std::clamp(coordinate, 0.0, std::nextafter(1.0, 0.0)); + + for (int shell = 0; shell < shell_count; ++shell) { + if (clamped < shell_boundaries[shell + 1]) { + return shell; + } + } + + return shell_count - 1; +} + +bool retryable_infinity_status( + const mean_field::mapping::MappingStatus status) { + return status == + mean_field::mapping::MappingStatus::at_compactified_infinity || + status == + mean_field::mapping::MappingStatus::outside_reference_domain || + status == mean_field::mapping::MappingStatus::non_finite_result || + status == mean_field::mapping::MappingStatus::non_positive_determinant; +} + +class ProjectionGeometry { +public: + ProjectionGeometry(const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement) + : m_fem(f), m_mapper(mapper), m_displacement(displacement), + m_workspace(f.mesh->Dimension()) {} + + mean_field::mapping::MappingStatus + Evaluate(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + mean_field::mapping::MappingPointContext &context, + const bool permit_infinity_limit) { + m_used_infinity_limit = false; + + const int element_id = transformation.ElementNo; + MFEM_VERIFY(element_id >= 0, + "Projection coefficient received an invalid element number."); + + const mfem::FiniteElement &displacement_element = + *m_fem.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *m_fem.compactificationFes->GetFE(element_id); + + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; + + mfem::DofTransformation *displacement_transform = + m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); + + mfem::DofTransformation *compactification_transform = + m_fem.compactificationFes->GetElementDofs(element_id, + compactification_dofs); + + mfem::Vector element_displacement; + mfem::Vector element_compactification; + + m_displacement.GetSubVector(displacement_dofs, element_displacement); + m_fem.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); + + if (displacement_transform != nullptr) { + displacement_transform->InvTransformPrimal(element_displacement); } - void zero_vacuum_density( - const mean_field::fem::FEM &f, - mfem::GridFunction &density - ) { - for (int i = 0; i < f.vacuumDensityTdofs.Size(); ++i) { - density(f.vacuumDensityTdofs[i]) = 0.0; - } + if (compactification_transform != nullptr) { + compactification_transform->InvTransformPrimal(element_compactification); } - 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); + const mean_field::mapping::ElementDisplacementData displacement_data = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement); + + const mean_field::mapping::ElementCompactificationData + compactification_data(compactification_element, + element_compactification); + + const mean_field::mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; + + mfem::Vector compactification_shape(compactification_element.GetDof()); + compactification_element.CalcShape(integration_point, + compactification_shape); + + const double coordinate = element_compactification * compactification_shape; + + mean_field::mapping::MappingStatus status = m_mapper.EvaluatePoint( + mapping_data, transformation, integration_point, m_workspace, context); + + if (status == mean_field::mapping::MappingStatus::valid) { + transformation.SetIntPoint(&integration_point); + return status; } - double global_dot( - const mfem::Vector &lhs, - const mfem::Vector &rhs, - MPI_Comm communicator - ) { - const double local_dot = lhs * rhs; - double result = 0.0; - MPI_Allreduce(&local_dot, &result, 1, MPI_DOUBLE, MPI_SUM, communicator); - return result; + const bool infinity_request = + m_mapper.IsCompactifiedElement(transformation) && + coordinate >= 1.0 - 1.0e-10; + + if (!permit_infinity_limit || !infinity_request || + !retryable_infinity_status(status)) { + transformation.SetIntPoint(&integration_point); + return status; } - double global_relative_error( - const mfem::Vector &computed, - const mfem::Vector &reference, - MPI_Comm communicator - ) { - REQUIRE(computed.Size() == reference.Size()); + const mfem::IntegrationPoint ¢er = + mfem::Geometries.GetCenter(transformation.GetGeometryType()); - mfem::Vector difference(computed); - difference -= reference; + constexpr std::array inward_fractions{ + 1.0e-12, 1.0e-11, 1.0e-10, 1.0e-9, 1.0e-8, 1.0e-7, + 1.0e-6, 1.0e-5, 1.0e-4, 1.0e-3, 1.0e-2}; - return global_norm(difference, communicator) / - std::max(global_norm(reference, communicator), std::numeric_limits::epsilon()); + for (const double fraction : inward_fractions) { + mfem::IntegrationPoint inward; + inward.x = (1.0 - fraction) * integration_point.x + fraction * center.x; + inward.y = (1.0 - fraction) * integration_point.y + fraction * center.y; + inward.z = (1.0 - fraction) * integration_point.z + fraction * center.z; + inward.weight = integration_point.weight; + + status = m_mapper.EvaluatePoint(mapping_data, transformation, inward, + m_workspace, context); + + if (status == mean_field::mapping::MappingStatus::valid) { + m_used_infinity_limit = true; + transformation.SetIntPoint(&integration_point); + return status; + } + + if (!retryable_infinity_status(status)) { + break; + } } - int get_shell(const double coordinate) { - const double clamped = std::clamp(coordinate, 0.0, std::nextafter(1.0, 0.0)); + transformation.SetIntPoint(&integration_point); + return status; + } - for (int shell = 0; shell < shell_count; ++shell) { - if (clamped < shell_boundaries[shell + 1]) { - return shell; - } - } + [[nodiscard]] bool UsedInfinityLimit() const noexcept { + return m_used_infinity_limit; + } - return shell_count - 1; - } +private: + const mean_field::fem::FEM &m_fem; + const mean_field::mapping::DomainMapper &m_mapper; + const mfem::GridFunction &m_displacement; + mean_field::mapping::DomainMapper::Workspace m_workspace; + bool m_used_infinity_limit{false}; +}; - bool retryable_infinity_status(const mean_field::mapping::MappingStatus status) { - return status == mean_field::mapping::MappingStatus::at_compactified_infinity || - status == mean_field::mapping::MappingStatus::outside_reference_domain || - status == mean_field::mapping::MappingStatus::non_finite_result || - status == mean_field::mapping::MappingStatus::non_positive_determinant; - } +class MonopolePotentialCoefficient final : public mfem::Coefficient { +public: + MonopolePotentialCoefficient( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &mapper, + const mfem::GridFunction &displacement, const double mass, + const double radius) + : m_geometry(f, mapper, displacement), + m_vacuum_attribute(field_dof_test_utils::vacuum_material_attribute), + m_mass(mass), m_radius(radius) {} - class ProjectionGeometry { - public: - ProjectionGeometry( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &mapper, - const mfem::GridFunction &displacement - ) - : m_fem(f), - m_mapper(mapper), - m_displacement(displacement), - m_workspace(f.mesh->Dimension()) { - } + double Eval(mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point) override { + mean_field::mapping::MappingPointContext context; - mean_field::mapping::MappingStatus Evaluate( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point, - mean_field::mapping::MappingPointContext &context, - const bool permit_infinity_limit - ) { - m_used_infinity_limit = false; + const mean_field::mapping::MappingStatus status = + m_geometry.Evaluate(transformation, integration_point, context, true); - const int element_id = transformation.ElementNo; - MFEM_VERIFY(element_id >= 0, "Projection coefficient received an invalid element number."); - - const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id); - - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; - - mfem::DofTransformation *displacement_transform = - m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); - - mfem::DofTransformation *compactification_transform = - m_fem.compactificationFes->GetElementDofs(element_id, compactification_dofs); - - mfem::Vector element_displacement; - mfem::Vector element_compactification; - - m_displacement.GetSubVector(displacement_dofs, element_displacement); - m_fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - - if (displacement_transform != nullptr) { - displacement_transform->InvTransformPrimal(element_displacement); - } - - if (compactification_transform != nullptr) { - compactification_transform->InvTransformPrimal(element_compactification); - } - - const mean_field::mapping::ElementDisplacementData displacement_data = - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, element_displacement - ); - - const mean_field::mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - mfem::Vector compactification_shape(compactification_element.GetDof()); - compactification_element.CalcShape(integration_point, compactification_shape); - - const double coordinate = element_compactification * compactification_shape; - - mean_field::mapping::MappingStatus status = - m_mapper.EvaluatePoint(mapping_data, transformation, integration_point, m_workspace, context); - - if (status == mean_field::mapping::MappingStatus::valid) { - transformation.SetIntPoint(&integration_point); - return status; - } - - const bool infinity_request = m_mapper.IsCompactifiedElement(transformation) && coordinate >= 1.0 - 1.0e-10; - - if (!permit_infinity_limit || !infinity_request || !retryable_infinity_status(status)) { - transformation.SetIntPoint(&integration_point); - return status; - } - - const mfem::IntegrationPoint ¢er = mfem::Geometries.GetCenter(transformation.GetGeometryType()); - - constexpr std::array inward_fractions{1.0e-12, 1.0e-11, 1.0e-10, 1.0e-9, 1.0e-8, 1.0e-7, - 1.0e-6, 1.0e-5, 1.0e-4, 1.0e-3, 1.0e-2}; - - for (const double fraction : inward_fractions) { - mfem::IntegrationPoint inward; - inward.x = (1.0 - fraction) * integration_point.x + fraction * center.x; - inward.y = (1.0 - fraction) * integration_point.y + fraction * center.y; - inward.z = (1.0 - fraction) * integration_point.z + fraction * center.z; - inward.weight = integration_point.weight; - - status = m_mapper.EvaluatePoint(mapping_data, transformation, inward, m_workspace, context); - - if (status == mean_field::mapping::MappingStatus::valid) { - m_used_infinity_limit = true; - transformation.SetIntPoint(&integration_point); - return status; - } - - if (!retryable_infinity_status(status)) { - break; - } - } - - transformation.SetIntPoint(&integration_point); - return status; - } - - [[nodiscard]] bool UsedInfinityLimit() const noexcept { - return m_used_infinity_limit; - } - - private: - const mean_field::fem::FEM &m_fem; - const mean_field::mapping::DomainMapperStateless &m_mapper; - const mfem::GridFunction &m_displacement; - mean_field::mapping::DomainMapperStateless::Workspace m_workspace; - bool m_used_infinity_limit{false}; - }; - - class MonopolePotentialCoefficient final : public mfem::Coefficient { - public: - MonopolePotentialCoefficient( - const mean_field::fem::FEM &f, - const mean_field::mapping::DomainMapperStateless &mapper, - const mfem::GridFunction &displacement, - const double mass, - const double radius - ) - : m_geometry( - f, - mapper, - displacement - ), - m_vacuum_attribute(mapper.GetVacuumElementAttribute()), - m_mass(mass), - m_radius(radius) { - } - - double Eval( - mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point - ) override { - mean_field::mapping::MappingPointContext context; - - const mean_field::mapping::MappingStatus status = - m_geometry.Evaluate(transformation, integration_point, context, true); - - MFEM_VERIFY( - status == mean_field::mapping::MappingStatus::valid, + MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, "Stateless monopole-potential projection failed with status " - << static_cast(status) << " on element " << transformation.ElementNo << '.' - ); + << static_cast(status) << " on element " + << transformation.ElementNo << '.'); - if (m_geometry.UsedInfinityLimit()) { - return 0.0; - } - - const double radius = context.physical_position.Norml2(); - - MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid monopole projection radius."); - - if (transformation.Attribute == m_vacuum_attribute) { - return -mean_field::utils::G * m_mass / radius; - } - - return -mean_field::utils::G * m_mass * (3.0 * m_radius * m_radius - radius * radius) / - (2.0 * m_radius * m_radius * m_radius); - } - - private: - ProjectionGeometry m_geometry; - int m_vacuum_attribute; - double m_mass; - double m_radius; - }; - - void local_to_true( - const mfem::ParFiniteElementSpace &space, - const mfem::Vector &local, - mfem::Vector &true_vector - ) { - true_vector.SetSize(space.GetTrueVSize()); - true_vector = 0.0; - - const mfem::Operator *prolongation = space.GetProlongationMatrix(); - - if (prolongation != nullptr) { - prolongation->MultTranspose(local, true_vector); - } else { - true_vector = local; - } + if (m_geometry.UsedInfinityLimit()) { + return 0.0; } - mfem::Vector assemble_monopole_projection_rhs( - mean_field::fem::FEM &f, - const mfem::GridFunction &displacement, - const double mass, - const double stellar_radius - ) { - mfem::Vector local_rhs(f.gravityFluxFes->GetVSize()); - local_rhs = 0.0; + const double radius = context.physical_position.Norml2(); - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, + "Invalid monopole projection radius."); - const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute(); - const int quadrature_order = 2 * f.gravityFluxFes->GetMaxElementOrder() + 8; + if (transformation.Attribute == m_vacuum_attribute) { + return -mean_field::utils::G * m_mass / radius; + } - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - const mfem::FiniteElement &gravity_element = *f.gravityFluxFes->GetFE(element_id); - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); + return -mean_field::utils::G * m_mass * + (3.0 * m_radius * m_radius - radius * radius) / + (2.0 * m_radius * m_radius * m_radius); + } - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); +private: + ProjectionGeometry m_geometry; + int m_vacuum_attribute; + double m_mass; + double m_radius; +}; - mfem::Array gravity_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; +void local_to_true(const mfem::ParFiniteElementSpace &space, + const mfem::Vector &local, mfem::Vector &true_vector) { + true_vector.SetSize(space.GetTrueVSize()); + true_vector = 0.0; - mfem::DofTransformation *gravity_transform = f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); - mfem::DofTransformation *displacement_transform = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_transform = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); + const mfem::Operator *prolongation = space.GetProlongationMatrix(); - mfem::Vector element_displacement; - mfem::Vector element_compactification; + if (prolongation != nullptr) { + prolongation->MultTranspose(local, true_vector); + } else { + true_vector = local; + } +} - displacement.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); +mfem::Vector assemble_monopole_projection_rhs( + mean_field::fem::FEM &f, const mfem::GridFunction &displacement, + const double mass, const double stellar_radius) { + mfem::Vector local_rhs(f.gravityFluxFes->GetVSize()); + local_rhs = 0.0; - if (displacement_transform != nullptr) { - displacement_transform->InvTransformPrimal(element_displacement); - } + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); - if (compactification_transform != nullptr) { - compactification_transform->InvTransformPrimal(element_compactification); - } + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + const int quadrature_order = 2 * f.gravityFluxFes->GetMaxElementOrder() + 8; - const mean_field::mapping::ElementDisplacementData displacement_data = - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, element_displacement - ); + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + const mfem::FiniteElement &gravity_element = + *f.gravityFluxFes->GetFE(element_id); + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); - const mean_field::mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; + mfem::Array gravity_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - const int dof_count = gravity_element.GetDof(); - const int dimension = transformation->GetSpaceDim(); + mfem::DofTransformation *gravity_transform = + f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); + mfem::DofTransformation *displacement_transform = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + mfem::DofTransformation *compactification_transform = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); - mfem::Vector element_rhs(dof_count); - mfem::Vector analytic_field(dimension); - mfem::Vector pulled_rhs_field(dimension); - mfem::DenseMatrix vector_shape(dof_count, dimension); + mfem::Vector element_displacement; + mfem::Vector element_compactification; - element_rhs = 0.0; + displacement.GetSubVector(displacement_dofs, element_displacement); + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); - const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + if (displacement_transform != nullptr) { + displacement_transform->InvTransformPrimal(element_displacement); + } - for (int q = 0; q < rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &point = rule.IntPoint(q); + if (compactification_transform != nullptr) { + compactification_transform->InvTransformPrimal(element_compactification); + } - mean_field::mapping::VolumeMappingContext context; + const mean_field::mapping::ElementDisplacementData displacement_data = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement); - const mean_field::mapping::MappingStatus status = - f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation, point, workspace, context); + const mean_field::mapping::ElementCompactificationData + compactification_data(compactification_element, + element_compactification); - MFEM_VERIFY( - status == mean_field::mapping::MappingStatus::valid, - "Mapped monopole projection RHS failed with status " - << static_cast(status) << " on element " << element_id << ", quadrature point " << q << '.' - ); + const mean_field::mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; - analytic_field = context.mapping.physical_position; + const int dof_count = gravity_element.GetDof(); + const int dimension = transformation->GetSpaceDim(); - const double radius = analytic_field.Norml2(); + mfem::Vector element_rhs(dof_count); + mfem::Vector analytic_field(dimension); + mfem::Vector pulled_rhs_field(dimension); + mfem::DenseMatrix vector_shape(dof_count, dimension); - MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid physical radius in projection RHS."); + element_rhs = 0.0; - 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); - } + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - context.mapping.mapping_jacobian.MultTranspose(analytic_field, pulled_rhs_field); + for (int q = 0; q < rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &point = rule.IntPoint(q); - transformation->SetIntPoint(&point); - gravity_element.CalcVShape(*transformation, vector_shape); + mean_field::mapping::VolumeMappingContext context; - const double weight = point.weight * transformation->Weight(); + const mean_field::mapping::MappingStatus status = + f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation, + point, workspace, context); - for (int i = 0; i < dof_count; ++i) { - for (int component = 0; component < dimension; ++component) { - element_rhs(i) += weight * vector_shape(i, component) * pulled_rhs_field(component); - } - } - } + MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, + "Mapped monopole projection RHS failed with status " + << static_cast(status) << " on element " + << element_id << ", quadrature point " << q << '.'); - if (gravity_transform != nullptr) { - gravity_transform->TransformDual(element_rhs); - } + analytic_field = context.mapping.physical_position; - local_rhs.AddElementVector(gravity_dofs, element_rhs); + const double radius = analytic_field.Norml2(); + + MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, + "Invalid physical radius in projection RHS."); + + 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); + } + + context.mapping.mapping_jacobian.MultTranspose(analytic_field, + pulled_rhs_field); + + transformation->SetIntPoint(&point); + gravity_element.CalcVShape(*transformation, vector_shape); + + const double weight = point.weight * transformation->Weight(); + + for (int i = 0; i < dof_count; ++i) { + for (int component = 0; component < dimension; ++component) { + element_rhs(i) += + weight * vector_shape(i, component) * pulled_rhs_field(component); } - - mfem::Vector true_rhs; - local_to_true(*f.gravityFluxFes, local_rhs, true_rhs); - - return true_rhs; + } } - mfem::Vector project_monopole_gradient( - mean_field::fem::FEM &f, - const mfem::ParGridFunction &displacement, - const double mass, - const double stellar_radius - ) { - mfem::Vector displacement_true; - displacement.GetTrueDofs(displacement_true); - - const mfem::Vector projection_rhs = assemble_monopole_projection_rhs(f, displacement, mass, stellar_radius); - - mean_field::operators::PreparedMappedHDivMassOperator mass_operator(f, *f.domainMapperStateless); - mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true)); - const mfem::Vector reduced_projection_rhs = mass_operator.GetFluxMap().gather(projection_rhs); - - mfem::Vector projected_gradient(mass_operator.Width()); - projected_gradient = 0.0; - - mfem::CGSolver solver(f.gravityFluxFes->GetComm()); - solver.SetOperator(mass_operator); - solver.SetRelTol(1.0e-9); - solver.SetAbsTol(1.0e-12); - solver.SetMaxIter(2000); - solver.SetPrintLevel(0); - solver.Mult(reduced_projection_rhs, projected_gradient); - - mfem::Vector projection_residual; - mass_operator.Mult(projected_gradient, projection_residual); - projection_residual -= reduced_projection_rhs; - - const double source_norm = global_norm(reduced_projection_rhs, f.gravityFluxFes->GetComm()); - const double residual_norm = global_norm(projection_residual, f.gravityFluxFes->GetComm()); - const double relative_residual = residual_norm / std::max(source_norm, std::numeric_limits::epsilon()); - - INFO("Mapped H(div) projection converged = " << solver.GetConverged()); - INFO("Mapped H(div) projection iterations = " << solver.GetNumIterations()); - INFO("Mapped H(div) projection reported final norm = " << solver.GetFinalNorm()); - INFO("Mapped H(div) projection direct residual norm = " << residual_norm); - INFO("Mapped H(div) projection direct relative residual = " << relative_residual); - - REQUIRE(std::isfinite(relative_residual)); - REQUIRE(relative_residual < 1.0e-8); - return mass_operator.GetFluxMap().scatter(projected_gradient); + if (gravity_transform != nullptr) { + gravity_transform->TransformDual(element_rhs); } - double mapped_hdiv_relative_error( - mean_field::fem::FEM &f, - const mfem::ParGridFunction &displacement, - const mfem::Vector &computed, - const mfem::Vector &reference - ) { - REQUIRE(computed.Size() == reference.Size()); + local_rhs.AddElementVector(gravity_dofs, element_rhs); + } - mfem::Vector displacement_true; - displacement.GetTrueDofs(displacement_true); + mfem::Vector true_rhs; + local_to_true(*f.gravityFluxFes, local_rhs, true_rhs); - mean_field::operators::PreparedMappedHDivMassOperator mass_operator(f, *f.domainMapperStateless); - mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true)); + return true_rhs; +} - mfem::Vector difference(computed); - difference -= reference; +mfem::Vector +project_monopole_gradient(mean_field::fem::FEM &f, + const mfem::ParGridFunction &displacement, + const double mass, const double stellar_radius) { + mfem::Vector displacement_true; + displacement.GetTrueDofs(displacement_true); - mfem::Vector difference_action; - mfem::Vector reference_action; + const mfem::Vector projection_rhs = + assemble_monopole_projection_rhs(f, displacement, mass, stellar_radius); - 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); + mean_field::operators::PreparedMappedHDivMassOperator mass_operator( + f, *f.domainMapperStateless); + mass_operator.Prepare( + mass_operator.GetDisplacementMap().gather(displacement_true)); + const mfem::Vector reduced_projection_rhs = + mass_operator.GetFluxMap().gather(projection_rhs); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + mfem::Vector projected_gradient(mass_operator.Width()); + projected_gradient = 0.0; - const double difference_energy = global_dot(reduced_difference, difference_action, communicator); - const double reference_energy = global_dot(reduced_reference, reference_action, communicator); + mfem::CGSolver solver(f.gravityFluxFes->GetComm()); + solver.SetOperator(mass_operator); + solver.SetRelTol(1.0e-9); + solver.SetAbsTol(1.0e-12); + solver.SetMaxIter(2000); + solver.SetPrintLevel(0); + solver.Mult(reduced_projection_rhs, projected_gradient); - REQUIRE(difference_energy >= -1.0e-12 * std::abs(reference_energy)); - REQUIRE(reference_energy > 0.0); + mfem::Vector projection_residual; + mass_operator.Mult(projected_gradient, projection_residual); + projection_residual -= reduced_projection_rhs; - return std::sqrt(std::max(0.0, difference_energy) / reference_energy); + const double source_norm = + global_norm(reduced_projection_rhs, f.gravityFluxFes->GetComm()); + const double residual_norm = + global_norm(projection_residual, f.gravityFluxFes->GetComm()); + const double relative_residual = + residual_norm / + std::max(source_norm, std::numeric_limits::epsilon()); + + INFO("Mapped H(div) projection converged = " << solver.GetConverged()); + INFO("Mapped H(div) projection iterations = " << solver.GetNumIterations()); + INFO("Mapped H(div) projection reported final norm = " + << solver.GetFinalNorm()); + INFO("Mapped H(div) projection direct residual norm = " << residual_norm); + INFO("Mapped H(div) projection direct relative residual = " + << relative_residual); + + REQUIRE(std::isfinite(relative_residual)); + REQUIRE(relative_residual < 1.0e-8); + return mass_operator.GetFluxMap().scatter(projected_gradient); +} + +double mapped_hdiv_relative_error(mean_field::fem::FEM &f, + const mfem::ParGridFunction &displacement, + const mfem::Vector &computed, + const mfem::Vector &reference) { + REQUIRE(computed.Size() == reference.Size()); + + mfem::Vector displacement_true; + displacement.GetTrueDofs(displacement_true); + + mean_field::operators::PreparedMappedHDivMassOperator mass_operator( + f, *f.domainMapperStateless); + mass_operator.Prepare( + mass_operator.GetDisplacementMap().gather(displacement_true)); + + mfem::Vector difference(computed); + 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); + + MPI_Comm communicator = f.gravityFluxFes->GetComm(); + + const double difference_energy = + global_dot(reduced_difference, difference_action, communicator); + const double reference_energy = + global_dot(reduced_reference, reference_action, communicator); + + REQUIRE(difference_energy >= -1.0e-12 * std::abs(reference_energy)); + REQUIRE(reference_energy > 0.0); + + return std::sqrt(std::max(0.0, difference_energy) / reference_energy); +} + +ShellMeasurement +measure_exterior_shells(mean_field::fem::FEM &f, + const mean_field::physics::GravitySolution &solution, + const mfem::GridFunction &displacement, + const double mass) { + std::array local{}; + long long local_invalid_points = 0; + + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); + + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + const int quadrature_order = + 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), + f.gravityFluxFes->GetMaxElementOrder()) + + 8; + + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); + + if (transformation->Attribute != vacuum_attribute) { + continue; } - ShellMeasurement measure_exterior_shells( - mean_field::fem::FEM &f, - const mean_field::physics::GravitySolution &solution, - const mfem::GridFunction &displacement, - const MappingPath mapping_path, - const double mass - ) { - std::array local{}; - long long local_invalid_points = 0; + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute(); - const int quadrature_order = - 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), f.gravityFluxFes->GetMaxElementOrder()) + 8; + mfem::DofTransformation *displacement_transform = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + mfem::DofTransformation *compactification_transform = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); + mfem::Vector element_displacement; + mfem::Vector element_compactification; - if (transformation->Attribute != vacuum_attribute) { - continue; - } + displacement.GetSubVector(displacement_dofs, element_displacement); + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); - - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; - - mfem::DofTransformation *displacement_transform = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_transform = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); - - mfem::Vector element_displacement; - mfem::Vector element_compactification; - - displacement.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - - if (displacement_transform != nullptr) { - displacement_transform->InvTransformPrimal(element_displacement); - } - - if (compactification_transform != nullptr) { - compactification_transform->InvTransformPrimal(element_compactification); - } - - const mean_field::mapping::ElementDisplacementData displacement_data = - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, element_displacement - ); - - const mean_field::mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - mfem::Vector compactification_shape(compactification_element.GetDof()); - - const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - - for (int q = 0; q < rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &point = rule.IntPoint(q); - - transformation->SetIntPoint(&point); - compactification_element.CalcShape(point, compactification_shape); - - const double coordinate = element_compactification * compactification_shape; - const int shell = get_shell(coordinate); - - mfem::Vector reference_field(3); - mfem::Vector physical_field(3); - mfem::Vector physical_position(3); - - solution.gradPhi.GetVectorValue(element_id, point, reference_field); - - if (mapping_path == MappingPath::stateless) { - mean_field::mapping::VolumeMappingContext context; - - const mean_field::mapping::MappingStatus status = f.domainMapperStateless->EvaluateVolume( - mapping_data, *transformation, point, workspace, context - ); - - if (status != mean_field::mapping::MappingStatus::valid) { - ++local_invalid_points; - continue; - } - - physical_position = context.mapping.physical_position; - mean_field::mapping::MapHDivFluxToPhysical(context.mapping, reference_field, physical_field); - } else { - f.mapping->GetPhysicalPoint(*transformation, point, physical_position); - - mfem::DenseMatrix jacobian(3); - f.mapping->ComputeJacobian(*transformation, jacobian); - - const double determinant = jacobian.Det(); - - if (!std::isfinite(determinant) || determinant <= 0.0) { - ++local_invalid_points; - continue; - } - - jacobian.Mult(reference_field, physical_field); - physical_field /= determinant; - } - - const double radius = physical_position.Norml2(); - - if (!std::isfinite(radius) || radius <= 0.0) { - ++local_invalid_points; - continue; - } - - mfem::Vector radial_unit(physical_position); - radial_unit /= radius; - - const double radial_field = physical_field * radial_unit; - - mfem::Vector tangential_field(physical_field); - tangential_field.Add(-radial_field, radial_unit); - - const double potential = solution.phi.GetValue(element_id, point); - - const double scaled_potential = -radius * potential / (mean_field::utils::G * mass); - const double scaled_radial_field = radius * radius * radial_field / (mean_field::utils::G * mass); - const double scaled_tangential_field = - radius * radius * tangential_field.Norml2() / (mean_field::utils::G * mass); - - if (!std::isfinite(scaled_potential) || !std::isfinite(scaled_radial_field) || - !std::isfinite(scaled_tangential_field)) { - ++local_invalid_points; - continue; - } - - const double weight = point.weight * transformation->Weight(); - - ShellAccumulator &accumulator = local[shell]; - - ++accumulator.points; - accumulator.weight += weight; - - accumulator.minimum_radius = std::min(accumulator.minimum_radius, radius); - accumulator.maximum_radius = std::max(accumulator.maximum_radius, radius); - - accumulator.potential_error_squared += weight * (scaled_potential - 1.0) * (scaled_potential - 1.0); - accumulator.radial_error_squared += weight * (scaled_radial_field - 1.0) * (scaled_radial_field - 1.0); - accumulator.tangential_squared += weight * scaled_tangential_field * scaled_tangential_field; - } - } - - MPI_Comm communicator = f.gravityFluxFes->GetComm(); - - ShellMeasurement measurement; - - MPI_Allreduce(&local_invalid_points, &measurement.invalid_points, 1, MPI_LONG_LONG, MPI_SUM, communicator); - - for (int shell = 0; shell < shell_count; ++shell) { - long long points = 0; - - MPI_Allreduce(&local[shell].points, &points, 1, MPI_LONG_LONG, MPI_SUM, communicator); - - const double local_sums[4]{ - local[shell].weight, local[shell].potential_error_squared, local[shell].radial_error_squared, - local[shell].tangential_squared - }; - - double sums[4]{}; - - MPI_Allreduce(local_sums, sums, 4, MPI_DOUBLE, MPI_SUM, communicator); - - double minimum_radius = 0.0; - double maximum_radius = 0.0; - - MPI_Allreduce(&local[shell].minimum_radius, &minimum_radius, 1, MPI_DOUBLE, MPI_MIN, communicator); - MPI_Allreduce(&local[shell].maximum_radius, &maximum_radius, 1, MPI_DOUBLE, MPI_MAX, communicator); - - measurement.shells[shell] = { - .points = points, - .minimum_radius = minimum_radius, - .maximum_radius = maximum_radius, - .potential_rms_error = - sums[0] > 0.0 ? std::sqrt(sums[1] / sums[0]) : std::numeric_limits::infinity(), - .radial_rms_error = - sums[0] > 0.0 ? std::sqrt(sums[2] / sums[0]) : std::numeric_limits::infinity(), - .tangential_rms = sums[0] > 0.0 ? std::sqrt(sums[3] / sums[0]) : std::numeric_limits::infinity() - }; - } - - return measurement; + if (displacement_transform != nullptr) { + displacement_transform->InvTransformPrimal(element_displacement); } - GravitationalEnergies compute_stellar_energies( - mean_field::fem::FEM &f, - const mfem::GridFunction &density, - const mean_field::physics::GravitySolution &solution, - const mfem::GridFunction &displacement - ) { - mean_field::mapping::DomainMapperStateless::Workspace workspace(f.mesh->Dimension()); - - double local_binding = 0.0; - double local_virial = 0.0; - long long local_invalid_points = 0; - double local_minimum_determinant = std::numeric_limits::infinity(); - double local_maximum_determinant = -std::numeric_limits::infinity(); - - const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute(); - - const int order = - 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), f.gravityFluxFes->GetMaxElementOrder()) + 8; - std::array local_status_counts{}; - - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - if (transformation->Attribute == vacuum_attribute) { - continue; - } - - const mfem::FiniteElement &displacement_element = *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = *f.compactificationFes->GetFE(element_id); - - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; - - mfem::DofTransformation *displacement_transform = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_transform = - f.compactificationFes->GetElementDofs(element_id, compactification_dofs); - - mfem::Vector element_displacement; - mfem::Vector element_compactification; - - displacement.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - - if (displacement_transform != nullptr) { - displacement_transform->InvTransformPrimal(element_displacement); - } - - if (compactification_transform != nullptr) { - compactification_transform->InvTransformPrimal(element_compactification); - } - - const mean_field::mapping::ElementDisplacementData displacement_data = - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, element_displacement - ); - - const mean_field::mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification - ); - - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, .compactification = compactification_data - }; - - const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), order); - - for (int q = 0; q < rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &point = rule.IntPoint(q); - - mean_field::mapping::VolumeMappingContext context; - - const mean_field::mapping::MappingStatus status = - f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation, point, workspace, context); - - const double mapping_determinant = context.mapping.mapping_determinant; - if (std::isfinite(mapping_determinant)) { - local_minimum_determinant = std::min(local_minimum_determinant, mapping_determinant); - local_maximum_determinant = std::max(local_maximum_determinant, mapping_determinant); - } - - const int status_index = mapping_status_index(status); - MFEM_VERIFY(status_index >= 0 && status_index < mapping_status_count, "Unexpected mapping status."); - ++local_status_counts[status_index]; - - if (status != mean_field::mapping::MappingStatus::valid) { - ++local_invalid_points; - continue; - } - - if (status != mean_field::mapping::MappingStatus::valid) { - ++local_invalid_points; - continue; - } - - mfem::Vector reference_field(3); - mfem::Vector physical_field(3); - - solution.gradPhi.GetVectorValue(element_id, point, reference_field); - - mean_field::mapping::MapHDivFluxToPhysical(context.mapping, reference_field, physical_field); - - const double rho = density.GetValue(element_id, point); - const double phi = solution.phi.GetValue(element_id, point); - - local_binding += 0.5 * rho * phi * context.quadrature.weight; - local_virial -= rho * (context.mapping.physical_position * physical_field) * context.quadrature.weight; - } - } - - GravitationalEnergies energies; - - MPI_Comm communicator = f.densityFes->GetComm(); - - MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, communicator); - MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, communicator); - MPI_Allreduce(&local_invalid_points, &energies.invalid_points, 1, MPI_LONG_LONG, MPI_SUM, communicator); - MPI_Allreduce( - local_status_counts.data(), energies.mapping_status_counts.data(), mapping_status_count, MPI_LONG_LONG, - MPI_SUM, communicator - ); - MPI_Allreduce( - &local_minimum_determinant, &energies.minimum_mapping_determinant, 1, MPI_DOUBLE, MPI_MIN, communicator - ); - MPI_Allreduce( - &local_maximum_determinant, &energies.maximum_mapping_determinant, 1, MPI_DOUBLE, MPI_MAX, communicator - ); - - return energies; + if (compactification_transform != nullptr) { + compactification_transform->InvTransformPrimal(element_compactification); } + + const mean_field::mapping::ElementDisplacementData displacement_data = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement); + + const mean_field::mapping::ElementCompactificationData + compactification_data(compactification_element, + element_compactification); + + const mean_field::mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; + + mfem::Vector compactification_shape(compactification_element.GetDof()); + + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + + for (int q = 0; q < rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &point = rule.IntPoint(q); + + transformation->SetIntPoint(&point); + compactification_element.CalcShape(point, compactification_shape); + + const double coordinate = + element_compactification * compactification_shape; + const int shell = get_shell(coordinate); + + mfem::Vector reference_field(3); + mfem::Vector physical_field(3); + mfem::Vector physical_position(3); + + solution.gradPhi.GetVectorValue(element_id, point, reference_field); + + mean_field::mapping::VolumeMappingContext context; + + const mean_field::mapping::MappingStatus status = + f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation, + point, workspace, context); + + if (status != mean_field::mapping::MappingStatus::valid) { + ++local_invalid_points; + continue; + } + + physical_position = context.mapping.physical_position; + mean_field::mapping::MapHDivFluxToPhysical( + context.mapping, reference_field, physical_field); + + const double radius = physical_position.Norml2(); + + if (!std::isfinite(radius) || radius <= 0.0) { + ++local_invalid_points; + continue; + } + + mfem::Vector radial_unit(physical_position); + radial_unit /= radius; + + const double radial_field = physical_field * radial_unit; + + mfem::Vector tangential_field(physical_field); + tangential_field.Add(-radial_field, radial_unit); + + const double potential = solution.phi.GetValue(element_id, point); + + const double scaled_potential = + -radius * potential / (mean_field::utils::G * mass); + const double scaled_radial_field = + radius * radius * radial_field / (mean_field::utils::G * mass); + const double scaled_tangential_field = radius * radius * + tangential_field.Norml2() / + (mean_field::utils::G * mass); + + if (!std::isfinite(scaled_potential) || + !std::isfinite(scaled_radial_field) || + !std::isfinite(scaled_tangential_field)) { + ++local_invalid_points; + continue; + } + + const double weight = point.weight * transformation->Weight(); + + ShellAccumulator &accumulator = local[shell]; + + ++accumulator.points; + accumulator.weight += weight; + + accumulator.minimum_radius = std::min(accumulator.minimum_radius, radius); + accumulator.maximum_radius = std::max(accumulator.maximum_radius, radius); + + accumulator.potential_error_squared += + weight * (scaled_potential - 1.0) * (scaled_potential - 1.0); + accumulator.radial_error_squared += + weight * (scaled_radial_field - 1.0) * (scaled_radial_field - 1.0); + accumulator.tangential_squared += + weight * scaled_tangential_field * scaled_tangential_field; + } + } + + MPI_Comm communicator = f.gravityFluxFes->GetComm(); + + ShellMeasurement measurement; + + MPI_Allreduce(&local_invalid_points, &measurement.invalid_points, 1, + MPI_LONG_LONG, MPI_SUM, communicator); + + for (int shell = 0; shell < shell_count; ++shell) { + long long points = 0; + + MPI_Allreduce(&local[shell].points, &points, 1, MPI_LONG_LONG, MPI_SUM, + communicator); + + const double local_sums[4]{ + local[shell].weight, local[shell].potential_error_squared, + local[shell].radial_error_squared, local[shell].tangential_squared}; + + double sums[4]{}; + + MPI_Allreduce(local_sums, sums, 4, MPI_DOUBLE, MPI_SUM, communicator); + + double minimum_radius = 0.0; + double maximum_radius = 0.0; + + MPI_Allreduce(&local[shell].minimum_radius, &minimum_radius, 1, MPI_DOUBLE, + MPI_MIN, communicator); + MPI_Allreduce(&local[shell].maximum_radius, &maximum_radius, 1, MPI_DOUBLE, + MPI_MAX, communicator); + + measurement.shells[shell] = { + .points = points, + .minimum_radius = minimum_radius, + .maximum_radius = maximum_radius, + .potential_rms_error = sums[0] > 0.0 + ? std::sqrt(sums[1] / sums[0]) + : std::numeric_limits::infinity(), + .radial_rms_error = sums[0] > 0.0 + ? std::sqrt(sums[2] / sums[0]) + : std::numeric_limits::infinity(), + .tangential_rms = sums[0] > 0.0 + ? std::sqrt(sums[3] / sums[0]) + : std::numeric_limits::infinity()}; + } + + return measurement; +} + +GravitationalEnergies +compute_stellar_energies(mean_field::fem::FEM &f, + const mfem::GridFunction &density, + const mean_field::physics::GravitySolution &solution, + const mfem::GridFunction &displacement) { + mean_field::mapping::DomainMapper::Workspace workspace( + f.mesh->Dimension()); + + double local_binding = 0.0; + double local_virial = 0.0; + long long local_invalid_points = 0; + double local_minimum_determinant = std::numeric_limits::infinity(); + double local_maximum_determinant = -std::numeric_limits::infinity(); + + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + + const int order = 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), + f.gravityFluxFes->GetMaxElementOrder()) + + 8; + std::array local_status_counts{}; + + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = + f.mesh->GetElementTransformation(element_id); + if (transformation->Attribute == vacuum_attribute) { + continue; + } + + const mfem::FiniteElement &displacement_element = + *f.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = + *f.compactificationFes->GetFE(element_id); + + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; + + mfem::DofTransformation *displacement_transform = + f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + mfem::DofTransformation *compactification_transform = + f.compactificationFes->GetElementDofs(element_id, + compactification_dofs); + + mfem::Vector element_displacement; + mfem::Vector element_compactification; + + displacement.GetSubVector(displacement_dofs, element_displacement); + f.compactificationCoordinate->GetSubVector(compactification_dofs, + element_compactification); + + if (displacement_transform != nullptr) { + displacement_transform->InvTransformPrimal(element_displacement); + } + + if (compactification_transform != nullptr) { + compactification_transform->InvTransformPrimal(element_compactification); + } + + const mean_field::mapping::ElementDisplacementData displacement_data = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement); + + const mean_field::mapping::ElementCompactificationData + compactification_data(compactification_element, + element_compactification); + + const mean_field::mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, + .compactification = compactification_data}; + + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), order); + + for (int q = 0; q < rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &point = rule.IntPoint(q); + + mean_field::mapping::VolumeMappingContext context; + + const mean_field::mapping::MappingStatus status = + f.domainMapperStateless->EvaluateVolume(mapping_data, *transformation, + point, workspace, context); + + const double mapping_determinant = context.mapping.mapping_determinant; + if (std::isfinite(mapping_determinant)) { + local_minimum_determinant = + std::min(local_minimum_determinant, mapping_determinant); + local_maximum_determinant = + std::max(local_maximum_determinant, mapping_determinant); + } + + const int status_index = mapping_status_index(status); + MFEM_VERIFY(status_index >= 0 && status_index < mapping_status_count, + "Unexpected mapping status."); + ++local_status_counts[status_index]; + + if (status != mean_field::mapping::MappingStatus::valid) { + ++local_invalid_points; + continue; + } + + if (status != mean_field::mapping::MappingStatus::valid) { + ++local_invalid_points; + continue; + } + + mfem::Vector reference_field(3); + mfem::Vector physical_field(3); + + solution.gradPhi.GetVectorValue(element_id, point, reference_field); + + mean_field::mapping::MapHDivFluxToPhysical( + context.mapping, reference_field, physical_field); + + const double rho = density.GetValue(element_id, point); + const double phi = solution.phi.GetValue(element_id, point); + + local_binding += 0.5 * rho * phi * context.quadrature.weight; + local_virial -= rho * + (context.mapping.physical_position * physical_field) * + context.quadrature.weight; + } + } + + GravitationalEnergies energies; + + MPI_Comm communicator = f.densityFes->GetComm(); + + MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, + communicator); + MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, + communicator); + MPI_Allreduce(&local_invalid_points, &energies.invalid_points, 1, + MPI_LONG_LONG, MPI_SUM, communicator); + MPI_Allreduce(local_status_counts.data(), + energies.mapping_status_counts.data(), mapping_status_count, + MPI_LONG_LONG, MPI_SUM, communicator); + MPI_Allreduce(&local_minimum_determinant, + &energies.minimum_mapping_determinant, 1, MPI_DOUBLE, MPI_MIN, + communicator); + MPI_Allreduce(&local_maximum_determinant, + &energies.maximum_mapping_determinant, 1, MPI_DOUBLE, MPI_MAX, + communicator); + + return energies; +} } // namespace -TEST_CASE( - "New Gravity Monopole Accuracy And Projection Floor", - tags::gravity_analytic_accuracy -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); +TEST_CASE("Gravity Field Monopole Accuracy And Projection Floor", + tags::gravity_analytic_accuracy) { + auto args = test_utils::setup_args(); + args.p.rtol = 1.0e-13; + 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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.compactificationCoordinate != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.compactificationCoordinate != nullptr); - const double radius = mean_field::utils::RADIUS; - const double mass = mean_field::utils::MASS; + const double radius = mean_field::utils::RADIUS; + const double mass = mean_field::utils::MASS; - const double density_value = mass / ((4.0 / 3.0) * M_PI * radius * radius * radius); + const double density_value = + mass / ((4.0 / 3.0) * M_PI * radius * radius * radius); - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement = 0.0; + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; - f.mapping->ResetDisplacement(); - mean_field::physics::update_stiffness_matrix(f); + *f.displacement = 0.0; - mfem::GridFunction density(f.densityFes.get()); - density = density_value; + mfem::GridFunction density(f.densityFes.get()); + density = density_value; - zero_vacuum_density(f, density); + zero_vacuum_density(f, density); - mean_field::analysis::conserve_mass(f, density, mass); + mean_field::analysis::conserve_mass(f, density, mass); - f.com = mean_field::analysis::get_com(f, density); - f.Q = mean_field::physics::compute_quadrupole_moment_tensor(f, density, f.com); + f.com = mean_field::analysis::get_com(f, density); + f.Q = + mean_field::physics::compute_quadrupole_moment_tensor(f, density, f.com); - const mean_field::physics::GravitySolution legacy_solution = mean_field::physics::grav_potential(f, args, density); + const mean_field::physics::GravitySolution numerical_solution = + mean_field::physics::solve_gravity_field(f, args, density, displacement); - const mean_field::physics::GravitySolution numerical_solution = - mean_field::physics::grav_potential_new(f, args, density, displacement); + MonopolePotentialCoefficient potential_coefficient( + f, *f.domainMapperStateless, displacement, mass, radius); - MonopolePotentialCoefficient potential_coefficient(f, *f.domainMapperStateless, displacement, mass, radius); + mean_field::physics::GravitySolution projected_solution(f); + projected_solution.phi = 0.0; + projected_solution.gradPhi = 0.0; - mean_field::physics::GravitySolution projected_solution(f); - projected_solution.phi = 0.0; - projected_solution.gradPhi = 0.0; + projected_solution.phi.ProjectCoefficient(potential_coefficient); - projected_solution.phi.ProjectCoefficient(potential_coefficient); + const mfem::Vector projected_gradient_true = + project_monopole_gradient(f, displacement, mass, radius); - const mfem::Vector projected_gradient_true = project_monopole_gradient(f, displacement, mass, radius); + projected_solution.gradPhi.SetFromTrueDofs(projected_gradient_true); - projected_solution.gradPhi.SetFromTrueDofs(projected_gradient_true); + const ShellMeasurement numerical = + measure_exterior_shells(f, numerical_solution, displacement, mass); - const ShellMeasurement legacy = - measure_exterior_shells(f, legacy_solution, displacement, MappingPath::legacy, mass); + const ShellMeasurement projected = + measure_exterior_shells(f, projected_solution, displacement, mass); - const ShellMeasurement numerical = - measure_exterior_shells(f, numerical_solution, displacement, MappingPath::stateless, mass); + const GravitationalEnergies energies = + compute_stellar_energies(f, density, numerical_solution, displacement); - const ShellMeasurement projected = - measure_exterior_shells(f, projected_solution, displacement, MappingPath::stateless, mass); + mfem::Vector numerical_gradient; + mfem::Vector numerical_potential; + mfem::Vector projected_gradient; + mfem::Vector projected_potential; - const GravitationalEnergies energies = compute_stellar_energies(f, density, numerical_solution, displacement); + numerical_solution.gradPhi.GetTrueDofs(numerical_gradient); + numerical_solution.phi.GetTrueDofs(numerical_potential); - mfem::Vector numerical_gradient; - mfem::Vector numerical_potential; - mfem::Vector projected_gradient; - mfem::Vector projected_potential; + projected_solution.gradPhi.GetTrueDofs(projected_gradient); + projected_solution.phi.GetTrueDofs(projected_potential); - numerical_solution.gradPhi.GetTrueDofs(numerical_gradient); - numerical_solution.phi.GetTrueDofs(numerical_potential); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - projected_solution.gradPhi.GetTrueDofs(projected_gradient); - projected_solution.phi.GetTrueDofs(projected_potential); + const double gradient_projection_gap = mapped_hdiv_relative_error( + f, displacement, numerical_gradient, projected_gradient); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + const double potential_projection_gap = global_relative_error( + numerical_potential, projected_potential, communicator); - const double gradient_projection_gap = - mapped_hdiv_relative_error(f, displacement, numerical_gradient, projected_gradient); + double maximum_numerical_potential_error = 0.0; + double maximum_projected_potential_error = 0.0; + double maximum_numerical_radial_error = 0.0; + double maximum_projected_radial_error = 0.0; + double maximum_numerical_tangential = 0.0; + double maximum_projected_tangential = 0.0; - const double potential_projection_gap = - global_relative_error(numerical_potential, projected_potential, communicator); + std::ostringstream report; - double maximum_numerical_potential_error = 0.0; - double maximum_projected_potential_error = 0.0; - double maximum_numerical_radial_error = 0.0; - double maximum_projected_radial_error = 0.0; - double maximum_numerical_tangential = 0.0; - double maximum_projected_tangential = 0.0; + for (int shell = 0; shell < shell_count; ++shell) { + const ShellMetrics &numerical_shell = numerical.shells[shell]; + const ShellMetrics &projected_shell = projected.shells[shell]; - std::ostringstream report; + maximum_numerical_potential_error = std::max( + maximum_numerical_potential_error, numerical_shell.potential_rms_error); - for (int shell = 0; shell < shell_count; ++shell) { - const ShellMetrics &old_shell = legacy.shells[shell]; - const ShellMetrics &numerical_shell = numerical.shells[shell]; - const ShellMetrics &projected_shell = projected.shells[shell]; + maximum_projected_potential_error = std::max( + maximum_projected_potential_error, projected_shell.potential_rms_error); - maximum_numerical_potential_error = - std::max(maximum_numerical_potential_error, numerical_shell.potential_rms_error); + maximum_numerical_radial_error = std::max(maximum_numerical_radial_error, + numerical_shell.radial_rms_error); - maximum_projected_potential_error = - std::max(maximum_projected_potential_error, projected_shell.potential_rms_error); + maximum_projected_radial_error = std::max(maximum_projected_radial_error, + projected_shell.radial_rms_error); - maximum_numerical_radial_error = std::max(maximum_numerical_radial_error, numerical_shell.radial_rms_error); + maximum_numerical_tangential = + std::max(maximum_numerical_tangential, numerical_shell.tangential_rms); - maximum_projected_radial_error = std::max(maximum_projected_radial_error, projected_shell.radial_rms_error); + maximum_projected_tangential = + std::max(maximum_projected_tangential, projected_shell.tangential_rms); - maximum_numerical_tangential = std::max(maximum_numerical_tangential, numerical_shell.tangential_rms); + report << "shell " << shell << " xi=[" << shell_boundaries[shell] << ", " + << shell_boundaries[shell + 1] << ")\n" + << " radius=[" << numerical_shell.minimum_radius << ", " + << numerical_shell.maximum_radius << "]\n" + << " potential error: solved=" + << numerical_shell.potential_rms_error + << ", projection=" << projected_shell.potential_rms_error << '\n' + << " radial error: solved=" << numerical_shell.radial_rms_error + << ", projection=" << projected_shell.radial_rms_error << '\n' + << " tangential amplitude: solved=" + << numerical_shell.tangential_rms + << ", projection=" << projected_shell.tangential_rms << '\n'; + } - maximum_projected_tangential = std::max(maximum_projected_tangential, projected_shell.tangential_rms); + const double analytic_energy = + -3.0 * mean_field::utils::G * mass * mass / (5.0 * radius); - report << "shell " << shell << " xi=[" << shell_boundaries[shell] << ", " << shell_boundaries[shell + 1] - << ")\n" - << " legacy radius=[" << old_shell.minimum_radius << ", " << old_shell.maximum_radius << "]\n" - << " new radius=[" << numerical_shell.minimum_radius << ", " << numerical_shell.maximum_radius << "]\n" - << " potential error: legacy=" << old_shell.potential_rms_error - << ", solved=" << numerical_shell.potential_rms_error - << ", projection=" << projected_shell.potential_rms_error << '\n' - << " radial error: legacy=" << old_shell.radial_rms_error - << ", solved=" << numerical_shell.radial_rms_error << ", projection=" << projected_shell.radial_rms_error - << '\n' - << " tangential amplitude: legacy=" << old_shell.tangential_rms - << ", solved=" << numerical_shell.tangential_rms << ", projection=" << projected_shell.tangential_rms - << '\n'; - } + const double binding_error = + std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy); + const double virial_error = + std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy); + const double consistency_error = + std::abs(energies.binding - energies.virial) / std::abs(energies.binding); - const double analytic_energy = -3.0 * mean_field::utils::G * mass * mass / (5.0 * radius); + INFO(report.str()); - const double binding_error = std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy); - const double virial_error = std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy); - const double consistency_error = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); + INFO("Gradient solution/projection mapped H(div) gap = " + << gradient_projection_gap); + INFO("Potential solution/projection DOF gap = " << potential_projection_gap); + INFO("Analytic energy = " << analytic_energy); + INFO("Computed binding energy = " << energies.binding); + INFO("Computed virial energy = " << energies.virial); + INFO("Relative binding error = " << binding_error); + INFO("Relative virial error = " << virial_error); + INFO("Relative virial consistency error = " << consistency_error); - INFO(report.str()); + REQUIRE(numerical.invalid_points == 0); + REQUIRE(projected.invalid_points == 0); + REQUIRE(energies.invalid_points == 0); - INFO("Gradient solution/projection mapped H(div) gap = " << gradient_projection_gap); - INFO("Potential solution/projection DOF gap = " << potential_projection_gap); - INFO("Analytic energy = " << analytic_energy); - INFO("Computed binding energy = " << energies.binding); - INFO("Computed virial energy = " << energies.virial); - INFO("Relative binding error = " << binding_error); - INFO("Relative virial error = " << virial_error); - INFO("Relative virial consistency error = " << consistency_error); + for (int shell = 0; shell < shell_count; ++shell) { + REQUIRE(numerical.shells[shell].points > 0); + REQUIRE(projected.shells[shell].points > 0); + } - REQUIRE(legacy.invalid_points == 0); - REQUIRE(numerical.invalid_points == 0); - REQUIRE(projected.invalid_points == 0); - REQUIRE(energies.invalid_points == 0); + CHECK(maximum_numerical_potential_error < 5.0e-2); + CHECK(maximum_projected_potential_error < 5.0e-2); + CHECK(maximum_numerical_radial_error < 5.0e-3); + CHECK(maximum_projected_radial_error < 5.0e-3); + CHECK(maximum_numerical_tangential < 5.0e-3); + CHECK(maximum_projected_tangential < 5.0e-3); + CHECK(gradient_projection_gap < 5.0e-3); + CHECK(potential_projection_gap < maximum_numerical_potential_error); - for (int shell = 0; shell < shell_count; ++shell) { - REQUIRE(legacy.shells[shell].points > 0); - REQUIRE(numerical.shells[shell].points > 0); - REQUIRE(projected.shells[shell].points > 0); - } + constexpr double virial_target = 1.0e-5; - CHECK(maximum_numerical_potential_error < 5.0e-2); - CHECK(maximum_projected_potential_error < 5.0e-2); - CHECK(maximum_numerical_radial_error < 5.0e-3); - CHECK(maximum_projected_radial_error < 5.0e-3); - CHECK(maximum_numerical_tangential < 5.0e-3); - CHECK(maximum_projected_tangential < 5.0e-3); - CHECK(gradient_projection_gap < 5.0e-3); - CHECK(potential_projection_gap < maximum_numerical_potential_error); - - constexpr double virial_target = 1.0e-5; - - CHECK(binding_error < virial_target); - CHECK(virial_error < virial_target); - CHECK(consistency_error < virial_target); + CHECK(binding_error < virial_target); + CHECK(virial_error < virial_target); + CHECK(consistency_error < virial_target); } TEST_CASE( - "New Gravity Virial Consistency Across Volume Preserving Deformation", - tags::gravity_consistency &tags::accuracy -) { - auto args = test_utils::setup_args(); - args.p.rtol = 1.0e-13; - args.p.max_iters = std::max(args.p.max_iters, 1000); + "Gravity Field Virial Consistency Across Volume Preserving Deformation", + tags::gravity_consistency_accuracy) { + auto args = test_utils::setup_args(); + args.p.rtol = 1.0e-13; + 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); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.mapping != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - const double radius = mean_field::utils::RADIUS; - const double mass = mean_field::utils::MASS; + const double radius = mean_field::utils::RADIUS; + const double mass = mean_field::utils::MASS; - const double central_density = 15.0 * mass / (8.0 * M_PI * radius * radius * radius); + const double central_density = + 15.0 * mass / (8.0 * M_PI * radius * radius * radius); - auto density_function = [central_density, radius](const mfem::Vector &position) { - const double normalized_radius_squared = (position * position) / (radius * radius); - return central_density * std::max(0.0, 1.0 - normalized_radius_squared); + auto density_function = [central_density, + radius](const mfem::Vector &position) { + const double normalized_radius_squared = + (position * position) / (radius * radius); + return central_density * std::max(0.0, 1.0 - normalized_radius_squared); + }; + + mfem::FunctionCoefficient density_coefficient(density_function); + mfem::GridFunction density(f.densityFes.get()); + density.ProjectCoefficient(density_coefficient); + + zero_vacuum_density(f, density); + + mean_field::analysis::conserve_mass(f, density, mass); + + constexpr std::array amplitudes{0.0, 0.02, 0.05, 0.1, + 0.2, 0.5, 1.0}; + + std::array consistency_errors{}; + std::array normalized_quadrupoles{}; + std::array binding_energies{}; + std::array virial_energies{}; + + std::ostringstream report; + + for (std::size_t index = 0; index < amplitudes.size(); ++index) { + const double amplitude = amplitudes[index]; + + const double x_scale = 1.0 + 0.15 * amplitude; + const double y_scale = 1.0 - 0.05 * amplitude; + const double z_scale = 1.0 / (x_scale * y_scale); + + REQUIRE_THAT(x_scale * y_scale * z_scale, + Catch::Matchers::WithinAbs(1.0, 1.0e-14)); + + auto displacement_function = [x_scale, y_scale, + z_scale](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + + value(0) = (x_scale - 1.0) * position(0); + value(1) = (y_scale - 1.0) * position(1); + value(2) = (z_scale - 1.0) * position(2); }; - mfem::FunctionCoefficient density_coefficient(density_function); - mfem::GridFunction density(f.densityFes.get()); - density.ProjectCoefficient(density_coefficient); + mfem::VectorFunctionCoefficient displacement_coefficient( + 3, displacement_function); + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement.ProjectCoefficient(displacement_coefficient); - zero_vacuum_density(f, density); + *f.displacement = displacement; - mean_field::analysis::conserve_mass(f, density, mass); + f.com = mean_field::analysis::get_com(f, density); + f.Q = mean_field::physics::compute_quadrupole_moment_tensor(f, density, + f.com); + const mfem::FiniteElementSpace *nodal_space = f.mesh->GetNodalFESpace(); - constexpr std::array amplitudes{0.0, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0}; + const mean_field::physics::GravitySolution solution = + mean_field::physics::solve_gravity_field(f, args, density, + displacement); + const GravitationalEnergies energies = + compute_stellar_energies(f, density, solution, displacement); - std::array consistency_errors{}; - std::array normalized_quadrupoles{}; - std::array binding_energies{}; - std::array virial_energies{}; + CAPTURE(amplitude, x_scale, y_scale, z_scale); + INFO("Mapping status valid = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::valid)]); + INFO("Mapping status invalid_dimension = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::invalid_dimension)]); + INFO("Mapping status non_finite_input = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::non_finite_input)]); + INFO("Mapping status invalid_reference_radius = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::invalid_reference_radius)]); + INFO("Mapping status at_compactified_infinity = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::at_compactified_infinity)]); + INFO("Mapping status outside_reference_domain = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::outside_reference_domain)]); + INFO("Mapping status non_finite_result = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::non_finite_result)]); + INFO("Mapping status non_positive_determinant = " + << energies.mapping_status_counts[mapping_status_index( + mean_field::mapping::MappingStatus::non_positive_determinant)]); + INFO("Total invalid mapping points = " << energies.invalid_points); + INFO("Mesh nodal order = " + << (nodal_space != nullptr ? nodal_space->GetMaxElementOrder() : -1)); + INFO("Displacement order = " << f.displacementFes->GetMaxElementOrder()); + INFO("Minimum discrete mapping determinant = " + << energies.minimum_mapping_determinant); + INFO("Maximum discrete mapping determinant = " + << energies.maximum_mapping_determinant); - std::ostringstream report; + REQUIRE(energies.invalid_points == 0); + REQUIRE(std::isfinite(energies.binding)); + REQUIRE(std::isfinite(energies.virial)); + REQUIRE(energies.binding < 0.0); + REQUIRE(energies.virial < 0.0); - for (std::size_t index = 0; index < amplitudes.size(); ++index) { - const double amplitude = amplitudes[index]; + binding_energies[index] = energies.binding; + virial_energies[index] = energies.virial; - const double x_scale = 1.0 + 0.15 * amplitude; - const double y_scale = 1.0 - 0.05 * amplitude; - const double z_scale = 1.0 / (x_scale * y_scale); + consistency_errors[index] = std::abs(energies.binding - energies.virial) / + std::abs(energies.binding); - REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14)); + normalized_quadrupoles[index] = f.Q.FNorm() / (mass * radius * radius); - auto displacement_function = [x_scale, y_scale, z_scale](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); + report << "amplitude=" << amplitude << ", scales=(" << x_scale << ", " + << y_scale << ", " << z_scale + << "), normalized quadrupole=" << normalized_quadrupoles[index] + << ", binding=" << binding_energies[index] + << ", virial=" << virial_energies[index] + << ", consistency error=" << consistency_errors[index] << '\n'; + } - value(0) = (x_scale - 1.0) * position(0); - value(1) = (y_scale - 1.0) * position(1); - value(2) = (z_scale - 1.0) * position(2); - }; + INFO(report.str()); - mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement.ProjectCoefficient(displacement_coefficient); + for (std::size_t index = 1; index < amplitudes.size(); ++index) { + CHECK(normalized_quadrupoles[index] > normalized_quadrupoles[index - 1]); + } - f.mapping->SetDisplacement(displacement); - mean_field::physics::update_stiffness_matrix(f); - - f.com = mean_field::analysis::get_com(f, density); - f.Q = mean_field::physics::compute_quadrupole_moment_tensor(f, density, f.com); - const mfem::FiniteElementSpace *nodal_space = f.mesh->GetNodalFESpace(); - - const mean_field::physics::GravitySolution solution = - mean_field::physics::grav_potential_new(f, args, density, displacement); - const GravitationalEnergies energies = compute_stellar_energies(f, density, solution, displacement); - - CAPTURE(amplitude, x_scale, y_scale, z_scale); - INFO( - "Mapping status valid = " - << energies.mapping_status_counts[mapping_status_index(mean_field::mapping::MappingStatus::valid)] - ); - INFO( - "Mapping status invalid_dimension = " - << energies - .mapping_status_counts[mapping_status_index(mean_field::mapping::MappingStatus::invalid_dimension)] - ); - INFO( - "Mapping status non_finite_input = " - << energies - .mapping_status_counts[mapping_status_index(mean_field::mapping::MappingStatus::non_finite_input)] - ); - INFO( - "Mapping status invalid_reference_radius = " << energies.mapping_status_counts[mapping_status_index( - mean_field::mapping::MappingStatus::invalid_reference_radius - )] - ); - INFO( - "Mapping status at_compactified_infinity = " << energies.mapping_status_counts[mapping_status_index( - mean_field::mapping::MappingStatus::at_compactified_infinity - )] - ); - INFO( - "Mapping status outside_reference_domain = " << energies.mapping_status_counts[mapping_status_index( - mean_field::mapping::MappingStatus::outside_reference_domain - )] - ); - INFO( - "Mapping status non_finite_result = " - << energies - .mapping_status_counts[mapping_status_index(mean_field::mapping::MappingStatus::non_finite_result)] - ); - INFO( - "Mapping status non_positive_determinant = " << energies.mapping_status_counts[mapping_status_index( - mean_field::mapping::MappingStatus::non_positive_determinant - )] - ); - INFO("Total invalid mapping points = " << energies.invalid_points); - INFO("Mesh nodal order = " << (nodal_space != nullptr ? nodal_space->GetMaxElementOrder() : -1)); - INFO("Displacement order = " << f.displacementFes->GetMaxElementOrder()); - INFO("Minimum discrete mapping determinant = " << energies.minimum_mapping_determinant); - INFO("Maximum discrete mapping determinant = " << energies.maximum_mapping_determinant); - - REQUIRE(energies.invalid_points == 0); - REQUIRE(std::isfinite(energies.binding)); - REQUIRE(std::isfinite(energies.virial)); - REQUIRE(energies.binding < 0.0); - REQUIRE(energies.virial < 0.0); - - binding_energies[index] = energies.binding; - virial_energies[index] = energies.virial; - - consistency_errors[index] = std::abs(energies.binding - energies.virial) / std::abs(energies.binding); - - normalized_quadrupoles[index] = f.Q.FNorm() / (mass * radius * radius); - - report << "amplitude=" << amplitude << ", scales=(" << x_scale << ", " << y_scale << ", " << z_scale - << "), normalized quadrupole=" << normalized_quadrupoles[index] - << ", binding=" << binding_energies[index] << ", virial=" << virial_energies[index] - << ", consistency error=" << consistency_errors[index] << '\n'; - } - - INFO(report.str()); - - for (std::size_t index = 1; index < amplitudes.size(); ++index) { - CHECK(normalized_quadrupoles[index] > normalized_quadrupoles[index - 1]); - } - - CHECK(consistency_errors[0] < 1.0e-5); - for (std::size_t index = 1; index < amplitudes.size(); ++index) { - CHECK(consistency_errors[index] < 5.0e-5); - } + CHECK(consistency_errors[0] < 1.0e-5); + for (std::size_t index = 1; index < amplitudes.size(); ++index) { + CHECK(consistency_errors[index] < 5.0e-5); + } } diff --git a/tests/quadrature/policy.cpp b/tests/quadrature/policy.cpp index f228bec..b3c7a9b 100644 --- a/tests/quadrature/policy.cpp +++ b/tests/quadrature/policy.cpp @@ -643,17 +643,15 @@ TEST_CASE( CHECK(fem.displacementFes->GetOrdering() == mfem::Ordering::byNODES); CHECK(fem.enthalpyFes->GetVDim() == 1); - REQUIRE(fem.blockTrueOffsets.Size() == 3); - CHECK(fem.blockTrueOffsets[0] == 0); - CHECK(fem.blockTrueOffsets[1] == fem.displacementFes->GetTrueVSize()); - CHECK(fem.blockTrueOffsets[2] == fem.displacementFes->GetTrueVSize() + fem.densityFes->GetTrueVSize()); + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - REQUIRE(fem.gravityBlockTrueOffsets.Size() == 3); - CHECK(fem.gravityBlockTrueOffsets[0] == 0); - CHECK(fem.gravityBlockTrueOffsets[1] == fem.gravityFluxFes->GetTrueVSize()); - CHECK( - fem.gravityBlockTrueOffsets[2] == fem.gravityFluxFes->GetTrueVSize() + fem.gravityPotentialFes->GetTrueVSize() - ); + const mean_field::field::FieldDofMap densityMap = + mean_field::field::make_field_dof_map(*fem.densityFes); + const mean_field::field::FieldDofMap displacementMap = + mean_field::field::make_field_dof_map(*fem.displacementFes); - CHECK(fem.gravityContext.source_form->Height() == fem.gravityPotentialFes->GetTrueVSize()); -} \ No newline at end of file + CHECK(densityMap.full_size() == fem.densityFes->GetTrueVSize()); + CHECK(densityMap.reduced_size() < densityMap.full_size()); + CHECK(displacementMap.full_size() == fem.displacementFes->GetTrueVSize()); + CHECK(displacementMap.reduced_size() == displacementMap.full_size()); +} diff --git a/tests/test_helpers.cppm b/tests/test_helpers.cppm index 9609d05..3d28352 100644 --- a/tests/test_helpers.cppm +++ b/tests/test_helpers.cppm @@ -3,6 +3,7 @@ module; #include #include #include +#include #include #include @@ -12,410 +13,456 @@ export module test_helpers; import mean_field; template struct Tag { - std::array chars{}; + std::array chars{}; - // ReSharper disable once CppNonExplicitConvertingConstructor - consteval Tag( - std::array< - char, - N> arr - ) - : chars(arr) { - } + // ReSharper disable once CppNonExplicitConvertingConstructor + consteval Tag(std::array arr) : chars(arr) {} - // ReSharper disable once CppNonExplicitConversionOperator - constexpr operator const char *() const { - return chars.data(); - } + // ReSharper disable once CppNonExplicitConversionOperator + constexpr operator const char *() const { return chars.data(); } - // ReSharper disable once CppNonExplicitConversionOperator - constexpr operator Catch::StringRef() const { - return Catch::StringRef(chars.data(), N - 1); - } + // ReSharper disable once CppNonExplicitConversionOperator + constexpr operator Catch::StringRef() const { + return Catch::StringRef(chars.data(), N - 1); + } - template consteval Tag operator&(const Tag &other) const { - std::array res{}; - std::ranges::copy(chars.begin(), chars.end() - 1, res.begin()); - std::ranges::copy(other.chars, res.begin() + (N - 1)); - return {res}; - } + template + consteval Tag operator&(const Tag &other) const { + std::array res{}; + std::ranges::copy(chars.begin(), chars.end() - 1, res.begin()); + std::ranges::copy(other.chars, res.begin() + (N - 1)); + return {res}; + } }; template consteval auto make_tag(const char (&str)[N]) { - std::array res{}; - res[0] = '['; - std::ranges::copy(str, str + N - 1, res.begin() + 1); - res[N] = ']'; - res[N + 1] = '\0'; - return Tag{res}; + std::array res{}; + res[0] = '['; + std::ranges::copy(str, str + N - 1, res.begin() + 1); + res[N] = ']'; + res[N + 1] = '\0'; + return Tag{res}; } -template < - std::size_t N, - std::size_t M> -consteval auto sub_tag( - const Tag &parent, - const char (&str)[M] -) { - return parent & make_tag(str); +template +consteval auto sub_tag(const Tag &parent, const char (&str)[M]) { + return parent & make_tag(str); } namespace test_utils::detail { - std::optional configured_args; +std::optional configured_args; - mean_field::utils::Args make_default_args() { - mean_field::utils::Args args; - args.mesh_file = "sandbox.smesh"; - args.p.rtol = 1.0e-12; - args.p.atol = 1.0e-12; - return args; - } +mean_field::utils::Args make_default_args() { + mean_field::utils::Args args; + args.mesh_file = "sandbox.smesh"; + args.p.rtol = 1.0e-12; + args.p.atol = 1.0e-12; + return args; +} } // namespace test_utils::detail export namespace test_utils { - void set_args(mean_field::utils::Args args) { - detail::configured_args = std::move(args); - } +void set_args(mean_field::utils::Args args) { + detail::configured_args = std::move(args); +} - mean_field::utils::Args setup_args() { - if (detail::configured_args.has_value()) { - return *detail::configured_args; - } +mean_field::utils::Args setup_args() { + if (detail::configured_args.has_value()) { + return *detail::configured_args; + } - return detail::make_default_args(); - } + return detail::make_default_args(); +} } // namespace test_utils export namespace gravity_prepared_test_utils { - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - template inline mean_field::field::FieldDofMap make_field_map(const mean_field::fem::FEM &f) { - if constexpr (std::same_as) { - return mean_field::field::make_field_dof_map(*f.densityFes); - } else if constexpr (std::same_as) { - return mean_field::field::make_field_dof_map(*f.displacementFes); - } else { - static_assert(std::same_as); - return mean_field::field::make_field_dof_map(*f.gravityFluxFes); - } +template +inline mean_field::field::FieldDofMap +make_field_map(const mean_field::fem::FEM &f) { + if constexpr (std::same_as) { + return mean_field::field::make_field_dof_map( + *f.densityFes); + } else if constexpr (std::same_as) { + return mean_field::field::make_field_dof_map( + *f.displacementFes); + } else { + static_assert(std::same_as); + return mean_field::field::make_field_dof_map( + *f.gravityFluxFes); + } +} + +template +inline mfem::Vector gather_field(const mean_field::fem::FEM &f, + const mfem::Vector &true_vector) { + return make_field_map(f).gather(true_vector); +} + +inline mfem::Vector make_deterministic_vector(const int size, + const double phase = 0.0) { + mfem::Vector vector(size); + + for (int i = 0; i < size; ++i) { + const double index = static_cast(i + 1); + vector(i) = std::sin(0.37 * index + phase) + + 0.31 * std::cos(0.19 * index - 0.5 * phase); + } + + return vector; +} + +inline mfem::Vector make_displacement(const mean_field::fem::FEM &f, + const double scale) { + mfem::ParGridFunction displacement(f.displacementFes.get()); + + auto displacement_function = [scale](const mfem::Vector &position, + mfem::Vector &value) { + value.SetSize(3); + value(0) = scale * (0.04 * position(0) + 0.01 * position(1) * position(2)); + value(1) = + scale * (-0.03 * position(1) + 0.008 * position(0) * position(2)); + value(2) = scale * (0.02 * position(2) - 0.006 * position(0) * position(1)); + }; + + mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), + displacement_function); + displacement.ProjectCoefficient(coefficient); + + mfem::Vector displacement_true; + displacement.GetTrueDofs(displacement_true); + return displacement_true; +} + +inline mfem::Vector make_domain_supported_density(const mean_field::fem::FEM &f, + const bool stellar) { + mfem::Vector attribute_values(f.mesh->attributes.Max()); + attribute_values = 0.0; + + using DomainSchema = + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + + for (int i = 0; i < f.mesh->attributes.Size(); ++i) { + const int attribute = f.mesh->attributes[i]; + const bool is_stellar = DomainSchema::template attribute_belongs_to< + mean_field::utils::domain::Stellar>(attribute); + + if (is_stellar == stellar) { + attribute_values(attribute - 1) = 1.0; } + } - template - inline mfem::Vector gather_field( - const mean_field::fem::FEM &f, - const mfem::Vector &true_vector - ) { - return make_field_map(f).gather(true_vector); - } + mfem::PWConstCoefficient coefficient(attribute_values); + mfem::ParGridFunction density(f.densityFes.get()); + density.ProjectCoefficient(coefficient); - inline mfem::Vector make_deterministic_vector( - const int size, - const double phase = 0.0 - ) { - mfem::Vector vector(size); + mfem::Vector density_true; + density.GetTrueDofs(density_true); + return density_true; +} - for (int i = 0; i < size; ++i) { - const double index = static_cast(i + 1); - vector(i) = std::sin(0.37 * index + phase) + 0.31 * std::cos(0.19 * index - 0.5 * phase); - } +inline mfem::Vector linear_combination(const mfem::Vector &first, + const double first_scale, + const mfem::Vector &second, + const double second_scale) { + MFEM_VERIFY(first.Size() == second.Size(), + "Cannot combine vectors with different sizes."); - return vector; - } + mfem::Vector combination(first); + combination *= first_scale; + combination.Add(second_scale, second); + return combination; +} - inline mfem::Vector make_displacement( - const mean_field::fem::FEM &f, - const double scale - ) { - mfem::ParGridFunction displacement(f.displacementFes.get()); +inline 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); +} - auto displacement_function = [scale](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = scale * (0.04 * position(0) + 0.01 * position(1) * position(2)); - value(1) = scale * (-0.03 * position(1) + 0.008 * position(0) * position(2)); - value(2) = scale * (0.02 * position(2) - 0.006 * position(0) * position(1)); - }; +inline double global_dot(const mfem::Vector &first, const mfem::Vector &second, + MPI_Comm communicator) { + MFEM_VERIFY(first.Size() == second.Size(), + "Cannot take the dot product of vectors with different sizes."); - mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), displacement_function); - displacement.ProjectCoefficient(coefficient); + const double local_dot = first * second; + double global_dot = 0.0; + MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator); + return global_dot; +} - mfem::Vector displacement_true; - displacement.GetTrueDofs(displacement_true); - return displacement_true; - } +inline double relative_error(const mfem::Vector &computed, + const mfem::Vector &reference, + MPI_Comm communicator) { + MFEM_VERIFY(computed.Size() == reference.Size(), + "Cannot compare vectors with different sizes."); - inline mfem::Vector make_domain_supported_density( - const mean_field::fem::FEM &f, - const bool stellar - ) { - mfem::Vector attribute_values(f.mesh->attributes.Max()); - attribute_values = 0.0; + mfem::Vector difference(computed); + difference -= reference; - const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute(); + return global_norm(difference, communicator) / + std::max(global_norm(reference, communicator), + std::numeric_limits::epsilon()); +} - for (int i = 0; i < f.mesh->attributes.Size(); ++i) { - const int attribute = f.mesh->attributes[i]; - const bool is_stellar = attribute != vacuum_attribute; - - if (is_stellar == stellar) { - attribute_values(attribute - 1) = 1.0; - } - } - - mfem::PWConstCoefficient coefficient(attribute_values); - mfem::ParGridFunction density(f.densityFes.get()); - density.ProjectCoefficient(coefficient); - - mfem::Vector density_true; - density.GetTrueDofs(density_true); - return density_true; - } - - inline mfem::Vector linear_combination( - const mfem::Vector &first, - const double first_scale, - const mfem::Vector &second, - const double second_scale - ) { - MFEM_VERIFY(first.Size() == second.Size(), "Cannot combine vectors with different sizes."); - - mfem::Vector combination(first); - combination *= first_scale; - combination.Add(second_scale, second); - return combination; - } - - inline 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); - } - - inline double global_dot( - const mfem::Vector &first, - const mfem::Vector &second, - MPI_Comm communicator - ) { - MFEM_VERIFY(first.Size() == second.Size(), "Cannot take the dot product of vectors with different sizes."); - - const double local_dot = first * second; - double global_dot = 0.0; - MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator); - return global_dot; - } - - inline double relative_error( - const mfem::Vector &computed, - const mfem::Vector &reference, - MPI_Comm communicator - ) { - MFEM_VERIFY(computed.Size() == reference.Size(), "Cannot compare vectors with different sizes."); - - mfem::Vector difference(computed); - difference -= reference; - - return global_norm(difference, communicator) / - std::max(global_norm(reference, communicator), std::numeric_limits::epsilon()); - } - - inline double relative_scalar_error( - const double computed, - const double reference - ) { - return std::abs(computed - reference) / std::max(std::abs(reference), std::numeric_limits::epsilon()); - } +inline double relative_scalar_error(const double computed, + const double reference) { + return std::abs(computed - reference) / + std::max(std::abs(reference), std::numeric_limits::epsilon()); +} } // namespace gravity_prepared_test_utils export namespace field_dof_test_utils { - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - template - inline mean_field::field::FieldDofMap make_map(const mfem::ParFiniteElementSpace &finiteElementSpace) { - return mean_field::field::make_field_dof_map(finiteElementSpace); - } +inline mean_field::mapping::DomainMapper make_domain_mapper() { + const mean_field::utils::Args args = test_utils::setup_args(); + return mean_field::mapping::DomainMapper( + args.domain_mapper_options, + std::make_unique(args.kelvin_options)); +} - template - inline mfem::Vector make_deterministic_supported_vector( - const mfem::ParFiniteElementSpace &finiteElementSpace, - const double phase - ) { - const mean_field::field::FieldDofMap map = make_map(finiteElementSpace); - const mfem::Vector full = - gravity_prepared_test_utils::make_deterministic_vector(map.full_size(), phase); - return map.gather(full); - } +inline constexpr int vacuum_material_attribute = + DomainSchema::template material_attribute< + mean_field::utils::domain::Vacuum>(); - inline mfem::Vector make_supported_displacement( - const mean_field::fem::FEM &f, - const double phase - ) { - const mean_field::field::FieldDofMap map = - make_map(*f.displacementFes); - return map.gather(gravity_prepared_test_utils::make_displacement(f, phase)); - } +template +inline mean_field::field::FieldDofMap +make_map(const mfem::ParFiniteElementSpace &finiteElementSpace) { + return mean_field::field::make_field_dof_map( + finiteElementSpace); +} - inline void apply_hydrostatic_reference( - const mean_field::fem::FEM &f, - const mean_field::physics::RigidRotation &rotation, - const mfem::Vector &enthalpy, - const mfem::Vector &gravityPotential, - const mfem::Vector &displacement, - const double bernoulliConstant, - mfem::Vector &residual - ) { - const mean_field::field::FieldDofMap enthalpyMap = - make_map(*f.enthalpyFes); - const mean_field::field::FieldDofMap gravityPotentialMap = - make_map(*f.gravityPotentialFes); - const mean_field::field::FieldDofMap displacementMap = - make_map(*f.displacementFes); +template +inline mfem::Vector make_deterministic_supported_vector( + const mfem::ParFiniteElementSpace &finiteElementSpace, const double phase) { + const mean_field::field::FieldDofMap map = + make_map(finiteElementSpace); + const mfem::Vector full = + gravity_prepared_test_utils::make_deterministic_vector(map.full_size(), + phase); + return map.gather(full); +} - mfem::Vector enthalpyTrue(enthalpyMap.full_size()); - mfem::Vector gravityPotentialTrue(gravityPotentialMap.full_size()); - mfem::Vector displacementTrue(displacementMap.full_size()); - mfem::Vector residualTrue; +inline mfem::Vector make_supported_displacement(const mean_field::fem::FEM &f, + const double phase) { + const mean_field::field::FieldDofMap map = + make_map(*f.displacementFes); + return map.gather(gravity_prepared_test_utils::make_displacement(f, phase)); +} - enthalpyMap.scatter(enthalpy, enthalpyTrue); - gravityPotentialMap.scatter(gravityPotential, gravityPotentialTrue); - displacementMap.scatter(displacement, displacementTrue); +inline void apply_hydrostatic_reference( + const mean_field::fem::FEM &f, + const mean_field::physics::RigidRotation &rotation, + const mfem::Vector &enthalpy, const mfem::Vector &gravityPotential, + const mfem::Vector &displacement, const double bernoulliConstant, + mfem::Vector &residual) { + const mean_field::field::FieldDofMap enthalpyMap = + make_map(*f.enthalpyFes); + const mean_field::field::FieldDofMap gravityPotentialMap = + make_map(*f.gravityPotentialFes); + const mean_field::field::FieldDofMap displacementMap = + make_map(*f.displacementFes); - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, enthalpyTrue, gravityPotentialTrue, displacementTrue, - bernoulliConstant, residualTrue - ); + mfem::Vector enthalpyTrue(enthalpyMap.full_size()); + mfem::Vector gravityPotentialTrue(gravityPotentialMap.full_size()); + mfem::Vector displacementTrue(displacementMap.full_size()); + mfem::Vector residualTrue; - residual.SetSize(enthalpyMap.reduced_size()); - enthalpyMap.gather(residualTrue, residual); - } + enthalpyMap.scatter(enthalpy, enthalpyTrue); + gravityPotentialMap.scatter(gravityPotential, gravityPotentialTrue); + displacementMap.scatter(displacement, displacementTrue); + + mean_field::operators::kernels::apply_hydrostatic_equilibrium( + f, *f.domainMapperStateless, rotation, enthalpyTrue, gravityPotentialTrue, + displacementTrue, bernoulliConstant, residualTrue); + + residual.SetSize(enthalpyMap.reduced_size()); + enthalpyMap.gather(residualTrue, residual); +} } // namespace field_dof_test_utils export namespace tags { - inline constexpr auto geometry = make_tag("geometry"); - inline constexpr auto physics = make_tag("physics"); - inline constexpr auto unit = make_tag("unit"); - inline constexpr auto mesh = make_tag("mesh"); - inline constexpr auto integration = make_tag("integration"); - inline constexpr auto solver = make_tag("solver"); - inline constexpr auto integrator = make_tag("integrator"); - inline constexpr auto mapping = make_tag("mapping"); - inline constexpr auto utils = make_tag("utils"); - inline constexpr auto mfem_operators = make_tag("operators"); - inline constexpr auto initialization = make_tag("initialization"); - inline constexpr auto accuracy = make_tag("accuracy"); - inline constexpr auto closure = make_tag("closure"); - inline constexpr auto kernels = make_tag("kernels"); - inline constexpr auto surface = make_tag("surface"); - inline constexpr auto model = make_tag("model"); +inline constexpr auto geometry = make_tag("geometry"); +inline constexpr auto physics = make_tag("physics"); +inline constexpr auto unit = make_tag("unit"); +inline constexpr auto mesh = make_tag("mesh"); +inline constexpr auto integration = make_tag("integration"); +inline constexpr auto solver = make_tag("solver"); +inline constexpr auto integrator = make_tag("integrator"); +inline constexpr auto mapping = make_tag("mapping"); +inline constexpr auto utils = make_tag("utils"); +inline constexpr auto mfem_operators = make_tag("operators"); +inline constexpr auto initialization = make_tag("initialization"); +inline constexpr auto accuracy = make_tag("accuracy"); +inline constexpr auto closure = make_tag("closure"); +inline constexpr auto kernels = make_tag("kernels"); +inline constexpr auto surface = make_tag("surface"); +inline constexpr auto model = make_tag("model"); - inline constexpr auto field = sub_tag(mesh & physics, "field"); +inline constexpr auto field = sub_tag(mesh & physics, "field"); +inline constexpr auto field_dof = field & make_tag("dof"); +inline constexpr auto field_dof_unit = field_dof & unit; +inline constexpr auto field_dof_integration = field_dof & integration; - inline constexpr auto legacy_comparison = make_tag("legacy_comparison"); - inline constexpr auto pressure = sub_tag(physics, "pressure"); +inline constexpr auto pressure = sub_tag(physics, "pressure"); - inline constexpr auto hydro = sub_tag(physics, "hydro"); - inline constexpr auto jacobian = sub_tag(integration & physics, "jacobian"); - inline constexpr auto residuals = sub_tag(integration & physics, "residuals"); - inline constexpr auto volume = sub_tag(mesh & geometry, "volume"); - inline constexpr auto quadrature = sub_tag(mesh & geometry & solver, "quadrature"); - inline constexpr auto convergence = sub_tag(solver, "convergence"); - inline constexpr auto transformations = sub_tag(mesh & geometry, "transformations"); +inline constexpr auto hydro = sub_tag(physics, "hydro"); +inline constexpr auto jacobian = sub_tag(integration & physics, "jacobian"); +inline constexpr auto residuals = sub_tag(integration & physics, "residuals"); +inline constexpr auto volume = sub_tag(mesh & geometry, "volume"); +inline constexpr auto quadrature = + sub_tag(mesh & geometry & solver, "quadrature"); +inline constexpr auto convergence = sub_tag(solver, "convergence"); +inline constexpr auto transformations = + sub_tag(mesh & geometry, "transformations"); - inline constexpr auto h_refinement = sub_tag(mesh & convergence, "h_refinement"); - inline constexpr auto p_refinement = sub_tag(mesh & convergence, "p_refinement"); +inline constexpr auto h_refinement = + sub_tag(mesh & convergence, "h_refinement"); +inline constexpr auto p_refinement = + sub_tag(mesh & convergence, "p_refinement"); - inline constexpr auto analytic_comparison = sub_tag(solver & physics & residuals, "analytic_comparison"); - inline constexpr auto self_consistency = sub_tag(solver & physics, "self_consistency"); +inline constexpr auto analytic_comparison = + sub_tag(solver & physics & residuals, "analytic_comparison"); +inline constexpr auto self_consistency = + sub_tag(solver & physics, "self_consistency"); - inline constexpr auto centrifugal = sub_tag(solver & physics, "centrifugal"); - inline constexpr auto advection = sub_tag(solver & physics, "advection"); - inline constexpr auto coriolis = sub_tag(solver & physics, "coriolis"); - inline constexpr auto gravity = sub_tag(solver & physics, "gravity"); - inline constexpr auto enthalpy = sub_tag(solver & physics, "enthalpy"); - inline constexpr auto barotrope = sub_tag(physics, "barotrope"); - inline constexpr auto mass_continuity = sub_tag(solver & physics, "mass_continuity"); - inline constexpr auto pressure_gradient = sub_tag(solver & physics, "pressure_gradient"); - inline constexpr auto viscosity = sub_tag(solver & physics, "viscosity"); +inline constexpr auto centrifugal = sub_tag(solver & physics, "centrifugal"); +inline constexpr auto advection = sub_tag(solver & physics, "advection"); +inline constexpr auto coriolis = sub_tag(solver & physics, "coriolis"); +inline constexpr auto gravity = sub_tag(solver & physics, "gravity"); +inline constexpr auto enthalpy = sub_tag(solver & physics, "enthalpy"); +inline constexpr auto barotrope = sub_tag(physics, "barotrope"); +inline constexpr auto mass_continuity = + sub_tag(solver & physics, "mass_continuity"); +inline constexpr auto pressure_gradient = + sub_tag(solver & physics, "pressure_gradient"); +inline constexpr auto viscosity = sub_tag(solver & physics, "viscosity"); - inline constexpr auto compactification = sub_tag(mesh & mapping, "compactification"); - inline constexpr auto kelvin = sub_tag(compactification, "kelvin"); +inline constexpr auto compactification = + sub_tag(mesh & mapping, "compactification"); +inline constexpr auto kelvin = sub_tag(compactification, "kelvin"); +inline constexpr auto mapping_evaluator = + mapping & make_tag("grid_function_evaluator"); +inline constexpr auto mapping_evaluator_unit = mapping_evaluator & unit; - inline constexpr auto prepared = sub_tag(solver & physics, "prepared"); - inline constexpr auto contexts = sub_tag(solver, "contexts"); +inline constexpr auto prepared = sub_tag(solver & physics, "prepared"); +inline constexpr auto contexts = sub_tag(solver, "contexts"); - inline constexpr auto domain = sub_tag(mesh, "domain"); +inline constexpr auto domain = sub_tag(mesh, "domain"); - // Canonical gravity-suite tags. These intentionally compose leaf tags - // exactly once so Catch2 output remains useful and free of repeated - // [solver]/[physics] entries inherited from older composite tags. - inline constexpr auto gravity_unit = gravity & unit; - inline constexpr auto gravity_integration = gravity & integration; - inline constexpr auto gravity_operator = gravity & mfem_operators; - inline constexpr auto gravity_prepared = gravity & make_tag("prepared"); - inline constexpr auto gravity_context = gravity & make_tag("context"); - inline constexpr auto gravity_kernel = gravity & kernels; - inline constexpr auto gravity_accuracy = gravity & accuracy; - inline constexpr auto gravity_legacy = gravity & legacy_comparison; - inline constexpr auto gravity_operator_unit = gravity_operator & unit; - inline constexpr auto gravity_operator_integration = gravity_operator & integration; - inline constexpr auto gravity_operator_convergence = gravity_operator & integration & make_tag("convergence"); - inline constexpr auto gravity_analytic = gravity & integration & make_tag("analytic_comparison"); - inline constexpr auto gravity_consistency = gravity & integration & make_tag("self_consistency"); - inline constexpr auto gravity_prepared_jacobian = gravity_prepared & integration & make_tag("jacobian"); - inline constexpr auto gravity_prepared_unit = gravity_prepared & unit; - inline constexpr auto gravity_prepared_jacobian_accuracy = gravity_prepared_jacobian & accuracy; - inline constexpr auto gravity_kernel_accuracy = gravity_kernel & accuracy; - inline constexpr auto gravity_kernel_integration = gravity_kernel & integration; - inline constexpr auto gravity_kernel_convergence = gravity_kernel & integration & make_tag("convergence"); - inline constexpr auto gravity_analytic_accuracy = gravity_analytic & accuracy; - inline constexpr auto gravity_analytic_initialization = gravity_analytic & initialization; - inline constexpr auto gravity_consistency_initialization = gravity_consistency & initialization; +// Canonical gravity-suite tags. These intentionally compose leaf tags +// exactly once so Catch2 output remains useful and free of repeated +// [solver]/[physics] entries inherited from older composite tags. +inline constexpr auto gravity_unit = gravity & unit; +inline constexpr auto gravity_integration = gravity & integration; +inline constexpr auto gravity_operator = gravity & mfem_operators; +inline constexpr auto gravity_prepared = gravity & make_tag("prepared"); +inline constexpr auto gravity_context = gravity & make_tag("context"); +inline constexpr auto gravity_kernel = gravity & kernels; +inline constexpr auto gravity_accuracy = gravity & accuracy; +inline constexpr auto gravity_operator_unit = gravity_operator & unit; +inline constexpr auto gravity_operator_integration = + gravity_operator & integration; +inline constexpr auto gravity_operator_convergence = + gravity_operator & integration & make_tag("convergence"); +inline constexpr auto gravity_analytic = + gravity & integration & make_tag("analytic_comparison"); +inline constexpr auto gravity_consistency = + gravity & integration & make_tag("self_consistency"); +inline constexpr auto gravity_prepared_jacobian = + gravity_prepared & integration & make_tag("jacobian"); +inline constexpr auto gravity_prepared_unit = gravity_prepared & unit; +inline constexpr auto gravity_prepared_jacobian_accuracy = + gravity_prepared_jacobian & accuracy; +inline constexpr auto gravity_kernel_accuracy = gravity_kernel & accuracy; +inline constexpr auto gravity_kernel_integration = gravity_kernel & integration; +inline constexpr auto gravity_kernel_convergence = + gravity_kernel & integration & make_tag("convergence"); +inline constexpr auto gravity_analytic_accuracy = gravity_analytic & accuracy; +inline constexpr auto gravity_consistency_accuracy = + gravity_consistency & accuracy; +inline constexpr auto gravity_integrator_unit = gravity & integrator & unit; - inline constexpr auto barotrope_prepared = barotrope & solver & make_tag("prepared"); - inline constexpr auto barotrope_prepared_jacobian = barotrope_prepared & integration & make_tag("jacobian"); - inline constexpr auto barotrope_context = barotrope & solver & make_tag("context"); - inline constexpr auto barotrope_context_integration = barotrope_context & integration; - inline constexpr auto barotrope_prepared_analytic = - barotrope_prepared & integration & make_tag("analytic_comparison"); - inline constexpr auto barotrope_prepared_jacobian_accuracy = barotrope_prepared_jacobian & accuracy; - inline constexpr auto barotrope_prepared_jacobian_geometry = barotrope_prepared_jacobian & geometry; - inline constexpr auto barotrope_prepared_jacobian_unit = barotrope_prepared_jacobian & unit; +inline constexpr auto barotrope_prepared = + barotrope & solver & make_tag("prepared"); +inline constexpr auto barotrope_eos_unit = barotrope & unit & make_tag("eos"); +inline constexpr auto barotrope_eos_jacobian = + barotrope_eos_unit & integration & make_tag("jacobian"); +inline constexpr auto barotrope_pressure_quadrature = + barotrope & mesh & geometry & solver & make_tag("pressure") & + make_tag("pressure_gradient") & make_tag("quadrature"); +inline constexpr auto barotrope_pressure_quadrature_unit = + barotrope_pressure_quadrature & unit; +inline constexpr auto barotrope_pressure_quadrature_accuracy = + barotrope_pressure_quadrature & accuracy; +inline constexpr auto barotrope_prepared_jacobian = + barotrope_prepared & integration & make_tag("jacobian"); +inline constexpr auto barotrope_context = + barotrope & solver & make_tag("context"); +inline constexpr auto barotrope_context_integration = + barotrope_context & integration; +inline constexpr auto barotrope_prepared_analytic = + barotrope_prepared & integration & make_tag("analytic_comparison"); +inline constexpr auto barotrope_prepared_jacobian_accuracy = + barotrope_prepared_jacobian & accuracy; +inline constexpr auto barotrope_prepared_jacobian_geometry = + barotrope_prepared_jacobian & geometry; +inline constexpr auto barotrope_prepared_jacobian_unit = + barotrope_prepared_jacobian & unit; - // Canonical hydrostatic-suite tags. The leaf tags are composed directly - // so inherited [physics]/[solver] tags appear only once. - inline constexpr auto barotrope_hydrostatic = barotrope & solver & make_tag("hydro"); - inline constexpr auto barotrope_hydrostatic_context = barotrope_hydrostatic & make_tag("context"); - inline constexpr auto barotrope_hydrostatic_prepared = barotrope_hydrostatic & make_tag("prepared"); - inline constexpr auto barotrope_hydrostatic_prepared_residual = - barotrope_hydrostatic_prepared & integration & make_tag("residual"); - inline constexpr auto barotrope_hydrostatic_prepared_jacobian = - barotrope_hydrostatic_prepared & integration & make_tag("jacobian"); - inline constexpr auto barotrope_hydrostatic_prepared_analytic = - barotrope_hydrostatic_prepared & integration & make_tag("analytic_comparison"); +// Canonical hydrostatic-suite tags. The leaf tags are composed directly +// so inherited [physics]/[solver] tags appear only once. +inline constexpr auto barotrope_hydrostatic = + barotrope & solver & make_tag("hydro"); +inline constexpr auto barotrope_hydrostatic_context = + barotrope_hydrostatic & make_tag("context"); +inline constexpr auto barotrope_hydrostatic_prepared = + barotrope_hydrostatic & make_tag("prepared"); +inline constexpr auto barotrope_hydrostatic_prepared_residual = + barotrope_hydrostatic_prepared & integration & make_tag("residual"); +inline constexpr auto barotrope_hydrostatic_prepared_jacobian = + barotrope_hydrostatic_prepared & integration & make_tag("jacobian"); +inline constexpr auto barotrope_hydrostatic_prepared_analytic = + barotrope_hydrostatic_prepared & integration & + make_tag("analytic_comparison"); - inline constexpr auto barotrope_mass_normalization = - barotrope & solver & make_tag("mass_normalization"); - inline constexpr auto barotrope_mass_normalization_context = - barotrope_mass_normalization & make_tag("context"); - inline constexpr auto barotrope_mass_normalization_prepared = - barotrope_mass_normalization & make_tag("prepared"); - inline constexpr auto barotrope_mass_normalization_jacobian = - barotrope_mass_normalization_prepared & integration & make_tag("jacobian"); - inline constexpr auto barotrope_mass_normalization_analytic = - barotrope_mass_normalization_prepared & integration & make_tag("analytic_comparison"); +inline constexpr auto barotrope_mass_normalization = + barotrope & solver & make_tag("mass_normalization"); +inline constexpr auto barotrope_mass_normalization_context = + barotrope_mass_normalization & make_tag("context"); +inline constexpr auto barotrope_mass_normalization_prepared = + barotrope_mass_normalization & make_tag("prepared"); +inline constexpr auto barotrope_mass_normalization_jacobian = + barotrope_mass_normalization_prepared & integration & make_tag("jacobian"); +inline constexpr auto barotrope_mass_normalization_analytic = + barotrope_mass_normalization_prepared & integration & + make_tag("analytic_comparison"); - inline constexpr auto rotation_prepared = centrifugal & make_tag("prepared"); - inline constexpr auto rotation_context = centrifugal & make_tag("context"); - inline constexpr auto rotation_analytic = centrifugal & integration & make_tag("analytic_comparison"); - inline constexpr auto rotation_context_unit = rotation_context & unit; - inline constexpr auto rotation_prepared_unit = rotation_prepared & unit; - inline constexpr auto rotation_prepared_jacobian = rotation_prepared & integration & make_tag("jacobian"); - inline constexpr auto rotation_prepared_jacobian_accuracy = rotation_prepared_jacobian & accuracy; - inline constexpr auto rotation_kernel_accuracy = centrifugal & kernels & accuracy; - inline constexpr auto rotation_analytic_unit = rotation_analytic & unit; - inline constexpr auto rotation_analytic_accuracy = rotation_analytic & accuracy; - inline constexpr auto rotation_analytic_accuracy_geometry = rotation_analytic_accuracy & geometry; +inline constexpr auto rotation_prepared = centrifugal & make_tag("prepared"); +inline constexpr auto rotation_context = centrifugal & make_tag("context"); +inline constexpr auto rotation_analytic = + centrifugal & integration & make_tag("analytic_comparison"); +inline constexpr auto rotation_context_unit = rotation_context & unit; +inline constexpr auto rotation_prepared_unit = rotation_prepared & unit; +inline constexpr auto rotation_prepared_jacobian = + rotation_prepared & integration & make_tag("jacobian"); +inline constexpr auto rotation_prepared_jacobian_accuracy = + rotation_prepared_jacobian & accuracy; +inline constexpr auto rotation_kernel_accuracy = + centrifugal & kernels & accuracy; +inline constexpr auto rotation_integrator_unit = centrifugal & integrator & unit; +inline constexpr auto rotation_integrator_integration = + centrifugal & integrator & integration; +inline constexpr auto rotation_integrator_convergence = + rotation_integrator_integration & convergence & h_refinement; +inline constexpr auto rotation_analytic_unit = rotation_analytic & unit; +inline constexpr auto rotation_analytic_accuracy = rotation_analytic & accuracy; +inline constexpr auto rotation_analytic_accuracy_geometry = + rotation_analytic_accuracy & geometry; } // namespace tags diff --git a/tests/utils/domain.cpp b/tests/utils/domain.cpp index cbd7e64..cd56e42 100644 --- a/tests/utils/domain.cpp +++ b/tests/utils/domain.cpp @@ -12,1124 +12,1232 @@ import mean_field; import test_helpers; namespace domain_test_utils { - struct UnregisteredDomain final : public mean_field::utils::domain::Domain { - static constexpr std::string_view name = "unregistered_domain"; - }; +struct UnregisteredDomain final : public mean_field::utils::domain::Domain { + static constexpr std::string_view name = "unregistered_domain"; +}; - struct UnregisteredBoundary final : public mean_field::utils::domain::Boundary { - static constexpr std::string_view name = "unregistered_boundary"; - }; +struct UnregisteredBoundary final : public mean_field::utils::domain::Boundary { + static constexpr std::string_view name = "unregistered_boundary"; +}; - struct BoundaryEdge { - int firstVertexId{-1}; - int secondVertexId{-1}; - int attribute{0}; - }; +struct BoundaryEdge { + int firstVertexId{-1}; + int secondVertexId{-1}; + int attribute{0}; +}; - struct StroidCase { - std::string_view name; - int refinementLevels{0}; - int order{1}; - double flattening{0.0}; - }; +struct StroidCase { + std::string_view name; + int refinementLevels{0}; + int order{1}; + double flattening{0.0}; +}; - template - concept CanFormMaterialList = requires { typename mean_field::utils::domain::MaterialList; }; +template +concept CanFormMaterialList = requires { + typename mean_field::utils::domain::MaterialList; +}; - template - concept CanFormBoundaryList = requires { typename mean_field::utils::domain::BoundaryList; }; +template +concept CanFormBoundaryList = requires { + typename mean_field::utils::domain::BoundaryList; +}; - template - concept CanFormDomainBoundary = - requires { typename mean_field::utils::domain::DomainBoundary; }; +template +concept CanFormDomainBoundary = requires { + typename mean_field::utils::domain::DomainBoundary; +}; - template - concept CanFormSchema = - requires { typename mean_field::utils::domain::DomainSchema; }; +template +concept CanFormSchema = requires { + typename mean_field::utils::domain::DomainSchema; +}; - [[nodiscard]] - int vertex_id( - const int xElementCount, - const int x, - const int y - ) { - return y * (xElementCount + 1) + x; +[[nodiscard]] +int vertex_id(const int xElementCount, const int x, const int y) { + return y * (xElementCount + 1) + x; +} + +[[nodiscard]] +int cell_index(const int xElementCount, const int x, const int y) { + return y * xElementCount + x; +} + +[[nodiscard]] +int cell_attribute(const std::vector &attributes, const int xElementCount, + const int x, const int y) { + return attributes.at( + static_cast(cell_index(xElementCount, x, y))); +} + +template +void append_interface_boundaries(std::vector &boundaries, + const std::vector &attributes, + const int xElementCount, + const int yElementCount, + FirstPredicateT firstPredicate, + SecondPredicateT secondPredicate, + const int boundaryAttribute) { + /* + * Vertical internal faces. + */ + for (int y = 0; y < yElementCount; ++y) { + for (int x = 1; x < xElementCount; ++x) { + const int leftAttribute = + cell_attribute(attributes, xElementCount, x - 1, y); + + const int rightAttribute = + cell_attribute(attributes, xElementCount, x, y); + + const bool matches = + (firstPredicate(leftAttribute) && secondPredicate(rightAttribute)) || + (secondPredicate(leftAttribute) && firstPredicate(rightAttribute)); + + if (!matches) { + continue; + } + + boundaries.push_back( + {.firstVertexId = vertex_id(xElementCount, x, y), + .secondVertexId = vertex_id(xElementCount, x, y + 1), + .attribute = boundaryAttribute}); } + } - [[nodiscard]] - int cell_index( - const int xElementCount, - const int x, - const int y - ) { - return y * xElementCount + x; + /* + * Horizontal internal faces. + */ + for (int y = 1; y < yElementCount; ++y) { + for (int x = 0; x < xElementCount; ++x) { + const int lowerAttribute = + cell_attribute(attributes, xElementCount, x, y - 1); + + const int upperAttribute = + cell_attribute(attributes, xElementCount, x, y); + + const bool matches = + (firstPredicate(lowerAttribute) && secondPredicate(upperAttribute)) || + (secondPredicate(lowerAttribute) && firstPredicate(upperAttribute)); + + if (!matches) { + continue; + } + + boundaries.push_back( + {.firstVertexId = vertex_id(xElementCount, x, y), + .secondVertexId = vertex_id(xElementCount, x + 1, y), + .attribute = boundaryAttribute}); } + } +} - [[nodiscard]] - int cell_attribute( - const std::vector &attributes, - const int xElementCount, - const int x, - const int y - ) { - return attributes.at(static_cast(cell_index(xElementCount, x, y))); +template +void append_exterior_boundaries(std::vector &boundaries, + const std::vector &attributes, + const int xElementCount, + const int yElementCount, PredicateT predicate, + const int boundaryAttribute) { + /* + * Bottom. + */ + for (int x = 0; x < xElementCount; ++x) { + if (predicate(cell_attribute(attributes, xElementCount, x, 0))) { + boundaries.push_back( + {.firstVertexId = vertex_id(xElementCount, x, 0), + .secondVertexId = vertex_id(xElementCount, x + 1, 0), + .attribute = boundaryAttribute}); } + } - template < - typename FirstPredicateT, - typename SecondPredicateT> - void append_interface_boundaries( - std::vector &boundaries, - const std::vector &attributes, - const int xElementCount, - const int yElementCount, - FirstPredicateT firstPredicate, - SecondPredicateT secondPredicate, - const int boundaryAttribute - ) { - /* - * Vertical internal faces. - */ - for (int y = 0; y < yElementCount; ++y) { - for (int x = 1; x < xElementCount; ++x) { - const int leftAttribute = cell_attribute(attributes, xElementCount, x - 1, y); - - const int rightAttribute = cell_attribute(attributes, xElementCount, x, y); - - const bool matches = (firstPredicate(leftAttribute) && secondPredicate(rightAttribute)) || - (secondPredicate(leftAttribute) && firstPredicate(rightAttribute)); - - if (!matches) { - continue; - } - - boundaries.push_back( - {.firstVertexId = vertex_id(xElementCount, x, y), - .secondVertexId = vertex_id(xElementCount, x, y + 1), - .attribute = boundaryAttribute} - ); - } - } - - /* - * Horizontal internal faces. - */ - for (int y = 1; y < yElementCount; ++y) { - for (int x = 0; x < xElementCount; ++x) { - const int lowerAttribute = cell_attribute(attributes, xElementCount, x, y - 1); - - const int upperAttribute = cell_attribute(attributes, xElementCount, x, y); - - const bool matches = (firstPredicate(lowerAttribute) && secondPredicate(upperAttribute)) || - (secondPredicate(lowerAttribute) && firstPredicate(upperAttribute)); - - if (!matches) { - continue; - } - - boundaries.push_back( - {.firstVertexId = vertex_id(xElementCount, x, y), - .secondVertexId = vertex_id(xElementCount, x + 1, y), - .attribute = boundaryAttribute} - ); - } - } + /* + * Top. + */ + for (int x = 0; x < xElementCount; ++x) { + if (predicate( + cell_attribute(attributes, xElementCount, x, yElementCount - 1))) { + boundaries.push_back( + {.firstVertexId = vertex_id(xElementCount, x, yElementCount), + .secondVertexId = vertex_id(xElementCount, x + 1, yElementCount), + .attribute = boundaryAttribute}); } + } - template - void append_exterior_boundaries( - std::vector &boundaries, - const std::vector &attributes, - const int xElementCount, - const int yElementCount, - PredicateT predicate, - const int boundaryAttribute - ) { - /* - * Bottom. - */ - for (int x = 0; x < xElementCount; ++x) { - if (predicate(cell_attribute(attributes, xElementCount, x, 0))) { - boundaries.push_back( - {.firstVertexId = vertex_id(xElementCount, x, 0), - .secondVertexId = vertex_id(xElementCount, x + 1, 0), - .attribute = boundaryAttribute} - ); - } - } - - /* - * Top. - */ - for (int x = 0; x < xElementCount; ++x) { - if (predicate(cell_attribute(attributes, xElementCount, x, yElementCount - 1))) { - boundaries.push_back( - {.firstVertexId = vertex_id(xElementCount, x, yElementCount), - .secondVertexId = vertex_id(xElementCount, x + 1, yElementCount), - .attribute = boundaryAttribute} - ); - } - } - - /* - * Left. - */ - for (int y = 0; y < yElementCount; ++y) { - if (predicate(cell_attribute(attributes, xElementCount, 0, y))) { - boundaries.push_back( - {.firstVertexId = vertex_id(xElementCount, 0, y), - .secondVertexId = vertex_id(xElementCount, 0, y + 1), - .attribute = boundaryAttribute} - ); - } - } - - /* - * Right. - */ - for (int y = 0; y < yElementCount; ++y) { - if (predicate(cell_attribute(attributes, xElementCount, xElementCount - 1, y))) { - boundaries.push_back( - {.firstVertexId = vertex_id(xElementCount, xElementCount, y), - .secondVertexId = vertex_id(xElementCount, xElementCount, y + 1), - .attribute = boundaryAttribute} - ); - } - } + /* + * Left. + */ + for (int y = 0; y < yElementCount; ++y) { + if (predicate(cell_attribute(attributes, xElementCount, 0, y))) { + boundaries.push_back( + {.firstVertexId = vertex_id(xElementCount, 0, y), + .secondVertexId = vertex_id(xElementCount, 0, y + 1), + .attribute = boundaryAttribute}); } + } - [[nodiscard]] - mfem::Mesh make_grid_mesh( - const int xElementCount, - const int yElementCount, - const std::vector &attributes, - const std::vector &boundaryEdges - ) { - REQUIRE(static_cast(attributes.size()) == xElementCount * yElementCount); - - mfem::Mesh mesh( - 2, (xElementCount + 1) * (yElementCount + 1), xElementCount * yElementCount, - static_cast(boundaryEdges.size()), 2 - ); - - for (int y = 0; y <= yElementCount; ++y) { - for (int x = 0; x <= xElementCount; ++x) { - mesh.AddVertex(static_cast(x), static_cast(y)); - } - } - - for (int y = 0; y < yElementCount; ++y) { - for (int x = 0; x < xElementCount; ++x) { - const int lowerLeft = vertex_id(xElementCount, x, y); - - const int lowerRight = vertex_id(xElementCount, x + 1, y); - - const int upperRight = vertex_id(xElementCount, x + 1, y + 1); - - const int upperLeft = vertex_id(xElementCount, x, y + 1); - - mesh.AddQuad( - lowerLeft, lowerRight, upperRight, upperLeft, cell_attribute(attributes, xElementCount, x, y) - ); - } - } - - for (const BoundaryEdge &boundary : boundaryEdges) { - mesh.AddBdrSegment(boundary.firstVertexId, boundary.secondVertexId, boundary.attribute); - } - - mesh.FinalizeTopology(false); - mesh.Finalize(false, false); - - REQUIRE(mesh.GetNBE() == static_cast(boundaryEdges.size())); - - return mesh; + /* + * Right. + */ + for (int y = 0; y < yElementCount; ++y) { + if (predicate( + cell_attribute(attributes, xElementCount, xElementCount - 1, y))) { + boundaries.push_back( + {.firstVertexId = vertex_id(xElementCount, xElementCount, y), + .secondVertexId = vertex_id(xElementCount, xElementCount, y + 1), + .attribute = boundaryAttribute}); } + } +} - [[nodiscard]] - std::vector make_layered_attributes() { - constexpr int xElementCount = 5; - constexpr int yElementCount = 5; +[[nodiscard]] +mfem::Mesh make_grid_mesh(const int xElementCount, const int yElementCount, + const std::vector &attributes, + const std::vector &boundaryEdges) { + REQUIRE(static_cast(attributes.size()) == xElementCount * yElementCount); - std::vector attributes(xElementCount * yElementCount, 3); + mfem::Mesh mesh(2, (xElementCount + 1) * (yElementCount + 1), + xElementCount * yElementCount, + static_cast(boundaryEdges.size()), 2); - for (int y = 1; y <= 3; ++y) { - for (int x = 1; x <= 3; ++x) { - attributes[static_cast(cell_index(xElementCount, x, y))] = 2; - } - } - - attributes[static_cast(cell_index(xElementCount, 2, 2))] = 1; - - return attributes; + for (int y = 0; y <= yElementCount; ++y) { + for (int x = 0; x <= xElementCount; ++x) { + mesh.AddVertex(static_cast(x), static_cast(y)); } + } - [[nodiscard]] - mfem::Mesh make_layered_mesh( - const bool includeStellarSurface = true, - const bool includeInfinitySurface = true, - const int stellarSurfaceAttribute = 1, - const int infinitySurfaceAttribute = 2 - ) { - constexpr int xElementCount = 5; - constexpr int yElementCount = 5; + for (int y = 0; y < yElementCount; ++y) { + for (int x = 0; x < xElementCount; ++x) { + const int lowerLeft = vertex_id(xElementCount, x, y); - const std::vector attributes = make_layered_attributes(); + const int lowerRight = vertex_id(xElementCount, x + 1, y); - std::vector boundaries; + const int upperRight = vertex_id(xElementCount, x + 1, y + 1); - const auto isStellar = [](const int materialId) { return materialId == 1 || materialId == 2; }; + const int upperLeft = vertex_id(xElementCount, x, y + 1); - const auto isVacuum = [](const int materialId) { return materialId == 3; }; - - if (includeStellarSurface) { - append_interface_boundaries( - boundaries, attributes, xElementCount, yElementCount, isStellar, isVacuum, stellarSurfaceAttribute - ); - } - - if (includeInfinitySurface) { - append_exterior_boundaries( - boundaries, attributes, xElementCount, yElementCount, isVacuum, infinitySurfaceAttribute - ); - } - - return make_grid_mesh(xElementCount, yElementCount, attributes, boundaries); + mesh.AddQuad(lowerLeft, lowerRight, upperRight, upperLeft, + cell_attribute(attributes, xElementCount, x, y)); } + } - template void check_schema_is_valid(const mfem::Mesh &mesh) { - const auto validation = mean_field::utils::domain::validate_schema(mesh); + for (const BoundaryEdge &boundary : boundaryEdges) { + mesh.AddBdrSegment(boundary.firstVertexId, boundary.secondVertexId, + boundary.attribute); + } - CHECK(validation.relationResults.size() == SchemaT::relationCount); + mesh.FinalizeTopology(false); + mesh.Finalize(false, false); - for (const auto &relationResult : validation.relationResults) { - INFO("Relation index = " << relationResult.relationIndex); + REQUIRE(mesh.GetNBE() == static_cast(boundaryEdges.size())); - INFO("Relation name = " << relationResult.relationName); + return mesh; +} - INFO("Failure enum = " << static_cast(relationResult.result.failure)); +[[nodiscard]] +std::vector make_layered_attributes() { + constexpr int xElementCount = 5; + constexpr int yElementCount = 5; - CHECK(relationResult.valid()); - } + std::vector attributes(xElementCount * yElementCount, 3); - CHECK(validation.valid()); + for (int y = 1; y <= 3; ++y) { + for (int x = 1; x <= 3; ++x) { + attributes[static_cast(cell_index(xElementCount, x, y))] = 2; } + } - using AlternateIdSchema = mean_field::utils::domain::DomainSchema< - mean_field::utils::domain::MaterialList< - mean_field::utils::domain::Material, - mean_field::utils::domain::Material, - mean_field::utils::domain::Material>, - mean_field::utils::domain::BoundaryList< - mean_field::utils::domain::BoundaryAttribute, - mean_field::utils::domain::BoundaryAttribute>, - mean_field::utils::domain::RelationList< - mean_field::utils::domain::Connected, - mean_field::utils::domain::Connected, - mean_field::utils::domain::Connected, - mean_field::utils::domain::Inscribed, - mean_field::utils::domain::Inscribed, - mean_field::utils::domain::DomainBoundary< - mean_field::utils::domain::StellarSurface, - mean_field::utils::domain::Stellar, - mean_field::utils::domain::Vacuum>, - mean_field::utils::domain:: - DomainBoundary>>; + attributes[static_cast(cell_index(xElementCount, 2, 2))] = 1; - [[nodiscard]] - stroid::config::MeshConfig make_stroid_config( - const int refinementLevels, - const int order, - const double flattening - ) { - stroid::config::MeshConfig config; + return attributes; +} - config.refinement_levels = refinementLevels; +[[nodiscard]] +mfem::Mesh make_layered_mesh(const bool includeStellarSurface = true, + const bool includeInfinitySurface = true, + const int stellarSurfaceAttribute = 1, + const int infinitySurfaceAttribute = 2) { + constexpr int xElementCount = 5; + constexpr int yElementCount = 5; - config.order = order; + const std::vector attributes = make_layered_attributes(); - config.include_external_domain = true; + std::vector boundaries; - config.r_core = 0.25; + const auto isStellar = [](const int materialId) { + return materialId == 1 || materialId == 2; + }; - config.r_star = 1.0; + const auto isVacuum = [](const int materialId) { return materialId == 3; }; - config.r_infinity = 4.0; + if (includeStellarSurface) { + append_interface_boundaries(boundaries, attributes, xElementCount, + yElementCount, isStellar, isVacuum, + stellarSurfaceAttribute); + } - config.flattening = flattening; + if (includeInfinitySurface) { + append_exterior_boundaries(boundaries, attributes, xElementCount, + yElementCount, isVacuum, + infinitySurfaceAttribute); + } - config.core_id = 1; + return make_grid_mesh(xElementCount, yElementCount, attributes, boundaries); +} - config.envelope_id = 2; +template void check_schema_is_valid(const mfem::Mesh &mesh) { + const auto validation = + mean_field::utils::domain::validate_schema(mesh); - config.vacuum_id = 3; + CHECK(validation.relationResults.size() == SchemaT::relationCount); - config.surface_bdr_id = 1; + for (const auto &relationResult : validation.relationResults) { + INFO("Relation index = " << relationResult.relationIndex); - config.inf_bdr_id = 2; + INFO("Relation name = " << relationResult.relationName); - config.optimization_methods = stroid::config::OptimizationMethods{.tmop = false, .smoothstep = true}; + INFO("Failure enum = " << static_cast(relationResult.result.failure)); - return config; - } + CHECK(relationResult.valid()); + } + + CHECK(validation.valid()); +} + +using AlternateIdSchema = mean_field::utils::domain::DomainSchema< + mean_field::utils::domain::MaterialList< + mean_field::utils::domain::Material, + mean_field::utils::domain::Material, + mean_field::utils::domain::Material>, + mean_field::utils::domain::BoundaryList< + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::StellarSurface, 101>, + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::InfinitySurface, 203>>, + mean_field::utils::domain::RelationList< + mean_field::utils::domain::Connected, + mean_field::utils::domain::Connected< + mean_field::utils::domain::Envelope>, + mean_field::utils::domain::Connected, + mean_field::utils::domain::Inscribed< + mean_field::utils::domain::Core, + mean_field::utils::domain::Envelope>, + mean_field::utils::domain::Inscribed, + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>, + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::InfinitySurface, + mean_field::utils::domain::Vacuum>>>; + +[[nodiscard]] +stroid::config::MeshConfig make_stroid_config(const int refinementLevels, + const int order, + const double flattening) { + stroid::config::MeshConfig config; + + config.refinement_levels = refinementLevels; + + config.order = order; + + config.include_external_domain = true; + + config.r_core = 0.25; + + config.r_star = 1.0; + + config.r_infinity = 4.0; + + config.flattening = flattening; + + config.core_id = 1; + + config.envelope_id = 2; + + config.vacuum_id = 3; + + config.surface_bdr_id = 1; + + config.inf_bdr_id = 2; + + config.optimization_methods = + stroid::config::OptimizationMethods{.tmop = false, .smoothstep = true}; + + return config; +} } // namespace domain_test_utils TEST_CASE( "Domain Types And Composite Domains Preserve Their Semantic Categories", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - STATIC_REQUIRE(mean_field::utils::domain::IsDomain); + tags::unit &tags::mesh &tags::utils &tags::domain) { + STATIC_REQUIRE( + mean_field::utils::domain::IsDomain); - STATIC_REQUIRE(mean_field::utils::domain::IsDomain); + STATIC_REQUIRE( + mean_field::utils::domain::IsDomain); - STATIC_REQUIRE(mean_field::utils::domain::IsDomain); + STATIC_REQUIRE( + mean_field::utils::domain::IsDomain); - STATIC_REQUIRE(mean_field::utils::domain::IsDomainSet); + STATIC_REQUIRE(mean_field::utils::domain::IsDomainSet< + mean_field::utils::domain::Stellar>); - STATIC_REQUIRE(mean_field::utils::domain::IsDomainSet); + STATIC_REQUIRE( + mean_field::utils::domain::IsDomainSet); - STATIC_REQUIRE_FALSE(mean_field::utils::domain::IsDomain); + STATIC_REQUIRE_FALSE( + mean_field::utils::domain::IsDomain); - STATIC_REQUIRE(mean_field::utils::domain::IsDomainOrSet); + STATIC_REQUIRE(mean_field::utils::domain::IsDomainOrSet< + mean_field::utils::domain::Stellar>); - STATIC_REQUIRE(mean_field::utils::domain::IsBoundary); + STATIC_REQUIRE(mean_field::utils::domain::IsBoundary< + mean_field::utils::domain::StellarSurface>); - STATIC_REQUIRE(mean_field::utils::domain::IsBoundary); + STATIC_REQUIRE(mean_field::utils::domain::IsBoundary< + mean_field::utils::domain::InfinitySurface>); - CHECK(true); + CHECK(true); } -TEST_CASE( - "Material Lists Reject Duplicate Ids And Duplicate Semantic Domains", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - STATIC_REQUIRE( - domain_test_utils::CanFormMaterialList< - mean_field::utils::domain::Material, - mean_field::utils::domain::Material> - ); +TEST_CASE("Material Lists Reject Duplicate Ids And Duplicate Semantic Domains", + tags::unit &tags::mesh &tags::utils &tags::domain) { + STATIC_REQUIRE( + domain_test_utils::CanFormMaterialList< + mean_field::utils::domain::Material, + mean_field::utils::domain::Material< + mean_field::utils::domain::Envelope, 2>>); - STATIC_REQUIRE_FALSE( - domain_test_utils::CanFormMaterialList< - mean_field::utils::domain::Material, - mean_field::utils::domain::Material> - ); + STATIC_REQUIRE_FALSE( + domain_test_utils::CanFormMaterialList< + mean_field::utils::domain::Material, + mean_field::utils::domain::Material< + mean_field::utils::domain::Envelope, 1>>); - STATIC_REQUIRE_FALSE( - domain_test_utils::CanFormMaterialList< - mean_field::utils::domain::Material, - mean_field::utils::domain::Material> - ); + STATIC_REQUIRE_FALSE( + domain_test_utils::CanFormMaterialList< + mean_field::utils::domain::Material, + mean_field::utils::domain::Material>); - /* - * The schema intentionally imposes no convention on the - * numerical range or indexing scheme used by a mesh producer. - */ - STATIC_REQUIRE( - domain_test_utils::CanFormMaterialList< - mean_field::utils::domain::Material, - mean_field::utils::domain::Material, - mean_field::utils::domain::Material> - ); + /* + * The schema intentionally imposes no convention on the + * numerical range or indexing scheme used by a mesh producer. + */ + STATIC_REQUIRE( + domain_test_utils::CanFormMaterialList< + mean_field::utils::domain::Material, + mean_field::utils::domain::Material< + mean_field::utils::domain::Envelope, -7>, + mean_field::utils::domain::Material>); - CHECK(true); + CHECK(true); } TEST_CASE( "Boundary Lists Reject Duplicate Ids And Duplicate Semantic Boundaries", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - STATIC_REQUIRE( - domain_test_utils::CanFormBoundaryList< - mean_field::utils::domain::BoundaryAttribute, - mean_field::utils::domain::BoundaryAttribute> - ); + tags::unit &tags::mesh &tags::utils &tags::domain) { + STATIC_REQUIRE(domain_test_utils::CanFormBoundaryList< + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::StellarSurface, 1>, + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::InfinitySurface, 2>>); - STATIC_REQUIRE_FALSE( - domain_test_utils::CanFormBoundaryList< - mean_field::utils::domain::BoundaryAttribute, - mean_field::utils::domain::BoundaryAttribute> - ); + STATIC_REQUIRE_FALSE(domain_test_utils::CanFormBoundaryList< + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::StellarSurface, 1>, + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::InfinitySurface, 1>>); - STATIC_REQUIRE_FALSE( - domain_test_utils::CanFormBoundaryList< - mean_field::utils::domain::BoundaryAttribute, - mean_field::utils::domain::BoundaryAttribute> - ); + STATIC_REQUIRE_FALSE(domain_test_utils::CanFormBoundaryList< + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::StellarSurface, 1>, + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::StellarSurface, 2>>); - STATIC_REQUIRE( - domain_test_utils::CanFormBoundaryList< - mean_field::utils::domain::BoundaryAttribute, - mean_field::utils::domain::BoundaryAttribute> - ); + STATIC_REQUIRE(domain_test_utils::CanFormBoundaryList< + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::StellarSurface, 0>, + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::InfinitySurface, -13>>); - CHECK(true); + CHECK(true); } -TEST_CASE( - "Domain Boundary Relations Accept Exactly One Or Two Domains", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - STATIC_REQUIRE( - domain_test_utils::CanFormDomainBoundary< - mean_field::utils::domain::InfinitySurface, mean_field::utils::domain::Vacuum> - ); +TEST_CASE("Domain Boundary Relations Accept Exactly One Or Two Domains", + tags::unit &tags::mesh &tags::utils &tags::domain) { + STATIC_REQUIRE(domain_test_utils::CanFormDomainBoundary< + mean_field::utils::domain::InfinitySurface, + mean_field::utils::domain::Vacuum>); - STATIC_REQUIRE( - domain_test_utils::CanFormDomainBoundary< - mean_field::utils::domain::StellarSurface, mean_field::utils::domain::Stellar, - mean_field::utils::domain::Vacuum> - ); + STATIC_REQUIRE(domain_test_utils::CanFormDomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>); - STATIC_REQUIRE_FALSE(domain_test_utils::CanFormDomainBoundary); + STATIC_REQUIRE_FALSE(domain_test_utils::CanFormDomainBoundary< + mean_field::utils::domain::StellarSurface>); - STATIC_REQUIRE_FALSE( - domain_test_utils::CanFormDomainBoundary< - mean_field::utils::domain::StellarSurface, mean_field::utils::domain::Core, - mean_field::utils::domain::Envelope, mean_field::utils::domain::Vacuum> - ); + STATIC_REQUIRE_FALSE( + domain_test_utils::CanFormDomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Core, mean_field::utils::domain::Envelope, + mean_field::utils::domain::Vacuum>); - CHECK(true); + CHECK(true); } TEST_CASE( "Domain Schemas Reject Relations That Reference Unregistered Entities", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - using IncompleteMaterials = mean_field::utils::domain::MaterialList< - mean_field::utils::domain::Material, - mean_field::utils::domain::Material>; + tags::unit &tags::mesh &tags::utils &tags::domain) { + using IncompleteMaterials = mean_field::utils::domain::MaterialList< + mean_field::utils::domain::Material, + mean_field::utils::domain::Material>; - using CompleteMaterials = mean_field::utils::domain::MaterialList< - mean_field::utils::domain::Material, - mean_field::utils::domain::Material, - mean_field::utils::domain::Material>; + using CompleteMaterials = mean_field::utils::domain::MaterialList< + mean_field::utils::domain::Material, + mean_field::utils::domain::Material, + mean_field::utils::domain::Material>; - using CompleteBoundaries = mean_field::utils::domain::BoundaryList< - mean_field::utils::domain::BoundaryAttribute, - mean_field::utils::domain::BoundaryAttribute>; + using CompleteBoundaries = mean_field::utils::domain::BoundaryList< + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::StellarSurface, 1>, + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::InfinitySurface, 2>>; - using InfinityOnlyBoundary = mean_field::utils::domain::BoundaryList< - mean_field::utils::domain::BoundaryAttribute>; + using InfinityOnlyBoundary = mean_field::utils::domain::BoundaryList< + mean_field::utils::domain::BoundaryAttribute< + mean_field::utils::domain::InfinitySurface, 2>>; - using MissingEnvelopeRelation = mean_field::utils::domain::RelationList< - mean_field::utils::domain::Connected>; + using MissingEnvelopeRelation = mean_field::utils::domain::RelationList< + mean_field::utils::domain::Connected< + mean_field::utils::domain::Envelope>>; - using MissingBoundaryRelation = mean_field::utils::domain::RelationList>; + using MissingBoundaryRelation = mean_field::utils::domain::RelationList< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>>; - STATIC_REQUIRE_FALSE( - domain_test_utils::CanFormSchema - ); + STATIC_REQUIRE_FALSE( + domain_test_utils::CanFormSchema); - STATIC_REQUIRE_FALSE( - domain_test_utils::CanFormSchema - ); + STATIC_REQUIRE_FALSE( + domain_test_utils::CanFormSchema); - CHECK(true); + CHECK(true); } -TEST_CASE( - "Core Envelope Vacuum Schema Exposes Exact Compile Time And Runtime Metadata", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - using SchemaT = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +TEST_CASE("Core Envelope Vacuum Schema Exposes Exact Compile Time And Runtime " + "Metadata", + tags::unit &tags::mesh &tags::utils &tags::domain) { + using SchemaT = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - STATIC_REQUIRE(mean_field::utils::domain::IsSchema); + STATIC_REQUIRE(mean_field::utils::domain::IsSchema); - STATIC_REQUIRE(SchemaT::materialCount == 3); + STATIC_REQUIRE(SchemaT::materialCount == 3); - STATIC_REQUIRE(SchemaT::boundaryCount == 2); + STATIC_REQUIRE(SchemaT::boundaryCount == 2); - STATIC_REQUIRE(SchemaT::relationCount == 7); + STATIC_REQUIRE(SchemaT::relationCount == 7); - constexpr auto materials = SchemaT::materials(); + constexpr auto materials = SchemaT::materials(); - constexpr auto boundaries = SchemaT::boundaries(); + constexpr auto boundaries = SchemaT::boundaries(); - STATIC_REQUIRE(materials[0].name == std::string_view{"core"}); + STATIC_REQUIRE(materials[0].name == std::string_view{"core"}); - STATIC_REQUIRE(materials[0].id == 1); + STATIC_REQUIRE(materials[0].id == 1); - STATIC_REQUIRE(materials[1].name == std::string_view{"envelope"}); + STATIC_REQUIRE(materials[1].name == std::string_view{"envelope"}); - STATIC_REQUIRE(materials[1].id == 2); + STATIC_REQUIRE(materials[1].id == 2); - STATIC_REQUIRE(materials[2].name == std::string_view{"vacuum"}); + STATIC_REQUIRE(materials[2].name == std::string_view{"vacuum"}); - STATIC_REQUIRE(materials[2].id == 3); + STATIC_REQUIRE(materials[2].id == 3); - STATIC_REQUIRE(boundaries[0].name == std::string_view{"stellar_surface"}); + STATIC_REQUIRE(boundaries[0].name == std::string_view{"stellar_surface"}); - STATIC_REQUIRE(boundaries[0].id == 1); + STATIC_REQUIRE(boundaries[0].id == 1); - STATIC_REQUIRE(boundaries[1].name == std::string_view{"infinity_surface"}); + STATIC_REQUIRE(boundaries[1].name == std::string_view{"infinity_surface"}); - STATIC_REQUIRE(boundaries[1].id == 2); + STATIC_REQUIRE(boundaries[1].id == 2); - STATIC_REQUIRE(SchemaT::template contains_domain()); + STATIC_REQUIRE( + SchemaT::template contains_domain()); - STATIC_REQUIRE(SchemaT::template contains_domain()); + STATIC_REQUIRE( + SchemaT::template contains_domain()); - STATIC_REQUIRE(SchemaT::template contains_domain()); + STATIC_REQUIRE( + SchemaT::template contains_domain()); - STATIC_REQUIRE(SchemaT::template attribute_belongs_to(1)); + STATIC_REQUIRE(SchemaT::template attribute_belongs_to< + mean_field::utils::domain::Stellar>(1)); - STATIC_REQUIRE(SchemaT::template attribute_belongs_to(2)); + STATIC_REQUIRE(SchemaT::template attribute_belongs_to< + mean_field::utils::domain::Stellar>(2)); - STATIC_REQUIRE_FALSE(SchemaT::template attribute_belongs_to(3)); + STATIC_REQUIRE_FALSE(SchemaT::template attribute_belongs_to< + mean_field::utils::domain::Stellar>(3)); - STATIC_REQUIRE(SchemaT::template attribute_belongs_to(1)); + STATIC_REQUIRE( + SchemaT::template attribute_belongs_to( + 1)); - STATIC_REQUIRE(SchemaT::template attribute_belongs_to(2)); + STATIC_REQUIRE( + SchemaT::template attribute_belongs_to( + 2)); - STATIC_REQUIRE(SchemaT::template attribute_belongs_to(3)); + STATIC_REQUIRE( + SchemaT::template attribute_belongs_to( + 3)); - STATIC_REQUIRE(SchemaT::template contains_boundary()); + STATIC_REQUIRE( + SchemaT::template material_attribute() == + 1); - STATIC_REQUIRE(SchemaT::template contains_boundary()); + STATIC_REQUIRE(SchemaT::template material_attribute< + mean_field::utils::domain::Envelope>() == 2); - STATIC_REQUIRE(SchemaT::template boundary_attribute() == 1); + STATIC_REQUIRE(SchemaT::template material_attribute< + mean_field::utils::domain::Vacuum>() == 3); - STATIC_REQUIRE(SchemaT::template boundary_attribute() == 2); + STATIC_REQUIRE(SchemaT::template contains_boundary< + mean_field::utils::domain::StellarSurface>()); - CHECK(true); + STATIC_REQUIRE(SchemaT::template contains_boundary< + mean_field::utils::domain::InfinitySurface>()); + + STATIC_REQUIRE(SchemaT::template boundary_attribute< + mean_field::utils::domain::StellarSurface>() == 1); + + STATIC_REQUIRE(SchemaT::template boundary_attribute< + mean_field::utils::domain::InfinitySurface>() == 2); + + CHECK(true); } -TEST_CASE( - "Connected Accepts Face Connected Atomic And Composite Domains", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); +TEST_CASE("Domain Schema Builds Exact MFEM Attribute Markers", + tags::domain &tags::utils &tags::unit) { + using namespace mean_field::utils::domain; + using Schema = CoreEnvelopeVacuumDomainSchema; - const auto coreResult = mean_field::utils::domain:: - RelationValidator>::template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); - REQUIRE(coreResult); - REQUIRE(coreResult.connectedDiagnostics.has_value()); + const mfem::Array stellarMarker = + make_attribute_marker(mesh); + const mfem::Array vacuumMarker = + make_attribute_marker(mesh); + const mfem::Array allMarker = make_attribute_marker(mesh); - CHECK(coreResult.connectedDiagnostics->domainElementCount == 1); + REQUIRE(stellarMarker.Size() == 3); + REQUIRE(vacuumMarker.Size() == 3); + REQUIRE(allMarker.Size() == 3); - CHECK(coreResult.connectedDiagnostics->visitedElementCount == 1); + CHECK(stellarMarker[0] == 1); + CHECK(stellarMarker[1] == 1); + CHECK(stellarMarker[2] == 0); - const auto stellarResult = mean_field::utils::domain:: - RelationValidator>::template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + CHECK(vacuumMarker[0] == 0); + CHECK(vacuumMarker[1] == 0); + CHECK(vacuumMarker[2] == 1); - REQUIRE(stellarResult); - REQUIRE(stellarResult.connectedDiagnostics.has_value()); - - CHECK(stellarResult.connectedDiagnostics->domainElementCount == 9); - - CHECK(stellarResult.connectedDiagnostics->visitedElementCount == 9); + CHECK(allMarker[0] == 1); + CHECK(allMarker[1] == 1); + CHECK(allMarker[2] == 1); } -TEST_CASE( - "Connected Rejects An Absent Domain", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 2}, {}); +TEST_CASE("Connected Accepts Face Connected Atomic And Composite Domains", + tags::unit &tags::mesh &tags::utils &tags::domain) { + mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); - const auto result = mean_field::utils::domain:: - RelationValidator>::template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto coreResult = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Connected>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + REQUIRE(coreResult); + REQUIRE(coreResult.connectedDiagnostics.has_value()); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainAbsent); + CHECK(coreResult.connectedDiagnostics->domainElementCount == 1); - REQUIRE(result.connectedDiagnostics.has_value()); + CHECK(coreResult.connectedDiagnostics->visitedElementCount == 1); - CHECK(result.connectedDiagnostics->domainElementCount == 0); + const auto stellarResult = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Connected< + mean_field::utils::domain::Stellar>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK(result.connectedDiagnostics->visitedElementCount == 0); + REQUIRE(stellarResult); + REQUIRE(stellarResult.connectedDiagnostics.has_value()); + + CHECK(stellarResult.connectedDiagnostics->domainElementCount == 9); + + CHECK(stellarResult.connectedDiagnostics->visitedElementCount == 9); } -TEST_CASE( - "Connected Rejects Multiple Face Disconnected Components", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 1, {1, 2, 1}, {}); +TEST_CASE("Connected Rejects An Absent Domain", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 2}, {}); - const auto result = mean_field::utils::domain:: - RelationValidator>::template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Connected>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainDisconnected); + CHECK(result.failure == + mean_field::utils::domain::RelationValidationFailure::DomainAbsent); - REQUIRE(result.connectedDiagnostics.has_value()); + REQUIRE(result.connectedDiagnostics.has_value()); - CHECK(result.connectedDiagnostics->domainElementCount == 2); + CHECK(result.connectedDiagnostics->domainElementCount == 0); - CHECK(result.connectedDiagnostics->visitedElementCount == 1); - - CHECK(result.connectedDiagnostics->elementId >= 0); + CHECK(result.connectedDiagnostics->visitedElementCount == 0); } -TEST_CASE( - "Inscribed Accepts Nested Atomic And Composite Domains", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); +TEST_CASE("Connected Rejects Multiple Face Disconnected Components", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = + domain_test_utils::make_grid_mesh(3, 1, {1, 2, 1}, {}); - const auto coreResult = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Inscribed>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Connected>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK(coreResult); + CHECK_FALSE(result); - const auto stellarResult = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Inscribed>:: - template validate(mesh); + CHECK( + result.failure == + mean_field::utils::domain::RelationValidationFailure::DomainDisconnected); - CHECK(stellarResult); + REQUIRE(result.connectedDiagnostics.has_value()); + + CHECK(result.connectedDiagnostics->domainElementCount == 2); + + CHECK(result.connectedDiagnostics->visitedElementCount == 1); + + CHECK(result.connectedDiagnostics->elementId >= 0); } -TEST_CASE( - "Inscribed Rejects An Absent Inner Domain", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 2, {2, 2, 2, 2}, {}); +TEST_CASE("Inscribed Accepts Nested Atomic And Composite Domains", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); - const auto result = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Inscribed>:: - template validate(mesh); + const auto coreResult = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed< + mean_field::utils::domain::Core, + mean_field::utils::domain::Envelope>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK(coreResult); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::InnerDomainAbsent); + const auto stellarResult = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + + CHECK(stellarResult); } -TEST_CASE( - "Inscribed Rejects An Absent Outer Domain", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, {1}, {}); +TEST_CASE("Inscribed Rejects An Absent Inner Domain", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = + domain_test_utils::make_grid_mesh(2, 2, {2, 2, 2, 2}, {}); - const auto result = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Inscribed>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed< + mean_field::utils::domain::Core, + mean_field::utils::domain::Envelope>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent); + CHECK( + result.failure == + mean_field::utils::domain::RelationValidationFailure::InnerDomainAbsent); +} + +TEST_CASE("Inscribed Rejects An Absent Outer Domain", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, {1}, {}); + + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed< + mean_field::utils::domain::Core, + mean_field::utils::domain::Envelope>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + + CHECK_FALSE(result); + + CHECK( + result.failure == + mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent); } TEST_CASE( "Inscribed Rejects An Inner Domain Touching The Computational Boundary", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 2, {1, 2, 2, 2}, {}); + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = + domain_test_utils::make_grid_mesh(2, 2, {1, 2, 2, 2}, {}); - const auto result = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Inscribed>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed< + mean_field::utils::domain::Core, + mean_field::utils::domain::Envelope>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::InnerDomainTouchesMeshBoundary); + CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure:: + InnerDomainTouchesMeshBoundary); - REQUIRE(result.inscribedDiagnostics.has_value()); + REQUIRE(result.inscribedDiagnostics.has_value()); - CHECK(result.inscribedDiagnostics->faceId >= 0); + CHECK(result.inscribedDiagnostics->faceId >= 0); - CHECK(result.inscribedDiagnostics->innerElementId >= 0); + CHECK(result.inscribedDiagnostics->innerElementId >= 0); - CHECK(result.inscribedDiagnostics->adjacentElementId == -1); + CHECK(result.inscribedDiagnostics->adjacentElementId == -1); } -TEST_CASE( - "Inscribed Rejects An Inner Domain Touching An Unexpected Material", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - std::vector attributes{2, 2, 2, 2, 1, 3, 2, 2, 2}; +TEST_CASE("Inscribed Rejects An Inner Domain Touching An Unexpected Material", + tags::unit &tags::mesh &tags::utils &tags::domain) { + std::vector attributes{2, 2, 2, 2, 1, 3, 2, 2, 2}; - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 3, attributes, {}); + const mfem::Mesh mesh = + domain_test_utils::make_grid_mesh(3, 3, attributes, {}); - const auto result = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Inscribed>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed< + mean_field::utils::domain::Core, + mean_field::utils::domain::Envelope>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::InnerDomainTouchesUnexpectedMaterial); + CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure:: + InnerDomainTouchesUnexpectedMaterial); - REQUIRE(result.inscribedDiagnostics.has_value()); + REQUIRE(result.inscribedDiagnostics.has_value()); - CHECK(result.inscribedDiagnostics->adjacentMaterialId == 3); + CHECK(result.inscribedDiagnostics->adjacentMaterialId == 3); } TEST_CASE( "Domain Boundary Accepts A Complete Internal Stellar Vacuum Interface", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - std::vector boundaries{{.firstVertexId = 1, .secondVertexId = 4, .attribute = 1}}; + tags::unit &tags::mesh &tags::utils &tags::domain) { + std::vector boundaries{ + {.firstVertexId = 1, .secondVertexId = 4, .attribute = 1}}; - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 3}, boundaries); + const mfem::Mesh mesh = + domain_test_utils::make_grid_mesh(2, 1, {2, 3}, boundaries); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK(result); + CHECK(result); - /* - * Interface ordering is intentionally semantic rather - * than oriented. - */ - const auto reversedResult = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + /* + * Interface ordering is intentionally semantic rather + * than oriented. + */ + const auto reversedResult = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Vacuum, + mean_field::utils::domain::Stellar>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK(reversedResult); + CHECK(reversedResult); } -TEST_CASE( - "Domain Boundary Accepts A Complete Exterior Vacuum Boundary", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const std::vector attributes{3}; +TEST_CASE("Domain Boundary Accepts A Complete Exterior Vacuum Boundary", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const std::vector attributes{3}; - std::vector boundaries; + std::vector boundaries; - domain_test_utils::append_exterior_boundaries( - boundaries, attributes, 1, 1, [](const int materialId) { return materialId == 3; }, 2 - ); + domain_test_utils::append_exterior_boundaries( + boundaries, attributes, 1, 1, + [](const int materialId) { return materialId == 3; }, 2); - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(1, 1, attributes, boundaries); + const mfem::Mesh mesh = + domain_test_utils::make_grid_mesh(1, 1, attributes, boundaries); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::InfinitySurface, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK(result); + CHECK(result); } TEST_CASE( "Domain Boundary Rejects A Tagged Internal Face For An Exterior Boundary", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = - domain_test_utils::make_grid_mesh(2, 1, {3, 3}, {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 2}}); + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh( + 2, 1, {3, 3}, + {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 2}}); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::InfinitySurface, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK( - result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryTaggedFaceHasWrongTopology - ); + CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure:: + DomainBoundaryTaggedFaceHasWrongTopology); } TEST_CASE( "Domain Boundary Rejects A Tagged Exterior Face Of The Wrong Material", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = - domain_test_utils::make_grid_mesh(1, 1, {2}, {{.firstVertexId = 0, .secondVertexId = 1, .attribute = 2}}); + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh( + 1, 1, {2}, {{.firstVertexId = 0, .secondVertexId = 1, .attribute = 2}}); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::InfinitySurface, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK( - result.failure == - mean_field::utils::domain::RelationValidationFailure::DomainBoundaryTaggedFaceTouchesUnexpectedMaterial - ); + CHECK(result.failure == + mean_field::utils::domain::RelationValidationFailure:: + DomainBoundaryTaggedFaceTouchesUnexpectedMaterial); } -TEST_CASE( - "Domain Boundary Rejects A Tagged Internal Interface With Unexpected Materials", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = - domain_test_utils::make_grid_mesh(2, 1, {1, 2}, {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 1}}); +TEST_CASE("Domain Boundary Rejects A Tagged Internal Interface With Unexpected " + "Materials", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh( + 2, 1, {1, 2}, + {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 1}}); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK( - result.failure == - mean_field::utils::domain::RelationValidationFailure::DomainBoundaryTaggedFaceTouchesUnexpectedMaterial - ); + CHECK(result.failure == + mean_field::utils::domain::RelationValidationFailure:: + DomainBoundaryTaggedFaceTouchesUnexpectedMaterial); } -TEST_CASE( - "Domain Boundary Rejects An Untagged Expected Interface", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 3}, {}); +TEST_CASE("Domain Boundary Rejects An Untagged Expected Interface", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 3}, {}); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged); + CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure:: + DomainBoundaryExpectedFaceIsUntagged); - REQUIRE(result.domainBoundaryDiagnostics.has_value()); + REQUIRE(result.domainBoundaryDiagnostics.has_value()); - CHECK(result.domainBoundaryDiagnostics->faceId >= 0); + CHECK(result.domainBoundaryDiagnostics->faceId >= 0); - CHECK(result.domainBoundaryDiagnostics->boundaryElementId == -1); + CHECK(result.domainBoundaryDiagnostics->boundaryElementId == -1); - CHECK_FALSE(result.domainBoundaryDiagnostics->actualBoundaryAttribute.has_value()); + CHECK_FALSE( + result.domainBoundaryDiagnostics->actualBoundaryAttribute.has_value()); } TEST_CASE( "Domain Boundary Rejects An Expected Interface With The Wrong Attribute", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = - domain_test_utils::make_grid_mesh(2, 1, {2, 3}, {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 9}}); + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh( + 2, 1, {2, 3}, + {{.firstVertexId = 1, .secondVertexId = 4, .attribute = 9}}); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK( - result.failure == - mean_field::utils::domain::RelationValidationFailure::DomainBoundaryExpectedFaceHasWrongAttribute - ); + CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure:: + DomainBoundaryExpectedFaceHasWrongAttribute); - REQUIRE(result.domainBoundaryDiagnostics.has_value()); + REQUIRE(result.domainBoundaryDiagnostics.has_value()); - REQUIRE(result.domainBoundaryDiagnostics->actualBoundaryAttribute.has_value()); + REQUIRE( + result.domainBoundaryDiagnostics->actualBoundaryAttribute.has_value()); - CHECK(*result.domainBoundaryDiagnostics->actualBoundaryAttribute == 9); + CHECK(*result.domainBoundaryDiagnostics->actualBoundaryAttribute == 9); - CHECK(result.domainBoundaryDiagnostics->expectedBoundaryAttribute == 1); + CHECK(result.domainBoundaryDiagnostics->expectedBoundaryAttribute == 1); } -TEST_CASE( - "Domain Boundary Rejects A Relation That Is Not Realized Anywhere", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 2}, {}); +TEST_CASE("Domain Boundary Rejects A Relation That Is Not Realized Anywhere", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(2, 1, {2, 2}, {}); - const auto result = mean_field::utils::domain::RelationValidator>:: - template validate(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::DomainBoundary< + mean_field::utils::domain::StellarSurface, + mean_field::utils::domain::Stellar, + mean_field::utils::domain::Vacuum>>:: + template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainBoundaryAbsent); + CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure:: + DomainBoundaryAbsent); } TEST_CASE( "Complete Schema Validation Accepts A Synthetic Core Envelope Vacuum Mesh", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); - const auto validation = - mean_field::utils::domain::validate_schema(mesh); + const auto validation = mean_field::utils::domain::validate_schema< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - REQUIRE(validation.valid()); + REQUIRE(validation.valid()); - REQUIRE(validation.relationResults.size() == 7); + REQUIRE(validation.relationResults.size() == 7); - CHECK(validation.failed_relation_count() == 0); + CHECK(validation.failed_relation_count() == 0); - CHECK(validation.passed_relation_count() == 7); + CHECK(validation.passed_relation_count() == 7); - CHECK_FALSE(validation.first_failed_relation_index().has_value()); + CHECK_FALSE(validation.first_failed_relation_index().has_value()); - const std::array expectedRelationNames{"connected", "connected", "connected", - "inscribed", "inscribed", "domain_boundary", - "domain_boundary"}; + const std::array expectedRelationNames{ + "connected", "connected", "connected", "inscribed", + "inscribed", "domain_boundary", "domain_boundary"}; - for (std::size_t relationIndex = 0; relationIndex < expectedRelationNames.size(); ++relationIndex) { - CHECK(validation.relationResults[relationIndex].relationIndex == relationIndex); + for (std::size_t relationIndex = 0; + relationIndex < expectedRelationNames.size(); ++relationIndex) { + CHECK(validation.relationResults[relationIndex].relationIndex == + relationIndex); - CHECK(validation.relationResults[relationIndex].relationName == expectedRelationNames[relationIndex]); + CHECK(validation.relationResults[relationIndex].relationName == + expectedRelationNames[relationIndex]); - CHECK(validation.relationResults[relationIndex].valid()); - } + CHECK(validation.relationResults[relationIndex].valid()); + } } -TEST_CASE( - "Complete Schema Validation Evaluates Every Relation After A Failure", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - /* - * All material topology and the outer vacuum boundary are valid. - * Only the Stellar/Vacuum boundary tagging is intentionally absent. - */ - const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(false, true); +TEST_CASE("Complete Schema Validation Evaluates Every Relation After A Failure", + tags::unit &tags::mesh &tags::utils &tags::domain) { + /* + * All material topology and the outer vacuum boundary are valid. + * Only the Stellar/Vacuum boundary tagging is intentionally absent. + */ + const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(false, true); - const auto validation = - mean_field::utils::domain::validate_schema(mesh); + const auto validation = mean_field::utils::domain::validate_schema< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(validation.valid()); + CHECK_FALSE(validation.valid()); - REQUIRE(validation.relationResults.size() == 7); + REQUIRE(validation.relationResults.size() == 7); - CHECK(validation.failed_relation_count() == 1); + CHECK(validation.failed_relation_count() == 1); - CHECK(validation.passed_relation_count() == 6); + CHECK(validation.passed_relation_count() == 6); - REQUIRE(validation.first_failed_relation_index().has_value()); + REQUIRE(validation.first_failed_relation_index().has_value()); - CHECK(*validation.first_failed_relation_index() == 5); + CHECK(*validation.first_failed_relation_index() == 5); - for (std::size_t relationIndex = 0; relationIndex < 7; ++relationIndex) { - CAPTURE(relationIndex); + for (std::size_t relationIndex = 0; relationIndex < 7; ++relationIndex) { + CAPTURE(relationIndex); - if (relationIndex == 5) { - CHECK_FALSE(validation.relationResults[relationIndex].valid()); + if (relationIndex == 5) { + CHECK_FALSE(validation.relationResults[relationIndex].valid()); - CHECK( - validation.relationResults[relationIndex].result.failure == - mean_field::utils::domain::RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged - ); + CHECK(validation.relationResults[relationIndex].result.failure == + mean_field::utils::domain::RelationValidationFailure:: + DomainBoundaryExpectedFaceIsUntagged); - continue; - } - - CHECK(validation.relationResults[relationIndex].valid()); + continue; } + + CHECK(validation.relationResults[relationIndex].valid()); + } } -TEST_CASE( - "STROID Meshes Satisfy The Core Envelope Vacuum Domain Schema", - tags::integration &tags::mesh &tags::utils &tags::domain -) { - const std::array testCases{ - domain_test_utils::StroidCase{ - .name = "spherical_low_order", .refinementLevels = 0, .order = 1, .flattening = 0.0 - }, - domain_test_utils::StroidCase{.name = "oblate", .refinementLevels = 0, .order = 2, .flattening = 0.15}, - domain_test_utils::StroidCase{.name = "refined_oblate", .refinementLevels = 1, .order = 2, .flattening = 0.10} - }; +TEST_CASE("STROID Meshes Satisfy The Core Envelope Vacuum Domain Schema", + tags::integration &tags::mesh &tags::utils &tags::domain) { + const std::array testCases{ + domain_test_utils::StroidCase{.name = "spherical_low_order", + .refinementLevels = 0, + .order = 1, + .flattening = 0.0}, + domain_test_utils::StroidCase{.name = "oblate", + .refinementLevels = 0, + .order = 2, + .flattening = 0.15}, + domain_test_utils::StroidCase{.name = "refined_oblate", + .refinementLevels = 1, + .order = 2, + .flattening = 0.10}}; - for (const domain_test_utils::StroidCase &testCase : testCases) { - INFO("STROID case = " << testCase.name); + for (const domain_test_utils::StroidCase &testCase : testCases) { + INFO("STROID case = " << testCase.name); - INFO("Refinement levels = " << testCase.refinementLevels); + INFO("Refinement levels = " << testCase.refinementLevels); - INFO("Order = " << testCase.order); + INFO("Order = " << testCase.order); - INFO("Flattening = " << testCase.flattening); + INFO("Flattening = " << testCase.flattening); - const stroid::config::MeshConfig config = - domain_test_utils::make_stroid_config(testCase.refinementLevels, testCase.order, testCase.flattening); + const stroid::config::MeshConfig config = + domain_test_utils::make_stroid_config( + testCase.refinementLevels, testCase.order, testCase.flattening); - stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); - - REQUIRE(stroidMesh.reference_mesh != nullptr); - - REQUIRE(stroidMesh.mesh != nullptr); - - /* - * Validate both the reference topology and the projected - * physical mesh. The mapping/projection must not alter - * material or boundary semantics. - */ - domain_test_utils::check_schema_is_valid( - *stroidMesh.reference_mesh - ); - - domain_test_utils::check_schema_is_valid( - *stroidMesh.mesh - ); - } -} - -TEST_CASE( - "STROID Material And Boundary Id Conventions Are Fully Schema Driven", - tags::integration &tags::mesh &tags::utils &tags::domain -) { - stroid::config::MeshConfig config = domain_test_utils::make_stroid_config(0, 1, 0.0); - - config.core_id = 11; - - config.envelope_id = 17; - - config.vacuum_id = 29; - - config.surface_bdr_id = 101; - - config.inf_bdr_id = 203; - - stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); + stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); REQUIRE(stroidMesh.reference_mesh != nullptr); REQUIRE(stroidMesh.mesh != nullptr); /* - * The same semantic topology must validate when a mesh generator - * uses an entirely different attribute numbering convention. + * Validate both the reference topology and the projected + * physical mesh. The mapping/projection must not alter + * material or boundary semantics. */ - domain_test_utils::check_schema_is_valid(*stroidMesh.reference_mesh); + domain_test_utils::check_schema_is_valid< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>( + *stroidMesh.reference_mesh); - domain_test_utils::check_schema_is_valid(*stroidMesh.mesh); - - /* - * Conversely, the production 1/2/3 + 1/2 schema must not silently - * accept a mesh generated under another numbering convention. - */ - const auto productionValidation = - mean_field::utils::domain::validate_schema( - *stroidMesh.mesh - ); - - CHECK_FALSE(productionValidation.valid()); - - CHECK(productionValidation.failed_relation_count() > 0); + domain_test_utils::check_schema_is_valid< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>( + *stroidMesh.mesh); + } } -TEST_CASE( - "Complete Schema Validation Rejects A Mesh Without Vacuum", - tags::unit &tags::mesh &tags::utils &tags::domain -) { - const mfem::Mesh mesh = domain_test_utils::make_grid_mesh(3, 3, {2, 2, 2, 2, 1, 2, 2, 2, 2}, {}); +TEST_CASE("STROID Material And Boundary Id Conventions Are Fully Schema Driven", + tags::integration &tags::mesh &tags::utils &tags::domain) { + stroid::config::MeshConfig config = + domain_test_utils::make_stroid_config(0, 1, 0.0); - const auto validation = - mean_field::utils::domain::validate_schema(mesh); + config.core_id = 11; - CHECK_FALSE(validation.valid()); + config.envelope_id = 17; - REQUIRE(validation.relationResults.size() == 7); + config.vacuum_id = 29; - /* - * Connected - */ - CHECK_FALSE(validation.relationResults[2].valid()); + config.surface_bdr_id = 101; - CHECK( - validation.relationResults[2].result.failure == - mean_field::utils::domain::RelationValidationFailure::DomainAbsent - ); + config.inf_bdr_id = 203; - /* - * Inscribed - */ - CHECK_FALSE(validation.relationResults[4].valid()); + stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); - CHECK( - validation.relationResults[4].result.failure == - mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent - ); -} \ No newline at end of file + REQUIRE(stroidMesh.reference_mesh != nullptr); + + REQUIRE(stroidMesh.mesh != nullptr); + + /* + * The same semantic topology must validate when a mesh generator + * uses an entirely different attribute numbering convention. + */ + domain_test_utils::check_schema_is_valid< + domain_test_utils::AlternateIdSchema>(*stroidMesh.reference_mesh); + + domain_test_utils::check_schema_is_valid< + domain_test_utils::AlternateIdSchema>(*stroidMesh.mesh); + + /* + * Conversely, the production 1/2/3 + 1/2 schema must not silently + * accept a mesh generated under another numbering convention. + */ + const auto productionValidation = mean_field::utils::domain::validate_schema< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>( + *stroidMesh.mesh); + + CHECK_FALSE(productionValidation.valid()); + + CHECK(productionValidation.failed_relation_count() > 0); +} + +TEST_CASE("Complete Schema Validation Rejects A Mesh Without Vacuum", + tags::unit &tags::mesh &tags::utils &tags::domain) { + const mfem::Mesh mesh = + domain_test_utils::make_grid_mesh(3, 3, {2, 2, 2, 2, 1, 2, 2, 2, 2}, {}); + + const auto validation = mean_field::utils::domain::validate_schema< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + + CHECK_FALSE(validation.valid()); + + REQUIRE(validation.relationResults.size() == 7); + + /* + * Connected + */ + CHECK_FALSE(validation.relationResults[2].valid()); + + CHECK(validation.relationResults[2].result.failure == + mean_field::utils::domain::RelationValidationFailure::DomainAbsent); + + /* + * Inscribed + */ + CHECK_FALSE(validation.relationResults[4].valid()); + + CHECK( + validation.relationResults[4].result.failure == + mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent); +}