From 0a7f18c5c72aa562c020feeab6845e853bfe5217 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sun, 30 Aug 2026 16:41:14 -0400 Subject: [PATCH] feat(surface): major work on implementing surface constraints in a presciption agnostic manner --- CMakeLists.txt | 40 +- experiments/README.md | 43 + .../gravity_completed_rigid_motion.cpp | 273 + experiments/rigid_motion_null_space.cpp | 174 + experiments/stellar_null_space.cppm | 519 ++ libmeanfield/impl/analysis/integral.cpp | 68 +- libmeanfield/impl/fem.cpp | 613 +- libmeanfield/impl/integrators/advection.cpp | 6 +- libmeanfield/impl/integrators/centrifugal.cpp | 6 +- libmeanfield/impl/integrators/coriolis.cpp | 6 +- libmeanfield/impl/integrators/gravity.cpp | 6 +- .../impl/integrators/mass_continuity.cpp | 26 +- libmeanfield/impl/integrators/viscosity.cpp | 6 +- libmeanfield/impl/mapping/coefficients.cpp | 44 +- libmeanfield/impl/mapping/domain_mapper.cpp | 1872 +++--- libmeanfield/impl/models/polytropic.cpp | 15 +- .../hydrostatic_equilibrium_context.cpp | 12 +- libmeanfield/impl/operators/gravity_field.cpp | 1392 +++-- .../kernels/barotropic_closure_kernels.cpp | 25 +- .../gravity_displacement_force_kernels.cpp | 1299 ++-- .../operators/kernels/gravity_kernels.cpp | 1371 +++-- .../hydrostatic_equilibrium_kernels.cpp | 1143 ++-- .../kernels/pressure_force_kernels.cpp | 1054 ++-- .../rotation_displacement_force_kernels.cpp | 1043 ++-- .../operators/prepared_barotropic_closure.cpp | 16 +- .../operators/prepared_gravity_source.cpp | 866 +-- .../impl/operators/prepared_hdiv_mass.cpp | 676 +-- .../prepared_hydrostatic_equilibrium.cpp | 2047 ++++--- .../operators/prepared_mass_normalization.cpp | 1236 ++-- .../operators/prepared_pressure_force.cpp | 16 +- .../prepared_stellar_equilibrium.cpp | 74 +- libmeanfield/impl/physics/gravity.cpp | 25 +- libmeanfield/impl/physics/solid.cpp | 15 +- libmeanfield/impl/utils/domain.cpp | 9 +- libmeanfield/impl/utils/misc.cpp | 32 +- libmeanfield/interface/eos/concepts.cppm | 100 + libmeanfield/interface/eos/eos_base.cppm | 16 - libmeanfield/interface/eos/evaluation.cppm | 90 + libmeanfield/interface/eos/polytropic.cppm | 177 +- .../interface/eos/pressure_surface.cppm | 128 + libmeanfield/interface/eos/quantities.cppm | 235 + libmeanfield/interface/eos/relations.cppm | 93 + libmeanfield/interface/eos/runtime.cppm | 645 ++ libmeanfield/interface/field/field_mfem.cppm | 282 +- .../integrators/pressure_gradient.cppm | 6 +- .../interface/mapping/domain_mapper.cppm | 445 +- libmeanfield/interface/mean_field.cppm | 15 +- .../interface/models/stellar_model.cppm | 190 +- .../models/structure/polytropic.cppm | 10 +- .../models/structure/structure_base.cppm | 6 +- .../prepared_centering_constraint.cppm | 103 + .../prepared_stellar_equilibrium.cppm | 50 +- .../prepared_surface_constraint.cppm | 235 + libmeanfield/interface/surface/compiled.cppm | 66 + libmeanfield/interface/surface/compiler.cppm | 143 + libmeanfield/interface/surface/constant.cppm | 65 + .../interface/surface/dependencies.cppm | 186 + libmeanfield/interface/surface/isobaric.cppm | 64 - .../interface/surface/surface_base.cppm | 53 - libmeanfield/interface/utils/domain.cppm | 2095 ++++--- libmeanfield/interface/utils/misc.cppm | 134 +- tests/field/field_dof_map.cpp | 96 +- tests/integrators/centrifugal.cpp | 19 +- tests/integrators/gravity.cpp | 25 +- tests/mapping/domain_mapper.cpp | 5215 ++++++++-------- tests/models/stellar_model.cpp | 265 +- .../hydrostatic_equilibrium_context.cpp | 4 +- .../operators/gravity_displacement_force.cpp | 1142 ++-- ...isplacement_force_analytic_comparisons.cpp | 554 +- tests/operators/gravity_field.cpp | 5289 ++++++++--------- .../kernels/barotropic_closure_kernels.cpp | 5 +- .../hydrostatic_equilibrium_kernels.cpp | 1374 ++--- .../kernels/pressure_force_kernels.cpp | 1266 ++-- .../operators/prepared_barotropic_closure.cpp | 16 +- tests/operators/prepared_hdiv_mass.cpp | 304 +- ...rostatic_equilibrium_analytic_accuracy.cpp | 633 +- ...rostatic_equilibrium_complete_jacobian.cpp | 16 +- .../operators/prepared_mass_normalization.cpp | 28 +- tests/operators/prepared_pressure_force.cpp | 1132 ++-- .../prepared_rotation_displacement_force.cpp | 985 ++- .../prepared_stellar_equilibrium.cpp | 4393 +++++++------- tests/physics/barotrope.cpp | 237 +- tests/physics/barotrope_pressure.cpp | 852 +-- .../equation_of_state_consumer_contracts.cpp | 125 + .../equation_of_state_runtime_view.cpp | 322 + .../physics/equation_of_state_type_system.cpp | 228 + tests/physics/gravity.cpp | 4275 +++++++------ tests/physics/gravity_monopole_accuracy.cpp | 2026 +++---- .../polytropic_eos_characterization.cpp | 156 + tests/physics/polytropic_eos_relations.cpp | 200 + .../surface/constant_surface_compilation.cpp | 329 + tests/surface/isobaric.cpp | 76 - tests/test_helpers.cppm | 787 +-- tests/test_main.cpp | 56 +- tests/utils/domain.cpp | 1780 +++--- 95 files changed, 30144 insertions(+), 25766 deletions(-) create mode 100644 experiments/gravity_completed_rigid_motion.cpp create mode 100644 experiments/rigid_motion_null_space.cpp create mode 100644 experiments/stellar_null_space.cppm create mode 100644 libmeanfield/interface/eos/concepts.cppm delete mode 100644 libmeanfield/interface/eos/eos_base.cppm create mode 100644 libmeanfield/interface/eos/evaluation.cppm create mode 100644 libmeanfield/interface/eos/pressure_surface.cppm create mode 100644 libmeanfield/interface/eos/quantities.cppm create mode 100644 libmeanfield/interface/eos/relations.cppm create mode 100644 libmeanfield/interface/eos/runtime.cppm create mode 100644 libmeanfield/interface/operators/prepared_centering_constraint.cppm create mode 100644 libmeanfield/interface/operators/prepared_surface_constraint.cppm create mode 100644 libmeanfield/interface/surface/compiled.cppm create mode 100644 libmeanfield/interface/surface/compiler.cppm create mode 100644 libmeanfield/interface/surface/constant.cppm create mode 100644 libmeanfield/interface/surface/dependencies.cppm delete mode 100644 libmeanfield/interface/surface/isobaric.cppm delete mode 100644 libmeanfield/interface/surface/surface_base.cppm create mode 100644 tests/physics/equation_of_state_consumer_contracts.cpp create mode 100644 tests/physics/equation_of_state_runtime_view.cpp create mode 100644 tests/physics/equation_of_state_type_system.cpp create mode 100644 tests/physics/polytropic_eos_characterization.cpp create mode 100644 tests/physics/polytropic_eos_relations.cpp create mode 100644 tests/surface/constant_surface_compilation.cpp delete mode 100644 tests/surface/isobaric.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f1f6e9f..f3653b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.28) -project(MeanField CXX) +project(MeanField C CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -138,15 +138,24 @@ target_sources(mean_field libmeanfield/interface/operators/kernels/rotation_displacement_force_kernels.cppm libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm libmeanfield/interface/operators/prepared_displacement_operator.cppm - libmeanfield/interface/eos/eos_base.cppm + libmeanfield/interface/eos/quantities.cppm + libmeanfield/interface/eos/relations.cppm + libmeanfield/interface/eos/concepts.cppm + libmeanfield/interface/eos/evaluation.cppm + libmeanfield/interface/eos/pressure_surface.cppm + libmeanfield/interface/eos/runtime.cppm libmeanfield/interface/eos/polytropic.cppm libmeanfield/interface/models/structure/structure_base.cppm libmeanfield/interface/models/structure/polytropic.cppm libmeanfield/interface/models/structure_profile.cppm - libmeanfield/interface/surface/surface_base.cppm - libmeanfield/interface/surface/isobaric.cppm + libmeanfield/interface/surface/constant.cppm + libmeanfield/interface/surface/dependencies.cppm + libmeanfield/interface/surface/compiled.cppm + libmeanfield/interface/surface/compiler.cppm libmeanfield/interface/models/stellar_model.cppm libmeanfield/interface/operators/prepared_mass_normalization.cppm + libmeanfield/interface/operators/prepared_centering_constraint.cppm + libmeanfield/interface/operators/prepared_surface_constraint.cppm libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm ) @@ -194,6 +203,12 @@ add_executable(tests tests/operators/contexts/gravity_field_context.cpp tests/physics/gravity_monopole_accuracy.cpp tests/physics/barotrope.cpp + tests/physics/polytropic_eos_characterization.cpp + tests/physics/equation_of_state_type_system.cpp + tests/physics/equation_of_state_consumer_contracts.cpp + tests/physics/polytropic_eos_relations.cpp + tests/physics/equation_of_state_runtime_view.cpp + tests/surface/constant_surface_compilation.cpp tests/operators/kernels/barotropic_closure_kernels.cpp tests/operators/prepared_barotropic_closure.cpp tests/operators/contexts/barotropic_closure_linearization_context.cpp @@ -215,7 +230,6 @@ add_executable(tests tests/operators/prepared_rotation_displacement_force_analytic.cpp tests/operators/prepared_rotation_displacement_force_affine_deformation.cpp tests/operators/prepared_displacement_operator.cpp - tests/surface/isobaric.cpp tests/models/stellar_model.cpp tests/operators/prepared_mass_normalization.cpp tests/operators/prepared_stellar_equilibrium.cpp @@ -233,6 +247,7 @@ target_sources(experiment_mod PUBLIC FILE_SET CXX_MODULES FILES experiments/experiment_results.cppm + experiments/stellar_null_space.cppm ) target_link_libraries(experiment_mod PUBLIC @@ -247,6 +262,21 @@ add_executable(experiments target_link_libraries(experiments PRIVATE mean_field test_mod experiment_mod Catch2::Catch2 Boost::boost) +add_executable(stellar_null_space_experiments + experiments/experiment_main.cpp + experiments/rigid_motion_null_space.cpp + experiments/gravity_completed_rigid_motion.cpp +) + +target_link_libraries(stellar_null_space_experiments + PRIVATE + mean_field + test_mod + experiment_mod + Catch2::Catch2 + Boost::boost +) + include (CTest) include (Catch) catch_discover_tests( diff --git a/experiments/README.md b/experiments/README.md index af31656..358404b 100644 --- a/experiments/README.md +++ b/experiments/README.md @@ -24,6 +24,49 @@ Run only the budget and choose its output path with: ./mean_field_experiments --experiment-output gravity_budget.csv --catch2 "[accuracy]" ``` +## Stellar-equilibrium null-space experiments + +`stellar_null_space_experiments` is a dedicated diagnostic executable rather +than an ordinary verification or validation test. It constructs the analytic +`n = 3` Lane-Emden seed, probes the three computational translations and three +computational rotations, and compares the Jacobian before and after the strong +centering-row replacement. It records total and residual-block response norms, +the isolated centering contribution, and centered finite-difference errors. + +The experiment prints rank-zero progress messages while it builds the seed, +solves its gravity field, and completes each rigid-mode case. Run it with: + +```text +mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \ + --experiment-output stellar_null_space.csv \ + --catch2 "[null_space][rigid_motion]" +``` + +The rotation sweep includes zero rotation and a spherical-state diagnostic at +half the Keplerian angular speed. The rotating result is an operator-symmetry +probe, not a definitive rotating-equilibrium null-space measurement. + +The gravity-completed probe solves the linearized mixed gravity subsystem for +the gravity-gradient and gravity-potential variations accompanying each rigid +displacement. It then measures the complete equilibrium response with and +without the centering rows: + +```text +mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \ + --experiment-output gravity_completed_null_space.csv \ + --catch2 "[null_space][gravity_completed]" +``` + +The gravity solver prints its convergence summary, while the experiment prints +the current mode and completed-case count. This probe prepares each rotation +state only once and does not repeat the expensive nonlinear finite-difference +calculations from the original rigid-motion diagnostic. + +A whole-Jacobian dense singular-value experiment is intentionally deferred. +The checked-in `sandbox.smesh` is too large for a useful dense SVD, and the +current matrix-free root operator does not provide a transpose action needed by +a scalable smallest-singular-value method. + The executable needs the same dependencies, generated module mapping, and configuration registration as the existing Catch2 test executable. Add `experiment_main.cpp` and `gravity_accuracy_budget.cpp` as a second executable diff --git a/experiments/gravity_completed_rigid_motion.cpp b/experiments/gravity_completed_rigid_motion.cpp new file mode 100644 index 0000000..2e284fe --- /dev/null +++ b/experiments/gravity_completed_rigid_motion.cpp @@ -0,0 +1,273 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +import experiment; +import experiment.stellar_null_space; +import mean_field; +import test_helpers; + +namespace { + class GravityUnknownJacobian final : public mfem::Operator { + public: + explicit GravityUnknownJacobian( + const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator + ) + : mfem::Operator( + stellarOperator.GetLayout().size(experiment::null_space::gravityGradientValue) + + stellarOperator.GetLayout().size(experiment::null_space::gravityPotentialValue) + ), + m_stellarOperator(stellarOperator), + m_gravityGradientSize(stellarOperator.GetLayout().size(experiment::null_space::gravityGradientValue)) { + MFEM_VERIFY(Width() == Height(), "The reduced gravity Jacobian must be square."); + } + + void Mult( + const mfem::Vector &gravityDirection, + mfem::Vector &gravityAction + ) const override { + MFEM_VERIFY(gravityDirection.Size() == Width(), "The reduced gravity direction has the wrong size."); + + const mfem::Vector gravityGradientDirection( + const_cast(gravityDirection.GetData()), m_gravityGradientSize + ); + const mfem::Vector gravityPotentialDirection( + const_cast(gravityDirection.GetData()) + m_gravityGradientSize, + Width() - m_gravityGradientSize + ); + + m_stellarOperator.GetGravityOperator().ApplyGravityUnknowns( + gravityGradientDirection, + gravityPotentialDirection, + m_stellarOperator.GetGravityContext().GetGeometryContext(), + gravityAction + ); + } + + [[nodiscard]] int gravity_gradient_size() const noexcept { + return m_gravityGradientSize; + } + + private: + const mean_field::operators::PreparedStellarEquilibriumOperator &m_stellarOperator; + int m_gravityGradientSize; + }; + + void add_block_metrics( + std::map< + std::string, + double> &metrics, + const std::string &prefix, + const std::array< + double, + 6> &norms + ) { + for (std::size_t block = 0; block < norms.size(); ++block) { + metrics.emplace(prefix + experiment::null_space::residualBlockNames[block] + "_norm", norms[block]); + } + } + + [[nodiscard]] mfem::Vector gravity_residual_blocks( + const mfem::Vector &completeAction, + const mean_field::operators::StellarEquilibriumLayout &layout + ) { + const mfem::Vector gradient = experiment::null_space::const_residual_view( + completeAction, layout, experiment::null_space::gravityGradientResidual + ); + const mfem::Vector potential = experiment::null_space::const_residual_view( + completeAction, layout, experiment::null_space::gravityPotentialResidual + ); + + mfem::Vector result(gradient.Size() + potential.Size()); + mfem::Vector(result.GetData(), gradient.Size()) = gradient; + mfem::Vector(result.GetData() + gradient.Size(), potential.Size()) = potential; + return result; + } + + void assign_gravity_completion( + mfem::Vector &completeDirection, + const mean_field::operators::StellarEquilibriumLayout &layout, + const mfem::Vector &gravityCompletion, + const int gravityGradientSize + ) { + const mfem::Vector gravityGradient( + const_cast(gravityCompletion.GetData()), gravityGradientSize + ); + const mfem::Vector gravityPotential( + const_cast(gravityCompletion.GetData()) + gravityGradientSize, + gravityCompletion.Size() - gravityGradientSize + ); + experiment::null_space::assign_value_block( + completeDirection, layout, experiment::null_space::gravityGradientValue, gravityGradient + ); + experiment::null_space::assign_value_block( + completeDirection, layout, experiment::null_space::gravityPotentialValue, gravityPotential + ); + } + + void apply_centering_rows( + const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator, + const mfem::Vector &direction, + mfem::Vector &action + ) { + const auto &layout = stellarOperator.GetLayout(); + const mfem::Vector displacementDirection = + experiment::null_space::const_value_view(direction, layout, experiment::null_space::displacementValue); + mfem::Vector displacementAction = + experiment::null_space::residual_view(action, layout, experiment::null_space::displacementResidual); + stellarOperator.GetCenteringConstraintOperator().ApplyJacobianRows(displacementDirection, displacementAction); + } +} // namespace + +TEST_CASE( + "Gravity-Completed Rigid Motion Responses Of The Stellar Equilibrium Jacobian", + "[null_space][gravity_completed]" +) { + mean_field::utils::Args args = test_utils::setup_args(); + args.p.rtol = 1.0e-11; + args.p.atol = std::min(args.p.atol, 1.0e-13); + args.p.max_iters = std::max(args.p.max_iters, 2000); + + experiment::null_space::N3Equilibrium fixture(std::move(args)); + const MPI_Comm communicator = fixture.fem().mesh->GetComm(); + int rank = 0; + MPI_Comm_rank(communicator, &rank); + + const auto modes = experiment::null_space::make_rigid_modes(fixture); + constexpr std::array rotationFractions{0.0, 0.5}; + const int totalCases = static_cast(rotationFractions.size() * modes.size()); + int completedCases = 0; + + for (const double rotationFraction : rotationFractions) { + const mean_field::physics::RigidRotation rotation = fixture.rotation(rotationFraction); + fixture.prepare(fixture.state(), rotation); + + GravityUnknownJacobian gravityUnknownJacobian(fixture.stellar_operator()); + mean_field::operators::ReducedGravityFieldPreconditioner gravityPreconditioner( + fixture.fem(), fixture.stellar_operator().GetGravityContext().GetGeometryContext() + ); + + mfem::MINRESSolver gravitySolver(communicator); + gravitySolver.SetOperator(gravityUnknownJacobian); + gravitySolver.SetPreconditioner(gravityPreconditioner); + gravitySolver.SetRelTol(1.0e-11); + gravitySolver.SetAbsTol(1.0e-13); + gravitySolver.SetMaxIter(2000); + gravitySolver.SetPrintLevel(1); + + for (const experiment::null_space::RigidMode &mode : modes) { + experiment::null_space::report_progress( + communicator, "solving the gravity completion for " + mode.name + " at rotation fraction " + + std::to_string(rotationFraction) + " (" + std::to_string(completedCases + 1) + "/" + + std::to_string(totalCases) + ")" + ); + + const mfem::Vector displacementOnlyAction = fixture.unpinned_jacobian_action(mode.direction); + mfem::Vector gravityRightHandSide = + gravity_residual_blocks(displacementOnlyAction, fixture.stellar_operator().GetLayout()); + gravityRightHandSide *= -1.0; + + mfem::Vector gravityCompletion(gravityUnknownJacobian.Width()); + gravityCompletion = 0.0; + gravitySolver.Mult(gravityRightHandSide, gravityCompletion); + + REQUIRE(gravitySolver.GetConverged()); + + mfem::Vector gravitySolveAction; + gravityUnknownJacobian.Mult(gravityCompletion, gravitySolveAction); + mfem::Vector gravitySolveResidual(gravitySolveAction); + gravitySolveResidual -= gravityRightHandSide; + + const double gravityRightHandSideNorm = + experiment::null_space::global_norm(gravityRightHandSide, communicator); + const double gravitySolveResidualNorm = + experiment::null_space::global_norm(gravitySolveResidual, communicator); + const double gravitySolveRelativeResidual = + gravitySolveResidualNorm / std::max(gravityRightHandSideNorm, std::numeric_limits::epsilon()); + + REQUIRE(std::isfinite(gravitySolveRelativeResidual)); + + mfem::Vector completedDirection(mode.direction); + assign_gravity_completion( + completedDirection, fixture.stellar_operator().GetLayout(), gravityCompletion, + gravityUnknownJacobian.gravity_gradient_size() + ); + + const mfem::Vector completedUnpinnedAction = fixture.unpinned_jacobian_action(completedDirection); + mfem::Vector completedConstrainedAction(completedUnpinnedAction); + apply_centering_rows(fixture.stellar_operator(), completedDirection, completedConstrainedAction); + + mfem::Vector centeringContribution(completedConstrainedAction); + centeringContribution -= completedUnpinnedAction; + + std::map metrics{ + {"displacement_only_input_norm", experiment::null_space::global_norm(mode.direction, communicator)}, + {"gravity_completion_norm", experiment::null_space::global_norm(gravityCompletion, communicator)}, + {"completed_input_norm", experiment::null_space::global_norm(completedDirection, communicator)}, + {"displacement_only_action_norm", + experiment::null_space::global_norm(displacementOnlyAction, communicator)}, + {"gravity_completed_unpinned_action_norm", + experiment::null_space::global_norm(completedUnpinnedAction, communicator)}, + {"gravity_completed_constrained_action_norm", + experiment::null_space::global_norm(completedConstrainedAction, communicator)}, + {"centering_contribution_norm", + experiment::null_space::global_norm(centeringContribution, communicator)}, + {"gravity_solve_rhs_norm", gravityRightHandSideNorm}, + {"gravity_solve_residual_norm", gravitySolveResidualNorm}, + {"gravity_solve_relative_residual", gravitySolveRelativeResidual}, + {"gravity_solve_iterations", static_cast(gravitySolver.GetNumIterations())}, + {"gravity_solve_final_norm", gravitySolver.GetFinalNorm()} + }; + + add_block_metrics( + metrics, "displacement_only_", + experiment::null_space::residual_block_norms( + displacementOnlyAction, fixture.stellar_operator().GetLayout(), communicator + ) + ); + add_block_metrics( + metrics, "gravity_completed_unpinned_", + experiment::null_space::residual_block_norms( + completedUnpinnedAction, fixture.stellar_operator().GetLayout(), communicator + ) + ); + add_block_metrics( + metrics, "gravity_completed_constrained_", + experiment::null_space::residual_block_norms( + completedConstrainedAction, fixture.stellar_operator().GetLayout(), communicator + ) + ); + + if (rank == 0) { + experiment::record_experiment_result( + "gravity_completed_stellar_rigid_motion_null_space", mode.name, + {{"mode_kind", + mode.kind == experiment::null_space::RigidModeKind::translation ? "translation" : "rotation"}, + {"axis", std::to_string(mode.axis)}, + {"rotation_fraction_of_keplerian", std::to_string(rotationFraction)}, + {"mesh_file", test_utils::setup_args().mesh_file}, + {"local_state_dofs", std::to_string(fixture.stellar_operator().Width())}}, + std::move(metrics) + ); + } + + ++completedCases; + experiment::null_space::report_progress( + communicator, "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) + + " gravity-completed rigid-mode cases" + ); + } + } + + experiment::null_space::report_progress( + communicator, "gravity-completed rigid-motion probe complete; writing CSV output" + ); +} diff --git a/experiments/rigid_motion_null_space.cpp b/experiments/rigid_motion_null_space.cpp new file mode 100644 index 0000000..b41a1c8 --- /dev/null +++ b/experiments/rigid_motion_null_space.cpp @@ -0,0 +1,174 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +import experiment; +import experiment.stellar_null_space; +import mean_field; +import test_helpers; + +namespace { + [[nodiscard]] double relative_difference( + const mfem::Vector &computed, + const mfem::Vector &reference, + const MPI_Comm communicator + ) { + mfem::Vector difference(computed); + difference -= reference; + const double scale = std::max( + {experiment::null_space::global_norm(computed, communicator), + experiment::null_space::global_norm(reference, communicator), std::numeric_limits::epsilon()} + ); + return experiment::null_space::global_norm(difference, communicator) / scale; + } + + void add_block_metrics( + std::map< + std::string, + double> &metrics, + const std::string &prefix, + const std::array< + double, + 6> &norms + ) { + for (std::size_t block = 0; block < norms.size(); ++block) { + metrics.emplace(prefix + experiment::null_space::residualBlockNames[block] + "_norm", norms[block]); + } + } +} // namespace + +TEST_CASE( + "Rigid Motion Responses Of The Stellar Equilibrium Jacobian", + "[null_space][rigid_motion]" +) { + mean_field::utils::Args args = test_utils::setup_args(); + args.p.rtol = 1.0e-12; + args.p.atol = std::min(args.p.atol, 1.0e-14); + args.p.max_iters = std::max(args.p.max_iters, 2000); + + experiment::null_space::N3Equilibrium fixture(std::move(args)); + const MPI_Comm communicator = fixture.fem().mesh->GetComm(); + int rank = 0; + MPI_Comm_rank(communicator, &rank); + + const auto modes = experiment::null_space::make_rigid_modes(fixture); + constexpr std::array rotationFractions{0.0, 0.5}; + constexpr std::array finiteDifferenceSteps{1.0e-4, 1.0e-6}; + const int totalCases = static_cast(rotationFractions.size() * modes.size()); + int completedCases = 0; + + for (const double rotationFraction : rotationFractions) { + const mean_field::physics::RigidRotation rotation = fixture.rotation(rotationFraction); + fixture.prepare(fixture.state(), rotation); + + mfem::Vector constrainedResidual; + fixture.stellar_operator().BuildResidual(constrainedResidual); + const mfem::Vector unpinnedResidual = fixture.unpinned_residual(); + + REQUIRE(constrainedResidual.Size() == unpinnedResidual.Size()); + REQUIRE(std::isfinite(experiment::null_space::global_norm(constrainedResidual, communicator))); + REQUIRE(std::isfinite(experiment::null_space::global_norm(unpinnedResidual, communicator))); + + for (const experiment::null_space::RigidMode &mode : modes) { + experiment::null_space::report_progress( + communicator, "probing " + mode.name + " at rotation fraction " + std::to_string(rotationFraction) + + " (" + std::to_string(completedCases + 1) + "/" + std::to_string(totalCases) + ")" + ); + + fixture.prepare(fixture.state(), rotation); + const mfem::Vector unpinnedAction = fixture.unpinned_jacobian_action(mode.direction); + mfem::Vector constrainedAction; + fixture.stellar_operator().Mult(mode.direction, constrainedAction); + + mfem::Vector centeringContribution(constrainedAction); + centeringContribution -= unpinnedAction; + + const double inputNorm = experiment::null_space::global_norm(mode.direction, communicator); + const double unpinnedNorm = experiment::null_space::global_norm(unpinnedAction, communicator); + const double constrainedNorm = experiment::null_space::global_norm(constrainedAction, communicator); + + REQUIRE(inputNorm > 0.0); + REQUIRE(std::isfinite(unpinnedNorm)); + REQUIRE(std::isfinite(constrainedNorm)); + + std::map metrics{ + {"input_algebraic_norm", inputNorm}, + {"unpinned_action_norm", unpinnedNorm}, + {"unpinned_action_per_input_norm", unpinnedNorm / inputNorm}, + {"constrained_action_norm", constrainedNorm}, + {"constrained_action_per_input_norm", constrainedNorm / inputNorm}, + {"centering_contribution_norm", + experiment::null_space::global_norm(centeringContribution, communicator)}, + {"unpinned_base_residual_norm", experiment::null_space::global_norm(unpinnedResidual, communicator)}, + {"constrained_base_residual_norm", + experiment::null_space::global_norm(constrainedResidual, communicator)} + }; + + add_block_metrics( + metrics, "unpinned_", + experiment::null_space::residual_block_norms( + unpinnedAction, fixture.stellar_operator().GetLayout(), communicator + ) + ); + add_block_metrics( + metrics, "constrained_", + experiment::null_space::residual_block_norms( + constrainedAction, fixture.stellar_operator().GetLayout(), communicator + ) + ); + + for (const double step : finiteDifferenceSteps) { + mfem::Vector plusState(fixture.state()); + plusState.Add(step, mode.direction); + fixture.prepare(plusState, rotation); + const mfem::Vector plusResidual = fixture.unpinned_residual(); + + mfem::Vector minusState(fixture.state()); + minusState.Add(-step, mode.direction); + fixture.prepare(minusState, rotation); + const mfem::Vector minusResidual = fixture.unpinned_residual(); + + mfem::Vector finiteDifference(plusResidual); + finiteDifference -= minusResidual; + finiteDifference /= 2.0 * step; + + const std::string stepName = step == finiteDifferenceSteps.front() ? "1e-4" : "1e-6"; + metrics.emplace( + "finite_difference_relative_error_" + stepName, + relative_difference(unpinnedAction, finiteDifference, communicator) + ); + } + + fixture.prepare(fixture.state(), rotation); + + if (rank == 0) { + experiment::record_experiment_result( + "stellar_rigid_motion_null_space", mode.name, + {{"mode_kind", + mode.kind == experiment::null_space::RigidModeKind::translation ? "translation" : "rotation"}, + {"axis", std::to_string(mode.axis)}, + {"rotation_fraction_of_keplerian", std::to_string(rotationFraction)}, + {"mesh_file", test_utils::setup_args().mesh_file}, + {"local_state_dofs", std::to_string(fixture.stellar_operator().Width())}}, + std::move(metrics) + ); + } + + ++completedCases; + experiment::null_space::report_progress( + communicator, + "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) + " rigid-mode cases" + ); + } + } + + experiment::null_space::report_progress(communicator, "rigid-motion probe complete; writing CSV output"); +} diff --git a/experiments/stellar_null_space.cppm b/experiments/stellar_null_space.cppm new file mode 100644 index 0000000..5f81494 --- /dev/null +++ b/experiments/stellar_null_space.cppm @@ -0,0 +1,519 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +export module experiment.stellar_null_space; + +import mean_field; +import test_helpers; + +export namespace experiment::null_space { + using Form = mean_field::utils::blocks::barotropic_equilibrium_form; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using Model = mean_field::models::StellarModel; + + 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 + ); + + inline constexpr std::array residualBlockNames{"gravity_gradient", "gravity_potential", "closure", + "displacement", "hydrostatic", "mass"}; + + 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 + [[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)); + } + + 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), "Null-space experiment received a block with the wrong size."); + value_view(vector, layout, block) = source; + } + + [[nodiscard]] inline double global_norm( + const mfem::Vector &vector, + const MPI_Comm communicator + ) { + const double localNormSquared = vector * vector; + double globalNormSquared = 0.0; + MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, communicator); + return std::sqrt(globalNormSquared); + } + + inline void report_progress( + const MPI_Comm communicator, + const std::string &message + ) { + int rank = 0; + MPI_Comm_rank(communicator, &rank); + if (rank == 0) { + std::cout << "[null-space experiment] " << message << std::endl; + } + } + + [[nodiscard]] inline mean_field::operators::StellarEquilibriumDependencies make_dependencies() { + return { + .discretization = {.identity = 2003, .revision = 1}, + .density = {.identity = 2011, .revision = 1}, + .displacement = {.identity = 2017, .revision = 1}, + .gravityGradient = {.identity = 2027, .revision = 1}, + .gravityPotential = {.identity = 2029, .revision = 1}, + .enthalpy = {.identity = 2039, .revision = 1}, + .bernoulliConstant = {.identity = 2053, .revision = 1}, + .rotation = {.identity = 2063, .revision = 1}, + .targetMass = {.identity = 2069, .revision = 1} + }; + } + + inline void increment_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]] inline mfem::Vector pack_gravity_state( + const mfem::Vector &density, + const mfem::Vector &displacement, + const mfem::Vector &gravityGradient, + const mfem::Vector &gravityPotential + ) { + const std::array offsets{ + 0, density.Size(), density.Size() + displacement.Size(), + density.Size() + displacement.Size() + gravityGradient.Size(), + density.Size() + displacement.Size() + gravityGradient.Size() + gravityPotential.Size() + }; + mfem::Vector packed(offsets.back()); + mfem::Vector(packed.GetData() + offsets[0], density.Size()) = density; + mfem::Vector(packed.GetData() + offsets[1], displacement.Size()) = displacement; + mfem::Vector(packed.GetData() + offsets[2], gravityGradient.Size()) = gravityGradient; + mfem::Vector(packed.GetData() + offsets[3], gravityPotential.Size()) = gravityPotential; + return packed; + } + + [[nodiscard]] inline Model make_model() { + const double pi = std::acos(-1.0); + const double targetMass = mean_field::utils::MASS; + constexpr double dimensionlessMass = 2.0182359509662283534; + const double polytropicConstant = + pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0); + + return Model{ + mean_field::models::structure::PolytropicStructure{ + mean_field::eos::Polytrope{3.0, polytropicConstant}, targetMass + }, + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} + }; + } + + class N3Equilibrium final { + public: + explicit N3Equilibrium(mean_field::utils::Args args) + : m_args(std::move(args)), + m_fem( + mean_field::fem::setup_fem( + m_args.mesh_file, + m_args, + 0 + ) + ), + m_model(make_model()), + m_operator( + m_fem, + *m_fem.domainMapperStateless, + m_model + ), + m_state(m_operator.GetLayout().value_offsets().Last()), + m_dependencies(make_dependencies()) { + MFEM_VERIFY(m_fem.okay(), "The null-space experiment could not construct the finite-element problem."); + m_state = 0.0; + initialize_state(); + } + + [[nodiscard]] mean_field::fem::FEM &fem() noexcept { + return m_fem; + } + + [[nodiscard]] const mean_field::fem::FEM &fem() const noexcept { + return m_fem; + } + + [[nodiscard]] mean_field::operators::PreparedStellarEquilibriumOperator &stellar_operator() noexcept { + return m_operator; + } + + [[nodiscard]] const mean_field::operators::PreparedStellarEquilibriumOperator & + stellar_operator() const noexcept { + return m_operator; + } + + [[nodiscard]] const mfem::Vector &state() const noexcept { + return m_state; + } + + [[nodiscard]] mean_field::physics::RigidRotation rotation(const double fractionOfKeplerian) const { + const double radius = mean_field::utils::RADIUS; + const double mass = mean_field::utils::MASS; + const double keplerianSpeed = std::sqrt(mean_field::utils::G * mass / (radius * radius * radius)); + + mfem::Vector angularVelocity(3); + angularVelocity = 0.0; + angularVelocity(2) = fractionOfKeplerian * keplerianSpeed; + + mfem::Vector center(3); + center = 0.0; + return mean_field::physics::RigidRotation(angularVelocity, center); + } + + void prepare( + const mfem::Vector &state, + const mean_field::physics::RigidRotation &rotation + ) { + m_currentState = state; + increment_state_revisions(m_dependencies); + ++m_dependencies.rotation.revision; + m_operator.Prepare(state, m_dependencies, rotation); + } + + [[nodiscard]] mfem::Vector unpinned_residual() const { + const auto &layout = m_operator.GetLayout(); + const mfem::Vector reducedDensity = const_value_view(m_currentState, layout, densityValue); + const mfem::Vector displacement = const_value_view(m_currentState, layout, displacementValue); + const mfem::Vector gravityGradient = const_value_view(m_currentState, layout, gravityGradientValue); + const mfem::Vector gravityPotential = const_value_view(m_currentState, layout, gravityPotentialValue); + const mfem::Vector gravityState = + pack_gravity_state(reducedDensity, displacement, gravityGradient, gravityPotential); + + mfem::Vector gravity; + mfem::Vector closure; + mfem::Vector displacementRows; + mfem::Vector hydrostatic; + mfem::Vector mass; + m_operator.GetGravityOperator().Mult(gravityState, gravity); + m_operator.GetBarotropicClosureOperator().BuildResidual(closure); + m_operator.GetDisplacementOperator().BuildResidual(displacementRows); + m_operator.GetHydrostaticOperator().BuildResidual(hydrostatic); + m_operator.GetSurfaceConstraintOperator().ApplyResidualRows(hydrostatic); + m_operator.GetMassNormalizationOperator().BuildResidual(mass); + + return pack_residual(gravity, closure, displacementRows, hydrostatic, mass); + } + + [[nodiscard]] mfem::Vector unpinned_jacobian_action(const mfem::Vector &direction) const { + const auto &layout = m_operator.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 gravity; + mfem::Vector closure; + mfem::Vector displacementRows; + mfem::Vector hydrostatic; + mfem::Vector mass; + m_operator.GetGravityJacobianOperator().Mult(gravityDirection, gravity); + m_operator.GetBarotropicClosureOperator().Mult( + reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, closure + ); + m_operator.GetDisplacementOperator().ApplyCompleteJacobianAction( + reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection, + displacementRows + ); + m_operator.GetHydrostaticOperator().ApplyCompleteJacobianAction( + reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection, + hydrostatic + ); + m_operator.GetSurfaceConstraintOperator().ApplyJacobianRows(reducedEnthalpyDirection, hydrostatic); + m_operator.GetMassNormalizationOperator().ApplyCompleteJacobianAction( + reducedDensityDirection, displacementDirection, mass + ); + + return pack_residual(gravity, closure, displacementRows, hydrostatic, mass); + } + + private: + void initialize_state() { + report_progress(m_fem.mesh->GetComm(), "constructing the analytic n=3 Lane-Emden state"); + + constexpr double surfaceCoordinate = 6.8968486193769603755; + constexpr int radialSampleCount = 8192; + const double pi = std::acos(-1.0); + const double radius = mean_field::utils::RADIUS; + const double targetMass = mean_field::utils::MASS; + constexpr double dimensionlessMass = 2.0182359509662283534; + const double polytropicConstant = + pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0); + const double centralDensity = + std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / radius, 3.0); + + const mean_field::models::structure::StructureSeed seed = + m_model.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = radialSampleCount}); + + const auto interpolate = [](const mfem::Vector &radii, const mfem::Vector &values, const double r) { + if (r <= radii(0)) { + return values(0); + } + const int finalIndex = radii.Size() - 1; + if (r >= radii(finalIndex)) { + return values(finalIndex); + } + int lower = 0; + int upper = finalIndex; + while (upper - lower > 1) { + const int middle = lower + (upper - lower) / 2; + if (radii(middle) <= r) { + lower = middle; + } else { + upper = middle; + } + } + const double fraction = (r - radii(lower)) / (radii(upper) - radii(lower)); + return (1.0 - fraction) * values(lower) + fraction * values(upper); + }; + + mfem::FunctionCoefficient densityCoefficient([&seed, &interpolate](const mfem::Vector &position) { + const double r = position.Norml2(); + return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.density, r); + }); + mfem::FunctionCoefficient enthalpyCoefficient([&seed, &interpolate](const mfem::Vector &position) { + const double r = position.Norml2(); + return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.enthalpy, r); + }); + + mfem::ParGridFunction densityField(m_fem.densityFes.get()); + mfem::ParGridFunction enthalpyField(m_fem.enthalpyFes.get()); + mfem::ParGridFunction displacementField(m_fem.displacementFes.get()); + densityField = 0.0; + enthalpyField = 0.0; + displacementField = 0.0; + densityField.ProjectCoefficient(densityCoefficient); + enthalpyField.ProjectCoefficient(enthalpyCoefficient); + *m_fem.displacement = displacementField; + + report_progress(m_fem.mesh->GetComm(), "solving the gravity field for the seed state"); + const mean_field::physics::GravitySolution gravity = + mean_field::physics::solve_gravity_field(m_fem, m_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); + gravity.gradPhi.GetTrueDofs(gravityGradientTrue); + gravity.phi.GetTrueDofs(gravityPotentialTrue); + + const auto &layout = m_operator.GetLayout(); + const mean_field::field::FieldDofMap densityMap = + mean_field::field::make_field_dof_map(*m_fem.densityFes); + const mean_field::field::FieldDofMap enthalpyMap = + mean_field::field::make_field_dof_map(*m_fem.enthalpyFes); + + assign_value_block(m_state, layout, densityValue, densityMap.gather(densityTrue)); + assign_value_block(m_state, layout, displacementValue, displacementTrue); + assign_value_block(m_state, layout, gravityGradientValue, gravityGradientTrue); + assign_value_block(m_state, layout, gravityPotentialValue, gravityPotentialTrue); + assign_value_block(m_state, layout, enthalpyValue, enthalpyMap.gather(enthalpyTrue)); + value_view(m_state, layout, bernoulliValue)(0) = -mean_field::utils::G * targetMass / radius; + + m_currentState = m_state; + prepare(m_state, rotation(0.0)); + report_progress(m_fem.mesh->GetComm(), "analytic state is prepared"); + } + + [[nodiscard]] mfem::Vector pack_residual( + const mfem::Vector &gravity, + const mfem::Vector &closure, + const mfem::Vector &displacementRows, + const mfem::Vector &hydrostatic, + const mfem::Vector &mass + ) const { + const auto &layout = m_operator.GetLayout(); + mfem::Vector result(layout.residual_offsets().Last()); + result = 0.0; + const mfem::Vector gravityGradient(gravity.GetData(), layout.size(gravityGradientResidual)); + const mfem::Vector gravityPotential( + gravity.GetData() + layout.size(gravityGradientResidual), layout.size(gravityPotentialResidual) + ); + residual_view(result, layout, gravityGradientResidual) = gravityGradient; + residual_view(result, layout, gravityPotentialResidual) = gravityPotential; + residual_view(result, layout, densityResidual) = closure; + residual_view(result, layout, displacementResidual) = displacementRows; + residual_view(result, layout, enthalpyResidual) = hydrostatic; + residual_view(result, layout, massResidual) = mass; + return result; + } + + mean_field::utils::Args m_args; + mean_field::fem::FEM m_fem; + Model m_model; + mean_field::operators::PreparedStellarEquilibriumOperator m_operator; + mfem::Vector m_state; + mfem::Vector m_currentState; + mean_field::operators::StellarEquilibriumDependencies m_dependencies; + }; + + enum class RigidModeKind : std::uint8_t { translation, rotation }; + + struct RigidMode final { + std::string name; + RigidModeKind kind; + int axis; + mfem::Vector direction; + }; + + [[nodiscard]] inline std::array< + RigidMode, + 6> + make_rigid_modes(const N3Equilibrium &fixture) { + const auto &fem = fixture.fem(); + const auto &layout = fixture.stellar_operator().GetLayout(); + std::array modes; + + for (int axis = 0; axis < 3; ++axis) { + mfem::ParGridFunction translation(fem.displacementFes.get()); + mfem::Vector translationValue(3); + translationValue = 0.0; + translationValue(axis) = 1.0; + mfem::VectorConstantCoefficient coefficient(translationValue); + translation.ProjectCoefficient(coefficient); + mfem::Vector translationTrue; + translation.GetTrueDofs(translationTrue); + + mfem::Vector direction(layout.value_offsets().Last()); + direction = 0.0; + assign_value_block(direction, layout, displacementValue, translationTrue); + modes[axis] = RigidMode{ + .name = std::string("translation_") + static_cast('x' + axis), + .kind = RigidModeKind::translation, + .axis = axis, + .direction = std::move(direction) + }; + } + + for (int axis = 0; axis < 3; ++axis) { + mfem::ParGridFunction rotation(fem.displacementFes.get()); + mfem::VectorFunctionCoefficient coefficient(3, [axis](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(3); + value = 0.0; + const int first = (axis + 1) % 3; + const int second = (axis + 2) % 3; + value(first) = -position(second); + value(second) = position(first); + }); + rotation.ProjectCoefficient(coefficient); + mfem::Vector rotationTrue; + rotation.GetTrueDofs(rotationTrue); + + mfem::Vector direction(layout.value_offsets().Last()); + direction = 0.0; + assign_value_block(direction, layout, displacementValue, rotationTrue); + modes[3 + axis] = RigidMode{ + .name = std::string("rotation_") + static_cast('x' + axis), + .kind = RigidModeKind::rotation, + .axis = axis, + .direction = std::move(direction) + }; + } + return modes; + } + + [[nodiscard]] inline std::array< + double, + 6> + residual_block_norms( + const mfem::Vector &action, + const mean_field::operators::StellarEquilibriumLayout &layout, + const MPI_Comm communicator + ) { + return { + global_norm(const_residual_view(action, layout, gravityGradientResidual), communicator), + global_norm(const_residual_view(action, layout, gravityPotentialResidual), communicator), + global_norm(const_residual_view(action, layout, densityResidual), communicator), + global_norm(const_residual_view(action, layout, displacementResidual), communicator), + global_norm(const_residual_view(action, layout, enthalpyResidual), communicator), + global_norm(const_residual_view(action, layout, massResidual), communicator) + }; + } +} // namespace experiment::null_space diff --git a/libmeanfield/impl/analysis/integral.cpp b/libmeanfield/impl/analysis/integral.cpp index 5c1dc71..0890216 100644 --- a/libmeanfield/impl/analysis/integral.cpp +++ b/libmeanfield/impl/analysis/integral.cpp @@ -14,20 +14,23 @@ namespace { ) { switch (domain) { case mean_field::utils::DOMAINS::CORE: - return mean_field::utils::domain::make_attribute_marker< - mean_field::utils::domain::Core, DomainSchema>(mesh); + return mean_field::utils::domain::make_attribute_marker( + mesh + ); case mean_field::utils::DOMAINS::ENVELOPE: - return mean_field::utils::domain::make_attribute_marker< - mean_field::utils::domain::Envelope, DomainSchema>(mesh); + return mean_field::utils::domain::make_attribute_marker( + mesh + ); case mean_field::utils::DOMAINS::ALL: - return mean_field::utils::domain::make_attribute_marker< - mean_field::utils::domain::All, DomainSchema>(mesh); + return mean_field::utils::domain::make_attribute_marker(mesh); case mean_field::utils::DOMAINS::STELLAR: - return mean_field::utils::domain::make_attribute_marker< - mean_field::utils::domain::Stellar, DomainSchema>(mesh); + return mean_field::utils::domain::make_attribute_marker( + mesh + ); case mean_field::utils::DOMAINS::VACUUM: - return mean_field::utils::domain::make_attribute_marker< - mean_field::utils::domain::Vacuum, DomainSchema>(mesh); + return mean_field::utils::domain::make_attribute_marker( + mesh + ); } MFEM_ABORT("Unsupported integration domain."); } @@ -62,15 +65,14 @@ namespace mean_field::analysis { mfem::LinearForm lf(fem.densityFes.get()); mfem::GridFunctionCoefficient gf_c(&gf); double local_integral; - mfem::Array elem_markers = make_domain_marker(*fem.mesh, domain); + 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.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate, gf_c + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, gf_c ); // ReSharper disable once CppDFAMemoryLeak // Disabled because MFEM @@ -107,16 +109,14 @@ namespace mean_field::analysis { ) { const int dim = fem.mesh->Dimension(); mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate + *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 (!DomainSchema::template attribute_belongs_to( - fem.mesh->GetAttribute(i))) + 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( @@ -129,16 +129,15 @@ namespace mean_field::analysis { mapping::VolumeMappingContext mapping_context; MFEM_VERIFY( - mapping_evaluator.EvaluateVolume(*trans, ip, mapping_context) == - mapping::MappingStatus::valid, + 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); + const double weight = mapping_context.quadrature.weight; + double rho_val = rho.GetValue(i, ip); const mfem::Vector &phys_point = mapping_context.mapping.physical_position; - const double mass_term = rho_val * weight; + const double mass_term = rho_val * weight; local_mass += mass_term; for (int d = 0; d < dim; ++d) { @@ -183,8 +182,7 @@ namespace mean_field::analysis { std::unique_ptr s2_coeff; if (fem.has_mapping()) { s2_coeff = std::make_unique( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate, s2_func + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, s2_func ); } else { s2_coeff = std::make_unique(s2_func); @@ -204,8 +202,7 @@ namespace mean_field::analysis { double local_I = 0.0; if (fem.has_mapping()) { mapping::MappedScalarCoefficient mapped_integrand( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate, I_integrand + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, I_integrand ); auto *integrator = new mfem::DomainLFIntegrator(mapped_integrand); integrator->SetIntRule(&integration_rule); @@ -239,18 +236,16 @@ namespace mean_field::analysis { double local_volume = 0.0; mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate ); for (int e = 0; e < mesh.GetNE(); ++e) { - const int attr = mesh.GetAttribute(e); - 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)); + const int attr = mesh.GetAttribute(e); + 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); @@ -266,8 +261,7 @@ namespace mean_field::analysis { if (physical) { mapping::VolumeMappingContext context; MFEM_VERIFY( - mapping_evaluator.EvaluateVolume(*T, ip, context) == - mapping::MappingStatus::valid, + mapping_evaluator.EvaluateVolume(*T, ip, context) == mapping::MappingStatus::valid, "Mesh-volume integration encountered an invalid mapping." ); dV = context.quadrature.weight; diff --git a/libmeanfield/impl/fem.cpp b/libmeanfield/impl/fem.cpp index 4b2a0ea..c7bab33 100644 --- a/libmeanfield/impl/fem.cpp +++ b/libmeanfield/impl/fem.cpp @@ -21,361 +21,322 @@ 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 DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + 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."); + 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: 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; } - - 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 de6024b..9a4f46b 100644 --- a/libmeanfield/impl/integrators/advection.cpp +++ b/libmeanfield/impl/integrators/advection.cpp @@ -9,7 +9,11 @@ namespace mean_field::integrators { const mfem::GridFunction &displacement, const mfem::GridFunction &compactification_coordinate ) - : m_mapping(mapper, displacement, compactification_coordinate) { + : m_mapping( + mapper, + displacement, + compactification_coordinate + ) { } void AdvectionIntegrator::AssembleElementVector( diff --git a/libmeanfield/impl/integrators/centrifugal.cpp b/libmeanfield/impl/integrators/centrifugal.cpp index 9423c01..582aed6 100644 --- a/libmeanfield/impl/integrators/centrifugal.cpp +++ b/libmeanfield/impl/integrators/centrifugal.cpp @@ -9,7 +9,11 @@ namespace mean_field::integrators { const mfem::GridFunction &compactification_coordinate, const mfem::Vector &omega ) - : m_mapping(mapper, displacement, compactification_coordinate), + : m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_omega(3) { MFEM_ASSERT(omega.Size() == 3, "Omega vector must be 3D"); m_omega = omega; diff --git a/libmeanfield/impl/integrators/coriolis.cpp b/libmeanfield/impl/integrators/coriolis.cpp index 86e3312..0bd9f26 100644 --- a/libmeanfield/impl/integrators/coriolis.cpp +++ b/libmeanfield/impl/integrators/coriolis.cpp @@ -10,7 +10,11 @@ namespace mean_field::integrators { const mfem::GridFunction &compactification_coordinate, const mfem::Vector &omega ) - : m_mapping(mapper, displacement, compactification_coordinate), + : m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_omega(omega) { m_omega_mat.SetSize(3, 3); m_omega_mat = 0.0; diff --git a/libmeanfield/impl/integrators/gravity.cpp b/libmeanfield/impl/integrators/gravity.cpp index cb99244..1f85128 100644 --- a/libmeanfield/impl/integrators/gravity.cpp +++ b/libmeanfield/impl/integrators/gravity.cpp @@ -19,7 +19,11 @@ namespace mean_field::integrators { const mfem::GridFunction &compactification_coordinate, const GravityForceJacobianMode jacobian_mode ) - : m_mapping(mapper, displacement, compactification_coordinate), + : m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_jacobian_mode(jacobian_mode) { } diff --git a/libmeanfield/impl/integrators/mass_continuity.cpp b/libmeanfield/impl/integrators/mass_continuity.cpp index d132d93..8dcddb2 100644 --- a/libmeanfield/impl/integrators/mass_continuity.cpp +++ b/libmeanfield/impl/integrators/mass_continuity.cpp @@ -9,7 +9,11 @@ namespace mean_field::integrators { const mfem::GridFunction &displacement, const mfem::GridFunction &compactification_coordinate ) - : m_mapping(mapper, displacement, compactification_coordinate) { }; + : m_mapping( + mapper, + displacement, + compactification_coordinate + ) { }; void ContinuityVolumeIntegrator::AssembleElementVector( const mfem::Array &el, @@ -174,7 +178,11 @@ namespace mean_field::integrators { const mfem::GridFunction &displacement, const mfem::GridFunction &compactification_coordinate ) - : m_mapping(mapper, displacement, compactification_coordinate) { + : m_mapping( + mapper, + displacement, + compactification_coordinate + ) { } void ContinuityFaceIntegrator::AssembleFaceVector( @@ -206,11 +214,11 @@ namespace mean_field::integrators { } mfem::Vector &r_rho = *elvect[1]; r_rho.SetSize(dof_rho_minus + dof_rho_plus); - r_rho = 0.0; + r_rho = 0.0; - const int attr_minus = Tr.Elem1->Attribute; - const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1; - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + const int attr_minus = Tr.Elem1->Attribute; + const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1; + 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 @@ -416,9 +424,9 @@ namespace mean_field::integrators { } bool ContinuityFaceIntegrator::skip_face(const mfem::FaceElementTransformations &Tr) { - const int attr_minus = Tr.Elem1->Attribute; - const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1; - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + const int attr_minus = Tr.Elem1->Attribute; + const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1; + 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 diff --git a/libmeanfield/impl/integrators/viscosity.cpp b/libmeanfield/impl/integrators/viscosity.cpp index e6ba869..5c2cbec 100644 --- a/libmeanfield/impl/integrators/viscosity.cpp +++ b/libmeanfield/impl/integrators/viscosity.cpp @@ -10,7 +10,11 @@ namespace mean_field::integrators { const double mu, const int quad_boost ) - : m_mapping(mapper, displacement, compactification_coordinate), + : m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_mu(mu), m_quad_boost(quad_boost) { } diff --git a/libmeanfield/impl/mapping/coefficients.cpp b/libmeanfield/impl/mapping/coefficients.cpp index 7ba93fd..46172cd 100644 --- a/libmeanfield/impl/mapping/coefficients.cpp +++ b/libmeanfield/impl/mapping/coefficients.cpp @@ -15,7 +15,11 @@ namespace mean_field::mapping { Coefficient &coeff, const COORDINATE_SPACE coord_space ) - : m_mapping(mapper, displacement, compactification_coordinate), + : m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_coeff(coeff), m_coord_space(coord_space) { }; @@ -28,7 +32,7 @@ namespace mean_field::mapping { switch (m_coord_space) { case COORDINATE_SPACE::PHYSICAL: { - f_val = eval_at_point(m_coeff, T, ip); + f_val = eval_at_point(m_coeff, T, ip); VolumeMappingContext context; MFEM_VERIFY( m_mapping.EvaluateVolume(T, ip, context) == MappingStatus::valid, @@ -63,7 +67,11 @@ namespace mean_field::mapping { const int dim ) : MatrixCoefficient(dim), - m_mapping(mapper, displacement, compactification_coordinate), + m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_scalar(&sigma), m_tensor(nullptr) { }; @@ -74,7 +82,11 @@ namespace mean_field::mapping { MatrixCoefficient &sigma ) : MatrixCoefficient(sigma.GetHeight()), - m_mapping(mapper, displacement, compactification_coordinate), + m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_scalar(nullptr), m_tensor(&sigma) { }; @@ -92,7 +104,7 @@ namespace mean_field::mapping { "Mapped diffusion coefficient encountered an invalid mapping." ); const mfem::DenseMatrix &JInv = context.mapping.inverse_mapping_jacobian; - const double detJ = context.mapping.mapping_determinant; + const double detJ = context.mapping.mapping_determinant; if (m_scalar) { const double sig_val = m_scalar->Eval(T, ip); @@ -120,7 +132,11 @@ namespace mean_field::mapping { VectorCoefficient &coeff ) : VectorCoefficient(coeff.GetVDim()), - m_mapping(mapper, displacement, compactification_coordinate), + m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_coeff(coeff) { }; void MappedVectorCoefficient::Eval( @@ -137,7 +153,7 @@ namespace mean_field::mapping { "Mapped vector coefficient encountered an invalid mapping." ); const mfem::DenseMatrix &JInv = context.mapping.inverse_mapping_jacobian; - const double detJ = context.mapping.mapping_determinant; + const double detJ = context.mapping.mapping_determinant; mfem::Vector C_phys(dim); m_coeff.Eval(C_phys, T, ip); @@ -157,7 +173,11 @@ namespace mean_field::mapping { Func f // std::function ) : m_f(std::move(f)), - m_mapping(mapper, displacement, compactification_coordinate) { }; + m_mapping( + mapper, + displacement, + compactification_coordinate + ) { }; double PhysicalPositionFunctionCoefficient::Eval( mfem::ElementTransformation &T, @@ -179,7 +199,11 @@ namespace mean_field::mapping { const int dim ) : MatrixCoefficient(dim), - m_mapping(mapper, displacement, compactification_coordinate) { + m_mapping( + mapper, + displacement, + compactification_coordinate + ) { } void MappedHDivMassCoefficient::Eval( @@ -195,7 +219,7 @@ namespace mean_field::mapping { "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; + 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 b1ccc6a..61e5581 100644 --- a/libmeanfield/impl/mapping/domain_mapper.cpp +++ b/libmeanfield/impl/mapping/domain_mapper.cpp @@ -12,978 +12,960 @@ 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; + 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; } - } - 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); -} - -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; + 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." + ); + } } - 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; - } + const mfem::FiniteElement &ElementCompactificationData::GetElement() const noexcept { + return *m_element; + } - mfem::Mult(base_context.inverse_mapping_jacobian, - variation.mapping_jacobian_variation, workspace.m_matrix_temp_1); + const mfem::Vector &ElementCompactificationData::GetDofs() const noexcept { + return m_dofs; + } - 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; + int ElementCompactificationData::GetDofCount() const noexcept { + return m_dofs.Size(); + } - 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; + 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." + ); + } - 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; - } + m_dimension = displacement_dofs.Size() / dof_count; + m_dof_matrix.SetSize(dof_count, m_dimension); - return MappingStatus::valid; -} + 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."); + } + } -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; + const mfem::FiniteElement &ElementDisplacementData::GetElement() const noexcept { + return *m_element; + } - 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); + const mfem::DenseMatrix &ElementDisplacementData::GetDofMatrix() const noexcept { + return m_dof_matrix; + } - 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; + int ElementDisplacementData::GetDimension() const noexcept { + return m_dimension; + } - variation.weight_variation = integration_point.weight * - transformation.Weight() * - variation.mapping.mapping_determinant_variation; + int ElementDisplacementData::GetDofCount() const noexcept { + return m_element->GetDof(); + } - if (!matrix_is_finite(variation.inverse_element_jacobian_variation) || - !std::isfinite(variation.weight_variation)) - return MappingStatus::non_finite_result; + mfem::Ordering::Type ElementDisplacementData::GetOrdering() const noexcept { + return m_ordering; + } - return MappingStatus::valid; -} + ElementDisplacementData ElementDisplacementDataFromElementVDofs( + const mfem::FiniteElement &element, + const mfem::Vector &displacement_dofs + ) { + return ElementDisplacementData(element, displacement_dofs, mfem::Ordering::byNODES); + } -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); + DomainMapper::Workspace::Workspace(const int dimension) { + SetDimension(dimension); + } - 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; + void DomainMapper::Workspace::SetDimension(const int dimension) { + if (dimension <= 0) { + throw std::invalid_argument("Domain mapping workspace dimension must be positive."); + } - 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; + m_dimension = dimension; - 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; + m_field_value.SetSize(dimension); + m_field_jacobian.SetSize(dimension, dimension); - 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; + m_compactification_point.coordinate = 0.0; + m_compactification_point.coordinate_gradient.SetSize(dimension); - 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); + m_reference_normal.SetSize(dimension); + m_mapped_normal.SetSize(dimension); + m_full_element_jacobian.SetSize(dimension, dimension); - 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; + m_vector_temp.SetSize(dimension); + m_matrix_temp_1.SetSize(dimension, dimension); + m_matrix_temp_2.SetSize(dimension, dimension); - const double mapped_normal_magnitude_variation = - base_context.quadrature.normal * variation.physical_normal_variation; + m_exterior_result.physical_position.SetSize(dimension); + m_exterior_result.mapping_jacobian.SetSize(dimension, dimension); - variation.physical_normal_variation.Add(-mapped_normal_magnitude_variation, - base_context.quadrature.normal); - variation.physical_normal_variation /= mapped_normal_magnitude; + m_exterior_variation.physical_position_variation.SetSize(dimension); + m_exterior_variation.mapping_jacobian_variation.SetSize(dimension, dimension); + } - variation.physical_surface_weight_variation = - integration_point.weight * mapped_normal_magnitude_variation; - variation.normal_flux_scale_variation = - mapped_normal_magnitude_variation / reference_normal_magnitude; + int DomainMapper::Workspace::GetDimension() const noexcept { + return m_dimension; + } - 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; - } + 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."); + } - return MappingStatus::valid; -} + 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; + } + + 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/models/polytropic.cpp b/libmeanfield/impl/models/polytropic.cpp index e2eee07..d676865 100644 --- a/libmeanfield/impl/models/polytropic.cpp +++ b/libmeanfield/impl/models/polytropic.cpp @@ -15,7 +15,7 @@ namespace mean_field::models::structure { validate(); } - const eos::EquationOfState &PolytropicStructure::equationOfState() const noexcept { + const eos::Polytrope &PolytropicStructure::equationOfState() const noexcept { return m_equationOfState; } @@ -29,7 +29,9 @@ namespace mean_field::models::structure { const double polytropicIndex = m_equationOfState.polytropic_index(); const std::vector laneEmdenSolution = solveLaneEmden(polytropicIndex); const double surfaceCoordinate = laneEmdenSolution.back().coordinate; - const double centralEnthalpy = m_equationOfState.enthalpy_from_density(request.centralDensity); + const double centralEnthalpy = + eos::evaluate(m_equationOfState, eos::DensityValue{request.centralDensity}) + .value(); const double radialScaleSquared = centralEnthalpy / (4.0 * std::numbers::pi_v * mean_field::utils::G * request.centralDensity); @@ -61,11 +63,12 @@ namespace mean_field::models::structure { const double dimensionlessRadius = sampleFraction * surfaceCoordinate; const double laneEmdenValue = interpolateLaneEmdenValue(laneEmdenSolution, dimensionlessRadius, interpolationIndex); - const double density = request.centralDensity * std::pow(laneEmdenValue, polytropicIndex); + const double density = request.centralDensity * std::pow(laneEmdenValue, polytropicIndex); - seed.radius(sampleIndex) = radialScale * dimensionlessRadius; - seed.density(sampleIndex) = density; - seed.enthalpy(sampleIndex) = m_equationOfState.enthalpy_from_density(density); + seed.radius(sampleIndex) = radialScale * dimensionlessRadius; + seed.density(sampleIndex) = density; + seed.enthalpy(sampleIndex) = + eos::evaluate(m_equationOfState, eos::DensityValue{density}).value(); } seed.radius(0) = 0.0; diff --git a/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp b/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp index 4a6b429..7a60b2c 100644 --- a/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp +++ b/libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp @@ -80,13 +80,19 @@ namespace mean_field::operators::context::hydrostatic { : m_f(f), m_domainMapper(domainMapper), m_enthalpyMap( - field::make_field_dof_map(*f.enthalpyFes) + field::make_field_dof_map< + field::Enthalpy, + DomainSchema>(*f.enthalpyFes) ), m_gravityPotentialMap( - field::make_field_dof_map(*f.gravityPotentialFes) + field::make_field_dof_map< + field::Gravity, + DomainSchema>(*f.gravityPotentialFes) ), m_displacementMap( - field::make_field_dof_map(*f.displacementFes) + field::make_field_dof_map< + field::Displacement, + DomainSchema>(*f.displacementFes) ) { MFEM_VERIFY(m_f.mesh != nullptr, "HydrostaticEquilibriumContext requires a mesh."); diff --git a/libmeanfield/impl/operators/gravity_field.cpp b/libmeanfield/impl/operators/gravity_field.cpp index 9ce066d..7b08f2b 100644 --- a/libmeanfield/impl/operators/gravity_field.cpp +++ b/libmeanfield/impl/operators/gravity_field.cpp @@ -11,750 +11,724 @@ import :solver.fields; import :operators.kernels.gravity_field; namespace { -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."); + 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.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."); + } -[[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."); + [[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." + ); - mfem::Vector inverse_mass_diagonal(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, + 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); - } + "or non-finite mass diagonal." + ); + inverse_mass_diagonal(i) = 1.0 / inverse_mass_diagonal(i); + } - mfem::ParMixedBilinearForm divergence(f.gravityFluxFes.get(), - f.gravityPotentialFes.get()); - auto integrator = std::make_unique(); + mfem::ParMixedBilinearForm divergence(f.gravityFluxFes.get(), f.gravityPotentialFes.get()); + auto integrator = std::make_unique(); - const mfem::FiniteElement &trial_element = *f.gravityFluxFes->GetTypicalFE(); - const mfem::FiniteElement &test_element = - *f.gravityPotentialFes->GetTypicalFE(); - const mfem::ElementTransformation &transformation = - *f.mesh->GetElementTransformation(0); + const mfem::FiniteElement &trial_element = *f.gravityFluxFes->GetTypicalFE(); + const mfem::FiniteElement &test_element = *f.gravityPotentialFes->GetTypicalFE(); + const mfem::ElementTransformation &transformation = *f.mesh->GetElementTransformation(0); - f.quadratureFactory->configure_gravity_divergence( - *integrator, quadrature::QuadratureRole::preconditioner, trial_element, - test_element, transformation, utils::DOMAINS::ALL, - quadrature::MappingKind::none); + f.quadratureFactory->configure_gravity_divergence( + *integrator, quadrature::QuadratureRole::preconditioner, trial_element, test_element, transformation, + utils::DOMAINS::ALL, quadrature::MappingKind::none + ); - divergence.AddDomainIntegrator(integrator.release()); - divergence.Assemble(); - divergence.Finalize(); + divergence.AddDomainIntegrator(integrator.release()); + divergence.Assemble(); + divergence.Finalize(); - 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); + 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); - return std::unique_ptr(mfem::ParMult( - divergence_matrix.get(), inverse_mass_divergence_transpose.get())); -} + 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."); + 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; + 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); -} + 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; + 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_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; -} + 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."); + 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; + 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); -} + 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::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."); + 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 (DomainSchema::template attribute_belongs_to( - f.mesh->attributes[i])) { - has_vacuum_domain = true; - break; + 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; } - } - - 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/kernels/barotropic_closure_kernels.cpp b/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp index 618c929..ad3aa07 100644 --- a/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp @@ -12,6 +12,8 @@ import :field.registry; import :utils.domain; namespace { + namespace eos = mean_field::eos; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; using ClosureDomain = mean_field::field::FieldDomainT; @@ -336,11 +338,21 @@ namespace { if (closureAction == ClosureAction::residual) { const double density = elementDensityInput * densityShape; - integrand = density - barotrope.density_from_enthalpy(baseEnthalpy); + const double equationOfStateDensity = + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{baseEnthalpy}) + .value(); + + integrand = density - equationOfStateDensity; } else { const double enthalpyVariation = elementEnthalpyVariation * enthalpyShape; - integrand = -barotrope.density_derivative_from_enthalpy(baseEnthalpy) * enthalpyVariation; + const double densityDerivative = + eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{baseEnthalpy} + ) + .value(); + + integrand = -densityDerivative * enthalpyVariation; } } @@ -628,11 +640,14 @@ namespace mean_field::operators::kernels { enthalpyElement.CalcShape(integrationPoint, enthalpyShape); - const double densityValue = elementBaseDensity * densityShape; + const double densityValue = elementBaseDensity * densityShape; - const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; + const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; - const double closureValue = densityValue - barotrope.density_from_enthalpy(enthalpyValue); + const double equationOfStateDensity = + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpyValue}).value(); + + const double closureValue = densityValue - equationOfStateDensity; const double geometryActionValue = closureValue * mappingVariation.weight_variation; diff --git a/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp b/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp index a7db7ee..ab45396 100644 --- a/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp @@ -11,688 +11,713 @@ module mean_field; import :operators.kernels.gravity_displacement_force; namespace { -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -[[nodiscard]] bool is_vacuum_attribute(const int attribute) { - return DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(attribute); -} - -enum class GravityDisplacementForceAction { - residual, - density, - gravityGradient, - displacement, - complete -}; - -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."); - - 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(), - "The gravity-displacement-force 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; - } -} - -[[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; - } - - if (ordering == mfem::Ordering::byVDIM) { - return scalarDof * dimension + component; - } - - MFEM_ABORT("The gravity-displacement-force test space uses an unsupported " - "ordering."); - - return -1; -} - -[[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(densityElement.GetOrder() == - mean_field::field::Density::Scalar::familyOrder, - "The gravity-displacement-force density element does not match " - "the registered density field."); - - 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."); - - MFEM_VERIFY(displacementElement.GetOrder() == - mean_field::field::Displacement::Vector::familyOrder, - "The gravity-displacement-force test element does not match the " - "registered displacement field."); - - 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); - - const mean_field::quadrature::MfemRule rule = - f.quadratureFactory->get(query, transformation.GetGeometryType()); - - MFEM_VERIFY(rule.integration_rule != nullptr, - "The quadrature policy did not return a gravity-displacement-" - "force integration rule."); - - return *rule.integration_rule; -} - -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); - } -} - -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.densityFes != nullptr, - "The gravity-displacement-force kernel requires the density " - "finite-element space."); - - MFEM_VERIFY(f.gravityFluxFes != nullptr, - "The gravity-displacement-force kernel requires the gravity-" - "gradient finite-element space."); - - MFEM_VERIFY(f.displacementFes != nullptr, - "The gravity-displacement-force kernel requires the displacement " - "finite-element space."); - - MFEM_VERIFY(f.compactificationFes != nullptr && - f.compactificationCoordinate != nullptr, - "The gravity-displacement-force kernel requires the " - "compactification coordinate."); - - MFEM_VERIFY(f.quadratureFactory != nullptr, - "The gravity-displacement-force kernel requires the quadrature " - "rule factory."); - - MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), - "The gravity-displacement-force displacement vector has the " - "wrong size."); - - MFEM_VERIFY(domainMapper.GetDimension() == f.mesh->Dimension(), - "The gravity-displacement-force mapper dimension does not match " - "the mesh dimension."); - - MFEM_VERIFY(f.displacementFes->GetVDim() == f.mesh->Dimension(), - "The gravity-displacement-force displacement dimension does not " - "match the mesh dimension."); - - validate_finite_vector( - displacementTrue, - "The gravity-displacement-force displacement contains a " - "non-finite value."); -} - -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 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(gravityGradient, message); -} - -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 needsBaseDensity = - requestedAction == GravityDisplacementForceAction::residual || - requestedAction == GravityDisplacementForceAction::gravityGradient || - requestedAction == GravityDisplacementForceAction::displacement || - requestedAction == GravityDisplacementForceAction::complete; - - const bool needsDensityVariation = - requestedAction == GravityDisplacementForceAction::density || - requestedAction == GravityDisplacementForceAction::complete; - - const bool needsBaseGravityGradient = - requestedAction == GravityDisplacementForceAction::residual || - requestedAction == GravityDisplacementForceAction::density || - requestedAction == GravityDisplacementForceAction::displacement || - requestedAction == GravityDisplacementForceAction::complete; - - const bool needsGravityGradientVariation = - requestedAction == GravityDisplacementForceAction::gravityGradient || - requestedAction == GravityDisplacementForceAction::complete; - - const bool needsDisplacementVariation = - requestedAction == GravityDisplacementForceAction::displacement || - requestedAction == GravityDisplacementForceAction::complete; - - if (needsBaseDensity) { - MFEM_VERIFY(baseDensityTrue != nullptr, - "The gravity-displacement-force action requires a base " - "density."); - - validate_density(f, *baseDensityTrue, - "The gravity-displacement-force base density is invalid."); - } - - if (needsDensityVariation) { - MFEM_VERIFY(densityVariationTrue != nullptr, - "The gravity-displacement-force action requires a density " - "variation."); - - validate_density(f, *densityVariationTrue, - "The gravity-displacement-force density variation is " - "invalid."); - } - - if (needsBaseGravityGradient) { - MFEM_VERIFY(baseGravityGradientTrue != nullptr, - "The gravity-displacement-force action requires a base " - "gravity gradient."); - - validate_gravity_gradient( - f, *baseGravityGradientTrue, - "The gravity-displacement-force base gravity gradient is " - "invalid."); - } - - 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."); - - 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; - - 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; + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); } - const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId); + enum class GravityDisplacementForceAction { residual, density, gravityGradient, displacement, complete }; - const mfem::FiniteElement &gravityGradientElement = - *f.gravityFluxFes->GetFE(elementId); + 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." + ); - const mfem::FiniteElement &displacementElement = - *f.displacementFes->GetFE(elementId); + localVector.SetSize(finiteElementSpace.GetVSize()); - const mfem::FiniteElement &compactificationElement = - *f.compactificationFes->GetFE(elementId); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); - 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 (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } } - if (needsDensityVariation) { - densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation); + 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." + ); + + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; + + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } } - if (needsBaseGravityGradient) { - baseGravityGradientLocal.GetSubVector(gravityGradientDofs, - elementBaseGravityGradient); + [[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; + } + + if (ordering == mfem::Ordering::byVDIM) { + return scalarDof * dimension + component; + } + + MFEM_ABORT( + "The gravity-displacement-force test space uses an unsupported " + "ordering." + ); + + return -1; } - 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); + [[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( - 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.GetOrder() == mean_field::field::Density::Scalar::familyOrder, + "The gravity-displacement-force density element does not match " + "the registered density field." + ); - densityElement.CalcShape(integrationPoint, densityShape); + 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." + ); - displacementElement.CalcShape(integrationPoint, displacementShape); + MFEM_VERIFY( + displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder, + "The gravity-displacement-force test element does not match the " + "registered displacement field." + ); - gravityGradientElement.CalcVShape(*transformation, gravityGradientShape); + 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 + ); - double baseDensityValue = 0.0; - double densityVariationValue = 0.0; + const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); - if (needsBaseDensity) { - baseDensityValue = elementBaseDensity * densityShape; - } + MFEM_VERIFY( + rule.integration_rule != nullptr, "The quadrature policy did not return a gravity-displacement-" + "force integration rule." + ); - if (needsDensityVariation) { - densityVariationValue = elementDensityVariation * densityShape; - } + return *rule.integration_rule; + } - if (needsBaseGravityGradient) { - gravityGradientShape.MultTranspose(elementBaseGravityGradient, - baseGravityReferenceValue); + 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); + } + } - mappingContext.mapping.mapping_jacobian.Mult(baseGravityReferenceValue, - mappedBaseGravity); - } else { - mappedBaseGravity = 0.0; - } + 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."); - if (needsGravityGradientVariation) { - gravityGradientShape.MultTranspose(elementGravityGradientVariation, - gravityVariationReferenceValue); + MFEM_VERIFY( + f.densityFes != nullptr, "The gravity-displacement-force kernel requires the density " + "finite-element space." + ); - mappingContext.mapping.mapping_jacobian.Mult( - gravityVariationReferenceValue, mappedGravityVariation); - } else { - mappedGravityVariation = 0.0; - } + MFEM_VERIFY( + f.gravityFluxFes != nullptr, "The gravity-displacement-force kernel requires the gravity-" + "gradient finite-element space." + ); - if (needsDisplacementVariation) { - mappingVariation.mapping.mapping_jacobian_variation.Mult( - baseGravityReferenceValue, mappedGeometryVariation); - } else { - mappedGeometryVariation = 0.0; - } + MFEM_VERIFY( + f.displacementFes != nullptr, "The gravity-displacement-force kernel requires the displacement " + "finite-element space." + ); - forceValue = 0.0; + MFEM_VERIFY( + f.compactificationFes != nullptr && f.compactificationCoordinate != nullptr, + "The gravity-displacement-force kernel requires the " + "compactification coordinate." + ); + + MFEM_VERIFY( + f.quadratureFactory != nullptr, "The gravity-displacement-force kernel requires the quadrature " + "rule factory." + ); + + MFEM_VERIFY( + displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The gravity-displacement-force displacement vector has the " + "wrong size." + ); + + MFEM_VERIFY( + domainMapper.GetDimension() == f.mesh->Dimension(), + "The gravity-displacement-force mapper dimension does not match " + "the mesh dimension." + ); + + MFEM_VERIFY( + f.displacementFes->GetVDim() == f.mesh->Dimension(), + "The gravity-displacement-force displacement dimension does not " + "match the mesh dimension." + ); + + validate_finite_vector( + displacementTrue, "The gravity-displacement-force displacement contains a " + "non-finite value." + ); + } + + 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 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(gravityGradient, message); + } + + 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 needsBaseDensity = requestedAction == GravityDisplacementForceAction::residual || + requestedAction == GravityDisplacementForceAction::gravityGradient || + requestedAction == GravityDisplacementForceAction::displacement || + requestedAction == GravityDisplacementForceAction::complete; + + const bool needsDensityVariation = requestedAction == GravityDisplacementForceAction::density || + requestedAction == GravityDisplacementForceAction::complete; + + const bool needsBaseGravityGradient = requestedAction == GravityDisplacementForceAction::residual || + requestedAction == GravityDisplacementForceAction::density || + requestedAction == GravityDisplacementForceAction::displacement || + requestedAction == GravityDisplacementForceAction::complete; + + const bool needsGravityGradientVariation = requestedAction == GravityDisplacementForceAction::gravityGradient || + requestedAction == GravityDisplacementForceAction::complete; + + const bool needsDisplacementVariation = requestedAction == GravityDisplacementForceAction::displacement || + requestedAction == GravityDisplacementForceAction::complete; + + if (needsBaseDensity) { + MFEM_VERIFY( + baseDensityTrue != nullptr, "The gravity-displacement-force action requires a base " + "density." + ); + + validate_density(f, *baseDensityTrue, "The gravity-displacement-force base density is invalid."); + } - if (requestedAction == GravityDisplacementForceAction::residual) { - forceValue.Add(baseDensityValue, mappedBaseGravity); - } else { if (needsDensityVariation) { - forceValue.Add(densityVariationValue, mappedBaseGravity); + MFEM_VERIFY( + densityVariationTrue != nullptr, "The gravity-displacement-force action requires a density " + "variation." + ); + + validate_density( + f, *densityVariationTrue, + "The gravity-displacement-force density variation is " + "invalid." + ); + } + + if (needsBaseGravityGradient) { + MFEM_VERIFY( + baseGravityGradientTrue != nullptr, "The gravity-displacement-force action requires a base " + "gravity gradient." + ); + + validate_gravity_gradient( + f, *baseGravityGradientTrue, + "The gravity-displacement-force base gravity gradient is " + "invalid." + ); } if (needsGravityGradientVariation) { - forceValue.Add(baseDensityValue, mappedGravityVariation); + 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) { - forceValue.Add(baseDensityValue, mappedGeometryVariation); + MFEM_VERIFY( + displacementVariationTrue != nullptr && + displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(), + "The gravity-displacement-force displacement variation is " + "invalid." + ); + + validate_finite_vector( + *displacementVariationTrue, "The gravity-displacement-force displacement variation " + "contains a non-finite value." + ); } - } - /* - * 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(); + mfem::Vector baseDensityLocal; + mfem::Vector densityVariationLocal; + mfem::Vector baseGravityGradientLocal; + mfem::Vector gravityGradientVariationLocal; + mfem::Vector displacementLocal; + mfem::Vector displacementVariationLocal; - 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 (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) { + 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); + } + + 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::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_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::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_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::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_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::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_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::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); -} + 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 54f52e8..bec9823 100644 --- a/libmeanfield/impl/operators/kernels/gravity_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/gravity_kernels.cpp @@ -6,788 +6,779 @@ module mean_field; import :operators.kernels.gravity_field; namespace { -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -[[nodiscard]] bool is_vacuum_attribute(const int attribute) { - return DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(attribute); -} + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); + } -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 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."); - local_vector.SetSize(finite_element_space.GetVSize()); + 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; - } -} + const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); + 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(); - if (prolongation != nullptr) { - prolongation->MultTranspose(local_vector, true_vector); - } else { - true_vector = local_vector; - } -} + const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->MultTranspose(local_vector, true_vector); + } else { + true_vector = local_vector; + } + } -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; - } -} + 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; + } + } -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; -} + 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_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 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 an H(div) mass 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 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 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 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::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."); + 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::DomainMapper::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; - } + 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); } - if (gravity_dof_transformation != nullptr) - gravity_dof_transformation->TransformDual(element_action); - local_action.AddElementVector(gravity_dofs, element_action); - } + 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." + ); - local_to_true(*f.gravityFluxFes, local_action, 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); -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."); + mfem::Vector local_action(f.gravityPotentialFes->GetVSize()); + local_action = 0.0; - 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); + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - mfem::Vector local_action(f.gravityPotentialFes->GetVSize()); - local_action = 0.0; + mfem::Array density_dofs; + mfem::Array potential_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); + mfem::Vector element_density; + mfem::Vector element_displacement; + mfem::Vector element_compactification; + mfem::Vector element_action; + mfem::Vector density_shape; + mfem::Vector potential_shape; - mfem::Array density_dofs; - mfem::Array potential_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + constexpr double source_scale = 4.0 * M_PI * utils::G; - mfem::Vector element_density; - mfem::Vector element_displacement; - mfem::Vector element_compactification; - mfem::Vector element_action; - mfem::Vector density_shape; - mfem::Vector potential_shape; + 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; - constexpr double source_scale = 4.0 * M_PI * utils::G; + 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); - 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; + 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 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); + density_local.GetSubVector(density_dofs, element_density); + displacement_local.GetSubVector(displacement_dofs, element_displacement); + f.compactificationCoordinate->GetSubVector(compactification_dofs, 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); + 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_local.GetSubVector(density_dofs, element_density); - displacement_local.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, - 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 + }; - 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); + const int density_dof_count = density_element.GetDof(); + const int potential_dof_count = potential_element.GetDof(); - 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_shape.SetSize(density_dof_count); + potential_shape.SetSize(potential_dof_count); + element_action.SetSize(potential_dof_count); + element_action = 0.0; - const int density_dof_count = density_element.GetDof(); - const int potential_dof_count = potential_element.GetDof(); + const mfem::IntegrationRule &integration_rule = + get_source_rule(f, density_element, potential_element, *transformation); - density_shape.SetSize(density_dof_count); - potential_shape.SetSize(potential_dof_count); - element_action.SetSize(potential_dof_count); - element_action = 0.0; + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); - const mfem::IntegrationRule &integration_rule = - get_source_rule(f, density_element, potential_element, *transformation); + 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 q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); + density_element.CalcShape(integration_point, density_shape); + potential_element.CalcShape(integration_point, potential_shape); - 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 double density_value = element_density * density_shape; + const double weight = source_scale * density_value * mapping_context.quadrature.weight; - density_element.CalcShape(integration_point, density_shape); - potential_element.CalcShape(integration_point, potential_shape); + for (int i = 0; i < potential_dof_count; ++i) + element_action(i) += weight * potential_shape(i); + } - const double density_value = element_density * density_shape; - const double weight = - source_scale * density_value * mapping_context.quadrature.weight; + if (potential_dof_transformation != nullptr) + potential_dof_transformation->TransformDual(element_action); + local_action.AddElementVector(potential_dofs, element_action); + } - for (int i = 0; i < potential_dof_count; ++i) - element_action(i) += weight * potential_shape(i); + local_to_true(*f.gravityPotentialFes, local_action, action); } - if (potential_dof_transformation != nullptr) - potential_dof_transformation->TransformDual(element_action); - local_action.AddElementVector(potential_dofs, element_action); - } + 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." + ); - local_to_true(*f.gravityPotentialFes, local_action, action); -} + mfem::Vector gravity_gradient_local; + mfem::Vector displacement_local; + mfem::Vector 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."); + 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::Vector gravity_gradient_local; - mfem::Vector displacement_local; - mfem::Vector displacement_variation_local; + mfem::Vector local_action(f.gravityFluxFes->GetVSize()); + local_action = 0.0; - 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); + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - mfem::Vector local_action(f.gravityFluxFes->GetVSize()); - local_action = 0.0; + mfem::Array gravity_gradient_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); + 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; - mfem::Array gravity_gradient_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + mfem::DenseMatrix gravity_gradient_shape; + mfem::DenseMatrix mass_tensor_variation; - 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); - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(element_displacement); - displacement_dof_transformation->InvTransformPrimal( - element_displacement_variation); + 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 (compactification_dof_transformation != nullptr) - compactification_dof_transformation->InvTransformPrimal( - element_compactification); + 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." + ); - 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}; + mfem::Vector density_local; + mfem::Vector displacement_local; + mfem::Vector displacement_variation_local; - const int gravity_gradient_dof_count = gravity_gradient_element.GetDof(); - const int dimension = transformation->GetSpaceDim(); + 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); - element_action.SetSize(gravity_gradient_dof_count); - element_action = 0.0; + mfem::Vector local_action(f.gravityPotentialFes->GetVSize()); + local_action = 0.0; - gravity_gradient_value.SetSize(dimension); - mass_tensor_variation_action.SetSize(dimension); + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - gravity_gradient_shape.SetSize(gravity_gradient_dof_count, dimension); - mass_tensor_variation.SetSize(dimension, dimension); + mfem::Array density_dofs; + mfem::Array potential_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - const mfem::IntegrationRule &integration_rule = get_hdiv_mass_rule( - f, domain_mapper, gravity_gradient_element, *transformation); + 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; - 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; + mapping::VolumeMappingVariation mapping_variation; - 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)); + constexpr double gravitational_source_scale = 4.0 * M_PI * utils::G; - 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."); + 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::ComputeHDivMassTensorVariation(mapping_context.mapping, - mapping_variation.mapping, - mass_tensor_variation); + if (is_vacuum_attribute(transformation->Attribute)) + continue; - 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(); + 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_shape.AddMult(mass_tensor_variation_action, - element_action, reference_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); } - - 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 993e4d7..f81d858 100644 --- a/libmeanfield/impl/operators/kernels/hydrostatic_equilibrium_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/hydrostatic_equilibrium_kernels.cpp @@ -11,603 +11,618 @@ module mean_field; import :operators.kernels.hydrostatic_equilibrium; namespace { -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -[[nodiscard]] bool is_vacuum_attribute(const int attribute) { - return DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(attribute); -} - -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."); - - 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; + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); } - const mfem::FiniteElement &enthalpyElement = - *f.enthalpyFes->GetFE(elementId); + 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."); - const mfem::FiniteElement &potentialElement = - *f.gravityPotentialFes->GetFE(elementId); + localVector.SetSize(finiteElementSpace.GetVSize()); - const mfem::FiniteElement &displacementElement = - *f.displacementFes->GetFE(elementId); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); - 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 (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } } - if (request.basePotentialTrue != nullptr) { - basePotentialLocal.GetSubVector(potentialDofs, elementBasePotential); + 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; + } } - if (request.enthalpyVariationTrue != nullptr) { - enthalpyVariationLocal.GetSubVector(enthalpyDofs, - elementEnthalpyVariation); + 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." + ); } - if (request.potentialVariationTrue != nullptr) { - potentialVariationLocal.GetSubVector(potentialDofs, - elementPotentialVariation); + 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.displacementVariationTrue != nullptr) { - displacementVariationLocal.GetSubVector(displacementDofs, - elementDisplacementVariation); + 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; + } + + 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 (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::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; + 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::DomainMapper &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::DomainMapper &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::DomainMapper &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::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; + 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::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; + 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); -} + 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 4813298..b1b3b42 100644 --- a/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp @@ -12,549 +12,575 @@ module mean_field; import :operators.kernels.pressure_force; namespace { -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + namespace eos = mean_field::eos; -[[nodiscard]] bool is_vacuum_attribute(const int attribute) { - return DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(attribute); -} + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -enum class PressureForceAction { residual, enthalpy, displacement }; - -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."); - - 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(), - "The pressure-force 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; - } -} - -[[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; - } - - if (ordering == mfem::Ordering::byVDIM) { - return scalarDof * dimension + component; - } - - MFEM_ABORT("The displacement space uses an unsupported ordering."); - return -1; -} - -[[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); - - MFEM_VERIFY(std::isfinite(extraOrder) && extraOrder >= 0.0 && - extraOrder <= - static_cast(std::numeric_limits::max()), - "The pressure EOS effective polynomial order is invalid."); - - return static_cast(std::ceil(extraOrder)); -} - -[[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(enthalpyElement.GetOrder() == - mean_field::field::Enthalpy::Scalar::familyOrder, - "The pressure-force enthalpy element does not match the " - "registered enthalpy field."); - - MFEM_VERIFY(displacementElement.GetOrder() == - mean_field::field::Displacement::Vector::familyOrder, - "The pressure-force test element does not match the " - "registered displacement field."); - - 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); - - const mean_field::quadrature::MfemRule rule = - f.quadratureFactory->get(query, transformation.GetGeometryType()); - - MFEM_VERIFY(rule.integration_rule != nullptr, - "The quadrature policy did not return a pressure-force " - "integration rule."); - - return *rule.integration_rule; -} - -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.enthalpyFes != nullptr, - "The pressure-force kernel requires the enthalpy " - "finite-element space."); - - MFEM_VERIFY(f.displacementFes != nullptr, - "The pressure-force kernel requires the displacement " - "finite-element space."); - - MFEM_VERIFY(f.compactificationFes != nullptr, - "The pressure-force kernel requires the compactification " - "finite-element space."); - - MFEM_VERIFY(f.compactificationCoordinate != nullptr, - "The pressure-force kernel requires the compactification " - "coordinate."); - - MFEM_VERIFY(f.quadratureFactory != nullptr, - "The pressure-force kernel requires the quadrature " - "rule factory."); - - MFEM_VERIFY(enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), - "The pressure-force enthalpy vector has the wrong size."); - - MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), - "The pressure-force displacement vector has the wrong size."); - - MFEM_VERIFY(domainMapper.GetDimension() == f.mesh->Dimension(), - "The pressure-force domain-mapper dimension does not match " - "the mesh dimension."); - - MFEM_VERIFY(f.displacementFes->GetVDim() == f.mesh->Dimension(), - "The displacement vector dimension does not match the " - "mesh dimension."); - - /* - * 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."); -} - -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."); - } - - mfem::Vector baseEnthalpyLocal; - mfem::Vector enthalpyVariationLocal; - mfem::Vector displacementLocal; - mfem::Vector displacementVariationLocal; - - true_to_local(*f.enthalpyFes, baseEnthalpyTrue, baseEnthalpyLocal); - - if (enthalpyVariationTrue != nullptr) { - true_to_local(*f.enthalpyFes, *enthalpyVariationTrue, - enthalpyVariationLocal); - } - - true_to_local(*f.displacementFes, displacementTrue, displacementLocal); - - if (displacementVariationTrue != nullptr) { - 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 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::Array enthalpyDofs; - - mfem::DenseMatrix displacementDShapeReference; - mfem::DenseMatrix displacementDShapePhysical; - mfem::DenseMatrix displacementDShapePhysicalVariation; - - mean_field::mapping::VolumeMappingContext mappingContext; - - 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 pressure-force kernel received a null element " - "transformation."); - - /* - * Skip vacuum before constructing or evaluating any mapping - * data for the element. - */ - if (is_vacuum_attribute(transformation->Attribute)) { - continue; + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); } - const mfem::FiniteElement &enthalpyElement = - *f.enthalpyFes->GetFE(elementId); + enum class PressureForceAction { residual, enthalpy, displacement }; - const mfem::FiniteElement &displacementElement = - *f.displacementFes->GetFE(elementId); + 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." + ); - const mfem::FiniteElement &compactificationElement = - *f.compactificationFes->GetFE(elementId); + localVector.SetSize(finiteElementSpace.GetVSize()); - mfem::DofTransformation *enthalpyDofTransformation = - f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); - 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); + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } } - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + 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 (displacementVariationTrue != nullptr) { - displacementVariationLocal.GetSubVector(displacementDofs, - elementDisplacementVariation); + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; + + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } } - f.compactificationCoordinate->GetSubVector(compactificationDofs, - elementCompactification); + [[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; + } - if (enthalpyDofTransformation != nullptr) { - enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy); + if (ordering == mfem::Ordering::byVDIM) { + return scalarDof * dimension + component; + } - if (enthalpyVariationTrue != nullptr) { - enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation); - } + MFEM_ABORT("The displacement space uses an unsupported ordering."); + return -1; } - 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); + [[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); 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)); + std::isfinite(extraOrder) && extraOrder >= 0.0 && + extraOrder <= static_cast(std::numeric_limits::max()), + "The pressure EOS effective polynomial order is invalid." + ); + + return static_cast(std::ceil(extraOrder)); + } + + [[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( + enthalpyElement.GetOrder() == mean_field::field::Enthalpy::Scalar::familyOrder, + "The pressure-force enthalpy element does not match the " + "registered enthalpy field." + ); + + MFEM_VERIFY( + displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder, + "The pressure-force test element does not match the " + "registered displacement 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 + ); + + const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); + + MFEM_VERIFY( + rule.integration_rule != nullptr, "The quadrature policy did not return a pressure-force " + "integration rule." + ); + + return *rule.integration_rule; + } + + 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.enthalpyFes != nullptr, "The pressure-force kernel requires the enthalpy " + "finite-element space." + ); + + MFEM_VERIFY( + f.displacementFes != nullptr, "The pressure-force kernel requires the displacement " + "finite-element space." + ); + + MFEM_VERIFY( + f.compactificationFes != nullptr, "The pressure-force kernel requires the compactification " + "finite-element space." + ); + + MFEM_VERIFY( + f.compactificationCoordinate != nullptr, "The pressure-force kernel requires the compactification " + "coordinate." + ); + + MFEM_VERIFY( + f.quadratureFactory != nullptr, "The pressure-force kernel requires the quadrature " + "rule factory." + ); + + MFEM_VERIFY( + enthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), + "The pressure-force enthalpy vector has the wrong size." + ); + + MFEM_VERIFY( + displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The pressure-force displacement vector has the wrong size." + ); + + MFEM_VERIFY( + domainMapper.GetDimension() == f.mesh->Dimension(), + "The pressure-force domain-mapper dimension does not match " + "the mesh dimension." + ); + + MFEM_VERIFY( + f.displacementFes->GetVDim() == f.mesh->Dimension(), "The displacement vector dimension does not match the " + "mesh dimension." + ); /* - * 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. + * ElementDisplacementDataFromElementVDofs currently consumes the + * registered byNODES layout. Keep this explicit so a future + * registry change fails immediately rather than silently + * corrupting the geometry. */ - mfem::Mult(displacementDShapeReference, - mappingVariation->inverse_element_jacobian_variation, - displacementDShapePhysicalVariation); - } + MFEM_VERIFY( + f.displacementFes->GetOrdering() == mfem::Ordering::byNODES, + "The pressure-force kernel requires the registered byNODES " + "displacement ordering." + ); + } - const double weightedPressureFactor = - pressureFactor * mappingContext.quadrature.weight; + 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); - 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 (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." + ); + } + + mfem::Vector baseEnthalpyLocal; + mfem::Vector enthalpyVariationLocal; + mfem::Vector displacementLocal; + mfem::Vector displacementVariationLocal; + + true_to_local(*f.enthalpyFes, baseEnthalpyTrue, baseEnthalpyLocal); + + if (enthalpyVariationTrue != nullptr) { + true_to_local(*f.enthalpyFes, *enthalpyVariationTrue, enthalpyVariationLocal); + } + + true_to_local(*f.displacementFes, displacementTrue, displacementLocal); + + if (displacementVariationTrue != nullptr) { + 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 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::Array enthalpyDofs; + + mfem::DenseMatrix displacementDShapeReference; + mfem::DenseMatrix displacementDShapePhysical; + mfem::DenseMatrix displacementDShapePhysicalVariation; + + mean_field::mapping::VolumeMappingContext mappingContext; + + 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 pressure-force kernel received a null element " + "transformation." + ); + + /* + * 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 = + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpyValue}) + .value(); + } else { + const double enthalpyVariationValue = elementEnthalpyVariation * enthalpyShape; + + pressureFactor = eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{enthalpyValue} + ) + .value() * + 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); } - - 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::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_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::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_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::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); -} + 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 0ecc65e..0eac3ff 100644 --- a/libmeanfield/impl/operators/kernels/rotation_displacement_force_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/rotation_displacement_force_kernels.cpp @@ -11,554 +11,585 @@ module mean_field; import :operators.kernels.rotational_displacement_force; namespace { -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -[[nodiscard]] bool is_vacuum_attribute(const int attribute) { - return DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(attribute); -} - -enum class RotationalDisplacementForceAction { - residual, - density, - displacement, - complete -}; - -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."); - - 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(), - "The rotational-displacement-force 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; - } -} - -[[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; - } - - if (ordering == mfem::Ordering::byVDIM) { - return scalarDof * dimension + component; - } - - MFEM_ABORT("The rotational-displacement-force test space uses an " - "unsupported ordering."); - - return -1; -} - -[[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_VERIFY(densityElement.GetOrder() == - mean_field::field::Density::Scalar::familyOrder, - "The rotational-displacement-force density element does not " - "match the registered density field."); - - MFEM_VERIFY(displacementElement.GetOrder() == - mean_field::field::Displacement::Vector::familyOrder, - "The rotational-displacement-force test element does not match " - "the registered displacement 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::field::Displacement::Form::CentrifugalForce>( - mean_field::quadrature::QuadratureRole::discretization, - transformation.OrderW(), std::array{1}, - 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 quadrature policy did not return a rotational-" - "displacement-force integration rule."); - - return *rule.integration_rule; -} - -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); - } -} - -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.mesh->Dimension() == 3, - "The rotational-displacement-force kernel requires a " - "three-dimensional mesh."); - - MFEM_VERIFY(f.densityFes != nullptr, - "The rotational-displacement-force kernel requires the density " - "finite-element space."); - - MFEM_VERIFY(f.displacementFes != nullptr, - "The rotational-displacement-force kernel requires the " - "displacement finite-element space."); - - MFEM_VERIFY(f.compactificationFes != nullptr && - f.compactificationCoordinate != nullptr, - "The rotational-displacement-force kernel requires the " - "compactification coordinate."); - - MFEM_VERIFY(f.quadratureFactory != nullptr, - "The rotational-displacement-force kernel requires the " - "quadrature-rule factory."); - - MFEM_VERIFY(displacementTrue.Size() == f.displacementFes->GetTrueVSize(), - "The rotational-displacement-force displacement vector has the " - "wrong size."); - - MFEM_VERIFY(domainMapper.GetDimension() == f.mesh->Dimension(), - "The rotational-displacement-force mapper dimension does not " - "match the mesh dimension."); - - MFEM_VERIFY(f.displacementFes->GetVDim() == f.mesh->Dimension(), - "The rotational-displacement-force displacement dimension does " - "not match the mesh dimension."); - - validate_finite_vector( - displacementTrue, - "The rotational-displacement-force displacement contains a " - "non-finite value."); -} - -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_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 needsBaseDensity = - requestedAction == RotationalDisplacementForceAction::residual || - requestedAction == RotationalDisplacementForceAction::displacement || - requestedAction == RotationalDisplacementForceAction::complete; - - const bool needsDensityVariation = - requestedAction == RotationalDisplacementForceAction::density || - requestedAction == RotationalDisplacementForceAction::complete; - - const bool needsDisplacementVariation = - requestedAction == RotationalDisplacementForceAction::displacement || - requestedAction == RotationalDisplacementForceAction::complete; - - if (needsBaseDensity) { - MFEM_VERIFY(baseDensityTrue != nullptr, - "The rotational-displacement-force action requires a base " - "density."); - - validate_density( - f, *baseDensityTrue, - "The rotational-displacement-force base density is invalid."); - } - - 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."); - - 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; - - 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; + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); } - const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId); + enum class RotationalDisplacementForceAction { residual, density, displacement, complete }; - const mfem::FiniteElement &displacementElement = - *f.displacementFes->GetFE(elementId); + 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." + ); - const mfem::FiniteElement &compactificationElement = - *f.compactificationFes->GetFE(elementId); + localVector.SetSize(finiteElementSpace.GetVSize()); - mfem::DofTransformation *densityDofTransformation = - f.densityFes->GetElementDofs(elementId, densityDofs); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); - mfem::DofTransformation *displacementDofTransformation = - f.displacementFes->GetElementVDofs(elementId, displacementDofs); - - mfem::DofTransformation *compactificationDofTransformation = - f.compactificationFes->GetElementDofs(elementId, compactificationDofs); - - if (needsBaseDensity) { - baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity); + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } } - if (needsDensityVariation) { - densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation); + 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." + ); + + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; + + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } } - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + [[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; + } - if (needsDisplacementVariation) { - displacementVariationLocal.GetSubVector(displacementDofs, - elementDisplacementVariation); + if (ordering == mfem::Ordering::byVDIM) { + return scalarDof * dimension + component; + } + + MFEM_ABORT( + "The rotational-displacement-force test space uses an " + "unsupported ordering." + ); + + return -1; } - 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); + [[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_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.GetOrder() == mean_field::field::Density::Scalar::familyOrder, + "The rotational-displacement-force density element does not " + "match the registered density field." + ); - densityElement.CalcShape(integrationPoint, densityShape); + MFEM_VERIFY( + displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder, + "The rotational-displacement-force test element does not match " + "the registered displacement field." + ); - displacementElement.CalcShape(integrationPoint, displacementShape); + /* + * 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 + ); - double baseDensityValue = 0.0; - double densityVariationValue = 0.0; + const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); - if (needsBaseDensity) { - baseDensityValue = elementBaseDensity * densityShape; - } + MFEM_VERIFY( + rule.integration_rule != nullptr, "The quadrature policy did not return a rotational-" + "displacement-force integration rule." + ); - if (needsDensityVariation) { - densityVariationValue = elementDensityVariation * densityShape; - } + return *rule.integration_rule; + } - rotation.potential_gradient(mappingContext.mapping.physical_position, - potentialGradient); + 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); + } + } - centrifugalAcceleration = potentialGradient; - centrifugalAcceleration *= -1.0; + 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."); - if (needsDisplacementVariation) { - rotation.potential_gradient_directional_derivative( - mappingVariation.mapping.physical_position_variation, - potentialGradientVariation); + MFEM_VERIFY( + f.mesh->Dimension() == 3, "The rotational-displacement-force kernel requires a " + "three-dimensional mesh." + ); - centrifugalAccelerationVariation = potentialGradientVariation; + MFEM_VERIFY( + f.densityFes != nullptr, "The rotational-displacement-force kernel requires the density " + "finite-element space." + ); - centrifugalAccelerationVariation *= -1.0; - } else { - centrifugalAccelerationVariation = 0.0; - } + MFEM_VERIFY( + f.displacementFes != nullptr, "The rotational-displacement-force kernel requires the " + "displacement finite-element space." + ); - weightedForce = 0.0; + MFEM_VERIFY( + f.compactificationFes != nullptr && f.compactificationCoordinate != nullptr, + "The rotational-displacement-force kernel requires the " + "compactification coordinate." + ); + + MFEM_VERIFY( + f.quadratureFactory != nullptr, "The rotational-displacement-force kernel requires the " + "quadrature-rule factory." + ); + + MFEM_VERIFY( + displacementTrue.Size() == f.displacementFes->GetTrueVSize(), + "The rotational-displacement-force displacement vector has the " + "wrong size." + ); + + MFEM_VERIFY( + domainMapper.GetDimension() == f.mesh->Dimension(), + "The rotational-displacement-force mapper dimension does not " + "match the mesh dimension." + ); + + MFEM_VERIFY( + f.displacementFes->GetVDim() == f.mesh->Dimension(), + "The rotational-displacement-force displacement dimension does " + "not match the mesh dimension." + ); + + validate_finite_vector( + displacementTrue, "The rotational-displacement-force displacement contains a " + "non-finite value." + ); + } + + 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_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 needsBaseDensity = requestedAction == RotationalDisplacementForceAction::residual || + requestedAction == RotationalDisplacementForceAction::displacement || + requestedAction == RotationalDisplacementForceAction::complete; + + const bool needsDensityVariation = requestedAction == RotationalDisplacementForceAction::density || + requestedAction == RotationalDisplacementForceAction::complete; + + const bool needsDisplacementVariation = requestedAction == RotationalDisplacementForceAction::displacement || + requestedAction == RotationalDisplacementForceAction::complete; + + if (needsBaseDensity) { + MFEM_VERIFY( + baseDensityTrue != nullptr, "The rotational-displacement-force action requires a base " + "density." + ); + + validate_density(f, *baseDensityTrue, "The rotational-displacement-force base density is invalid."); + } - if (requestedAction == RotationalDisplacementForceAction::residual) { - weightedForce.Add(baseDensityValue * mappingContext.quadrature.weight, - centrifugalAcceleration); - } else { if (needsDensityVariation) { - weightedForce.Add(densityVariationValue * - mappingContext.quadrature.weight, - centrifugalAcceleration); + 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) { - weightedForce.Add(baseDensityValue * mappingContext.quadrature.weight, - centrifugalAccelerationVariation); + MFEM_VERIFY( + displacementVariationTrue != nullptr && + displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(), + "The rotational-displacement-force displacement variation " + "is invalid." + ); - weightedForce.Add(baseDensityValue * - mappingVariation.weight_variation, - centrifugalAcceleration); + validate_finite_vector( + *displacementVariationTrue, "The rotational-displacement-force displacement variation " + "contains a non-finite value." + ); } - } - 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); + mfem::Vector baseDensityLocal; + mfem::Vector densityVariationLocal; + mfem::Vector displacementLocal; + mfem::Vector displacementVariationLocal; - 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 (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) { + 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); + } + + 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::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_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::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_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::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_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::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); -} + 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 053c1b0..391b7e5 100644 --- a/libmeanfield/impl/operators/prepared_barotropic_closure.cpp +++ b/libmeanfield/impl/operators/prepared_barotropic_closure.cpp @@ -398,11 +398,17 @@ namespace mean_field::operators { data.enthalpyBasis(quadraturePoint, enthalpyDof) = enthalpyShape(enthalpyDof); } - const double density = elementBaseDensity * densityShape; - const double enthalpy = elementBaseEnthalpy * enthalpyShape; - const double quadratureWeight = mappingContext.quadrature.weight; - const double eosDensity = m_equationOfState.density_from_enthalpy(enthalpy); - const double enthalpyDerivative = m_equationOfState.density_derivative_from_enthalpy(enthalpy); + const double density = elementBaseDensity * densityShape; + const double enthalpy = elementBaseEnthalpy * enthalpyShape; + const double quadratureWeight = mappingContext.quadrature.weight; + const eos::SpecificEnthalpyValue specificEnthalpy{enthalpy}; + const double eosDensity = + eos::evaluate(m_equationOfState, specificEnthalpy).value(); + const double enthalpyDerivative = + eos::partialDerivative( + m_equationOfState, specificEnthalpy + ) + .value(); MFEM_VERIFY( std::isfinite(quadratureWeight) && quadratureWeight > 0.0 && std::isfinite(eosDensity) && diff --git a/libmeanfield/impl/operators/prepared_gravity_source.cpp b/libmeanfield/impl/operators/prepared_gravity_source.cpp index 11c13ef..c4279b7 100644 --- a/libmeanfield/impl/operators/prepared_gravity_source.cpp +++ b/libmeanfield/impl/operators/prepared_gravity_source.cpp @@ -9,531 +9,535 @@ 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::field::Gravity::Form::SourceProjection>( - 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::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::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); - } + 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 (DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(transformation.Attribute)) { - return 0.0; - } + "ID." + ); + if (DomainSchema::template attribute_belongs_to( + 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::DomainMapper &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::DomainMapper::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::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."); + 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< + 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." + ); - 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; + m_stellar_marker = utils::domain::make_attribute_marker(*f.mesh); } - m_elements.emplace_back(); - ElementPAData &data = m_elements.back(); + 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." + ); - data.element_id = element_id; + for (int i = 0; i < displacement.Size(); ++i) { + MFEM_VERIFY( + std::isfinite(displacement(i)), "PreparedMappedGravitySourceOperator received a non-finite " + "displacement value." + ); + } - data.density_dof_transformation = - m_fem.densityFes->GetElementDofs(element_id, data.density_dofs); + 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.potential_dof_transformation = - m_fem.gravityPotentialFes->GetElementDofs(element_id, - data.potential_dofs); + FrozenMappedGravitySourceCoefficient source_coefficient(m_fem, m_domain_mapper, m_displacement_true); - const mfem::FiniteElement &density_element = - *m_fem.densityFes->GetFE(element_id); + 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 &potential_element = - *m_fem.gravityPotentialFes->GetFE(element_id); + if (attribute <= 0 || attribute > m_stellar_marker.Size() || m_stellar_marker[attribute - 1] == 0) { + continue; + } - mfem::ElementTransformation &transformation = - *m_fem.mesh->GetElementTransformation(element_id); + m_elements.emplace_back(); + ElementPAData &data = m_elements.back(); - const mfem::IntegrationRule &integration_rule = get_source_rule( - m_fem, density_element, potential_element, transformation); + data.element_id = element_id; - const int quadrature_point_count = integration_rule.GetNPoints(); + data.density_dof_transformation = m_fem.densityFes->GetElementDofs(element_id, data.density_dofs); - const int density_dof_count = density_element.GetDof(); + data.potential_dof_transformation = + m_fem.gravityPotentialFes->GetElementDofs(element_id, data.potential_dofs); - const int potential_dof_count = potential_element.GetDof(); + const mfem::FiniteElement &density_element = *m_fem.densityFes->GetFE(element_id); - data.density_basis.SetSize(quadrature_point_count, density_dof_count); + const mfem::FiniteElement &potential_element = *m_fem.gravityPotentialFes->GetFE(element_id); - data.potential_basis.SetSize(quadrature_point_count, potential_dof_count); + mfem::ElementTransformation &transformation = *m_fem.mesh->GetElementTransformation(element_id); - data.quadrature_data.SetSize(quadrature_point_count); + const mfem::IntegrationRule &integration_rule = + get_source_rule(m_fem, density_element, potential_element, transformation); - mfem::Vector density_shape(density_dof_count); - mfem::Vector potential_shape(potential_dof_count); + const int quadrature_point_count = integration_rule.GetNPoints(); - for (int quadrature_point = 0; quadrature_point < quadrature_point_count; - ++quadrature_point) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(quadrature_point); + const int density_dof_count = density_element.GetDof(); - transformation.SetIntPoint(&integration_point); + const int potential_dof_count = potential_element.GetDof(); - // CalcPhysShape matches the scalar mixed-mass discretization, - // including the finite-element map type. - density_element.CalcPhysShape(transformation, density_shape); + data.density_basis.SetSize(quadrature_point_count, density_dof_count); - potential_element.CalcPhysShape(transformation, potential_shape); + data.potential_basis.SetSize(quadrature_point_count, potential_dof_count); - for (int i = 0; i < density_dof_count; ++i) { - data.density_basis(quadrature_point, i) = density_shape(i); - } + data.quadrature_data.SetSize(quadrature_point_count); - for (int i = 0; i < potential_dof_count; ++i) { - data.potential_basis(quadrature_point, i) = potential_shape(i); - } + mfem::Vector density_shape(density_dof_count); + mfem::Vector potential_shape(potential_dof_count); - const double coefficient_value = - source_coefficient.Eval(transformation, integration_point); + for (int quadrature_point = 0; quadrature_point < quadrature_point_count; ++quadrature_point) { + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(quadrature_point); - transformation.SetIntPoint(&integration_point); + transformation.SetIntPoint(&integration_point); - const double quadrature_value = integration_point.weight * - transformation.Weight() * - coefficient_value; + // CalcPhysShape matches the scalar mixed-mass discretization, + // including the finite-element map type. + density_element.CalcPhysShape(transformation, density_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 - << "."); + potential_element.CalcPhysShape(transformation, potential_shape); - data.quadrature_data(quadrature_point) = quadrature_value; + 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; } - } + 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(!m_elements.empty(), - "PreparedMappedGravitySourceOperator found no stellar elements."); + MFEM_VERIFY( + density.Size() == Width(), "PreparedMappedGravitySourceOperator received a density vector " + "with the wrong size." + ); - 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."); + m_density_true.SetSize(m_density_map.full_size()); + m_density_map.scatter(density, m_density_true); - MFEM_VERIFY(density.Size() == Width(), - "PreparedMappedGravitySourceOperator received a density vector " - "with the wrong size."); + mfem::Vector density_local; - m_density_true.SetSize(m_density_map.full_size()); - m_density_map.scatter(density, m_density_true); + true_to_local(*m_fem.densityFes, m_density_true, density_local); - mfem::Vector density_local; + mfem::Vector local_action(m_fem.gravityPotentialFes->GetVSize()); + local_action = 0.0; - true_to_local(*m_fem.densityFes, m_density_true, density_local); + mfem::Vector element_density; + mfem::Vector quadrature_density; + mfem::Vector element_action; - mfem::Vector local_action(m_fem.gravityPotentialFes->GetVSize()); - local_action = 0.0; + for (const ElementPAData &data : m_elements) { + density_local.GetSubVector(data.density_dofs, element_density); - mfem::Vector element_density; - mfem::Vector quadrature_density; - mfem::Vector element_action; + if (data.density_dof_transformation != nullptr) { + data.density_dof_transformation->InvTransformPrimal(element_density); + } - for (const ElementPAData &data : m_elements) { - density_local.GetSubVector(data.density_dofs, element_density); + quadrature_density.SetSize(data.quadrature_data.Size()); - if (data.density_dof_transformation != nullptr) { - data.density_dof_transformation->InvTransformPrimal(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); } - quadrature_density.SetSize(data.quadrature_data.Size()); + void PreparedMappedGravitySourceOperator::MultTranspose( + const mfem::Vector &potential, + mfem::Vector &action + ) const { + MFEM_VERIFY( + m_is_prepared, "PreparedMappedGravitySourceOperator must be prepared before " + "MultTranspose is called." + ); - // B_density * x_e - data.density_basis.Mult(element_density, quadrature_density); + MFEM_VERIFY( + potential.Size() == Height(), "PreparedMappedGravitySourceOperator received a potential vector " + "with the wrong size." + ); - // D * B_density * x_e - for (int q = 0; q < quadrature_density.Size(); ++q) { - quadrature_density(q) *= data.quadrature_data(q); + 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; } - 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); + std::uint64_t PreparedMappedGravitySourceOperator::GetPreparationCount() const noexcept { + return m_preparation_count; } - 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::GetDensityMap() const noexcept { + return m_density_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::GetPotentialMap() const noexcept { + return m_potential_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); + const field::FieldDofMap &PreparedMappedGravitySourceOperator::GetDisplacementMap() const noexcept { + return m_displacement_map; } - - 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 444d923..db655cb 100644 --- a/libmeanfield/impl/operators/prepared_hdiv_mass.cpp +++ b/libmeanfield/impl/operators/prepared_hdiv_mass.cpp @@ -8,405 +8,413 @@ 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(); -} - -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; + 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(); } - const mfem::FiniteElement &element = *f.gravityFluxFes->GetFE(element_id); - const mfem::ElementTransformation &transformation = - *f.mesh->GetElementTransformation(element_id); + 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()); - MFEM_VERIFY(element.GetGeomType() == representative_element.GetGeomType(), + 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(), "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."); - } -} - -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; + "geometry-weight order." + ); + } } - LoadElement(element_id); + 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); + } - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = *m_displacement_data, - .compactification = *m_compactification_data}; + void Eval( + mfem::DenseMatrix &mass_tensor, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point + ) override { + transformation.SetIntPoint(&integration_point); - mean_field::mapping::VolumeMappingContext mapping_context; + 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 mean_field::mapping::MappingStatus status = - m_domain_mapper.EvaluateVolume(mapping_data, transformation, - integration_point, m_workspace, - mapping_context); + const bool element_is_vacuum = + DomainSchema::template attribute_belongs_to( + transformation.Attribute + ); - MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, + 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, "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::DomainMapper &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::DomainMapper::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::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."); + 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< + 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." + ); - m_stellar_marker = - utils::domain::make_attribute_marker(*f.mesh); - m_vacuum_marker = - utils::domain::make_attribute_marker( - *f.mesh); + 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_stellar_mass_form.reset(); - m_vacuum_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_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); + 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_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_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_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."); + 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_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); -} + 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); + } -void PreparedMappedHDivMassOperator::AssembleDiagonal( - mfem::Vector &diagonal) const { - mfem::Vector true_diagonal; - AssembleTrueDiagonal(true_diagonal); - diagonal.SetSize(Height()); - m_flux_map.gather(true_diagonal, diagonal); -} + void PreparedMappedHDivMassOperator::AssembleDiagonal(mfem::Vector &diagonal) const { + mfem::Vector true_diagonal; + AssembleTrueDiagonal(true_diagonal); + diagonal.SetSize(Height()); + m_flux_map.gather(true_diagonal, diagonal); + } -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."); + 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." + ); - 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; -} + 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; + } -bool PreparedMappedHDivMassOperator::IsPrepared() const noexcept { - return m_is_prepared; -} + bool PreparedMappedHDivMassOperator::IsPrepared() const noexcept { + return m_is_prepared; + } -std::uint64_t -PreparedMappedHDivMassOperator::GetPreparationCount() const noexcept { - return m_preparation_count; -} + 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::GetFluxMap() const noexcept { + return m_flux_map; + } -const field::FieldDofMap & -PreparedMappedHDivMassOperator::GetDisplacementMap() const noexcept { - return m_displacement_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 2a92d09..473be01 100644 --- a/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp @@ -12,1268 +12,1237 @@ module mean_field; import :operators.prepared_hydrostatic_equilibrium; namespace { -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -[[nodiscard]] bool is_vacuum_attribute(const int attribute) { - return DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(attribute); -} + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); + } -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."); + 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." + ); - localVector.SetSize(finiteElementSpace.GetVSize()); + localVector.SetSize(finiteElementSpace.GetVSize()); - const mfem::Operator *prolongation = - finiteElementSpace.GetProlongationMatrix(); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } -} + 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(), - "Hydrostatic local vector has the wrong size."); + 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." + ); - trueVector.SetSize(finiteElementSpace.GetTrueVSize()); - trueVector = 0.0; + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; - const mfem::Operator *prolongation = - finiteElementSpace.GetProlongationMatrix(); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->MultTranspose(localVector, trueVector); - } else { - trueVector = localVector; - } -} + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } + } -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."); + 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." + ); - block.SetSize(size); + block.SetSize(size); - for (int entry = 0; entry < size; ++entry) { - block(entry) = source(offset + entry); - } -} + for (int entry = 0; entry < size; ++entry) { + block(entry) = source(offset + entry); + } + } -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; + 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(enthalpyElement.GetOrder() == - mean_field::field::Enthalpy::Scalar::familyOrder, - "The prepared hydrostatic enthalpy 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." + ); - MFEM_VERIFY(gravityPotentialElement.GetOrder() == - mean_field::field::Gravity::Potential::familyOrder, - "The prepared hydrostatic potential element does " - "not match the registered field."); + MFEM_VERIFY( + gravityPotentialElement.GetOrder() == mean_field::field::Gravity::Potential::familyOrder, + "The prepared hydrostatic potential element does " + "not match the registered 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 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::field::Enthalpy::Form::EquilibriumGravity>( - 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 + ); - // 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); + // 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 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 constantQuery = EnthalpyField::make_query( + mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, + 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 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 selectedRule = - std::max_element(candidateRules.begin(), candidateRules.end(), - [](const auto &left, const auto &right) { - return left.resolution.order < right.resolution.order; - }); + const auto selectedRule = + std::max_element(candidateRules.begin(), candidateRules.end(), [](const auto &left, const auto &right) { + return left.resolution.order < right.resolution.order; + }); - MFEM_VERIFY(selectedRule != candidateRules.end() && - selectedRule->integration_rule != nullptr, - "The quadrature policy did not return a valid " - "hydrostatic-equilibrium 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; -} + 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."); -} - -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; + MFEM_VERIFY( + m_enthalpySize > 0 && m_gravityPotentialSize > 0 && m_displacementSize > 0, + "HydrostaticJacobianBlockLayout received an empty " + "finite-element space." + ); } - const mfem::FiniteElement &enthalpyElement = - *m_fem.enthalpyFes->GetFE(elementId); + int HydrostaticJacobianBlockLayout::Offset(const HydrostaticJacobianInputBlock block) const { + switch (block) { + case HydrostaticJacobianInputBlock::enthalpy: + return 0; - const mfem::FiniteElement &gravityPotentialElement = - *m_fem.gravityPotentialFes->GetFE(elementId); + case HydrostaticJacobianInputBlock::gravityPotential: + return m_enthalpySize; - MFEM_VERIFY(enthalpyElement.GetGeomType() == - gravityPotentialElement.GetGeomType() && - enthalpyElement.GetGeomType() == - transformation->GetGeometryType(), - "Hydrostatic element geometries do not agree."); + case HydrostaticJacobianInputBlock::bernoulliConstant: + return m_enthalpySize + m_gravityPotentialSize; - m_elements.emplace_back(); - ElementPAData &data = m_elements.back(); - data.elementId = elementId; + case HydrostaticJacobianInputBlock::displacement: + return m_enthalpySize + m_gravityPotentialSize + 1; + } - data.enthalpyDofTransformation = - m_fem.enthalpyFes->GetElementDofs(elementId, data.enthalpyDofs); + MFEM_ABORT( + "HydrostaticJacobianBlockLayout received an " + "unknown input block." + ); - 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); - } + return 0; } - } -} -void PreparedHydrostaticEquilibriumOperator::PrepareGeometry() { - mfem::Vector displacementLocal; + int HydrostaticJacobianBlockLayout::Size(const HydrostaticJacobianInputBlock block) const { + switch (block) { + case HydrostaticJacobianInputBlock::enthalpy: + return m_enthalpySize; - true_to_local(*m_fem.displacementFes, m_context.GetDisplacementTrue(), - displacementLocal); + case HydrostaticJacobianInputBlock::gravityPotential: + return m_gravityPotentialSize; - mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); + case HydrostaticJacobianInputBlock::bernoulliConstant: + return 1; - mfem::Array compactificationDofs; + case HydrostaticJacobianInputBlock::displacement: + return m_displacementSize; + } - mfem::Vector elementDisplacement; - mfem::Vector elementCompactification; + MFEM_ABORT( + "HydrostaticJacobianBlockLayout received an " + "unknown input block." + ); - for (ElementPAData &data : m_elements) { - mfem::ElementTransformation *transformation = - m_fem.mesh->GetElementTransformation(data.elementId); + return 0; + } - MFEM_VERIFY(transformation != nullptr && data.integrationRule != nullptr, + 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; + } + + 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::DomainMapper::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, "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 (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(elementDisplacement); + } - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal( - elementCompactification); - } + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal(elementCompactification); + } - const mfem::FiniteElement &displacementElement = - *m_fem.displacementFes->GetFE(data.elementId); + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); - const mfem::FiniteElement &compactificationElement = - *m_fem.compactificationFes->GetFE(data.elementId); + const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); - data.baseDisplacementData.emplace( - mapping::ElementDisplacementDataFromElementVDofs(displacementElement, - elementDisplacement)); + data.baseDisplacementData.emplace( + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement) + ); - data.compactificationData.emplace(compactificationElement, - elementCompactification); + data.compactificationData.emplace(compactificationElement, elementCompactification); - 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(); - data.physicalPositions.SetSize(quadraturePointCount, - m_fem.mesh->Dimension()); + data.physicalPositions.SetSize(quadraturePointCount, m_fem.mesh->Dimension()); - data.quadratureWeights.SetSize(quadraturePointCount); + data.quadratureWeights.SetSize(quadraturePointCount); - data.baseMappingContexts.resize(quadraturePointCount); + data.baseMappingContexts.resize(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::VolumeMappingContext &mappingContext = - data.baseMappingContexts[quadraturePoint]; + mapping::VolumeMappingContext &mappingContext = data.baseMappingContexts[quadraturePoint]; - const mapping::MappingStatus mappingStatus = - m_domainMapper.EvaluateVolume(mappingData, *transformation, - integrationPoint, workspace, - mappingContext); + 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)); + 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; + 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), + MFEM_VERIFY( + std::isfinite(quadratureWeight) && quadratureWeight > 0.0, "Prepared hydrostatic geometry encountered " - "a non-finite physical position."); + "an invalid quadrature weight." + ); - data.physicalPositions(quadraturePoint, component) = position; - } + 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(); + void PreparedHydrostaticEquilibriumOperator::PrepareAlgebraicJacobianBlocks() { + for (ElementPAData &data : m_elements) { + const int quadraturePointCount = data.quadratureWeights.Size(); - const int enthalpyDofCount = data.enthalpyBasis.Width(); + const int enthalpyDofCount = data.enthalpyBasis.Width(); - const int gravityPotentialDofCount = data.gravityPotentialBasis.Width(); + const int gravityPotentialDofCount = data.gravityPotentialBasis.Width(); - MFEM_VERIFY(data.enthalpyBasis.Height() == quadraturePointCount && + 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 < 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 < 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; + } + } } - } - } - } - - ++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); } - if (data.gravityPotentialDofTransformation != nullptr) { - data.gravityPotentialDofTransformation->InvTransformPrimal( - elementGravityPotential); + 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; + } + } } - const int quadraturePointCount = data.quadratureWeights.Size(); + void PreparedHydrostaticEquilibriumOperator::FinalizeDisplacementJacobianPreparation() { + const int dimension = m_fem.mesh->Dimension(); - quadratureEnthalpy.SetSize(quadraturePointCount); + for (const ElementPAData &data : m_elements) { + const int quadraturePointCount = data.quadratureWeights.Size(); - 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 && + 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; -} - -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); + ++m_displacementJacobianStatistics.preparations; } - localResidual.AddElementVector(data.enthalpyDofs, elementResidual); - } + void PreparedHydrostaticEquilibriumOperator::AssembleCachedResidual() { + mfem::Vector localResidual(m_fem.enthalpyFes->GetVSize()); - local_to_true(*m_fem.enthalpyFes, localResidual, m_fullEnthalpyAction); + localResidual = 0.0; + mfem::Vector elementResidual; - m_cachedResidual.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, m_cachedResidual); -} + for (const ElementPAData &data : m_elements) { + elementResidual.SetSize(data.enthalpyDofs.Size()); -void PreparedHydrostaticEquilibriumOperator::BuildResidual( - mfem::Vector &residual) const { - VerifyPrepared(); - residual = m_cachedResidual; - ++m_residualApplicationCount; -} + data.enthalpyBasis.MultTranspose(data.weightedResidual, elementResidual); -void PreparedHydrostaticEquilibriumOperator::ApplyEnthalpyJacobianAction( - const mfem::Vector &enthalpyVariation, mfem::Vector &action) const { - VerifyPrepared(); + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->TransformDual(elementResidual); + } - MFEM_VERIFY( - enthalpyVariation.Size() == m_context.GetEnthalpyMap().reduced_size(), - "Prepared hydrostatic enthalpy variation has the wrong supported size."); + localResidual.AddElementVector(data.enthalpyDofs, elementResidual); + } - m_context.GetEnthalpyMap().scatter(enthalpyVariation, - m_enthalpyVariationTrue); + local_to_true(*m_fem.enthalpyFes, localResidual, m_fullEnthalpyAction); - 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); + m_cachedResidual.SetSize(m_context.GetEnthalpyMap().reduced_size()); + m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, m_cachedResidual); } - elementAction.SetSize(data.enthalpyJacobian.Height()); - - data.enthalpyJacobian.Mult(elementVariation, elementAction); - - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementAction); + void PreparedHydrostaticEquilibriumOperator::BuildResidual(mfem::Vector &residual) const { + VerifyPrepared(); + residual = m_cachedResidual; + ++m_residualApplicationCount; } - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } + void PreparedHydrostaticEquilibriumOperator::ApplyEnthalpyJacobianAction( + const mfem::Vector &enthalpyVariation, + mfem::Vector &action + ) const { + VerifyPrepared(); - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); + MFEM_VERIFY( + enthalpyVariation.Size() == m_context.GetEnthalpyMap().reduced_size(), + "Prepared hydrostatic enthalpy variation has the wrong supported size." + ); - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); + m_context.GetEnthalpyMap().scatter(enthalpyVariation, m_enthalpyVariationTrue); - ++m_algebraicJacobianStatistics.enthalpyApplications; -} + mfem::Vector enthalpyVariationLocal; -void PreparedHydrostaticEquilibriumOperator:: - ApplyGravityPotentialJacobianAction( + 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( 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); + 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; } - elementAction.SetSize(data.gravityPotentialJacobian.Height()); + void PreparedHydrostaticEquilibriumOperator::ApplyBernoulliConstantJacobianAction( + const double bernoulliConstantVariation, + mfem::Vector &action + ) const { + VerifyPrepared(); - data.gravityPotentialJacobian.Mult(elementVariation, elementAction); + MFEM_VERIFY( + std::isfinite(bernoulliConstantVariation), "Prepared hydrostatic Bernoulli-constant Jacobian " + "received a non-finite variation." + ); - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(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; } - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } + void PreparedHydrostaticEquilibriumOperator::ApplyAlgebraicJacobianAction( + const mfem::Vector &enthalpyVariation, + const mfem::Vector &gravityPotentialVariation, + const double bernoulliConstantVariation, + mfem::Vector &action + ) const { + VerifyPrepared(); - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); + MFEM_VERIFY( + std::isfinite(bernoulliConstantVariation), "Prepared hydrostatic algebraic Jacobian received " + "a non-finite Bernoulli-constant variation." + ); - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); + MFEM_VERIFY( + enthalpyVariation.Size() == m_context.GetEnthalpyMap().reduced_size(), + "Prepared hydrostatic algebraic enthalpy variation has the wrong " + "supported size." + ); - ++m_algebraicJacobianStatistics.gravityPotentialApplications; -} + MFEM_VERIFY( + gravityPotentialVariation.Size() == m_context.GetGravityPotentialMap().reduced_size(), + "Prepared hydrostatic algebraic gravity-potential variation has " + "the wrong supported size." + ); -void PreparedHydrostaticEquilibriumOperator:: - ApplyBernoulliConstantJacobianAction( - const double bernoulliConstantVariation, mfem::Vector &action) const { - VerifyPrepared(); + m_context.GetEnthalpyMap().scatter(enthalpyVariation, m_enthalpyVariationTrue); + m_context.GetGravityPotentialMap().scatter(gravityPotentialVariation, m_gravityPotentialVariationTrue); - MFEM_VERIFY(std::isfinite(bernoulliConstantVariation), - "Prepared hydrostatic Bernoulli-constant Jacobian " - "received a non-finite variation."); + mfem::Vector enthalpyVariationLocal; + mfem::Vector gravityPotentialVariationLocal; - mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); + true_to_local(*m_fem.enthalpyFes, m_enthalpyVariationTrue, enthalpyVariationLocal); - localAction = 0.0; - mfem::Vector elementAction; + true_to_local(*m_fem.gravityPotentialFes, m_gravityPotentialVariationTrue, gravityPotentialVariationLocal); - for (const ElementPAData &data : m_elements) { - elementAction = data.bernoulliConstantJacobian; - elementAction *= bernoulliConstantVariation; + mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(elementAction); - } + localAction = 0.0; - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } + mfem::Vector elementEnthalpyVariation; + mfem::Vector elementGravityPotentialVariation; + mfem::Vector elementAction; + mfem::Vector elementWorkspace; - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); + for (const ElementPAData &data : m_elements) { + enthalpyVariationLocal.GetSubVector(data.enthalpyDofs, elementEnthalpyVariation); - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); + gravityPotentialVariationLocal.GetSubVector(data.gravityPotentialDofs, elementGravityPotentialVariation); - ++m_algebraicJacobianStatistics.bernoulliConstantApplications; -} + if (data.enthalpyDofTransformation != nullptr) { + data.enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation); + } -void PreparedHydrostaticEquilibriumOperator::ApplyAlgebraicJacobianAction( - const mfem::Vector &enthalpyVariation, - const mfem::Vector &gravityPotentialVariation, - const double bernoulliConstantVariation, mfem::Vector &action) const { - VerifyPrepared(); + if (data.gravityPotentialDofTransformation != nullptr) { + data.gravityPotentialDofTransformation->InvTransformPrimal(elementGravityPotentialVariation); + } - 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(), + 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); + 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; } - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } + void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianAction( + const mfem::Vector &displacementVariation, + mfem::Vector &action + ) const { + VerifyPrepared(); - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); + MFEM_VERIFY( + displacementVariation.Size() == m_context.GetDisplacementMap().reduced_size(), + "Prepared hydrostatic displacement variation has the wrong " + "supported size." + ); - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); + m_context.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue); - ++m_algebraicJacobianStatistics.combinedApplications; -} + mfem::Vector displacementVariationLocal; -void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianAction( - const mfem::Vector &displacementVariation, mfem::Vector &action) const { - VerifyPrepared(); + true_to_local(*m_fem.displacementFes, m_displacementVariationTrue, displacementVariationLocal); - MFEM_VERIFY(displacementVariation.Size() == - m_context.GetDisplacementMap().reduced_size(), - "Prepared hydrostatic displacement variation has the wrong " - "supported size."); + mfem::Vector localAction(m_fem.enthalpyFes->GetVSize()); - m_context.GetDisplacementMap().scatter(displacementVariation, - m_displacementVariationTrue); + localAction = 0.0; - mfem::Vector displacementVariationLocal; + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); - true_to_local(*m_fem.displacementFes, m_displacementVariationTrue, - displacementVariationLocal); + mfem::Vector elementDisplacementVariation; + mfem::Vector weightedQuadratureVariation; + mfem::Vector elementAction; - 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() && + 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; + 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; } - elementAction.SetSize(data.enthalpyDofs.Size()); + void PreparedHydrostaticEquilibriumOperator::ApplyCompleteJacobianAction( + const mfem::Vector &enthalpyVariation, + const mfem::Vector &gravityPotentialVariation, + const double bernoulliConstantVariation, + const mfem::Vector &displacementVariation, + mfem::Vector &action + ) const { + VerifyPrepared(); - data.enthalpyBasis.MultTranspose(weightedQuadratureVariation, - elementAction); + mfem::Vector displacementAction; - if (data.enthalpyDofTransformation != nullptr) { - data.enthalpyDofTransformation->TransformDual(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; } - localAction.AddElementVector(data.enthalpyDofs, elementAction); - } + bool PreparedHydrostaticEquilibriumOperator::IsPrepared() const noexcept { + return m_isPrepared; + } - local_to_true(*m_fem.enthalpyFes, localAction, m_fullEnthalpyAction); + const context::hydrostatic::HydrostaticPreparationStatistics & + PreparedHydrostaticEquilibriumOperator::GetContextPreparationStatistics() const noexcept { + return m_context.GetPreparationStatistics(); + } - action.SetSize(m_context.GetEnthalpyMap().reduced_size()); - m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action); + std::uint64_t PreparedHydrostaticEquilibriumOperator::GetResidualPreparationCount() const noexcept { + return m_residualPreparationCount; + } - ++m_displacementJacobianStatistics.applications; -} + std::uint64_t PreparedHydrostaticEquilibriumOperator::GetResidualApplicationCount() const noexcept { + return m_residualApplicationCount; + } -void PreparedHydrostaticEquilibriumOperator::ApplyCompleteJacobianAction( - const mfem::Vector &enthalpyVariation, - const mfem::Vector &gravityPotentialVariation, - const double bernoulliConstantVariation, - const mfem::Vector &displacementVariation, mfem::Vector &action) const { - VerifyPrepared(); + const PreparedHydrostaticAlgebraicJacobianStatistics & + PreparedHydrostaticEquilibriumOperator::GetAlgebraicJacobianStatistics() const noexcept { + return m_algebraicJacobianStatistics; + } - mfem::Vector displacementAction; + const PreparedHydrostaticDisplacementJacobianStatistics & + PreparedHydrostaticEquilibriumOperator::GetDisplacementJacobianStatistics() const noexcept { + return m_displacementJacobianStatistics; + } - ApplyAlgebraicJacobianAction(enthalpyVariation, gravityPotentialVariation, - bernoulliConstantVariation, action); + const PreparedHydrostaticCompleteJacobianStatistics & + PreparedHydrostaticEquilibriumOperator::GetCompleteJacobianStatistics() const noexcept { + return m_completeJacobianStatistics; + } - ApplyDisplacementJacobianAction(displacementVariation, displacementAction); + std::size_t PreparedHydrostaticEquilibriumOperator::GetStellarElementCount() const noexcept { + return m_elements.size(); + } - MFEM_VERIFY(action.Size() == displacementAction.Size(), - "Prepared hydrostatic complete Jacobian produced " - "incompatible algebraic and displacement actions."); + const fem::FEM &PreparedHydrostaticEquilibriumOperator::GetFEM() const noexcept { + return m_fem; + } - action += displacementAction; - ++m_completeJacobianStatistics.applications; -} + const field::FieldDofMap &PreparedHydrostaticEquilibriumOperator::GetEnthalpyMap() const noexcept { + return m_context.GetEnthalpyMap(); + } -bool PreparedHydrostaticEquilibriumOperator::IsPrepared() const noexcept { - return m_isPrepared; -} + const field::FieldDofMap &PreparedHydrostaticEquilibriumOperator::GetGravityPotentialMap() const noexcept { + return m_context.GetGravityPotentialMap(); + } -const context::hydrostatic::HydrostaticPreparationStatistics & -PreparedHydrostaticEquilibriumOperator::GetContextPreparationStatistics() - const noexcept { - return m_context.GetPreparationStatistics(); -} + const field::FieldDofMap &PreparedHydrostaticEquilibriumOperator::GetDisplacementMap() const noexcept { + return m_context.GetDisplacementMap(); + } -std::uint64_t -PreparedHydrostaticEquilibriumOperator::GetResidualPreparationCount() - const noexcept { - return m_residualPreparationCount; -} + void PreparedHydrostaticEquilibriumOperator::VerifyPrepared() const { + MFEM_VERIFY( + m_isPrepared, "PreparedHydrostaticEquilibriumOperator must be " + "prepared before residual or Jacobian application." + ); + } -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( + 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 d254801..a10b463 100644 --- a/libmeanfield/impl/operators/prepared_mass_normalization.cpp +++ b/libmeanfield/impl/operators/prepared_mass_normalization.cpp @@ -9,742 +9,718 @@ module mean_field; import :operators.prepared_mass_normalization; namespace { -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -[[nodiscard]] bool is_vacuum_attribute(const int attribute) { - return DomainSchema::template attribute_belongs_to< - mean_field::utils::domain::Vacuum>(attribute); -} + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); + } -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); - } -} + 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); + } + } -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."); + 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."); - localVector.SetSize(finiteElementSpace.GetVSize()); + localVector.SetSize(finiteElementSpace.GetVSize()); - const mfem::Operator *prolongation = - finiteElementSpace.GetProlongationMatrix(); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->Mult(trueVector, localVector); - } else { - localVector = trueVector; - } -} + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } + } -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::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; - MFEM_VERIFY(densityElement.GetOrder() == - mean_field::field::Density::Scalar::familyOrder, - "The mass-normalization element does not match the registered " - "density field."); + MFEM_VERIFY( + densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder, + "The mass-normalization element does not match the registered " + "density field." + ); - 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); + 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 auto resolution = - f.quadratureFactory->get(query, transformation.GetGeometryType()); + const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType()); - MFEM_VERIFY( - resolution.integration_rule != nullptr, - "The quadrature policy did not return a mass-normalization rule."); + MFEM_VERIFY( + resolution.integration_rule != nullptr, "The quadrature policy did not return a mass-normalization rule." + ); - return *resolution.integration_rule; -} + return *resolution.integration_rule; + } -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_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." + ); - const auto &revisions = gravityContext.GetRevisions(); + 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."); -} + 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); -} + 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::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."); + 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()); -} - -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; + m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size()); + m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size()); } - ++localStellarElementCount; - m_elements.emplace_back(); - ElementPAData &data = m_elements.back(); - data.elementId = elementId; + 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." + ); - data.densityDofTransformation = - m_fem.densityFes->GetElementDofs(elementId, data.densityDofs); + validate_shared_gravity_revisions(m_gravityContext, dependencies); - data.displacementDofTransformation = m_fem.displacementFes->GetElementVDofs( - elementId, data.displacementDofs); + 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.compactificationDofTransformation = - m_fem.compactificationFes->GetElementDofs(elementId, - data.compactificationDofs); + const bool rebuildStaticPlan = + !m_isPrepared || dependencies.discretization != m_preparedDependencies.discretization; - const mfem::FiniteElement &densityElement = - *m_fem.densityFes->GetFE(elementId); + const bool refreshGeometry = + rebuildStaticPlan || dependencies.displacement != m_preparedDependencies.displacement; - const mfem::IntegrationRule &integrationRule = - get_mass_normalization_rule(m_fem, densityElement, *transformation); + const bool refreshDensity = rebuildStaticPlan || dependencies.density != m_preparedDependencies.density; - data.quadraturePoints.resize(integrationRule.GetNPoints()); + const bool updateTargetMass = !m_isPrepared || dependencies.targetMass != m_preparedDependencies.targetMass || + state.targetMass != m_targetMass; - for (int quadraturePoint = 0; - quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) { - QuadraturePointData &point = data.quadraturePoints[quadraturePoint]; + m_isPrepared = false; - point.integrationPoint = integrationRule.IntPoint(quadraturePoint); + PreparedMassNormalizationReport report; - point.densityShape.SetSize(densityElement.GetDof()); - densityElement.CalcShape(point.integrationPoint, point.densityShape); - } - } + if (rebuildStaticPlan) { + BuildStaticPlan(); + report.rebuiltStaticPlan = true; + } - int globalStellarElementCount = 0; - MPI_Allreduce(&localStellarElementCount, &globalStellarElementCount, 1, - MPI_INT, MPI_SUM, m_fem.mesh->GetComm()); + if (refreshGeometry) { + RefreshGeometry(m_gravityContext.GetGeometryContext().GetDisplacementTrue()); + report.refreshedGeometry = true; + } - MFEM_VERIFY(globalStellarElementCount > 0, - "PreparedMassNormalizationOperator found no stellar elements."); -} + if (refreshDensity) { + RefreshDensity(m_gravityContext.GetDensityTrue()); + report.refreshedDensity = 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 (updateTargetMass) { + m_targetMass = state.targetMass; + report.updatedTargetMass = true; + } - mfem::Vector displacementLocal; - true_to_local(*m_fem.displacementFes, displacement, displacementLocal); + 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; + } - 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); + m_preparedDependencies = dependencies; + m_isPrepared = true; + return report; } - if (data.compactificationDofTransformation != nullptr) { - data.compactificationDofTransformation->InvTransformPrimal( - data.compactification); + 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; + } + + ++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."); } - const mfem::FiniteElement &displacementElement = - *m_fem.displacementFes->GetFE(data.elementId); + 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 &compactificationElement = - *m_fem.compactificationFes->GetFE(data.elementId); + mfem::Vector displacementLocal; + true_to_local(*m_fem.displacementFes, displacement, displacementLocal); - const mapping::ElementDisplacementData displacementData = - mapping::ElementDisplacementDataFromElementVDofs(displacementElement, - data.baseDisplacement); + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); - const mapping::ElementCompactificationData compactificationData( - compactificationElement, data.compactification); + for (ElementPAData &data : m_elements) { + displacementLocal.GetSubVector(data.displacementDofs, data.baseDisplacement); - const mapping::ElementMappingData mappingData{ - .displacement = displacementData, - .compactification = compactificationData}; + m_fem.compactificationCoordinate->GetSubVector(data.compactificationDofs, data.compactification); - mfem::ElementTransformation *transformation = - m_fem.mesh->GetElementTransformation(data.elementId); + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(data.baseDisplacement); + } - for (QuadraturePointData &point : data.quadraturePoints) { - const mapping::MappingStatus status = m_domainMapper.EvaluateVolume( - mappingData, *transformation, point.integrationPoint, workspace, - point.mappingContext); + if (data.compactificationDofTransformation != nullptr) { + data.compactificationDofTransformation->InvTransformPrimal(data.compactification); + } - 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 &displacementElement = *m_fem.displacementFes->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 mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); - mfem::Vector densityLocal; - true_to_local(*m_fem.densityFes, density, densityLocal); + const mapping::ElementDisplacementData displacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement); - mfem::Vector elementDensity; + const mapping::ElementCompactificationData compactificationData( + compactificationElement, data.compactification + ); - for (ElementPAData &data : m_elements) { - densityLocal.GetSubVector(data.densityDofs, elementDensity); + const mapping::ElementMappingData mappingData{ + .displacement = displacementData, .compactification = compactificationData + }; - if (data.densityDofTransformation != nullptr) { - data.densityDofTransformation->InvTransformPrimal(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) + ); + } + } } - 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::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." + ); -void PreparedMassNormalizationOperator::AssembleResidual() { - double localMass = 0.0; + mfem::Vector densityLocal; + true_to_local(*m_fem.densityFes, density, densityLocal); - for (const ElementPAData &data : m_elements) { - for (const QuadraturePointData &point : data.quadraturePoints) { - localMass += point.density * point.mappingContext.quadrature.weight; - } - } + mfem::Vector elementDensity; - m_currentMass = GlobalSum(localMass); - MFEM_VERIFY(std::isfinite(m_currentMass), - "PreparedMassNormalizationOperator assembled a non-finite mass."); + for (ElementPAData &data : m_elements) { + densityLocal.GetSubVector(data.densityDofs, elementDensity); - m_cachedResidual.SetSize(1); - m_cachedResidual(0) = m_currentMass - m_targetMass; - ++m_preparationCount; -} + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal(elementDensity); + } -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); + for (QuadraturePointData &point : data.quadraturePoints) { + point.density = elementDensity * point.densityShape; + MFEM_VERIFY( + std::isfinite(point.density), "PreparedMassNormalizationOperator produced a non-finite " + "quadrature density." + ); + } + } } - for (const QuadraturePointData &point : data.quadraturePoints) { - localAction += (elementDensityVariation * point.densityShape) * - point.mappingContext.quadrature.weight; - } - } + void PreparedMassNormalizationOperator::AssembleResidual() { + double localMass = 0.0; - return localAction; -} + for (const ElementPAData &data : m_elements) { + for (const QuadraturePointData &point : data.quadraturePoints) { + localMass += point.density * point.mappingContext.quadrature.weight; + } + } -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_currentMass = GlobalSum(localMass); + MFEM_VERIFY(std::isfinite(m_currentMass), "PreparedMassNormalizationOperator assembled a non-finite mass."); - 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); + m_cachedResidual.SetSize(1); + m_cachedResidual(0) = m_currentMass - m_targetMass; + ++m_preparationCount; } - 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; + void PreparedMassNormalizationOperator::BuildResidual(mfem::Vector &residual) const { + VerifyPrepared(); + residual = m_cachedResidual; + ++m_residualApplicationCount; } - } - return localAction; -} + 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."); -void PreparedMassNormalizationOperator::ApplyDensityJacobianAction( - const mfem::Vector &densityVariation, mfem::Vector &action) const { - VerifyPrepared(); + mfem::Vector densityVariationLocal; + true_to_local(*m_fem.densityFes, densityVariation, densityVariationLocal); - 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); + mfem::Vector elementDensityVariation; + double localAction = 0.0; - action.SetSize(1); - action(0) = GlobalSum(EvaluateDensityActionLocal(m_densityVariationTrue)); - ++m_actionStatistics.densityApplications; -} + for (const ElementPAData &data : m_elements) { + densityVariationLocal.GetSubVector(data.densityDofs, elementDensityVariation); -void PreparedMassNormalizationOperator::ApplyDisplacementJacobianAction( - const mfem::Vector &displacementVariation, mfem::Vector &action) const { - VerifyPrepared(); + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal(elementDensityVariation); + } - 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); + for (const QuadraturePointData &point : data.quadraturePoints) { + localAction += (elementDensityVariation * point.densityShape) * point.mappingContext.quadrature.weight; + } + } - action.SetSize(1); - action(0) = - GlobalSum(EvaluateDisplacementActionLocal(m_displacementVariationTrue)); - ++m_actionStatistics.displacementApplications; -} + return localAction; + } -void PreparedMassNormalizationOperator::ApplyCompleteJacobianAction( - const mfem::Vector &densityVariation, - const mfem::Vector &displacementVariation, mfem::Vector &action) const { - VerifyPrepared(); + 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." + ); - 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."); + mfem::Vector displacementVariationLocal; + true_to_local(*m_fem.displacementFes, displacementVariation, displacementVariationLocal); - m_gravityContext.GetDensityMap().scatter(densityVariation, - m_densityVariationTrue); - m_gravityContext.GetDisplacementMap().scatter(displacementVariation, - m_displacementVariationTrue); + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); - const double localAction = - EvaluateDensityActionLocal(m_densityVariationTrue) + - EvaluateDisplacementActionLocal(m_displacementVariationTrue); + mfem::Vector elementDisplacementVariation; + double localAction = 0.0; - action.SetSize(1); - action(0) = GlobalSum(localAction); - ++m_actionStatistics.completeApplications; -} + for (const ElementPAData &data : m_elements) { + displacementVariationLocal.GetSubVector(data.displacementDofs, 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; -} + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation); + } -bool PreparedMassNormalizationOperator::IsPrepared() const noexcept { - if (!m_isPrepared || !m_gravityContext.IsPrepared()) { - return false; - } + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->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 mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); -double PreparedMassNormalizationOperator::GetCurrentMass() const { - VerifyPrepared(); - return m_currentMass; -} + const mapping::ElementDisplacementData baseDisplacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement); -double PreparedMassNormalizationOperator::GetTargetMass() const { - VerifyPrepared(); - return m_targetMass; -} + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacementVariation); -std::uint64_t -PreparedMassNormalizationOperator::GetPreparationCount() const noexcept { - return m_preparationCount; -} + const mapping::ElementCompactificationData compactificationData( + compactificationElement, data.compactification + ); -std::uint64_t PreparedMassNormalizationOperator::GetResidualApplicationCount() - const noexcept { - return m_residualApplicationCount; -} + const mapping::ElementMappingData mappingData{ + .displacement = baseDisplacementData, .compactification = compactificationData + }; -const PreparedMassNormalizationActionStatistics & -PreparedMassNormalizationOperator::GetActionStatistics() const noexcept { - return m_actionStatistics; -} + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); -const fem::FEM &PreparedMassNormalizationOperator::GetFEM() const noexcept { - return m_fem; -} + for (const QuadraturePointData &point : data.quadraturePoints) { + mapping::VolumeMappingVariation variation; -const context::gravity_field::GravityFieldLinearizationContext & -PreparedMassNormalizationOperator::GetGravityContext() const noexcept { - return m_gravityContext; -} + const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation( + mappingData, directionData, *transformation, point.integrationPoint, point.mappingContext, + workspace, variation + ); -void PreparedMassNormalizationOperator::VerifyPrepared() const { - MFEM_VERIFY(IsPrepared(), - "PreparedMassNormalizationOperator must be prepared for the " - "current shared gravity-context revisions."); -} + MFEM_VERIFY( + status == mapping::MappingStatus::valid, "Stateless mapping variation failed in the " + "mass-normalization displacement action. Element: " + << data.elementId + << ", status: " << static_cast(status) + ); -PreparedMassNormalizationJacobianOperator:: - PreparedMassNormalizationJacobianOperator( + 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( 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 32e930b..c0a5e11 100644 --- a/libmeanfield/impl/operators/prepared_pressure_force.cpp +++ b/libmeanfield/impl/operators/prepared_pressure_force.cpp @@ -617,13 +617,19 @@ namespace mean_field::operators { data.enthalpyJacobian = 0.0; for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { - const double enthalpy = quadratureEnthalpy(quadraturePoint); + const double enthalpy = quadratureEnthalpy(quadraturePoint); - const double pressure = m_equationOfState.pressure_from_enthalpy(enthalpy); + const eos::SpecificEnthalpyValue specificEnthalpy{enthalpy}; + const double pressure = + eos::evaluate(m_equationOfState, specificEnthalpy).value(); - const double pressureDerivative = m_equationOfState.pressure_derivative_from_enthalpy(enthalpy); + const double pressureDerivative = + eos::partialDerivative( + m_equationOfState, specificEnthalpy + ) + .value(); - const double quadratureWeight = data.quadratureWeights(quadraturePoint); + const double quadratureWeight = data.quadratureWeights(quadraturePoint); MFEM_VERIFY( std::isfinite(pressure) && std::isfinite(pressureDerivative), @@ -1134,4 +1140,4 @@ namespace mean_field::operators { const BarotropicEquilibriumLayout &PreparedPressureForceJacobianOperator::GetLayout() const noexcept { return m_layout; } -} // namespace mean_field::operators \ No newline at end of file +} // namespace mean_field::operators diff --git a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp index ecb0366..986afc9 100644 --- a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp @@ -21,6 +21,12 @@ namespace { ); } + [[nodiscard]] mfem::Vector make_computational_origin(const mfem::ParMesh &mesh) { + mfem::Vector origin(mesh.SpaceDimension()); + origin = 0.0; + return origin; + } + [[nodiscard]] mean_field::operators::StellarEquilibriumLayout make_layout( const mean_field::field::FieldDofMap &densityMap, const mean_field::field::FieldDofMap &displacementMap, @@ -253,6 +259,8 @@ namespace mean_field::operators { field::FieldDofMap gravityFluxMap; field::FieldDofMap gravityPotentialMap; field::FieldDofMap enthalpyMap; + field::FieldBoundaryDofMap pressureSurfaceRows; + field::FieldPointDofMap centerDisplacementRows; StellarEquilibriumLayout layout; mfem::Array gravityStateOffsets; @@ -284,6 +292,23 @@ namespace mean_field::operators { field::Enthalpy, DomainSchema>(*f.enthalpyFes) ), + pressureSurfaceRows( + field::make_field_boundary_dof_map< + field::Enthalpy, + utils::domain::StellarSurface, + DomainSchema>( + *f.enthalpyFes, + enthalpyMap + ) + ), + centerDisplacementRows( + field::make_field_point_dof_map( + *f.displacementFes, + displacementMap, + make_computational_origin(*f.mesh), + 1.0e-12 + ) + ), layout(make_layout( densityMap, displacementMap, @@ -314,27 +339,15 @@ namespace mean_field::operators { fem::FEM &f, const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, - const models::StellarModel &stellarModel - ) - : PreparedStellarEquilibriumOperator( - f, - domainMapper, - equationOfState, - stellarModel.targetMass() - ) { - } - - PreparedStellarEquilibriumOperator::PreparedStellarEquilibriumOperator( - fem::FEM &f, - const mapping::DomainMapper &domainMapper, - const eos::Polytrope &equationOfState, - const double targetMass + const double targetMass, + const PressureSurfaceConstraintView surfaceConstraint ) : PreparedStellarEquilibriumOperator( f, domainMapper, equationOfState, targetMass, + surfaceConstraint, MakeConstructionData(f) ) { } @@ -344,6 +357,7 @@ namespace mean_field::operators { const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const double targetMass, + const PressureSurfaceConstraintView surfaceConstraint, ConstructionData constructionData ) : mfem::Operator( @@ -390,6 +404,11 @@ namespace mean_field::operators { domainMapper, m_gravityContext ), + m_surfaceConstraintOperator( + constructionData.pressureSurfaceRows, + surfaceConstraint + ), + m_centeringConstraintOperator(constructionData.centerDisplacementRows), m_targetMass(targetMass) { MFEM_VERIFY( std::isfinite(m_targetMass) && m_targetMass > 0.0, @@ -506,6 +525,14 @@ namespace mean_field::operators { report.massNormalization = m_massNormalizationOperator.Prepare({.targetMass = m_targetMass}, make_mass_dependencies(dependencies)); + report.surfaceConstraint = m_surfaceConstraintOperator.Prepare( + reducedEnthalpy, !wasPrepared || dependencies.enthalpy != m_preparedDependencies.enthalpy + ); + + report.centeringConstraint = m_centeringConstraintOperator.Prepare( + displacement, !wasPrepared || dependencies.displacement != m_preparedDependencies.displacement + ); + const bool dependenciesChanged = !wasPrepared || dependencies != m_preparedDependencies; if (dependenciesChanged || report.DidAnyChildWork()) { AssembleResidual(); @@ -542,7 +569,9 @@ namespace mean_field::operators { m_gravityOperator.Mult(m_gravityState, gravity); m_barotropicClosureOperator.BuildResidual(closure); m_displacementOperator.BuildResidual(displacement); + m_centeringConstraintOperator.ApplyResidualRows(displacement); m_hydrostaticOperator.BuildResidual(hydrostatic); + m_surfaceConstraintOperator.ApplyResidualRows(hydrostatic); m_massNormalizationOperator.BuildResidual(mass); m_cachedResidual.SetSize(Height()); @@ -659,11 +688,13 @@ namespace mean_field::operators { reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection, displacementAction ); + m_centeringConstraintOperator.ApplyJacobianRows(displacementDirection, displacementAction); m_hydrostaticOperator.ApplyCompleteJacobianAction( reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection, hydrostaticAction ); + m_surfaceConstraintOperator.ApplyJacobianRows(reducedEnthalpyDirection, hydrostaticAction); m_massNormalizationOperator.ApplyCompleteJacobianAction( reducedDensityDirection, displacementDirection, massAction @@ -712,7 +743,8 @@ namespace mean_field::operators { bool PreparedStellarEquilibriumOperator::IsPrepared() const noexcept { return m_isPrepared && m_gravityContext.IsPrepared() && m_barotropicClosureOperator.IsPrepared() && m_hydrostaticOperator.IsPrepared() && m_displacementOperator.IsPrepared() && - m_massNormalizationOperator.IsPrepared(); + m_massNormalizationOperator.IsPrepared() && m_surfaceConstraintOperator.IsPrepared() && + m_centeringConstraintOperator.IsPrepared(); } double PreparedStellarEquilibriumOperator::GetTargetMass() const noexcept { @@ -771,6 +803,16 @@ namespace mean_field::operators { return m_massNormalizationOperator; } + const PreparedPressureSurfaceConstraint & + PreparedStellarEquilibriumOperator::GetSurfaceConstraintOperator() const noexcept { + return m_surfaceConstraintOperator; + } + + const PreparedCenteringConstraint & + PreparedStellarEquilibriumOperator::GetCenteringConstraintOperator() const noexcept { + return m_centeringConstraintOperator; + } + void PreparedStellarEquilibriumOperator::VerifyPrepared() const { MFEM_VERIFY( IsPrepared(), "PreparedStellarEquilibriumOperator must be prepared before residual or Jacobian application." diff --git a/libmeanfield/impl/physics/gravity.cpp b/libmeanfield/impl/physics/gravity.cpp index af96b3e..ce5cf31 100644 --- a/libmeanfield/impl/physics/gravity.cpp +++ b/libmeanfield/impl/physics/gravity.cpp @@ -13,16 +13,14 @@ namespace mean_field::physics { ) { const int dim = fem.mesh->Dimension(); mfem::DenseMatrix local_Q(dim, dim); - local_Q = 0.0; + local_Q = 0.0; using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate ); for (int i = 0; i < fem.mesh->GetNE(); ++i) { - if (!DomainSchema::template attribute_belongs_to( - fem.mesh->GetAttribute(i))) + if (!DomainSchema::template attribute_belongs_to(fem.mesh->GetAttribute(i))) continue; mfem::ElementTransformation *trans = fem.mesh->GetElementTransformation(i); @@ -40,13 +38,12 @@ namespace mean_field::physics { mapping::VolumeMappingContext mapping_context; MFEM_VERIFY( - mapping_evaluator.EvaluateVolume(*trans, ip, mapping_context) == - mapping::MappingStatus::valid, + 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 weight = mapping_context.quadrature.weight; - const double rho_val = rho.GetValue(i, ip); + const double rho_val = rho.GetValue(i, ip); const mfem::Vector &phys_point = mapping_context.mapping.physical_position; @@ -145,7 +142,7 @@ namespace mean_field::physics { constexpr auto gravity_poisson_residual_block = utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const field::FieldDofGridFunctionAdapter density_adapter = field::make_field_dof_grid_function_adapter(*f.densityFes); const field::FieldDofGridFunctionAdapter displacement_adapter = @@ -220,12 +217,8 @@ namespace mean_field::physics { GravitySolution solution(f); - 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 - ); + 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 2cf001c..564141e 100644 --- a/libmeanfield/impl/physics/solid.cpp +++ b/libmeanfield/impl/physics/solid.cpp @@ -9,16 +9,14 @@ namespace mean_field::physics { const fem::FEM &fem, const mfem::GridFunction &rho_ref ) { - double local_I = 0.0; + double local_I = 0.0; using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate ); for (int i = 0; i < fem.mesh->GetNE(); i++) { - if (!DomainSchema::template attribute_belongs_to( - fem.mesh->GetAttribute(i))) + if (!DomainSchema::template attribute_belongs_to(fem.mesh->GetAttribute(i))) continue; mfem::ElementTransformation *T = fem.mesh->GetElementTransformation(i); @@ -37,14 +35,13 @@ namespace mean_field::physics { mapping::VolumeMappingContext mapping_context; MFEM_VERIFY( - mapping_evaluator.EvaluateVolume(*T, ip, mapping_context) == - mapping::MappingStatus::valid, + 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 weight = mapping_context.quadrature.weight; + const double r_cyl_sq = x_phys(0) * x_phys(0) + x_phys(1) * x_phys(1); + 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 e37ea83..4448261 100644 --- a/libmeanfield/impl/utils/domain.cpp +++ b/libmeanfield/impl/utils/domain.cpp @@ -13,8 +13,7 @@ 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 + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate ); mfem::Array init_elem; @@ -39,8 +38,7 @@ namespace mean_field::utils { mapping::MappingPointContext context; MFEM_VERIFY( - mapping_evaluator.EvaluatePoint(*T0, origin_ip[0], context) == - mapping::MappingStatus::valid, + mapping_evaluator.EvaluatePoint(*T0, origin_ip[0], context) == mapping::MappingStatus::valid, "Reference-point initialization encountered an invalid mapping." ); @@ -104,8 +102,7 @@ namespace mean_field::utils { T->SetIntPoint(&ip); mapping::MappingPointContext context; - if (mapping_evaluator.EvaluatePoint(*T, ip, context) != - mapping::MappingStatus::valid) { + if (mapping_evaluator.EvaluatePoint(*T, ip, context) != mapping::MappingStatus::valid) { return false; } const mfem::Vector ¤t_x_phys = context.physical_position; diff --git a/libmeanfield/impl/utils/misc.cpp b/libmeanfield/impl/utils/misc.cpp index 8d3db90..3ff63f0 100644 --- a/libmeanfield/impl/utils/misc.cpp +++ b/libmeanfield/impl/utils/misc.cpp @@ -4,21 +4,25 @@ module; module mean_field; 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)); + } -int get_mesh_order(const mfem::Mesh &mesh) { - if (mesh.GetNodes() != nullptr) { - return mesh.GetNodes()->FESpace()->GetMaxElementOrder(); - } - return 1; -} + int get_mesh_order(const mfem::Mesh &mesh) { + if (mesh.GetNodes() != nullptr) { + return mesh.GetNodes()->FESpace()->GetMaxElementOrder(); + } + return 1; + } } // namespace mean_field::utils diff --git a/libmeanfield/interface/eos/concepts.cppm b/libmeanfield/interface/eos/concepts.cppm new file mode 100644 index 0000000..f3088d5 --- /dev/null +++ b/libmeanfield/interface/eos/concepts.cppm @@ -0,0 +1,100 @@ +module; + +#include +#include + +export module mean_field:eos.concepts; +export import :eos.relations; + +export namespace mean_field::eos { + namespace detail { + template struct ImplementsRelation : std::false_type { }; + + template < + typename EquationOfState, + typename Output, + typename... Inputs> + struct ImplementsRelation< + EquationOfState, + Relation< + Output, + Inputs...>> : std::bool_constant < + requires( + const std::remove_cvref_t &equationOfState, + QuantityValue... inputValues + ) { + {equationOfState.evaluate(Relation{}, inputValues...)} + ->std::same_as>; + }>{}; + + template struct ImplementsRelationCatalog : std::false_type { }; + + template + struct ImplementsRelationCatalog> + : std::bool_constant<(ImplementsRelation::value && ...)> { }; + + template struct IsEquationOfStateModel : std::false_type { }; + + template + struct IsEquationOfStateModel::Relations>> + : std::bool_constant< + ValidRelationCatalog::Relations> && + ImplementsRelationCatalog< + std::remove_cvref_t, + typename std::remove_cvref_t::Relations>::value> { }; + + template + struct ImplementsPartialDerivative : std::false_type { }; + + template < + typename EquationOfState, + typename Output, + typename... Inputs, + typename InputQuantity> + struct ImplementsPartialDerivative< + EquationOfState, + Relation< + Output, + Inputs...>, + InputQuantity> : std::bool_constant < + (std::same_as< + InputQuantity, + Inputs> || + ...) && + requires( + const std::remove_cvref_t &equationOfState, + QuantityValue... inputValues + ) { + {equationOfState + .partialDerivative(Relation{}, WithRespectTo{}, inputValues...)} + ->std::same_as>; + }>{}; + } // namespace detail + + template + concept EquationOfStateModel = detail::IsEquationOfStateModel::value; + + template + concept SupportsRelation = + EquationOfStateModel && ThermodynamicRelationType && + relationCatalogContains::Relations, RelationType>; + + template + concept SupportsPartialDerivative = + SupportsRelation && ThermodynamicQuantityType && + detail::ImplementsPartialDerivative::value; + + template + concept StructureSeedEquationOfState = + EquationOfStateModel && SupportsRelation; + + template + concept BarotropicClosureEquationOfState = + EquationOfStateModel && SupportsRelation && + SupportsPartialDerivative; + + template + concept PressureForceEquationOfState = + EquationOfStateModel && SupportsRelation && + SupportsPartialDerivative; +} // namespace mean_field::eos diff --git a/libmeanfield/interface/eos/eos_base.cppm b/libmeanfield/interface/eos/eos_base.cppm deleted file mode 100644 index 9cd4d41..0000000 --- a/libmeanfield/interface/eos/eos_base.cppm +++ /dev/null @@ -1,16 +0,0 @@ -export module mean_field:eos.base; - -export namespace mean_field::eos { - class EquationOfState { - public: - virtual ~EquationOfState() = default; - [[nodiscard]] virtual double pressure_from_density(double density) const = 0; - [[nodiscard]] virtual double pressure_from_enthalpy(double enthalpy) const = 0; - [[nodiscard]] virtual double enthalpy_from_density(double density) const = 0; - [[nodiscard]] virtual double enthalpy_from_pressure(double pressure) const = 0; - [[nodiscard]] virtual double density_from_enthalpy(double enthalpy) const = 0; - [[nodiscard]] virtual double density_derivative_from_enthalpy(double enthalpy) const = 0; - [[nodiscard]] virtual double pressure_derivative_from_enthalpy(double enthalpy) const = 0; - [[nodiscard]] virtual double pressure_derivative_from_density(double density) const = 0; - }; -} // namespace mean_field::eos \ No newline at end of file diff --git a/libmeanfield/interface/eos/evaluation.cppm b/libmeanfield/interface/eos/evaluation.cppm new file mode 100644 index 0000000..51cfc28 --- /dev/null +++ b/libmeanfield/interface/eos/evaluation.cppm @@ -0,0 +1,90 @@ +module; + +#include +#include +#include + +export module mean_field:eos.evaluation; +export import :eos.concepts; + +export namespace mean_field::eos { + enum class EvaluationErrorCode { + unsupported_relation, + unsupported_derivative, + wrong_input_count, + wrong_input_quantity, + nonfinite_input, + outside_domain, + nonfinite_result + }; + + class EvaluationError final : public std::domain_error { + public: + explicit EvaluationError( + const EvaluationErrorCode code, + std::string message + ) + : std::domain_error(std::move(message)), + m_code(code) { + } + + [[nodiscard]] EvaluationErrorCode code() const noexcept { + return m_code; + } + + private: + EvaluationErrorCode m_code; + }; + + template < + ThermodynamicQuantityType OutputQuantity, + EquationOfStateModel EquationOfState, + QuantityValueType... InputValues> + requires SupportsRelation< + EquationOfState, + Relation< + OutputQuantity, + QuantityOfT...>> + [[nodiscard]] constexpr QuantityValue evaluate( + const EquationOfState &equationOfState, + const InputValues... inputValues + ) noexcept(noexcept(equationOfState + .evaluate( + Relation< + OutputQuantity, + QuantityOfT...>{}, + inputValues... + ))) { + return equationOfState.evaluate(Relation...>{}, inputValues...); + } + + template < + ThermodynamicQuantityType OutputQuantity, + ThermodynamicQuantityType InputQuantity, + EquationOfStateModel EquationOfState, + QuantityValueType... InputValues> + requires SupportsPartialDerivative< + EquationOfState, + Relation< + OutputQuantity, + QuantityOfT...>, + InputQuantity> + [[nodiscard]] constexpr PartialDerivative< + OutputQuantity, + InputQuantity> + partialDerivative( + const EquationOfState &equationOfState, + const InputValues... inputValues + ) noexcept(noexcept(equationOfState + .partialDerivative( + Relation< + OutputQuantity, + QuantityOfT...>{}, + WithRespectTo{}, + inputValues... + ))) { + return equationOfState.partialDerivative( + Relation...>{}, WithRespectTo{}, inputValues... + ); + } +} // namespace mean_field::eos diff --git a/libmeanfield/interface/eos/polytropic.cppm b/libmeanfield/interface/eos/polytropic.cppm index 36409fc..65f8990 100644 --- a/libmeanfield/interface/eos/polytropic.cppm +++ b/libmeanfield/interface/eos/polytropic.cppm @@ -3,11 +3,18 @@ module; #include #include export module mean_field:eos.polytrope; -export import :eos.base; +export import :eos.evaluation; export namespace mean_field::eos { - class Polytrope final : public EquationOfState { + class Polytrope final { public: + using Relations = RelationCatalog< + PressureFromDensity, + PressureFromSpecificEnthalpy, + SpecificEnthalpyFromDensity, + SpecificEnthalpyFromPressure, + DensityFromSpecificEnthalpy>; + Polytrope( const double polytropic_index, const double polytropic_constant @@ -49,82 +56,128 @@ export namespace mean_field::eos { return m_enthalpy_scale; } - [[nodiscard]] double pressure_from_density(const double density) const override { - validate_nonnegativity(density, "density"); - if (density == 0.0) { - return 0.0; + [[nodiscard]] PressureValue evaluate( + PressureFromDensity, + const DensityValue density + ) const { + validate_nonnegativity(density.value(), "density"); + if (density.value() == 0.0) { + return PressureValue{0.0}; } - return m_polytropic_constant * std::pow(density, 1.0 + 1.0 / m_polytropic_index); + return PressureValue{m_polytropic_constant * std::pow(density.value(), 1.0 + 1.0 / m_polytropic_index)}; } - [[nodiscard]] double enthalpy_from_density(const double density) const override { - validate_nonnegativity(density, "density"); - if (density == 0.0) { - return 0.0; + [[nodiscard]] SpecificEnthalpyValue evaluate( + SpecificEnthalpyFromDensity, + const DensityValue density + ) const { + validate_nonnegativity(density.value(), "density"); + if (density.value() == 0.0) { + return SpecificEnthalpyValue{0.0}; } - return m_enthalpy_scale * std::pow(density, 1.0 / m_polytropic_index); + return SpecificEnthalpyValue{m_enthalpy_scale * std::pow(density.value(), 1.0 / m_polytropic_index)}; } - [[nodiscard]] double density_from_enthalpy(const double enthalpy) const override { - validate_finite(enthalpy, "enthalpy"); + [[nodiscard]] DensityValue evaluate( + DensityFromSpecificEnthalpy, + const SpecificEnthalpyValue specificEnthalpy + ) const { + validate_finite(specificEnthalpy.value(), "specific enthalpy"); - if (enthalpy <= 0.0) { - return 0.0; + if (specificEnthalpy.value() <= 0.0) { + return DensityValue{0.0}; } - return std::pow(enthalpy / m_enthalpy_scale, m_polytropic_index); + return DensityValue{std::pow(specificEnthalpy.value() / m_enthalpy_scale, m_polytropic_index)}; } - [[nodiscard]] double pressure_from_enthalpy(const double enthalpy) const override { - validate_finite(enthalpy, "enthalpy"); + [[nodiscard]] PressureValue evaluate( + PressureFromSpecificEnthalpy, + const SpecificEnthalpyValue specificEnthalpy + ) const { + const DensityValue density = evaluate(DensityFromSpecificEnthalpy{}, specificEnthalpy); - if (enthalpy <= 0.0) { - return 0.0; + if (specificEnthalpy.value() <= 0.0) { + return PressureValue{0.0}; } - return density_from_enthalpy(enthalpy) * enthalpy / (m_polytropic_index + 1.0); + return PressureValue{density.value() * specificEnthalpy.value() / (m_polytropic_index + 1.0)}; } - [[nodiscard]] double density_derivative_from_enthalpy(const double enthalpy) const override { - validate_finite(enthalpy, "enthalpy"); - if (enthalpy < 0.0) { - return 0.0; + [[nodiscard]] SpecificEnthalpyValue evaluate( + SpecificEnthalpyFromPressure, + const PressureValue pressure + ) const { + validate_nonnegativity(pressure.value(), "pressure"); + if (pressure.value() == 0.0) { + return SpecificEnthalpyValue{0.0}; } - if (enthalpy == 0.0) { - return m_polytropic_index == 1.0 ? 1.0 / m_enthalpy_scale : 0.0; - } + const double indexPlusOne = m_polytropic_index + 1.0; - return m_polytropic_index / m_enthalpy_scale * - std::pow(enthalpy / m_enthalpy_scale, m_polytropic_index - 1.0); + return SpecificEnthalpyValue{ + indexPlusOne * std::pow(m_polytropic_constant, m_polytropic_index / indexPlusOne) * + std::pow(pressure.value(), 1.0 / indexPlusOne) + }; } - [[nodiscard]] double pressure_derivative_from_enthalpy(const double enthalpy) const override { - validate_finite(enthalpy, "enthalpy"); - - if (enthalpy <= 0.0) { - return 0.0; + [[nodiscard]] PartialDerivative< + quantity::Density, + quantity::SpecificEnthalpy> + partialDerivative( + DensityFromSpecificEnthalpy, + WithRespectTo, + const SpecificEnthalpyValue specificEnthalpy + ) const { + validate_finite(specificEnthalpy.value(), "specific enthalpy"); + if (specificEnthalpy.value() < 0.0) { + return PartialDerivative{0.0}; } - return density_from_enthalpy(enthalpy); - } - - [[nodiscard]] double pressure_derivative_from_density(const double density) const override { - validate_nonnegativity(density, "density"); - if (density == 0.0) { - return 0.0; + if (specificEnthalpy.value() == 0.0) { + return PartialDerivative{ + m_polytropic_index == 1.0 ? 1.0 / m_enthalpy_scale : 0.0 + }; } - return m_polytropic_constant * (1.0 + 1.0 / m_polytropic_index) * - std::pow(density, 1.0 / m_polytropic_index); + return PartialDerivative{ + m_polytropic_index / m_enthalpy_scale * + std::pow(specificEnthalpy.value() / m_enthalpy_scale, m_polytropic_index - 1.0) + }; } - [[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); + [[nodiscard]] PartialDerivative< + quantity::Pressure, + quantity::SpecificEnthalpy> + partialDerivative( + PressureFromSpecificEnthalpy, + WithRespectTo, + const SpecificEnthalpyValue specificEnthalpy + ) const { + const DensityValue density = evaluate(DensityFromSpecificEnthalpy{}, specificEnthalpy); + + return PartialDerivative{density.value()}; + } + + [[nodiscard]] PartialDerivative< + quantity::Pressure, + quantity::Density> + partialDerivative( + PressureFromDensity, + WithRespectTo, + const DensityValue density + ) const { + validate_nonnegativity(density.value(), "density"); + if (density.value() == 0.0) { + return PartialDerivative{0.0}; + } + + return PartialDerivative{ + m_polytropic_constant * (1.0 + 1.0 / m_polytropic_index) * + std::pow(density.value(), 1.0 / m_polytropic_index) + }; } private: @@ -133,12 +186,12 @@ export namespace mean_field::eos { 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 - ) + throw EvaluationError( + EvaluationErrorCode::nonfinite_input, std::format( + "The {} must be finite. Instead a value of {} has been " + "provided", + quantity, value + ) ); } } @@ -149,13 +202,13 @@ export namespace mean_field::eos { ) { 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 - ) + throw EvaluationError( + EvaluationErrorCode::outside_domain, std::format( + "The {} must be non-negative. Instead a value of {} " + "has been " + "provided", + quantity, value + ) ); } } diff --git a/libmeanfield/interface/eos/pressure_surface.cppm b/libmeanfield/interface/eos/pressure_surface.cppm new file mode 100644 index 0000000..b9df808 --- /dev/null +++ b/libmeanfield/interface/eos/pressure_surface.cppm @@ -0,0 +1,128 @@ +module; + +#include +#include + +export module mean_field:eos.pressure_surface; + +export import :eos.evaluation; + +export namespace mean_field::eos { + namespace detail { + template < + ThermodynamicQuantityType InputQuantity, + typename SurfaceState> + [[nodiscard]] constexpr auto pressureSurfaceRelationInput( + const PressureValue targetPressure, + const SurfaceState &state + ) { + if constexpr (std::same_as) { + return targetPressure; + } else { + return state.value(InputQuantity{}); + } + } + + template struct PressureSurfaceRelationOperations; + + template + struct PressureSurfaceRelationOperations> { + template < + typename EquationOfState, + typename SurfaceState> + [[nodiscard]] static QuantityValue requiredCarrierValue( + const EquationOfState &equationOfState, + const PressureValue targetPressure, + const SurfaceState &state + ) { + return evaluate( + equationOfState, pressureSurfaceRelationInput(targetPressure, state)... + ); + } + + template < + typename InputQuantity, + typename EquationOfState, + typename SurfaceState, + typename SurfaceVariation> + [[nodiscard]] static double inputJacobianContribution( + const EquationOfState &equationOfState, + const PressureValue targetPressure, + const SurfaceState &state, + const SurfaceVariation &variation + ) { + if constexpr (std::same_as) { + return 0.0; + } else { + const auto derivative = partialDerivative( + equationOfState, pressureSurfaceRelationInput(targetPressure, state)... + ); + return derivative.value() * variation.value(InputQuantity{}).value(); + } + } + + template < + typename EquationOfState, + typename SurfaceState, + typename SurfaceVariation> + [[nodiscard]] static double carrierCorrectionJacobianAction( + const EquationOfState &equationOfState, + const PressureValue targetPressure, + const SurfaceState &state, + const SurfaceVariation &variation + ) { + return ( + 0.0 + ... + + inputJacobianContribution(equationOfState, targetPressure, state, variation) + ); + } + }; + } // namespace detail + + /* + * EOS-owned resolution of a constant-pressure condition into the carrier + * quantity used by an equation formulation. No field or solver concepts + * enter this type. + */ + template + class ResolvedPressureSurfaceRelation final { + public: + using RelationType = SelectedRelation; + using CarrierQuantity = RelationOutputT; + + ResolvedPressureSurfaceRelation( + const EquationOfState &equationOfState, + const PressureValue targetPressure + ) noexcept + : m_equationOfState(std::addressof(equationOfState)), + m_targetPressure(targetPressure) { + } + + [[nodiscard]] PressureValue targetPressure() const noexcept { + return m_targetPressure; + } + + template + [[nodiscard]] QuantityValue requiredCarrierValue(const SurfaceState &state) const { + return detail::PressureSurfaceRelationOperations::requiredCarrierValue( + *m_equationOfState, m_targetPressure, state + ); + } + + template < + typename SurfaceState, + typename SurfaceVariation> + [[nodiscard]] double carrierCorrectionJacobianAction( + const SurfaceState &state, + const SurfaceVariation &variation + ) const { + return detail::PressureSurfaceRelationOperations::carrierCorrectionJacobianAction( + *m_equationOfState, m_targetPressure, state, variation + ); + } + + private: + const EquationOfState *m_equationOfState; + PressureValue m_targetPressure; + }; +} // namespace mean_field::eos diff --git a/libmeanfield/interface/eos/quantities.cppm b/libmeanfield/interface/eos/quantities.cppm new file mode 100644 index 0000000..3e47cc8 --- /dev/null +++ b/libmeanfield/interface/eos/quantities.cppm @@ -0,0 +1,235 @@ +module; + +#include +#include +#include +#include + +export module mean_field:eos.quantities; + +export namespace mean_field::eos { + struct ThermodynamicQuantity { }; + + template + concept ThermodynamicQuantityType = + std::same_as> && std::derived_from; + + namespace quantity { + struct Density final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "density"; + }; + + struct Pressure final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "pressure"; + }; + + struct SpecificEnthalpy final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "specific_enthalpy"; + }; + } // namespace quantity + + template + concept Numeric = std::integral || std::floating_point; + + template class QuantityValue final { + public: + explicit constexpr QuantityValue(const double value) noexcept : m_value(value) { + } + + [[nodiscard]] constexpr double value() const noexcept { + return m_value; + } + + [[nodiscard]] friend constexpr bool operator==( + const QuantityValue &, + const QuantityValue & + ) noexcept = default; + + friend constexpr QuantityValue operator+( + const QuantityValue &lhs, + const QuantityValue &rhs + ) noexcept { + return QuantityValue{lhs.m_value + rhs.m_value}; + } + + friend constexpr QuantityValue operator-( + const QuantityValue &lhs, + const QuantityValue &rhs + ) noexcept { + return QuantityValue{lhs.m_value - rhs.m_value}; + } + + template + friend constexpr QuantityValue operator*( + const QuantityValue &lhs, + rhsT rhs + ) noexcept { + return QuantityValue{lhs.m_value * static_cast(rhs)}; + } + + template + friend constexpr QuantityValue operator*( + lhsT lhs, + const QuantityValue &rhs + ) noexcept { + return QuantityValue{static_cast(lhs) * rhs.m_value}; + } + + template + friend constexpr QuantityValue operator/( + const QuantityValue &lhs, + rhsT rhs + ) noexcept { + return QuantityValue{lhs.m_value / static_cast(rhs)}; + } + + template + friend constexpr std::partial_ordering operator<=>( + const QuantityValue &lhs, + compT rhs + ) noexcept { + return lhs.m_value <=> static_cast(rhs); + } + + template + friend constexpr std::partial_ordering operator<=>( + compT lhs, + const QuantityValue &rhs + ) noexcept { + return static_cast(lhs) <=> rhs.m_value; + } + + friend constexpr std::partial_ordering operator<=>( + const QuantityValue &lhs, + const QuantityValue &rhs + ) noexcept { + return lhs.m_value <=> rhs.m_value; + } + + private: + double m_value; + }; + + using DensityValue = QuantityValue; + using PressureValue = QuantityValue; + using SpecificEnthalpyValue = QuantityValue; + + template struct IsQuantityValue : std::false_type { }; + + template struct IsQuantityValue> : std::true_type { }; + + template + concept QuantityValueType = IsQuantityValue>::value; + + template struct QuantityOf; + + template struct QuantityOf> { + using Type = Quantity; + }; + + template using QuantityOfT = typename QuantityOf>::Type; + + template + class PartialDerivative final { + public: + explicit constexpr PartialDerivative(const double value) noexcept : m_value(value) { + } + + [[nodiscard]] constexpr double value() const noexcept { + return m_value; + } + + friend constexpr PartialDerivative< + OutputQuantity, + InputQuantity> + operator+( + const PartialDerivative< + OutputQuantity, + InputQuantity> &lhs, + const PartialDerivative< + OutputQuantity, + InputQuantity> &rhs + ) noexcept; + + friend constexpr PartialDerivative< + OutputQuantity, + InputQuantity> + operator-( + const PartialDerivative< + OutputQuantity, + InputQuantity> &lhs, + const PartialDerivative< + OutputQuantity, + InputQuantity> &rhs + ) noexcept; + + template + friend constexpr PartialDerivative< + OutputQuantity, + InputQuantity> + operator*( + const PartialDerivative< + OutputQuantity, + InputQuantity> &, + rhsT + ) noexcept; + + template + friend constexpr PartialDerivative< + OutputQuantity, + InputQuantity> + operator*( + lhsT, + const PartialDerivative< + OutputQuantity, + InputQuantity> & + ) noexcept; + + template + friend constexpr PartialDerivative< + OutputQuantity, + InputQuantity> + operator/( + const PartialDerivative< + OutputQuantity, + InputQuantity> &, + rhsT + ) noexcept; + + template + friend constexpr std::partial_ordering operator<=>( + const PartialDerivative< + OutputQuantity, + InputQuantity> &lhs, + cmpT rhs + ) noexcept { + return lhs.m_value <=> static_cast(rhs); + } + + template + friend constexpr std::partial_ordering operator<=>( + cmpT lhs, + const PartialDerivative< + OutputQuantity, + InputQuantity> &rhs + ) noexcept { + return static_cast(lhs) <=> rhs.m_value; + } + + friend constexpr std::partial_ordering operator<=>( + const PartialDerivative< + OutputQuantity, + InputQuantity> &lhs, + const PartialDerivative< + OutputQuantity, + InputQuantity> &rhs + ) noexcept { + return lhs.m_value <=> rhs.m_value; + } + + private: + double m_value; + }; + + template struct WithRespectTo final { }; +} // namespace mean_field::eos diff --git a/libmeanfield/interface/eos/relations.cppm b/libmeanfield/interface/eos/relations.cppm new file mode 100644 index 0000000..d8bf825 --- /dev/null +++ b/libmeanfield/interface/eos/relations.cppm @@ -0,0 +1,93 @@ +module; + +#include +#include +#include +#include + +export module mean_field:eos.relations; +export import :eos.quantities; + +export namespace mean_field::eos { + template struct QuantityList final { }; + + template struct Relation final { + using OutputQuantity = Output; + using InputQuantities = QuantityList; + + static constexpr std::size_t inputCount = sizeof...(Inputs); + }; + + template struct RelationCatalog final { + static constexpr std::size_t size = sizeof...(Relations); + }; + + namespace detail { + template struct TypesAreUnique; + + template struct IsThermodynamicRelation : std::false_type { }; + + template + struct IsThermodynamicRelation> + : std::bool_constant< + ThermodynamicQuantityType && (ThermodynamicQuantityType && ...) && + TypesAreUnique::value> { }; + + template struct TypesAreUnique : std::true_type { }; + + template + struct TypesAreUnique + : std::bool_constant<(!std::same_as && ...) && TypesAreUnique::value> { }; + + template struct IsValidRelationCatalog : std::false_type { }; + + template + struct IsValidRelationCatalog> + : std::bool_constant< + (sizeof...(Relations) > 0) && (IsThermodynamicRelation::value && ...) && + TypesAreUnique::value> { }; + + template struct CatalogContainsRelation : std::false_type { }; + + template + struct CatalogContainsRelation, RelationType> + : std::bool_constant<(std::same_as || ...)> { }; + + template struct RelationContainsInput : std::false_type { }; + + template + struct RelationContainsInput, Quantity> + : std::bool_constant<(std::same_as || ...)> { }; + + template struct QuantityAt; + + template struct QuantityAt> { + using Type = std::tuple_element_t>; + }; + } // namespace detail + + template + concept ThermodynamicRelationType = detail::IsThermodynamicRelation>::value; + + template + concept ValidRelationCatalog = detail::IsValidRelationCatalog>::value; + + template + inline constexpr bool relationCatalogContains = + detail::CatalogContainsRelation, std::remove_cv_t>::value; + + template + inline constexpr bool relationContainsInput = + detail::RelationContainsInput, std::remove_cv_t>::value; + + template using RelationOutputT = typename RelationType::OutputQuantity; + + template + using RelationInputT = typename detail::QuantityAt::Type; + + using PressureFromDensity = Relation; + using PressureFromSpecificEnthalpy = Relation; + using SpecificEnthalpyFromDensity = Relation; + using SpecificEnthalpyFromPressure = Relation; + using DensityFromSpecificEnthalpy = Relation; +} // namespace mean_field::eos diff --git a/libmeanfield/interface/eos/runtime.cppm b/libmeanfield/interface/eos/runtime.cppm new file mode 100644 index 0000000..cfe5e4c --- /dev/null +++ b/libmeanfield/interface/eos/runtime.cppm @@ -0,0 +1,645 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +export module mean_field:eos.runtime; +export import :eos.evaluation; + +export namespace mean_field::eos { + class ThermodynamicQuantityId final { + public: + explicit constexpr ThermodynamicQuantityId(const std::string_view name) noexcept : m_name(name) { + } + + [[nodiscard]] constexpr std::string_view name() const noexcept { + return m_name; + } + + [[nodiscard]] friend constexpr bool operator==( + const ThermodynamicQuantityId &, + const ThermodynamicQuantityId & + ) noexcept = default; + + private: + std::string_view m_name; + }; + + template + concept RuntimeIdentifiedThermodynamicQuantity = ThermodynamicQuantityType && requires { + { Quantity::identifier } -> std::convertible_to; + } && (std::string_view{Quantity::identifier}.size() > 0); + + template + inline constexpr ThermodynamicQuantityId thermodynamicQuantityId{std::string_view{Quantity::identifier}}; + + struct RuntimeQuantityValue final { + ThermodynamicQuantityId quantity; + double value; + }; + + struct RuntimeRelationDescriptor final { + ThermodynamicQuantityId outputQuantity; + std::span inputQuantities; + std::uint64_t partialDerivativeMask; + + [[nodiscard]] constexpr bool hasPartialDerivative(const std::size_t inputIndex) const noexcept { + return inputIndex < inputQuantities.size() && + (partialDerivativeMask & (std::uint64_t{1} << inputIndex)) != 0; + } + }; + + namespace detail { + template struct HasRuntimeQuantityIdentifiers : std::false_type { }; + + template + struct HasRuntimeQuantityIdentifiers> + : std::bool_constant< + RuntimeIdentifiedThermodynamicQuantity && + (RuntimeIdentifiedThermodynamicQuantity && ...)> { }; + + template struct RuntimeRelationQuantities; + + template struct RuntimeRelationQuantities> { + using Type = std::tuple; + }; + + template + using RuntimeCatalogQuantityTuple = + decltype(std::tuple_cat(std::declval::Type>()...)); + + template < + typename FirstQuantity, + typename SecondQuantity> + [[nodiscard]] consteval bool runtimeQuantityIdentifiersAreCompatible() { + if constexpr (std::same_as) { + return true; + } else { + return thermodynamicQuantityId != thermodynamicQuantityId; + } + } + + template < + typename QuantityTuple, + std::size_t First, + std::size_t... Offsets> + [[nodiscard]] consteval bool runtimeQuantityIdentifierIsUnambiguous(std::index_sequence) { + return ( + runtimeQuantityIdentifiersAreCompatible< + std::tuple_element_t, + std::tuple_element_t>() && + ... + ); + } + + template < + typename QuantityTuple, + std::size_t... Indices> + [[nodiscard]] consteval bool runtimeQuantityIdentifiersAreUnambiguous(std::index_sequence) { + return ( + runtimeQuantityIdentifierIsUnambiguous( + std::make_index_sequence - Indices - 1>{} + ) && + ... + ); + } + + template + struct RuntimeRelationsAreSupported : std::false_type { }; + + template + struct RuntimeRelationsAreSupported + : std::bool_constant>( + std::make_index_sequence>>{} + )> { }; + + template struct RuntimeCatalogIsSupported : std::false_type { }; + + template + struct RuntimeCatalogIsSupported> + : RuntimeRelationsAreSupported<(HasRuntimeQuantityIdentifiers::value && ...), Relations...> { }; + } // namespace detail + + template + concept RuntimeEquationOfStateModel = + EquationOfStateModel && + detail::RuntimeCatalogIsSupported::Relations>::value; + + namespace detail { + template struct RuntimeRelationStorage; + + template + struct RuntimeRelationStorage> { + using RelationType = Relation; + + static_assert( + sizeof...(Inputs) <= 64, + "Runtime EOS relation descriptors support at most 64 inputs." + ); + + inline static constexpr std::array inputQuantityIds{ + thermodynamicQuantityId... + }; + + template + [[nodiscard]] static consteval std::uint64_t makePartialDerivativeMask(std::index_sequence) { + using InputTuple = std::tuple; + + return ( + std::uint64_t{0} | ... | + (SupportsPartialDerivative> + ? (std::uint64_t{1} << Indices) + : std::uint64_t{0}) + ); + } + + inline static constexpr std::uint64_t partialDerivativeMask = + makePartialDerivativeMask(std::index_sequence_for{}); + + inline static constexpr RuntimeRelationDescriptor descriptor{ + thermodynamicQuantityId, std::span{inputQuantityIds}, + partialDerivativeMask + }; + }; + + template struct RuntimeCatalogStorage; + + template + struct RuntimeCatalogStorage> { + inline static constexpr std::array descriptors{ + RuntimeRelationStorage::descriptor... + }; + }; + + [[nodiscard]] inline std::expected< + double, + EvaluationError> + runtimeEvaluationFailure( + const EvaluationErrorCode code, + std::string message + ) { + return std::unexpected{EvaluationError{code, std::move(message)}}; + } + + template < + typename EquationOfState, + typename Output, + typename... Inputs> + [[nodiscard]] std::expected< + double, + EvaluationError> + evaluateRuntimeRelation( + const EquationOfState &equationOfState, + Relation< + Output, + Inputs...>, + const std::span inputValues + ) { + const auto invoke = [&](std::index_sequence) { + return eos::evaluate(equationOfState, QuantityValue{inputValues[Indices].value}...) + .value(); + }; + + try { + return invoke(std::index_sequence_for{}); + } catch (const EvaluationError &error) { + return std::unexpected{error}; + } + } + + template < + typename InputQuantity, + typename EquationOfState, + typename Output, + typename... Inputs> + [[nodiscard]] bool tryRuntimePartialDerivative( + const EquationOfState &equationOfState, + Relation< + Output, + Inputs...> relation, + const ThermodynamicQuantityId withRespectTo, + const std::span inputValues, + std::expected< + double, + EvaluationError> &result + ) { + if (withRespectTo != thermodynamicQuantityId) { + return false; + } + + if constexpr (SupportsPartialDerivative, InputQuantity>) { + const auto invoke = [&](std::index_sequence) { + return eos::partialDerivative( + equationOfState, QuantityValue{inputValues[Indices].value}... + ) + .value(); + }; + + try { + result = invoke(std::index_sequence_for{}); + } catch (const EvaluationError &error) { + result = std::unexpected{error}; + } + } else { + result = runtimeEvaluationFailure( + EvaluationErrorCode::unsupported_derivative, + "The requested EOS partial derivative is not available." + ); + } + + return true; + } + + template < + typename EquationOfState, + typename Output, + typename... Inputs> + [[nodiscard]] std::expected< + double, + EvaluationError> + evaluateRuntimePartialDerivative( + const EquationOfState &equationOfState, + Relation< + Output, + Inputs...> relation, + const ThermodynamicQuantityId withRespectTo, + const std::span inputValues + ) { + std::expected result = runtimeEvaluationFailure( + EvaluationErrorCode::unsupported_derivative, + "The requested quantity is not an input to the EOS relation." + ); + + const bool matched = + (tryRuntimePartialDerivative(equationOfState, relation, withRespectTo, inputValues, result) || + ...); + + static_cast(matched); + return result; + } + + template < + typename EquationOfState, + typename RelationType> + [[nodiscard]] bool runtimeRelationMatches( + const ThermodynamicQuantityId outputQuantity, + const std::span inputValues + ) { + const RuntimeRelationDescriptor &descriptor = + RuntimeRelationStorage::descriptor; + + if (descriptor.outputQuantity != outputQuantity || + descriptor.inputQuantities.size() != inputValues.size()) { + return false; + } + + for (std::size_t index = 0; index < inputValues.size(); ++index) { + if (descriptor.inputQuantities[index] != inputValues[index].quantity) { + return false; + } + } + + return true; + } + + template struct RuntimeCatalogDispatch; + + template + struct RuntimeCatalogDispatch> { + [[nodiscard]] static std::expected< + double, + EvaluationError> + evaluate( + const void *object, + const ThermodynamicQuantityId outputQuantity, + const std::span inputValues + ) { + const auto &equationOfState = *static_cast(object); + + std::expected result = runtimeEvaluationFailure( + EvaluationErrorCode::unsupported_relation, "The requested EOS relation is not available." + ); + + const bool matched = + ((runtimeRelationMatches(outputQuantity, inputValues) + ? (result = evaluateRuntimeRelation(equationOfState, Relations{}, inputValues), true) + : false) || + ...); + + static_cast(matched); + return result; + } + + [[nodiscard]] static std::expected< + double, + EvaluationError> + partialDerivative( + const void *object, + const ThermodynamicQuantityId outputQuantity, + const ThermodynamicQuantityId withRespectTo, + const std::span inputValues + ) { + const auto &equationOfState = *static_cast(object); + + std::expected result = runtimeEvaluationFailure( + EvaluationErrorCode::unsupported_relation, "The requested EOS relation is not available." + ); + + const bool matched = + ((runtimeRelationMatches(outputQuantity, inputValues) + ? (result = evaluateRuntimePartialDerivative( + equationOfState, Relations{}, withRespectTo, inputValues + ), + true) + : false) || + ...); + + static_cast(matched); + return result; + } + }; + + template + using RuntimeAdapter = RuntimeCatalogDispatch; + + template + [[nodiscard]] constexpr std::span runtimeRelationDescriptors() noexcept { + return RuntimeCatalogStorage::descriptors; + } + } // namespace detail + + class EquationOfStateView final { + public: + template + explicit EquationOfStateView(EquationOfState &equationOfState) noexcept + : m_object(std::addressof(equationOfState)), + m_relations(detail::runtimeRelationDescriptors>()), + m_evaluate(&detail::RuntimeAdapter>::evaluate), + m_partialDerivative(&detail::RuntimeAdapter>::partialDerivative) { + } + + [[nodiscard]] std::span relations() const noexcept { + return m_relations; + } + + [[nodiscard]] bool supports( + const ThermodynamicQuantityId outputQuantity, + const std::span inputQuantities + ) const noexcept { + return findRelation(outputQuantity, inputQuantities) != nullptr; + } + + template < + RuntimeIdentifiedThermodynamicQuantity OutputQuantity, + RuntimeIdentifiedThermodynamicQuantity... InputQuantities> + [[nodiscard]] bool supports() const noexcept { + constexpr std::array inputs{ + thermodynamicQuantityId... + }; + + return supports(thermodynamicQuantityId, std::span{inputs}); + } + + [[nodiscard]] std::expected< + RuntimeQuantityValue, + EvaluationError> + tryEvaluate( + const ThermodynamicQuantityId outputQuantity, + const std::span inputValues + ) const { + const auto validation = validateRelationRequest(outputQuantity, inputValues); + + if (!validation.has_value()) { + return std::unexpected{validation.error()}; + } + + auto result = m_evaluate(m_object, outputQuantity, inputValues); + if (!result.has_value()) { + return std::unexpected{result.error()}; + } + + return RuntimeQuantityValue{outputQuantity, *result}; + } + + template < + RuntimeIdentifiedThermodynamicQuantity OutputQuantity, + QuantityValueType... InputValues> + [[nodiscard]] std::expected< + QuantityValue, + EvaluationError> + tryEvaluate(const InputValues... inputValues) const { + constexpr bool inputsHaveRuntimeIdentifiers = + (RuntimeIdentifiedThermodynamicQuantity> && ...); + + static_assert(inputsHaveRuntimeIdentifiers, "Every runtime EOS input quantity needs a stable identifier."); + + const std::array runtimeInputs{ + RuntimeQuantityValue{thermodynamicQuantityId>, inputValues.value()}... + }; + + auto result = tryEvaluate( + thermodynamicQuantityId, std::span{runtimeInputs} + ); + + if (!result.has_value()) { + return std::unexpected{result.error()}; + } + + return QuantityValue{result->value}; + } + + [[nodiscard]] std::expected< + double, + EvaluationError> + tryPartialDerivative( + const ThermodynamicQuantityId outputQuantity, + const ThermodynamicQuantityId withRespectTo, + const std::span inputValues + ) const { + const auto validation = validateRelationRequest(outputQuantity, inputValues); + + if (!validation.has_value()) { + return std::unexpected{validation.error()}; + } + + const RuntimeRelationDescriptor &descriptor = **validation; + bool derivativeAvailable = false; + + for (std::size_t index = 0; index < descriptor.inputQuantities.size(); ++index) { + if (descriptor.inputQuantities[index] == withRespectTo) { + derivativeAvailable = descriptor.hasPartialDerivative(index); + break; + } + } + + if (!derivativeAvailable) { + return runtimeFailure( + EvaluationErrorCode::unsupported_derivative, + "The requested EOS partial derivative is not available." + ); + } + + return m_partialDerivative(m_object, outputQuantity, withRespectTo, inputValues); + } + + template < + RuntimeIdentifiedThermodynamicQuantity OutputQuantity, + RuntimeIdentifiedThermodynamicQuantity InputQuantity, + QuantityValueType... InputValues> + [[nodiscard]] std::expected< + PartialDerivative< + OutputQuantity, + InputQuantity>, + EvaluationError> + tryPartialDerivative(const InputValues... inputValues) const { + constexpr bool inputsHaveRuntimeIdentifiers = + (RuntimeIdentifiedThermodynamicQuantity> && ...); + + static_assert(inputsHaveRuntimeIdentifiers, "Every runtime EOS input quantity needs a stable identifier."); + + const std::array runtimeInputs{ + RuntimeQuantityValue{thermodynamicQuantityId>, inputValues.value()}... + }; + + auto result = tryPartialDerivative( + thermodynamicQuantityId, thermodynamicQuantityId, + std::span{runtimeInputs} + ); + + if (!result.has_value()) { + return std::unexpected{result.error()}; + } + + return PartialDerivative{*result}; + } + + private: + using RuntimeEvaluateFunction = std::expected< + double, + EvaluationError> (*)( + const void *, + ThermodynamicQuantityId, + std::span + ); + + using RuntimePartialDerivativeFunction = std::expected< + double, + EvaluationError> (*)( + const void *, + ThermodynamicQuantityId, + ThermodynamicQuantityId, + std::span + ); + + [[nodiscard]] const RuntimeRelationDescriptor *findRelation( + const ThermodynamicQuantityId outputQuantity, + const std::span inputQuantities + ) const noexcept { + for (const RuntimeRelationDescriptor &descriptor : m_relations) { + if (descriptor.outputQuantity != outputQuantity || + descriptor.inputQuantities.size() != inputQuantities.size()) { + continue; + } + + bool matches = true; + for (std::size_t index = 0; index < inputQuantities.size(); ++index) { + if (descriptor.inputQuantities[index] != inputQuantities[index]) { + matches = false; + break; + } + } + + if (matches) { + return std::addressof(descriptor); + } + } + + return nullptr; + } + + [[nodiscard]] std::expected< + const RuntimeRelationDescriptor *, + EvaluationError> + validateRelationRequest( + const ThermodynamicQuantityId outputQuantity, + const std::span inputValues + ) const { + bool outputAvailable = false; + bool inputCountAvailable = false; + + for (const RuntimeRelationDescriptor &descriptor : m_relations) { + if (descriptor.outputQuantity != outputQuantity) { + continue; + } + + outputAvailable = true; + + if (descriptor.inputQuantities.size() != inputValues.size()) { + continue; + } + + inputCountAvailable = true; + bool matches = true; + + for (std::size_t index = 0; index < inputValues.size(); ++index) { + if (descriptor.inputQuantities[index] != inputValues[index].quantity) { + matches = false; + break; + } + } + + if (matches) { + return std::addressof(descriptor); + } + } + + if (!outputAvailable) { + return runtimeFailure( + EvaluationErrorCode::unsupported_relation, + "The EOS does not provide a relation for output quantity '" + std::string{outputQuantity.name()} + + "'." + ); + } + + if (!inputCountAvailable) { + return runtimeFailure( + EvaluationErrorCode::wrong_input_count, "No EOS relation for output quantity '" + + std::string{outputQuantity.name()} + + "' accepts the supplied number of inputs." + ); + } + + return runtimeFailure( + EvaluationErrorCode::wrong_input_quantity, "No EOS relation for output quantity '" + + std::string{outputQuantity.name()} + + "' accepts the supplied input quantities." + ); + } + + template + [[nodiscard]] static std::expected< + Value, + EvaluationError> + runtimeFailure( + const EvaluationErrorCode code, + std::string message + ) { + return std::unexpected{EvaluationError{code, std::move(message)}}; + } + + const void *m_object; + std::span m_relations; + RuntimeEvaluateFunction m_evaluate; + RuntimePartialDerivativeFunction m_partialDerivative; + }; +} // namespace mean_field::eos diff --git a/libmeanfield/interface/field/field_mfem.cppm b/libmeanfield/interface/field/field_mfem.cppm index e7c014d..d30eec2 100644 --- a/libmeanfield/interface/field/field_mfem.cppm +++ b/libmeanfield/interface/field/field_mfem.cppm @@ -1,6 +1,7 @@ module; #include +#include #include #include #include @@ -888,6 +889,282 @@ export namespace mean_field::field { mfem::Array m_trueToReduced; }; + /* + * Boundary rows expressed in a field's reduced solver ordering. + * + * This object is deliberately independent of any particular physical + * surface condition. Its template constructor below combines a field, + * a semantic boundary, and a domain schema. Consequently the same + * topology machinery can be used by any compiled surface formulation; + * it is not tied to enthalpy or pressure. + */ + class FieldBoundaryDofMap final { + public: + FieldBoundaryDofMap() = default; + + FieldBoundaryDofMap( + const int fieldReducedSize, + const mfem::Array &boundaryReducedDofs + ) + : m_fieldReducedSize(fieldReducedSize), + m_boundaryReducedDofs(boundaryReducedDofs) { + if (m_fieldReducedSize < 0) { + throw std::invalid_argument("FieldBoundaryDofMap requires a non-negative field size."); + } + + m_boundaryReducedDofMarker.SetSize(m_fieldReducedSize); + m_boundaryReducedDofMarker = 0; + + int previousReducedDof = -1; + for (const int reducedDof : m_boundaryReducedDofs) { + if (reducedDof < 0 || reducedDof >= m_fieldReducedSize) { + throw std::invalid_argument("FieldBoundaryDofMap contains a DOF outside the reduced field vector."); + } + if (reducedDof <= previousReducedDof) { + throw std::invalid_argument("FieldBoundaryDofMap indices must be strictly increasing and unique."); + } + + m_boundaryReducedDofMarker[reducedDof] = 1; + previousReducedDof = reducedDof; + } + } + + [[nodiscard]] int field_size() const noexcept { + return m_fieldReducedSize; + } + + [[nodiscard]] int size() const noexcept { + return m_boundaryReducedDofs.Size(); + } + + [[nodiscard]] bool empty() const noexcept { + return size() == 0; + } + + [[nodiscard]] const mfem::Array &reduced_dofs() const noexcept { + return m_boundaryReducedDofs; + } + + [[nodiscard]] const mfem::Array &reduced_dof_marker() const noexcept { + return m_boundaryReducedDofMarker; + } + + [[nodiscard]] bool contains(const int reducedDof) const { + if (reducedDof < 0 || reducedDof >= m_fieldReducedSize) { + throw std::out_of_range("Reduced DOF index is outside FieldBoundaryDofMap."); + } + return m_boundaryReducedDofMarker[reducedDof] != 0; + } + + private: + int m_fieldReducedSize{0}; + mfem::Array m_boundaryReducedDofs; + mfem::Array m_boundaryReducedDofMarker; + }; + + /* Point-supported rows in a field's reduced solver ordering. */ + class FieldPointDofMap final { + public: + FieldPointDofMap() = default; + + FieldPointDofMap( + const int fieldReducedSize, + const mfem::Array &pointReducedDofs + ) + : m_selectedDofs( + fieldReducedSize, + pointReducedDofs + ) { + } + + [[nodiscard]] int field_size() const noexcept { + return m_selectedDofs.field_size(); + } + + [[nodiscard]] int size() const noexcept { + return m_selectedDofs.size(); + } + + [[nodiscard]] bool empty() const noexcept { + return m_selectedDofs.empty(); + } + + [[nodiscard]] const mfem::Array &reduced_dofs() const noexcept { + return m_selectedDofs.reduced_dofs(); + } + + [[nodiscard]] const mfem::Array &reduced_dof_marker() const noexcept { + return m_selectedDofs.reduced_dof_marker(); + } + + [[nodiscard]] bool contains(const int reducedDof) const { + return m_selectedDofs.contains(reducedDof); + } + + private: + FieldBoundaryDofMap m_selectedDofs; + }; + + template < + MfemDomainField FieldT, + utils::domain::IsBoundary BoundaryT, + utils::domain::IsSchema SchemaT> + [[nodiscard]] FieldBoundaryDofMap make_field_boundary_dof_map( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const FieldDofMap &fieldDofMap + ) { + static_assert( + SchemaT::template contains_boundary(), + "The requested boundary is not registered in the supplied DomainSchema." + ); + + MFEM_VERIFY( + !finiteElementSpace.Nonconforming(), + "Field boundary true-DOF resolution currently requires a conforming mfem::ParFiniteElementSpace." + ); + MFEM_VERIFY( + fieldDofMap.full_size() == finiteElementSpace.GetTrueVSize(), + "The field map and finite-element space have incompatible true-DOF sizes." + ); + + const mfem::Mesh *mesh = finiteElementSpace.GetMesh(); + MFEM_VERIFY(mesh != nullptr, "Field boundary DOF resolution requires an MFEM mesh."); + + mfem::Array boundaryVDofMarker(finiteElementSpace.GetVSize()); + boundaryVDofMarker = 0; + + mfem::Array boundaryElementVDofs; + for (int boundaryElement = 0; boundaryElement < mesh->GetNBE(); ++boundaryElement) { + if (!SchemaT::template boundary_attribute_matches(mesh->GetBdrAttribute(boundaryElement))) { + continue; + } + + finiteElementSpace.GetBdrElementVDofs(boundaryElement, boundaryElementVDofs); + for (const int encodedVDof : boundaryElementVDofs) { + const int vdof = mfem::FiniteElementSpace::DecodeDof(encodedVDof); + MFEM_VERIFY( + vdof >= 0 && vdof < finiteElementSpace.GetVSize(), "MFEM returned an invalid boundary vector DOF." + ); + boundaryVDofMarker[vdof] = 1; + } + } + + finiteElementSpace.Synchronize(boundaryVDofMarker); + + mfem::Array boundaryReducedDofMarker(fieldDofMap.reduced_size()); + boundaryReducedDofMarker = 0; + + for (int vdof = 0; vdof < boundaryVDofMarker.Size(); ++vdof) { + if (boundaryVDofMarker[vdof] == 0) { + continue; + } + + const int trueDof = finiteElementSpace.GetLocalTDofNumber(vdof); + if (trueDof < 0) { + continue; + } + + const std::optional reducedDof = fieldDofMap.reduced_dof(trueDof); + MFEM_VERIFY( + reducedDof.has_value(), + "A boundary DOF selected for the field is absent from that field's reduced solver map." + ); + boundaryReducedDofMarker[*reducedDof] = 1; + } + + mfem::Array boundaryReducedDofs; + mfem::FiniteElementSpace::MarkerToList(boundaryReducedDofMarker, boundaryReducedDofs); + return FieldBoundaryDofMap(fieldDofMap.reduced_size(), boundaryReducedDofs); + } + + template + [[nodiscard]] FieldPointDofMap make_field_point_dof_map( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const FieldDofMap &fieldDofMap, + const mfem::Vector &point, + const double tolerance + ) { + MFEM_VERIFY( + !finiteElementSpace.Nonconforming(), + "Field point true-DOF resolution currently requires a conforming mfem::ParFiniteElementSpace." + ); + MFEM_VERIFY( + fieldDofMap.full_size() == finiteElementSpace.GetTrueVSize(), + "The field map and finite-element space have incompatible true-DOF sizes." + ); + MFEM_VERIFY( + std::isfinite(tolerance) && tolerance >= 0.0, "The field point tolerance must be finite and non-negative." + ); + + const mfem::Mesh *mesh = finiteElementSpace.GetMesh(); + MFEM_VERIFY(mesh != nullptr, "Field point DOF resolution requires an MFEM mesh."); + MFEM_VERIFY( + point.Size() == mesh->SpaceDimension(), "The requested field point has the wrong coordinate dimension." + ); + + mfem::Array pointVDofMarker(finiteElementSpace.GetVSize()); + pointVDofMarker = 0; + + mfem::Array vertexVDofs; + for (int vertex = 0; vertex < mesh->GetNV(); ++vertex) { + const mfem::real_t *coordinates = mesh->GetVertex(vertex); + double distanceSquared = 0.0; + for (int component = 0; component < point.Size(); ++component) { + const double difference = coordinates[component] - point(component); + distanceSquared += difference * difference; + } + if (std::sqrt(distanceSquared) > tolerance) { + continue; + } + + finiteElementSpace.GetVertexVDofs(vertex, vertexVDofs); + for (const int encodedVDof : vertexVDofs) { + const int vdof = mfem::FiniteElementSpace::DecodeDof(encodedVDof); + MFEM_VERIFY( + vdof >= 0 && vdof < finiteElementSpace.GetVSize(), "MFEM returned an invalid point vector DOF." + ); + pointVDofMarker[vdof] = 1; + } + } + + finiteElementSpace.Synchronize(pointVDofMarker); + + mfem::Array pointReducedDofMarker(fieldDofMap.reduced_size()); + pointReducedDofMarker = 0; + for (int vdof = 0; vdof < pointVDofMarker.Size(); ++vdof) { + if (pointVDofMarker[vdof] == 0) { + continue; + } + + const int trueDof = finiteElementSpace.GetLocalTDofNumber(vdof); + if (trueDof < 0) { + continue; + } + + const std::optional reducedDof = fieldDofMap.reduced_dof(trueDof); + MFEM_VERIFY( + reducedDof.has_value(), + "A point DOF selected for the field is absent from that field's reduced solver map." + ); + pointReducedDofMarker[*reducedDof] = 1; + } + + mfem::Array pointReducedDofs; + mfem::FiniteElementSpace::MarkerToList(pointReducedDofMarker, pointReducedDofs); + + const long long localPointDofCount = pointReducedDofs.Size(); + long long globalPointDofCount = 0; + MPI_Allreduce( + &localPointDofCount, &globalPointDofCount, 1, MPI_LONG_LONG, MPI_SUM, finiteElementSpace.GetComm() + ); + MFEM_VERIFY( + globalPointDofCount == finiteElementSpace.GetVDim(), + "The requested geometric point must identify exactly one field vertex globally." + ); + + return FieldPointDofMap(fieldDofMap.reduced_size(), pointReducedDofs); + } + /* * Canonical adapter between an MFEM GridFunction and a reduced field * vector. @@ -1015,9 +1292,6 @@ export namespace mean_field::field { [[nodiscard]] FieldDofGridFunctionAdapter make_field_dof_grid_function_adapter(const mfem::ParFiniteElementSpace &finiteElementSpace) { - return FieldDofGridFunctionAdapter( - make_field_dof_map(finiteElementSpace), - finiteElementSpace - ); + return FieldDofGridFunctionAdapter(make_field_dof_map(finiteElementSpace), finiteElementSpace); } } // namespace mean_field::field diff --git a/libmeanfield/interface/integrators/pressure_gradient.cppm b/libmeanfield/interface/integrators/pressure_gradient.cppm index b8c736c..14fdaa1 100644 --- a/libmeanfield/interface/integrators/pressure_gradient.cppm +++ b/libmeanfield/interface/integrators/pressure_gradient.cppm @@ -41,7 +41,11 @@ export namespace mean_field::integrators { const mfem::GridFunction &compactification_coordinate, utils::EOS_P eos ) - : m_mapping(mapper, displacement, compactification_coordinate), + : m_mapping( + mapper, + displacement, + compactification_coordinate + ), m_eos(std::move(eos)) { } diff --git a/libmeanfield/interface/mapping/domain_mapper.cppm b/libmeanfield/interface/mapping/domain_mapper.cppm index dc07633..ae87580 100644 --- a/libmeanfield/interface/mapping/domain_mapper.cppm +++ b/libmeanfield/interface/mapping/domain_mapper.cppm @@ -8,259 +8,288 @@ import :mapping.compactification; import :utils.user; export namespace mean_field::mapping { -enum class FaceElementSide : uint8_t { element_1, element_2 }; + 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); + 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; + [[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; -}; + 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; -}; + struct CompactificationPointData { + double coordinate{0.0}; + mfem::Vector coordinate_gradient; + }; -[[nodiscard]] ElementDisplacementData -ElementDisplacementDataFromElementVDofs(const mfem::FiniteElement &element, - const mfem::Vector &displacement_dofs); + [[nodiscard]] ElementDisplacementData ElementDisplacementDataFromElementVDofs( + const mfem::FiniteElement &element, + const mfem::Vector &displacement_dofs + ); -class ElementCompactificationData { -public: - ElementCompactificationData(const mfem::FiniteElement &element, - const mfem::Vector &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; + [[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; -}; + private: + const mfem::FiniteElement *m_element; + mfem::Vector m_dofs; + }; -struct ElementMappingData { - const ElementDisplacementData &displacement; - const ElementCompactificationData &compactification; -}; + struct ElementMappingData { + const ElementDisplacementData &displacement; + const ElementCompactificationData &compactification; + }; -class DomainMapper { -public: - class Workspace { - public: - explicit Workspace(int dimension = 3); + class DomainMapper { + public: + class Workspace { + public: + explicit Workspace(int dimension = 3); - void SetDimension(int dimension); + void SetDimension(int dimension); - [[nodiscard]] int GetDimension() const noexcept; + [[nodiscard]] int GetDimension() const noexcept; - private: - friend class DomainMapper; + private: + friend class DomainMapper; - int m_dimension; + 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_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_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_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; + 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; - }; + compactification::ExteriorMapResult m_exterior_result; + compactification::ExteriorMapVariation m_exterior_variation; + }; -public: - DomainMapper( - utils::DomainMapperOptions options, - std::unique_ptr exterior_map); + 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; + 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 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 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 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 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 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]] 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; + [[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; + 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; + 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]] 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 mfem::ElementTransformation &SelectFaceElementTransformation( + mfem::FaceElementTransformations &transformation, + FaceElementSide side + ); - [[nodiscard]] static const mfem::IntegrationPoint & - SelectFaceElementIntegrationPoint( - 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; -}; + 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); + 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; + /* + * 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(); + /* + * 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 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 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]] 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]] 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); + [[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); + 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); + 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; + 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}; -}; + 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 3a1b381..100ae3f 100644 --- a/libmeanfield/interface/mean_field.cppm +++ b/libmeanfield/interface/mean_field.cppm @@ -54,10 +54,19 @@ export import :operators.prepared_displacement_residual; export import :model.structure_profile; export import :model.structure.base; export import :model.structure.polytropic; -export import :eos.base; +export import :eos.quantities; +export import :eos.relations; +export import :eos.concepts; +export import :eos.evaluation; +export import :eos.pressure_surface; +export import :eos.runtime; export import :eos.polytrope; -export import :surface.base; -export import :surface.isobaric; +export import :surface.constant; +export import :surface.dependencies; +export import :surface.compiled; +export import :surface.compiler; export import :model.stellar; export import :operators.prepared_mass_normalization; +export import :operators.prepared_centering_constraint; +export import :operators.prepared_surface_constraint; export import :operators.prepared_stellar_equilibrium; diff --git a/libmeanfield/interface/models/stellar_model.cppm b/libmeanfield/interface/models/stellar_model.cppm index 6a277fd..7915e9b 100644 --- a/libmeanfield/interface/models/stellar_model.cppm +++ b/libmeanfield/interface/models/stellar_model.cppm @@ -7,62 +7,84 @@ module; export module mean_field:model.stellar; -export import :eos.base; +export import :eos.runtime; export import :model.structure.base; -export import :surface.base; +export import :surface.compiler; export namespace mean_field::models { - template - concept StructurePrescription = - std::derived_from, mean_field::models::structure::StructureBase>; + namespace detail { + template + concept ConstEquationOfStateReference = + std::is_lvalue_reference_v && std::is_const_v> && + eos::EquationOfStateModel>; + } // namespace detail template - concept SurfacePrescription = std::derived_from, mean_field::surface::SurfaceBase>; + concept StructurePrescription = requires( + const std::remove_cvref_t &structurePrescription, + const structure::StructureSeedRequest &seedRequest + ) { + { structurePrescription.equationOfState() } noexcept -> detail::ConstEquationOfStateReference; + { structurePrescription.targetMass() } noexcept -> std::same_as; + { structurePrescription.makeInitialSeed(seedRequest) } -> std::same_as; + { structurePrescription.validate() } -> std::same_as; + }; - /* - * Public ownership facade for a physical structure prescription and its - * stellar-surface prescription. - * - * The concrete prescriptions are allocated once at construction. Their - * stable addresses allow future prepared operators and contexts to borrow - * references without making ownership part of the user-facing API. - */ + template + using StructureEquationOfStateT = + std::remove_cvref_t &>().equationOfState())>; + + template + concept SurfacePrescription = + surface::ConstantPressureSurfaceType && + surface::PressureSurfaceCompilable>; + + template + requires SurfacePrescription> class StellarModel final { public: - template < - StructurePrescription StructureType, - SurfacePrescription SurfaceType> + using StructurePrescriptionType = Structure; + using SurfacePrescriptionType = surface::ConstantPressureSurface; + using EquationOfStateType = StructureEquationOfStateT; + using SurfaceConstraintType = + surface::CompiledPressureSurfaceConstraintT; + + template + requires std::same_as< + std::remove_cvref_t, + Structure> explicit StellarModel( - StructureType &&structurePrescription, - SurfaceType &&surfacePrescription + StructureArgument &&structurePrescription, + const surface::ConstantPressureSurface surfacePrescription ) - : StellarModel( - std::make_unique>( - std::forward(structurePrescription) - ), - std::make_unique>(std::forward(surfacePrescription)) + : m_structurePrescription( + std::make_unique(std::forward(structurePrescription)) + ), + m_surfacePrescription(std::make_unique(surfacePrescription)), + m_compiledSurfaceConstraint( + std::make_unique(validateAndCompileSurface( + *m_structurePrescription, + *m_surfacePrescription + )) ) { } ~StellarModel() = default; StellarModel(const StellarModel &) = delete; - StellarModel &operator=(const StellarModel &) = delete; - StellarModel(StellarModel &&) noexcept = default; - StellarModel &operator=(StellarModel &&) noexcept = default; - [[nodiscard]] const mean_field::models::structure::StructureBase &structurePrescription() const noexcept { + [[nodiscard]] const Structure &structurePrescription() const noexcept { return *m_structurePrescription; } - [[nodiscard]] const mean_field::surface::SurfaceBase &surfacePrescription() const noexcept { + [[nodiscard]] const surface::ConstantPressureSurface &surfacePrescription() const noexcept { return *m_surfacePrescription; } - [[nodiscard]] const mean_field::eos::EquationOfState &equationOfState() const noexcept { + [[nodiscard]] const EquationOfStateType &equationOfState() const noexcept { return m_structurePrescription->equationOfState(); } @@ -70,45 +92,99 @@ export namespace mean_field::models { return m_structurePrescription->targetMass(); } - [[nodiscard]] mean_field::models::structure::StructureSeed - makeInitialSeed(const mean_field::models::structure::StructureSeedRequest &request) const { + [[nodiscard]] structure::StructureSeed makeInitialSeed(const structure::StructureSeedRequest &request) const { return m_structurePrescription->makeInitialSeed(request); } - [[nodiscard]] const mean_field::surface::ResolvedSurfaceCondition &resolvedSurfaceCondition() const noexcept { - return m_resolvedSurfaceCondition; + [[nodiscard]] const SurfaceConstraintType &compiledSurfaceConstraint() const noexcept { + return *m_compiledSurfaceConstraint; } private: - explicit StellarModel( - std::unique_ptr structurePrescription, - std::unique_ptr surfacePrescription - ) - : m_structurePrescription(std::move(structurePrescription)), - m_surfacePrescription(std::move(surfacePrescription)), - m_resolvedSurfaceCondition(validateAndResolve( - *m_structurePrescription, - *m_surfacePrescription - )) { - } - - [[nodiscard]] static mean_field::surface::ResolvedSurfaceCondition validateAndResolve( - const mean_field::models::structure::StructureBase &structurePrescription, - const mean_field::surface::SurfaceBase &surfacePrescription + [[nodiscard]] static SurfaceConstraintType validateAndCompileSurface( + const Structure &structurePrescription, + const surface::ConstantPressureSurface &surfacePrescription ) { structurePrescription.validate(); - const mean_field::eos::EquationOfState &equationOfState = structurePrescription.equationOfState(); - - surfacePrescription.validate(equationOfState); - - return surfacePrescription.resolve(equationOfState); + return surface::compilePressureSurfaceConstraint( + surfacePrescription, structurePrescription.equationOfState() + ); } - std::unique_ptr m_structurePrescription; + std::unique_ptr m_structurePrescription; + std::unique_ptr m_surfacePrescription; + std::unique_ptr m_compiledSurfaceConstraint; + }; - std::unique_ptr m_surfacePrescription; + template + StellarModel( + Structure &&, + surface::ConstantPressureSurface + ) -> StellarModel>; - mean_field::surface::ResolvedSurfaceCondition m_resolvedSurfaceCondition; + namespace detail { + template struct IsStellarModel : std::false_type { }; + + template struct IsStellarModel> : std::true_type { }; + } // namespace detail + + template + concept StellarModelType = detail::IsStellarModel>::value; + + class StellarModelView final { + public: + template + requires StellarModelType && + eos::RuntimeEquationOfStateModel::EquationOfStateType> + explicit StellarModelView(Model &model) noexcept + : m_equationOfState(model.equationOfState()), + m_structurePrescription(std::addressof(model.structurePrescription())), + m_makeInitialSeed(&makeInitialSeedFor::StructurePrescriptionType>), + m_targetMass(model.targetMass()), + m_surfaceCondition(model.compiledSurfaceConstraint().descriptor()), + m_surfaceDependencies(model.compiledSurfaceConstraint().runtimeDependencies()) { + } + + [[nodiscard]] eos::EquationOfStateView equationOfState() const noexcept { + return m_equationOfState; + } + + [[nodiscard]] double targetMass() const noexcept { + return m_targetMass; + } + + [[nodiscard]] structure::StructureSeed makeInitialSeed(const structure::StructureSeedRequest &request) const { + return m_makeInitialSeed(m_structurePrescription, request); + } + + [[nodiscard]] surface::PressureSurfaceDescriptor surfaceCondition() const noexcept { + return m_surfaceCondition; + } + + [[nodiscard]] surface::RuntimeSurfaceConstraintDependencies surfaceDependencies() const noexcept { + return m_surfaceDependencies; + } + + private: + using MakeInitialSeedFunction = structure::StructureSeed (*)( + const void *, + const structure::StructureSeedRequest & + ); + + template + [[nodiscard]] static structure::StructureSeed makeInitialSeedFor( + const void *structurePrescription, + const structure::StructureSeedRequest &request + ) { + return static_cast(structurePrescription)->makeInitialSeed(request); + } + + eos::EquationOfStateView m_equationOfState; + const void *m_structurePrescription; + MakeInitialSeedFunction m_makeInitialSeed; + double m_targetMass; + surface::PressureSurfaceDescriptor m_surfaceCondition; + surface::RuntimeSurfaceConstraintDependencies m_surfaceDependencies; }; } // namespace mean_field::models diff --git a/libmeanfield/interface/models/structure/polytropic.cppm b/libmeanfield/interface/models/structure/polytropic.cppm index 2fe049f..812f6c9 100644 --- a/libmeanfield/interface/models/structure/polytropic.cppm +++ b/libmeanfield/interface/models/structure/polytropic.cppm @@ -12,20 +12,20 @@ export import :model.structure.base; import :utils.misc; export namespace mean_field::models::structure { - class PolytropicStructure final : public StructureBase { + class PolytropicStructure final { public: explicit PolytropicStructure( eos::Polytrope equationOfState, double targetMass ); - [[nodiscard]] const eos::EquationOfState &equationOfState() const noexcept override; + [[nodiscard]] const eos::Polytrope &equationOfState() const noexcept; - [[nodiscard]] double targetMass() const noexcept override; + [[nodiscard]] double targetMass() const noexcept; - [[nodiscard]] StructureSeed makeInitialSeed(const StructureSeedRequest &request) const override; + [[nodiscard]] StructureSeed makeInitialSeed(const StructureSeedRequest &request) const; - void validate() const override; + void validate() const; private: struct LaneEmdenPoint { diff --git a/libmeanfield/interface/models/structure/structure_base.cppm b/libmeanfield/interface/models/structure/structure_base.cppm index 58dcdfd..a3cfd4c 100644 --- a/libmeanfield/interface/models/structure/structure_base.cppm +++ b/libmeanfield/interface/models/structure/structure_base.cppm @@ -1,7 +1,7 @@ module; #include export module mean_field:model.structure.base; -export import :eos.base; +export import :eos.runtime; export namespace mean_field::models::structure { struct StructureSeed { @@ -23,7 +23,7 @@ export namespace mean_field::models::structure { public: virtual ~StructureBase() = default; - [[nodiscard]] virtual const eos::EquationOfState &equationOfState() const noexcept = 0; + [[nodiscard]] virtual eos::EquationOfStateView equationOfState() const noexcept = 0; [[nodiscard]] virtual double targetMass() const noexcept = 0; @@ -34,4 +34,4 @@ export namespace mean_field::models::structure { protected: StructureBase() = default; }; -} // namespace mean_field::models::structure \ No newline at end of file +} // namespace mean_field::models::structure diff --git a/libmeanfield/interface/operators/prepared_centering_constraint.cppm b/libmeanfield/interface/operators/prepared_centering_constraint.cppm new file mode 100644 index 0000000..49d3ca4 --- /dev/null +++ b/libmeanfield/interface/operators/prepared_centering_constraint.cppm @@ -0,0 +1,103 @@ +module; + +#include +#include + +#include + +export module mean_field:operators.prepared_centering_constraint; + +export import :field.mfem; + +export namespace mean_field::operators { + struct PreparedCenteringConstraintReport final { + bool cachedCenterDisplacement{false}; + + [[nodiscard]] bool DidAnyWork() const noexcept { + return cachedCenterDisplacement; + } + }; + + /* + * Strong translational gauge: the material point at the computational + * origin has zero displacement. The three corresponding displacement + * residual rows replace redundant force-balance rows. + */ + class PreparedCenteringConstraint final { + public: + explicit PreparedCenteringConstraint(field::FieldPointDofMap centerRows) + : m_centerRows(std::move(centerRows)), + m_centerDisplacement(m_centerRows.size()) { + } + + [[nodiscard]] PreparedCenteringConstraintReport Prepare( + const mfem::Vector &displacement, + const bool displacementChanged + ) { + MFEM_VERIFY( + displacement.Size() == m_centerRows.field_size(), + "The centering constraint received a displacement vector with the wrong size." + ); + + PreparedCenteringConstraintReport report; + if (!m_isPrepared || displacementChanged) { + for (int centerIndex = 0; centerIndex < m_centerRows.size(); ++centerIndex) { + const double value = displacement(m_centerRows.reduced_dofs()[centerIndex]); + MFEM_VERIFY( + std::isfinite(value), "The centering constraint received a non-finite center displacement." + ); + m_centerDisplacement(centerIndex) = value; + } + report.cachedCenterDisplacement = true; + } + + m_isPrepared = true; + return report; + } + + void ApplyResidualRows(mfem::Vector &displacementResidual) const { + VerifyPrepared(); + MFEM_VERIFY( + displacementResidual.Size() == m_centerRows.field_size(), + "The centering constraint received a displacement residual with the wrong size." + ); + + for (int centerIndex = 0; centerIndex < m_centerRows.size(); ++centerIndex) { + displacementResidual(m_centerRows.reduced_dofs()[centerIndex]) = m_centerDisplacement(centerIndex); + } + } + + void ApplyJacobianRows( + const mfem::Vector &displacementVariation, + mfem::Vector &displacementAction + ) const { + VerifyPrepared(); + MFEM_VERIFY( + displacementVariation.Size() == m_centerRows.field_size() && + displacementAction.Size() == m_centerRows.field_size(), + "The centering constraint received a Jacobian vector with the wrong size." + ); + + for (const int centerRow : m_centerRows.reduced_dofs()) { + displacementAction(centerRow) = displacementVariation(centerRow); + } + } + + [[nodiscard]] bool IsPrepared() const noexcept { + return m_isPrepared; + } + + [[nodiscard]] const field::FieldPointDofMap &GetCenterRows() const noexcept { + return m_centerRows; + } + + private: + void VerifyPrepared() const { + MFEM_VERIFY(m_isPrepared, "The centering constraint must be prepared before row application."); + } + + field::FieldPointDofMap m_centerRows; + mfem::Vector m_centerDisplacement; + bool m_isPrepared{false}; + }; +} // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm index bbb2ede..108a4d7 100644 --- a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm +++ b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm @@ -1,7 +1,9 @@ module; #include +#include #include +#include #include @@ -16,9 +18,11 @@ export import :operators.context.gravity_field; export import :operators.gravity_field; export import :operators.gravity_field_jacobian; export import :operators.prepared_barotropic_closure; +export import :operators.prepared_centering_constraint; export import :operators.prepared_displacement_residual; export import :operators.prepared_hydrostatic_equilibrium; export import :operators.prepared_mass_normalization; +export import :operators.prepared_surface_constraint; export import :physics.rigid_rotation; export import :utils.blocks; @@ -50,11 +54,14 @@ export namespace mean_field::operators { PreparedHydrostaticEquilibriumReport hydrostatic; PreparedDisplacementResidualReport displacement; PreparedMassNormalizationReport massNormalization; + PreparedSurfaceConstraintReport surfaceConstraint; + PreparedCenteringConstraintReport centeringConstraint; bool assembledResidual{false}; [[nodiscard]] bool DidAnyChildWork() const noexcept { return gravity.DidAnyWork() || barotropicClosure.DidAnyWork() || hydrostatic.DidAnyWork() || - displacement.DidAnyWork() || massNormalization.DidAnyWork(); + displacement.DidAnyWork() || massNormalization.DidAnyWork() || surfaceConstraint.DidAnyWork() || + centeringConstraint.DidAnyWork(); } [[nodiscard]] bool DidAnyWork() const noexcept { @@ -74,19 +81,27 @@ export namespace mean_field::operators { class PreparedStellarEquilibriumOperator final : public mfem::Operator { public: + template + requires std::same_as< + typename std::remove_cvref_t::EquationOfStateType, + eos::Polytrope> && + SingleFieldPressureSurfaceConstraintFor< + typename std::remove_cvref_t::SurfaceConstraintType, + field::Enthalpy> && + std::is_lvalue_reference_v PreparedStellarEquilibriumOperator( fem::FEM &f, const mapping::DomainMapper &domainMapper, - const eos::Polytrope &equationOfState, - double targetMass - ); - - PreparedStellarEquilibriumOperator( - fem::FEM &f, - const mapping::DomainMapper &domainMapper, - const eos::Polytrope &equationOfState, - const models::StellarModel &stellarModel - ); + Model &&stellarModel + ) + : PreparedStellarEquilibriumOperator( + f, + domainMapper, + stellarModel.equationOfState(), + stellarModel.targetMass(), + PressureSurfaceConstraintView{stellarModel.compiledSurfaceConstraint()} + ) { + } PreparedStellarEquilibriumOperator(const PreparedStellarEquilibriumOperator &) = delete; PreparedStellarEquilibriumOperator &operator=(const PreparedStellarEquilibriumOperator &) = delete; @@ -122,6 +137,8 @@ export namespace mean_field::operators { [[nodiscard]] const PreparedHydrostaticEquilibriumOperator &GetHydrostaticOperator() const noexcept; [[nodiscard]] const PreparedDisplacementResidualOperator &GetDisplacementOperator() const noexcept; [[nodiscard]] const PreparedMassNormalizationOperator &GetMassNormalizationOperator() const noexcept; + [[nodiscard]] const PreparedPressureSurfaceConstraint &GetSurfaceConstraintOperator() const noexcept; + [[nodiscard]] const PreparedCenteringConstraint &GetCenteringConstraintOperator() const noexcept; private: struct ConstructionData; @@ -133,6 +150,15 @@ export namespace mean_field::operators { const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, double targetMass, + PressureSurfaceConstraintView surfaceConstraint + ); + + PreparedStellarEquilibriumOperator( + fem::FEM &f, + const mapping::DomainMapper &domainMapper, + const eos::Polytrope &equationOfState, + double targetMass, + PressureSurfaceConstraintView surfaceConstraint, ConstructionData constructionData ); @@ -150,6 +176,8 @@ export namespace mean_field::operators { PreparedHydrostaticEquilibriumOperator m_hydrostaticOperator; PreparedDisplacementResidualOperator m_displacementOperator; PreparedMassNormalizationOperator m_massNormalizationOperator; + PreparedPressureSurfaceConstraint m_surfaceConstraintOperator; + PreparedCenteringConstraint m_centeringConstraintOperator; StellarEquilibriumDependencies m_preparedDependencies; mfem::Vector m_cachedResidual; diff --git a/libmeanfield/interface/operators/prepared_surface_constraint.cppm b/libmeanfield/interface/operators/prepared_surface_constraint.cppm new file mode 100644 index 0000000..85ac6d1 --- /dev/null +++ b/libmeanfield/interface/operators/prepared_surface_constraint.cppm @@ -0,0 +1,235 @@ +module; + +#include +#include +#include +#include +#include + +#include + +export module mean_field:operators.prepared_surface_constraint; + +export import :field.mfem; +export import :surface.compiled; + +namespace mean_field::operators::detail { + template struct SingleQuantitySurfaceState final { + eos::QuantityValue quantityValue; + + [[nodiscard]] eos::QuantityValue value(Quantity) const noexcept { + return quantityValue; + } + }; +} // namespace mean_field::operators::detail + +export namespace mean_field::operators { + /* + * Runtime enforcement currently supports a pointwise pressure constraint + * whose row field is also its sole state field. The concept is expressed + * entirely in compiled-constraint metadata: no thermodynamic carrier or + * concrete field is selected by this prepared layer. + */ + template + concept SingleFieldPressureSurfaceConstraint = + requires { + typename std::remove_cvref_t::PhysicalQuantity; + typename std::remove_cvref_t::CarrierQuantity; + typename std::remove_cvref_t::CarrierField; + typename std::remove_cvref_t::SurfaceDependencies; + } && std::same_as::PhysicalQuantity, eos::quantity::Pressure> && + std::same_as< + typename std::remove_cvref_t::SurfaceDependencies::RowField, + typename std::remove_cvref_t::CarrierField> && + std::same_as< + typename std::remove_cvref_t::SurfaceDependencies::StateFieldTypes, + field::TypeList::CarrierField>>; + + template + concept SingleFieldPressureSurfaceConstraintFor = + SingleFieldPressureSurfaceConstraint && + std::same_as::SurfaceDependencies::RowField, Field>; + + /* + * Non-owning runtime bridge for a statically compiled pressure constraint. + * There is one function-pointer dispatch per complete row application; + * the concrete loop remains templated so EOS operations can be inlined. + */ + class PressureSurfaceConstraintView final { + public: + template + explicit PressureSurfaceConstraintView(const Constraint &constraint) noexcept + : m_constraint(std::addressof(constraint)), + m_applyResidualRows(&applyResidualRows), + m_applyJacobianRows(&applyJacobianRows), + m_descriptor(constraint.descriptor()) { + } + + void ApplyResidualRows( + const mfem::Vector &surfaceState, + const field::FieldBoundaryDofMap &surfaceRows, + mfem::Vector &rowResidual + ) const { + m_applyResidualRows(m_constraint, surfaceState, surfaceRows, rowResidual); + } + + void ApplyJacobianRows( + const mfem::Vector &surfaceState, + const field::FieldBoundaryDofMap &surfaceRows, + const mfem::Vector &stateVariation, + mfem::Vector &rowAction + ) const { + m_applyJacobianRows(m_constraint, surfaceState, surfaceRows, stateVariation, rowAction); + } + + [[nodiscard]] surface::PressureSurfaceDescriptor descriptor() const noexcept { + return m_descriptor; + } + + private: + using ApplyResidualRowsFunction = void (*)( + const void *, + const mfem::Vector &, + const field::FieldBoundaryDofMap &, + mfem::Vector & + ); + using ApplyJacobianRowsFunction = void (*)( + const void *, + const mfem::Vector &, + const field::FieldBoundaryDofMap &, + const mfem::Vector &, + mfem::Vector & + ); + + template + static void applyResidualRows( + const void *constraint, + const mfem::Vector &surfaceState, + const field::FieldBoundaryDofMap &surfaceRows, + mfem::Vector &rowResidual + ) { + using CarrierQuantity = typename Constraint::CarrierQuantity; + + for (int surfaceIndex = 0; surfaceIndex < surfaceRows.size(); ++surfaceIndex) { + const detail::SingleQuantitySurfaceState state{ + eos::QuantityValue{surfaceState(surfaceIndex)} + }; + rowResidual(surfaceRows.reduced_dofs()[surfaceIndex]) = + static_cast(constraint)->residual(state); + } + } + + template + static void applyJacobianRows( + const void *constraint, + const mfem::Vector &surfaceState, + const field::FieldBoundaryDofMap &surfaceRows, + const mfem::Vector &stateVariation, + mfem::Vector &rowAction + ) { + using CarrierQuantity = typename Constraint::CarrierQuantity; + + for (int surfaceIndex = 0; surfaceIndex < surfaceRows.size(); ++surfaceIndex) { + const int reducedDof = surfaceRows.reduced_dofs()[surfaceIndex]; + const detail::SingleQuantitySurfaceState state{ + eos::QuantityValue{surfaceState(surfaceIndex)} + }; + const detail::SingleQuantitySurfaceState variation{ + eos::QuantityValue{stateVariation(reducedDof)} + }; + rowAction(reducedDof) = static_cast(constraint)->jacobianAction(state, variation); + } + } + + const void *m_constraint; + ApplyResidualRowsFunction m_applyResidualRows; + ApplyJacobianRowsFunction m_applyJacobianRows; + surface::PressureSurfaceDescriptor m_descriptor; + }; + + struct PreparedSurfaceConstraintReport final { + bool cachedSurfaceState{false}; + + [[nodiscard]] bool DidAnyWork() const noexcept { + return cachedSurfaceState; + } + }; + + class PreparedPressureSurfaceConstraint final { + public: + PreparedPressureSurfaceConstraint( + field::FieldBoundaryDofMap surfaceRows, + const PressureSurfaceConstraintView constraint + ) + : m_surfaceRows(std::move(surfaceRows)), + m_constraint(constraint), + m_surfaceState(m_surfaceRows.size()) { + } + + [[nodiscard]] PreparedSurfaceConstraintReport Prepare( + const mfem::Vector &reducedState, + const bool stateChanged + ) { + MFEM_VERIFY( + reducedState.Size() == m_surfaceRows.field_size(), + "The pressure surface constraint received a state vector with the wrong size." + ); + + PreparedSurfaceConstraintReport report; + if (!m_isPrepared || stateChanged) { + for (int surfaceIndex = 0; surfaceIndex < m_surfaceRows.size(); ++surfaceIndex) { + const double value = reducedState(m_surfaceRows.reduced_dofs()[surfaceIndex]); + MFEM_VERIFY(std::isfinite(value), "The pressure surface constraint received non-finite state."); + m_surfaceState(surfaceIndex) = value; + } + report.cachedSurfaceState = true; + } + + m_isPrepared = true; + return report; + } + + void ApplyResidualRows(mfem::Vector &rowResidual) const { + VerifyPrepared(); + MFEM_VERIFY( + rowResidual.Size() == m_surfaceRows.field_size(), + "The pressure surface constraint received a residual vector with the wrong size." + ); + m_constraint.ApplyResidualRows(m_surfaceState, m_surfaceRows, rowResidual); + } + + void ApplyJacobianRows( + const mfem::Vector &stateVariation, + mfem::Vector &rowAction + ) const { + VerifyPrepared(); + MFEM_VERIFY( + stateVariation.Size() == m_surfaceRows.field_size() && rowAction.Size() == m_surfaceRows.field_size(), + "The pressure surface constraint received a Jacobian vector with the wrong size." + ); + m_constraint.ApplyJacobianRows(m_surfaceState, m_surfaceRows, stateVariation, rowAction); + } + + [[nodiscard]] bool IsPrepared() const noexcept { + return m_isPrepared; + } + + [[nodiscard]] const field::FieldBoundaryDofMap &GetSurfaceRows() const noexcept { + return m_surfaceRows; + } + + [[nodiscard]] surface::PressureSurfaceDescriptor GetPhysicalCondition() const noexcept { + return m_constraint.descriptor(); + } + + private: + void VerifyPrepared() const { + MFEM_VERIFY(m_isPrepared, "The pressure surface constraint must be prepared before row application."); + } + + field::FieldBoundaryDofMap m_surfaceRows; + PressureSurfaceConstraintView m_constraint; + mfem::Vector m_surfaceState; + bool m_isPrepared{false}; + }; +} // namespace mean_field::operators diff --git a/libmeanfield/interface/surface/compiled.cppm b/libmeanfield/interface/surface/compiled.cppm new file mode 100644 index 0000000..ef69674 --- /dev/null +++ b/libmeanfield/interface/surface/compiled.cppm @@ -0,0 +1,66 @@ +module; + +export module mean_field:surface.compiled; + +export import :eos.pressure_surface; +export import :surface.constant; +export import :surface.dependencies; + +export namespace mean_field::surface { + template < + eos::EquationOfStateModel EquationOfState, + SurfaceConstraintFormulationType Formulation, + eos::ThermodynamicRelationType SelectedRelation, + typename Dependencies> + class CompiledPressureSurfaceConstraint final { + public: + using PhysicalCondition = ConstantPressureSurface; + using PhysicalQuantity = eos::quantity::Pressure; + using CarrierQuantity = typename Formulation::CarrierQuantity; + using CarrierField = typename Formulation::CarrierField; + using Relation = SelectedRelation; + using SurfaceDependencies = Dependencies; + + CompiledPressureSurfaceConstraint( + const ConstantPressureSurface condition, + const EquationOfState &equationOfState + ) noexcept + : m_condition(condition), + m_resolvedRelation( + equationOfState, + condition.targetPressure() + ) { + } + + [[nodiscard]] eos::PressureValue targetPressure() const noexcept { + return m_condition.targetPressure(); + } + + [[nodiscard]] PressureSurfaceDescriptor descriptor() const noexcept { + return m_condition.descriptor(); + } + + [[nodiscard]] static constexpr RuntimeSurfaceConstraintDependencies runtimeDependencies() noexcept { + return SurfaceDependencies::runtimeDescription(); + } + + template [[nodiscard]] double residual(const SurfaceState &state) const { + return state.value(CarrierQuantity{}).value() - m_resolvedRelation.requiredCarrierValue(state).value(); + } + + template < + typename SurfaceState, + typename SurfaceVariation> + [[nodiscard]] double jacobianAction( + const SurfaceState &state, + const SurfaceVariation &variation + ) const { + return variation.value(CarrierQuantity{}).value() - + m_resolvedRelation.carrierCorrectionJacobianAction(state, variation); + } + + private: + ConstantPressureSurface m_condition; + eos::ResolvedPressureSurfaceRelation m_resolvedRelation; + }; +} // namespace mean_field::surface diff --git a/libmeanfield/interface/surface/compiler.cppm b/libmeanfield/interface/surface/compiler.cppm new file mode 100644 index 0000000..d7c1153 --- /dev/null +++ b/libmeanfield/interface/surface/compiler.cppm @@ -0,0 +1,143 @@ +module; + +#include +#include +#include + +export module mean_field:surface.compiler; + +export import :surface.compiled; + +export namespace mean_field::surface { + namespace detail { + template + struct PressureSurfaceRelationMatches : std::false_type { }; + + template + struct PressureSurfaceRelationMatches< + eos::Relation, + Formulation, + EquationOfState> + : std::bool_constant< + std::same_as && + (std::same_as || ...) && + ((std::same_as || + (surfaceBindingCount == 1 && + eos::SupportsPartialDerivative< + EquationOfState, + eos::Relation, + InputQuantities>)) && + ...)> { }; + + template + struct MatchingPressureSurfaceRelations; + + template + struct MatchingPressureSurfaceRelations, Formulation, EquationOfState> { + using Tuple = decltype(std::tuple_cat( + std::conditional_t< + PressureSurfaceRelationMatches::value, + std::tuple, + std::tuple<>>{}... + )); + + static constexpr std::size_t count = std::tuple_size_v; + }; + + template struct UniquePressureSurfaceRelation { + using Type = void; + }; + + template struct UniquePressureSurfaceRelation<1, Tuple> { + using Type = std::tuple_element_t<0, Tuple>; + }; + + template struct AppendSurfaceDependency; + + template + struct AppendSurfaceDependency, Field> { + using Type = SurfaceConstraintDependencies; + }; + + template + struct AppendPressureSurfaceInputDependency { + using Type = + typename AppendSurfaceDependency>::Type; + }; + + template + struct AppendPressureSurfaceInputDependency { + using Type = Dependencies; + }; + + template + struct AppendPressureSurfaceInputDependencies; + + template + struct AppendPressureSurfaceInputDependencies { + using Type = Dependencies; + }; + + template + struct AppendPressureSurfaceInputDependencies { + using WithFirst = typename AppendPressureSurfaceInputDependency::Type; + using Type = typename AppendPressureSurfaceInputDependencies::Type; + }; + + template struct PressureSurfaceDependenciesForRelation; + + template + struct PressureSurfaceDependenciesForRelation, Formulation> { + using InitialDependencies = + SurfaceConstraintDependencies; + using Type = typename AppendPressureSurfaceInputDependencies< + InitialDependencies, + typename Formulation::StateBindings, + InputQuantities...>::Type; + }; + + template + struct PressureSurfaceCompilation { + using Matches = + MatchingPressureSurfaceRelations; + using Relation = typename UniquePressureSurfaceRelation::Type; + }; + + template + requires(PressureSurfaceCompilation::Matches::count == 1) + struct CompiledPressureSurfaceConstraintType { + using Compilation = PressureSurfaceCompilation; + using Relation = typename Compilation::Relation; + using Dependencies = typename PressureSurfaceDependenciesForRelation::Type; + using Type = CompiledPressureSurfaceConstraint; + }; + } // namespace detail + + template + concept PressureSurfaceCompilable = + SurfaceConstraintFormulationType && eos::EquationOfStateModel && + (detail::PressureSurfaceCompilation, std::remove_cvref_t>:: + Matches::count == 1); + + template + requires PressureSurfaceCompilable + using CompiledPressureSurfaceConstraintT = typename detail::CompiledPressureSurfaceConstraintType< + std::remove_cvref_t, + std::remove_cvref_t>::Type; + + template < + SurfaceConstraintFormulationType Formulation, + eos::EquationOfStateModel EquationOfState> + requires PressureSurfaceCompilable< + Formulation, + EquationOfState> + [[nodiscard]] CompiledPressureSurfaceConstraintT< + Formulation, + EquationOfState> + compilePressureSurfaceConstraint( + const ConstantPressureSurface condition, + const EquationOfState &equationOfState + ) noexcept { + return CompiledPressureSurfaceConstraintT{condition, equationOfState}; + } +} // namespace mean_field::surface diff --git a/libmeanfield/interface/surface/constant.cppm b/libmeanfield/interface/surface/constant.cppm new file mode 100644 index 0000000..8478bb8 --- /dev/null +++ b/libmeanfield/interface/surface/constant.cppm @@ -0,0 +1,65 @@ +module; + +#include +#include +#include +#include + +export module mean_field:surface.constant; + +export import :eos.quantities; + +export namespace mean_field::surface { + struct PressureSurfaceDescriptor final { + double targetPressure; + }; + + /* + * The only physical surface prescription currently supported by + * MeanField. It says nothing about which thermodynamic variable appears + * in a nonlinear state vector; resolving pressure into that representation + * is an EOS responsibility. + */ + class ConstantPressureSurface final { + public: + using PhysicalQuantity = eos::quantity::Pressure; + using TargetValue = eos::PressureValue; + + explicit ConstantPressureSurface(const TargetValue targetPressure) : m_targetPressure(targetPressure) { + if (!std::isfinite(targetPressure.value())) { + throw std::invalid_argument( + std::format( + "The target surface pressure must be finite. Instead P = {} was provided.", + targetPressure.value() + ) + ); + } + if (targetPressure.value() < 0.0) { + throw std::invalid_argument( + std::format( + "The target surface pressure must be non-negative. Instead P = {} was provided.", + targetPressure.value() + ) + ); + } + } + + [[nodiscard]] TargetValue targetPressure() const noexcept { + return m_targetPressure; + } + + [[nodiscard]] PressureSurfaceDescriptor descriptor() const noexcept { + return PressureSurfaceDescriptor{.targetPressure = m_targetPressure.value()}; + } + + private: + TargetValue m_targetPressure; + }; + + template + concept ConstantPressureSurfaceType = std::same_as, ConstantPressureSurface>; + + // Familiar physical terminology retained as a synonym, not as a second + // surface-condition type. + using Isobaric = ConstantPressureSurface; +} // namespace mean_field::surface diff --git a/libmeanfield/interface/surface/dependencies.cppm b/libmeanfield/interface/surface/dependencies.cppm new file mode 100644 index 0000000..597675c --- /dev/null +++ b/libmeanfield/interface/surface/dependencies.cppm @@ -0,0 +1,186 @@ +module; + +#include +#include +#include +#include +#include +#include + +export module mean_field:surface.dependencies; + +export import :eos.relations; +export import :field.registry; + +export namespace mean_field::surface { + template + concept SurfaceFieldType = requires { + { Candidate::name } -> std::convertible_to; + } && (std::string_view{Candidate::name}.size() > 0); + + class SurfaceFieldId final { + public: + explicit constexpr SurfaceFieldId(const std::string_view name) noexcept : m_name(name) { + } + + [[nodiscard]] constexpr std::string_view name() const noexcept { + return m_name; + } + + [[nodiscard]] friend constexpr bool operator==( + const SurfaceFieldId &, + const SurfaceFieldId & + ) noexcept = default; + + private: + std::string_view m_name; + }; + + template inline constexpr SurfaceFieldId surfaceFieldId{std::string_view{Field::name}}; + + template + struct SurfaceStateBinding final { + using Quantity = ThermodynamicQuantity; + using FieldType = Field; + }; + + template struct SurfaceStateBindings final { }; + + namespace detail { + template struct SurfaceTypesAreUnique : std::true_type { }; + + template + struct SurfaceTypesAreUnique + : std::bool_constant< + (!std::same_as && ...) && SurfaceTypesAreUnique::value> { }; + + template struct SurfaceBindingsAreValid : std::false_type { }; + + template + struct SurfaceBindingsAreValid> + : std::bool_constant< + (sizeof...(Bindings) > 0) && + (requires { + typename Bindings::Quantity; + typename Bindings::FieldType; + } && ...) && + (eos::ThermodynamicQuantityType< + typename Bindings::Quantity> && ...) && + (SurfaceFieldType && ...) && + SurfaceTypesAreUnique< + typename Bindings::Quantity...>::value> { }; + + template struct SurfaceBindingCount; + + template + struct SurfaceBindingCount, Quantity> + : std::integral_constant< + std::size_t, + (std::size_t{0} + ... + + (std::same_as ? std::size_t{1} : std::size_t{0}))> { + }; + + template struct SurfaceFieldForQuantity; + + template + struct SurfaceFieldForQuantity, Quantity> + : std::conditional_t< + std::same_as, + std::type_identity, + SurfaceFieldForQuantity, Quantity>> { }; + + template + struct CarrierFieldMatchesSurfaceBinding : std::false_type { }; + + template + struct CarrierFieldMatchesSurfaceBinding + : std::bool_constant::type, + typename Candidate::CarrierField>> { }; + + template < + typename Candidate, + bool BindingsAreValid = SurfaceBindingsAreValid::value> + struct FormulationBindingsMatchCarrier : std::false_type { }; + + template + struct FormulationBindingsMatchCarrier + : CarrierFieldMatchesSurfaceBinding< + Candidate, + SurfaceBindingCount< + typename Candidate::StateBindings, + typename Candidate::CarrierQuantity>::value> { }; + + template + struct IsSurfaceConstraintFormulation : std::false_type { }; + + template + struct IsSurfaceConstraintFormulation< + Candidate, + std::void_t< + typename Candidate::CarrierQuantity, + typename Candidate::CarrierField, + typename Candidate::StateBindings>> + : std::bool_constant< + eos::ThermodynamicQuantityType && + SurfaceFieldType && + FormulationBindingsMatchCarrier::value> { }; + } // namespace detail + + template + concept ValidSurfaceStateBindings = detail::SurfaceBindingsAreValid>::value; + + template + inline constexpr std::size_t surfaceBindingCount = detail::SurfaceBindingCount::value; + + template + requires(surfaceBindingCount == 1) + using SurfaceFieldForQuantityT = typename detail::SurfaceFieldForQuantity::type; + + template < + eos::ThermodynamicQuantityType CarrierThermodynamicQuantity, + SurfaceFieldType CarrierFieldType, + ValidSurfaceStateBindings Bindings> + requires( + surfaceBindingCount == 1 && + std::same_as, CarrierFieldType> + ) + struct SurfaceConstraintFormulation final { + using CarrierQuantity = CarrierThermodynamicQuantity; + using CarrierField = CarrierFieldType; + using StateBindings = Bindings; + }; + + using BarotropicSurfaceFormulation = SurfaceConstraintFormulation< + eos::quantity::SpecificEnthalpy, + field::Enthalpy, + SurfaceStateBindings>>; + + template + concept SurfaceConstraintFormulationType = + detail::IsSurfaceConstraintFormulation>::value; + + struct RuntimeSurfaceConstraintDependencies final { + SurfaceFieldId residualRowField; + std::span stateFields; + }; + + template + struct SurfaceConstraintDependencies final { + using RowField = ResidualField; + using StateFieldTypes = field::TypeList; + + inline static constexpr std::array runtimeStateFields{ + surfaceFieldId... + }; + + [[nodiscard]] static constexpr RuntimeSurfaceConstraintDependencies runtimeDescription() noexcept { + return RuntimeSurfaceConstraintDependencies{ + .residualRowField = surfaceFieldId, + .stateFields = std::span{runtimeStateFields} + }; + } + }; +} // namespace mean_field::surface diff --git a/libmeanfield/interface/surface/isobaric.cppm b/libmeanfield/interface/surface/isobaric.cppm deleted file mode 100644 index d5223bb..0000000 --- a/libmeanfield/interface/surface/isobaric.cppm +++ /dev/null @@ -1,64 +0,0 @@ -module; - -#include -#include -#include - -export module mean_field:surface.isobaric; - -export import :surface.base; - -export namespace mean_field::surface { - class Isobaric final : public SurfaceBase { - public: - explicit Isobaric(const double targetPressure = 0.0) : m_targetPressure(targetPressure) { - validateTargetPressure(); - } - - [[nodiscard]] double targetPressure() const noexcept { - return m_targetPressure; - } - - [[nodiscard]] ResolvedSurfaceCondition - resolve(const mean_field::eos::EquationOfState &equationOfState) const override { - return ResolvedSurfaceCondition{resolveTargetEnthalpy(equationOfState)}; - } - - void validate(const mean_field::eos::EquationOfState &equationOfState) const override { - static_cast(resolveTargetEnthalpy(equationOfState)); - } - - private: - [[nodiscard]] double resolveTargetEnthalpy(const mean_field::eos::EquationOfState &equationOfState) const { - validateTargetPressure(); - - const double targetEnthalpy = equationOfState.enthalpy_from_pressure(m_targetPressure); - - if (!std::isfinite(targetEnthalpy) || targetEnthalpy < 0.0) { - throw std::domain_error( - std::format( - "The equation of state resolved the isobaric " - "target P = {} to the invalid enthalpy h = {}.", - m_targetPressure, targetEnthalpy - ) - ); - } - - return targetEnthalpy; - } - - void validateTargetPressure() const { - if (!std::isfinite(m_targetPressure) || m_targetPressure < 0.0) { - throw std::invalid_argument( - std::format( - "The target surface pressure must be finite and " - "non-negative. Instead P = {} was provided.", - m_targetPressure - ) - ); - } - } - - double m_targetPressure; - }; -} // namespace mean_field::surface diff --git a/libmeanfield/interface/surface/surface_base.cppm b/libmeanfield/interface/surface/surface_base.cppm deleted file mode 100644 index 75a6b5b..0000000 --- a/libmeanfield/interface/surface/surface_base.cppm +++ /dev/null @@ -1,53 +0,0 @@ -module; - -#include -#include - -export module mean_field:surface.base; - -export import :eos.base; - -export namespace mean_field::surface { - struct ResolvedSurfaceCondition final { - double targetEnthalpy{0.0}; - - explicit ResolvedSurfaceCondition(const double requestedTargetEnthalpy) - : targetEnthalpy(requestedTargetEnthalpy) { - if (!std::isfinite(targetEnthalpy) || targetEnthalpy < 0.0) { - throw std::invalid_argument( - "A resolved surface enthalpy must be finite and " - "non-negative." - ); - } - } - - [[nodiscard]] double residual(const double enthalpy) const { - if (!std::isfinite(enthalpy)) { - throw std::invalid_argument("A surface enthalpy value must be finite."); - } - - return enthalpy - targetEnthalpy; - } - - [[nodiscard]] static double jacobianAction(const double enthalpyVariation) { - if (!std::isfinite(enthalpyVariation)) { - throw std::invalid_argument("A surface enthalpy variation must be finite."); - } - - return enthalpyVariation; - } - }; - - class SurfaceBase { - public: - virtual ~SurfaceBase() = default; - - [[nodiscard]] virtual ResolvedSurfaceCondition - resolve(const mean_field::eos::EquationOfState &equationOfState) const = 0; - - virtual void validate(const mean_field::eos::EquationOfState &equationOfState) const = 0; - - protected: - SurfaceBase() = default; - }; -} // namespace mean_field::surface diff --git a/libmeanfield/interface/utils/domain.cppm b/libmeanfield/interface/utils/domain.cppm index cc3e7c6..824f25b 100644 --- a/libmeanfield/interface/utils/domain.cppm +++ b/libmeanfield/interface/utils/domain.cppm @@ -11,1179 +11,1118 @@ 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; - } - } - } - - 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); - } - - 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< - RelationValidationResult::InscribedDiagnostics>( - {.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< - RelationValidationResult::InscribedDiagnostics>( - {.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< - 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); + 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; } - if (visitedElementCount == domainElementCount) { - return {.connectedDiagnostics = std::make_optional< - RelationValidationResult::ConnectedDiagnostics>( - {.domainElementCount = domainElementCount, - .visitedElementCount = visitedElementCount})}; + 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; } - int disconnectedElementId = -1; + template struct MaterialDomainsAreUnique; - for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { - const std::size_t index = static_cast(elementId); + template <> struct MaterialDomainsAreUnique<> : std::true_type { }; - if (belongsToDomain[index] && !visited[index]) { - disconnectedElementId = elementId; + template struct MaterialDomainsAreUnique : std::true_type { }; - break; - } - } + template + struct MaterialDomainsAreUnique + : std::bool_constant< + (!std::is_same_v && + ...) && + MaterialDomainsAreUnique::value> { }; - return { - .failure = RelationValidationFailure::DomainDisconnected, - .connectedDiagnostics = - std::make_optional( - {.elementId = disconnectedElementId, - .domainElementCount = domainElementCount, - .visitedElementCount = visitedElementCount})}; - } -}; + template struct BoundaryTypesAreUnique; -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 BoundaryTypesAreUnique<> : std::true_type { }; - static_assert(SchemaT::template contains_boundary(), - "DomainBoundary refers to a boundary which is not " - "registered in the supplied DomainSchema."); + template struct BoundaryTypesAreUnique : std::true_type { }; - static_assert((SchemaT::template contains_domain() && ...), - "DomainBoundary refers to a domain which is not " - "completely registered in the supplied DomainSchema."); + template + struct BoundaryTypesAreUnique + : std::bool_constant< + (!std::is_same_v && + ...) && + BoundaryTypesAreUnique::value> { }; - constexpr int expectedBoundaryAttribute = - SchemaT::template boundary_attribute(); + template + concept HaveUniqueMaterialIds = material_ids_are_unique(); - using DomainsTuple = std::tuple; + 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< + MaterialDescriptor, + count> 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< + BoundaryDescriptor, + count> 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; /* - * Record all MFEM boundary elements associated with each - * mesh face. + * Compile-time validation that every semantic entity + * referenced by a relation is registered by the schema. * - * 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. + * Connected and Inscribed only reference domains. + * DomainBoundary references both a boundary and one or + * two domains. */ - std::vector> boundaryElementsByFace( - static_cast(mesh.GetNumFaces())); + template + struct RelationUsesRegisteredEntities; - for (int boundaryElementId = 0; boundaryElementId < mesh.GetNBE(); - ++boundaryElementId) { - const int faceId = mesh.GetBdrElementFaceIndex(boundaryElementId); + template + struct RelationUsesRegisteredEntities, MaterialsT, BoundariesT> + : std::bool_constant::registered> { }; - if (faceId >= 0 && faceId < mesh.GetNumFaces()) { - boundaryElementsByFace[static_cast(faceId)].push_back( - boundaryElementId); - } - } + template + struct RelationUsesRegisteredEntities, MaterialsT, BoundariesT> + : std::bool_constant< + DomainMaterialResolver::registered && + DomainMaterialResolver::registered> { }; - /* - * 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}; + template + struct RelationUsesRegisteredEntities, MaterialsT, BoundariesT> + : std::bool_constant< + BoundaryAttributeResolver::registered && + (DomainMaterialResolver::registered && ...)> { }; - if (faceId < 0 || faceId >= mesh.GetNumFaces()) { - return diagnostics; - } + template + struct RelationsUseRegisteredEntities; - mesh.GetFaceElements(faceId, &diagnostics.firstElementId, - &diagnostics.secondElementId); + template + struct RelationsUseRegisteredEntities> + : std::bool_constant<(RelationUsesRegisteredEntities::value && ...)> { }; - if (diagnostics.firstElementId >= 0) { - diagnostics.firstMaterialId = - mesh.GetAttribute(diagnostics.firstElementId); - } + template + concept HaveValidRelationEntities = RelationsUseRegisteredEntities::value; - if (diagnostics.secondElementId >= 0) { - diagnostics.secondMaterialId = - mesh.GetAttribute(diagnostics.secondElementId); - } + template + requires HaveValidRelationEntities + struct DomainSchema { + using materials_type = Materials; + using boundaries_type = Boundaries; + using relations_type = Relations; - return diagnostics; + 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}; }; - /* - * 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; + std::optional inscribedDiagnostics = std::nullopt; - const bool secondExists = secondElementId >= 0; + struct ConnectedDiagnostics { + int elementId{-1}; + int domainElementCount{0}; + int visitedElementCount{0}; + }; - return firstExists != secondExists; - } else { - return firstElementId >= 0 && secondElementId >= 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(); + } }; - /* - * 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>; + template struct RelationValidator; - const bool firstExists = firstElementId >= 0; + 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." + ); - const bool secondExists = secondElementId >= 0; + static_assert( + SchemaT::template contains_domain(), "The outer domain of Inscribed is not " + "registered in the supplied schema." + ); - if (firstExists == secondExists) { - return false; + 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 {}; } - - 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; + 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." + ); - /* - * 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); + std::vector belongsToDomain(static_cast(mesh.GetNE()), false); - if (boundaryAttribute != expectedBoundaryAttribute) { - continue; - } + int domainElementCount = 0; + int firstDomainElement = -1; - foundTaggedBoundary = true; + for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { + const int materialId = mesh.GetAttribute(elementId); - const int faceId = mesh.GetBdrElementFaceIndex(boundaryElementId); + const bool belongs = SchemaT::template attribute_belongs_to(materialId); - int firstElementId = -1; - int secondElementId = -1; + belongsToDomain[static_cast(elementId)] = belongs; - mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); + if (!belongs) { + continue; + } - if (!has_required_topology(firstElementId, secondElementId)) { - return {.failure = RelationValidationFailure:: - DomainBoundaryTaggedFaceHasWrongTopology, - .domainBoundaryDiagnostics = std::make_optional< - RelationValidationResult::DomainBoundaryDiagnostics>( - make_diagnostics(faceId, boundaryElementId, - boundaryAttribute))}; - } + ++domainElementCount; - if (!face_matches_domains(firstElementId, secondElementId)) { - return {.failure = RelationValidationFailure:: - DomainBoundaryTaggedFaceTouchesUnexpectedMaterial, - .domainBoundaryDiagnostics = std::make_optional< - RelationValidationResult::DomainBoundaryDiagnostics>( - make_diagnostics(faceId, boundaryElementId, - boundaryAttribute))}; - } - } + if (firstDomainElement < 0) { + firstDomainElement = elementId; + } + } - bool foundExpectedFace = false; + if (domainElementCount == 0) { + return { + .failure = RelationValidationFailure::DomainAbsent, + .connectedDiagnostics = std::make_optional( + {.domainElementCount = 0, .visitedElementCount = 0} + ) + }; + } - /* - * 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; + std::vector> adjacency(static_cast(mesh.GetNE())); - mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); + for (int faceId = 0; faceId < mesh.GetNumFaces(); ++faceId) { + int firstElementId = -1; + int secondElementId = -1; - if (!face_matches_domains(firstElementId, secondElementId)) { - continue; - } + mesh.GetFaceElements(faceId, &firstElementId, &secondElementId); - foundExpectedFace = true; + if (firstElementId < 0 || secondElementId < 0) { + continue; + } - const auto &boundaryElementIds = - boundaryElementsByFace[static_cast(faceId)]; + const bool firstBelongs = belongsToDomain[static_cast(firstElementId)]; - if (boundaryElementIds.empty()) { - return { - .failure = - RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged, - .domainBoundaryDiagnostics = std::make_optional< - RelationValidationResult::DomainBoundaryDiagnostics>( - make_diagnostics(faceId, -1, std::nullopt))}; - } + const bool secondBelongs = belongsToDomain[static_cast(secondElementId)]; - for (const int boundaryElementId : boundaryElementIds) { - const int actualBoundaryAttribute = - mesh.GetBdrAttribute(boundaryElementId); + if (!(firstBelongs && secondBelongs)) { + continue; + } - if (actualBoundaryAttribute == expectedBoundaryAttribute) { - 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(); } - return {.failure = RelationValidationFailure:: - DomainBoundaryExpectedFaceHasWrongAttribute, - .domainBoundaryDiagnostics = std::make_optional< - RelationValidationResult::DomainBoundaryDiagnostics>( - make_diagnostics(faceId, boundaryElementId, - actualBoundaryAttribute))}; - } + [[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 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))}; + template < + IsDomainOrSet DomainT, + IsSchema SchemaT> + [[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; } - return {}; - } -}; + using CoreEnvelopeVacuumDomainSchema = DomainSchema< + MaterialList, Material, Material>, + BoundaryList, BoundaryAttribute>, + RelationList< + // All Domains must be fully connected + Connected, + Connected, + Connected, -struct SchemaRelationValidationResult { - std::size_t relationIndex{0}; - std::string_view relationName; - RelationValidationResult result; + // 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, - [[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>>; + // 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 e0a847d..eee3515 100644 --- a/libmeanfield/interface/utils/misc.cppm +++ b/libmeanfield/interface/utils/misc.cppm @@ -11,82 +11,90 @@ export module mean_field:utils.misc; 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) { - using Schema = domain::CoreEnvelopeVacuumDomainSchema; + 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) { - 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; + 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; } - 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"; + bool is_vacuum( + const mfem::ElementTransformation &Tr, + const mfem::Array2D &elmats + ) { + using Schema = domain::CoreEnvelopeVacuumDomainSchema; -constexpr double G = 1.0; -constexpr double MASS = 1.0; -constexpr double RADIUS = 1.0; + 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 true; + } + return false; + } -[[maybe_unused]] constexpr char HOST[10] = "localhost"; -[[maybe_unused]] constexpr int PORT = 19916; + 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"; -template -concept is_xad = std::is_same_v> || - std::is_same_v> || - std::is_same_v>; + constexpr double G = 1.0; + constexpr double MASS = 1.0; + constexpr double RADIUS = 1.0; -template -concept is_real = std::is_floating_point_v || is_xad; + [[maybe_unused]] constexpr char HOST[10] = "localhost"; + [[maybe_unused]] constexpr int PORT = 19916; -template -using EOS_P = std::function; + template + concept is_xad = std::is_same_v> || std::is_same_v> || + std::is_same_v>; -enum class DOMAINS : uint8_t { - CORE = 1 << 0, - ENVELOPE = 1 << 1, - VACUUM = 1 << 2, - STELLAR = CORE | ENVELOPE, - ALL = CORE | ENVELOPE | VACUUM -}; + template + concept is_real = std::is_floating_point_v || is_xad; -DOMAINS operator|(DOMAINS lhs, DOMAINS rhs); + template using EOS_P = std::function; -DOMAINS operator&(DOMAINS lhs, DOMAINS rhs); + enum class DOMAINS : uint8_t { + CORE = 1 << 0, + ENVELOPE = 1 << 1, + VACUUM = 1 << 2, + STELLAR = CORE | ENVELOPE, + ALL = CORE | ENVELOPE | VACUUM + }; -int get_mesh_order(const mfem::Mesh &mesh); + DOMAINS operator|( + DOMAINS lhs, + DOMAINS rhs + ); + + DOMAINS operator&( + DOMAINS lhs, + DOMAINS rhs + ); + + int get_mesh_order(const mfem::Mesh &mesh); } // namespace mean_field::utils diff --git a/tests/field/field_dof_map.cpp b/tests/field/field_dof_map.cpp index 0564e40..e0f50ff 100644 --- a/tests/field/field_dof_map.cpp +++ b/tests/field/field_dof_map.cpp @@ -94,6 +94,65 @@ namespace field_dof_map_test_utils { domain::RelationList<>>; } // namespace field_dof_map_test_utils +TEST_CASE( + "Field Boundary DOF Map Selects The Stellar Surface In Reduced Field Ordering", + tags::surface_boundary_dof_topology +) { + namespace domain = mean_field::utils::domain; + namespace field = mean_field::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 field::FieldDofMap enthalpyMap = + field::make_field_dof_map(*f.enthalpyFes); + const field::FieldBoundaryDofMap stellarSurface = + field::make_field_boundary_dof_map( + *f.enthalpyFes, enthalpyMap + ); + CHECK(stellarSurface.field_size() == enthalpyMap.reduced_size()); + CHECK(field_dof_map_test_utils::global_sum(stellarSurface.size()) > 0); + CHECK( + field_dof_map_test_utils::global_sum(stellarSurface.size()) < + field_dof_map_test_utils::global_sum(enthalpyMap.reduced_size()) + ); + + for (const int reducedDof : stellarSurface.reduced_dofs()) { + CAPTURE(reducedDof); + CHECK(stellarSurface.contains(reducedDof)); + CHECK(enthalpyMap.contains_true_dof(enthalpyMap.true_dof(reducedDof))); + } +} + +TEST_CASE( + "Field Point DOF Map Selects One Vector Vertex At The Computational Origin", + tags::translational_centering_topology +) { + namespace field = mean_field::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 field::FieldDofMap displacementMap = + field::make_field_dof_map(*f.displacementFes); + mfem::Vector origin(f.mesh->SpaceDimension()); + origin = 0.0; + + const field::FieldPointDofMap centerRows = + field::make_field_point_dof_map(*f.displacementFes, displacementMap, origin, 1.0e-12); + + CHECK(centerRows.field_size() == displacementMap.reduced_size()); + CHECK(field_dof_map_test_utils::global_sum(centerRows.size()) == f.mesh->SpaceDimension()); + + for (const int reducedDof : centerRows.reduced_dofs()) { + CAPTURE(reducedDof); + CHECK(centerRows.contains(reducedDof)); + CHECK(displacementMap.contains_true_dof(displacementMap.true_dof(reducedDof))); + } +} + TEST_CASE( "Field DOF Map Preserves Canonical Bidirectional Indexing", tags::field_dof_unit @@ -523,9 +582,7 @@ TEST_CASE( STATIC_REQUIRE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter); - STATIC_REQUIRE_FALSE( - field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter - ); + STATIC_REQUIRE_FALSE(field_dof_map_test_utils::CanMakeFieldDofGridFunctionAdapter); CHECK(true); } @@ -750,9 +807,8 @@ TEST_CASE( 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 fec = field::Field::make_fec(2); + auto finiteElementSpace = field::Field::make_fespace(mesh, *fec); REQUIRE(finiteElementSpace != nullptr); @@ -796,9 +852,8 @@ TEST_CASE( 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 fec = field::Field::make_fec(2); + auto finiteElementSpace = field::Field::make_fespace(mesh, *fec); REQUIRE(finiteElementSpace != nullptr); @@ -849,9 +904,8 @@ TEST_CASE( 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 fec = field::Field::make_fec(2); + auto finiteElementSpace = field::Field::make_fespace(mesh, *fec); REQUIRE(finiteElementSpace != nullptr); @@ -900,9 +954,8 @@ TEST_CASE( 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 fec = field::Field::make_fec(2); + auto finiteElementSpace = field::Field::make_fespace(mesh, *fec); REQUIRE(finiteElementSpace != nullptr); @@ -939,13 +992,11 @@ TEST_CASE( 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 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); + auto otherFec = field::Field::make_fec(2); + auto otherFiniteElementSpace = field::Field::make_fespace(mesh, *otherFec); REQUIRE(finiteElementSpace != nullptr); REQUIRE(otherFiniteElementSpace != nullptr); @@ -959,8 +1010,7 @@ TEST_CASE( const mfem::Array empty; CHECK_THROWS_AS( (field::FieldDofGridFunctionAdapter( - field::FieldDofMap(finiteElementSpace->GetTrueVSize() + 1, empty), - *finiteElementSpace + field::FieldDofMap(finiteElementSpace->GetTrueVSize() + 1, empty), *finiteElementSpace )), std::invalid_argument ); diff --git a/tests/integrators/centrifugal.cpp b/tests/integrators/centrifugal.cpp index b7773eb..b070afa 100644 --- a/tests/integrators/centrifugal.cpp +++ b/tests/integrators/centrifugal.cpp @@ -11,7 +11,10 @@ using namespace mean_field; namespace { struct SerialMappingData { explicit SerialMappingData(mfem::Mesh &mesh) - : compactification_fes(&mesh, &compactification_fec), + : compactification_fes( + &mesh, + &compactification_fec + ), compactification_coordinate(&compactification_fes), mapper(field_dof_test_utils::make_domain_mapper()) { compactification_coordinate = 0.0; @@ -83,7 +86,7 @@ TEST_CASE( quadrature::RuleFactory quadrature_factory(std::move(policy)); const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general; - const int position_order = displacement_element->GetOrder(); + const int position_order = displacement_element->GetOrder(); quadrature_factory.configure_centrifugal( integrator, quadrature::QuadratureRole::discretization, *density_element, *velocity_element, *transformation, @@ -183,7 +186,7 @@ TEST_CASE( quadrature::RuleFactory quadrature_factory(std::move(policy)); const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general; - const int position_order = displacement_element->GetOrder(); + const int position_order = displacement_element->GetOrder(); quadrature_factory.configure_centrifugal( integrator, quadrature::QuadratureRole::discretization, *density_element, *velocity_element, *transformation, @@ -344,7 +347,7 @@ TEST_CASE( quadrature::RuleFactory quadrature_factory(std::move(policy)); const quadrature::MappingKind mapping_kind = quadrature::MappingKind::general; - const int position_order = displacement_element->GetOrder(); + const int position_order = displacement_element->GetOrder(); quadrature_factory.configure_centrifugal( integrator, quadrature::QuadratureRole::discretization, *density_element, *velocity_element, *transformation, @@ -606,8 +609,8 @@ TEST_CASE( 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); + 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); mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical); velocity_element->CalcShape(integration_point, velocity_shape); @@ -808,7 +811,7 @@ TEST_CASE( mapping_evaluator.GetQuadratureContext(*transformation, integration_point); const double signed_map_determinant = context.detJ; - local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant); + local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant); mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical); velocity_element->CalcShape(integration_point, velocity_shape); @@ -999,7 +1002,7 @@ TEST_CASE( mapping_evaluator.GetQuadratureContext(*transformation, integration_point); const double signed_map_determinant = context.detJ; - local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant); + local_minimum_determinant = std::min(local_minimum_determinant, signed_map_determinant); mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical); velocity_element->CalcShape(integration_point, velocity_shape); diff --git a/tests/integrators/gravity.cpp b/tests/integrators/gravity.cpp index 8ef0eea..855f379 100644 --- a/tests/integrators/gravity.cpp +++ b/tests/integrators/gravity.cpp @@ -43,8 +43,8 @@ TEST_CASE( 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(); + 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); @@ -129,8 +129,7 @@ TEST_CASE( element_residual[displacement_block] = &displacement_residual; integrators::GravityMomentumIntegrator integrator( - domain_mapper, displacement, compactification_coordinate, - integrators::GravityForceJacobianMode::field_coupled + domain_mapper, displacement, compactification_coordinate, integrators::GravityForceJacobianMode::field_coupled ); const int maximum_order = std::max( @@ -297,12 +296,10 @@ TEST_CASE( mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; mfem::GridFunction compactification_coordinate(&compactification_fes); - compactification_coordinate = 0.0; + compactification_coordinate = 0.0; mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper(); - mapping::GridFunctionMappingEvaluator mapping_evaluator( - domain_mapper, displacement, compactification_coordinate - ); + mapping::GridFunctionMappingEvaluator mapping_evaluator(domain_mapper, displacement, compactification_coordinate); auto reference_density = [](const mfem::Vector &x) { return 1.0 + x(0); }; @@ -381,8 +378,7 @@ TEST_CASE( element_residual[displacement_block] = &displacement_residual; integrators::GravityMomentumIntegrator integrator( - domain_mapper, displacement, compactification_coordinate, - 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); @@ -471,11 +467,9 @@ TEST_CASE( mfem::GridFunction displacement(&displacement_fes); displacement = 0.0; mfem::GridFunction compactification_coordinate(&compactification_fes); - compactification_coordinate = 0.0; + compactification_coordinate = 0.0; mapping::DomainMapper domain_mapper = field_dof_test_utils::make_domain_mapper(); - mapping::GridFunctionMappingEvaluator mapping_evaluator( - domain_mapper, displacement, compactification_coordinate - ); + mapping::GridFunctionMappingEvaluator mapping_evaluator(domain_mapper, displacement, compactification_coordinate); auto radial_gravity = [](const mfem::Vector &x, mfem::Vector &gravity) { gravity.SetSize(3); @@ -553,8 +547,7 @@ TEST_CASE( element_residual[displacement_block] = &displacement_residual; integrators::GravityMomentumIntegrator integrator( - domain_mapper, displacement, compactification_coordinate, - 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); diff --git a/tests/mapping/domain_mapper.cpp b/tests/mapping/domain_mapper.cpp index 1bcb387..71d5db5 100644 --- a/tests/mapping/domain_mapper.cpp +++ b/tests/mapping/domain_mapper.cpp @@ -13,3011 +13,2860 @@ 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}); -} - -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); + std::unique_ptr make_kelvin_compactification() { + return std::make_unique( + mapping::compactification::options::KelvinCompactificationOptions{.r_star_ref = 1.0, .r_inf_ref = 4.0} + ); } - } - 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::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; -} - -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)); + mfem::Vector make_constant_compactification_dofs( + const mfem::FiniteElement &element, + const double value = 0.0 + ) { + mfem::Vector dofs(element.GetDof()); + dofs = value; + return dofs; } - } -} -struct QuadraticMappingData { - mfem::Vector base_dofs; - mfem::Vector direction_dofs; - mfem::Vector plus_dofs; - mfem::Vector minus_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()); - 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); - } -}; + mfem::Vector dofs(element.GetDof()); + mfem::Vector reference_position(dimension); -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))); -} + for (int i = 0; i < element.GetDof(); ++i) { + transformation.Transform(nodes.IntPoint(i), reference_position); + dofs(i) = function(reference_position); + } -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); + return dofs; } - } -} -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); -} + 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 + } { + } -double evaluate_reference_hdiv_divergence(const mfem::Vector &position) { - return 2.0 * (position(0) + position(1) + position(2)); -} + 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 + } { + } -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)); -} + ElementMappingDataOwner(const ElementMappingDataOwner &) = delete; + ElementMappingDataOwner &operator=(const ElementMappingDataOwner &) = delete; + ElementMappingDataOwner(ElementMappingDataOwner &&) = delete; + ElementMappingDataOwner &operator=(ElementMappingDataOwner &&) = delete; -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; + [[nodiscard]] const mapping::ElementMappingData &Get() const noexcept { + return m_element_data; + } - CHECK_THAT(finite_difference, - Catch::Matchers::WithinAbs(analytic, tolerance)); -} + private: + mapping::ElementCompactificationData m_compactification; + mapping::ElementMappingData m_element_data; + }; -double relative_vector_difference(const mfem::Vector &lhs, - const mfem::Vector &rhs) { - mfem::Vector difference(lhs); - difference -= rhs; + 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)); + } - const double scale = std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-12}); - return difference.Norml2() / scale; -} + 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()); -double relative_matrix_difference(const mfem::DenseMatrix &lhs, - const mfem::DenseMatrix &rhs) { - mfem::DenseMatrix difference(lhs); - difference -= rhs; + 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)); + } + } - const double scale = std::max({lhs.FNorm(), rhs.FNorm(), 1.0e-12}); - return difference.FNorm() / scale; -} + 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; + } } // 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::DomainMapper::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::DomainMapper::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::DomainMapperOptions valid_options{ - .dimension = dimension, .vacuum_element_attribute = 3}; - mapping::DomainMapper 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.GetExteriorMap().GetName() == "KelvinCompactification"); + REQUIRE(mapper.GetDimension() == dimension); + REQUIRE(mapper.GetExteriorMap().GetName() == "KelvinCompactification"); - const utils::DomainMapperOptions invalid_dimension{ - .dimension = 0, .vacuum_element_attribute = 3}; - const utils::DomainMapperOptions 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::DomainMapper( - invalid_dimension, make_kelvin_compactification()), - std::invalid_argument); - CHECK_THROWS_AS(mapping::DomainMapper( - 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::DomainMapper(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::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::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); - - 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); + 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); + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + mfem::Vector expected_position(dimension); + transformation->Transform(integration_point, expected_position); - REQUIRE(mapper.EvaluateFace(element_data.Get(), *transformation, - mapping::FaceElementSide::element_1, - integration_point, workspace, - context) == mapping::MappingStatus::valid); + REQUIRE( + mapper.EvaluatePoint(element_data.Get(), *transformation, integration_point, workspace, context) == + mapping::MappingStatus::valid + ); - 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)); + 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 Matches Exact Affine Point Mapping", - tags::unit &tags::mapping) { - SingleElementFixture fixture; +TEST_CASE( + "Stateless Domain Mapper Preserves Identity Volume Geometry", + 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(); + const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); + const mapping::ElementDisplacementData displacement(element, zero_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); - 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::VolumeMappingContext context; - 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); + 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); - transformation->SetAllIntPoints(&integration_point); + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - mfem::Vector raw_normal(dimension); - mfem::Vector mapped_normal(dimension); - mfem::CalcOrtho(transformation->Jacobian(), raw_normal); + REQUIRE( + mapper.EvaluateVolume(element_data.Get(), *transformation, integration_point, workspace, context) == + mapping::MappingStatus::valid + ); - inverse_mapping_jacobian.MultTranspose(raw_normal, mapped_normal); - mapped_normal *= mapping_determinant; + transformation->SetIntPoint(&integration_point); - const double raw_normal_magnitude = raw_normal.Norml2(); - const double mapped_normal_magnitude = mapped_normal.Norml2(); + mfem::DenseMatrix expected_inverse(dimension); + mfem::CalcInverse(transformation->Jacobian(), expected_inverse); - 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_weight = integration_point.weight * transformation->Weight(); - 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)); + 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 Has No Cross State Contamination", - tags::unit &tags::mapping) { - SingleElementFixture fixture; +TEST_CASE( + "Stateless Domain Mapper Preserves Identity Face Geometry", + 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(); + const mfem::Vector zero_dofs = fixture.MakeZeroElementDofs(); + const mapping::ElementDisplacementData displacement(element, zero_dofs, mfem::Ordering::byVDIM); + const ElementMappingDataOwner element_data(displacement); - 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; + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() + ); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::FaceMappingContext context; - 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); + 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 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 mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 4); - 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); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - mapping::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::Workspace workspace(dimension); + REQUIRE( + mapper.EvaluateFace( + element_data.Get(), *transformation, mapping::FaceElementSide::element_1, integration_point, + workspace, context + ) == mapping::MappingStatus::valid + ); - const mfem::IntegrationPoint &integration_point = - mfem::Geometries.GetCenter(transformation->GetGeometryType()); + transformation->SetAllIntPoints(&integration_point); - mapping::MappingPointContext first_a; - mapping::MappingPointContext result_b; - mapping::MappingPointContext second_a; + mfem::Vector raw_normal(dimension); + mfem::CalcOrtho(transformation->Jacobian(), raw_normal); - 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 raw_normal_magnitude = raw_normal.Norml2(); + mfem::Vector expected_normal(raw_normal); + expected_normal /= raw_normal_magnitude; - check_point_context(first_a, second_a, 0.0); + const double expected_surface_weight = integration_point.weight * raw_normal_magnitude; - mfem::Vector state_difference(result_b.physical_position); - state_difference -= first_a.physical_position; - CHECK(state_difference.Norml2() > 1.0e-3); + 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 Reports Invalid Element States", - 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(); - mfem::ElementTransformation *transformation = - fixture.mesh.GetElementTransformation(0); - const mfem::IntegrationPoint &integration_point = - mfem::Geometries.GetCenter(transformation->GetGeometryType()); + const mfem::FiniteElement &element = fixture.GetElement(); + mfem::ElementTransformation *transformation = fixture.mesh.GetElementTransformation(0); - mapping::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::Workspace workspace(dimension); - mapping::MappingPointContext 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::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); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() + ); + mapping::DomainMapper::Workspace workspace(dimension); + mapping::MappingPointContext context; - CHECK(mapper.EvaluatePoint(non_finite_data.Get(), *transformation, - integration_point, workspace, context) == - mapping::MappingStatus::non_finite_input); + mfem::DenseMatrix inverse_deformation_jacobian(dimension); + mfem::CalcInverse(deformation_jacobian, inverse_deformation_jacobian); + const double deformation_determinant = deformation_jacobian.Det(); - mfem::DenseMatrix singular_gradient(dimension); - singular_gradient = 0.0; - for (int i = 0; i < dimension; ++i) - singular_gradient(i, i) = -1.0; + REQUIRE(deformation_determinant > 0.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); + const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - CHECK(mapper.EvaluatePoint(singular_data.Get(), *transformation, - integration_point, workspace, context) == - mapping::MappingStatus::non_positive_determinant); + 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); - 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 expected_position = + evaluate_affine_physical_position(reference_position, displacement_gradient, displacement_offset); - 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); + REQUIRE( + mapper.EvaluatePoint(element_data.Get(), *transformation, integration_point, workspace, context) == + mapping::MappingStatus::valid + ); - 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); + 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 Matches Exact Quadratic Point Mapping", - tags::unit &tags::mapping) { - QuadraticElementFixture fixture; +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::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); + const mfem::DenseMatrix displacement_gradient = make_affine_displacement_gradient(); + const mfem::Vector displacement_offset = make_vector(0.07, -0.04, 0.03); - mapping::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::Workspace workspace(dimension); - mapping::MappingPointContext context; + 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 mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), 6); + 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); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(q); + mapping::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() + ); + mapping::DomainMapper::Workspace workspace(dimension); - mfem::Vector reference_position(dimension); - mfem::Vector expected_displacement(dimension); - transformation->Transform(integration_point, reference_position); - evaluate_quadratic_displacement(reference_position, expected_displacement); + mapping::VolumeMappingContext by_vdim_context; + mapping::VolumeMappingContext by_nodes_context; - mfem::Vector expected_position(reference_position); - expected_position += expected_displacement; + const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - 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); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - mfem::DenseMatrix expected_inverse(dimension); - mfem::CalcInverse(expected_jacobian, expected_inverse); - const double expected_determinant = expected_jacobian.Det(); + 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 + ); - REQUIRE(expected_determinant > 0.0); - REQUIRE(mapper.EvaluatePoint(element_data.Get(), *transformation, - integration_point, workspace, - 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)); + } +} - 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 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); + 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::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() + ); + mapping::DomainMapper::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::DomainMapper mapper( + {.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification() + ); + mapping::DomainMapper::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::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 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::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::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::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::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::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::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::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::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::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); - mapping::DomainMapper::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); + 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::DomainMapper mapper({.dimension = dimension, .vacuum_element_attribute = 3}, std::move(exterior_map)); + mapping::DomainMapper::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; } else { - radial_extension = - std::max(0.0, (r_infinity - radius) / (r_infinity - r_star)); + ++stellar_elements; } - const double scale = radial_extension * angular_deformation; - displacement_value(0) = scale * x; - displacement_value(1) = scale * y; - displacement_value(2) = scale * z; - }; + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + mapping::VolumeMappingContext context; - mfem::VectorFunctionCoefficient displacement_coefficient( - dimension, rotating_displacement); - displacement.ProjectCoefficient(displacement_coefficient); + REQUIRE( + mapper.EvaluateVolume(element_data.Get(), *transformation, integration_point, workspace, context) == + mapping::MappingStatus::valid + ); - std::unique_ptr - exterior_map = - std::make_unique( - mapping::compactification::options::KelvinCompactificationOptions{ - .r_star_ref = r_star, .r_inf_ref = r_infinity}); + minimum_mapping_determinant = std::min(minimum_mapping_determinant, context.mapping.mapping_determinant); + maximum_mapping_determinant = std::max(maximum_mapping_determinant, context.mapping.mapping_determinant); - mapping::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - std::move(exterior_map)); - mapping::DomainMapper::Workspace workspace(dimension); + REQUIRE(context.mapping.mapping_determinant > 0.0); + REQUIRE(context.quadrature.weight > 0.0); - 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 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) + ); - int stellar_elements = 0; - int vacuum_elements = 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); - 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); + 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); - mfem::Array element_vdofs; - mfem::Vector element_dofs; - displacement_space.GetElementVDofs(element_id, element_vdofs); - displacement.GetSubVector(element_vdofs, element_dofs); + if (transformation->Attribute == 3) { + transformation->SetIntPoint(&integration_point); - const mapping::ElementDisplacementData element_displacement = - mapping::ElementDisplacementDataFromElementVDofs(*element, - element_dofs); + mfem::Vector compactification_gradient(dimension); + const double coordinate = compactification_coordinate.GetValue(element_id, integration_point); + compactification_coordinate.GetGradient(*transformation, 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); + 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 + }; - 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); + 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 + ); - if (transformation->Attribute == 3) { - ++vacuum_elements; - } else { - ++stellar_elements; + 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; + } + } } - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(q); - mapping::VolumeMappingContext context; + 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); - REQUIRE(mapper.EvaluateVolume(element_data.Get(), *transformation, - integration_point, workspace, - context) == mapping::MappingStatus::valid); + 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)); - minimum_mapping_determinant = std::min( - minimum_mapping_determinant, context.mapping.mapping_determinant); - maximum_mapping_determinant = std::max( - maximum_mapping_determinant, context.mapping.mapping_determinant); + 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); - 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); + 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::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); + 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; - - 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); - - 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); - - 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); - }; - - 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); - - 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; + 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::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::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); }; - mfem::VectorFunctionCoefficient displacement_coefficient( - dimension, displacement_function); + 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::GridFunction displacement(&displacement_space); + mfem::GridFunction direction(&displacement_space); displacement.ProjectCoefficient(displacement_coefficient); + direction.ProjectCoefficient(direction_coefficient); - mapping::DomainMapper mapper( - {.dimension = dimension, .vacuum_element_attribute = 3}, - make_kelvin_compactification()); + 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); - REQUIRE(displacement_space.GetOrdering() == space_ordering); - REQUIRE(displacement.VectorDim() == dimension); + int core_envelope_faces = 0; + int stellar_vacuum_faces = 0; - for (int element_id = 0; element_id < mesh.GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = - mesh.GetElementTransformation(element_id); + 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; - REQUIRE(transformation != nullptr); + 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); - const mfem::FiniteElement &displacement_element = - *displacement_space.GetFE(element_id); + if (!core_envelope_interface && !stellar_vacuum_interface) + continue; - mfem::Array element_vdofs; + if (core_envelope_interface) + ++core_envelope_faces; + if (stellar_vacuum_interface) + ++stellar_vacuum_faces; - mfem::DofTransformation *dof_transformation = - displacement_space.GetElementVDofs(element_id, element_vdofs); + const int element_1 = transformation->Elem1->ElementNo; + const int element_2 = transformation->Elem2->ElementNo; - mfem::Vector element_displacement; + 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.GetSubVector(element_vdofs, element_displacement); + 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); - if (dof_transformation != nullptr) { - dof_transformation->InvTransformPrimal(element_displacement); - } + 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 mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, element_displacement); + 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 + ); - REQUIRE(displacement_data.GetOrdering() == mfem::Ordering::byNODES); + const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get(transformation->GetGeometryType(), 6); - const ElementMappingDataOwner element_data(displacement_data); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); - const mfem::IntegrationRule &integration_rule = mfem::IntRules.Get( - transformation->GetGeometryType(), quadrature_order); + mapping::FaceMappingContext context_1; + mapping::FaceMappingContext context_2; + mapping::FaceMappingVariation variation_1; + mapping::FaceMappingVariation variation_2; - mfem::Vector shape(displacement_element.GetDof()); + 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::DenseMatrix physical_dshape(displacement_element.GetDof(), - dimension); + 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 + ); - mfem::Vector computed_value(dimension); - mfem::Vector expected_value(dimension); + 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::DenseMatrix computed_gradient(dimension, dimension); + mfem::Vector normal_sum(context_1.quadrature.normal); + normal_sum += context_2.quadrature.normal; + CHECK(normal_sum.Norml2() < interface_tolerance); - mfem::DenseMatrix expected_gradient(dimension, dimension); + 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 + ); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(q); + 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); - CAPTURE(static_cast(space_ordering), element_id, q); + const mfem::Vector physical_flux = make_vector(0.7, -0.4, 0.9); + mfem::Vector reference_flux_1; + mfem::Vector reference_flux_2; - transformation->SetIntPoint(&integration_point); + mapping::MapPhysicalFluxToHDivReference(context_1.mapping, physical_flux, reference_flux_1); + mapping::MapPhysicalFluxToHDivReference(context_2.mapping, physical_flux, reference_flux_2); - displacement_element.CalcShape(integration_point, shape); + 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; - 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_THAT(flux_1 + flux_2, WithinAbs(0.0, interface_tolerance)); } - - 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); - } } - }; - SECTION("Global finite-element-space ordering is byNODES") { - check_space_ordering(mfem::Ordering::byNODES); - } + INFO("Core-envelope interface faces = " << core_envelope_faces); + INFO("Stellar-vacuum interface faces = " << stellar_vacuum_faces); - SECTION("Global finite-element-space ordering is byVDIM") { - check_space_ordering(mfem::Ordering::byVDIM); - } + 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); + + 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::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); + } + } + }; + + 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); + 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::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; + 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); + 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(); + /* Refreshing before the first evaluation is a validated no-op. */ + evaluator.Refresh(); - mfem::ElementTransformation *transformation = - mesh.GetElementTransformation(0); - REQUIRE(transformation != nullptr); + mfem::ElementTransformation *transformation = mesh.GetElementTransformation(0); + REQUIRE(transformation != nullptr); - mfem::IntegrationPoint integration_point; - integration_point.Set3(0.31, 0.43, 0.57); + 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); + 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); + 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); + 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(); + /* + * 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); + 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; + 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); + 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); + 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(); + 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::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; + 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); + 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::ElementTransformation *transformation = mesh.GetElementTransformation(0); + REQUIRE(transformation != nullptr); - mfem::IntegrationPoint integration_point; - integration_point.Set3(0.29, 0.37, 0.61); + 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); + mapping::MappingPointContext initial_context; + REQUIRE( + evaluator.EvaluatePoint(*transformation, integration_point, initial_context) == mapping::MappingStatus::valid + ); + REQUIRE(initial_context.compactified); - compactification_coordinate = 0.40; + compactification_coordinate = 0.40; - /* Lazy invalidation is idempotent and reloads on the next evaluation. */ - evaluator.InvalidateCache(); - evaluator.InvalidateCache(); + /* 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); + 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)); + 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; + 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); + 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); + 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::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; + 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()); + 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); + 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); + mapping::GridFunctionMappingEvaluator evaluator(mapper, displacement, compactification_coordinate); - mfem::ElementTransformation *transformation = - mesh.GetElementTransformation(0); - REQUIRE(transformation != nullptr); + mfem::ElementTransformation *transformation = mesh.GetElementTransformation(0); + REQUIRE(transformation != nullptr); - mfem::IntegrationPoint integration_point; - integration_point.Set3(0.5, 0.5, 0.5); + 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); + mapping::MappingPointContext context; + REQUIRE(evaluator.EvaluatePoint(*transformation, integration_point, context) == mapping::MappingStatus::valid); - displacement.SetSpace(&alternate_displacement_space); + 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); + CHECK_THROWS_AS(evaluator.Refresh(), std::invalid_argument); + CHECK_THROWS_AS(evaluator.EvaluatePoint(*transformation, integration_point, context), std::invalid_argument); } diff --git a/tests/models/stellar_model.cpp b/tests/models/stellar_model.cpp index fc9e372..b9b4f87 100644 --- a/tests/models/stellar_model.cpp +++ b/tests/models/stellar_model.cpp @@ -1,4 +1,6 @@ +#include #include +#include #include #include #include @@ -12,17 +14,11 @@ import test_helpers; namespace { struct StellarModelExtensionTracker final { int structureValidationCount{0}; - int surfaceValidationCount{0}; - int surfaceResolutionCount{0}; - const mean_field::eos::EquationOfState *structureEquationOfState{nullptr}; - - const mean_field::eos::EquationOfState *surfaceValidationEquationOfState{nullptr}; - - const mean_field::eos::EquationOfState *surfaceResolutionEquationOfState{nullptr}; + const mean_field::eos::Polytrope *structureEquationOfState{nullptr}; }; - class StellarModelTestStructure final : public mean_field::models::structure::StructureBase { + class StellarModelTestStructure final { public: explicit StellarModelTestStructure(std::shared_ptr tracker) : m_tracker(std::move(tracker)), @@ -32,17 +28,17 @@ namespace { ) { } - [[nodiscard]] const mean_field::eos::EquationOfState &equationOfState() const noexcept override { + [[nodiscard]] const mean_field::eos::Polytrope &equationOfState() const noexcept { m_tracker->structureEquationOfState = &m_equationOfState; return m_equationOfState; } - [[nodiscard]] double targetMass() const noexcept override { + [[nodiscard]] double targetMass() const noexcept { return 2.5; } [[nodiscard]] mean_field::models::structure::StructureSeed - makeInitialSeed(const mean_field::models::structure::StructureSeedRequest &request) const override { + makeInitialSeed(const mean_field::models::structure::StructureSeedRequest &request) const { mean_field::models::structure::StructureSeed seed; seed.radius.SetSize(2); @@ -65,7 +61,7 @@ namespace { return seed; } - void validate() const override { + void validate() const { ++m_tracker->structureValidationCount; } @@ -74,59 +70,85 @@ namespace { mean_field::eos::Polytrope m_equationOfState; }; - class StellarModelTestSurface final : public mean_field::surface::SurfaceBase { + class StructureWithoutSeed final { public: - explicit StellarModelTestSurface(std::shared_ptr tracker) - : m_tracker(std::move(tracker)) { - } + [[nodiscard]] const mean_field::eos::Polytrope &equationOfState() const noexcept; - [[nodiscard]] - mean_field::surface::ResolvedSurfaceCondition - resolve(const mean_field::eos::EquationOfState &equationOfState) const override { - ++m_tracker->surfaceResolutionCount; + [[nodiscard]] double targetMass() const noexcept; - m_tracker->surfaceResolutionEquationOfState = &equationOfState; - - return mean_field::surface::ResolvedSurfaceCondition{0.375}; - } - - void validate(const mean_field::eos::EquationOfState &equationOfState) const override { - ++m_tracker->surfaceValidationCount; - - m_tracker->surfaceValidationEquationOfState = &equationOfState; - } - - private: - std::shared_ptr m_tracker; + void validate() const; }; + + class SurfaceWithoutPhysicalQuantity final { }; + + struct ModelSurfaceState final { + double specificEnthalpy; + + [[nodiscard]] mean_field::eos::SpecificEnthalpyValue + value(mean_field::eos::quantity::SpecificEnthalpy) const noexcept { + return mean_field::eos::SpecificEnthalpyValue{specificEnthalpy}; + } + }; + + using PolytropicStellarModel = mean_field::models::StellarModel; + + using ExtensionStellarModel = mean_field::models::StellarModel; } // namespace TEST_CASE( "Stellar Model Owns Structure And Surface Prescriptions", - tags::barotrope &tags::unit &tags::model + tags::stellar_model_type_contract ) { - STATIC_REQUIRE_FALSE(std::is_copy_constructible_v); + STATIC_CHECK(mean_field::models::StructurePrescription); + STATIC_CHECK(mean_field::models::StructurePrescription); + STATIC_CHECK_FALSE(mean_field::models::StructurePrescription); - STATIC_REQUIRE_FALSE(std::is_copy_assignable_v); + STATIC_CHECK( + mean_field::models::SurfacePrescription< + mean_field::surface::ConstantPressureSurface, mean_field::eos::Polytrope> + ); + STATIC_CHECK_FALSE( + mean_field::models::SurfacePrescription + ); - STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + STATIC_CHECK_FALSE(std::derived_from); + STATIC_CHECK_FALSE( + std::derived_from< + mean_field::models::structure::PolytropicStructure, mean_field::models::structure::StructureBase> + ); + STATIC_CHECK( + std::same_as< + decltype(std::declval().equationOfState()), + mean_field::eos::EquationOfStateView> + ); - STATIC_REQUIRE(std::is_nothrow_move_assignable_v); + STATIC_REQUIRE_FALSE(std::is_copy_constructible_v); + + STATIC_REQUIRE_FALSE(std::is_copy_assignable_v); + + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + + STATIC_REQUIRE(std::is_nothrow_move_assignable_v); mean_field::models::StellarModel model{ mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0}, - mean_field::surface::Isobaric{0.0} + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} }; - CHECK(model.targetMass() == 1.0); - CHECK(model.resolvedSurfaceCondition().targetEnthalpy == 0.0); - - CHECK( - dynamic_cast(&model.structurePrescription()) != - nullptr + STATIC_CHECK(std::same_as); + STATIC_CHECK( + std::same_as< + decltype(model.structurePrescription()), const mean_field::models::structure::PolytropicStructure &> ); + STATIC_CHECK( + std::same_as + ); + STATIC_CHECK(std::same_as); - CHECK(dynamic_cast(&model.surfacePrescription()) != nullptr); + CHECK(model.targetMass() == 1.0); + CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{0.0}); + CHECK(&model.equationOfState() == &model.structurePrescription().equationOfState()); + CHECK(model.surfacePrescription().targetPressure() == mean_field::eos::PressureValue{0.0}); } TEST_CASE( @@ -135,7 +157,7 @@ TEST_CASE( ) { mean_field::models::StellarModel model{ mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0}, - mean_field::surface::Isobaric{} + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} }; const mean_field::models::structure::StructureSeed seed = @@ -153,45 +175,49 @@ TEST_CASE( TEST_CASE( "Moving A Stellar Model Preserves Stable Prescription Addresses", - tags::barotrope &tags::unit &tags::model + tags::barotrope &tags::unit &tags::model &tags::surface_constraint_lifetime ) { mean_field::models::StellarModel originalModel{ mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0}, - mean_field::surface::Isobaric{} + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} }; - const mean_field::models::structure::StructureBase *structureAddress = &originalModel.structurePrescription(); + const mean_field::models::structure::PolytropicStructure *structureAddress = &originalModel.structurePrescription(); - const mean_field::surface::SurfaceBase *surfaceAddress = &originalModel.surfacePrescription(); + const mean_field::surface::ConstantPressureSurface *surfaceAddress = &originalModel.surfacePrescription(); - const mean_field::eos::EquationOfState *equationOfStateAddress = &originalModel.equationOfState(); + const mean_field::eos::Polytrope *equationOfStateAddress = &originalModel.equationOfState(); + + const auto *compiledSurfaceConstraintAddress = &originalModel.compiledSurfaceConstraint(); mean_field::models::StellarModel movedModel{std::move(originalModel)}; CHECK(&movedModel.structurePrescription() == structureAddress); CHECK(&movedModel.surfacePrescription() == surfaceAddress); CHECK(&movedModel.equationOfState() == equationOfStateAddress); + CHECK(&movedModel.compiledSurfaceConstraint() == compiledSurfaceConstraintAddress); CHECK(movedModel.targetMass() == 1.0); } TEST_CASE( - "Stellar Model Resolves A Positive Isobaric Surface", + "Stellar Model Compiles A Positive Constant Pressure Surface", tags::barotrope &tags::unit &tags::model ) { constexpr double targetPressure = 0.03125; mean_field::models::StellarModel model{ mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0}, - mean_field::surface::Isobaric{targetPressure} + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{targetPressure}} }; - const double targetEnthalpy = model.resolvedSurfaceCondition().targetEnthalpy; + const double requiredSpecificEnthalpy = mean_field::eos::evaluate( + model.equationOfState(), mean_field::eos::PressureValue{targetPressure} + ) + .value(); - CHECK(targetEnthalpy > 0.0); - CHECK( - std::abs(model.equationOfState().pressure_from_enthalpy(targetEnthalpy) - targetPressure) < - 64.0 * std::numeric_limits::epsilon() - ); + CHECK(requiredSpecificEnthalpy > 0.0); + CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{targetPressure}); + CHECK(model.compiledSurfaceConstraint().residual(ModelSurfaceState{requiredSpecificEnthalpy}) == 0.0); } TEST_CASE( @@ -200,27 +226,22 @@ TEST_CASE( ) { const auto tracker = std::make_shared(); - mean_field::models::StellarModel model{StellarModelTestStructure{tracker}, StellarModelTestSurface{tracker}}; + mean_field::models::StellarModel model{ + StellarModelTestStructure{tracker}, + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.375}} + }; + + STATIC_CHECK(std::same_as); REQUIRE(tracker->structureValidationCount == 1); - REQUIRE(tracker->surfaceValidationCount == 1); - REQUIRE(tracker->surfaceResolutionCount == 1); - CHECK(dynamic_cast(&model.structurePrescription()) != nullptr); - - CHECK(dynamic_cast(&model.surfacePrescription()) != nullptr); - - const mean_field::eos::EquationOfState *ownedEquationOfState = &model.equationOfState(); + const mean_field::eos::Polytrope *ownedEquationOfState = &model.equationOfState(); CHECK(tracker->structureEquationOfState == ownedEquationOfState); - CHECK(tracker->surfaceValidationEquationOfState == ownedEquationOfState); - - CHECK(tracker->surfaceResolutionEquationOfState == ownedEquationOfState); - CHECK(model.targetMass() == 2.5); - CHECK(model.resolvedSurfaceCondition().targetEnthalpy == 0.375); + CHECK(model.compiledSurfaceConstraint().targetPressure() == mean_field::eos::PressureValue{0.375}); } TEST_CASE( @@ -229,23 +250,25 @@ TEST_CASE( ) { mean_field::models::StellarModel sourceModel{ mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.25}, - mean_field::surface::Isobaric{0.0} + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} }; mean_field::models::StellarModel destinationModel{ mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{2.0, 0.5}, 4.0}, - mean_field::surface::Isobaric{0.02} + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.02}} }; - const mean_field::models::structure::StructureBase *sourceStructureAddress = &sourceModel.structurePrescription(); + const mean_field::models::structure::PolytropicStructure *sourceStructureAddress = + &sourceModel.structurePrescription(); - const mean_field::surface::SurfaceBase *sourceSurfaceAddress = &sourceModel.surfacePrescription(); + const mean_field::surface::ConstantPressureSurface *sourceSurfaceAddress = &sourceModel.surfacePrescription(); - const mean_field::eos::EquationOfState *sourceEquationOfStateAddress = &sourceModel.equationOfState(); + const mean_field::eos::Polytrope *sourceEquationOfStateAddress = &sourceModel.equationOfState(); - const double sourceTargetEnthalpy = sourceModel.resolvedSurfaceCondition().targetEnthalpy; + const mean_field::eos::PressureValue sourceTargetPressure = + sourceModel.compiledSurfaceConstraint().targetPressure(); - destinationModel = std::move(sourceModel); + destinationModel = std::move(sourceModel); CHECK(&destinationModel.structurePrescription() == sourceStructureAddress); @@ -255,5 +278,83 @@ TEST_CASE( CHECK(destinationModel.targetMass() == 1.25); - CHECK(destinationModel.resolvedSurfaceCondition().targetEnthalpy == sourceTargetEnthalpy); -} \ No newline at end of file + CHECK(destinationModel.compiledSurfaceConstraint().targetPressure() == sourceTargetPressure); +} + +TEST_CASE( + "Stellar Model View Supports Heterogeneous Typed Models", + tags::stellar_model_runtime_view +) { + const auto tracker = std::make_shared(); + + const mean_field::models::StellarModel polytropicModel{ + mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0}, + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} + }; + + const mean_field::models::StellarModel extensionModel{ + StellarModelTestStructure{tracker}, + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.375}} + }; + + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK_FALSE(std::constructible_from); + + const std::array views{ + mean_field::models::StellarModelView{polytropicModel}, mean_field::models::StellarModelView{extensionModel} + }; + + CHECK(views[0].targetMass() == 1.0); + CHECK(views[1].targetMass() == 2.5); + CHECK(views[1].surfaceCondition().targetPressure == 0.375); + REQUIRE(views[1].surfaceDependencies().stateFields.size() == 1); + CHECK( + views[1].surfaceDependencies().residualRowField == + mean_field::surface::surfaceFieldId + ); + + const auto pressure = + views[0].equationOfState().tryEvaluate(mean_field::eos::DensityValue{0.7}); + + REQUIRE(pressure.has_value()); + CHECK( + pressure->value() == mean_field::eos::evaluate( + polytropicModel.equationOfState(), mean_field::eos::DensityValue{0.7} + ) + .value() + ); + + const mean_field::models::structure::StructureSeed seed = + views[1].makeInitialSeed({.centralDensity = 1.75, .radialSampleCount = 2}); + + CHECK(seed.centralDensity == 1.75); + CHECK(seed.radius.Size() == 2); +} + +TEST_CASE( + "Stellar Model View Retains Stable Pointees When Its Owner Moves", + tags::stellar_model_runtime_view +) { + mean_field::models::StellarModel originalModel{ + mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0}, + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} + }; + + const mean_field::models::StellarModelView view{originalModel}; + PolytropicStellarModel movedModel{std::move(originalModel)}; + + const auto pressure = + view.equationOfState().tryEvaluate(mean_field::eos::DensityValue{0.7}); + const mean_field::models::structure::StructureSeed seed = + view.makeInitialSeed({.centralDensity = 1.0, .radialSampleCount = 8}); + + REQUIRE(pressure.has_value()); + CHECK(view.targetMass() == movedModel.targetMass()); + CHECK( + pressure->value() == mean_field::eos::evaluate( + movedModel.equationOfState(), mean_field::eos::DensityValue{0.7} + ) + .value() + ); + CHECK(seed.radius.Size() == 8); +} diff --git a/tests/operators/contexts/hydrostatic_equilibrium_context.cpp b/tests/operators/contexts/hydrostatic_equilibrium_context.cpp index 8297909..57a54bc 100644 --- a/tests/operators/contexts/hydrostatic_equilibrium_context.cpp +++ b/tests/operators/contexts/hydrostatic_equilibrium_context.cpp @@ -164,9 +164,7 @@ TEST_CASE( CHECK_FALSE(gravityPotentialReport.updatedDisplacement); CHECK_FALSE(gravityPotentialReport.updatedBernoulliConstant); - CHECK( - context.GetBaseGravityPotentialTrue()(context.GetGravityPotentialMap().true_dof(0)) == gravityPotential(0) - ); + CHECK(context.GetBaseGravityPotentialTrue()(context.GetGravityPotentialMap().true_dof(0)) == gravityPotential(0)); ++dependencies.bernoulliConstant.revision; diff --git a/tests/operators/gravity_displacement_force.cpp b/tests/operators/gravity_displacement_force.cpp index e350db3..187c024 100644 --- a/tests/operators/gravity_displacement_force.cpp +++ b/tests/operators/gravity_displacement_force.cpp @@ -11,763 +11,729 @@ 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< - 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 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 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}; -} - -[[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; + return {valueSizes, residualSizes}; } - f.densityFes->GetElementDofs(elementId, densityDofs); + [[nodiscard]] mfem::Vector make_density( + const mean_field::fem::FEM &f, + const double phase + ) { + mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::Vector elementDensity(densityDofs.Size()); - elementDensity = 1.0; - densityField.SetSubVector(densityDofs, elementDensity); - ++localVacuumElements; - } + 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); + }); - int globalVacuumElements = 0; - MPI_Allreduce(&localVacuumElements, &globalVacuumElements, 1, MPI_INT, - MPI_SUM, f.mesh->GetComm()); + densityField.ProjectCoefficient(densityCoefficient); - REQUIRE(globalVacuumElements > 0); + 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]] 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}}; -} + 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); + }); -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); -} + densityField.ProjectCoefficient(densityCoefficient); -[[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 densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; + } - mfem::Vector difference(left); - difference -= right; + [[nodiscard]] mfem::Vector make_gravity_gradient( + const mean_field::fem::FEM &f, + const double phase + ) { + mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); - 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()}); + auto gravityFunction = [phase](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(3); - return gravity_prepared_test_utils::global_norm(difference, communicator) / - scale; -} + value(0) = 0.31 + 0.08 * position(0) + 0.03 * phase * position(1); -[[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(1) = -0.17 + 0.06 * position(1) - 0.02 * phase * position(2); - mfem::Vector minusDensity(baseDensity); - minusDensity.Add(-step, densityDirection); + value(2) = 0.23 - 0.05 * position(2) + 0.025 * phase * position(0); + }; - mfem::Vector plusGravity(baseGravityGradient); - plusGravity.Add(step, gravityGradientDirection); + mfem::VectorFunctionCoefficient gravityCoefficient(3, gravityFunction); - mfem::Vector minusGravity(baseGravityGradient); - minusGravity.Add(-step, gravityGradientDirection); + gravityField.ProjectCoefficient(gravityCoefficient); - mfem::Vector plusDisplacement(baseDisplacement); - plusDisplacement.Add(step, displacementDirection); + mfem::Vector gravityTrue; + gravityField.GetTrueDofs(gravityTrue); + return gravityTrue; + } - mfem::Vector minusDisplacement(baseDisplacement); - minusDisplacement.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 plusResidual; - mfem::Vector minusResidual; + auto gravityFunction = [phase](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(3); - mean_field::operators::kernels::apply_gravity_displacement_force_residual( - f, *f.domainMapperStateless, plusDensity, plusGravity, plusDisplacement, - plusResidual); + 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, minusDensity, minusGravity, - minusDisplacement, minusResidual); + value(1) = -0.11 * std::cos(position(1) - phase) + 0.04 * position(2); - plusResidual -= minusResidual; - plusResidual /= 2.0 * step; - return plusResidual; -} + value(2) = 0.09 * std::sin(position(2) + 0.5 * phase) - 0.02 * position(0); + }; -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); + mfem::VectorFunctionCoefficient gravityCoefficient(3, gravityFunction); - for (int entry = 0; entry < result.Size(); ++entry) { - result(entry) = action(offset + entry); - } + gravityField.ProjectCoefficient(gravityCoefficient); - return result; -} + 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; + } + + 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; + } } // 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::field::Displacement::Form::GravityForce>( - 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::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 e010d45..66b1b7f 100644 --- a/tests/operators/gravity_displacement_force_analytic_comparisons.cpp +++ b/tests/operators/gravity_displacement_force_analytic_comparisons.cpp @@ -11,386 +11,370 @@ 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 &scales) { - return scales[0] * scales[1] * scales[2]; -} + [[nodiscard]] double determinant( + const std::array< + double, + 3> &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 &referenceGravity) { - mfem::ParGridFunction gravityField(f.gravityFluxFes.get()); + [[nodiscard]] mfem::Vector make_reference_gravity( + const mean_field::fem::FEM &f, + const std::array< + double, + 3> &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 &scales) { - mfem::ParGridFunction displacementField(f.displacementFes.get()); + [[nodiscard]] mfem::Vector make_affine_displacement( + const mean_field::fem::FEM &f, + const std::array< + double, + 3> &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.domainMapperStateless != 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< - 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}}}}; + 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}}} + }; - 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.domainMapperStateless != nullptr); - *f.displacement = 0.0; + 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::solve_gravity_field(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); + REQUIRE(computedWork > 0.0); + CHECK(relativeError < 1.0e-5); } diff --git a/tests/operators/gravity_field.cpp b/tests/operators/gravity_field.cpp index 0f701de..15c1857 100644 --- a/tests/operators/gravity_field.cpp +++ b/tests/operators/gravity_field.cpp @@ -12,2761 +12,2633 @@ using namespace mean_field; using Catch::Matchers::WithinAbs; namespace { -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); - -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()}; - - 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; - - 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; - - 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_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; - - 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; - - 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::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) != - 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); - } - - 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()); - - mfem::Vector difference(lhs); - difference -= rhs; - - 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; - - auto field_function = [](const mfem::Vector &position, mfem::Vector &value) { - const double radius_squared = position * position; - - value.SetSize(3); - value = 0.0; - - 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); - - 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; - - 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; - - 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()); - - 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); - - 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}; -}; - -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); - }; - - mfem::VectorFunctionCoefficient displacement_coefficient( - 3, displacement_function); - displacement.ProjectCoefficient(displacement_coefficient); - } - - 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); + 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); + + 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() + }; + + return blocks::form_layout(value_sizes, residual_sizes); } - if (displacement_dof_transformation != nullptr) { - displacement_dof_transformation->InvTransformPrimal(element_displacement); + 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); } - if (compactification_dof_transformation != nullptr) { - compactification_dof_transformation->InvTransformPrimal( - element_compactification); + 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; } - const mapping::ElementDisplacementData displacement_data = - mapping::ElementDisplacementDataFromElementVDofs(displacement_element, - element_displacement); + 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; + } - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification); + 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); + }; - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, - .compactification = compactification_data}; + mfem::VectorFunctionCoefficient coefficient(3, displacement_function); + mfem::ParGridFunction displacement(f.displacementFes.get()); + mfem::Vector displacement_true; - const int gravity_dof_count = gravity_element.GetDof(); + displacement.ProjectCoefficient(coefficient); + displacement.GetTrueDofs(displacement_true); + return displacement_true; + } - const int dimension = transformation.GetSpaceDim(); + 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; - element_action.SetSize(gravity_dof_count); - element_action = 0.0; + density.ProjectCoefficient(coefficient); + density.GetTrueDofs(density_true); + return density_true; + } - quadrature_flux.SetSize(dimension); - mapped_quadrature_flux.SetSize(dimension); + mfem::Vector make_vacuum_density( + const fem::FEM &f, + const double value + ) { + mfem::ParGridFunction density(f.densityFes.get()); + density = 0.0; - vector_shape.SetSize(gravity_dof_count, dimension); + mfem::Array element_dofs; + mfem::Vector element_values; - mapped_mass_tensor.SetSize(dimension, dimension); + 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; - 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); + f.densityFes->GetElementDofs(element_id, element_dofs); + element_values.SetSize(element_dofs.Size()); + element_values = value; + density.SetSubVector(element_dofs, element_values); } - 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 density_true; + density.GetTrueDofs(density_true); + return density_true; } - if (gravity_dof_transformation != nullptr) { - gravity_dof_transformation->TransformDual(element_action); + double relative_difference( + const mfem::Vector &lhs, + const mfem::Vector &rhs + ) { + REQUIRE(lhs.Size() == rhs.Size()); + + mfem::Vector difference(lhs); + difference -= rhs; + + return difference.Norml2() / std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-14}); } - total_local.AddElementVector(gravity_dofs, element_action); + 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; - if (is_vacuum) { - vacuum_local.AddElementVector(gravity_dofs, element_action); + auto field_function = [](const mfem::Vector &position, mfem::Vector &value) { + const double radius_squared = position * position; - ++reference.vacuum_elements; - } else { - stellar_local.AddElementVector(gravity_dofs, element_action); + value.SetSize(3); + value = 0.0; - ++reference.stellar_elements; + 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); + + 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; + + gravity_gradient.ProjectCoefficient(coefficient); + gravity_gradient.GetTrueDofs(gravity_gradient_true); + return gravity_gradient_true; } - } - reference_local_to_true(*fem.gravityFluxFes, total_local, - reference.total_action); + 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); + } + + 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; + + 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}); + } + + 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); + }; + + mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); + displacement.ProjectCoefficient(displacement_coefficient); + } + + 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."); - 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::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); + } + + 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::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) + ); + } } // 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); - REQUIRE(f.domainMapperStateless != nullptr); - const blocks::form_layout layout = make_gravity_layout(f); + 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); + 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} + }; - gravity_operator.Prepare(state, revisions); - gravity_operator.Mult(state, residual); + gravity_operator.Prepare(state, revisions); + gravity_operator.Mult(state, residual); - REQUIRE(residual.Size() == layout.residual_offsets().Last()); - CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-14)); + REQUIRE(residual.Size() == layout.residual_offsets().Last()); + CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-14)); - 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 gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.31); + set_block(state, layout.value_offsets(), gravity_potential_block, gravity_potential); - gravity_operator.Mult(state, residual); + 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); - const auto &geometry_context = linearization_context.GetGeometryContext(); - const auto &flux_map = linearization_context.GetGravityGradientMap(); - const auto &potential_map = linearization_context.GetGravityPotentialMap(); + const auto &geometry_context = linearization_context.GetGeometryContext(); + const auto &flux_map = linearization_context.GetGravityGradientMap(); + const auto &potential_map = linearization_context.GetGravityPotentialMap(); - 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); + 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); - 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); + 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); - 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); - 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); - 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); - 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); - 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)); + 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.displacement = 0.0; + *fem.displacement = 0.0; - 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.displacement = 0.0; + *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.displacement = 0.0; + *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.displacement = 0.0; + *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.displacement = 0.0; + *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::DomainMapper &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); - - 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); - - 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; - - 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_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); - - 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); - - 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); - - REQUIRE(f.domainMapperStateless != nullptr); - - 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; - - 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; + mfem::Vector zero_direction_action; operators::kernels::apply_mapped_hdiv_mass_variation( - f, domain_mapper, fields.gravity_gradient, base_displacement, - fields.displacement_direction_1, analytic_action); + f, domain_mapper, fields.gravity_gradient, fields.displacement, zero_direction, zero_direction_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); + INFO("Zero-direction action norm = " << global_norm(zero_direction_action, communicator)); + CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); - const double relative_error = global_relative_error( - analytic_action, finite_difference_action, communicator); + 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("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("Zero-gravity action norm = " << global_norm(zero_gravity_action, communicator)); + CHECK(global_norm(zero_gravity_action, communicator) < 1.0e-13); - CHECK_THAT(relative_error, - Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); - } + 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 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 + ); + + 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 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)); +} + +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); + + 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; + + 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_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 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); + + 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::DomainMapper &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::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(); + 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; - operators::kernels::apply_mapped_source_variation( - f, domain_mapper, density, fields.displacement, zero_direction, - 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); + 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); + 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); + 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; + 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 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_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); + 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()); + 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); + 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 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); + 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)); + 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); +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); + 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(); + 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; + 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_source_variation( + f, domain_mapper, density, base_displacement, fields.displacement_direction_1, analytic_action + ); + + const mfem::Vector finite_difference_action = centered_source_geometry_difference( + f, domain_mapper, density, base_displacement, fields.displacement_direction_1, difference_step + ); + + REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize()); + + const double relative_error = global_relative_error(analytic_action, finite_difference_action, communicator); + + 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); + + 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::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, base_displacement, - fields.displacement_direction_1, analytic_action); - - const mfem::Vector finite_difference_action = - centered_source_geometry_difference( - f, domain_mapper, density, base_displacement, - fields.displacement_direction_1, difference_step); + f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, analytic_action + ); REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); - REQUIRE(finite_difference_action.Size() == - f.gravityPotentialFes->GetTrueVSize()); - const double relative_error = global_relative_error( - analytic_action, finite_difference_action, communicator); + constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; + std::array errors{}; - 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); + 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] + ); - 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::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, 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::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(); - - 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.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 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)); + 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, 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::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(); + + 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.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 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.displacement = 0.0; + *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); @@ -2792,418 +2664,375 @@ 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.displacement = 0.0; + *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.domainMapperStateless != 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; - 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); + 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 + ); - 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::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); - 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]); - } + 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]); + } - 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(); + 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(mass_preparations_before_solve > 0); - REQUIRE(source_preparations_before_solve > 0); + REQUIRE(mass_preparations_before_solve > 0); + REQUIRE(source_preparations_before_solve > 0); - mfem::Vector right_hand_side; - reduced_operator.BuildRightHandSide(density, right_hand_side); + mfem::Vector right_hand_side; + reduced_operator.BuildRightHandSide(density, right_hand_side); - REQUIRE(right_hand_side.Size() == reduced_operator.Height()); + REQUIRE(right_hand_side.Size() == reduced_operator.Height()); - MPI_Comm communicator = f.mesh->GetComm(); - const double right_hand_side_norm = - global_norm(right_hand_side, communicator); + MPI_Comm communicator = f.mesh->GetComm(); + const double right_hand_side_norm = global_norm(right_hand_side, communicator); - 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); + 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); - REQUIRE(right_hand_side_norm > 0.0); + REQUIRE(right_hand_side_norm > 0.0); - mfem::Vector gravity_state(reduced_operator.Width()); - gravity_state = 0.0; + mfem::Vector gravity_state(reduced_operator.Width()); + gravity_state = 0.0; - constexpr double relative_solver_tolerance = 1.0e-14; - constexpr double absolute_solver_tolerance = 1.0e-14; - const int maximum_iterations = 2000; + constexpr double relative_solver_tolerance = 1.0e-14; + constexpr double absolute_solver_tolerance = 1.0e-14; + const int maximum_iterations = 2000; - 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); + 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); - MEAN_FIELD_PROFILE_RESET(); - MEAN_FIELD_PROFILE_CALL_WARMUP("MINRES total", 0, - minres.Mult(right_hand_side, gravity_state)); - MEAN_FIELD_PROFILE_PRINT(communicator); + MEAN_FIELD_PROFILE_RESET(); + MEAN_FIELD_PROFILE_CALL_WARMUP("MINRES total", 0, minres.Mult(right_hand_side, gravity_state)); + MEAN_FIELD_PROFILE_PRINT(communicator); - 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(); + 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(); - INFO("Mass preparations after solve = " << mass_preparations_after_solve); - INFO("Source preparations after solve = " << source_preparations_after_solve); + INFO("Mass preparations after solve = " << mass_preparations_after_solve); + INFO("Source preparations after solve = " << source_preparations_after_solve); - CHECK(mass_preparations_after_solve == mass_preparations_before_solve); - CHECK(source_preparations_after_solve == source_preparations_before_solve); + CHECK(mass_preparations_after_solve == mass_preparations_before_solve); + CHECK(source_preparations_after_solve == source_preparations_before_solve); - mfem::Vector operator_action; - reduced_operator.Mult(gravity_state, operator_action); + mfem::Vector operator_action; + reduced_operator.Mult(gravity_state, operator_action); - mfem::Vector reduced_residual(operator_action); - reduced_residual -= right_hand_side; + mfem::Vector reduced_residual(operator_action); + reduced_residual -= right_hand_side; - 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); + 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); - 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; + 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; - 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); + 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(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); + 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); - constexpr double direct_residual_tolerance = 1.0e-11; + constexpr double direct_residual_tolerance = 1.0e-11; - 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_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)); - mfem::Vector full_state(layout.value_offsets().Last()); - full_state = 0.0; + mfem::Vector full_state(layout.value_offsets().Last()); + full_state = 0.0; - 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); + 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); - const operators::context::gravity_field::GravityFieldRevisions - full_state_revisions{.discretization = {1}, - .displacement = {1}, - .density = {1}, - .gravity_gradient = {1}, - .gravity_potential = {1}}; + const operators::context::gravity_field::GravityFieldRevisions full_state_revisions{ + .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} + }; - gravity_operator.Prepare(full_state, full_state_revisions); + gravity_operator.Prepare(full_state, full_state_revisions); - REQUIRE(linearization_context.IsPrepared()); + REQUIRE(linearization_context.IsPrepared()); - check_linearization_context_matches_state( - linearization_context, full_state, layout.value_offsets(), communicator); + check_linearization_context_matches_state(linearization_context, full_state, layout.value_offsets(), communicator); - mfem::Vector full_residual; - gravity_operator.Mult(full_state, full_residual); + mfem::Vector full_residual; + gravity_operator.Mult(full_state, full_residual); - REQUIRE(full_residual.Size() == layout.residual_offsets().Last()); + REQUIRE(full_residual.Size() == layout.residual_offsets().Last()); - mfem::Vector residual_representation_difference(full_residual); - residual_representation_difference -= reduced_residual; + 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; + 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); + 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)); + 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/barotropic_closure_kernels.cpp b/tests/operators/kernels/barotropic_closure_kernels.cpp index 39365b4..712c8f1 100644 --- a/tests/operators/kernels/barotropic_closure_kernels.cpp +++ b/tests/operators/kernels/barotropic_closure_kernels.cpp @@ -72,7 +72,10 @@ TEST_CASE( constexpr double enthalpyValue = 0.8; - const double densityValue = barotrope.density_from_enthalpy(enthalpyValue); + const double densityValue = mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpyValue} + ) + .value(); const mfem::Vector enthalpy = project_constant(*f.enthalpyFes, enthalpyValue); diff --git a/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp b/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp index 60674f1..eb938da 100644 --- a/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp +++ b/tests/operators/kernels/hydrostatic_equilibrium_kernels.cpp @@ -9,900 +9,848 @@ 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 = field_dof_test_utils::vacuum_material_attribute; - - 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; + return trueVector; } - } - mfem::PWConstCoefficient coefficient(attributeValues); + mfem::Vector make_constant_field( + mfem::ParFiniteElementSpace &finiteElementSpace, + const double value + ) { + mfem::ConstantCoefficient coefficient(value); - return project_scalar(*f.gravityPotentialFes, coefficient); -} + return project_scalar(finiteElementSpace, coefficient); + } -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) {} + 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); + }); - 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); - } + return project_scalar(*f.enthalpyFes, coefficient); + } -private: - const mean_field::fem::FEM &f_; + 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 mean_field::mapping::DomainMapper &domainMapper_; + return project_scalar(*f.gravityPotentialFes, coefficient); + } - const mfem::Vector &displacementTrue_; -}; + 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 = field_dof_test_utils::vacuum_material_attribute; + + 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::DomainMapper &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::DomainMapper &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(); +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); + mfem::Vector position(3); + mfem::Vector direction(3); - position(0) = 0.71; - position(1) = -0.42; - position(2) = 0.36; + position(0) = 0.71; + position(1) = -0.42; + position(2) = 0.36; - direction(0) = -0.17; - direction(1) = 0.29; - direction(2) = 0.11; + direction(0) = -0.17; + direction(1) = 0.29; + direction(2) = 0.11; - constexpr double epsilon = 1.0e-7; + constexpr double epsilon = 1.0e-7; - mfem::Vector plusPosition(position); - mfem::Vector minusPosition(position); + mfem::Vector plusPosition(position); + mfem::Vector minusPosition(position); - plusPosition.Add(epsilon, direction); - minusPosition.Add(-epsilon, direction); + plusPosition.Add(epsilon, direction); + minusPosition.Add(-epsilon, direction); - const double centeredDerivative = - (rotation.potential(plusPosition) - rotation.potential(minusPosition)) / - (2.0 * epsilon); + const double centeredDerivative = + (rotation.potential(plusPosition) - rotation.potential(minusPosition)) / (2.0 * epsilon); - const double analyticDerivative = - rotation.potential_directional_derivative(position, direction); + 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()); + const double relativeError = std::abs(centeredDerivative - analyticDerivative) / + std::max(std::abs(analyticDerivative), std::numeric_limits::epsilon()); - INFO("Rigid-rotation derivative error = " << relativeError); + INFO("Rigid-rotation derivative error = " << relativeError); - CHECK(relativeError < 2.0e-9); + CHECK(relativeError < 2.0e-9); } -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(); +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(); - 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(); - constexpr double bernoulliConstant = 0.73; - constexpr double potentialValue = -0.21; - constexpr double constantOffset = 0.40; + constexpr double bernoulliConstant = 0.73; + constexpr double potentialValue = -0.21; + constexpr double constantOffset = 0.40; - mfem::FunctionCoefficient enthalpyCoefficient( - [&rotation](const mfem::Vector &position) { - return bernoulliConstant - potentialValue + - rotation.potential(position); - }); + mfem::FunctionCoefficient enthalpyCoefficient([&rotation](const mfem::Vector &position) { + return bernoulliConstant - potentialValue + rotation.potential(position); + }); - const mfem::Vector interpolatedEnthalpy = - hydrostatic_kernel_test_utils::project_scalar(*f.enthalpyFes, - enthalpyCoefficient); + const mfem::Vector interpolatedEnthalpy = + hydrostatic_kernel_test_utils::project_scalar(*f.enthalpyFes, enthalpyCoefficient); - 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 displacement = - gravity_prepared_test_utils::make_displacement(f, 0.0); + const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 0.0); - const MPI_Comm communicator = f.mesh->GetComm(); + 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; + /* + * 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, + interpolatedResidual + ); - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, interpolatedEnthalpy, potential, - displacement, bernoulliConstant + constantOffset, - interpolatedReferenceResidual); + 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 interpolatedResidualNorm = + gravity_prepared_test_utils::global_norm(interpolatedResidual, communicator); - const double interpolatedReferenceNorm = - gravity_prepared_test_utils::global_norm(interpolatedReferenceResidual, - communicator); + const double interpolatedReferenceNorm = + gravity_prepared_test_utils::global_norm(interpolatedReferenceResidual, communicator); - REQUIRE(interpolatedReferenceNorm > 1.0e-12); + REQUIRE(interpolatedReferenceNorm > 1.0e-12); - const double representationFloor = - interpolatedResidualNorm / interpolatedReferenceNorm; + const double representationFloor = interpolatedResidualNorm / interpolatedReferenceNorm; - INFO("Interpolated rotating-state residual norm = " - << interpolatedResidualNorm); + INFO("Interpolated rotating-state residual norm = " << interpolatedResidualNorm); - INFO("Interpolated rotating-state relative " - "representation floor = " - << representationFloor); + 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); + /* + * 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); + /* + * 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); + mfem::Vector correctionRightHandSide(interpolatedResidual); - correctionRightHandSide *= -1.0; + correctionRightHandSide *= -1.0; - mfem::Vector enthalpyCorrection(f.enthalpyFes->GetTrueVSize()); + mfem::Vector enthalpyCorrection(f.enthalpyFes->GetTrueVSize()); - enthalpyCorrection = 0.0; + enthalpyCorrection = 0.0; - mfem::CGSolver projectionSolver(communicator); + mfem::CGSolver projectionSolver(communicator); - projectionSolver.SetOperator(enthalpyMassOperator); + projectionSolver.SetOperator(enthalpyMassOperator); - projectionSolver.SetRelTol(1.0e-12); - projectionSolver.SetAbsTol(1.0e-15); - projectionSolver.SetMaxIter(1000); - projectionSolver.SetPrintLevel(0); + projectionSolver.SetRelTol(1.0e-12); + projectionSolver.SetAbsTol(1.0e-15); + projectionSolver.SetMaxIter(1000); + projectionSolver.SetPrintLevel(0); - projectionSolver.Mult(correctionRightHandSide, enthalpyCorrection); + projectionSolver.Mult(correctionRightHandSide, enthalpyCorrection); - INFO("Discrete-equilibrium projection converged = " - << projectionSolver.GetConverged()); + INFO("Discrete-equilibrium projection converged = " << projectionSolver.GetConverged()); - INFO("Discrete-equilibrium projection iterations = " - << projectionSolver.GetNumIterations()); + INFO("Discrete-equilibrium projection iterations = " << projectionSolver.GetNumIterations()); - INFO("Discrete-equilibrium projection final norm = " - << projectionSolver.GetFinalNorm()); + INFO("Discrete-equilibrium projection final norm = " << projectionSolver.GetFinalNorm()); - REQUIRE(projectionSolver.GetConverged()); + REQUIRE(projectionSolver.GetConverged()); - mfem::Vector correctionEquationResidual; + mfem::Vector correctionEquationResidual; - enthalpyMassOperator.Mult(enthalpyCorrection, correctionEquationResidual); + enthalpyMassOperator.Mult(enthalpyCorrection, correctionEquationResidual); - correctionEquationResidual -= correctionRightHandSide; + correctionEquationResidual -= correctionRightHandSide; - const double correctionEquationNorm = - gravity_prepared_test_utils::global_norm(correctionEquationResidual, - communicator); + const double correctionEquationNorm = + gravity_prepared_test_utils::global_norm(correctionEquationResidual, communicator); - INFO("Discrete-equilibrium correction-equation " - "residual norm = " - << correctionEquationNorm); + INFO( + "Discrete-equilibrium correction-equation " + "residual norm = " + << correctionEquationNorm + ); - CHECK(correctionEquationNorm <= - std::max(5.0e-12 * interpolatedResidualNorm, 5.0e-15)); + CHECK(correctionEquationNorm <= std::max(5.0e-12 * interpolatedResidualNorm, 5.0e-15)); - mfem::Vector discreteEnthalpy(interpolatedEnthalpy); + mfem::Vector discreteEnthalpy(interpolatedEnthalpy); - discreteEnthalpy += enthalpyCorrection; + discreteEnthalpy += enthalpyCorrection; - mfem::Vector exactResidual; - mfem::Vector referenceResidual; + 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, + exactResidual + ); - mean_field::operators::kernels::apply_hydrostatic_equilibrium( - f, *f.domainMapperStateless, rotation, discreteEnthalpy, potential, - displacement, bernoulliConstant + constantOffset, referenceResidual); + 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 exactNorm = gravity_prepared_test_utils::global_norm(exactResidual, communicator); - const double referenceNorm = - gravity_prepared_test_utils::global_norm(referenceResidual, communicator); + const double referenceNorm = gravity_prepared_test_utils::global_norm(referenceResidual, communicator); - const double correctionNorm = gravity_prepared_test_utils::global_norm( - enthalpyCorrection, communicator); + const double correctionNorm = gravity_prepared_test_utils::global_norm(enthalpyCorrection, communicator); - INFO("Enthalpy representation correction norm = " << correctionNorm); + INFO("Enthalpy representation correction norm = " << correctionNorm); - INFO("Discrete manufactured residual norm = " << exactNorm); + INFO("Discrete manufactured residual norm = " << exactNorm); - INFO("Discrete reference residual norm = " << referenceNorm); + INFO("Discrete reference residual norm = " << referenceNorm); - REQUIRE(referenceNorm > 1.0e-12); + REQUIRE(referenceNorm > 1.0e-12); - CHECK(exactNorm <= 5.0e-12 * referenceNorm); + 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, trialEnthalpy, trialPotential, - trialDisplacement, trialConstant, residual); + f, *f.domainMapperStateless, rotation, enthalpy, vacuumPotential, displacement, 0.0, residual + ); - return residual; - }; + mean_field::operators::kernels::apply_hydrostatic_equilibrium_potential_action( + f, *f.domainMapperStateless, vacuumPotential, displacement, vacuumAction + ); - mfem::Vector plusEnthalpy(enthalpy); - mfem::Vector minusEnthalpy(enthalpy); + mean_field::operators::kernels::apply_hydrostatic_equilibrium_potential_action( + f, *f.domainMapperStateless, stellarPotential, displacement, stellarAction + ); - plusEnthalpy.Add(epsilon, enthalpyVariation); + const MPI_Comm communicator = f.mesh->GetComm(); - minusEnthalpy.Add(-epsilon, enthalpyVariation); + const double residualNorm = gravity_prepared_test_utils::global_norm(residual, 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 vacuumActionNorm = gravity_prepared_test_utils::global_norm(vacuumAction, communicator); - mfem::Vector plusPotential(potential); - mfem::Vector minusPotential(potential); + const double stellarActionNorm = gravity_prepared_test_utils::global_norm(stellarAction, communicator); - plusPotential.Add(epsilon, potentialVariation); + REQUIRE(stellarActionNorm > 1.0e-12); - minusPotential.Add(-epsilon, potentialVariation); + CHECK(residualNorm <= 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); + CHECK(vacuumActionNorm <= 5.0e-13 * stellarActionNorm); } -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(); +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); + 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 firstDirection = - gravity_prepared_test_utils::make_deterministic_vector( - f.displacementFes->GetTrueVSize(), 0.31); + const mfem::Vector enthalpyVariation = + gravity_prepared_test_utils::make_deterministic_vector(f.enthalpyFes->GetTrueVSize(), 0.23); - const mfem::Vector secondDirection = - gravity_prepared_test_utils::make_deterministic_vector( - f.displacementFes->GetTrueVSize(), 0.83); + const mfem::Vector potentialVariation = + gravity_prepared_test_utils::make_deterministic_vector(f.gravityPotentialFes->GetTrueVSize(), 0.47); - constexpr double firstScale = 0.43; - constexpr double secondScale = -0.29; - constexpr double bernoulliConstant = 0.41; + const mfem::Vector displacementVariation = + gravity_prepared_test_utils::make_deterministic_vector(f.displacementFes->GetTrueVSize(), 0.71); - const mfem::Vector combinedDirection = - gravity_prepared_test_utils::linear_combination( - firstDirection, firstScale, secondDirection, secondScale); + constexpr double bernoulliConstant = 0.41; + constexpr double constantVariation = -0.37; + constexpr double epsilon = 1.0e-7; - mfem::Vector firstAction; - mfem::Vector secondAction; - mfem::Vector combinedAction; + mfem::Vector enthalpyAction; + mfem::Vector potentialAction; + mfem::Vector constantAction; + mfem::Vector displacementAction; + mfem::Vector completeAction; - 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_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, secondDirection, secondAction); + 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, combinedDirection, combinedAction); + mean_field::operators::kernels::apply_hydrostatic_equilibrium_constant_action( + f, *f.domainMapperStateless, constantVariation, displacement, constantAction + ); - const mfem::Vector expectedAction = - gravity_prepared_test_utils::linear_combination( - firstAction, firstScale, secondAction, secondScale); + mean_field::operators::kernels::apply_hydrostatic_equilibrium_displacement_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, + displacementVariation, displacementAction + ); - const double linearityError = gravity_prepared_test_utils::relative_error( - combinedAction, expectedAction, f.mesh->GetComm()); + mean_field::operators::kernels::apply_hydrostatic_equilibrium_action( + f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, enthalpyVariation, + potentialVariation, constantVariation, displacementVariation, completeAction + ); - INFO("Hydrostatic displacement-linearity error = " << linearityError); + mfem::Vector blockAction(enthalpyAction); + blockAction += potentialAction; + blockAction += constantAction; + blockAction += displacementAction; - CHECK(linearityError < 5.0e-12); + 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); } 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 b0865fe..0a2405f 100644 --- a/tests/operators/kernels/pressure_force_kernels.cpp +++ b/tests/operators/kernels/pressure_force_kernels.cpp @@ -10,704 +10,664 @@ 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; -} - -[[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; + return vector; } - 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_zero_displacement(const mean_field::fem::FEM &f) { + mfem::Vector displacementTrue(f.displacementFes->GetTrueVSize()); + displacementTrue = 0.0; + return displacementTrue; } - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacement); + [[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; } - if (compactificationDofTransformation != nullptr) { - compactificationDofTransformation->InvTransformPrimal( - elementCompactification); + [[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; } - const mean_field::mapping::ElementDisplacementData displacementData = - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacementElement, elementDisplacement); + [[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::ElementCompactificationData compactificationData( - compactificationElement, elementCompactification); + MFEM_VERIFY(component >= 0 && component < dimension, "The requested vector component is invalid."); - const mean_field::mapping::ElementMappingData mappingData{ - .displacement = displacementData, - .compactification = compactificationData}; + MFEM_VERIFY(coordinate >= -1 && coordinate < dimension, "The requested coordinate is invalid."); - 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); + /* + * 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::MfemRule rule = - f.quadratureFactory->get(query, transformation->GetGeometryType()); + value(component) = coordinate < 0 ? 1.0 : position(coordinate); + } + ); - MFEM_VERIFY(rule.integration_rule != nullptr, - "The pressure-integral quadrature rule is null."); + mfem::ParGridFunction field(f.displacementFes.get()); - enthalpyShape.SetSize(enthalpyElement.GetDof()); + field.ProjectCoefficient(coefficient); - for (int quadratureIndex = 0; - quadratureIndex < rule.integration_rule->GetNPoints(); - ++quadratureIndex) { - const mfem::IntegrationPoint &integrationPoint = - rule.integration_rule->IntPoint(quadratureIndex); + mfem::Vector fieldTrue; + field.GetTrueDofs(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; + return fieldTrue; } - } - double globalPressureIntegral = 0.0; + [[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."); - MPI_Allreduce(&localPressureIntegral, &globalPressureIntegral, 1, MPI_DOUBLE, - MPI_SUM, f.mesh->GetComm()); + const double localDot = left * right; + double globalDot = 0.0; - return globalPressureIntegral; -} + 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; + } + + 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 = mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpyValue} + ) + .value(); + + 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; + } } // 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()); - - /* - * 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); - - mfem::Vector residualTrue; - - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, - residualTrue); - - REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); - - const double residualNorm = - gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); - - 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(); - - 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); - - /* - * 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); - - mfem::Vector 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()); - - INFO("Positive-pressure residual norm = " << residualNorm); - - CHECK(std::isfinite(residualNorm)); - - 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(); - - 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 double residualNorm = - gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); - - REQUIRE(residualNorm > 0.0); - - 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); - - 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 dotProductScale = - std::fmax(residualNorm * translationNorm, 1.0); - - CAPTURE(component, translationWork, 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 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); + const double enthalpyNorm = gravity_prepared_test_utils::global_norm(enthalpyTrue, f.mesh->GetComm()); /* - * The signs must be opposite because the implemented pressure - * force is the negative variation of the pressure-volume - * functional. + * Ensure this is a real exclusion test rather than another + * all-zero-input test. */ - CHECK(residualWork * pressureVolumeDerivative < 0.0); - } + REQUIRE(enthalpyNorm > 0.0); - INFO("Best pressure-volume relative discrepancy = " - << bestRelativeDiscrepancy); + const mfem::Vector displacementTrue = pressure_force_kernel_test_utils::make_zero_displacement(f); - /* - * This is intentionally a provisional but meaningful threshold. - * We will tighten it after measuring the convergence plateau. - */ - CHECK(bestRelativeDiscrepancy < 1.0e-8); + mfem::Vector residualTrue; + + mean_field::operators::kernels::apply_pressure_force_residual( + f, *f.domainMapperStateless, barotrope, enthalpyTrue, displacementTrue, residualTrue + ); + + REQUIRE(residualTrue.Size() == f.displacementFes->GetTrueVSize()); + + const double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); + + 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(); + + 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); + + /* + * 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); + + mfem::Vector 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()); + + INFO("Positive-pressure residual norm = " << residualNorm); + + CHECK(std::isfinite(residualNorm)); + + 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(); + + 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 double residualNorm = gravity_prepared_test_utils::global_norm(residualTrue, f.mesh->GetComm()); + + REQUIRE(residualNorm > 0.0); + + 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); + + 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 dotProductScale = std::fmax(residualNorm * translationNorm, 1.0); + + CAPTURE(component, translationWork, 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 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); } diff --git a/tests/operators/prepared_barotropic_closure.cpp b/tests/operators/prepared_barotropic_closure.cpp index 4749617..b5bbc0e 100644 --- a/tests/operators/prepared_barotropic_closure.cpp +++ b/tests/operators/prepared_barotropic_closure.cpp @@ -242,8 +242,12 @@ namespace prepared_barotropic_closure_test_utils { const ClosureCondition &condition ) { mfem::FunctionCoefficient coefficient([&equationOfState, condition](const mfem::Vector &position) { - const double enthalpy = evaluate_enthalpy(position, condition); - return condition.densityFactor * equationOfState.density_from_enthalpy(enthalpy) + condition.densityOffset + + const double enthalpy = evaluate_enthalpy(position, condition); + const double equationOfStateDensity = mean_field::eos::evaluate( + equationOfState, mean_field::eos::SpecificEnthalpyValue{enthalpy} + ) + .value(); + return condition.densityFactor * equationOfStateDensity + condition.densityOffset + condition.densityGradient * (0.40 * position(0) + 0.25 * position(1) - 0.15 * position(2)); }); return project_scalar(*f.densityFes, coefficient); @@ -784,8 +788,12 @@ namespace prepared_barotropic_closure_test_utils { const Maps maps(f); const mean_field::eos::Polytrope equationOfState(3.0, 1.5); - constexpr double enthalpyValue = 1.20; - const double equilibriumDensityValue = equationOfState.density_from_enthalpy(enthalpyValue); + constexpr double enthalpyValue = 1.20; + const double equilibriumDensityValue = + mean_field::eos::evaluate( + equationOfState, mean_field::eos::SpecificEnthalpyValue{enthalpyValue} + ) + .value(); const mfem::Vector enthalpy = reduce(maps.enthalpy, make_constant_field(*f.enthalpyFes, enthalpyValue)); const mfem::Vector equilibriumDensity = diff --git a/tests/operators/prepared_hdiv_mass.cpp b/tests/operators/prepared_hdiv_mass.cpp index 5129c81..43db310 100644 --- a/tests/operators/prepared_hdiv_mass.cpp +++ b/tests/operators/prepared_hdiv_mass.cpp @@ -10,181 +10,157 @@ 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); + 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()); const mfem::Vector displacement = - prepared_operator.GetDisplacementMap().gather(displacement_true); - + prepared_operator.GetDisplacementMap().gather(prepared_test::make_displacement(f, 1.0)); prepared_operator.Prepare(displacement); - mfem::Vector prepared_action; + 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); - 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); + mfem::Vector first_action; + mfem::Vector second_action; + mfem::Vector combination_action; + mfem::Vector zero_action; - const double relative_error = prepared_test::relative_error( - prepared_action, reference_action, communicator); + prepared_operator.Mult(first, first_action); + prepared_operator.Mult(second, second_action); + prepared_operator.Mult(combination, combination_action); - 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 expected_combination = prepared_test::linear_combination(first_action, 1.7, second_action, -0.4); - REQUIRE(prepared_operator.IsPrepared()); - CHECK_THAT(relative_error, WithinAbs(0.0, 2.0e-11)); + mfem::Vector zero(first.Size()); + zero = 0.0; + prepared_operator.Mult(zero, zero_action); - if (deformation_scale == 0.0) { - identity_action = prepared_action; - } else { - deformed_action = prepared_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)))); } - } - - 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()); - 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 e652ff1..f8d07b0 100644 --- a/tests/operators/prepared_hydrostatic_equilibrium_analytic_accuracy.cpp +++ b/tests/operators/prepared_hydrostatic_equilibrium_analytic_accuracy.cpp @@ -9,430 +9,423 @@ 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 &values) { - mfem::Vector vector(3); + mfem::Vector make_vector( + const std::array< + double, + 3> &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 = field_dof_test_utils::vacuum_material_attribute; + 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; - - 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}}}}; +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; - auto args = test_utils::setup_args(); + constexpr double deformationX = 1.08; + constexpr double deformationY = 0.96; - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + /* + * The third scale makes the affine deformation + * volume-preserving: + * + * det(F) = sx * sy * sz = 1. + */ + constexpr double deformationZ = 1.0 / (deformationX * deformationY); - const MPI_Comm communicator = f.mesh->GetComm(); + 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 mean_field::field::FieldDofMap enthalpyMap = - field_dof_test_utils::make_map( - *f.enthalpyFes); + auto args = test_utils::setup_args(); - const mean_field::field::FieldDofMap gravityPotentialMap = - field_dof_test_utils::make_map( - *f.gravityPotentialFes); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - const mean_field::field::FieldDofMap displacementMap = - field_dof_test_utils::make_map( - *f.displacementFes); + const MPI_Comm communicator = f.mesh->GetComm(); - const mfem::Array stellarElementMarker = - prepared_hydrostatic_analytic_solve_test_utils:: - make_stellar_element_marker(f); + const mean_field::field::FieldDofMap enthalpyMap = + field_dof_test_utils::make_map(*f.enthalpyFes); - 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 gravityPotentialMap = + field_dof_test_utils::make_map(*f.gravityPotentialFes); - REQUIRE(std::abs(deformationDeterminant - 1.0) < 2.0e-14); + const mean_field::field::FieldDofMap displacementMap = + field_dof_test_utils::make_map(*f.displacementFes); - const mean_field::physics::RigidRotation rotation = - prepared_hydrostatic_analytic_solve_test_utils::make_rotation( - analyticCase); + const mfem::Array stellarElementMarker = + prepared_hydrostatic_analytic_solve_test_utils::make_stellar_element_marker(f); - auto displacementFunction = - [&analyticCase](const mfem::Vector &referencePosition, - mfem::Vector &displacementValue) { - mfem::Vector physicalPosition; + for (const AnalyticCase &analyticCase : analyticCases) { + DYNAMIC_SECTION(analyticCase.name) { + const double deformationDeterminant = + analyticCase.deformationScale[0] * analyticCase.deformationScale[1] * analyticCase.deformationScale[2]; - prepared_hydrostatic_analytic_solve_test_utils::map_to_physical( - referencePosition, analyticCase, physicalPosition); + REQUIRE(std::abs(deformationDeterminant - 1.0) < 2.0e-14); - displacementValue.SetSize(3); - displacementValue = physicalPosition; - displacementValue -= referencePosition; - }; + const mean_field::physics::RigidRotation rotation = + prepared_hydrostatic_analytic_solve_test_utils::make_rotation(analyticCase); - auto potentialFunction = [&analyticCase, &rotation]( - const mfem::Vector &referencePosition) { - return prepared_hydrostatic_analytic_solve_test_utils:: - exact_potential_value(referencePosition, analyticCase, rotation); - }; + auto displacementFunction = + [&analyticCase](const mfem::Vector &referencePosition, mfem::Vector &displacementValue) { + mfem::Vector physicalPosition; - auto enthalpyFunction = [](const mfem::Vector &referencePosition) { - return prepared_hydrostatic_analytic_solve_test_utils:: - exact_enthalpy_value(referencePosition); - }; + prepared_hydrostatic_analytic_solve_test_utils::map_to_physical( + referencePosition, analyticCase, physicalPosition + ); - mfem::VectorFunctionCoefficient displacementCoefficient( - f.mesh->Dimension(), displacementFunction); + displacementValue.SetSize(3); + displacementValue = physicalPosition; + displacementValue -= referencePosition; + }; - mfem::FunctionCoefficient potentialCoefficient(potentialFunction); + auto potentialFunction = [&analyticCase, &rotation](const mfem::Vector &referencePosition) { + return prepared_hydrostatic_analytic_solve_test_utils::exact_potential_value( + referencePosition, analyticCase, rotation + ); + }; - mfem::FunctionCoefficient exactEnthalpyCoefficient(enthalpyFunction); + auto enthalpyFunction = [](const mfem::Vector &referencePosition) { + return prepared_hydrostatic_analytic_solve_test_utils::exact_enthalpy_value(referencePosition); + }; - /* - * Project the prescribed geometry and potential. - */ - mfem::ParGridFunction displacementField(f.displacementFes.get()); + mfem::VectorFunctionCoefficient displacementCoefficient(f.mesh->Dimension(), displacementFunction); - mfem::ParGridFunction potentialField(f.gravityPotentialFes.get()); + mfem::FunctionCoefficient potentialCoefficient(potentialFunction); - displacementField.ProjectCoefficient(displacementCoefficient); + mfem::FunctionCoefficient exactEnthalpyCoefficient(enthalpyFunction); - potentialField.ProjectCoefficient(potentialCoefficient); + /* + * Project the prescribed geometry and potential. + */ + mfem::ParGridFunction displacementField(f.displacementFes.get()); - mfem::Vector displacementTrue; - mfem::Vector gravityPotentialTrue; + mfem::ParGridFunction potentialField(f.gravityPotentialFes.get()); - displacementField.GetTrueDofs(displacementTrue); - potentialField.GetTrueDofs(gravityPotentialTrue); + displacementField.ProjectCoefficient(displacementCoefficient); - const mfem::Vector displacement = - displacementMap.gather(displacementTrue); - const mfem::Vector gravityPotential = - gravityPotentialMap.gather(gravityPotentialTrue); + potentialField.ProjectCoefficient(potentialCoefficient); - /* - * 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()); + mfem::Vector displacementTrue; + mfem::Vector gravityPotentialTrue; - projectedEnthalpyField.ProjectCoefficient(exactEnthalpyCoefficient); + displacementField.GetTrueDofs(displacementTrue); + potentialField.GetTrueDofs(gravityPotentialTrue); - mfem::ParGridFunction zeroEnthalpyField(f.enthalpyFes.get()); + const mfem::Vector displacement = displacementMap.gather(displacementTrue); + const mfem::Vector gravityPotential = gravityPotentialMap.gather(gravityPotentialTrue); - zeroEnthalpyField = 0.0; + /* + * 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()); - const double exactEnthalpyNorm = zeroEnthalpyField.ComputeL2Error( - exactEnthalpyCoefficient, nullptr, &stellarElementMarker); + projectedEnthalpyField.ProjectCoefficient(exactEnthalpyCoefficient); - const double projectionError = projectedEnthalpyField.ComputeL2Error( - exactEnthalpyCoefficient, nullptr, &stellarElementMarker); + mfem::ParGridFunction zeroEnthalpyField(f.enthalpyFes.get()); - REQUIRE(exactEnthalpyNorm > 0.0); + zeroEnthalpyField = 0.0; - const double relativeProjectionError = - projectionError / exactEnthalpyNorm; + const double exactEnthalpyNorm = + zeroEnthalpyField.ComputeL2Error(exactEnthalpyCoefficient, nullptr, &stellarElementMarker); - /* - * Begin deliberately far from equilibrium. - */ - mfem::Vector enthalpy(enthalpyMap.reduced_size()); + const double projectionError = + projectedEnthalpyField.ComputeL2Error(exactEnthalpyCoefficient, nullptr, &stellarElementMarker); - enthalpy = 0.0; + REQUIRE(exactEnthalpyNorm > 0.0); - auto dependencies = - prepared_hydrostatic_analytic_solve_test_utils::make_dependencies(); + const double relativeProjectionError = projectionError / exactEnthalpyNorm; - mean_field::operators::PreparedHydrostaticEquilibriumOperator - preparedOperator(f, *f.domainMapperStateless); + /* + * Begin deliberately far from equilibrium. + */ + mfem::Vector enthalpy(enthalpyMap.reduced_size()); - const auto initialReport = preparedOperator.Prepare( - prepared_hydrostatic_analytic_solve_test_utils::make_state( - enthalpy, gravityPotential, displacement), - dependencies, rotation); + enthalpy = 0.0; - REQUIRE(initialReport.preparedResidual); - REQUIRE(initialReport.preparedAlgebraicJacobianBlocks); + auto dependencies = prepared_hydrostatic_analytic_solve_test_utils::make_dependencies(); - mfem::Vector initialResidual; + mean_field::operators::PreparedHydrostaticEquilibriumOperator preparedOperator(f, *f.domainMapperStateless); - preparedOperator.BuildResidual(initialResidual); + const auto initialReport = preparedOperator.Prepare( + prepared_hydrostatic_analytic_solve_test_utils::make_state(enthalpy, gravityPotential, displacement), + dependencies, rotation + ); - const double initialResidualNorm = - gravity_prepared_test_utils::global_norm(initialResidual, - communicator); + REQUIRE(initialReport.preparedResidual); + REQUIRE(initialReport.preparedAlgebraicJacobianBlocks); - REQUIRE(initialResidualNorm > 1.0e-12); + mfem::Vector 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); + preparedOperator.BuildResidual(initialResidual); - mfem::Vector rightHandSide(initialResidual); - rightHandSide *= -1.0; + const double initialResidualNorm = gravity_prepared_test_utils::global_norm(initialResidual, communicator); - mfem::Vector enthalpyCorrection(enthalpyMap.reduced_size()); + REQUIRE(initialResidualNorm > 1.0e-12); - enthalpyCorrection = 0.0; + /* + * 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 + ); - /* - * 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 rightHandSide(initialResidual); + rightHandSide *= -1.0; - linearSolver.SetOperator(enthalpyJacobian); + mfem::Vector enthalpyCorrection(enthalpyMap.reduced_size()); - linearSolver.SetRelTol(1.0e-13); - linearSolver.SetAbsTol(1.0e-14); - linearSolver.SetMaxIter(2000); - linearSolver.SetPrintLevel(0); + enthalpyCorrection = 0.0; - linearSolver.Mult(rightHandSide, enthalpyCorrection); + /* + * The reduced operator contains only stellar-supported + * enthalpy DOFs and is positive definite. MINRES remains + * appropriate for this symmetric system. + */ + mfem::MINRESSolver linearSolver(communicator); - INFO("Linear solver converged = " << linearSolver.GetConverged()); + linearSolver.SetOperator(enthalpyJacobian); - INFO("Linear solver iterations = " << linearSolver.GetNumIterations()); + linearSolver.SetRelTol(1.0e-13); + linearSolver.SetAbsTol(1.0e-14); + linearSolver.SetMaxIter(2000); + linearSolver.SetPrintLevel(0); - INFO("Linear solver final norm = " << linearSolver.GetFinalNorm()); + linearSolver.Mult(rightHandSide, enthalpyCorrection); - REQUIRE(linearSolver.GetConverged()); + INFO("Linear solver converged = " << linearSolver.GetConverged()); - enthalpy += enthalpyCorrection; + INFO("Linear solver iterations = " << linearSolver.GetNumIterations()); - /* - * Only the enthalpy state changed. Geometry, rotation, - * and algebraic Jacobian data must remain reusable. - */ - ++dependencies.enthalpy.revision; + INFO("Linear solver final norm = " << linearSolver.GetFinalNorm()); - const auto solvedReport = preparedOperator.Prepare( - prepared_hydrostatic_analytic_solve_test_utils::make_state( - enthalpy, gravityPotential, displacement), - dependencies, rotation); + REQUIRE(linearSolver.GetConverged()); - CHECK(solvedReport.contextReport.updatedEnthalpy); + enthalpy += enthalpyCorrection; - CHECK(solvedReport.contextReport.preparedBaseState); + /* + * Only the enthalpy state changed. Geometry, rotation, + * and algebraic Jacobian data must remain reusable. + */ + ++dependencies.enthalpy.revision; - CHECK_FALSE(solvedReport.contextReport.preparedGeometryState); + const auto solvedReport = preparedOperator.Prepare( + prepared_hydrostatic_analytic_solve_test_utils::make_state(enthalpy, gravityPotential, displacement), + dependencies, rotation + ); - CHECK_FALSE(solvedReport.preparedAlgebraicJacobianBlocks); + CHECK(solvedReport.contextReport.updatedEnthalpy); - mfem::Vector solvedResidual; + CHECK(solvedReport.contextReport.preparedBaseState); - preparedOperator.BuildResidual(solvedResidual); + CHECK_FALSE(solvedReport.contextReport.preparedGeometryState); - const double solvedResidualNorm = - gravity_prepared_test_utils::global_norm(solvedResidual, - communicator); + CHECK_FALSE(solvedReport.preparedAlgebraicJacobianBlocks); - const double residualReduction = solvedResidualNorm / initialResidualNorm; + mfem::Vector 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()); + preparedOperator.BuildResidual(solvedResidual); - mfem::Vector enthalpyTrue(enthalpyMap.full_size()); - enthalpyMap.scatter(enthalpy, enthalpyTrue); - solvedEnthalpyField.SetFromTrueDofs(enthalpyTrue); + const double solvedResidualNorm = gravity_prepared_test_utils::global_norm(solvedResidual, communicator); - const double solvedAnalyticError = solvedEnthalpyField.ComputeL2Error( - exactEnthalpyCoefficient, nullptr, &stellarElementMarker); + const double residualReduction = solvedResidualNorm / initialResidualNorm; - const double relativeSolvedAnalyticError = - solvedAnalyticError / exactEnthalpyNorm; + /* + * 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()); - INFO("Deformation determinant = " << deformationDeterminant); + mfem::Vector enthalpyTrue(enthalpyMap.full_size()); + enthalpyMap.scatter(enthalpy, enthalpyTrue); + solvedEnthalpyField.SetFromTrueDofs(enthalpyTrue); - INFO("Initial weak residual norm = " << initialResidualNorm); + const double solvedAnalyticError = + solvedEnthalpyField.ComputeL2Error(exactEnthalpyCoefficient, nullptr, &stellarElementMarker); - INFO("Solved weak residual norm = " << solvedResidualNorm); + const double relativeSolvedAnalyticError = solvedAnalyticError / exactEnthalpyNorm; - INFO("Weak residual reduction = " << residualReduction); + INFO("Deformation determinant = " << deformationDeterminant); - INFO("Relative analytic projection floor = " << relativeProjectionError); + INFO("Initial weak residual norm = " << initialResidualNorm); - INFO("Relative solved analytic L2 error = " - << relativeSolvedAnalyticError); + INFO("Solved weak residual norm = " << solvedResidualNorm); - /* - * The discrete Bernoulli equation must be solved essentially - * to the linear-solver floor. - */ - CHECK(residualReduction < 1.0e-10); + INFO("Weak residual reduction = " << residualReduction); - /* - * 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 analytic projection floor = " << relativeProjectionError); - /* - * Record that the analytic error remains within one order of - * magnitude of the direct enthalpy projection floor. - */ - CHECK(relativeSolvedAnalyticError / relativeProjectionError < 5.0); + 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); + } } - } } diff --git a/tests/operators/prepared_hydrostatic_equilibrium_complete_jacobian.cpp b/tests/operators/prepared_hydrostatic_equilibrium_complete_jacobian.cpp index 34c9277..de538ad 100644 --- a/tests/operators/prepared_hydrostatic_equilibrium_complete_jacobian.cpp +++ b/tests/operators/prepared_hydrostatic_equilibrium_complete_jacobian.cpp @@ -138,9 +138,7 @@ TEST_CASE( mean_field::operators::PreparedHydrostaticEquilibriumOperator preparedOperator(f, *f.domainMapperStateless); const mfem::Vector enthalpy = - field_dof_test_utils::make_deterministic_supported_vector( - *f.enthalpyFes, 0.34 - ); + field_dof_test_utils::make_deterministic_supported_vector(*f.enthalpyFes, 0.34); const mfem::Vector gravityPotential = field_dof_test_utils::make_deterministic_supported_vector( @@ -161,9 +159,7 @@ TEST_CASE( ); const mfem::Vector enthalpyVariation = - field_dof_test_utils::make_deterministic_supported_vector( - *f.enthalpyFes, 1.07 - ); + field_dof_test_utils::make_deterministic_supported_vector(*f.enthalpyFes, 1.07); const mfem::Vector gravityPotentialVariation = field_dof_test_utils::make_deterministic_supported_vector( @@ -236,9 +232,7 @@ TEST_CASE( mean_field::operators::PreparedHydrostaticEquilibriumOperator preparedOperator(f, *f.domainMapperStateless); const mfem::Vector enthalpy = - field_dof_test_utils::make_deterministic_supported_vector( - *f.enthalpyFes, 0.41 - ); + field_dof_test_utils::make_deterministic_supported_vector(*f.enthalpyFes, 0.41); const mfem::Vector gravityPotential = field_dof_test_utils::make_deterministic_supported_vector( @@ -277,9 +271,7 @@ TEST_CASE( CHECK(preparedOperator.GetEnthalpyMap().inactive_size() > 0); const mfem::Vector enthalpyVariation = - field_dof_test_utils::make_deterministic_supported_vector( - *f.enthalpyFes, 1.12 - ); + field_dof_test_utils::make_deterministic_supported_vector(*f.enthalpyFes, 1.12); const mfem::Vector gravityPotentialVariation = field_dof_test_utils::make_deterministic_supported_vector( diff --git a/tests/operators/prepared_mass_normalization.cpp b/tests/operators/prepared_mass_normalization.cpp index f09b390..96263c1 100644 --- a/tests/operators/prepared_mass_normalization.cpp +++ b/tests/operators/prepared_mass_normalization.cpp @@ -26,16 +26,13 @@ namespace mass_normalization_test_utils { ); [[nodiscard]] mean_field::operators::MassNormalizationLayout make_layout(const mean_field::fem::FEM &f) { - const auto densityMap = - field_dof_test_utils::make_map(*f.densityFes); + const auto densityMap = field_dof_test_utils::make_map(*f.densityFes); const auto displacementMap = field_dof_test_utils::make_map(*f.displacementFes); - const auto gravityFluxMap = - field_dof_test_utils::make_map(*f.gravityFluxFes); + const auto gravityFluxMap = field_dof_test_utils::make_map(*f.gravityFluxFes); const auto gravityPotentialMap = field_dof_test_utils::make_map(*f.gravityPotentialFes); - const auto enthalpyMap = - field_dof_test_utils::make_map(*f.enthalpyFes); + const auto enthalpyMap = field_dof_test_utils::make_map(*f.enthalpyFes); const std::array valueSizes{ densityMap.reduced_size(), displacementMap.reduced_size(), gravityFluxMap.reduced_size(), @@ -43,7 +40,7 @@ namespace mass_normalization_test_utils { }; const std::array residualSizes{ - gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(), + gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(), displacementMap.reduced_size(), enthalpyMap.reduced_size(), 1 }; @@ -345,8 +342,7 @@ TEST_CASE( mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext); massOperator.Prepare({.targetMass = 1.11}, dependencies); - const mfem::Vector reducedDisplacementDirection = - gravityContext.GetDisplacementMap().gather(displacementDirection); + const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection); mfem::Vector analyticAction; massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, analyticAction); @@ -476,9 +472,8 @@ TEST_CASE( mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext); massOperator.Prepare({.targetMass = 1.19}, dependencies); - const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection); - const mfem::Vector reducedDisplacementDirection = - gravityContext.GetDisplacementMap().gather(displacementDirection); + const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection); + const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection); mfem::Vector densityAction; mfem::Vector displacementAction; @@ -486,9 +481,7 @@ TEST_CASE( massOperator.ApplyDensityJacobianAction(reducedDensityDirection, densityAction); massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, displacementAction); - massOperator.ApplyCompleteJacobianAction( - reducedDensityDirection, reducedDisplacementDirection, completeAction - ); + massOperator.ApplyCompleteJacobianAction(reducedDensityDirection, reducedDisplacementDirection, completeAction); CHECK( mass_normalization_test_utils::relative_error(completeAction(0), densityAction(0) + displacementAction(0)) < @@ -498,10 +491,7 @@ TEST_CASE( const auto layout = mass_normalization_test_utils::make_layout(f); mean_field::operators::PreparedMassNormalizationJacobianOperator adapter(layout, massOperator); - CHECK( - layout.size(mass_normalization_test_utils::densityValue) == - gravityContext.GetDensityMap().reduced_size() - ); + CHECK(layout.size(mass_normalization_test_utils::densityValue) == gravityContext.GetDensityMap().reduced_size()); CHECK( layout.size(mass_normalization_test_utils::displacementValue) == gravityContext.GetDisplacementMap().reduced_size() diff --git a/tests/operators/prepared_pressure_force.cpp b/tests/operators/prepared_pressure_force.cpp index de8e068..b283edc 100644 --- a/tests/operators/prepared_pressure_force.cpp +++ b/tests/operators/prepared_pressure_force.cpp @@ -11,711 +11,699 @@ 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(*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)) {} -}; + 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) + ) { + } + }; -[[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(); + 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(); +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); + 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); - const mfem::Vector enthalpy = maps.enthalpy.gather( - prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.47)); + 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 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 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)); + 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); + mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); - preparedOperator.Prepare( - {.enthalpy = enthalpy, .displacement = displacement}, - prepared_pressure_force_test_utils::make_dependencies()); + preparedOperator.Prepare( + {.enthalpy = enthalpy, .displacement = displacement}, prepared_pressure_force_test_utils::make_dependencies() + ); - const mfem::Vector enthalpyTrue = maps.enthalpy.scatter(enthalpy); + const mfem::Vector enthalpyTrue = maps.enthalpy.scatter(enthalpy); - const mfem::Vector displacementTrue = maps.displacement.scatter(displacement); + const mfem::Vector displacementTrue = maps.displacement.scatter(displacement); - const mfem::Vector enthalpyDirectionTrue = - maps.enthalpy.scatter(enthalpyDirection); + const mfem::Vector enthalpyDirectionTrue = maps.enthalpy.scatter(enthalpyDirection); - const mfem::Vector displacementDirectionTrue = - maps.displacement.scatter(displacementDirection); + const mfem::Vector displacementDirectionTrue = maps.displacement.scatter(displacementDirection); - mfem::Vector preparedEnthalpyAction; - mfem::Vector kernelEnthalpyActionTrue; + mfem::Vector preparedEnthalpyAction; + mfem::Vector kernelEnthalpyActionTrue; - preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, - preparedEnthalpyAction); + preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, preparedEnthalpyAction); - mean_field::operators::kernels::apply_pressure_force_enthalpy_action( - f, *f.domainMapperStateless, equationOfState, enthalpyTrue, - enthalpyDirectionTrue, displacementTrue, kernelEnthalpyActionTrue); + 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); + const mfem::Vector kernelEnthalpyAction = maps.displacement.gather(kernelEnthalpyActionTrue); - CHECK(prepared_pressure_force_test_utils::relative_difference( - preparedEnthalpyAction, kernelEnthalpyAction, f.mesh->GetComm()) < - 2.0e-12); + CHECK( + prepared_pressure_force_test_utils::relative_difference( + preparedEnthalpyAction, kernelEnthalpyAction, f.mesh->GetComm() + ) < 2.0e-12 + ); - mfem::Vector preparedDisplacementAction; - mfem::Vector kernelDisplacementActionTrue; + mfem::Vector preparedDisplacementAction; + mfem::Vector kernelDisplacementActionTrue; - preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, - preparedDisplacementAction); + preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, preparedDisplacementAction); - mean_field::operators::kernels::apply_pressure_force_displacement_action( - f, *f.domainMapperStateless, equationOfState, enthalpyTrue, - displacementDirectionTrue, displacementTrue, - kernelDisplacementActionTrue); + 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); + const mfem::Vector kernelDisplacementAction = maps.displacement.gather(kernelDisplacementActionTrue); - CHECK(prepared_pressure_force_test_utils::relative_difference( - preparedDisplacementAction, kernelDisplacementAction, - f.mesh->GetComm()) < 2.0e-12); + CHECK( + prepared_pressure_force_test_utils::relative_difference( + preparedDisplacementAction, kernelDisplacementAction, f.mesh->GetComm() + ) < 2.0e-12 + ); - mfem::Vector fusedAction; + mfem::Vector fusedAction; - preparedOperator.ApplyCompleteJacobianAction( - enthalpyDirection, displacementDirection, fusedAction); + preparedOperator.ApplyCompleteJacobianAction(enthalpyDirection, displacementDirection, fusedAction); - mfem::Vector expectedFusedAction(kernelEnthalpyAction); + mfem::Vector expectedFusedAction(kernelEnthalpyAction); - expectedFusedAction += kernelDisplacementAction; + expectedFusedAction += kernelDisplacementAction; - CHECK(prepared_pressure_force_test_utils::relative_difference( - fusedAction, expectedFusedAction, f.mesh->GetComm()) < 2.0e-12); + CHECK( + prepared_pressure_force_test_utils::relative_difference(fusedAction, expectedFusedAction, f.mesh->GetComm()) < + 2.0e-12 + ); } 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(); - - 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) { + 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, refinementLevels[levelIndex]); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); - REQUIRE(f.mesh->Dimension() == dimension); - REQUIRE(f.mesh->GetNE() > 0); - const MPI_Comm communicator = f.mesh->GetComm(); + const prepared_pressure_force_test_utils::Maps maps(f); - constexpr double supportRadius = - supportRadiusFraction * mean_field::utils::RADIUS; + const mean_field::eos::Polytrope equationOfState(3.0, 0.25); - constexpr double supportRadiusSquared = supportRadius * supportRadius; + const mfem::Vector enthalpy = + maps.enthalpy.gather(prepared_pressure_force_test_utils::make_positive_enthalpy_true(f, 0.53)); - auto analyticEnthalpyFunction = [supportRadiusSquared]( - const mfem::Vector &position) { - const double normalizedRadiusSquared = - (position * position) / supportRadiusSquared; + const mfem::Vector displacement = maps.displacement.gather(gravity_prepared_test_utils::make_displacement(f, 0.71)); - if (normalizedRadiusSquared >= 1.0) { - return 0.0; - } + const mfem::Vector enthalpyDirection = + maps.enthalpy.gather(prepared_pressure_force_test_utils::make_enthalpy_direction_true(f, 0.89)); - const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; + const mfem::Vector displacementDirection = + maps.displacement.gather(prepared_pressure_force_test_utils::make_displacement_direction_true(f, 0.97)); - return amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / - distanceToSupportBoundary); - }; + mean_field::operators::PreparedPressureForceOperator preparedOperator(f, *f.domainMapperStateless, equationOfState); - auto analyticPressureForceFunction = [supportRadiusSquared]( - const mfem::Vector &position, - mfem::Vector &force) { - force.SetSize(dimension); - force = 0.0; + preparedOperator.Prepare( + {.enthalpy = enthalpy, .displacement = displacement}, prepared_pressure_force_test_utils::make_dependencies() + ); - const double normalizedRadiusSquared = - (position * position) / supportRadiusSquared; + const mean_field::operators::BarotropicEquilibriumLayout layout = + prepared_pressure_force_test_utils::make_coupled_layout(maps); - if (normalizedRadiusSquared >= 1.0) { - return; - } + mean_field::operators::PreparedPressureForceJacobianOperator adapter(layout, preparedOperator); - const double distanceToSupportBoundary = 1.0 - normalizedRadiusSquared; + CHECK(layout.size(prepared_pressure_force_test_utils::enthalpyValue) == maps.enthalpy.reduced_size()); - const double enthalpy = - amplitude * std::exp(-bumpSharpness * normalizedRadiusSquared / - distanceToSupportBoundary); + CHECK(layout.size(prepared_pressure_force_test_utils::densityValue) == maps.density.reduced_size()); - const double pressureGradientScale = - -2.0 * bumpSharpness * std::pow(enthalpy, 4.0) / - (supportRadiusSquared * distanceToSupportBoundary * - distanceToSupportBoundary); + mfem::BlockVector direction(layout.value_offsets()); - for (int component = 0; component < dimension; ++component) { - force(component) = pressureGradientScale * position(component); - } - }; + direction = 0.0; - mfem::FunctionCoefficient analyticEnthalpyCoefficient( - analyticEnthalpyFunction); + /* + * Populate unrelated columns deliberately. + */ + direction.GetBlock(prepared_pressure_force_test_utils::densityValue) = 0.37; - mfem::VectorFunctionCoefficient analyticPressureForceCoefficient( - dimension, analyticPressureForceFunction); + direction.GetBlock(prepared_pressure_force_test_utils::gravityGradientValue) = -0.41; - mfem::ParGridFunction discreteEnthalpyField(f.enthalpyFes.get()); + direction.GetBlock(prepared_pressure_force_test_utils::gravityPotentialValue) = 0.59; - discreteEnthalpyField.ProjectCoefficient(analyticEnthalpyCoefficient); + direction.GetBlock(prepared_pressure_force_test_utils::barotropicConstantValue) = -0.73; - mfem::Vector discreteEnthalpyTrue; - discreteEnthalpyField.GetTrueDofs(discreteEnthalpyTrue); + direction.GetBlock(prepared_pressure_force_test_utils::displacementValue) = displacementDirection; - mfem::Vector zeroDisplacement(f.displacementFes->GetTrueVSize()); + direction.GetBlock(prepared_pressure_force_test_utils::enthalpyValue) = enthalpyDirection; - zeroDisplacement = 0.0; + mfem::Vector expectedDisplacementAction; - const mean_field::eos::Polytrope barotrope(3.0, 0.25); + preparedOperator.ApplyCompleteJacobianAction(enthalpyDirection, displacementDirection, expectedDisplacementAction); - mfem::Vector discreteResidual; + mfem::Vector action; - mean_field::operators::kernels::apply_pressure_force_residual( - f, *f.domainMapperStateless, barotrope, discreteEnthalpyTrue, - zeroDisplacement, discreteResidual); + adapter.Mult(direction, action); - REQUIRE(discreteResidual.Size() == f.displacementFes->GetTrueVSize()); + const mfem::Vector displacementResidualAction = prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::displacementResidual + ); - mfem::Array stellarMarker(f.mesh->attributes.Max()); + CHECK( + prepared_pressure_force_test_utils::relative_difference( + displacementResidualAction, expectedDisplacementAction, f.mesh->GetComm() + ) < 2.0e-14 + ); - stellarMarker = 0; + CHECK( + prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::gravityGradientResidual + ) + .Norml2() == 0.0 + ); - const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; + CHECK( + prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::gravityPotentialResidual + ) + .Norml2() == 0.0 + ); - for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size(); - ++attributeIndex) { - const int attribute = f.mesh->attributes[attributeIndex]; + CHECK( + prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::densityResidual + ) + .Norml2() == 0.0 + ); - if (attribute != vacuumAttribute) { - stellarMarker[attribute - 1] = 1; - } - } + CHECK( + prepared_pressure_force_test_utils::copy_residual_block( + action, layout, prepared_pressure_force_test_utils::enthalpyResidual + ) + .Norml2() == 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); - } - - 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(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); + 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 = field_dof_test_utils::vacuum_material_attribute; + + 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(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 64191fd..4f2fd75 100644 --- a/tests/operators/prepared_rotation_displacement_force.cpp +++ b/tests/operators/prepared_rotation_displacement_force.cpp @@ -11,682 +11,635 @@ 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< - 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 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 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}; -} - -[[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); - }); - - 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.17 * std::sin(0.9 * position(0) + phase) - - 0.12 * std::cos(0.8 * position(1) - phase) + 0.07 * position(2); - }); - - densityField.ProjectCoefficient(densityCoefficient); - - 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); - - const mfem::Vector second = - gravity_prepared_test_utils::make_displacement(f, 0.27); - - 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; - - mfem::Vector center(3); - center(0) = 0.04; - center(1) = -0.03; - center(2) = 0.02; - - return mean_field::physics::RigidRotation(angularVelocity, center); -} - -[[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}}; -} - -[[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; + return {valueSizes, residualSizes}; } - f.densityFes->GetElementDofs(elementId, densityDofs); + [[nodiscard]] mfem::Vector make_density( + const mean_field::fem::FEM &f, + const double phase + ) { + mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::Vector elementDensity(densityDofs.Size()); - elementDensity = 1.0; - densityField.SetSubVector(densityDofs, elementDensity); - ++localVacuumElements; - } + 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); + }); - int globalVacuumElements = 0; + densityField.ProjectCoefficient(densityCoefficient); - 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.17 * std::sin(0.9 * position(0) + phase) - 0.12 * std::cos(0.8 * position(1) - phase) + + 0.07 * position(2); + }); -[[nodiscard]] double global_norm(const mfem::Vector &vector, - MPI_Comm communicator) { - const double localSquaredNorm = vector * vector; - double globalSquaredNorm = 0.0; + densityField.ProjectCoefficient(densityCoefficient); - MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, - communicator); + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; + } - return std::sqrt(globalSquaredNorm); -} + [[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]] double global_dot(const mfem::Vector &left, - const mfem::Vector &right, - MPI_Comm communicator) { - REQUIRE(left.Size() == right.Size()); + const mfem::Vector second = gravity_prepared_test_utils::make_displacement(f, 0.27); - const double localDot = left * right; - double globalDot = 0.0; + direction -= second; + return direction; + } - MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator); + [[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; - return globalDot; -} + mfem::Vector center(3); + center(0) = 0.04; + center(1) = -0.03; + center(2) = 0.02; -[[nodiscard]] double relative_difference(const mfem::Vector &computed, - const mfem::Vector &reference, - MPI_Comm communicator) { - REQUIRE(computed.Size() == reference.Size()); + return mean_field::physics::RigidRotation(angularVelocity, center); + } - mfem::Vector difference(computed); - difference -= reference; + [[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 global_norm(difference, communicator) / - std::max(global_norm(reference, communicator), - std::numeric_limits::epsilon()); -} + [[nodiscard]] mfem::Vector make_vacuum_only_density(const mean_field::fem::FEM &f) { + mfem::ParGridFunction densityField(f.densityFes.get()); + densityField = 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); + const int vacuumAttribute = field_dof_test_utils::vacuum_material_attribute; - mfem::Vector minusDensity(baseDensity); - minusDensity.Add(-step, densityDirection); + mfem::Array densityDofs; + int localVacuumElements = 0; - mfem::Vector plusDisplacement(baseDisplacement); - plusDisplacement.Add(step, displacementDirection); + for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId); - mfem::Vector minusDisplacement(baseDisplacement); - minusDisplacement.Add(-step, displacementDirection); + REQUIRE(transformation != nullptr); - mfem::Vector plusResidual; - mfem::Vector minusResidual; + if (transformation->Attribute != vacuumAttribute) { + continue; + } - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, rotation, plusDensity, plusDisplacement, - plusResidual); + f.densityFes->GetElementDofs(elementId, densityDofs); - mean_field::operators::kernels::apply_rotational_displacement_force_residual( - f, *f.domainMapperStateless, rotation, minusDensity, minusDisplacement, - minusResidual); + mfem::Vector elementDensity(densityDofs.Size()); + elementDensity = 1.0; + densityField.SetSubVector(densityDofs, elementDensity); + ++localVacuumElements; + } - plusResidual -= minusResidual; - plusResidual /= 2.0 * step; - return plusResidual; -} + int globalVacuumElements = 0; -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); + MPI_Allreduce(&localVacuumElements, &globalVacuumElements, 1, MPI_INT, MPI_SUM, f.mesh->GetComm()); - for (int entry = 0; entry < result.Size(); ++entry) { - result(entry) = action(offset + entry); - } + REQUIRE(globalVacuumElements > 0); - return result; -} + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; + } + + [[nodiscard]] double global_norm( + const mfem::Vector &vector, + MPI_Comm communicator + ) { + const double localSquaredNorm = vector * vector; + double globalSquaredNorm = 0.0; + + MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, communicator); + + return std::sqrt(globalSquaredNorm); + } + + [[nodiscard]] double global_dot( + const mfem::Vector &left, + const mfem::Vector &right, + MPI_Comm communicator + ) { + REQUIRE(left.Size() == right.Size()); + + const double localDot = left * right; + double globalDot = 0.0; + + MPI_Allreduce(&localDot, &globalDot, 1, MPI_DOUBLE, MPI_SUM, communicator); + + return globalDot; + } + + [[nodiscard]] double relative_difference( + 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()); + } + + [[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); + + mfem::Vector minusDensity(baseDensity); + minusDensity.Add(-step, densityDirection); + + 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_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; + } } // 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::field::Displacement::Form::CentrifugalForce>( - 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::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 d40d920..13d4374 100644 --- a/tests/operators/prepared_stellar_equilibrium.cpp +++ b/tests/operators/prepared_stellar_equilibrium.cpp @@ -13,2427 +13,2582 @@ import mean_field; import test_helpers; namespace stellar_equilibrium_test_utils { -using Form = mean_field::utils::blocks::barotropic_equilibrium_form; -using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - -namespace field = mean_field::field; - -struct FieldMaps final { - field::FieldDofMap density; - field::FieldDofMap displacement; - field::FieldDofMap gravityFlux; - field::FieldDofMap gravityPotential; - field::FieldDofMap enthalpy; - - explicit FieldMaps(const mean_field::fem::FEM &f) - : density(field::make_field_dof_map( - *f.densityFes)), - displacement( - field::make_field_dof_map( - *f.displacementFes)), - gravityFlux(field::make_field_dof_map( - *f.gravityFluxFes)), - gravityPotential( - field::make_field_dof_map( - *f.gravityPotentialFes)), - enthalpy(field::make_field_dof_map( - *f.enthalpyFes)) {} -}; - -constexpr auto densityValue = mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::density_field.mass_term); -constexpr auto displacementValue = - mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::displacement_field.geometry_term); -constexpr auto gravityGradientValue = - mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::gravity_field.gradient_term); -constexpr auto gravityPotentialValue = - mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::gravity_field.poisson_term); -constexpr auto enthalpyValue = mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::enthalpy_field.specific_term); -constexpr auto bernoulliValue = - mean_field::utils::blocks::get_value_block( - mean_field::utils::blocks::barotropic_constant_field - .mass_normalization_term); - -constexpr auto gravityGradientResidual = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::gravity_field.gradient_term); -constexpr auto gravityPotentialResidual = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::gravity_field.poisson_term); -constexpr auto densityResidual = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::density_field.mass_term); -constexpr auto displacementResidual = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::displacement_field.geometry_term); -constexpr auto enthalpyResidual = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::enthalpy_field.specific_term); -constexpr auto massResidual = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::barotropic_constant_field - .mass_normalization_term); - -template -[[nodiscard]] mfem::Vector -value_view(mfem::Vector &vector, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::value_block block) { - return mfem::Vector(vector.GetData() + layout.offset(block), - layout.size(block)); -} - -template -void assign_value_block( - mfem::Vector &vector, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::value_block block, - const mfem::Vector &source) { - MFEM_VERIFY(source.Size() == layout.size(block), - "Source vector has the wrong size for the coupled value block."); - - const int offset = layout.offset(block); - - for (int dof = 0; dof < source.Size(); ++dof) { - vector(offset + dof) = source(dof); - } -} - -template -[[nodiscard]] mfem::Vector -const_value_view(const mfem::Vector &vector, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::value_block block) { - return mfem::Vector(const_cast(vector.GetData()) + - layout.offset(block), - layout.size(block)); -} - -template -[[nodiscard]] mfem::Vector -residual_view(mfem::Vector &vector, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::residual_block block) { - return mfem::Vector(vector.GetData() + layout.offset(block), - layout.size(block)); -} - -template -[[nodiscard]] mfem::Vector const_residual_view( - const mfem::Vector &vector, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::residual_block block) { - return mfem::Vector(const_cast(vector.GetData()) + - layout.offset(block), - layout.size(block)); -} - -[[nodiscard]] mfem::Vector reduce_density(const mean_field::fem::FEM &f, - const mfem::Vector &fullDensity) { - const field::FieldDofMap map = - field::make_field_dof_map(*f.densityFes); - return map.gather(fullDensity); -} - -[[nodiscard]] mfem::Vector reduce_enthalpy(const mean_field::fem::FEM &f, - const mfem::Vector &fullEnthalpy) { - const field::FieldDofMap map = - field::make_field_dof_map(*f.enthalpyFes); - return map.gather(fullEnthalpy); -} - -[[nodiscard]] mfem::Vector pack_gravity_state( - const mfem::Vector &density, const mfem::Vector &displacement, - const mfem::Vector &gravityGradient, const mfem::Vector &gravityPotential) { - const std::array blockSizes{density.Size(), displacement.Size(), - gravityGradient.Size(), - gravityPotential.Size()}; - - const std::array offsets{ - 0, blockSizes[0], blockSizes[0] + blockSizes[1], - blockSizes[0] + blockSizes[1] + blockSizes[2], - blockSizes[0] + blockSizes[1] + blockSizes[2] + blockSizes[3]}; - - mfem::Vector packed(offsets[4]); - - const std::array blocks{ - &density, &displacement, &gravityGradient, &gravityPotential}; - - for (int block = 0; block < 4; ++block) { - mfem::Vector destination(packed.GetData() + offsets[block], - blockSizes[block]); - destination = *blocks[block]; - } - - return packed; -} - -[[nodiscard]] mfem::Vector project_density(const mean_field::fem::FEM &f, - const double phase) { - mfem::ParGridFunction field(f.densityFes.get()); - mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { - return 0.88 + 0.07 * std::sin(0.73 * position(0) + phase) + - 0.05 * std::cos(0.59 * position(1) - phase) + - 0.025 * position(2) * position(2); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_density_direction(const mean_field::fem::FEM &f, const double phase) { - mfem::ParGridFunction field(f.densityFes.get()); - mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { - return 0.16 * std::sin(0.91 * position(0) + phase) - - 0.12 * std::cos(0.77 * position(1) - phase) + 0.06 * position(2); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_constant_density(const mean_field::fem::FEM &f, const double value) { - mfem::ParGridFunction field(f.densityFes.get()); - mfem::ConstantCoefficient coefficient(value); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector project_displacement(const mean_field::fem::FEM &f, - const double scale) { - return gravity_prepared_test_utils::make_displacement(f, scale); -} - -[[nodiscard]] mfem::Vector -project_displacement_direction(const mean_field::fem::FEM &f, - const double scale) { - mfem::ParGridFunction field(f.displacementFes.get()); - mfem::VectorFunctionCoefficient coefficient( - f.mesh->Dimension(), - [scale](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = - scale * (0.06 * position(0) + 0.014 * position(1) * position(2)); - value(1) = - scale * (-0.045 * position(1) + 0.011 * position(0) * position(2)); - value(2) = - scale * (0.035 * position(2) - 0.009 * position(0) * position(1)); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_gravity_gradient(const mean_field::fem::FEM &f, const double phase) { - mfem::ParGridFunction field(f.gravityFluxFes.get()); - mfem::VectorFunctionCoefficient coefficient( - f.mesh->Dimension(), - [phase](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.27 + 0.07 * position(0) + 0.025 * phase * position(1); - value(1) = -0.19 + 0.055 * position(1) - 0.018 * phase * position(2); - value(2) = 0.21 - 0.045 * position(2) + 0.021 * phase * position(0); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_gravity_direction(const mean_field::fem::FEM &f, const double phase) { - mfem::ParGridFunction field(f.gravityFluxFes.get()); - mfem::VectorFunctionCoefficient coefficient( - f.mesh->Dimension(), - [phase](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - value(0) = 0.13 * std::sin(position(0) + phase) + 0.025 * position(1); - value(1) = -0.10 * std::cos(position(1) - phase) + 0.035 * position(2); - value(2) = - 0.08 * std::sin(position(2) + 0.5 * phase) - 0.018 * position(0); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_gravity_potential(const mean_field::fem::FEM &f, const double phase) { - mfem::ParGridFunction field(f.gravityPotentialFes.get()); - mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { - return 0.24 + 0.09 * std::sin(0.67 * position(0) + phase) - - 0.06 * std::cos(0.53 * position(1) - phase) + 0.035 * position(2); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_potential_direction(const mean_field::fem::FEM &f, const double phase) { - mfem::ParGridFunction field(f.gravityPotentialFes.get()); - mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { - return 0.17 * std::sin(0.81 * position(0) + phase) + - 0.11 * std::cos(0.69 * position(1) - phase) - 0.07 * position(2); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector project_enthalpy(const mean_field::fem::FEM &f, - const double phase) { - mfem::ParGridFunction field(f.enthalpyFes.get()); - mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { - return 0.82 + 0.08 * std::sin(0.62 * position(0) + phase) + - 0.045 * std::cos(0.57 * position(1) - phase) + - 0.02 * position(2) * position(2); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_enthalpy_direction(const mean_field::fem::FEM &f, const double phase) { - mfem::ParGridFunction field(f.enthalpyFes.get()); - mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { - return 0.21 * std::sin(0.74 * position(0) + phase) - - 0.14 * std::cos(0.64 * position(1) - phase) + 0.075 * position(2); - }); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mfem::Vector -project_constant_scalar(mfem::ParFiniteElementSpace &finiteElementSpace, - const double value) { - mfem::ParGridFunction field(&finiteElementSpace); - mfem::ConstantCoefficient coefficient(value); - field.ProjectCoefficient(coefficient); - mfem::Vector result; - field.GetTrueDofs(result); - return result; -} - -[[nodiscard]] mean_field::physics::RigidRotation -make_rotation(const double scale) { - mfem::Vector angularVelocity(3); - angularVelocity(0) = scale * 0.16; - angularVelocity(1) = scale * -0.08; - angularVelocity(2) = scale * 0.58; - - mfem::Vector center(3); - center(0) = 0.03; - center(1) = -0.025; - center(2) = 0.015; - return mean_field::physics::RigidRotation(angularVelocity, center); -} - -[[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() { - return make_rotation(0.0); -} - -[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies -make_dependencies() { - return {.discretization = {.identity = 1009, .revision = 3}, - .density = {.identity = 1013, .revision = 5}, - .displacement = {.identity = 1019, .revision = 7}, - .gravityGradient = {.identity = 1021, .revision = 11}, - .gravityPotential = {.identity = 1031, .revision = 13}, - .enthalpy = {.identity = 1033, .revision = 17}, - .bernoulliConstant = {.identity = 1039, .revision = 19}, - .rotation = {.identity = 1049, .revision = 23}, - .targetMass = {.identity = 1051, .revision = 29}}; -} - -void increment_all_state_revisions( - mean_field::operators::StellarEquilibriumDependencies &dependencies) { - ++dependencies.density.revision; - ++dependencies.displacement.revision; - ++dependencies.gravityGradient.revision; - ++dependencies.gravityPotential.revision; - ++dependencies.enthalpy.revision; - ++dependencies.bernoulliConstant.revision; -} - -[[nodiscard]] mfem::Vector -make_state(const mean_field::fem::FEM &f, - const mean_field::operators::StellarEquilibriumLayout &layout) { - const FieldMaps maps(f); - - mfem::Vector state(layout.value_offsets().Last()); - state = 0.0; - - { - const mfem::Vector fullDensity = project_density(f, 0.31); - const mfem::Vector reducedDensity = maps.density.gather(fullDensity); - assign_value_block(state, layout, densityValue, reducedDensity); - } - - assign_value_block(state, layout, displacementValue, - project_displacement(f, 0.63)); - assign_value_block(state, layout, gravityGradientValue, - project_gravity_gradient(f, 0.43)); - assign_value_block(state, layout, gravityPotentialValue, - project_gravity_potential(f, 0.47)); - - { - const mfem::Vector fullEnthalpy = project_enthalpy(f, 0.53); - const mfem::Vector reducedEnthalpy = maps.enthalpy.gather(fullEnthalpy); - assign_value_block(state, layout, enthalpyValue, reducedEnthalpy); - } - - value_view(state, layout, bernoulliValue)(0) = 1.07; - - return state; -} - -[[nodiscard]] mfem::Vector -make_direction(const mean_field::fem::FEM &f, - const mean_field::operators::StellarEquilibriumLayout &layout) { - const FieldMaps maps(f); - - mfem::Vector direction(layout.value_offsets().Last()); - direction = 0.0; - - { - const mfem::Vector fullDensityDirection = - project_density_direction(f, 0.61); - const mfem::Vector reducedDensityDirection = - maps.density.gather(fullDensityDirection); - assign_value_block(direction, layout, densityValue, - reducedDensityDirection); - } - - assign_value_block(direction, layout, displacementValue, - project_displacement_direction(f, 0.79)); - assign_value_block(direction, layout, gravityGradientValue, - project_gravity_direction(f, 0.83)); - assign_value_block(direction, layout, gravityPotentialValue, - project_potential_direction(f, 0.89)); - - { - const mfem::Vector fullEnthalpyDirection = - project_enthalpy_direction(f, 0.97); - const mfem::Vector reducedEnthalpyDirection = - maps.enthalpy.gather(fullEnthalpyDirection); - assign_value_block(direction, layout, enthalpyValue, - reducedEnthalpyDirection); - } - - value_view(direction, layout, bernoulliValue)(0) = -0.37; - - return direction; -} - -[[nodiscard]] double global_norm(const mfem::Vector &vector, - const MPI_Comm communicator) { - return gravity_prepared_test_utils::global_norm(vector, communicator); -} - -[[nodiscard]] double relative_difference(const mfem::Vector &left, - const mfem::Vector &right, - const MPI_Comm communicator) { - mfem::Vector difference(left); - difference -= right; - const double scale = std::max( - {global_norm(left, communicator), global_norm(right, communicator), - 100.0 * std::numeric_limits::epsilon()}); - return global_norm(difference, communicator) / scale; -} - -[[nodiscard]] mfem::Vector explicit_residual( - const mean_field::operators::PreparedStellarEquilibriumOperator - &stellarOperator, - const mean_field::fem::FEM &f, const mfem::Vector &state) { - const mean_field::operators::StellarEquilibriumLayout &layout = - stellarOperator.GetLayout(); - const mfem::Vector reducedDensity = - const_value_view(state, layout, densityValue); - const mfem::Vector displacement = - const_value_view(state, layout, displacementValue); - const mfem::Vector gravityGradient = - const_value_view(state, layout, gravityGradientValue); - const mfem::Vector gravityPotential = - const_value_view(state, layout, gravityPotentialValue); - - const mfem::Vector gravityState = pack_gravity_state( - reducedDensity, displacement, gravityGradient, gravityPotential); - - mfem::Vector gravity; - mfem::Vector closure; - mfem::Vector displacementResidualValue; - mfem::Vector hydrostatic; - mfem::Vector mass; - - stellarOperator.GetGravityOperator().Mult(gravityState, gravity); - stellarOperator.GetBarotropicClosureOperator().BuildResidual(closure); - stellarOperator.GetDisplacementOperator().BuildResidual( - displacementResidualValue); - stellarOperator.GetHydrostaticOperator().BuildResidual(hydrostatic); - stellarOperator.GetMassNormalizationOperator().BuildResidual(mass); - - mfem::Vector result(layout.residual_offsets().Last()); - result = 0.0; - - MFEM_VERIFY(gravity.Size() == layout.size(gravityGradientResidual) + - layout.size(gravityPotentialResidual), - "Explicit gravity residual has the wrong size."); - - const mfem::Vector gravityGradientResidualValue( - gravity.GetData(), layout.size(gravityGradientResidual)); - const mfem::Vector gravityPotentialResidualValue( - gravity.GetData() + layout.size(gravityGradientResidual), - layout.size(gravityPotentialResidual)); - - residual_view(result, layout, gravityGradientResidual) = - gravityGradientResidualValue; - residual_view(result, layout, gravityPotentialResidual) = - gravityPotentialResidualValue; - - residual_view(result, layout, densityResidual) = closure; - - residual_view(result, layout, displacementResidual) = - displacementResidualValue; - - residual_view(result, layout, enthalpyResidual) = hydrostatic; - - residual_view(result, layout, massResidual) = mass; - - return result; -} - -[[nodiscard]] mfem::Vector explicit_jacobian_action( - const mean_field::operators::PreparedStellarEquilibriumOperator - &stellarOperator, - const mfem::Vector &direction) { - const mean_field::operators::StellarEquilibriumLayout &layout = - stellarOperator.GetLayout(); - - const mfem::Vector reducedDensityDirection = - const_value_view(direction, layout, densityValue); - const mfem::Vector displacementDirection = - const_value_view(direction, layout, displacementValue); - const mfem::Vector gravityGradientDirection = - const_value_view(direction, layout, gravityGradientValue); - const mfem::Vector gravityPotentialDirection = - const_value_view(direction, layout, gravityPotentialValue); - const mfem::Vector reducedEnthalpyDirection = - const_value_view(direction, layout, enthalpyValue); - const mfem::Vector bernoulliDirection = - const_value_view(direction, layout, bernoulliValue); - - const mfem::Vector gravityDirection = - pack_gravity_state(reducedDensityDirection, displacementDirection, - gravityGradientDirection, gravityPotentialDirection); - - mfem::Vector gravityAction; - mfem::Vector closureAction; - mfem::Vector displacementAction; - mfem::Vector hydrostaticAction; - mfem::Vector massAction; - - stellarOperator.GetGravityJacobianOperator().Mult(gravityDirection, - gravityAction); - - stellarOperator.GetBarotropicClosureOperator().Mult( - reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, - closureAction); - - stellarOperator.GetDisplacementOperator().ApplyCompleteJacobianAction( - reducedDensityDirection, displacementDirection, gravityGradientDirection, - reducedEnthalpyDirection, displacementAction); - - stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction( - reducedEnthalpyDirection, gravityPotentialDirection, - bernoulliDirection(0), displacementDirection, hydrostaticAction); - - stellarOperator.GetMassNormalizationOperator().ApplyCompleteJacobianAction( - reducedDensityDirection, displacementDirection, massAction); - - mfem::Vector result(layout.residual_offsets().Last()); - result = 0.0; - - MFEM_VERIFY(gravityAction.Size() == layout.size(gravityGradientResidual) + - layout.size(gravityPotentialResidual), - "Explicit gravity Jacobian action has the wrong size."); - - const mfem::Vector gravityGradientAction( - gravityAction.GetData(), layout.size(gravityGradientResidual)); - const mfem::Vector gravityPotentialAction( - gravityAction.GetData() + layout.size(gravityGradientResidual), - layout.size(gravityPotentialResidual)); - - residual_view(result, layout, gravityGradientResidual) = - gravityGradientAction; - residual_view(result, layout, gravityPotentialResidual) = - gravityPotentialAction; - - residual_view(result, layout, densityResidual) = closureAction; - - residual_view(result, layout, displacementResidual) = displacementAction; - - residual_view(result, layout, enthalpyResidual) = hydrostaticAction; - - residual_view(result, layout, massResidual) = massAction; - - return result; -} - -[[nodiscard]] long long global_sum(const int localValue, - const MPI_Comm communicator) { - const long long local = static_cast(localValue); - long long global = 0; - - MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, communicator); - - return global; -} + 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) + ) { + } + }; + + [[nodiscard]] auto make_stellar_model( + const mean_field::eos::Polytrope &equationOfState, + const double targetMass, + const mean_field::eos::PressureValue surfacePressure = mean_field::eos::PressureValue{0.0} + ) { + return mean_field::models::StellarModel{ + mean_field::models::structure::PolytropicStructure{equationOfState, targetMass}, + mean_field::surface::ConstantPressureSurface{surfacePressure} + }; + } + + 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.GetCenteringConstraintOperator().ApplyResidualRows(displacementResidualValue); + stellarOperator.GetHydrostaticOperator().BuildResidual(hydrostatic); + stellarOperator.GetSurfaceConstraintOperator().ApplyResidualRows(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.GetCenteringConstraintOperator().ApplyJacobianRows(displacementDirection, displacementAction); + + stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction( + reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection, + hydrostaticAction + ); + stellarOperator.GetSurfaceConstraintOperator().ApplyJacobianRows(reducedEnthalpyDirection, 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; + } -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); -} + [[nodiscard]] long long global_sum( + const int localValue, + const MPI_Comm communicator + ) { + const long long local = static_cast(localValue); + long long global = 0; + + MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, communicator); + + return global; + } + + template + [[nodiscard]] double block_relative_difference( + const mfem::Vector &left, + const mfem::Vector &right, + const mean_field::operators::StellarEquilibriumLayout &layout, + const mean_field::utils::blocks::residual_block block, + const MPI_Comm communicator + ) { + return relative_difference( + const_residual_view(left, layout, block), const_residual_view(right, layout, block), communicator + ); + } } // namespace stellar_equilibrium_test_utils TEST_CASE( "Prepared Stellar Equilibrium Uses Supported Field DOFs For Solver Blocks", - tags::barotrope &tags::prepared &tags::field &tags::unit) { - mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); + 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::models::StellarModel stellarModel{ + mean_field::models::structure::PolytropicStructure{mean_field::eos::Polytrope{3.0, 0.25}, 1.0}, + mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} + }; + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); - const auto &layout = stellarOperator.GetLayout(); - const stellar_equilibrium_test_utils::FieldMaps maps(f); + CHECK(stellarOperator.GetTargetMass() == stellarModel.targetMass()); - 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); + const auto &layout = stellarOperator.GetLayout(); + const stellar_equilibrium_test_utils::FieldMaps maps(f); - 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::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(maps.displacement.is_identity()); - CHECK(maps.gravityFlux.is_identity()); - CHECK(maps.gravityPotential.is_identity()); + 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(stellarOperator.Width() == layout.value_offsets().Last()); - CHECK(stellarOperator.Height() == layout.residual_offsets().Last()); + CHECK(maps.displacement.is_identity()); + CHECK(maps.gravityFlux.is_identity()); + CHECK(maps.gravityPotential.is_identity()); - const MPI_Comm communicator = f.mesh->GetComm(); + CHECK(stellarOperator.Width() == layout.value_offsets().Last()); + CHECK(stellarOperator.Height() == layout.residual_offsets().Last()); - 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 MPI_Comm communicator = f.mesh->GetComm(); - 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 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); - 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); + 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); - REQUIRE(globalDensityFull > 0); - REQUIRE(globalEnthalpyFull > 0); + 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); - CHECK(globalDensityReduced > 0); - CHECK(globalDensityReduced < globalDensityFull); + REQUIRE(globalDensityFull > 0); + REQUIRE(globalEnthalpyFull > 0); - CHECK(globalEnthalpyReduced > 0); - CHECK(globalEnthalpyReduced < globalEnthalpyFull); + CHECK(globalDensityReduced > 0); + CHECK(globalDensityReduced < globalDensityFull); + + CHECK(globalEnthalpyReduced > 0); + CHECK(globalEnthalpyReduced < globalEnthalpyFull); } -TEST_CASE("Prepared Stellar Equilibrium Jacobian Is The Exact Restricted Full " - "Child Jacobian", - tags::barotrope_prepared_jacobian_accuracy &tags::field) { - mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); +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); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.19); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); - 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 Replaces Stellar Surface Rows With The Compiled Physical Constraint", + tags::surface_row_replacement +) { + 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 equationOfState(3.0, 0.25); + constexpr double targetPressure = 0.03125; + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model( + equationOfState, 1.19, mean_field::eos::PressureValue{targetPressure} + ); + + STATIC_CHECK( + mean_field::operators::SingleFieldPressureSurfaceConstraintFor< + typename std::remove_cvref_t::SurfaceConstraintType, mean_field::field::Enthalpy> + ); + + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); + + const auto &layout = stellarOperator.GetLayout(); + const mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout); + const auto report = stellarOperator.Prepare( + state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_rotation(0.82) + ); + + CHECK(report.surfaceConstraint.cachedSurfaceState); + + const auto &surfaceRows = stellarOperator.GetSurfaceConstraintOperator().GetSurfaceRows(); + CHECK(stellar_equilibrium_test_utils::global_sum(surfaceRows.size(), f.mesh->GetComm()) > 0); + + const double requiredSurfaceEnthalpy = mean_field::eos::evaluate( + equationOfState, mean_field::eos::PressureValue{targetPressure} + ) + .value(); + + mfem::Vector residual; + stellarOperator.BuildResidual(residual); + const mfem::Vector enthalpyState = + stellar_equilibrium_test_utils::const_value_view(state, layout, stellar_equilibrium_test_utils::enthalpyValue); + const mfem::Vector enthalpyResidual = stellar_equilibrium_test_utils::const_residual_view( + residual, layout, stellar_equilibrium_test_utils::enthalpyResidual + ); + + for (const int reducedDof : surfaceRows.reduced_dofs()) { + CAPTURE(reducedDof); + CHECK(std::abs(enthalpyResidual(reducedDof) - (enthalpyState(reducedDof) - requiredSurfaceEnthalpy)) < 1.0e-13); + } + + const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout); + mfem::Vector action; + stellarOperator.Mult(direction, action); + const mfem::Vector enthalpyDirection = stellar_equilibrium_test_utils::const_value_view( + direction, layout, stellar_equilibrium_test_utils::enthalpyValue + ); + const mfem::Vector enthalpyAction = stellar_equilibrium_test_utils::const_residual_view( + action, layout, stellar_equilibrium_test_utils::enthalpyResidual + ); + + for (const int reducedDof : surfaceRows.reduced_dofs()) { + CAPTURE(reducedDof); + CHECK(std::abs(enthalpyAction(reducedDof) - enthalpyDirection(reducedDof)) < 1.0e-13); + } + + mfem::Vector nonEnthalpyDirection(direction); + stellar_equilibrium_test_utils::value_view( + nonEnthalpyDirection, layout, stellar_equilibrium_test_utils::enthalpyValue + ) = 0.0; + stellarOperator.Mult(nonEnthalpyDirection, action); + + const mfem::Vector nonEnthalpySurfaceAction = stellar_equilibrium_test_utils::const_residual_view( + action, layout, stellar_equilibrium_test_utils::enthalpyResidual + ); + for (const int reducedDof : surfaceRows.reduced_dofs()) { + CAPTURE(reducedDof); + CHECK(nonEnthalpySurfaceAction(reducedDof) == 0.0); + } +} + +TEST_CASE( + "Prepared Stellar Equilibrium Replaces Center Force Rows With A Translational Centering Constraint", + tags::translational_centering_enforcement +) { + 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 equationOfState(3.0, 0.25); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(equationOfState, 1.19); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); + + const auto &layout = stellarOperator.GetLayout(); + mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout); + + mfem::Vector translation(f.mesh->SpaceDimension()); + translation(0) = 0.17; + translation(1) = -0.11; + translation(2) = 0.08; + mfem::VectorConstantCoefficient translationCoefficient(translation); + mfem::ParGridFunction translationField(f.displacementFes.get()); + translationField.ProjectCoefficient(translationCoefficient); + mfem::Vector translationTrue; + translationField.GetTrueDofs(translationTrue); + stellar_equilibrium_test_utils::assign_value_block( + state, layout, stellar_equilibrium_test_utils::displacementValue, translationTrue + ); + + const auto report = stellarOperator.Prepare( + state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_zero_rotation() + ); + CHECK(report.centeringConstraint.cachedCenterDisplacement); + + const auto ¢erRows = stellarOperator.GetCenteringConstraintOperator().GetCenterRows(); + CHECK(stellar_equilibrium_test_utils::global_sum(centerRows.size(), f.mesh->GetComm()) == f.mesh->SpaceDimension()); + + mfem::Vector residual; + stellarOperator.BuildResidual(residual); + const mfem::Vector displacementState = stellar_equilibrium_test_utils::const_value_view( + state, layout, stellar_equilibrium_test_utils::displacementValue + ); + const mfem::Vector displacementResidual = stellar_equilibrium_test_utils::const_residual_view( + residual, layout, stellar_equilibrium_test_utils::displacementResidual + ); + for (const int centerRow : centerRows.reduced_dofs()) { + CAPTURE(centerRow); + CHECK(displacementResidual(centerRow) == displacementState(centerRow)); + } + + mfem::Vector translationDirection(layout.value_offsets().Last()); + translationDirection = 0.0; + stellar_equilibrium_test_utils::assign_value_block( + translationDirection, layout, stellar_equilibrium_test_utils::displacementValue, translationTrue + ); + + mfem::Vector action; + stellarOperator.Mult(translationDirection, action); + const mfem::Vector displacementDirection = stellar_equilibrium_test_utils::const_value_view( + translationDirection, layout, stellar_equilibrium_test_utils::displacementValue + ); + const mfem::Vector displacementAction = stellar_equilibrium_test_utils::const_residual_view( + action, layout, stellar_equilibrium_test_utils::displacementResidual + ); + for (const int centerRow : centerRows.reduced_dofs()) { + CAPTURE(centerRow); + CHECK(displacementAction(centerRow) == displacementDirection(centerRow)); + } + + mfem::Vector nonDisplacementDirection = stellar_equilibrium_test_utils::make_direction(f, layout); + stellar_equilibrium_test_utils::value_view( + nonDisplacementDirection, layout, stellar_equilibrium_test_utils::displacementValue + ) = 0.0; + stellarOperator.Mult(nonDisplacementDirection, action); + + const mfem::Vector nonDisplacementAction = stellar_equilibrium_test_utils::const_residual_view( + action, layout, stellar_equilibrium_test_utils::displacementResidual + ); + for (const int centerRow : centerRows.reduced_dofs()) { + CAPTURE(centerRow); + CHECK(nonDisplacementAction(centerRow) == 0.0); + } } 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); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.13); + Operator stellarOperator(f, *f.domainMapperStateless, stellarModel); - 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.surfaceConstraint.DidAnyWork()); + CHECK(report.centeringConstraint.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()); +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::eos::Polytrope barotrope(1.0, 0.25); + constexpr double enthalpy = 0.60; + const double density = mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpy} + ) + .value(); + 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 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); + 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 surfacePressure = mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpy} + ); + const auto stellarModel = + stellar_equilibrium_test_utils::make_stellar_model(barotrope, targetMass, surfacePressure); - const auto &layout = stellarOperator.GetLayout(); - mfem::Vector state(layout.value_offsets().Last()); - state = 0.0; + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); - 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))); + 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::gravityPotentialValue, - stellar_equilibrium_test_utils::project_constant_scalar( - *f.gravityPotentialFes, gravityPotential)); + 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::enthalpyValue, - stellar_equilibrium_test_utils::reduce_enthalpy( - f, stellar_equilibrium_test_utils::project_constant_scalar( - *f.enthalpyFes, enthalpy))); + 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::value_view( - state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = - bernoulliConstant; + 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) + ) + ); - stellarOperator.Prepare(state, - stellar_equilibrium_test_utils::make_dependencies(), - stellar_equilibrium_test_utils::make_zero_rotation()); + stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) = + bernoulliConstant; - mfem::Vector residual; - stellarOperator.BuildResidual(residual); + stellarOperator.Prepare( + state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_zero_rotation() + ); - 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)); + mfem::Vector residual; + stellarOperator.BuildResidual(residual); - INFO("Exact n=1 closure norm = " << closureNorm); - INFO("Exact constant hydrostatic norm = " << hydrostaticNorm); - INFO("Independent constant-density mass error = " << massError); + 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) + ); - CHECK(closureNorm < 2.0e-12); - CHECK(hydrostaticNorm < 2.0e-12); - CHECK(massError < 2.0e-11 * targetMass); + 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()); + 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 mean_field::eos::Polytrope barotrope(3.0, 0.25); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.0); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); - const auto &layout = stellarOperator.GetLayout(); - mfem::Vector state(layout.value_offsets().Last()); - state = 0.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()); + 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); + 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()); + 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); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.17); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); + 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); + 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); } - - mfem::Vector action; - stellarOperator.Mult(columnDirection, action); - - const std::array rowActions{ - stellar_equilibrium_test_utils::const_residual_view( - action, layout, - stellar_equilibrium_test_utils::gravityGradientResidual), - stellar_equilibrium_test_utils::const_residual_view( - action, layout, - stellar_equilibrium_test_utils::gravityPotentialResidual), - stellar_equilibrium_test_utils::const_residual_view( - action, layout, stellar_equilibrium_test_utils::densityResidual), - stellar_equilibrium_test_utils::const_residual_view( - action, layout, - stellar_equilibrium_test_utils::displacementResidual), - stellar_equilibrium_test_utils::const_residual_view( - action, layout, stellar_equilibrium_test_utils::enthalpyResidual), - stellar_equilibrium_test_utils::const_residual_view( - action, layout, stellar_equilibrium_test_utils::massResidual)}; - - double allowedNormSquared = 0.0; - - for (int row = 0; row < 6; ++row) { - CAPTURE(row); - - const double rowNorm = stellar_equilibrium_test_utils::global_norm( - rowActions[row], f.mesh->GetComm()); - - if (shapeCase.allowedRows[row]) { - allowedNormSquared += rowNorm * rowNorm; - } else { - CHECK(rowNorm == 0.0); - } - } - - CHECK(allowedNormSquared > 0.0); - } } -TEST_CASE("Prepared Stellar Equilibrium Complete Jacobian Matches Every " - "Coupled Centered Difference Block", - tags::barotrope &tags::prepared &tags::jacobian &tags::accuracy - &tags::geometry) { - mean_field::utils::Args args = test_utils::setup_args(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(f.okay()); +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); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.21); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); + 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); + stellarOperator.Prepare(baseState, dependencies, rotation); - mfem::Vector analyticAction; - stellarOperator.Mult(direction, analyticAction); + 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); + 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); + 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; + 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())}; + 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]); + 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); + 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()); +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); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.09); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); + 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); + 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 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); + 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 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; + 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); + 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; + 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); + 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 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()); + 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); + 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); + 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); + 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()); + 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); + const mean_field::eos::Polytrope barotrope(3.0, 0.25); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.15); + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); + 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); + 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 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 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); + const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout); + mfem::Vector action; + stellarOperator.Mult(direction, action); + stellarOperator.Mult(direction, action); + stellarOperator.Mult(direction, action); - CHECK(stellarOperator.GetBarotropicClosureOperator().GetPreparationCount() == - closurePreparations); - CHECK( - stellarOperator.GetHydrostaticOperator().GetResidualPreparationCount() == - hydrostaticPreparations); - CHECK( - stellarOperator.GetDisplacementOperator().GetResidualPreparationCount() == - displacementPreparations); - CHECK(stellarOperator.GetMassNormalizationOperator().GetPreparationCount() == - massPreparations); - CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies); - CHECK(stellarOperator.GetStatistics().jacobianApplications == 3); + CHECK(stellarOperator.GetBarotropicClosureOperator().GetPreparationCount() == closurePreparations); + CHECK(stellarOperator.GetHydrostaticOperator().GetResidualPreparationCount() == hydrostaticPreparations); + CHECK(stellarOperator.GetDisplacementOperator().GetResidualPreparationCount() == displacementPreparations); + CHECK(stellarOperator.GetMassNormalizationOperator().GetPreparationCount() == massPreparations); + CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies); + CHECK(stellarOperator.GetStatistics().jacobianApplications == 3); - stellar_equilibrium_test_utils::value_view( - state, layout, stellar_equilibrium_test_utils::gravityPotentialValue) - .Add(0.03, stellar_equilibrium_test_utils::project_potential_direction( - f, 0.41)); - ++dependencies.gravityPotential.revision; + stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::gravityPotentialValue) + .Add(0.03, stellar_equilibrium_test_utils::project_potential_direction(f, 0.41)); + ++dependencies.gravityPotential.revision; - const auto potentialReport = - stellarOperator.Prepare(state, dependencies, rotation); + const auto potentialReport = stellarOperator.Prepare(state, dependencies, rotation); - CHECK_FALSE(potentialReport.barotropicClosure.DidAnyWork()); - CHECK(potentialReport.hydrostatic.DidAnyWork()); - CHECK_FALSE(potentialReport.displacement.DidAnyWork()); - CHECK_FALSE(potentialReport.massNormalization.DidAnyWork()); - CHECK(potentialReport.assembledResidual); + CHECK_FALSE(potentialReport.barotropicClosure.DidAnyWork()); + CHECK(potentialReport.hydrostatic.DidAnyWork()); + CHECK_FALSE(potentialReport.displacement.DidAnyWork()); + CHECK_FALSE(potentialReport.massNormalization.DidAnyWork()); + CHECK(potentialReport.assembledResidual); - stellar_equilibrium_test_utils::value_view( - state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) += 0.09; - ++dependencies.bernoulliConstant.revision; + stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::bernoulliValue)(0) += + 0.09; + ++dependencies.bernoulliConstant.revision; - const auto bernoulliReport = - stellarOperator.Prepare(state, dependencies, rotation); + const auto bernoulliReport = stellarOperator.Prepare(state, dependencies, rotation); - CHECK_FALSE(bernoulliReport.barotropicClosure.DidAnyWork()); - CHECK(bernoulliReport.hydrostatic.DidAnyWork()); - CHECK_FALSE(bernoulliReport.displacement.DidAnyWork()); - CHECK_FALSE(bernoulliReport.massNormalization.DidAnyWork()); - CHECK(bernoulliReport.assembledResidual); + CHECK_FALSE(bernoulliReport.barotropicClosure.DidAnyWork()); + CHECK(bernoulliReport.hydrostatic.DidAnyWork()); + CHECK_FALSE(bernoulliReport.displacement.DidAnyWork()); + CHECK_FALSE(bernoulliReport.massNormalization.DidAnyWork()); + CHECK(bernoulliReport.assembledResidual); - ++dependencies.targetMass.revision; - const auto targetReport = - stellarOperator.Prepare(state, dependencies, rotation); + ++dependencies.targetMass.revision; + const auto targetReport = stellarOperator.Prepare(state, dependencies, rotation); - CHECK_FALSE(targetReport.barotropicClosure.DidAnyWork()); - CHECK_FALSE(targetReport.hydrostatic.DidAnyWork()); - CHECK_FALSE(targetReport.displacement.DidAnyWork()); - CHECK(targetReport.massNormalization.DidAnyWork()); - CHECK(targetReport.assembledResidual); + CHECK_FALSE(targetReport.barotropicClosure.DidAnyWork()); + CHECK_FALSE(targetReport.hydrostatic.DidAnyWork()); + CHECK_FALSE(targetReport.displacement.DidAnyWork()); + CHECK(targetReport.massNormalization.DidAnyWork()); + CHECK(targetReport.assembledResidual); } -TEST_CASE("Prepared Stellar Equilibrium Matches The Analytic N1 Lane Emden " - "State Up To The Mixed Projection Floor", - tags::barotrope &tags::prepared &tags::analytic_comparison - &tags::accuracy &tags::gravity &tags::hydro &tags::residuals) { - class LaneEmdenGravityGradientCoefficient final - : public mfem::VectorCoefficient { - public: - LaneEmdenGravityGradientCoefficient(const int dimension, - const int vacuumAttribute, - const double stellarRadius, - const double centralDensity, - const double targetMass, - const double polytropicConstant) - : mfem::VectorCoefficient(dimension), - m_vacuumAttribute(vacuumAttribute), m_stellarRadius(stellarRadius), - m_centralDensity(centralDensity), m_targetMass(targetMass), - m_polytropicConstant(polytropicConstant) {} - - void Eval(mfem::Vector &value, mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integrationPoint) override { - mfem::Vector computationalPosition; - transformation.Transform(integrationPoint, computationalPosition); - - value.SetSize(vdim); - value = 0.0; - - const double radius = computationalPosition.Norml2(); - - if (!std::isfinite(radius) || - radius <= 100.0 * std::numeric_limits::epsilon()) { - return; - } - - double radialGradient = 0.0; - - if (transformation.Attribute == m_vacuumAttribute) { - /* - * In the compactified exterior, the three-dimensional H(div) - * Piola pullback of the inverse-square monopole field reduces - * to this finite computational-space expression. - */ - radialGradient = - mean_field::utils::G * m_targetMass / (radius * radius); - } else { - const double pi = std::acos(-1.0); - const double xi = pi * radius / m_stellarRadius; - - if (std::abs(xi) < 1.0e-5) { - /* - * sin(xi) - xi cos(xi) = xi^3 / 3 + O(xi^5). - */ - radialGradient = (4.0 / 3.0) * pi * mean_field::utils::G * - m_centralDensity * radius; - } else { - radialGradient = 2.0 * m_polytropicConstant * m_centralDensity * pi / - m_stellarRadius * - (std::sin(xi) - xi * std::cos(xi)) / (xi * xi); +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) { } - } - value = computationalPosition; - value *= radialGradient / radius; - } + void Eval( + mfem::Vector &value, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integrationPoint + ) override { + mfem::Vector computationalPosition; + transformation.Transform(integrationPoint, computationalPosition); - private: - int m_vacuumAttribute; - double m_stellarRadius; - double m_centralDensity; - double m_targetMass; - double m_polytropicConstant; - }; + value.SetSize(vdim); + value = 0.0; - mean_field::utils::Args args = test_utils::setup_args(); + const double radius = computationalPosition.Norml2(); - mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); + if (!std::isfinite(radius) || radius <= 100.0 * std::numeric_limits::epsilon()) { + return; + } - REQUIRE(f.okay()); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); + double radialGradient = 0.0; - *f.displacement = 0.0; + if (transformation.Attribute == m_vacuumAttribute) { + /* + * In the compactified exterior, the three-dimensional H(div) + * Piola pullback of the inverse-square monopole field reduces + * to this finite computational-space expression. + */ + radialGradient = mean_field::utils::G * m_targetMass / (radius * radius); + } else { + const double pi = std::acos(-1.0); + const double xi = pi * radius / m_stellarRadius; - const double pi = std::acos(-1.0); - const double stellarRadius = mean_field::utils::RADIUS; - const double targetMass = mean_field::utils::MASS; + 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); + } + } - /* - * 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; + value = computationalPosition; + value *= radialGradient / radius; + } - /* - * The analytic n = 1 mass is - * - * M = 4 rho_c R^3 / pi. - */ - const double centralDensity = - pi * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius); + private: + int m_vacuumAttribute; + double m_stellarRadius; + double m_centralDensity; + double m_targetMass; + double m_polytropicConstant; + }; - const double bernoulliConstant = - -mean_field::utils::G * targetMass / stellarRadius; + mean_field::utils::Args args = test_utils::setup_args(); - const mean_field::eos::Polytrope barotrope(1.0, polytropicConstant); + mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); - const auto densityFunction = [centralDensity, stellarRadius, - pi](const mfem::Vector &position) { - const double radius = position.Norml2(); + REQUIRE(f.okay()); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); - if (radius >= stellarRadius) { - return 0.0; - } + *f.displacement = 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(); + const double pi = std::acos(-1.0); + const double stellarRadius = mean_field::utils::RADIUS; + const double targetMass = mean_field::utils::MASS; /* - * Phi tends to zero at compactified infinity. + * 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. */ - if (!std::isfinite(radius)) { - return 0.0; - } - - if (radius >= stellarRadius) { - return radius > 0.0 ? -mean_field::utils::G * targetMass / radius : 0.0; - } - - const double xi = pi * radius / stellarRadius; - - const double density = - std::abs(xi) < 100.0 * std::numeric_limits::epsilon() - ? centralDensity - : centralDensity * std::sin(xi) / xi; - - const double enthalpy = 2.0 * polytropicConstant * density; + const double polytropicConstant = 2.0 * mean_field::utils::G * stellarRadius * stellarRadius / pi; /* - * Hydrostatic equilibrium is h + Phi = C. + * The analytic n = 1 mass is + * + * M = 4 rho_c R^3 / pi. */ - return bernoulliConstant - enthalpy; - }; + const double centralDensity = pi * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius); - mfem::FunctionCoefficient densityCoefficient(densityFunction); - mfem::FunctionCoefficient enthalpyCoefficient(enthalpyFunction); + const double bernoulliConstant = -mean_field::utils::G * targetMass / stellarRadius; - mean_field::mapping::PhysicalPositionFunctionCoefficient potentialCoefficient( - *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, - potentialFunction); + const mean_field::eos::Polytrope barotrope(1.0, polytropicConstant); - LaneEmdenGravityGradientCoefficient gravityGradientCoefficient( - f.mesh->Dimension(), field_dof_test_utils::vacuum_material_attribute, - stellarRadius, centralDensity, targetMass, polytropicConstant); + const auto densityFunction = [centralDensity, stellarRadius, pi](const mfem::Vector &position) { + const double radius = position.Norml2(); - mfem::ParGridFunction densityField(f.densityFes.get()); - mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); - mfem::ParGridFunction gravityPotentialField(f.gravityPotentialFes.get()); - mfem::ParGridFunction gravityGradientField(f.gravityFluxFes.get()); + if (radius >= stellarRadius) { + return 0.0; + } - densityField = 0.0; - enthalpyField = 0.0; - gravityPotentialField = 0.0; - gravityGradientField = 0.0; + const double xi = pi * radius / stellarRadius; - densityField.ProjectCoefficient(densityCoefficient); - enthalpyField.ProjectCoefficient(enthalpyCoefficient); - gravityPotentialField.ProjectCoefficient(potentialCoefficient); - gravityGradientField.ProjectCoefficient(gravityGradientCoefficient); + if (std::abs(xi) < 100.0 * std::numeric_limits::epsilon()) { + return centralDensity; + } - mfem::Vector densityTrue; - mfem::Vector enthalpyTrue; - mfem::Vector gravityPotentialTrue; - mfem::Vector gravityGradientTrue; + return centralDensity * std::sin(xi) / xi; + }; - densityField.GetTrueDofs(densityTrue); - enthalpyField.GetTrueDofs(enthalpyTrue); - gravityPotentialField.GetTrueDofs(gravityPotentialTrue); - gravityGradientField.GetTrueDofs(gravityGradientTrue); + const auto enthalpyFunction = [centralDensity, stellarRadius, polytropicConstant, + pi](const mfem::Vector &position) { + const double radius = position.Norml2(); - mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( - f, *f.domainMapperStateless, barotrope, targetMass); + if (radius >= stellarRadius) { + return 0.0; + } - const mean_field::operators::StellarEquilibriumLayout &layout = - stellarOperator.GetLayout(); + const double xi = pi * radius / stellarRadius; - mfem::Vector analyticState(layout.value_offsets().Last()); - analyticState = 0.0; + const double density = std::abs(xi) < 100.0 * std::numeric_limits::epsilon() + ? centralDensity + : 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)); + return 2.0 * polytropicConstant * density; + }; - stellar_equilibrium_test_utils::assign_value_block( - analyticState, layout, - stellar_equilibrium_test_utils::gravityGradientValue, - gravityGradientTrue); + const auto potentialFunction = [centralDensity, stellarRadius, targetMass, polytropicConstant, bernoulliConstant, + pi](const mfem::Vector &physicalPosition) { + const double radius = physicalPosition.Norml2(); - stellar_equilibrium_test_utils::assign_value_block( - analyticState, layout, - stellar_equilibrium_test_utils::gravityPotentialValue, - gravityPotentialTrue); + /* + * Phi tends to zero at compactified infinity. + */ + if (!std::isfinite(radius)) { + return 0.0; + } - stellar_equilibrium_test_utils::assign_value_block( - analyticState, layout, stellar_equilibrium_test_utils::enthalpyValue, - stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyTrue)); + if (radius >= stellarRadius) { + return radius > 0.0 ? -mean_field::utils::G * targetMass / radius : 0.0; + } - stellar_equilibrium_test_utils::value_view( - analyticState, layout, - stellar_equilibrium_test_utils::bernoulliValue)(0) = bernoulliConstant; + const double xi = pi * radius / stellarRadius; - mean_field::operators::StellarEquilibriumDependencies dependencies = - stellar_equilibrium_test_utils::make_dependencies(); + const double density = std::abs(xi) < 100.0 * std::numeric_limits::epsilon() + ? centralDensity + : centralDensity * std::sin(xi) / xi; - const mean_field::physics::RigidRotation zeroRotation = - stellar_equilibrium_test_utils::make_zero_rotation(); + const double enthalpy = 2.0 * polytropicConstant * density; - stellarOperator.Prepare(analyticState, dependencies, zeroRotation); + /* + * Hydrostatic equilibrium is h + Phi = C. + */ + return bernoulliConstant - enthalpy; + }; - mfem::Vector analyticResidual; - stellarOperator.BuildResidual(analyticResidual); + mfem::FunctionCoefficient densityCoefficient(densityFunction); + mfem::FunctionCoefficient enthalpyCoefficient(enthalpyFunction); - 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()); + mean_field::mapping::PhysicalPositionFunctionCoefficient potentialCoefficient( + *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, potentialFunction + ); - 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()); + LaneEmdenGravityGradientCoefficient gravityGradientCoefficient( + f.mesh->Dimension(), field_dof_test_utils::vacuum_material_attribute, stellarRadius, centralDensity, targetMass, + polytropicConstant + ); - 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()); + mfem::ParGridFunction densityField(f.densityFes.get()); + mfem::ParGridFunction enthalpyField(f.enthalpyFes.get()); + mfem::ParGridFunction gravityPotentialField(f.gravityPotentialFes.get()); + mfem::ParGridFunction gravityGradientField(f.gravityFluxFes.get()); - 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()); + densityField = 0.0; + enthalpyField = 0.0; + gravityPotentialField = 0.0; + gravityGradientField = 0.0; - 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()); + densityField.ProjectCoefficient(densityCoefficient); + enthalpyField.ProjectCoefficient(enthalpyCoefficient); + gravityPotentialField.ProjectCoefficient(potentialCoefficient); + gravityGradientField.ProjectCoefficient(gravityGradientCoefficient); - const double analyticMassError = - std::abs(stellar_equilibrium_test_utils::const_residual_view( - analyticResidual, layout, - stellar_equilibrium_test_utils::massResidual)(0)); + mfem::Vector densityTrue; + mfem::Vector enthalpyTrue; + mfem::Vector gravityPotentialTrue; + mfem::Vector gravityGradientTrue; - /* - * Construct a deliberately inconsistent nearby state. The analytic - * projection should have a substantially smaller residual in every row. - */ - mfem::Vector perturbedState(analyticState); + densityField.GetTrueDofs(densityTrue); + enthalpyField.GetTrueDofs(enthalpyTrue); + gravityPotentialField.GetTrueDofs(gravityPotentialTrue); + gravityGradientField.GetTrueDofs(gravityGradientTrue); - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, stellar_equilibrium_test_utils::densityValue); - block *= 1.12; - } + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, targetMass); - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, - stellar_equilibrium_test_utils::gravityGradientValue); - block *= 0.87; - } + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, - stellar_equilibrium_test_utils::gravityPotentialValue); - block *= 1.08; - } + const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout(); - { - mfem::Vector block = stellar_equilibrium_test_utils::value_view( - perturbedState, layout, stellar_equilibrium_test_utils::enthalpyValue); - block *= 0.91; - } + mfem::Vector analyticState(layout.value_offsets().Last()); + analyticState = 0.0; - stellar_equilibrium_test_utils::value_view( - perturbedState, layout, - stellar_equilibrium_test_utils::bernoulliValue)(0) *= 1.04; + 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::increment_all_state_revisions(dependencies); + stellar_equilibrium_test_utils::assign_value_block( + analyticState, layout, stellar_equilibrium_test_utils::gravityGradientValue, gravityGradientTrue + ); - stellarOperator.Prepare(perturbedState, dependencies, zeroRotation); + stellar_equilibrium_test_utils::assign_value_block( + analyticState, layout, stellar_equilibrium_test_utils::gravityPotentialValue, gravityPotentialTrue + ); - mfem::Vector perturbedResidual; - stellarOperator.BuildResidual(perturbedResidual); + stellar_equilibrium_test_utils::assign_value_block( + analyticState, layout, stellar_equilibrium_test_utils::enthalpyValue, + stellar_equilibrium_test_utils::reduce_enthalpy(f, enthalpyTrue) + ); - 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()); + stellar_equilibrium_test_utils::value_view(analyticState, layout, stellar_equilibrium_test_utils::bernoulliValue)( + 0 + ) = bernoulliConstant; - 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()); + mean_field::operators::StellarEquilibriumDependencies dependencies = + stellar_equilibrium_test_utils::make_dependencies(); - 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 mean_field::physics::RigidRotation zeroRotation = stellar_equilibrium_test_utils::make_zero_rotation(); - 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()); + stellarOperator.Prepare(analyticState, dependencies, zeroRotation); - 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()); + mfem::Vector analyticResidual; + stellarOperator.BuildResidual(analyticResidual); - const double perturbedMassError = - std::abs(stellar_equilibrium_test_utils::const_residual_view( - perturbedResidual, layout, - stellar_equilibrium_test_utils::massResidual)(0)); + 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() + ); - 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); + 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() + ); - INFO("Gravity-gradient residual: analytic = " - << analyticGradientNorm << ", perturbed = " << perturbedGradientNorm); + 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() + ); - INFO("Poisson residual: analytic = " - << analyticPoissonNorm << ", perturbed = " << perturbedPoissonNorm); + 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() + ); - INFO("Closure residual: analytic = " - << analyticClosureNorm << ", perturbed = " << perturbedClosureNorm); + 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() + ); - INFO("Displacement residual: analytic = " << analyticDisplacementNorm - << ", perturbed = " - << perturbedDisplacementNorm); + const double analyticMassError = std::abs( + stellar_equilibrium_test_utils:: + const_residual_view(analyticResidual, layout, stellar_equilibrium_test_utils::massResidual)(0) + ); - INFO("Hydrostatic residual: analytic = " << analyticHydrostaticNorm - << ", perturbed = " - << perturbedHydrostaticNorm); + /* + * Construct a deliberately inconsistent nearby state. The analytic + * projection should have a substantially smaller residual in every row. + */ + mfem::Vector perturbedState(analyticState); - INFO("Mass error: analytic = " << analyticMassError - << ", perturbed = " << perturbedMassError); + { + mfem::Vector block = stellar_equilibrium_test_utils::value_view( + perturbedState, layout, stellar_equilibrium_test_utils::densityValue + ); + block *= 1.12; + } - REQUIRE(std::isfinite(perturbedGradientNorm)); - REQUIRE(perturbedPoissonNorm > 0.0); - REQUIRE(perturbedClosureNorm > 0.0); - REQUIRE(perturbedDisplacementNorm > 0.0); - REQUIRE(perturbedHydrostaticNorm > 0.0); - REQUIRE(perturbedMassError > 0.0); + { + mfem::Vector block = stellar_equilibrium_test_utils::value_view( + perturbedState, layout, stellar_equilibrium_test_utils::gravityGradientValue + ); + block *= 0.87; + } - /* - * 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); + { + mfem::Vector block = stellar_equilibrium_test_utils::value_view( + perturbedState, layout, stellar_equilibrium_test_utils::gravityPotentialValue + ); + block *= 1.08; + } - CHECK(analyticHydrostaticNorm < 0.10 * perturbedHydrostaticNorm); + { + mfem::Vector block = stellar_equilibrium_test_utils::value_view( + perturbedState, layout, stellar_equilibrium_test_utils::enthalpyValue + ); + block *= 0.91; + } - /* - * 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); + stellar_equilibrium_test_utils::value_view(perturbedState, layout, stellar_equilibrium_test_utils::bernoulliValue)( + 0 + ) *= 1.04; - CHECK(analyticDisplacementNorm < 0.35 * perturbedDisplacementNorm); + stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); - CHECK(analyticMassError < 5.0e-5 * targetMass); + stellarOperator.Prepare(perturbedState, dependencies, zeroRotation); - CHECK(analyticMassError < 0.10 * perturbedMassError); + 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(); + 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.domainMapperStateless != 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); - - /* - * 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); + densityField.ProjectCoefficient(densityCoefficient); + enthalpyField.ProjectCoefficient(enthalpyCoefficient); /* - * The Lane-Emden state must be closer to equilibrium than the nearby - * perturbed state in every residual row. + * Gravity initialization and the prepared root operator must see the + * same undeformed geometry. */ - CHECK(equilibriumRowNorms[row] < perturbedRowNorms[row]); + *f.displacement = displacementField; - /* - * 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 mean_field::physics::GravitySolution gravitySolution = + mean_field::physics::solve_gravity_field(f, args, densityField, displacementField); - /* - * Record an absolute regression bound for the current coarse-mesh - * displacement-force projection floor. - */ - CHECK(equilibriumRowNorms[3] < 1.0e-3); + mfem::Vector densityTrue; + mfem::Vector enthalpyTrue; + mfem::Vector displacementTrue; + mfem::Vector gravityGradientTrue; + mfem::Vector gravityPotentialTrue; - const double equilibriumMassError = - std::abs(stellar_equilibrium_test_utils::const_residual_view( - equilibriumResidual, layout, - stellar_equilibrium_test_utils::massResidual)(0)); + densityField.GetTrueDofs(densityTrue); + enthalpyField.GetTrueDofs(enthalpyTrue); + displacementField.GetTrueDofs(displacementTrue); + gravitySolution.gradPhi.GetTrueDofs(gravityGradientTrue); + gravitySolution.phi.GetTrueDofs(gravityPotentialTrue); - INFO("Equilibrium relative mass error = " << equilibriumMassError / - targetMass); + const double bernoulliConstant = -mean_field::utils::G * targetMass / stellarRadius; - CHECK(equilibriumMassError < 5.0e-4 * targetMass); + const mean_field::eos::Polytrope barotrope(3.0, polytropicConstant); - /* - * 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; + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, targetMass); - mfem::Vector linearizedDeparture(jacobianAction); - linearizedDeparture *= perturbationScale; + mean_field::operators::PreparedStellarEquilibriumOperator stellarOperator( + f, *f.domainMapperStateless, stellarModel + ); - mfem::Vector nonlinearRemainder(residualDeparture); - nonlinearRemainder -= linearizedDeparture; + const mean_field::operators::StellarEquilibriumLayout &layout = stellarOperator.GetLayout(); - const double departureNorm = stellar_equilibrium_test_utils::global_norm( - residualDeparture, f.mesh->GetComm()); - - const double nonlinearRemainderNorm = - stellar_equilibrium_test_utils::global_norm(nonlinearRemainder, - f.mesh->GetComm()); - - /* - * Apply the known restoring correction -epsilon p through the Jacobian. - * - * This predicts the residual after returning to the equilibrium state: - * - * R(x + epsilon p) - epsilon J(x)p approximately R(x). - */ - mfem::Vector restoredResidualPrediction(perturbedResidual); - restoredResidualPrediction.Add(-perturbationScale, jacobianAction); - - restoredResidualPrediction -= equilibriumResidual; - - const double restoredDistance = stellar_equilibrium_test_utils::global_norm( - restoredResidualPrediction, f.mesh->GetComm()); - - INFO("Residual departure norm = " << departureNorm); - INFO("Nonlinear remainder norm = " << nonlinearRemainderNorm); - INFO("Distance after the restoring Jacobian correction = " - << restoredDistance); - INFO("Relative first-order remainder = " << nonlinearRemainderNorm / - departureNorm); - - REQUIRE(std::isfinite(departureNorm)); - REQUIRE(std::isfinite(nonlinearRemainderNorm)); - REQUIRE(std::isfinite(restoredDistance)); - REQUIRE(departureNorm > 0.0); - - CHECK(nonlinearRemainderNorm < 5.0e-2 * departureNorm); - - CHECK(restoredDistance < 5.0e-2 * departureNorm); - - /* - * Rotational shape response - * - * At moderate rotation, the leading deformation is a smooth, axisymmetric, - * approximately quadrupolar oblateness. A convenient volume-preserving - * affine representative is - * - * delta d(X) = (X, Y, -2 Z). - * - * It moves the equator outward, moves the poles inward, and has zero trace. - * A cusp is not expected until the nonlinear solution approaches mass - * shedding. - */ - { - const double keplerianAngularSpeed = - std::sqrt(mean_field::utils::G * targetMass / - (stellarRadius * stellarRadius * stellarRadius)); - - constexpr double rotationFraction = 0.50; - const double angularSpeed = rotationFraction * keplerianAngularSpeed; - - mfem::Vector angularVelocity(3); - angularVelocity = 0.0; - angularVelocity(2) = angularSpeed; - - mfem::Vector rotationCenter(3); - rotationCenter = 0.0; - - const mean_field::physics::RigidRotation rotation(angularVelocity, - rotationCenter); - - /* - * Return from the perturbed state used by the preceding Jacobian test to - * the spherical equilibrium state, while changing the rotation stream. - */ - stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); - ++dependencies.rotation.revision; - - stellarOperator.Prepare(equilibriumState, dependencies, rotation); - - mfem::Vector rotatingSphericalResidual; - stellarOperator.BuildResidual(rotatingSphericalResidual); - - mfem::ParGridFunction oblateDisplacementField(f.displacementFes.get()); - - mfem::VectorFunctionCoefficient oblateDisplacementCoefficient( - f.mesh->Dimension(), - [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - - /* - * Positive amplitude: - * - * equator: d = (x, y, 0), outward - * pole: d = (0, 0, -2 z), inward - * - * The displacement gradient has trace 1 + 1 - 2 = 0, so this is - * volume preserving to first order. - */ - value(0) = position(0); - value(1) = position(1); - value(2) = -2.0 * position(2); - }); - - oblateDisplacementField = 0.0; - oblateDisplacementField.ProjectCoefficient(oblateDisplacementCoefficient); - - mfem::Vector oblateDisplacement; - oblateDisplacementField.GetTrueDofs(oblateDisplacement); - - mfem::Vector oblateDirection(layout.value_offsets().Last()); - oblateDirection = 0.0; + mfem::Vector equilibriumState(layout.value_offsets().Last()); + equilibriumState = 0.0; stellar_equilibrium_test_utils::assign_value_block( - oblateDirection, layout, - stellar_equilibrium_test_utils::displacementValue, oblateDisplacement); + equilibriumState, layout, stellar_equilibrium_test_utils::densityValue, + stellar_equilibrium_test_utils::reduce_density(f, densityTrue) + ); - const mfem::Vector equilibriumDisplacementResidual = - stellar_equilibrium_test_utils::const_residual_view( - equilibriumResidual, layout, - stellar_equilibrium_test_utils::displacementResidual); + stellar_equilibrium_test_utils::assign_value_block( + equilibriumState, layout, stellar_equilibrium_test_utils::displacementValue, displacementTrue + ); - const mfem::Vector rotatingDisplacementResidual = - stellar_equilibrium_test_utils::const_residual_view( - rotatingSphericalResidual, layout, - stellar_equilibrium_test_utils::displacementResidual); + 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); /* - * Subtract the nonrotating force-balance projection floor. The remainder - * is the displacement residual introduced by rotation. + * Construct a physically safe perturbation direction. Density and + * enthalpy perturbations vanish at the surface because they are + * proportional to the equilibrium profiles. */ - mfem::Vector rotationInducedResidual(rotatingDisplacementResidual); - rotationInducedResidual -= equilibriumDisplacementResidual; + mfem::Vector perturbationDirection(layout.value_offsets().Last()); + perturbationDirection = 0.0; - const double rotationInducedWork = gravity_prepared_test_utils::global_dot( - rotationInducedResidual, oblateDisplacement, f.mesh->GetComm()); + mfem::Vector densityDirection(densityTrue); + densityDirection *= 0.12; - const double rotationInducedNorm = - stellar_equilibrium_test_utils::global_norm(rotationInducedResidual, - f.mesh->GetComm()); + mfem::Vector gravityGradientDirection(gravityGradientTrue); + gravityGradientDirection *= -0.09; - const double oblateDirectionNorm = - stellar_equilibrium_test_utils::global_norm(oblateDisplacement, - f.mesh->GetComm()); + mfem::Vector gravityPotentialDirection(gravityPotentialTrue); + gravityPotentialDirection *= 0.07; - const double workScale = rotationInducedNorm * oblateDirectionNorm; + mfem::Vector enthalpyDirection(enthalpyTrue); + enthalpyDirection *= -0.11; - 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); + const mfem::Vector displacementDirection = stellar_equilibrium_test_utils::project_displacement_direction(f, 0.15); - REQUIRE(std::isfinite(rotationInducedWork)); - REQUIRE(std::isfinite(rotationInducedNorm)); - REQUIRE(std::isfinite(oblateDirectionNorm)); - REQUIRE(rotationInducedNorm > 0.0); - REQUIRE(oblateDirectionNorm > 0.0); - REQUIRE(workScale > 0.0); + 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; /* - * 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. + * Evaluate J delta-x at the equilibrium state before changing the + * prepared base point. */ - CHECK(rotationInducedWork < 0.0); + mfem::Vector jacobianAction; - CHECK(rotationInducedWork < -1.0e-3 * workScale); + 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 auto unconstrainedHydrostaticNorm = [&stellarOperator, &layout, &f](const mfem::Vector &residual) { + const mfem::Vector hydrostaticResidual = stellar_equilibrium_test_utils::const_residual_view( + residual, layout, stellar_equilibrium_test_utils::enthalpyResidual + ); + mfem::Vector volumeHydrostaticResidual(hydrostaticResidual.Size()); + volumeHydrostaticResidual = hydrostaticResidual; + for (const int constrainedRow : + stellarOperator.GetSurfaceConstraintOperator().GetSurfaceRows().reduced_dofs()) { + volumeHydrostaticResidual(constrainedRow) = 0.0; + } + return stellar_equilibrium_test_utils::global_norm(volumeHydrostaticResidual, f.mesh->GetComm()); + }; + + const auto pressureSurfaceNorm = [&stellarOperator, &layout, &f](const mfem::Vector &residual) { + const mfem::Vector hydrostaticResidual = stellar_equilibrium_test_utils::const_residual_view( + residual, layout, stellar_equilibrium_test_utils::enthalpyResidual + ); + double localNormSquared = 0.0; + for (const int constrainedRow : + stellarOperator.GetSurfaceConstraintOperator().GetSurfaceRows().reduced_dofs()) { + localNormSquared += hydrostaticResidual(constrainedRow) * hydrostaticResidual(constrainedRow); + } + double globalNormSquared = 0.0; + MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm()); + return std::sqrt(globalNormSquared); + }; + + 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), + unconstrainedHydrostaticNorm(equilibriumResidual), + 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), + unconstrainedHydrostaticNorm(perturbedResidual), + residualBlockNorm(perturbedResidual, stellar_equilibrium_test_utils::massResidual) + }; + + constexpr std::array rowNames{"gravity-gradient", "Poisson", "closure", + "displacement", "hydrostatic", "mass"}; /* - * Evaluate the displacement column of the complete coupled Jacobian at - * the rotating spherical state. + * 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. */ - mfem::Vector oblateJacobianAction; - stellarOperator.Mult(oblateDirection, oblateJacobianAction); + 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 + }; - const mfem::Vector oblateDisplacementJacobianAction = - stellar_equilibrium_test_utils::const_residual_view( - oblateJacobianAction, layout, - stellar_equilibrium_test_utils::displacementResidual); + for (int row = 0; row < 6; ++row) { + CAPTURE(row); + CAPTURE(rowNames[row]); + CAPTURE(equilibriumRowNorms[row]); + CAPTURE(perturbedRowNorms[row]); + CAPTURE(maximumEquilibriumFractions[row]); - const double residualDirectionalDerivative = - gravity_prepared_test_utils::global_dot( - rotatingDisplacementResidual, oblateDisplacementJacobianAction, - f.mesh->GetComm()); + REQUIRE(std::isfinite(equilibriumRowNorms[row])); + REQUIRE(std::isfinite(perturbedRowNorms[row])); + REQUIRE(perturbedRowNorms[row] > 0.0); - const double jacobianDirectionNormSquared = - gravity_prepared_test_utils::global_dot( - oblateDisplacementJacobianAction, oblateDisplacementJacobianAction, - 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]); - 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); + /* + * 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]); } - ++dependencies.displacement.revision; + /* + * Record an absolute regression bound for the current coarse-mesh + * displacement-force projection floor. + */ + CHECK(equilibriumRowNorms[3] < 1.0e-3); - stellarOperator.Prepare(oblateState, dependencies, rotation); + const double equilibriumPressureSurfaceNorm = pressureSurfaceNorm(equilibriumResidual); + const double perturbedPressureSurfaceNorm = pressureSurfaceNorm(perturbedResidual); + INFO("Equilibrium pressure-surface projection floor = " << equilibriumPressureSurfaceNorm); + INFO("Perturbed pressure-surface residual norm = " << perturbedPressureSurfaceNorm); + CHECK(equilibriumPressureSurfaceNorm < perturbedPressureSurfaceNorm); + CHECK(equilibriumPressureSurfaceNorm < 5.0e-4); - mfem::Vector nonlinearOblateResidual; - stellarOperator.BuildResidual(nonlinearOblateResidual); + const double equilibriumMassError = std::abs( + stellar_equilibrium_test_utils:: + const_residual_view(equilibriumResidual, layout, stellar_equilibrium_test_utils::massResidual)(0) + ); - const double nonlinearOblateDisplacementNorm = - stellar_equilibrium_test_utils::global_norm( + INFO("Equilibrium relative mass error = " << equilibriumMassError / targetMass); + + CHECK(equilibriumMassError < 5.0e-4 * targetMass); + + /* + * The nonlinear residual departure should be + * + * R(x + epsilon p) - R(x) + * = epsilon J(x) p + O(epsilon^2). + */ + mfem::Vector residualDeparture(perturbedResidual); + residualDeparture -= equilibriumResidual; + + mfem::Vector linearizedDeparture(jacobianAction); + linearizedDeparture *= perturbationScale; + + mfem::Vector nonlinearRemainder(residualDeparture); + nonlinearRemainder -= linearizedDeparture; + + const double departureNorm = stellar_equilibrium_test_utils::global_norm(residualDeparture, f.mesh->GetComm()); + + const double nonlinearRemainderNorm = + stellar_equilibrium_test_utils::global_norm(nonlinearRemainder, f.mesh->GetComm()); + + /* + * Apply the known restoring correction -epsilon p through the Jacobian. + * + * This predicts the residual after returning to the equilibrium state: + * + * R(x + epsilon p) - epsilon J(x)p approximately R(x). + */ + mfem::Vector restoredResidualPrediction(perturbedResidual); + restoredResidualPrediction.Add(-perturbationScale, jacobianAction); + + restoredResidualPrediction -= equilibriumResidual; + + const double restoredDistance = + stellar_equilibrium_test_utils::global_norm(restoredResidualPrediction, f.mesh->GetComm()); + + INFO("Residual departure norm = " << departureNorm); + INFO("Nonlinear remainder norm = " << nonlinearRemainderNorm); + INFO("Distance after the restoring Jacobian correction = " << restoredDistance); + INFO("Relative first-order remainder = " << nonlinearRemainderNorm / departureNorm); + + REQUIRE(std::isfinite(departureNorm)); + REQUIRE(std::isfinite(nonlinearRemainderNorm)); + REQUIRE(std::isfinite(restoredDistance)); + REQUIRE(departureNorm > 0.0); + + CHECK(nonlinearRemainderNorm < 5.0e-2 * departureNorm); + + CHECK(restoredDistance < 5.0e-2 * departureNorm); + + /* + * Rotational shape response + * + * At moderate rotation, the leading deformation is a smooth, axisymmetric, + * approximately quadrupolar oblateness. A convenient volume-preserving + * affine representative is + * + * delta d(X) = (X, Y, -2 Z). + * + * It moves the equator outward, moves the poles inward, and has zero trace. + * A cusp is not expected until the nonlinear solution approaches mass + * shedding. + */ + { + const double keplerianAngularSpeed = + std::sqrt(mean_field::utils::G * targetMass / (stellarRadius * stellarRadius * stellarRadius)); + + constexpr double rotationFraction = 0.50; + const double angularSpeed = rotationFraction * keplerianAngularSpeed; + + mfem::Vector angularVelocity(3); + angularVelocity = 0.0; + angularVelocity(2) = angularSpeed; + + mfem::Vector rotationCenter(3); + rotationCenter = 0.0; + + const mean_field::physics::RigidRotation rotation(angularVelocity, rotationCenter); + + /* + * Return from the perturbed state used by the preceding Jacobian test to + * the spherical equilibrium state, while changing the rotation stream. + */ + stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); + ++dependencies.rotation.revision; + + stellarOperator.Prepare(equilibriumState, dependencies, rotation); + + mfem::Vector rotatingSphericalResidual; + stellarOperator.BuildResidual(rotatingSphericalResidual); + + mfem::ParGridFunction oblateDisplacementField(f.displacementFes.get()); + + mfem::VectorFunctionCoefficient oblateDisplacementCoefficient( + f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(3); + + /* + * Positive amplitude: + * + * equator: d = (x, y, 0), outward + * pole: d = (0, 0, -2 z), inward + * + * The displacement gradient has trace 1 + 1 - 2 = 0, so this is + * volume preserving to first order. + */ + value(0) = position(0); + value(1) = position(1); + value(2) = -2.0 * position(2); + } + ); + + oblateDisplacementField = 0.0; + oblateDisplacementField.ProjectCoefficient(oblateDisplacementCoefficient); + + mfem::Vector oblateDisplacement; + oblateDisplacementField.GetTrueDofs(oblateDisplacement); + + mfem::Vector oblateDirection(layout.value_offsets().Last()); + oblateDirection = 0.0; + + stellar_equilibrium_test_utils::assign_value_block( + oblateDirection, layout, stellar_equilibrium_test_utils::displacementValue, oblateDisplacement + ); + + const mfem::Vector equilibriumDisplacementResidual = stellar_equilibrium_test_utils::const_residual_view( + equilibriumResidual, layout, stellar_equilibrium_test_utils::displacementResidual + ); + + const mfem::Vector rotatingDisplacementResidual = stellar_equilibrium_test_utils::const_residual_view( + rotatingSphericalResidual, layout, stellar_equilibrium_test_utils::displacementResidual + ); + + /* + * Subtract the nonrotating force-balance projection floor. The remainder + * is the displacement residual introduced by rotation. + */ + mfem::Vector rotationInducedResidual(rotatingDisplacementResidual); + rotationInducedResidual -= equilibriumDisplacementResidual; + + const double rotationInducedWork = + gravity_prepared_test_utils::global_dot(rotationInducedResidual, oblateDisplacement, f.mesh->GetComm()); + + const double rotationInducedNorm = + stellar_equilibrium_test_utils::global_norm(rotationInducedResidual, f.mesh->GetComm()); + + const double oblateDirectionNorm = + stellar_equilibrium_test_utils::global_norm(oblateDisplacement, f.mesh->GetComm()); + + const double workScale = rotationInducedNorm * oblateDirectionNorm; + + INFO("Keplerian angular speed = " << keplerianAngularSpeed); + INFO("Applied angular speed = " << angularSpeed); + INFO("Rotation fraction = " << rotationFraction); + INFO("Rotation-induced displacement residual norm = " << rotationInducedNorm); + INFO("Rotation-induced work against the oblate direction = " << rotationInducedWork); + INFO("Normalized oblate work = " << rotationInducedWork / workScale); + + REQUIRE(std::isfinite(rotationInducedWork)); + REQUIRE(std::isfinite(rotationInducedNorm)); + REQUIRE(std::isfinite(oblateDirectionNorm)); + REQUIRE(rotationInducedNorm > 0.0); + REQUIRE(oblateDirectionNorm > 0.0); + REQUIRE(workScale > 0.0); + + /* + * The force residual uses the convention R_rot(w) = -integral rho a_c.w. + * Therefore negative work against this direction means that -R, the + * Newton right-hand side, drives a positive oblate deformation. + */ + CHECK(rotationInducedWork < 0.0); + + CHECK(rotationInducedWork < -1.0e-3 * workScale); + + /* + * Evaluate the displacement column of the complete coupled Jacobian at + * the rotating spherical state. + */ + mfem::Vector oblateJacobianAction; + stellarOperator.Mult(oblateDirection, oblateJacobianAction); + + const mfem::Vector oblateDisplacementJacobianAction = stellar_equilibrium_test_utils::const_residual_view( + oblateJacobianAction, layout, stellar_equilibrium_test_utils::displacementResidual + ); + + const double residualDirectionalDerivative = gravity_prepared_test_utils::global_dot( + rotatingDisplacementResidual, oblateDisplacementJacobianAction, f.mesh->GetComm() + ); + + const double jacobianDirectionNormSquared = gravity_prepared_test_utils::global_dot( + oblateDisplacementJacobianAction, oblateDisplacementJacobianAction, f.mesh->GetComm() + ); + + REQUIRE(std::isfinite(residualDirectionalDerivative)); + REQUIRE(std::isfinite(jacobianDirectionNormSquared)); + REQUIRE(jacobianDirectionNormSquared > 0.0); + + /* + * Minimize the linearized displacement-residual norm along the oblate + * direction: + * + * alpha_* = -(R_d, J_d p) / ||J_d p||^2. + * + * A positive alpha_* means that the operator selects equatorial expansion + * and polar contraction rather than the prolate direction. + */ + const double optimalLinearizedAmplitude = -residualDirectionalDerivative / jacobianDirectionNormSquared; + + INFO("Displacement-residual directional derivative = " << residualDirectionalDerivative); + INFO("Optimal linearized oblate amplitude = " << optimalLinearizedAmplitude); + + REQUIRE(std::isfinite(optimalLinearizedAmplitude)); + CHECK(residualDirectionalDerivative < 0.0); + REQUIRE(optimalLinearizedAmplitude > 0.0); + + /* + * Take only a fraction of the predicted step and cap it at a two-percent + * surface deformation. This keeps the test safely inside the local + * linearization regime. + */ + const double appliedOblateAmplitude = std::min(0.25 * optimalLinearizedAmplitude, 2.0e-2); + + REQUIRE(appliedOblateAmplitude > 0.0); + + mfem::Vector predictedDisplacementResidual(rotatingDisplacementResidual); + predictedDisplacementResidual.Add(appliedOblateAmplitude, oblateDisplacementJacobianAction); + + const double rotatingDisplacementNorm = + stellar_equilibrium_test_utils::global_norm(rotatingDisplacementResidual, f.mesh->GetComm()); + + const double predictedDisplacementNorm = + stellar_equilibrium_test_utils::global_norm(predictedDisplacementResidual, f.mesh->GetComm()); + + INFO("Rotating spherical displacement residual norm = " << rotatingDisplacementNorm); + INFO("Predicted oblate displacement residual norm = " << predictedDisplacementNorm); + + CHECK(predictedDisplacementNorm < rotatingDisplacementNorm); + + /* + * Apply the same positive oblate displacement to the nonlinear operator. + * Only the displacement row is compared: a complete rotating equilibrium + * also requires simultaneous changes in rho, g, Phi, h, and C. + */ + mfem::Vector oblateState(equilibriumState); + + { + mfem::Vector displacementBlock = stellar_equilibrium_test_utils::value_view( + oblateState, layout, stellar_equilibrium_test_utils::displacementValue + ); + + displacementBlock.Add(appliedOblateAmplitude, oblateDisplacement); + } + + ++dependencies.displacement.revision; + + stellarOperator.Prepare(oblateState, dependencies, rotation); + + mfem::Vector nonlinearOblateResidual; + stellarOperator.BuildResidual(nonlinearOblateResidual); + + const double nonlinearOblateDisplacementNorm = stellar_equilibrium_test_utils::global_norm( stellar_equilibrium_test_utils::const_residual_view( - nonlinearOblateResidual, layout, - stellar_equilibrium_test_utils::displacementResidual), - f.mesh->GetComm()); + nonlinearOblateResidual, layout, stellar_equilibrium_test_utils::displacementResidual + ), + f.mesh->GetComm() + ); - const double equatorialRadiusScale = 1.0 + appliedOblateAmplitude; + const double equatorialRadiusScale = 1.0 + appliedOblateAmplitude; - const double polarRadiusScale = 1.0 - 2.0 * appliedOblateAmplitude; + const double polarRadiusScale = 1.0 - 2.0 * appliedOblateAmplitude; - const double equatorialToPolarRadiusRatio = - equatorialRadiusScale / polarRadiusScale; + 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); + 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(equatorialRadiusScale > 1.0); + CHECK(polarRadiusScale < 1.0); + CHECK(polarRadiusScale > 0.0); + CHECK(equatorialToPolarRadiusRatio > 1.0); - CHECK(nonlinearOblateDisplacementNorm < rotatingDisplacementNorm); - } + CHECK(nonlinearOblateDisplacementNorm < rotatingDisplacementNorm); + } } diff --git a/tests/physics/barotrope.cpp b/tests/physics/barotrope.cpp index 5ac6632..534c01d 100644 --- a/tests/physics/barotrope.cpp +++ b/tests/physics/barotrope.cpp @@ -8,115 +8,202 @@ import mean_field; import test_helpers; -TEST_CASE("Polytropic EOS Satisfies Its Analytic Identities", - tags::barotrope_eos_unit) { - constexpr double polytropic_index = 3.0; - constexpr double polytropic_constant = 1.5; +TEST_CASE( + "Polytropic EOS Satisfies Its Analytic Identities", + tags::barotrope_eos_unit +) { + using namespace mean_field::eos; - const mean_field::eos::Polytrope barotrope(polytropic_index, - polytropic_constant); + constexpr double polytropic_index = 3.0; + constexpr double polytropic_constant = 1.5; - const std::array densities{1.0e-6, 1.0e-3, 0.1, 0.7, 2.0}; + const Polytrope barotrope(polytropic_index, polytropic_constant); - for (const double density : densities) { - const double pressure = barotrope.pressure_from_density(density); + using densityV = DensityValue; + using pressureV = PressureValue; + using enthalpyV = SpecificEnthalpyValue; - const double enthalpy = barotrope.enthalpy_from_density(density); + constexpr std::array densities{ + densityV{1.0e-6}, densityV{1.0e-3}, densityV{0.1}, densityV{0.7}, densityV{2.0} + }; - const double reconstructed_density = - barotrope.density_from_enthalpy(enthalpy); + for (const densityV density : densities) { + const pressureV pressure = evaluate(barotrope, density); - const double reconstructed_pressure = - barotrope.pressure_from_enthalpy(enthalpy); + const enthalpyV enthalpy = evaluate(barotrope, density); + const densityV reconstructed_density = evaluate(barotrope, enthalpy); - const double reconstructed_enthalpy = - barotrope.enthalpy_from_pressure(pressure); + const pressureV reconstructed_pressure = evaluate(barotrope, enthalpy); - CHECK_THAT(reconstructed_density, - Catch::Matchers::WithinRel(density, 2.0e-14)); + const enthalpyV reconstructed_enthalpy = evaluate(barotrope, pressure); - CHECK_THAT(reconstructed_pressure, - Catch::Matchers::WithinRel(pressure, 2.0e-14)); + CHECK_THAT(reconstructed_density.value(), Catch::Matchers::WithinRel(density.value(), 2.0e-14)); - CHECK_THAT(reconstructed_enthalpy, - Catch::Matchers::WithinRel(enthalpy, 2.0e-14)); + CHECK_THAT(reconstructed_pressure.value(), Catch::Matchers::WithinRel(pressure.value(), 2.0e-14)); - CHECK_THAT(pressure, - Catch::Matchers::WithinRel( - density * enthalpy / (polytropic_index + 1.0), 2.0e-14)); + CHECK_THAT(reconstructed_enthalpy.value(), Catch::Matchers::WithinRel(enthalpy.value(), 2.0e-14)); - CHECK_THAT(barotrope.pressure_derivative_from_enthalpy(enthalpy), - Catch::Matchers::WithinRel(density, 2.0e-14)); + CHECK_THAT( + pressure.value(), + Catch::Matchers::WithinRel(density.value() * enthalpy.value() / (polytropic_index + 1.0), 2.0e-14) + ); - CHECK_THAT( - barotrope.pressure_derivative_from_density(density), - Catch::Matchers::WithinRel(enthalpy / polytropic_index, 2.0e-14)); - } + CHECK_THAT( + (mean_field::eos::partialDerivative< + mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>( + barotrope, mean_field::eos::SpecificEnthalpyValue{enthalpy} + ) + .value()), + Catch::Matchers::WithinRel(density.value(), 2.0e-14) + ); + + CHECK_THAT( + (mean_field::eos::partialDerivative< + mean_field::eos::quantity::Pressure, mean_field::eos::quantity::Density>( + barotrope, mean_field::eos::DensityValue{density} + ) + .value()), + Catch::Matchers::WithinRel(enthalpy.value() / polytropic_index, 2.0e-14) + ); + } } -TEST_CASE("Polytropic EOS Derivatives Match Centered Differences", - tags::barotrope_eos_jacobian) { - const mean_field::eos::Polytrope barotrope(3.0, 1.5); +TEST_CASE( + "Polytropic EOS Derivatives Match Centered Differences", + tags::barotrope_eos_jacobian +) { + using namespace mean_field::eos; - const std::array enthalpies{0.05, 0.2, 0.7, 1.4}; + const Polytrope barotrope(3.0, 1.5); - for (const double enthalpy : enthalpies) { - const double step = 1.0e-6 * std::max(1.0, enthalpy); + using densityV = DensityValue; + using pressureV = PressureValue; + using enthalpyV = SpecificEnthalpyValue; - const double density_difference = - (barotrope.density_from_enthalpy(enthalpy + step) - - barotrope.density_from_enthalpy(enthalpy - step)) / - (2.0 * step); + constexpr std::array enthalpies{enthalpyV{0.05}, enthalpyV{0.2}, enthalpyV{0.7}, enthalpyV{1.4}}; - const double pressure_difference = - (barotrope.pressure_from_enthalpy(enthalpy + step) - - barotrope.pressure_from_enthalpy(enthalpy - step)) / - (2.0 * step); + for (const enthalpyV enthalpy : enthalpies) { + const enthalpyV step = enthalpyV{1.0e-6} * std::max(1.0, enthalpy.value()); - CHECK_THAT( - density_difference, - Catch::Matchers::WithinRel( - barotrope.density_derivative_from_enthalpy(enthalpy), 5.0e-10)); + const densityV density_difference = (evaluate(barotrope, enthalpy + step) - + evaluate(barotrope, enthalpy - step)) / + (2.0 * step.value()); - CHECK_THAT( - pressure_difference, - Catch::Matchers::WithinRel( - barotrope.pressure_derivative_from_enthalpy(enthalpy), 5.0e-10)); - } + const pressureV pressure_difference = (evaluate(barotrope, enthalpy + step) - + evaluate(barotrope, enthalpy - step)) / + (2.0 * step.value()); + + CHECK_THAT( + density_difference.value(), + Catch::Matchers::WithinRel( + mean_field::eos::partialDerivative< + mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>( + barotrope, enthalpy + ) + .value(), + 5.0e-10 + ) + ); + + CHECK_THAT( + pressure_difference.value(), + Catch::Matchers::WithinRel( + mean_field::eos::partialDerivative< + mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>( + barotrope, enthalpy + ) + .value(), + 5.0e-10 + ) + ); + } } -TEST_CASE("Polytropic EOS Has An Exact Zero Density Surface", - tags::barotrope_eos_unit) { - const mean_field::eos::Polytrope 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( + mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0} + ) + .value() == 0.0 + ); + CHECK( + mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{0.0} + ) + .value() == 0.0 + ); - CHECK(barotrope.pressure_from_enthalpy(-1.0) == 0.0); - CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0); + CHECK( + mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0} + ) + .value() == 0.0 + ); + CHECK( + mean_field::eos::evaluate( + barotrope, mean_field::eos::SpecificEnthalpyValue{0.0} + ) + .value() == 0.0 + ); - CHECK(barotrope.density_derivative_from_enthalpy(-1.0) == 0.0); + CHECK( + (mean_field::eos::partialDerivative< + mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>( + barotrope, mean_field::eos::SpecificEnthalpyValue{-1.0} + ) + .value() == 0.0) + ); - CHECK(barotrope.density_derivative_from_enthalpy(0.0) == 0.0); + CHECK( + (mean_field::eos::partialDerivative< + mean_field::eos::quantity::Density, mean_field::eos::quantity::SpecificEnthalpy>( + barotrope, mean_field::eos::SpecificEnthalpyValue{0.0} + ) + .value() == 0.0) + ); - CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0); + CHECK( + (mean_field::eos::partialDerivative< + mean_field::eos::quantity::Pressure, mean_field::eos::quantity::SpecificEnthalpy>( + barotrope, mean_field::eos::SpecificEnthalpyValue{0.0} + ) + .value() == 0.0) + ); } -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); +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::eos::Polytrope(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::eos::Polytrope(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::eos::Polytrope 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( + mean_field::eos::evaluate(barotrope, mean_field::eos::DensityValue{-1.0}), + std::domain_error + ); - CHECK_THROWS_AS(barotrope.enthalpy_from_density(-1.0), std::domain_error); + CHECK_THROWS_AS( + mean_field::eos::evaluate( + barotrope, mean_field::eos::DensityValue{-1.0} + ), + std::domain_error + ); - CHECK_THROWS_AS(barotrope.enthalpy_from_pressure(-1.0), std::domain_error); + CHECK_THROWS_AS( + mean_field::eos::evaluate( + barotrope, mean_field::eos::PressureValue{-1.0} + ), + std::domain_error + ); } diff --git a/tests/physics/barotrope_pressure.cpp b/tests/physics/barotrope_pressure.cpp index 7edfcf4..f2cb7b8 100644 --- a/tests/physics/barotrope_pressure.cpp +++ b/tests/physics/barotrope_pressure.cpp @@ -13,561 +13,603 @@ import mean_field; import test_helpers; +namespace eos = mean_field::eos; + 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); -} + 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; + 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); + for (int pointIndex = 0; pointIndex < integrationRule.GetNPoints(); ++pointIndex) { + const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(pointIndex); - integral += integrationPoint.weight * integrand(integrationPoint); - } + integral += integrationPoint.weight * integrand(integrationPoint); + } - return integral; -} + return integral; + } } // namespace polytropic_eos_test_utils -TEST_CASE("Polytropic EOS Satisfies Its Thermodynamic Identities", - tags::barotrope_eos_unit) { - constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; +TEST_CASE( + "Polytropic EOS Satisfies Its Thermodynamic Identities", + tags::barotrope_eos_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 std::array densities{1.0e-4, 0.02, 0.37, 2.4}; - constexpr double polytropicConstant = 0.73; + constexpr double polytropicConstant = 0.73; - for (const double polytropicIndex : polytropicIndices) { - DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { - const mean_field::eos::Polytrope barotrope(polytropicIndex, - polytropicConstant); + for (const double polytropicIndex : polytropicIndices) { + DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { + const mean_field::eos::Polytrope barotrope(polytropicIndex, polytropicConstant); - const double expectedEnthalpyScale = - (polytropicIndex + 1.0) * polytropicConstant; + const double expectedEnthalpyScale = (polytropicIndex + 1.0) * polytropicConstant; - CHECK(barotrope.polytropic_index() == polytropicIndex); + CHECK(barotrope.polytropic_index() == polytropicIndex); - CHECK(barotrope.polytropic_constant() == polytropicConstant); + CHECK(barotrope.polytropic_constant() == polytropicConstant); - CHECK(barotrope.enthalpy_scale() == expectedEnthalpyScale); + CHECK(barotrope.enthalpy_scale() == expectedEnthalpyScale); - for (const double density : densities) { - CAPTURE(polytropicIndex, polytropicConstant, density); + for (const double density : densities) { + CAPTURE(polytropicIndex, polytropicConstant, density); - const double expectedPressure = - polytropicConstant * std::pow(density, 1.0 + 1.0 / polytropicIndex); + const double expectedPressure = polytropicConstant * std::pow(density, 1.0 + 1.0 / polytropicIndex); - const double expectedEnthalpy = - expectedEnthalpyScale * std::pow(density, 1.0 / polytropicIndex); + const double expectedEnthalpy = expectedEnthalpyScale * std::pow(density, 1.0 / polytropicIndex); - const double pressureFromDensity = - barotrope.pressure_from_density(density); + const double pressureFromDensity = + eos::evaluate(barotrope, eos::DensityValue{density}).value(); - const double enthalpyFromDensity = - barotrope.enthalpy_from_density(density); + const double enthalpyFromDensity = + eos::evaluate(barotrope, eos::DensityValue{density}).value(); - const double recoveredDensity = - barotrope.density_from_enthalpy(enthalpyFromDensity); + const double recoveredDensity = + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpyFromDensity}) + .value(); - const double pressureFromEnthalpy = - barotrope.pressure_from_enthalpy(enthalpyFromDensity); + const double pressureFromEnthalpy = + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpyFromDensity}) + .value(); - CHECK_THAT(pressureFromDensity, - Catch::Matchers::WithinRel(expectedPressure, 2.0e-13)); + CHECK_THAT(pressureFromDensity, Catch::Matchers::WithinRel(expectedPressure, 2.0e-13)); - CHECK_THAT(enthalpyFromDensity, - Catch::Matchers::WithinRel(expectedEnthalpy, 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(recoveredDensity, Catch::Matchers::WithinRel(density, 5.0e-13)); - CHECK_THAT(pressureFromEnthalpy, - Catch::Matchers::WithinRel(expectedPressure, 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: + * + * 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)); + /* + * Polytropic identity: + * + * dP / dh = rho. + * + * The implementation should return the same + * value as the density from specific enthalpy relation. + */ + CHECK( + (eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{enthalpyFromDensity} + ) + .value() == recoveredDensity) + ); - /* - * 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)); - } + /* + * Since + * + * h = (n + 1) K rho^(1/n), + * + * it follows that + * + * dP / d rho = h / n. + */ + CHECK_THAT( + (eos::partialDerivative( + barotrope, eos::DensityValue{density} + ) + .value()), + Catch::Matchers::WithinRel(enthalpyFromDensity / polytropicIndex, 5.0e-13) + ); + } + } } - } } -TEST_CASE("Polytropic EOS Pressure Derivatives Match Centered Differences", - tags::barotrope_eos_jacobian) { - 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 positiveValues{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::eos::Polytrope 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 : positiveValues) { - 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_eos_test_utils::centered_derivative( - [&barotrope](const double perturbedEnthalpy) { - return barotrope.pressure_from_enthalpy(perturbedEnthalpy); - }, - enthalpy, step); + const double numericalDerivative = polytropic_eos_test_utils::centered_derivative( + [&barotrope](const double perturbedEnthalpy) { + return eos::evaluate( + barotrope, eos::SpecificEnthalpyValue{perturbedEnthalpy} + ) + .value(); + }, + enthalpy, step + ); - const double analyticDerivative = - barotrope.pressure_derivative_from_enthalpy(enthalpy); + const double analyticDerivative = + eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{enthalpy} + ) + .value(); - 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)); + 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 numericalDerivative = polytropic_eos_test_utils::centered_derivative( + [&barotrope](const double perturbedDensity) { + return eos::evaluate(barotrope, eos::DensityValue{perturbedDensity}) + .value(); + }, + density, step + ); - const double analyticDerivative = - barotrope.pressure_derivative_from_density(density); + const double analyticDerivative = + eos::partialDerivative( + barotrope, eos::DensityValue{density} + ) + .value(); - CAPTURE(polytropicIndex, density, step, numericalDerivative, - analyticDerivative); + CAPTURE(polytropicIndex, density, step, numericalDerivative, analyticDerivative); - CHECK_THAT(numericalDerivative, - Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); - } + CHECK_THAT(numericalDerivative, Catch::Matchers::WithinRel(analyticDerivative, 5.0e-8)); + } + } } - } } -TEST_CASE("Polytropic EOS Density Derivative Matches Centered Differences", - tags::barotrope_eos_jacobian) { - 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 std::array enthalpies{0.2, 0.73, 1.8}; + constexpr std::array enthalpies{0.2, 0.73, 1.8}; - constexpr double polytropicConstant = 0.61; + constexpr double polytropicConstant = 0.61; - for (const double polytropicIndex : polytropicIndices) { - const mean_field::eos::Polytrope 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 : enthalpies) { + const double step = 2.0e-6 * std::max(1.0, std::abs(enthalpy)); - const double numericalDerivative = - polytropic_eos_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 eos::evaluate( + barotrope, eos::SpecificEnthalpyValue{perturbedEnthalpy} + ) + .value(); + }, + enthalpy, step + ); - const double analyticDerivative = - barotrope.density_derivative_from_enthalpy(enthalpy); + const double analyticDerivative = + eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{enthalpy} + ) + .value(); - 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)); + } + } } - } } -TEST_CASE("Polytropic EOS Defines Consistent Surface And Exterior Behavior", - tags::barotrope_eos_unit) { - constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; +TEST_CASE( + "Polytropic EOS Defines Consistent Surface And Exterior Behavior", + tags::barotrope_eos_unit +) { + constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; - constexpr double polytropicConstant = 0.47; - constexpr double exteriorEnthalpy = -0.3; + constexpr double polytropicConstant = 0.47; + constexpr double exteriorEnthalpy = -0.3; - for (const double polytropicIndex : polytropicIndices) { - const mean_field::eos::Polytrope barotrope(polytropicIndex, - polytropicConstant); + for (const double polytropicIndex : polytropicIndices) { + const mean_field::eos::Polytrope barotrope(polytropicIndex, polytropicConstant); - DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { - /* - * Exact surface values. - */ - CHECK(barotrope.density_from_enthalpy(0.0) == 0.0); + DYNAMIC_SECTION("polytropic index n = " << polytropicIndex) { + /* + * Exact surface values. + */ + CHECK(eos::evaluate(barotrope, eos::SpecificEnthalpyValue{0.0}).value() == 0.0); - CHECK(barotrope.pressure_from_enthalpy(0.0) == 0.0); + CHECK(eos::evaluate(barotrope, eos::SpecificEnthalpyValue{0.0}).value() == 0.0); - CHECK(barotrope.pressure_derivative_from_enthalpy(0.0) == 0.0); + CHECK( + (eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{0.0} + ) + .value() == 0.0) + ); - CHECK(barotrope.pressure_from_density(0.0) == 0.0); + CHECK(eos::evaluate(barotrope, eos::DensityValue{0.0}).value() == 0.0); - CHECK(barotrope.enthalpy_from_density(0.0) == 0.0); + CHECK(eos::evaluate(barotrope, eos::DensityValue{0.0}).value() == 0.0); - CHECK(barotrope.pressure_derivative_from_density(0.0) == 0.0); + CHECK( + (eos::partialDerivative( + barotrope, eos::DensityValue{0.0} + ) + .value() == 0.0) + ); - /* - * Positive-part extension into h < 0. - */ - CHECK(barotrope.density_from_enthalpy(exteriorEnthalpy) == 0.0); + /* + * Positive-part extension into h < 0. + */ + CHECK( + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{exteriorEnthalpy}) + .value() == 0.0 + ); - CHECK(barotrope.pressure_from_enthalpy(exteriorEnthalpy) == 0.0); + CHECK( + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{exteriorEnthalpy}) + .value() == 0.0 + ); - CHECK(barotrope.density_derivative_from_enthalpy(exteriorEnthalpy) == - 0.0); + CHECK( + (eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{exteriorEnthalpy} + ) + .value() == 0.0) + ); - CHECK(barotrope.pressure_derivative_from_enthalpy(exteriorEnthalpy) == - 0.0); + CHECK( + (eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{exteriorEnthalpy} + ) + .value() == 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; + /* + * 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( + (eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{0.0} + ) + .value() == expectedSurfaceDensityDerivative) + ); + } } - } } -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); +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); - CHECK_THROWS_AS( - mean_field::eos::Polytrope(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); - CHECK_THROWS_AS(mean_field::eos::Polytrope(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::eos::Polytrope(3.0, -1.0), std::invalid_argument); + CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, -1.0), std::invalid_argument); - const mean_field::eos::Polytrope barotrope(3.0, 0.75); + const mean_field::eos::Polytrope barotrope(3.0, 0.75); - CHECK_THROWS_AS(barotrope.pressure_from_density(-0.1), std::domain_error); + CHECK_THROWS_AS(eos::evaluate(barotrope, eos::DensityValue{-0.1}), std::domain_error); - CHECK_THROWS_AS(barotrope.enthalpy_from_density(-0.1), std::domain_error); + CHECK_THROWS_AS( + eos::evaluate(barotrope, eos::DensityValue{-0.1}), std::domain_error + ); - CHECK_THROWS_AS(barotrope.pressure_derivative_from_density(-0.1), - std::domain_error); + CHECK_THROWS_AS( + (eos::partialDerivative(barotrope, eos::DensityValue{-0.1})), + std::domain_error + ); - constexpr std::array nonfiniteValues{ - std::numeric_limits::infinity(), - -std::numeric_limits::infinity(), - std::numeric_limits::quiet_NaN()}; + constexpr std::array nonfiniteValues{ + std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN() + }; - for (const double nonfiniteValue : nonfiniteValues) { - CAPTURE(nonfiniteValue); + for (const double nonfiniteValue : nonfiniteValues) { + CAPTURE(nonfiniteValue); - CHECK_THROWS_AS(barotrope.density_from_enthalpy(nonfiniteValue), - std::domain_error); + CHECK_THROWS_AS( + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{nonfiniteValue}), + std::domain_error + ); - CHECK_THROWS_AS(barotrope.pressure_from_enthalpy(nonfiniteValue), - std::domain_error); + CHECK_THROWS_AS( + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{nonfiniteValue}), + std::domain_error + ); - CHECK_THROWS_AS(barotrope.density_derivative_from_enthalpy(nonfiniteValue), - std::domain_error); + CHECK_THROWS_AS( + (eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{nonfiniteValue} + )), + std::domain_error + ); - CHECK_THROWS_AS(barotrope.pressure_derivative_from_enthalpy(nonfiniteValue), - std::domain_error); - } + CHECK_THROWS_AS( + (eos::partialDerivative( + barotrope, eos::SpecificEnthalpyValue{nonfiniteValue} + )), + std::domain_error + ); + } } -TEST_CASE("Pressure Force And Pressure Integral Have Distinct Registered Forms", - tags::barotrope_pressure_quadrature_unit) { - 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; - /* - * 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; + /* + * 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; - constexpr int geometryWeightOrder = 2; + constexpr int geometryWeightOrder = 2; - 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 pressureIntegralQuery = + EnthalpyField::make_query( + 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::field::Enthalpy::Form::PressureForce>( - mean_field::quadrature::QuadratureRole::discretization, - 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, geometryWeightOrder, + std::array{pressureExtraOrder}, mean_field::utils::DOMAINS::STELLAR, + mean_field::quadrature::MappingKind::general + ); - STATIC_CHECK( - mean_field::field::Enthalpy::Form::PressureIntegral::dynamicOrderCount == - 1); + STATIC_CHECK(mean_field::field::Enthalpy::Form::PressureIntegral::dynamicOrderCount == 1); - STATIC_CHECK( - mean_field::field::Enthalpy::Form::PressureForce::dynamicOrderCount == 1); + STATIC_CHECK(mean_field::field::Enthalpy::Form::PressureForce::dynamicOrderCount == 1); - STATIC_CHECK(mean_field::field::Enthalpy::Form::PressureIntegral::policyKey != - mean_field::field::Enthalpy::Form::PressureForce::policyKey); + STATIC_CHECK( + mean_field::field::Enthalpy::Form::PressureIntegral::policyKey != + mean_field::field::Enthalpy::Form::PressureForce::policyKey + ); - REQUIRE(pressureIntegralQuery.base_order.has_value()); + REQUIRE(pressureIntegralQuery.base_order.has_value()); - REQUIRE(pressureForceQuery.base_order.has_value()); + REQUIRE(pressureForceQuery.base_order.has_value()); - /* - * Pressure integral: - * - * degree(P) + degree(J) - * = 12 + 2 - * = 14. - */ - CHECK(*pressureIntegralQuery.base_order == 14); + /* + * 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); + /* + * 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(pressureIntegralQuery.term == mean_field::quadrature::Term::pressure_integral); - CHECK(pressureForceQuery.term == - mean_field::quadrature::Term::pressure_force); + CHECK(pressureForceQuery.term == mean_field::quadrature::Term::pressure_force); - CHECK(pressureIntegralQuery.role == - mean_field::quadrature::QuadratureRole::diagnostic); + CHECK(pressureIntegralQuery.role == mean_field::quadrature::QuadratureRole::diagnostic); - CHECK(pressureForceQuery.role == - mean_field::quadrature::QuadratureRole::discretization); + CHECK(pressureForceQuery.role == mean_field::quadrature::QuadratureRole::discretization); - CHECK(pressureIntegralQuery.domain == mean_field::utils::DOMAINS::STELLAR); + CHECK(pressureIntegralQuery.domain == mean_field::utils::DOMAINS::STELLAR); - CHECK(pressureForceQuery.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); + /* + * 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; + ruleSet.pressure_integral.boost = 3; + ruleSet.pressure_force.boost = 5; - const mean_field::quadrature::Policy policy(std::move(ruleSet)); + const mean_field::quadrature::Policy policy(std::move(ruleSet)); - const mean_field::quadrature::Resolution pressureIntegralResolution = - policy.resolve(pressureIntegralQuery); + const mean_field::quadrature::Resolution pressureIntegralResolution = policy.resolve(pressureIntegralQuery); - const mean_field::quadrature::Resolution pressureForceResolution = - policy.resolve(pressureForceQuery); + const mean_field::quadrature::Resolution pressureForceResolution = policy.resolve(pressureForceQuery); - CHECK(pressureIntegralResolution.base_order == 14); + CHECK(pressureIntegralResolution.base_order == 14); - CHECK(pressureIntegralResolution.boost == 3); + CHECK(pressureIntegralResolution.boost == 3); - CHECK(pressureIntegralResolution.order == 17); + CHECK(pressureIntegralResolution.order == 17); - CHECK(pressureForceResolution.base_order == 16); + CHECK(pressureForceResolution.base_order == 16); - CHECK(pressureForceResolution.boost == 5); + CHECK(pressureForceResolution.boost == 5); - CHECK(pressureForceResolution.order == 21); + 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; +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 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::eos::Polytrope barotrope(3.0, 0.25); + /* + * 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 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 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); + 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 + ); - 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::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 pressureIntegralRule = + ruleFactory.get(pressureIntegralQuery, mfem::Geometry::CUBE); - const mean_field::quadrature::MfemRule pressureForceRule = - ruleFactory.get(pressureForceQuery, 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; + /* + * 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); + const double enthalpy = std::pow(coordinateProduct, 3.0); - return barotrope.pressure_from_enthalpy(enthalpy); - }); + return eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpy}).value(); + } + ); - 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_eos_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 = + eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpy}).value(); - 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)); + CHECK_THAT(numericalPressureForceIntegral, Catch::Matchers::WithinAbs(analyticPressureForceIntegral, 5.0e-14)); } diff --git a/tests/physics/equation_of_state_consumer_contracts.cpp b/tests/physics/equation_of_state_consumer_contracts.cpp new file mode 100644 index 0000000..dd98c06 --- /dev/null +++ b/tests/physics/equation_of_state_consumer_contracts.cpp @@ -0,0 +1,125 @@ +#include + +import mean_field; +import test_helpers; + +namespace { + namespace eos = mean_field::eos; + + class DensityClosureEquationOfState final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::DensityValue evaluate( + eos::DensityFromSpecificEnthalpy, + const eos::SpecificEnthalpyValue specificEnthalpy + ) const noexcept { + return eos::DensityValue{specificEnthalpy.value()}; + } + + [[nodiscard]] constexpr eos::PartialDerivative< + eos::quantity::Density, + eos::quantity::SpecificEnthalpy> + partialDerivative( + eos::DensityFromSpecificEnthalpy, + eos::WithRespectTo, + eos::SpecificEnthalpyValue + ) const noexcept { + return eos::PartialDerivative{1.0}; + } + }; + + class DensityClosureWithoutDerivative final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::DensityValue evaluate( + eos::DensityFromSpecificEnthalpy, + const eos::SpecificEnthalpyValue specificEnthalpy + ) const noexcept { + return eos::DensityValue{specificEnthalpy.value()}; + } + }; + + class EnthalpyPressureEquationOfState final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::PressureValue evaluate( + eos::PressureFromSpecificEnthalpy, + const eos::SpecificEnthalpyValue specificEnthalpy + ) const noexcept { + return eos::PressureValue{2.0 * specificEnthalpy.value()}; + } + + [[nodiscard]] constexpr eos::PartialDerivative< + eos::quantity::Pressure, + eos::quantity::SpecificEnthalpy> + partialDerivative( + eos::PressureFromSpecificEnthalpy, + eos::WithRespectTo, + eos::SpecificEnthalpyValue + ) const noexcept { + return eos::PartialDerivative{2.0}; + } + }; + + class DensitySeedEquationOfState final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + eos::SpecificEnthalpyFromDensity, + const eos::DensityValue density + ) const noexcept { + return eos::SpecificEnthalpyValue{3.0 * density.value()}; + } + }; + + class GeneralEquationOfStateWithoutCurrentConsumerRelations final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + eos::SpecificEnthalpyFromPressure, + const eos::PressureValue pressure + ) const noexcept { + return eos::SpecificEnthalpyValue{pressure.value()}; + } + }; +} // namespace + +TEST_CASE( + "Barotropic Closure EOS Requires Density And Its Enthalpy Derivative", + tags::barotropic_closure_equation_of_state_contract +) { + STATIC_CHECK(eos::BarotropicClosureEquationOfState); + STATIC_CHECK(eos::BarotropicClosureEquationOfState); + STATIC_CHECK(eos::EquationOfStateModel); + STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState); + STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState); +} + +TEST_CASE( + "Pressure Force EOS Requires Pressure And Its Enthalpy Derivative", + tags::pressure_force_equation_of_state_contract +) { + STATIC_CHECK(eos::PressureForceEquationOfState); + STATIC_CHECK(eos::PressureForceEquationOfState); + STATIC_CHECK_FALSE(eos::PressureForceEquationOfState); + STATIC_CHECK_FALSE(eos::PressureForceEquationOfState); +} + +TEST_CASE( + "Structure Seed EOS Requires Enthalpy From Density", + tags::structure_seed_equation_of_state_contract +) { + STATIC_CHECK(eos::StructureSeedEquationOfState); + STATIC_CHECK(eos::StructureSeedEquationOfState); + STATIC_CHECK_FALSE(eos::StructureSeedEquationOfState); + + STATIC_CHECK(eos::EquationOfStateModel); + STATIC_CHECK_FALSE(eos::StructureSeedEquationOfState); + STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState); + STATIC_CHECK_FALSE(eos::PressureForceEquationOfState); +} diff --git a/tests/physics/equation_of_state_runtime_view.cpp b/tests/physics/equation_of_state_runtime_view.cpp new file mode 100644 index 0000000..418d1a8 --- /dev/null +++ b/tests/physics/equation_of_state_runtime_view.cpp @@ -0,0 +1,322 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +import mean_field; +import test_helpers; + +namespace { + namespace eos = mean_field::eos; + + class LinearPressureEquationOfState final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::PressureValue evaluate( + eos::PressureFromDensity, + const eos::DensityValue density + ) const noexcept { + return eos::PressureValue{2.0 * density.value() + 0.5}; + } + }; + + struct DensityAlias final : eos::ThermodynamicQuantity { + static constexpr std::string_view identifier = "density"; + }; + + class AmbiguouslyIdentifiedEquationOfState final { + public: + using Relations = eos::RelationCatalog>; + + [[nodiscard]] constexpr eos::QuantityValue evaluate( + eos::Relation< + DensityAlias, + eos::quantity::Density>, + const eos::DensityValue density + ) const noexcept { + return eos::QuantityValue{density.value()}; + } + }; + + [[nodiscard]] std::expected< + eos::PressureValue, + eos::EvaluationError> + pressureAtDensity( + const eos::EquationOfStateView equationOfState, + const eos::DensityValue density + ) { + return equationOfState.tryEvaluate(density); + } + + [[nodiscard]] const eos::RuntimeRelationDescriptor *findRelation( + const eos::EquationOfStateView equationOfState, + const eos::ThermodynamicQuantityId output, + const eos::ThermodynamicQuantityId input + ) { + for (const eos::RuntimeRelationDescriptor &relation : equationOfState.relations()) { + if (relation.outputQuantity == output && relation.inputQuantities.size() == 1 && + relation.inputQuantities[0] == input) { + return std::addressof(relation); + } + } + + return nullptr; + } +} // namespace + +TEST_CASE( + "Runtime EOS View Generates The Polytropic Relation Catalog", + tags::equation_of_state_runtime_contract +) { + STATIC_CHECK(eos::RuntimeEquationOfStateModel); + STATIC_CHECK(eos::RuntimeEquationOfStateModel); + STATIC_CHECK(eos::EquationOfStateModel); + STATIC_CHECK_FALSE(eos::RuntimeEquationOfStateModel); + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK_FALSE(std::constructible_from); + + const eos::Polytrope equationOfState(3.0, 0.25); + const eos::Polytrope secondEquationOfState(1.5, 0.73); + + const eos::EquationOfStateView view{equationOfState}; + const eos::EquationOfStateView secondView{secondEquationOfState}; + + REQUIRE(view.relations().size() == eos::Polytrope::Relations::size); + CHECK(view.relations().data() == secondView.relations().data()); + + CHECK(eos::thermodynamicQuantityId.name() == "density"); + CHECK(eos::thermodynamicQuantityId.name() == "pressure"); + CHECK(eos::thermodynamicQuantityId.name() == "specific_enthalpy"); + + const eos::RuntimeRelationDescriptor *pressureFromDensity = findRelation( + view, eos::thermodynamicQuantityId, + eos::thermodynamicQuantityId + ); + + REQUIRE(pressureFromDensity != nullptr); + CHECK(pressureFromDensity->hasPartialDerivative(0)); + + const eos::RuntimeRelationDescriptor *specificEnthalpyFromPressure = findRelation( + view, eos::thermodynamicQuantityId, + eos::thermodynamicQuantityId + ); + + REQUIRE(specificEnthalpyFromPressure != nullptr); + CHECK_FALSE(specificEnthalpyFromPressure->hasPartialDerivative(0)); + + const eos::RuntimeRelationDescriptor *pressureFromSpecificEnthalpy = findRelation( + view, eos::thermodynamicQuantityId, + eos::thermodynamicQuantityId + ); + + REQUIRE(pressureFromSpecificEnthalpy != nullptr); + CHECK(pressureFromSpecificEnthalpy->hasPartialDerivative(0)); + + const eos::RuntimeRelationDescriptor *specificEnthalpyFromDensity = findRelation( + view, eos::thermodynamicQuantityId, + eos::thermodynamicQuantityId + ); + + REQUIRE(specificEnthalpyFromDensity != nullptr); + CHECK_FALSE(specificEnthalpyFromDensity->hasPartialDerivative(0)); + + const eos::RuntimeRelationDescriptor *densityFromSpecificEnthalpy = findRelation( + view, eos::thermodynamicQuantityId, + eos::thermodynamicQuantityId + ); + + REQUIRE(densityFromSpecificEnthalpy != nullptr); + CHECK(densityFromSpecificEnthalpy->hasPartialDerivative(0)); +} + +TEST_CASE( + "Runtime EOS View Matches Typed Polytropic Evaluation", + tags::equation_of_state_runtime_compatibility +) { + const eos::Polytrope equationOfState(3.0, 0.25); + const eos::EquationOfStateView view{equationOfState}; + + const eos::DensityValue density{0.7}; + const eos::SpecificEnthalpyValue specificEnthalpy{0.9}; + const eos::PressureValue pressure{0.04}; + + const auto runtimePressureFromDensity = view.tryEvaluate(density); + const auto runtimePressureFromSpecificEnthalpy = view.tryEvaluate(specificEnthalpy); + const auto runtimeSpecificEnthalpyFromDensity = view.tryEvaluate(density); + const auto runtimeSpecificEnthalpyFromPressure = view.tryEvaluate(pressure); + const auto runtimeDensityFromSpecificEnthalpy = view.tryEvaluate(specificEnthalpy); + + REQUIRE(runtimePressureFromDensity.has_value()); + REQUIRE(runtimePressureFromSpecificEnthalpy.has_value()); + REQUIRE(runtimeSpecificEnthalpyFromDensity.has_value()); + REQUIRE(runtimeSpecificEnthalpyFromPressure.has_value()); + REQUIRE(runtimeDensityFromSpecificEnthalpy.has_value()); + + CHECK( + runtimePressureFromDensity->value() == eos::evaluate(equationOfState, density).value() + ); + CHECK( + runtimePressureFromSpecificEnthalpy->value() == + eos::evaluate(equationOfState, specificEnthalpy).value() + ); + CHECK( + runtimeSpecificEnthalpyFromDensity->value() == + eos::evaluate(equationOfState, density).value() + ); + CHECK( + runtimeSpecificEnthalpyFromPressure->value() == + eos::evaluate(equationOfState, pressure).value() + ); + CHECK( + runtimeDensityFromSpecificEnthalpy->value() == + eos::evaluate(equationOfState, specificEnthalpy).value() + ); + + const std::array runtimeDensityInput{ + eos::RuntimeQuantityValue{eos::thermodynamicQuantityId, density.value()} + }; + + const auto erasedPressureFromDensity = view.tryEvaluate( + eos::thermodynamicQuantityId, + std::span{runtimeDensityInput} + ); + + REQUIRE(erasedPressureFromDensity.has_value()); + CHECK(erasedPressureFromDensity->quantity == eos::thermodynamicQuantityId); + CHECK(erasedPressureFromDensity->value == runtimePressureFromDensity->value()); + + const auto runtimePressureDerivative = + view.tryPartialDerivative(specificEnthalpy); + + const auto runtimeDensityDerivative = + view.tryPartialDerivative(specificEnthalpy); + + const auto runtimePressureDensityDerivative = + view.tryPartialDerivative(density); + + REQUIRE(runtimePressureDerivative.has_value()); + REQUIRE(runtimeDensityDerivative.has_value()); + REQUIRE(runtimePressureDensityDerivative.has_value()); + + CHECK( + runtimePressureDerivative->value() == + eos::partialDerivative( + equationOfState, specificEnthalpy + ) + .value() + ); + CHECK( + runtimeDensityDerivative->value() == + eos::partialDerivative( + equationOfState, specificEnthalpy + ) + .value() + ); + CHECK( + runtimePressureDensityDerivative->value() == + eos::partialDerivative(equationOfState, density).value() + ); + + const auto erasedPressureDensityDerivative = view.tryPartialDerivative( + eos::thermodynamicQuantityId, eos::thermodynamicQuantityId, + std::span{runtimeDensityInput} + ); + + REQUIRE(erasedPressureDensityDerivative.has_value()); + CHECK(*erasedPressureDensityDerivative == runtimePressureDensityDerivative->value()); +} + +TEST_CASE( + "Runtime EOS View Reports Unsupported And Invalid Requests", + tags::equation_of_state_runtime_contract +) { + const eos::Polytrope equationOfState(3.0, 0.25); + const eos::EquationOfStateView view{equationOfState}; + + constexpr eos::ThermodynamicQuantityId temperature{"temperature"}; + + const std::array densityInput{eos::RuntimeQuantityValue{eos::thermodynamicQuantityId, 0.7}}; + + const std::array pressureInput{ + eos::RuntimeQuantityValue{eos::thermodynamicQuantityId, 0.04} + }; + + const std::array noInputs{}; + + const auto unsupportedOutput = + view.tryEvaluate(temperature, std::span{densityInput}); + REQUIRE_FALSE(unsupportedOutput.has_value()); + CHECK(unsupportedOutput.error().code() == eos::EvaluationErrorCode::unsupported_relation); + + const auto wrongInputCount = view.tryEvaluate( + eos::thermodynamicQuantityId, std::span{noInputs} + ); + REQUIRE_FALSE(wrongInputCount.has_value()); + CHECK(wrongInputCount.error().code() == eos::EvaluationErrorCode::wrong_input_count); + + const auto wrongInputQuantity = view.tryEvaluate( + eos::thermodynamicQuantityId, std::span{pressureInput} + ); + REQUIRE_FALSE(wrongInputQuantity.has_value()); + CHECK(wrongInputQuantity.error().code() == eos::EvaluationErrorCode::wrong_input_quantity); + + const auto unsupportedDerivative = view.tryPartialDerivative( + eos::thermodynamicQuantityId, + eos::thermodynamicQuantityId, std::span{pressureInput} + ); + REQUIRE_FALSE(unsupportedDerivative.has_value()); + CHECK(unsupportedDerivative.error().code() == eos::EvaluationErrorCode::unsupported_derivative); + + const auto invalidDensity = view.tryEvaluate(eos::DensityValue{-0.1}); + REQUIRE_FALSE(invalidDensity.has_value()); + CHECK(invalidDensity.error().code() == eos::EvaluationErrorCode::outside_domain); + + const auto nonfiniteDensity = + view.tryEvaluate(eos::DensityValue{std::numeric_limits::quiet_NaN()}); + REQUIRE_FALSE(nonfiniteDensity.has_value()); + CHECK(nonfiniteDensity.error().code() == eos::EvaluationErrorCode::nonfinite_input); +} + +TEST_CASE( + "One Runtime EOS Function Accepts Heterogeneous Concrete Models", + tags::equation_of_state_runtime_compatibility +) { + const eos::Polytrope polytrope(3.0, 0.25); + const LinearPressureEquationOfState linearEquationOfState; + + const std::array views{eos::EquationOfStateView{polytrope}, eos::EquationOfStateView{linearEquationOfState}}; + + const eos::DensityValue density{0.7}; + + const auto polytropicPressure = pressureAtDensity(views[0], density); + const auto linearPressure = pressureAtDensity(views[1], density); + + REQUIRE(polytropicPressure.has_value()); + REQUIRE(linearPressure.has_value()); + + CHECK(polytropicPressure->value() == eos::evaluate(polytrope, density).value()); + CHECK(linearPressure->value() == 1.9); +} + +TEST_CASE( + "Runtime EOS View Remains Valid When Stable Ownership Moves", + tags::equation_of_state_runtime_contract +) { + auto owner = std::make_unique(3.0, 0.25); + const eos::EquationOfStateView view{*owner}; + + auto movedOwner = std::move(owner); + + const auto pressure = view.tryEvaluate(eos::DensityValue{0.7}); + + REQUIRE(movedOwner != nullptr); + REQUIRE(pressure.has_value()); + CHECK(pressure->value() == eos::evaluate(*movedOwner, eos::DensityValue{0.7}).value()); +} diff --git a/tests/physics/equation_of_state_type_system.cpp b/tests/physics/equation_of_state_type_system.cpp new file mode 100644 index 0000000..8f7671a --- /dev/null +++ b/tests/physics/equation_of_state_type_system.cpp @@ -0,0 +1,228 @@ +#include +#include +#include + +#include + +import mean_field; +import test_helpers; + +namespace { + namespace eos = mean_field::eos; + + struct Entropy final : eos::ThermodynamicQuantity { }; + struct ElectronFraction final : eos::ThermodynamicQuantity { }; + + using SpecificEnthalpyFromPressureAndEntropy = + eos::Relation; + + class CompleteEquationOfState final { + public: + using Relations = eos::RelationCatalog< + eos::PressureFromDensity, + eos::SpecificEnthalpyFromPressure, + SpecificEnthalpyFromPressureAndEntropy>; + + [[nodiscard]] constexpr eos::PressureValue evaluate( + eos::PressureFromDensity, + const eos::DensityValue density + ) const noexcept { + return eos::PressureValue{2.0 * density.value()}; + } + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + eos::SpecificEnthalpyFromPressure, + const eos::PressureValue pressure + ) const noexcept { + return eos::SpecificEnthalpyValue{3.0 * pressure.value()}; + } + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + SpecificEnthalpyFromPressureAndEntropy, + const eos::PressureValue pressure, + const eos::QuantityValue entropy + ) const noexcept { + return eos::SpecificEnthalpyValue{3.0 * pressure.value() + 5.0 * entropy.value()}; + } + + [[nodiscard]] constexpr eos::PartialDerivative< + eos::quantity::SpecificEnthalpy, + Entropy> + partialDerivative( + SpecificEnthalpyFromPressureAndEntropy, + eos::WithRespectTo, + eos::PressureValue, + eos::QuantityValue + ) const noexcept { + return eos::PartialDerivative{5.0}; + } + }; + + class MissingRelationImplementation final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] eos::PressureValue evaluate( + eos::PressureFromDensity, + eos::DensityValue density + ) const { + return eos::PressureValue{density.value()}; + } + }; + + class IncorrectRelationOutput final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] eos::DensityValue evaluate( + eos::PressureFromDensity, + eos::DensityValue density + ) const { + return density; + } + }; + + class InvalidRelationCatalog final { + public: + using Relations = eos::RelationCatalog>; + }; + + template + concept CanEvaluateDensityFromSpecificEnthalpy = requires(const EquationOfState &equationOfState) { + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{1.0}); + }; +} // namespace + +TEST_CASE( + "Thermodynamic Values Preserve Physical Quantity Types", + tags::equation_of_state_quantity_types +) { + STATIC_CHECK(eos::ThermodynamicQuantityType); + STATIC_CHECK(eos::ThermodynamicQuantityType); + STATIC_CHECK(eos::ThermodynamicQuantityType); + STATIC_CHECK_FALSE(eos::ThermodynamicQuantityType); + + STATIC_CHECK_FALSE(std::same_as); + STATIC_CHECK_FALSE(std::same_as); + STATIC_CHECK_FALSE(std::is_convertible_v); + STATIC_CHECK_FALSE(std::is_constructible_v); + + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK(std::is_standard_layout_v); + STATIC_CHECK(sizeof(eos::DensityValue) == sizeof(double)); + STATIC_CHECK(sizeof(eos::PressureValue) == sizeof(double)); + STATIC_CHECK(sizeof(eos::SpecificEnthalpyValue) == sizeof(double)); + STATIC_CHECK(std::is_empty_v); + + constexpr eos::DensityValue density{-0.25}; + STATIC_CHECK(density.value() == -0.25); +} + +TEST_CASE( + "Thermodynamic Derivatives Preserve Numerator And Denominator Types", + tags::equation_of_state_quantity_types +) { + using PressureByDensity = eos::PartialDerivative; + + using PressureBySpecificEnthalpy = eos::PartialDerivative; + + STATIC_CHECK_FALSE(std::same_as); + STATIC_CHECK_FALSE(std::is_convertible_v); + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK(std::is_standard_layout_v); + STATIC_CHECK(sizeof(PressureByDensity) == sizeof(double)); + + constexpr PressureByDensity derivative{1.75}; + STATIC_CHECK(derivative.value() == 1.75); +} + +TEST_CASE( + "EOS Relation Catalog Rejects Invalid And Duplicate Relations", + tags::equation_of_state_relation_contract +) { + using ValidCatalog = eos::RelationCatalog; + + using DuplicateCatalog = eos::RelationCatalog; + + using InvalidRelation = eos::Relation; + using InvalidCatalog = eos::RelationCatalog; + using RepeatedInputRelation = + eos::Relation; + using RepeatedInputCatalog = eos::RelationCatalog; + + STATIC_CHECK(eos::ValidRelationCatalog); + STATIC_CHECK_FALSE(eos::ValidRelationCatalog); + STATIC_CHECK_FALSE(eos::ValidRelationCatalog); + STATIC_CHECK_FALSE(eos::ValidRelationCatalog); + STATIC_CHECK_FALSE(eos::ValidRelationCatalog>); + + STATIC_CHECK(eos::relationCatalogContains); + STATIC_CHECK_FALSE(eos::relationCatalogContains); + STATIC_CHECK(eos::relationContainsInput); + STATIC_CHECK_FALSE(eos::relationContainsInput); + + STATIC_CHECK(std::same_as, eos::quantity::Pressure>); + STATIC_CHECK(std::same_as, eos::quantity::Density>); +} + +TEST_CASE( + "EOS Model Contract Requires Every Declared Relation", + tags::equation_of_state_relation_contract +) { + STATIC_CHECK(eos::EquationOfStateModel); + STATIC_CHECK_FALSE(eos::EquationOfStateModel); + STATIC_CHECK_FALSE(eos::EquationOfStateModel); + STATIC_CHECK_FALSE(eos::EquationOfStateModel); + + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK_FALSE(eos::SupportsRelation); + STATIC_CHECK_FALSE(CanEvaluateDensityFromSpecificEnthalpy); + STATIC_CHECK( + eos::SupportsPartialDerivative + ); + STATIC_CHECK_FALSE( + eos::SupportsPartialDerivative< + CompleteEquationOfState, SpecificEnthalpyFromPressureAndEntropy, ElectronFraction> + ); +} + +TEST_CASE( + "EOS Evaluation Selects Relations From Typed Inputs", + tags::equation_of_state_relation_contract +) { + constexpr CompleteEquationOfState equationOfState; + + constexpr eos::PressureValue pressure = + eos::evaluate(equationOfState, eos::DensityValue{1.25}); + + constexpr eos::SpecificEnthalpyValue specificEnthalpy = eos::evaluate( + equationOfState, eos::PressureValue{0.5}, eos::QuantityValue{0.2} + ); + + constexpr auto entropyDerivative = eos::partialDerivative( + equationOfState, eos::PressureValue{0.5}, eos::QuantityValue{0.2} + ); + + STATIC_CHECK(noexcept(eos::evaluate(equationOfState, eos::DensityValue{1.25}))); + STATIC_CHECK( + noexcept(eos::partialDerivative( + equationOfState, eos::PressureValue{0.5}, eos::QuantityValue{0.2} + )) + ); + + STATIC_CHECK(pressure.value() == 2.5); + STATIC_CHECK(specificEnthalpy.value() == 2.5); + STATIC_CHECK(entropyDerivative.value() == 5.0); +} + +TEST_CASE( + "EOS Evaluation Errors Retain A Structured Cause", + tags::equation_of_state_relation_contract +) { + const eos::EvaluationError error( + eos::EvaluationErrorCode::outside_domain, "Density is outside the relation domain." + ); + + CHECK(error.code() == eos::EvaluationErrorCode::outside_domain); + CHECK(std::string_view{error.what()} == "Density is outside the relation domain."); +} diff --git a/tests/physics/gravity.cpp b/tests/physics/gravity.cpp index 1f7143f..20196d7 100644 --- a/tests/physics/gravity.cpp +++ b/tests/physics/gravity.cpp @@ -18,2610 +18,2449 @@ import test_helpers; using namespace mean_field; namespace { -// 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; + // 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_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_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()); -} + 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 GravitationalEnergies { + double binding; + double virial; + }; -struct HomogeneousEllipsoidAnalytic { - double coefficient_x; - double coefficient_y; - double coefficient_z; - double energy_kernel; -}; + 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) {} + 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); + 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) == + 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; + "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); + 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); - } + mapping::MapPhysicalFluxToHDivReference(context, field_physical, value); + } -private: - mapping::GridFunctionMappingEvaluator mapping_evaluator; - double density; - double coefficient_x; - double coefficient_y; - double coefficient_z; -}; + 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; + 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); + 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; + for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) { + if (!DomainSchema::template attribute_belongs_to(f.mesh->GetAttribute(elem_id))) { + continue; + } + + 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}; } - mfem::ElementTransformation *transformation = - f.mesh->GetElementTransformation(elem_id); - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + void zero_vacuum_density( + const fem::FEM &f, + mfem::GridFunction &rho + ) { + using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); + const field::FieldDofMap densityMap = field::make_field_dof_map(*f.densityFes); - 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); + mfem::Vector densityTrue; + rho.GetTrueDofs(densityTrue); - 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; + const mfem::Vector supportedDensity = densityMap.gather(densityTrue); + densityMap.scatter(supportedDensity, densityTrue); + rho.SetFromTrueDofs(densityTrue); } - 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; + int get_gravity_quadrature_order(const fem::FEM &f) { + return 2 * std::max(f.gravityPotentialFes->GetMaxElementOrder(), f.gravityFluxFes->GetMaxElementOrder()) + 8; } - 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)); + 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; + } - return ds_squared_dt / delta; - }; + 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) + ); - 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 normalized_axis_x * normalized_axis_y * normalized_axis_z * ds_squared_dt / + ((target_axis_squared + s_squared) * delta); + }; - return dimensionless_integral / length_scale; -} - -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); - - return {.coefficient_x = coefficient_x, - .coefficient_y = coefficient_y, - .coefficient_z = coefficient_z, - .energy_kernel = energy_kernel}; -} - -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 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); - - constexpr int shell_count = - static_cast(exterior_shell_boundaries.size()) - 1; - - std::array local_shells{}; - - mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - - const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; - - 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; + double integration_error = 0.0; + return boost::math::quadrature::gauss_kronrod::integrate( + integrand, 0.0, 1.0, 15, 1.0e-13, &integration_error + ); } - const mfem::FiniteElement &displacement_element = - *f.displacementFes->GetFE(element_id); + 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 mfem::FiniteElement &compactification_element = - *f.compactificationFes->GetFE(element_id); + 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) + ); - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + return ds_squared_dt / delta; + }; - mfem::DofTransformation *displacement_dof_transformation = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); + 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 + ); - 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); + 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); + + return { + .coefficient_x = coefficient_x, + .coefficient_y = coefficient_y, + .coefficient_z = coefficient_z, + .energy_kernel = energy_kernel + }; } - const mapping::ElementDisplacementData displacement_data( - displacement_element, element_displacement, - f.displacementFes->GetOrdering()); + 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}; + }; - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification); + 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}; + }; - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, - .compactification = compactification_data}; + constexpr std::array exterior_shell_boundaries{0.0, 0.25, 0.50, 0.75, 0.90, 1.0}; - mfem::Vector compactification_shape(compactification_element.GetDof()); + 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 mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + const double coordinate = std::clamp(compactification_coordinate, 0.0, std::nextafter(1.0, 0.0)); - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(q); + for (int shell = 0; shell < static_cast(exterior_shell_boundaries.size()) - 1; ++shell) { + if (coordinate < exterior_shell_boundaries[shell + 1]) { + return shell; + } + } - 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); + return static_cast(exterior_shell_boundaries.size()) - 2; } - if (compactification_dof_transformation != nullptr) { - compactification_dof_transformation->InvTransformPrimal( - element_compactification); + std::array< + ExteriorMonopoleShellMetrics, + 5> + 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); + + constexpr int shell_count = static_cast(exterior_shell_boundaries.size()) - 1; + + std::array local_shells{}; + + mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); + + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + + 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); + + 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; } - const mapping::ElementDisplacementData displacement_data( - displacement_element, element_displacement, - m_fem.displacementFes->GetOrdering()); + 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()) { + } - const mapping::ElementCompactificationData compactification_data( - compactification_element, element_compactification); + 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; - mfem::Vector requested_compactification_shape( - compactification_element.GetDof()); + const int element_id = transformation.ElementNo; - compactification_element.CalcShape(integration_point, - requested_compactification_shape); + MFEM_VERIFY(element_id >= 0, "Projection coefficient received an invalid element number."); - const double requested_compactification_coordinate = - element_compactification * requested_compactification_shape; + const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id); - constexpr double infinity_candidate_tolerance = 1.0e-8; + const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id); - const bool requested_infinity_limit = - std::isfinite(requested_compactification_coordinate) && - requested_compactification_coordinate >= - 1.0 - infinity_candidate_tolerance; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - const mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, - .compactification = compactification_data}; + mfem::DofTransformation *displacement_dof_transformation = + m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mapping::MappingStatus status = m_domain_mapper.EvaluatePoint( - mapping_data, transformation, integration_point, m_workspace, context); + mfem::DofTransformation *compactification_dof_transformation = + m_fem.compactificationFes->GetElementDofs(element_id, compactification_dofs); - if (status == mapping::MappingStatus::valid) { - transformation.SetIntPoint(&integration_point); - return status; - } + mfem::Vector element_displacement; + mfem::Vector element_compactification; - if (!permit_infinity_limit || - !m_domain_mapper.IsCompactifiedElement(transformation)) { - transformation.SetIntPoint(&integration_point); - return status; - } + m_displacement.GetSubVector(displacement_dofs, element_displacement); - 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); + m_fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - if (!retryable_boundary_status) { - transformation.SetIntPoint(&integration_point); - return status; - } + if (displacement_dof_transformation != nullptr) { + displacement_dof_transformation->InvTransformPrimal(element_displacement); + } - const mfem::IntegrationPoint &element_center = - mfem::Geometries.GetCenter(transformation.GetGeometryType()); + if (compactification_dof_transformation != nullptr) { + compactification_dof_transformation->InvTransformPrimal(element_compactification); + } - /* - * 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 mapping::ElementDisplacementData displacement_data( + displacement_element, element_displacement, m_fem.displacementFes->GetOrdering() + ); - for (const double inward_fraction : inward_fractions) { - mfem::IntegrationPoint inward_point; + const mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification + ); - inward_point.x = (1.0 - inward_fraction) * integration_point.x + - inward_fraction * element_center.x; + mfem::Vector requested_compactification_shape(compactification_element.GetDof()); - inward_point.y = (1.0 - inward_fraction) * integration_point.y + - inward_fraction * element_center.y; + compactification_element.CalcShape(integration_point, requested_compactification_shape); - inward_point.z = (1.0 - inward_fraction) * integration_point.z + - inward_fraction * element_center.z; + const double requested_compactification_coordinate = + element_compactification * requested_compactification_shape; - inward_point.weight = integration_point.weight; + constexpr double infinity_candidate_tolerance = 1.0e-8; - status = m_domain_mapper.EvaluatePoint( - mapping_data, transformation, inward_point, m_workspace, context); + const bool requested_infinity_limit = + std::isfinite(requested_compactification_coordinate) && + requested_compactification_coordinate >= 1.0 - infinity_candidate_tolerance; - if (status == mapping::MappingStatus::valid) { - m_last_evaluation_used_infinity_limit = true; - transformation.SetIntPoint(&integration_point); - return status; - } + const mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, .compactification = compactification_data + }; - 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; - } - } + mapping::MappingStatus status = + m_domain_mapper.EvaluatePoint(mapping_data, transformation, integration_point, m_workspace, context); - transformation.SetIntPoint(&integration_point); - return status; - } + if (status == mapping::MappingStatus::valid) { + transformation.SetIntPoint(&integration_point); + return status; + } - [[nodiscard]] - bool LastEvaluationUsedInfinityLimit() const noexcept { - return m_last_evaluation_used_infinity_limit; - } + if (!permit_infinity_limit || !m_domain_mapper.IsCompactifiedElement(transformation)) { + transformation.SetIntPoint(&integration_point); + return status; + } -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) {} + 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 Eval(mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point) override { - mapping::MappingPointContext context; + if (!retryable_boundary_status) { + transformation.SetIntPoint(&integration_point); + return status; + } - const mapping::MappingStatus status = - m_geometry.Evaluate(transformation, integration_point, context, true); + const mfem::IntegrationPoint &element_center = mfem::Geometries.GetCenter(transformation.GetGeometryType()); - MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, + /* + * 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::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::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) {} + 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) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - *f.displacement = 0.0; +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; - 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 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); + 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 + ); - 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 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); - mfem::Vector x_physical; - mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, - x_physical); + mfem::Vector x_physical; + mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, x_physical); - const double radial_coordinate = x_physical.Norml2(); - if (radial_coordinate < 1.0e-9) { - continue; + const double radial_coordinate = x_physical.Norml2(); + if (radial_coordinate < 1.0e-9) { + continue; + } + + 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 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); + 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); - 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 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); - 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); + 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 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); + constexpr double energy_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); - - 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)); + 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) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - *f.displacement = 0.0; +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; - 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 central_density = - (15.0 * mass) / (8.0 * M_PI * std::pow(radius, 3.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)); - 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)); - }; + 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)); + }; - 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); - } + 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); + } - 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); + 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); - 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 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); + 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); - 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); + 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 analytic_tolerance = 1.0e-5; - constexpr double consistency_tolerance = 1.0e-6; + 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)); + 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) { - auto args = test_utils::setup_args(); - fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); - *f.displacement = 0.0; +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; - 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 radius = utils::RADIUS; + const double mass = utils::MASS; - // 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); + // 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); - 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; + 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; + } + + 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.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, rational_rho + ); + } else { + rho_coeff = std::make_unique(rational_rho); } - const double denominator = 1.0 + concentration * normalized_radius_squared; - return density_scale * (1.0 - normalized_radius_squared) / - (denominator * denominator); - }; + 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, rational_rho); - } else { - rho_coeff = std::make_unique(rational_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::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); - 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 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 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); - 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)); + CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance)); } -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); +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.displacement = displacement; + 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.domainMapperStateless, *f.displacement, - *f.compactificationCoordinate, 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); - } - } - - 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; + if (attribute != 3) { + analytic_field_projection.ProjectCoefficient(analytic_field_coefficient, attribute); + } } - mfem::ElementTransformation *transformation = - f.mesh->GetElementTransformation(elem_id); - const mfem::IntegrationRule &integration_rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + 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; - for (int q = 0; q < integration_rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &integration_point = - integration_rule.IntPoint(q); - transformation->SetIntPoint(&integration_point); + 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 + ); - 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; + for (int elem_id = 0; elem_id < f.mesh->GetNE(); ++elem_id) { + if (f.mesh->GetAttribute(elem_id) == 3) { + continue; + } - 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; + mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elem_id); + const mfem::IntegrationRule &integration_rule = + mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - 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); + for (int q = 0; q < integration_rule.GetNPoints(); ++q) { + const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); + transformation->SetIntPoint(&integration_point); - projected_field_difference = projected_field_physical; - projected_field_difference -= grad_phi_analytic; - local_projection_error_squared += - (projected_field_difference * projected_field_difference) * weight; + 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; - 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_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_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; + } } - } - 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)); - // 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; + // 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, 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); + 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) { - 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.displacement = displacement; + 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; + 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.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, ellipsoidal_rho + ); + } else { + rho_coeff = std::make_unique(ellipsoidal_rho); } - const double denominator = 1.0 + concentration * ellipsoidal_radius_squared; - return density_scale * (1.0 - ellipsoidal_radius_squared) / - (denominator * denominator); - }; + 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, ellipsoidal_rho); - } else { - rho_coeff = std::make_unique(ellipsoidal_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 double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); + INFO("Normalized quadrupole = " << normalized_quadrupole); + REQUIRE(normalized_quadrupole > 1.0e-3); - f.com = analysis::get_com(f, rho_grid); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_grid, f.com); + 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 normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); - INFO("Normalized quadrupole = " << normalized_quadrupole); - REQUIRE(normalized_quadrupole > 1.0e-3); + REQUIRE(energies.binding < 0.0); + REQUIRE(energies.virial < 0.0); - 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 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); - 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)); + CHECK_THAT(relative_consistency_error, Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance)); } -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); +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.displacement = 0.0; + *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::solve_gravity_field(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; - mapping::GridFunctionMappingEvaluator mapping_evaluator( - *f.domainMapperStateless, *f.displacement, - *f.compactificationCoordinate); + 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; + 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; + mapping_evaluator.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); } - 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); + double global_maximum_absolute_error = 0.0; + double global_maximum_relative_error = 0.0; - mfem::Vector physical_position; - mapping_evaluator.GetPhysicalPoint(*transformation, integration_point, - physical_position); + MPI_Allreduce( + &local_maximum_absolute_error, &global_maximum_absolute_error, 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm() + ); - const double radial_coordinate = physical_position.Norml2(); + MPI_Allreduce( + &local_maximum_relative_error, &global_maximum_relative_error, 1, MPI_DOUBLE, MPI_MAX, f.densityFes->GetComm() + ); - if (radial_coordinate < 1.0e-9) { - continue; - } + 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); - const double analytic_potential = - -(utils::G * mass / (2.0 * std::pow(radius, 3.0))) * - (3.0 * radius * radius - radial_coordinate * radial_coordinate); + 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 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); + REQUIRE(energies.binding < 0.0); + REQUIRE(energies.virial < 0.0); - local_maximum_absolute_error = - std::max(local_maximum_absolute_error, absolute_error); - local_maximum_relative_error = - std::max(local_maximum_relative_error, relative_error); - } + constexpr double energy_tolerance = 1.0e-5; + constexpr double consistency_tolerance = 1.0e-6; - 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)); + 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("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); +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::VectorFunctionCoefficient displacement_coefficient(3, affine_displacement); - mfem::ParGridFunction displacement(f.displacementFes.get()); + mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement.ProjectCoefficient(displacement_coefficient); + displacement.ProjectCoefficient(displacement_coefficient); - *f.displacement = displacement; + *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_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); + 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); - if (ellipsoidal_radius_squared >= 1.0) { - return 0.0; - } + if (ellipsoidal_radius_squared >= 1.0) { + return 0.0; + } - const double denominator = 1.0 + concentration * ellipsoidal_radius_squared; + const double denominator = 1.0 + concentration * ellipsoidal_radius_squared; - return density_scale * (1.0 - ellipsoidal_radius_squared) / - (denominator * denominator); - }; + return density_scale * (1.0 - ellipsoidal_radius_squared) / (denominator * denominator); + }; - mapping::PhysicalPositionFunctionCoefficient density_coefficient( - *f.domainMapperStateless, *f.displacement, - *f.compactificationCoordinate, ellipsoidal_density); + mapping::PhysicalPositionFunctionCoefficient density_coefficient( + *f.domainMapperStateless, *f.displacement, *f.compactificationCoordinate, ellipsoidal_density + ); - mfem::GridFunction density(f.densityFes.get()); + mfem::GridFunction density(f.densityFes.get()); - density.ProjectCoefficient(density_coefficient); + density.ProjectCoefficient(density_coefficient); - zero_vacuum_density(f, density); - analysis::conserve_mass(f, density, mass); + zero_vacuum_density(f, density); + analysis::conserve_mass(f, density, mass); - f.com = analysis::get_com(f, density); - f.Q = physics::compute_quadrupole_moment_tensor(f, density, f.com); + f.com = analysis::get_com(f, density); + f.Q = physics::compute_quadrupole_moment_tensor(f, density, f.com); - const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); + const double normalized_quadrupole = f.Q.FNorm() / (mass * radius * radius); - INFO("Normalized quadrupole = " << normalized_quadrupole); + INFO("Normalized quadrupole = " << normalized_quadrupole); - REQUIRE(normalized_quadrupole > 1.0e-3); + REQUIRE(normalized_quadrupole > 1.0e-3); - const physics::GravitySolution gravity_solution = - physics::solve_gravity_field(f, args, density, displacement); + const physics::GravitySolution gravity_solution = physics::solve_gravity_field(f, args, density, displacement); - const int base_quadrature_order = get_gravity_quadrature_order(f); + const int base_quadrature_order = get_gravity_quadrature_order(f); - const GravitationalEnergies base_energies = compute_gravitational_energies( - f, density, gravity_solution, base_quadrature_order); + const GravitationalEnergies base_energies = + compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order); - const GravitationalEnergies medium_energies = compute_gravitational_energies( - f, density, gravity_solution, base_quadrature_order + 4); + const GravitationalEnergies medium_energies = + compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order + 4); - const GravitationalEnergies fine_energies = compute_gravitational_energies( - f, density, gravity_solution, base_quadrature_order + 8); + const GravitationalEnergies fine_energies = + compute_gravitational_energies(f, density, gravity_solution, base_quadrature_order + 8); - REQUIRE(fine_energies.binding < 0.0); - REQUIRE(fine_energies.virial < 0.0); + REQUIRE(fine_energies.binding < 0.0); + REQUIRE(fine_energies.virial < 0.0); - const double base_consistency_error = - std::abs(base_energies.binding - base_energies.virial) / - std::abs(base_energies.binding); + const double base_consistency_error = + std::abs(base_energies.binding - base_energies.virial) / std::abs(base_energies.binding); - const double medium_consistency_error = - std::abs(medium_energies.binding - medium_energies.virial) / - std::abs(medium_energies.binding); + const double medium_consistency_error = + std::abs(medium_energies.binding - medium_energies.virial) / std::abs(medium_energies.binding); - const double fine_consistency_error = - std::abs(fine_energies.binding - fine_energies.virial) / - std::abs(fine_energies.binding); + const double fine_consistency_error = + std::abs(fine_energies.binding - fine_energies.virial) / std::abs(fine_energies.binding); - const double binding_quadrature_change = - std::abs(fine_energies.binding - medium_energies.binding) / - std::abs(fine_energies.binding); + const double binding_quadrature_change = + std::abs(fine_energies.binding - medium_energies.binding) / std::abs(fine_energies.binding); - const double virial_quadrature_change = - std::abs(fine_energies.virial - medium_energies.virial) / - std::abs(fine_energies.virial); + const double virial_quadrature_change = + std::abs(fine_energies.virial - medium_energies.virial) / std::abs(fine_energies.virial); - INFO("Base-order consistency error = " << base_consistency_error); + INFO("Base-order consistency error = " << base_consistency_error); - INFO("Medium-order consistency error = " << medium_consistency_error); + INFO("Medium-order consistency error = " << medium_consistency_error); - INFO("Fine-order consistency error = " << fine_consistency_error); + INFO("Fine-order consistency error = " << fine_consistency_error); - INFO("Medium-to-fine binding-energy change = " << binding_quadrature_change); + INFO("Medium-to-fine binding-energy change = " << binding_quadrature_change); - INFO("Medium-to-fine virial-energy change = " << virial_quadrature_change); + INFO("Medium-to-fine virial-energy change = " << virial_quadrature_change); - constexpr double diagnostic_quadrature_tolerance = 1.0e-7; + constexpr double diagnostic_quadrature_tolerance = 1.0e-7; - CHECK_THAT( - fine_consistency_error, - Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance)); + CHECK_THAT(fine_consistency_error, Catch::Matchers::WithinAbs(0.0, rational_profile_virial_tolerance)); - CHECK_THAT(binding_quadrature_change, - Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); + CHECK_THAT(binding_quadrature_change, Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); - CHECK_THAT(virial_quadrature_change, - Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); + CHECK_THAT(virial_quadrature_change, Catch::Matchers::WithinAbs(0.0, diagnostic_quadrature_tolerance)); } -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); +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); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.domainMapperStateless != nullptr); - REQUIRE(f.compactificationCoordinate != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.domainMapperStateless != nullptr); + REQUIRE(f.compactificationCoordinate != nullptr); - const double stellar_radius = utils::RADIUS; + const double stellar_radius = utils::RADIUS; - const double mass = utils::MASS; + const double mass = utils::MASS; - const double analytic_volume = - (4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius; + const double analytic_volume = (4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius; - const double density = mass / analytic_volume; + const double density = mass / analytic_volume; - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement = 0.0; + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement = 0.0; - *f.displacement = 0.0; + *f.displacement = 0.0; - mfem::GridFunction rho_uniform(f.densityFes.get()); - rho_uniform = density; + mfem::GridFunction rho_uniform(f.densityFes.get()); + rho_uniform = density; - zero_vacuum_density(f, rho_uniform); + zero_vacuum_density(f, rho_uniform); - analysis::conserve_mass(f, rho_uniform, mass); + analysis::conserve_mass(f, rho_uniform, mass); - f.com = analysis::get_com(f, rho_uniform); + f.com = analysis::get_com(f, rho_uniform); - f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); + f.Q = physics::compute_quadrupole_moment_tensor(f, rho_uniform, f.com); - const physics::GravitySolution numerical_solution = - physics::solve_gravity_field(f, args, rho_uniform, displacement); + const physics::GravitySolution numerical_solution = + physics::solve_gravity_field(f, args, rho_uniform, displacement); - StatelessMonopolePotentialCoefficient analytic_potential_coefficient( - f, *f.domainMapperStateless, displacement, mass, stellar_radius); + StatelessMonopolePotentialCoefficient analytic_potential_coefficient( + f, *f.domainMapperStateless, displacement, mass, stellar_radius + ); - StatelessMonopoleHDivCoefficient analytic_field_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; + physics::GravitySolution analytic_projection(f); + analytic_projection.phi = 0.0; + analytic_projection.gradPhi = 0.0; - analytic_projection.phi.ProjectCoefficient(analytic_potential_coefficient); + analytic_projection.phi.ProjectCoefficient(analytic_potential_coefficient); - analytic_projection.gradPhi.ProjectCoefficient(analytic_field_coefficient); + analytic_projection.gradPhi.ProjectCoefficient(analytic_field_coefficient); - const std::array numerical_metrics = - measure_exterior_monopole_shells(f, numerical_solution, displacement, - mass); + const std::array numerical_metrics = + measure_exterior_monopole_shells(f, numerical_solution, displacement, mass); - const std::array projection_metrics = - measure_exterior_monopole_shells(f, analytic_projection, displacement, - mass); + const std::array projection_metrics = + measure_exterior_monopole_shells(f, analytic_projection, displacement, mass); - mfem::Vector numerical_gradient_true; - mfem::Vector numerical_potential_true; - mfem::Vector projected_gradient_true; - mfem::Vector projected_potential_true; + 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.gradPhi.GetTrueDofs(numerical_gradient_true); - numerical_solution.phi.GetTrueDofs(numerical_potential_true); + numerical_solution.phi.GetTrueDofs(numerical_potential_true); - analytic_projection.gradPhi.GetTrueDofs(projected_gradient_true); + analytic_projection.gradPhi.GetTrueDofs(projected_gradient_true); - analytic_projection.phi.GetTrueDofs(projected_potential_true); + analytic_projection.phi.GetTrueDofs(projected_potential_true); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - const double gradient_projection_gap = global_relative_vector_error( - numerical_gradient_true, projected_gradient_true, communicator); + 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); + 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; + 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; + std::ostringstream report; - report << "Global numerical/projection gradient DOF gap = " - << gradient_projection_gap << '\n' - << "Global numerical/projection potential DOF gap = " - << potential_projection_gap << '\n'; + 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]; + for (int shell = 0; shell < 5; ++shell) { + const ExteriorMonopoleShellMetrics &numerical = numerical_metrics[shell]; - const ExteriorMonopoleShellMetrics &projected = projection_metrics[shell]; + const ExteriorMonopoleShellMetrics &projected = projection_metrics[shell]; - maximum_numerical_potential_error = std::max( - maximum_numerical_potential_error, numerical.potential_rms_error); + 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_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_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_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_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); + 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'; - } + 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()); + INFO(report.str()); - REQUIRE(std::isfinite(gradient_projection_gap)); - REQUIRE(std::isfinite(potential_projection_gap)); + 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); + 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); + /* + * 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); + 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); + /* + * 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); + CHECK(potential_projection_gap < maximum_numerical_potential_error); } -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); +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 fem = fem::setup_fem(args.mesh_file, args, 0); + fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(fem.domainMapperStateless != nullptr); - REQUIRE(fem.domainMapperStateless != nullptr); + REQUIRE(fem.domainMapperStateless != nullptr); + REQUIRE(fem.domainMapperStateless != nullptr); - 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); - 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; - 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)); - 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); - }; + 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); + mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); + mfem::ParGridFunction displacement(fem.displacementFes.get()); + displacement.ProjectCoefficient(displacement_coefficient); - *fem.displacement = displacement; + *fem.displacement = 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 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); + 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); + 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); + 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); + 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); + 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; - }; + return -M_PI * utils::G * numerical_density * potential_kernel; + }; - 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); + 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); - 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; + 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); - mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate); + mfem::Vector physical_position(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; + 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, + "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()) + ); + } } - const mfem::IntegrationRule &rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + 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() + ); - 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); + double maximum_relative_error = 0.0; + MPI_Allreduce( + &local_maximum_relative_error, &maximum_relative_error, 1, MPI_DOUBLE, MPI_MAX, fem.densityFes->GetComm() + ); - 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 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]); - 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; + 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); - 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())); - } - } + REQUIRE(std::isfinite(solution_relative_error)); + REQUIRE(std::isfinite(projection_relative_error)); + REQUIRE(std::isfinite(solution_projection_gap)); + REQUIRE(std::isfinite(maximum_relative_error)); - 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); + /* + * 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) {} - - double Eval(mfem::ElementTransformation &transformation, - const mfem::IntegrationPoint &integration_point) override { - if (transformation.Attribute == m_vacuum_attribute) { - return 0.0; + FerrersVacuumMaskedCoefficient( + Coefficient &coefficient, + const int vacuum_attribute + ) + : m_coefficient(coefficient), + m_vacuum_attribute(vacuum_attribute) { } - return m_coefficient.Eval(transformation, integration_point); - } + 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); + } private: - Coefficient &m_coefficient; - int m_vacuum_attribute; + Coefficient &m_coefficient; + int m_vacuum_attribute; }; -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]; +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]; - 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; + 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); } + 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) + }; + /* - * Map u in [0, infinity) to t in [0, 1]: + * Expansion of * - * u = L^2 [t / (1 - t)]^2. + * -pi G rho_c abc / 2 + * integral [(1 - m^2(u))^2 / Delta(u)] du. */ - const double one_minus_t = 1.0 - t; - const double s = t / one_minus_t; + double potential_kernel = analytic.potential_constant; - const double u = length_scale * length_scale * s * s; + 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 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; + 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]; + } } - 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); + return -0.5 * M_PI * utils::G * central_density * potential_kernel; } -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}; +static void evaluate_ferrers_n1_gradient( + const mfem::Vector &position, + const double central_density, + const FerrersN1Analytic &analytic, + mfem::Vector &gradient +) { + gradient.SetSize(3); - FerrersN1Analytic analytic{ - .potential_constant = compute_ferrers_n1_coefficient(semi_axes, -1, -1), - .first_coefficients = {}, - .second_coefficients = {}}; + const std::array coordinate_squared{ + position(0) * position(0), position(1) * position(1), position(2) * position(2) + }; - for (int axis = 0; axis < 3; ++axis) { - analytic.first_coefficients[axis] = - compute_ferrers_n1_coefficient(semi_axes, axis, -1); - } + for (int axis = 0; axis < 3; ++axis) { + double coefficient = analytic.first_coefficients[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); + for (int other_axis = 0; other_axis < 3; ++other_axis) { + coefficient -= analytic.second_coefficients[axis][other_axis] * coordinate_squared[other_axis]; + } - analytic.second_coefficients[first_axis][second_axis] = coefficient; - - analytic.second_coefficients[second_axis][first_axis] = coefficient; + /* + * 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; } - } - - 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)}; +TEST_CASE( + "Gravity Field Matches Analytic Ferrers Ellipsoid", + tags::gravity_analytic_accuracy +) { + auto args = test_utils::setup_args(); - /* - * Expansion of - * - * -pi G rho_c abc / 2 - * integral [(1 - m^2(u))^2 / Delta(u)] du. - */ - double potential_kernel = analytic.potential_constant; + args.p.rtol = 1.0e-13; + args.p.atol = 1.0e-14; + args.p.max_iters = std::max(args.p.max_iters, 2000); - for (int first_axis = 0; first_axis < 3; ++first_axis) { - potential_kernel -= 2.0 * analytic.first_coefficients[first_axis] * - coordinate_squared[first_axis]; + fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); - 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]; - } - } + REQUIRE(fem.domainMapperStateless != nullptr); + REQUIRE(fem.domainMapperStateless != nullptr); - return -0.5 * M_PI * utils::G * central_density * potential_kernel; -} + const double radius = utils::RADIUS; -static void evaluate_ferrers_n1_gradient(const mfem::Vector &position, - const double central_density, - const FerrersN1Analytic &analytic, - mfem::Vector &gradient) { - gradient.SetSize(3); + const double mass = utils::MASS; - const std::array coordinate_squared{position(0) * position(0), - position(1) * position(1), - position(2) * position(2)}; + constexpr double x_scale = 1.0; + constexpr double y_scale = 1.0; + constexpr double z_scale = 1.0 / (x_scale * y_scale); - for (int axis = 0; axis < 3; ++axis) { - double coefficient = analytic.first_coefficients[axis]; + const double semi_axis_x = x_scale * radius; + const double semi_axis_y = y_scale * radius; + const double semi_axis_z = z_scale * radius; - for (int other_axis = 0; other_axis < 3; ++other_axis) { - coefficient -= analytic.second_coefficients[axis][other_axis] * - coordinate_squared[other_axis]; - } + 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; /* - * The solver stores grad(Phi), which points outward for a - * negative gravitational potential. + * For rho = rho_c (1 - m^2), the exact mass is + * + * M = 8 pi a b c rho_c / 15. */ - 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); + 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 c6e9f51..e0ff24e 100644 --- a/tests/physics/gravity_monopole_accuracy.cpp +++ b/tests/physics/gravity_monopole_accuracy.cpp @@ -14,1201 +14,1131 @@ 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; -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); -} - -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); + constexpr int mapping_status_index(const mean_field::mapping::MappingStatus status) { + return static_cast(status); } - if (compactification_transform != nullptr) { - compactification_transform->InvTransformPrimal(element_compactification); + 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); } - 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_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 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_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 mfem::IntegrationPoint ¢er = - mfem::Geometries.GetCenter(transformation.GetGeometryType()); + double global_relative_error( + const mfem::Vector &computed, + const mfem::Vector &reference, + MPI_Comm communicator + ) { + REQUIRE(computed.Size() == reference.Size()); - 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}; + mfem::Vector difference(computed); + difference -= reference; - 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; - } + return global_norm(difference, communicator) / + std::max(global_norm(reference, communicator), std::numeric_limits::epsilon()); } - transformation.SetIntPoint(&integration_point); - return status; - } + int get_shell(const double coordinate) { + const double clamped = std::clamp(coordinate, 0.0, std::nextafter(1.0, 0.0)); - [[nodiscard]] bool UsedInfinityLimit() const noexcept { - return m_used_infinity_limit; - } - -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}; -}; - -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) {} - - 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, - "Stateless monopole-potential projection failed with status " - << 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; - } -} - -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; - - 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 * f.gravityFluxFes->GetMaxElementOrder() + 8; - - 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::Array gravity_dofs; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; - - 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_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 int dof_count = gravity_element.GetDof(); - const int dimension = transformation->GetSpaceDim(); - - mfem::Vector element_rhs(dof_count); - mfem::Vector analytic_field(dimension); - mfem::Vector pulled_rhs_field(dimension); - mfem::DenseMatrix vector_shape(dof_count, dimension); - - element_rhs = 0.0; - - const mfem::IntegrationRule &rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - - for (int 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); - - 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 << '.'); - - analytic_field = context.mapping.physical_position; - - 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); + for (int shell = 0; shell < shell_count; ++shell) { + if (clamped < shell_boundaries[shell + 1]) { + return shell; + } } - } + + return shell_count - 1; } - if (gravity_transform != nullptr) { - gravity_transform->TransformDual(element_rhs); + 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; } - local_rhs.AddElementVector(gravity_dofs, element_rhs); - } + 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()) { + } - mfem::Vector true_rhs; - local_to_true(*f.gravityFluxFes, local_rhs, true_rhs); + 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; - return true_rhs; -} + const int element_id = transformation.ElementNo; + MFEM_VERIFY(element_id >= 0, "Projection coefficient received an invalid element number."); -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::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id); + const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id); - const mfem::Vector projection_rhs = - assemble_monopole_projection_rhs(f, displacement, mass, stellar_radius); + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; - 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::DofTransformation *displacement_transform = + m_fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::Vector projected_gradient(mass_operator.Width()); - projected_gradient = 0.0; + mfem::DofTransformation *compactification_transform = + m_fem.compactificationFes->GetElementDofs(element_id, compactification_dofs); - 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 element_displacement; + mfem::Vector element_compactification; - mfem::Vector projection_residual; - mass_operator.Mult(projected_gradient, projection_residual); - projection_residual -= reduced_projection_rhs; + m_displacement.GetSubVector(displacement_dofs, element_displacement); + m_fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); - 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()); + if (displacement_transform != nullptr) { + displacement_transform->InvTransformPrimal(element_displacement); + } - 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); + if (compactification_transform != nullptr) { + compactification_transform->InvTransformPrimal(element_compactification); + } - REQUIRE(std::isfinite(relative_residual)); - REQUIRE(relative_residual < 1.0e-8); - return mass_operator.GetFluxMap().scatter(projected_gradient); -} + const mean_field::mapping::ElementDisplacementData displacement_data = + mean_field::mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, element_displacement + ); -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()); + const mean_field::mapping::ElementCompactificationData compactification_data( + compactification_element, element_compactification + ); - mfem::Vector displacement_true; - displacement.GetTrueDofs(displacement_true); + const mean_field::mapping::ElementMappingData mapping_data{ + .displacement = displacement_data, .compactification = compactification_data + }; - mean_field::operators::PreparedMappedHDivMassOperator mass_operator( - f, *f.domainMapperStateless); - mass_operator.Prepare( - mass_operator.GetDisplacementMap().gather(displacement_true)); + mfem::Vector compactification_shape(compactification_element.GetDof()); + compactification_element.CalcShape(integration_point, compactification_shape); - mfem::Vector difference(computed); - difference -= reference; + const double coordinate = element_compactification * compactification_shape; - mfem::Vector difference_action; - mfem::Vector reference_action; + mean_field::mapping::MappingStatus status = + m_mapper.EvaluatePoint(mapping_data, transformation, integration_point, m_workspace, context); - 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); + if (status == mean_field::mapping::MappingStatus::valid) { + transformation.SetIntPoint(&integration_point); + return status; + } - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + const bool infinity_request = m_mapper.IsCompactifiedElement(transformation) && coordinate >= 1.0 - 1.0e-10; - const double difference_energy = - global_dot(reduced_difference, difference_action, communicator); - const double reference_energy = - global_dot(reduced_reference, reference_action, communicator); + if (!permit_infinity_limit || !infinity_request || !retryable_infinity_status(status)) { + transformation.SetIntPoint(&integration_point); + return status; + } - REQUIRE(difference_energy >= -1.0e-12 * std::abs(reference_energy)); - REQUIRE(reference_energy > 0.0); + const mfem::IntegrationPoint ¢er = mfem::Geometries.GetCenter(transformation.GetGeometryType()); - return std::sqrt(std::max(0.0, difference_energy) / reference_energy); -} + 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}; -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; + 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; - mean_field::mapping::DomainMapper::Workspace workspace( - f.mesh->Dimension()); + status = m_mapper.EvaluatePoint(mapping_data, transformation, inward, m_workspace, context); - 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; + if (status == mean_field::mapping::MappingStatus::valid) { + m_used_infinity_limit = true; + transformation.SetIntPoint(&integration_point); + return status; + } - for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = - f.mesh->GetElementTransformation(element_id); + if (!retryable_infinity_status(status)) { + break; + } + } - if (transformation->Attribute != vacuum_attribute) { - continue; + 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::DomainMapper &m_mapper; + const mfem::GridFunction &m_displacement; + mean_field::mapping::DomainMapper::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::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) { + } + + 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, + "Stateless monopole-potential projection failed with status " + << 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; + } } - const mfem::FiniteElement &displacement_element = - *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = - *f.compactificationFes->GetFE(element_id); + 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; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - mfem::DofTransformation *displacement_transform = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_transform = - f.compactificationFes->GetElementDofs(element_id, - compactification_dofs); + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + const int quadrature_order = 2 * f.gravityFluxFes->GetMaxElementOrder() + 8; - mfem::Vector element_displacement; - mfem::Vector element_compactification; + 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); - displacement.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, - element_compactification); + mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - if (displacement_transform != nullptr) { - displacement_transform->InvTransformPrimal(element_displacement); + mfem::Array gravity_dofs; + mfem::Array displacement_dofs; + mfem::Array compactification_dofs; + + 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_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 int dof_count = gravity_element.GetDof(); + const int dimension = transformation->GetSpaceDim(); + + mfem::Vector element_rhs(dof_count); + mfem::Vector analytic_field(dimension); + mfem::Vector pulled_rhs_field(dimension); + mfem::DenseMatrix vector_shape(dof_count, dimension); + + element_rhs = 0.0; + + const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + + for (int 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); + + 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 << '.' + ); + + analytic_field = context.mapping.physical_position; + + 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); + } + } + } + + if (gravity_transform != nullptr) { + gravity_transform->TransformDual(element_rhs); + } + + local_rhs.AddElementVector(gravity_dofs, element_rhs); + } + + mfem::Vector true_rhs; + local_to_true(*f.gravityFluxFes, local_rhs, true_rhs); + + return true_rhs; } - if (compactification_transform != nullptr) { - compactification_transform->InvTransformPrimal(element_compactification); + 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); } - const mean_field::mapping::ElementDisplacementData displacement_data = - mean_field::mapping::ElementDisplacementDataFromElementVDofs( - displacement_element, element_displacement); + 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()); - const mean_field::mapping::ElementCompactificationData - compactification_data(compactification_element, - element_compactification); + mfem::Vector displacement_true; + displacement.GetTrueDofs(displacement_true); - const mean_field::mapping::ElementMappingData mapping_data{ - .displacement = displacement_data, - .compactification = compactification_data}; + mean_field::operators::PreparedMappedHDivMassOperator mass_operator(f, *f.domainMapperStateless); + mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true)); - mfem::Vector compactification_shape(compactification_element.GetDof()); + mfem::Vector difference(computed); + difference -= reference; - const mfem::IntegrationRule &rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + mfem::Vector difference_action; + mfem::Vector reference_action; - for (int q = 0; q < rule.GetNPoints(); ++q) { - const mfem::IntegrationPoint &point = rule.IntPoint(q); + 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); - transformation->SetIntPoint(&point); - compactification_element.CalcShape(point, compactification_shape); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - const double coordinate = - element_compactification * compactification_shape; - const int shell = get_shell(coordinate); + const double difference_energy = global_dot(reduced_difference, difference_action, communicator); + const double reference_energy = global_dot(reduced_reference, reference_action, communicator); - mfem::Vector reference_field(3); - mfem::Vector physical_field(3); - mfem::Vector physical_position(3); + REQUIRE(difference_energy >= -1.0e-12 * std::abs(reference_energy)); + REQUIRE(reference_energy > 0.0); - 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; + return std::sqrt(std::max(0.0, difference_energy) / reference_energy); } - const mfem::FiniteElement &displacement_element = - *f.displacementFes->GetFE(element_id); - const mfem::FiniteElement &compactification_element = - *f.compactificationFes->GetFE(element_id); + 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; - mfem::Array displacement_dofs; - mfem::Array compactification_dofs; + mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension()); - mfem::DofTransformation *displacement_transform = - f.displacementFes->GetElementVDofs(element_id, displacement_dofs); - mfem::DofTransformation *compactification_transform = - f.compactificationFes->GetElementDofs(element_id, - compactification_dofs); + 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; - mfem::Vector element_displacement; - mfem::Vector element_compactification; + for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); - displacement.GetSubVector(displacement_dofs, element_displacement); - f.compactificationCoordinate->GetSubVector(compactification_dofs, - element_compactification); + if (transformation->Attribute != vacuum_attribute) { + continue; + } - if (displacement_transform != nullptr) { - displacement_transform->InvTransformPrimal(element_displacement); + 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); + + 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; } - if (compactification_transform != nullptr) { - compactification_transform->InvTransformPrimal(element_compactification); + 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; } - - 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("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); +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.domainMapperStateless != 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.displacement = 0.0; + *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 numerical_solution = - mean_field::physics::solve_gravity_field(f, args, density, displacement); + const mean_field::physics::GravitySolution numerical_solution = + mean_field::physics::solve_gravity_field(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 numerical = measure_exterior_shells(f, numerical_solution, displacement, mass); - const ShellMeasurement projected = - measure_exterior_shells(f, projected_solution, displacement, mass); + const ShellMeasurement projected = measure_exterior_shells(f, projected_solution, displacement, mass); - const GravitationalEnergies energies = - compute_stellar_energies(f, density, numerical_solution, displacement); + const GravitationalEnergies energies = compute_stellar_energies(f, density, numerical_solution, displacement); - mfem::Vector numerical_gradient; - mfem::Vector numerical_potential; - mfem::Vector projected_gradient; - mfem::Vector projected_potential; + mfem::Vector numerical_gradient; + mfem::Vector numerical_potential; + mfem::Vector projected_gradient; + mfem::Vector projected_potential; - numerical_solution.gradPhi.GetTrueDofs(numerical_gradient); - numerical_solution.phi.GetTrueDofs(numerical_potential); + numerical_solution.gradPhi.GetTrueDofs(numerical_gradient); + numerical_solution.phi.GetTrueDofs(numerical_potential); - projected_solution.gradPhi.GetTrueDofs(projected_gradient); - projected_solution.phi.GetTrueDofs(projected_potential); + projected_solution.gradPhi.GetTrueDofs(projected_gradient); + projected_solution.phi.GetTrueDofs(projected_potential); - MPI_Comm communicator = f.gravityFluxFes->GetComm(); + MPI_Comm communicator = f.gravityFluxFes->GetComm(); - const double gradient_projection_gap = mapped_hdiv_relative_error( - f, displacement, numerical_gradient, projected_gradient); + const double gradient_projection_gap = + mapped_hdiv_relative_error(f, displacement, numerical_gradient, projected_gradient); - const double potential_projection_gap = global_relative_error( - numerical_potential, projected_potential, communicator); + const double potential_projection_gap = + global_relative_error(numerical_potential, projected_potential, 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 = 0.0; - double maximum_projected_tangential = 0.0; + 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; - std::ostringstream report; + std::ostringstream report; - for (int shell = 0; shell < shell_count; ++shell) { - const ShellMetrics &numerical_shell = numerical.shells[shell]; - const ShellMetrics &projected_shell = projected.shells[shell]; + for (int shell = 0; shell < shell_count; ++shell) { + const ShellMetrics &numerical_shell = numerical.shells[shell]; + const ShellMetrics &projected_shell = projected.shells[shell]; - maximum_numerical_potential_error = std::max( - maximum_numerical_potential_error, numerical_shell.potential_rms_error); + maximum_numerical_potential_error = + std::max(maximum_numerical_potential_error, numerical_shell.potential_rms_error); - maximum_projected_potential_error = std::max( - maximum_projected_potential_error, projected_shell.potential_rms_error); + maximum_projected_potential_error = + std::max(maximum_projected_potential_error, projected_shell.potential_rms_error); - maximum_numerical_radial_error = std::max(maximum_numerical_radial_error, - numerical_shell.radial_rms_error); + maximum_numerical_radial_error = std::max(maximum_numerical_radial_error, numerical_shell.radial_rms_error); - maximum_projected_radial_error = std::max(maximum_projected_radial_error, - projected_shell.radial_rms_error); + maximum_projected_radial_error = std::max(maximum_projected_radial_error, projected_shell.radial_rms_error); - maximum_numerical_tangential = - std::max(maximum_numerical_tangential, numerical_shell.tangential_rms); + maximum_numerical_tangential = std::max(maximum_numerical_tangential, numerical_shell.tangential_rms); - maximum_projected_tangential = - std::max(maximum_projected_tangential, projected_shell.tangential_rms); + maximum_projected_tangential = std::max(maximum_projected_tangential, projected_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'; - } + 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'; + } - const double analytic_energy = - -3.0 * mean_field::utils::G * mass * mass / (5.0 * radius); + const double analytic_energy = -3.0 * mean_field::utils::G * mass * mass / (5.0 * radius); - 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 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(report.str()); + INFO(report.str()); - 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("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); - REQUIRE(numerical.invalid_points == 0); - REQUIRE(projected.invalid_points == 0); - REQUIRE(energies.invalid_points == 0); + REQUIRE(numerical.invalid_points == 0); + REQUIRE(projected.invalid_points == 0); + REQUIRE(energies.invalid_points == 0); - for (int shell = 0; shell < shell_count; ++shell) { - REQUIRE(numerical.shells[shell].points > 0); - REQUIRE(projected.shells[shell].points > 0); - } + for (int shell = 0; shell < shell_count; ++shell) { + REQUIRE(numerical.shells[shell].points > 0); + REQUIRE(projected.shells[shell].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); + 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; + 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( "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); + 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.domainMapperStateless != 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); - }; - - 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); + 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::VectorFunctionCoefficient displacement_coefficient( - 3, displacement_function); - mfem::ParGridFunction displacement(f.displacementFes.get()); - displacement.ProjectCoefficient(displacement_coefficient); + mfem::FunctionCoefficient density_coefficient(density_function); + mfem::GridFunction density(f.densityFes.get()); + density.ProjectCoefficient(density_coefficient); - *f.displacement = displacement; + zero_vacuum_density(f, density); - 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(); + mean_field::analysis::conserve_mass(f, density, mass); - 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); + constexpr std::array amplitudes{0.0, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0}; - 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::array consistency_errors{}; + std::array normalized_quadrupoles{}; + std::array binding_energies{}; + std::array virial_energies{}; - 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); + std::ostringstream report; - binding_energies[index] = energies.binding; - virial_energies[index] = energies.virial; + for (std::size_t index = 0; index < amplitudes.size(); ++index) { + const double amplitude = amplitudes[index]; - consistency_errors[index] = std::abs(energies.binding - energies.virial) / - std::abs(energies.binding); + 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); - normalized_quadrupoles[index] = f.Q.FNorm() / (mass * radius * radius); + REQUIRE_THAT(x_scale * y_scale * z_scale, Catch::Matchers::WithinAbs(1.0, 1.0e-14)); - 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'; - } + auto displacement_function = [x_scale, y_scale, z_scale](const mfem::Vector &position, mfem::Vector &value) { + value.SetSize(3); - INFO(report.str()); + value(0) = (x_scale - 1.0) * position(0); + value(1) = (y_scale - 1.0) * position(1); + value(2) = (z_scale - 1.0) * position(2); + }; - for (std::size_t index = 1; index < amplitudes.size(); ++index) { - CHECK(normalized_quadrupoles[index] > normalized_quadrupoles[index - 1]); - } + mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); + mfem::ParGridFunction displacement(f.displacementFes.get()); + displacement.ProjectCoefficient(displacement_coefficient); - CHECK(consistency_errors[0] < 1.0e-5); - for (std::size_t index = 1; index < amplitudes.size(); ++index) { - CHECK(consistency_errors[index] < 5.0e-5); - } + *f.displacement = displacement; + + 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::solve_gravity_field(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); + } } diff --git a/tests/physics/polytropic_eos_characterization.cpp b/tests/physics/polytropic_eos_characterization.cpp new file mode 100644 index 0000000..7c996fe --- /dev/null +++ b/tests/physics/polytropic_eos_characterization.cpp @@ -0,0 +1,156 @@ +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace eos = mean_field::eos; + +TEST_CASE( + "Polytropic EOS Pressure To Specific Enthalpy Relation Is Characterized", + tags::polytropic_eos_characterization +) { + constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; + constexpr std::array polytropicConstants{0.25, 0.73, 2.0}; + constexpr std::array pressures{0.0, 1.0e-12, 1.0e-4, 0.3, 5.0}; + + for (const double polytropicIndex : polytropicIndices) { + for (const double polytropicConstant : polytropicConstants) { + const mean_field::eos::Polytrope equationOfState(polytropicIndex, polytropicConstant); + + for (const double pressure : pressures) { + CAPTURE(polytropicIndex, polytropicConstant, pressure); + + const double indexPlusOne = polytropicIndex + 1.0; + const double expectedEnthalpy = indexPlusOne * + std::pow(polytropicConstant, polytropicIndex / indexPlusOne) * + std::pow(pressure, 1.0 / indexPlusOne); + + const double enthalpy = + eos::evaluate(equationOfState, eos::PressureValue{pressure}) + .value(); + + if (pressure == 0.0) { + CHECK(enthalpy == 0.0); + } else { + CHECK_THAT(enthalpy, Catch::Matchers::WithinRel(expectedEnthalpy, 5.0e-14)); + + const double recoveredPressure = + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{enthalpy}) + .value(); + + CHECK_THAT(recoveredPressure, Catch::Matchers::WithinRel(pressure, 5.0e-13)); + } + } + } + } +} + +TEST_CASE( + "Polytropic EOS Domain Contract Covers Every Relation", + tags::polytropic_eos_characterization +) { + constexpr double infinity = std::numeric_limits::infinity(); + constexpr double quietNaN = std::numeric_limits::quiet_NaN(); + + for (const double invalidIndex : std::array{0.999, infinity, -infinity, quietNaN}) { + CAPTURE(invalidIndex); + CHECK_THROWS_AS(mean_field::eos::Polytrope(invalidIndex, 1.0), std::invalid_argument); + } + + for (const double invalidConstant : std::array{0.0, -0.1, infinity, -infinity, quietNaN}) { + CAPTURE(invalidConstant); + CHECK_THROWS_AS(mean_field::eos::Polytrope(3.0, invalidConstant), std::invalid_argument); + } + + const mean_field::eos::Polytrope equationOfState(3.0, 0.75); + + constexpr double negativeDensity = -0.1; + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::DensityValue{negativeDensity}), std::domain_error + ); + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::DensityValue{negativeDensity}), + std::domain_error + ); + CHECK_THROWS_AS( + (eos::partialDerivative( + equationOfState, eos::DensityValue{negativeDensity} + )), + std::domain_error + ); + + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::PressureValue{-0.1}), std::domain_error + ); + + constexpr double exteriorEnthalpy = -0.1; + CHECK( + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}).value() == + 0.0 + ); + CHECK( + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy}).value() == + 0.0 + ); + CHECK( + (eos::partialDerivative( + equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy} + ) + .value() == 0.0) + ); + CHECK( + (eos::partialDerivative( + equationOfState, eos::SpecificEnthalpyValue{exteriorEnthalpy} + ) + .value() == 0.0) + ); + + for (const double nonfiniteValue : std::array{infinity, -infinity, quietNaN}) { + CAPTURE(nonfiniteValue); + + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::DensityValue{nonfiniteValue}), + std::domain_error + ); + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::DensityValue{nonfiniteValue}), + std::domain_error + ); + CHECK_THROWS_AS( + (eos::partialDerivative( + equationOfState, eos::DensityValue{nonfiniteValue} + )), + std::domain_error + ); + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::PressureValue{nonfiniteValue}), + std::domain_error + ); + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}), + std::domain_error + ); + CHECK_THROWS_AS( + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue}), + std::domain_error + ); + CHECK_THROWS_AS( + (eos::partialDerivative( + equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue} + )), + std::domain_error + ); + CHECK_THROWS_AS( + (eos::partialDerivative( + equationOfState, eos::SpecificEnthalpyValue{nonfiniteValue} + )), + std::domain_error + ); + } +} diff --git a/tests/physics/polytropic_eos_relations.cpp b/tests/physics/polytropic_eos_relations.cpp new file mode 100644 index 0000000..3b9f10a --- /dev/null +++ b/tests/physics/polytropic_eos_relations.cpp @@ -0,0 +1,200 @@ +#include +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace { + namespace eos = mean_field::eos; + + template + concept HasAnyUnaryEquationOfStateConversion = + requires(const Candidate &candidate, const double value) { candidate.pressure_from_density(value); } || + requires(const Candidate &candidate, const double value) { candidate.pressure_from_enthalpy(value); } || + requires(const Candidate &candidate, const double value) { candidate.enthalpy_from_density(value); } || + requires(const Candidate &candidate, const double value) { candidate.enthalpy_from_pressure(value); } || + requires(const Candidate &candidate, const double value) { candidate.density_from_enthalpy(value); } || + requires(const Candidate &candidate, const double value) { + candidate.density_derivative_from_enthalpy(value); + } || + requires(const Candidate &candidate, const double value) { + candidate.pressure_derivative_from_enthalpy(value); + } || + requires(const Candidate &candidate, const double value) { candidate.pressure_derivative_from_density(value); }; +} // namespace + +TEST_CASE( + "Polytropic EOS Declares Its Thermodynamic Relation Contract", + tags::polytropic_eos_relation_contract +) { + using Polytrope = eos::Polytrope; + + STATIC_CHECK(eos::EquationOfStateModel); + STATIC_CHECK_FALSE(std::is_polymorphic_v); + STATIC_CHECK_FALSE(HasAnyUnaryEquationOfStateConversion); + STATIC_CHECK(Polytrope::Relations::size == 5); + + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK(eos::SupportsRelation); + STATIC_CHECK(eos::SupportsRelation); + + STATIC_CHECK(eos::SupportsPartialDerivative); + STATIC_CHECK( + eos::SupportsPartialDerivative + ); + STATIC_CHECK( + eos::SupportsPartialDerivative + ); + + STATIC_CHECK_FALSE( + eos::SupportsPartialDerivative + ); + STATIC_CHECK_FALSE( + eos::SupportsPartialDerivative + ); +} + +TEST_CASE( + "Polytropic EOS Typed Relations Preserve Analytic Values", + tags::polytropic_eos_characterization +) { + constexpr std::array polytropicIndices{1.0, 1.5, 3.0}; + constexpr std::array polytropicConstants{0.25, 0.73}; + constexpr std::array densities{0.0, 1.0e-6, 0.2, 2.0}; + constexpr std::array specificEnthalpies{-0.3, 0.0, 0.2, 1.7}; + constexpr std::array pressures{0.0, 1.0e-8, 0.3, 4.0}; + + for (const double polytropicIndex : polytropicIndices) { + for (const double polytropicConstant : polytropicConstants) { + const eos::Polytrope equationOfState(polytropicIndex, polytropicConstant); + + for (const double density : densities) { + CAPTURE(polytropicIndex, polytropicConstant, density); + + const double expectedPressure = polytropicConstant * std::pow(density, 1.0 + 1.0 / polytropicIndex); + const double expectedSpecificEnthalpy = + (polytropicIndex + 1.0) * polytropicConstant * std::pow(density, 1.0 / polytropicIndex); + + CHECK( + eos::evaluate(equationOfState, eos::DensityValue{density}).value() == + expectedPressure + ); + + CHECK( + eos::evaluate(equationOfState, eos::DensityValue{density}) + .value() == expectedSpecificEnthalpy + ); + + CHECK_THAT( + (eos::partialDerivative( + equationOfState, eos::DensityValue{density} + ) + .value()), + Catch::Matchers::WithinRel( + density == 0.0 ? 0.0 : expectedSpecificEnthalpy / polytropicIndex, 2.0e-15 + ) + ); + } + + for (const double specificEnthalpy : specificEnthalpies) { + CAPTURE(polytropicIndex, polytropicConstant, specificEnthalpy); + + const double expectedDensity = + specificEnthalpy <= 0.0 + ? 0.0 + : std::pow(specificEnthalpy / ((polytropicIndex + 1.0) * polytropicConstant), polytropicIndex); + + CHECK( + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy}) + .value() == expectedDensity + ); + + CHECK( + eos::evaluate( + equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy} + ) + .value() == + (specificEnthalpy <= 0.0 ? 0.0 : expectedDensity * specificEnthalpy / (polytropicIndex + 1.0)) + ); + + CHECK( + (eos::partialDerivative( + equationOfState, eos::SpecificEnthalpyValue{specificEnthalpy} + ) + .value() == expectedDensity) + ); + } + + for (const double pressure : pressures) { + CAPTURE(polytropicIndex, polytropicConstant, pressure); + + const double indexPlusOne = polytropicIndex + 1.0; + const double expectedSpecificEnthalpy = indexPlusOne * + std::pow(polytropicConstant, polytropicIndex / indexPlusOne) * + std::pow(pressure, 1.0 / indexPlusOne); + + CHECK( + eos::evaluate(equationOfState, eos::PressureValue{pressure}) + .value() == expectedSpecificEnthalpy + ); + } + } + } +} + +TEST_CASE( + "Typed Polytropic EOS Preserves Domain And Exterior Semantics", + tags::polytropic_eos_relation_contract +) { + const eos::Polytrope equationOfState(3.0, 0.75); + + try { + static_cast(eos::evaluate(equationOfState, eos::DensityValue{-0.1})); + FAIL("A negative density must be rejected."); + } catch (const eos::EvaluationError &error) { + CHECK(error.code() == eos::EvaluationErrorCode::outside_domain); + } + + try { + static_cast(eos::evaluate( + equationOfState, eos::PressureValue{std::numeric_limits::quiet_NaN()} + )); + FAIL("A nonfinite pressure must be rejected."); + } catch (const eos::EvaluationError &error) { + CHECK(error.code() == eos::EvaluationErrorCode::nonfinite_input); + } + + constexpr double exteriorSpecificEnthalpy = -0.3; + + CHECK( + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy}) + .value() == 0.0 + ); + + CHECK( + eos::evaluate(equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy}) + .value() == 0.0 + ); + + CHECK( + (eos::partialDerivative( + equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy} + ) + .value() == 0.0) + ); + + CHECK( + (eos::partialDerivative( + equationOfState, eos::SpecificEnthalpyValue{exteriorSpecificEnthalpy} + ) + .value() == 0.0) + ); +} diff --git a/tests/surface/constant_surface_compilation.cpp b/tests/surface/constant_surface_compilation.cpp new file mode 100644 index 0000000..4660250 --- /dev/null +++ b/tests/surface/constant_surface_compilation.cpp @@ -0,0 +1,329 @@ +#include +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace { + namespace eos = mean_field::eos; + namespace field = mean_field::field; + namespace surface = mean_field::surface; + + struct Entropy final : eos::ThermodynamicQuantity { + static constexpr std::string_view identifier = "entropy"; + }; + + struct ElectronFraction final : eos::ThermodynamicQuantity { + static constexpr std::string_view identifier = "electron_fraction"; + }; + + struct EntropyField final { + static constexpr std::string_view name = "entropy"; + }; + + struct ElectronFractionField final { + static constexpr std::string_view name = "electron_fraction"; + }; + + using SpecificEnthalpyFromPressureEntropyAndElectronFraction = + eos::Relation; + + class GeneralStellarMatterEquationOfState final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + SpecificEnthalpyFromPressureEntropyAndElectronFraction, + const eos::PressureValue pressure, + const eos::QuantityValue entropy, + const eos::QuantityValue electronFraction + ) const noexcept { + return eos::SpecificEnthalpyValue{ + 2.0 * pressure.value() + 3.0 * entropy.value() + 5.0 * electronFraction.value() + }; + } + + [[nodiscard]] constexpr eos::PartialDerivative< + eos::quantity::SpecificEnthalpy, + Entropy> + partialDerivative( + SpecificEnthalpyFromPressureEntropyAndElectronFraction, + eos::WithRespectTo, + eos::PressureValue, + eos::QuantityValue, + eos::QuantityValue + ) const noexcept { + return eos::PartialDerivative{3.0}; + } + + [[nodiscard]] constexpr eos::PartialDerivative< + eos::quantity::SpecificEnthalpy, + ElectronFraction> + partialDerivative( + SpecificEnthalpyFromPressureEntropyAndElectronFraction, + eos::WithRespectTo, + eos::PressureValue, + eos::QuantityValue, + eos::QuantityValue + ) const noexcept { + return eos::PartialDerivative{5.0}; + } + }; + + class GeneralEquationOfStateWithoutElectronFractionPartial final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + SpecificEnthalpyFromPressureEntropyAndElectronFraction, + const eos::PressureValue pressure, + const eos::QuantityValue entropy, + const eos::QuantityValue electronFraction + ) const noexcept { + return eos::SpecificEnthalpyValue{pressure.value() + entropy.value() + electronFraction.value()}; + } + + [[nodiscard]] constexpr eos::PartialDerivative< + eos::quantity::SpecificEnthalpy, + Entropy> + partialDerivative( + SpecificEnthalpyFromPressureEntropyAndElectronFraction, + eos::WithRespectTo, + eos::PressureValue, + eos::QuantityValue, + eos::QuantityValue + ) const noexcept { + return eos::PartialDerivative{1.0}; + } + }; + + using SpecificEnthalpyFromPressureAndEntropy = + eos::Relation; + + class AmbiguousSurfaceEquationOfState final { + public: + using Relations = + eos::RelationCatalog; + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + eos::SpecificEnthalpyFromPressure, + const eos::PressureValue pressure + ) const noexcept { + return eos::SpecificEnthalpyValue{pressure.value()}; + } + + [[nodiscard]] constexpr eos::SpecificEnthalpyValue evaluate( + SpecificEnthalpyFromPressureAndEntropy, + const eos::PressureValue pressure, + const eos::QuantityValue entropy + ) const noexcept { + return eos::SpecificEnthalpyValue{pressure.value() + entropy.value()}; + } + + [[nodiscard]] constexpr eos::PartialDerivative< + eos::quantity::SpecificEnthalpy, + Entropy> + partialDerivative( + SpecificEnthalpyFromPressureAndEntropy, + eos::WithRespectTo, + eos::PressureValue, + eos::QuantityValue + ) const noexcept { + return eos::PartialDerivative{1.0}; + } + }; + + class DensityOnlyEquationOfState final { + public: + using Relations = eos::RelationCatalog; + + [[nodiscard]] constexpr eos::PressureValue evaluate( + eos::PressureFromDensity, + const eos::DensityValue density + ) const noexcept { + return eos::PressureValue{density.value()}; + } + }; + + using GeneralSurfaceFormulation = surface::SurfaceConstraintFormulation< + eos::quantity::SpecificEnthalpy, + field::Enthalpy, + surface::SurfaceStateBindings< + surface::SurfaceStateBinding, + surface::SurfaceStateBinding, + surface::SurfaceStateBinding>>; + + struct PolytropicSurfaceState final { + double specificEnthalpy; + + [[nodiscard]] eos::SpecificEnthalpyValue value(eos::quantity::SpecificEnthalpy) const noexcept { + return eos::SpecificEnthalpyValue{specificEnthalpy}; + } + }; + + struct GeneralSurfaceState final { + double specificEnthalpy; + double entropy; + double electronFraction; + + [[nodiscard]] eos::SpecificEnthalpyValue value(eos::quantity::SpecificEnthalpy) const noexcept { + return eos::SpecificEnthalpyValue{specificEnthalpy}; + } + + [[nodiscard]] eos::QuantityValue value(Entropy) const noexcept { + return eos::QuantityValue{entropy}; + } + + [[nodiscard]] eos::QuantityValue value(ElectronFraction) const noexcept { + return eos::QuantityValue{electronFraction}; + } + }; + + template + concept HasTargetEnthalpy = requires(const Candidate &candidate) { candidate.targetEnthalpy; }; +} // namespace + +TEST_CASE( + "Constant Pressure Surface Prescribes Only A Pressure Quantity", + tags::surface_prescription_type_contract +) { + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::constructible_from); + STATIC_CHECK_FALSE(std::constructible_from); + STATIC_CHECK_FALSE(std::constructible_from); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK(std::is_trivially_copyable_v); + + const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.03125}}; + + CHECK(pressureSurface.targetPressure() == eos::PressureValue{0.03125}); + CHECK(pressureSurface.descriptor().targetPressure == 0.03125); + + CHECK_THROWS_AS(surface::ConstantPressureSurface{eos::PressureValue{-0.1}}, std::invalid_argument); + CHECK_THROWS_AS( + surface::ConstantPressureSurface{eos::PressureValue{std::numeric_limits::infinity()}}, + std::invalid_argument + ); +} + +TEST_CASE( + "Polytropic EOS Resolves Constant Surface Pressure Through Its Enthalpy Relation", + tags::surface_constraint_compilation +) { + using Formulation = surface::BarotropicSurfaceFormulation; + + STATIC_CHECK(surface::PressureSurfaceCompilable); + STATIC_CHECK_FALSE(surface::PressureSurfaceCompilable); + + const eos::Polytrope equationOfState(3.0, 0.25); + const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.03125}}; + const auto constraint = surface::compilePressureSurfaceConstraint(pressureSurface, equationOfState); + + using Constraint = std::remove_cvref_t; + using Dependencies = Constraint::SurfaceDependencies; + + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as>); + STATIC_CHECK_FALSE(HasTargetEnthalpy); + + const double requiredSpecificEnthalpy = + eos::evaluate(equationOfState, pressureSurface.targetPressure()).value(); + const PolytropicSurfaceState state{requiredSpecificEnthalpy}; + const PolytropicSurfaceState variation{-0.19}; + + CHECK(constraint.targetPressure() == eos::PressureValue{0.03125}); + CHECK(constraint.residual(state) == 0.0); + CHECK(constraint.jacobianAction(state, variation) == -0.19); + + const auto runtimeDependencies = constraint.runtimeDependencies(); + REQUIRE(runtimeDependencies.stateFields.size() == 1); + CHECK(runtimeDependencies.residualRowField == surface::surfaceFieldId); + CHECK(runtimeDependencies.stateFields[0] == surface::surfaceFieldId); +} + +TEST_CASE( + "General EOS Resolves Constant Surface Pressure With Local Composition", + tags::surface_constraint_compilation +) { + STATIC_CHECK(surface::PressureSurfaceCompilable); + STATIC_CHECK_FALSE( + surface::PressureSurfaceCompilable + ); + STATIC_CHECK_FALSE( + surface::PressureSurfaceCompilable< + GeneralSurfaceFormulation, GeneralEquationOfStateWithoutElectronFractionPartial> + ); + STATIC_CHECK_FALSE(surface::PressureSurfaceCompilable); + + const GeneralStellarMatterEquationOfState equationOfState; + const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.4}}; + const auto constraint = + surface::compilePressureSurfaceConstraint(pressureSurface, equationOfState); + + using Constraint = std::remove_cvref_t; + using Dependencies = Constraint::SurfaceDependencies; + + STATIC_CHECK(std::same_as); + STATIC_CHECK( + std::same_as< + Dependencies::StateFieldTypes, field::TypeList> + ); + + constexpr GeneralSurfaceState firstSurface{ + .specificEnthalpy = 2.0 * 0.4 + 3.0 * 0.2 + 5.0 * 0.1, .entropy = 0.2, .electronFraction = 0.1 + }; + constexpr GeneralSurfaceState secondSurface{ + .specificEnthalpy = 2.0 * 0.4 + 3.0 * 0.3 + 5.0 * 0.1, .entropy = 0.3, .electronFraction = 0.1 + }; + + CHECK(firstSurface.specificEnthalpy != secondSurface.specificEnthalpy); + CHECK(constraint.residual(firstSurface) == 0.0); + CHECK(constraint.residual(secondSurface) == 0.0); + + const auto runtimeDependencies = constraint.runtimeDependencies(); + REQUIRE(runtimeDependencies.stateFields.size() == 3); + CHECK(runtimeDependencies.stateFields[0] == surface::surfaceFieldId); + CHECK(runtimeDependencies.stateFields[1] == surface::surfaceFieldId); + CHECK(runtimeDependencies.stateFields[2] == surface::surfaceFieldId); +} + +TEST_CASE( + "General EOS Pressure Surface Jacobian Includes Every Local State Dependency", + tags::surface_constraint_jacobian +) { + const GeneralStellarMatterEquationOfState equationOfState; + const surface::ConstantPressureSurface pressureSurface{eos::PressureValue{0.4}}; + const auto constraint = + surface::compilePressureSurfaceConstraint(pressureSurface, equationOfState); + + constexpr GeneralSurfaceState state{.specificEnthalpy = 1.7, .entropy = 0.2, .electronFraction = 0.1}; + constexpr GeneralSurfaceState variation{.specificEnthalpy = 0.7, .entropy = -0.2, .electronFraction = 0.05}; + constexpr double step = 1.0e-7; + + const GeneralSurfaceState forward{ + .specificEnthalpy = state.specificEnthalpy + step * variation.specificEnthalpy, + .entropy = state.entropy + step * variation.entropy, + .electronFraction = state.electronFraction + step * variation.electronFraction + }; + const GeneralSurfaceState backward{ + .specificEnthalpy = state.specificEnthalpy - step * variation.specificEnthalpy, + .entropy = state.entropy - step * variation.entropy, + .electronFraction = state.electronFraction - step * variation.electronFraction + }; + + const double finiteDifference = (constraint.residual(forward) - constraint.residual(backward)) / (2.0 * step); + const double jacobianAction = constraint.jacobianAction(state, variation); + + CHECK(jacobianAction == variation.specificEnthalpy - 3.0 * variation.entropy - 5.0 * variation.electronFraction); + CHECK_THAT(finiteDifference, Catch::Matchers::WithinAbs(jacobianAction, 2.0e-9)); +} diff --git a/tests/surface/isobaric.cpp b/tests/surface/isobaric.cpp deleted file mode 100644 index 4a6dc15..0000000 --- a/tests/surface/isobaric.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include -#include -#include - -#include - -import mean_field; -import test_helpers; - -TEST_CASE( - "Isobaric Surface Resolves Zero Pressure To Zero Enthalpy", - tags::barotrope &tags::unit &tags::surface -) { - const mean_field::eos::Polytrope equationOfState(3.0, 0.25); - - const mean_field::surface::Isobaric surface; - - const mean_field::surface::ResolvedSurfaceCondition resolved = surface.resolve(equationOfState); - - CHECK(surface.targetPressure() == 0.0); - CHECK(resolved.targetEnthalpy == 0.0); - CHECK(resolved.residual(0.0) == 0.0); - CHECK(resolved.residual(0.37) == 0.37); - CHECK(resolved.jacobianAction(-0.19) == -0.19); -} - -TEST_CASE( - "Isobaric Surface Resolves Positive Pressure Through The EOS", - tags::barotrope &tags::unit &tags::surface -) { - const mean_field::eos::Polytrope equationOfState(3.0, 0.25); - - constexpr double targetPressure = 0.03125; - - const mean_field::surface::Isobaric surface(targetPressure); - - const mean_field::surface::ResolvedSurfaceCondition resolved = surface.resolve(equationOfState); - - const double recoveredPressure = equationOfState.pressure_from_enthalpy(resolved.targetEnthalpy); - - INFO("Resolved surface enthalpy = " << resolved.targetEnthalpy); - INFO("Recovered surface pressure = " << recoveredPressure); - - CHECK(resolved.targetEnthalpy > 0.0); - CHECK(std::abs(recoveredPressure - targetPressure) < 64.0 * std::numeric_limits::epsilon()); - CHECK(resolved.residual(resolved.targetEnthalpy) == 0.0); -} - -TEST_CASE( - "Isobaric Surface Rejects Invalid Pressure Targets", - tags::barotrope &tags::unit &tags::surface -) { - CHECK_THROWS_AS(mean_field::surface::Isobaric(-1.0), std::invalid_argument); - - CHECK_THROWS_AS(mean_field::surface::Isobaric(std::numeric_limits::infinity()), std::invalid_argument); - - CHECK_THROWS_AS(mean_field::surface::Isobaric(std::numeric_limits::quiet_NaN()), std::invalid_argument); -} - -TEST_CASE( - "Surface Base Dispatch Preserves The Isobaric Prescription", - tags::barotrope &tags::unit &tags::surface -) { - const mean_field::eos::Polytrope equationOfState(3.0, 0.25); - - const mean_field::surface::Isobaric isobaric(0.02); - - const mean_field::surface::SurfaceBase &surface = isobaric; - - surface.validate(equationOfState); - - const mean_field::surface::ResolvedSurfaceCondition resolved = surface.resolve(equationOfState); - - CHECK(resolved.targetEnthalpy > 0.0); - CHECK(resolved.residual(resolved.targetEnthalpy) == 0.0); -} diff --git a/tests/test_helpers.cppm b/tests/test_helpers.cppm index 3d28352..868d949 100644 --- a/tests/test_helpers.cppm +++ b/tests/test_helpers.cppm @@ -13,456 +13,469 @@ export module test_helpers; import mean_field; template struct Tag { - std::array chars{}; + std::array chars{}; - // ReSharper disable once CppNonExplicitConvertingConstructor - consteval Tag(std::array arr) : chars(arr) {} + // ReSharper disable once CppNonExplicitConvertingConstructor + consteval Tag( + std::array< + char, + N> 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 -consteval auto sub_tag(const Tag &parent, const char (&str)[M]) { - return parent & make_tag(str); +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); } 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 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 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); + } } - } - mfem::PWConstCoefficient coefficient(attribute_values); - mfem::ParGridFunction density(f.densityFes.get()); - density.ProjectCoefficient(coefficient); + 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::Vector density_true; - density.GetTrueDofs(density_true); - return density_true; -} + inline mfem::Vector make_deterministic_vector( + const int size, + const double phase = 0.0 + ) { + mfem::Vector vector(size); -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."); + 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); + } - mfem::Vector combination(first); - combination *= first_scale; - combination.Add(second_scale, second); - return combination; -} + return vector; + } -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 mfem::Vector make_displacement( + const mean_field::fem::FEM &f, + const double scale + ) { + mfem::ParGridFunction displacement(f.displacementFes.get()); -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."); + 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)); + }; - 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::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), displacement_function); + displacement.ProjectCoefficient(coefficient); -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 displacement_true; + displacement.GetTrueDofs(displacement_true); + return displacement_true; + } - mfem::Vector difference(computed); - difference -= reference; + 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; - return global_norm(difference, communicator) / - std::max(global_norm(reference, communicator), - std::numeric_limits::epsilon()); -} + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; -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()); -} + 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(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()); + } } // 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; -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)); -} + 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) + ); + } -inline constexpr int vacuum_material_attribute = - DomainSchema::template material_attribute< - mean_field::utils::domain::Vacuum>(); + inline constexpr int vacuum_material_attribute = + DomainSchema::template material_attribute(); -template -inline mean_field::field::FieldDofMap -make_map(const mfem::ParFiniteElementSpace &finiteElementSpace) { - return mean_field::field::make_field_dof_map( - finiteElementSpace); -} + template + inline mean_field::field::FieldDofMap make_map(const mfem::ParFiniteElementSpace &finiteElementSpace) { + return mean_field::field::make_field_dof_map(finiteElementSpace); + } -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); -} + 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 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)); -} + 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)); + } -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); + 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); - mfem::Vector enthalpyTrue(enthalpyMap.full_size()); - mfem::Vector gravityPotentialTrue(gravityPotentialMap.full_size()); - mfem::Vector displacementTrue(displacementMap.full_size()); - mfem::Vector residualTrue; + mfem::Vector enthalpyTrue(enthalpyMap.full_size()); + mfem::Vector gravityPotentialTrue(gravityPotentialMap.full_size()); + mfem::Vector displacementTrue(displacementMap.full_size()); + mfem::Vector residualTrue; - enthalpyMap.scatter(enthalpy, enthalpyTrue); - gravityPotentialMap.scatter(gravityPotential, gravityPotentialTrue); - displacementMap.scatter(displacement, displacementTrue); + 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); + 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); -} + 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_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 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 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 mapping_evaluator = - mapping & make_tag("grid_function_evaluator"); -inline constexpr auto mapping_evaluator_unit = mapping_evaluator & unit; + 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_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; + // 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_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; + 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 polytropic_eos_characterization = + barotrope & unit & make_tag("eos") & make_tag("characterization"); + inline constexpr auto polytropic_eos_relation_contract = + barotrope & unit & make_tag("eos") & make_tag("relation_contract"); + inline constexpr auto polytropic_eos_compatibility = barotrope & unit & make_tag("eos") & make_tag("compatibility"); + inline constexpr auto equation_of_state = physics & make_tag("eos"); + inline constexpr auto equation_of_state_type_system = equation_of_state & unit & make_tag("type_system"); + inline constexpr auto equation_of_state_quantity_types = equation_of_state_type_system & make_tag("quantity_types"); + inline constexpr auto equation_of_state_relation_contract = + equation_of_state_type_system & make_tag("relation_contract"); + inline constexpr auto equation_of_state_runtime_view = equation_of_state & unit & make_tag("runtime_view"); + inline constexpr auto equation_of_state_runtime_contract = + equation_of_state_runtime_view & make_tag("relation_contract"); + inline constexpr auto equation_of_state_runtime_compatibility = + equation_of_state_runtime_view & make_tag("compatibility"); + inline constexpr auto equation_of_state_consumer_contract = + equation_of_state & unit & make_tag("consumer_contract"); + inline constexpr auto barotropic_closure_equation_of_state_contract = + equation_of_state_consumer_contract & make_tag("barotropic_closure"); + inline constexpr auto pressure_force_equation_of_state_contract = + equation_of_state_consumer_contract & make_tag("pressure_force"); + inline constexpr auto structure_seed_equation_of_state_contract = + equation_of_state_consumer_contract & make_tag("structure_seed"); + inline constexpr auto stellar_model_type_contract = barotrope & model & unit & make_tag("type_contract"); + inline constexpr auto stellar_model_runtime_view = barotrope & model & unit & make_tag("runtime_view"); + inline constexpr auto surface_prescription_type_contract = + surface & physics & unit & make_tag("prescription") & make_tag("type_contract"); + inline constexpr auto surface_constraint_compilation = + surface & physics & unit & make_tag("constraint_compilation"); + inline constexpr auto surface_constraint_jacobian = surface_constraint_compilation & jacobian; + inline constexpr auto surface_constraint_lifetime = surface & model & unit & make_tag("constraint_lifetime"); + inline constexpr auto surface_boundary_dof_topology = + surface & field_dof & integration & make_tag("boundary_topology"); + inline constexpr auto surface_row_replacement = + surface & barotrope_prepared & integration & make_tag("row_replacement"); + inline constexpr auto translational_centering = geometry & solver & make_tag("translational_centering"); + inline constexpr auto translational_centering_topology = + translational_centering & field_dof & integration & make_tag("point_topology"); + inline constexpr auto translational_centering_enforcement = + translational_centering & barotrope_prepared & integration & make_tag("row_replacement"); + 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_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; + 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/test_main.cpp b/tests/test_main.cpp index e03270e..4e44ba1 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -223,6 +224,7 @@ class CheckReporter : public Catch::StreamingReporterBase { bool passed; std::size_t assertionsPassed; std::size_t assertionsFailed; + double durationSeconds; std::vector failureMessages; std::vector infoMessages; }; @@ -231,6 +233,7 @@ class CheckReporter : public Catch::StreamingReporterBase { std::vector m_currentInfos; std::unordered_set m_currentInfoSequences; std::vector m_testRunData; + std::chrono::time_point m_testStartTime; void captureInfoMessages(Catch::AssertionStats const &assertionStats) { for (auto const &message : assertionStats.infoMessages) { @@ -253,9 +256,8 @@ public: } static std::string getDescription() { - return "Console reporter with wrapping, tags, and collapsible HTML " - "export " - "with ANSI color rendering."; + return "Console reporter with wrapping, tags, live test progress, and collapsible HTML " + "export with ANSI color rendering."; } void testRunStarting(Catch::TestRunInfo const &_testRunInfo) override { @@ -263,8 +265,20 @@ public: std::cout << '\n'; std::cout << std::left << std::setw(85) << "Test Case Name" - << "Status " << std::right << std::setw(8) << "Passed" << std::setw(8) << "Failed" << '\n'; - std::cout << std::string(121, '-') << '\n'; + << "Status " << std::right << std::setw(8) << "Passed" << std::setw(8) << "Failed" << std::setw(12) + << "Time (s)" << '\n'; + std::cout << std::string(133, '-') << '\n'; + } + + void testCaseStarting(Catch::TestCaseInfo const &testInfo) override { + StreamingReporterBase::testCaseStarting(testInfo); + + m_testStartTime = std::chrono::steady_clock::now(); + std::string name = testInfo.name; + auto wrappedName = wrapText(name, 83); + + // Print progress line, \r to overwrite later, \033[K to clear till end of line + std::cout << "\r\033[K" << std::left << std::setw(85) << (wrappedName[0] + " ...") << std::flush; } void assertionEnded(Catch::AssertionStats const &assertionStats) override { @@ -300,14 +314,20 @@ public: void testCaseEnded(Catch::TestCaseStats const &stats) override { StreamingReporterBase::testCaseEnded(stats); - bool passed = stats.totals.assertions.allPassed(); - std::string mark = passed ? "\033[32m✓\033[0m" : "\033[31m✗\033[0m"; + auto endTime = std::chrono::steady_clock::now(); + std::chrono::duration elapsed = endTime - m_testStartTime; + double duration_s = elapsed.count(); - std::string name = stats.testInfo->name; - auto wrappedName = wrapText(name, 83); + bool passed = stats.totals.assertions.allPassed(); + std::string mark = passed ? "\033[32m✓\033[0m" : "\033[31m✗\033[0m"; - std::cout << std::left << std::setw(85) << wrappedName[0] << mark << " " << std::right << std::setw(8) - << stats.totals.assertions.passed << std::setw(8) << stats.totals.assertions.failed << '\n'; + std::string name = stats.testInfo->name; + auto wrappedName = wrapText(name, 83); + + // Overwrite the loading line with the actual result + std::cout << "\r\033[K" << std::left << std::setw(85) << wrappedName[0] << mark << " " << std::right + << std::setw(8) << stats.totals.assertions.passed << std::setw(8) << stats.totals.assertions.failed + << std::setw(11) << std::fixed << std::setprecision(3) << duration_s << "s\n"; for (size_t i = 1; i < wrappedName.size(); ++i) { std::cout << " \033[90m↳ \033[0m" // Dim indent arrow @@ -327,12 +347,12 @@ public: for (auto const &failure : m_currentFailures) { std::cout << failure << '\n'; } - std::cout << std::string(121, '-') << '\n'; + std::cout << std::string(133, '-') << '\n'; } m_testRunData.push_back( - {name, tagsStr, passed, stats.totals.assertions.passed, stats.totals.assertions.failed, m_currentFailures, - m_currentInfos} + {name, tagsStr, passed, stats.totals.assertions.passed, stats.totals.assertions.failed, duration_s, + m_currentFailures, m_currentInfos} ); m_currentFailures.clear(); @@ -343,7 +363,7 @@ public: void testRunEnded(Catch::TestRunStats const &_testRunStats) override { StreamingReporterBase::testRunEnded(_testRunStats); - std::cout << std::string(121, '=') << '\n'; + std::cout << std::string(133, '=') << '\n'; auto const &tc = _testRunStats.totals.testCases; auto const &as = _testRunStats.totals.assertions; @@ -444,7 +464,9 @@ private: html << " \n"; html << "
\n"; html << " ✓ " << test.assertionsPassed << " | "; - html << " ✗ " << test.assertionsFailed << "\n"; + html << " ✗ " << test.assertionsFailed << " | "; + html << " ⌛ " << std::fixed << std::setprecision(3) + << test.durationSeconds << "s\n"; html << "
\n"; html << " \n"; @@ -589,4 +611,4 @@ int main( test_utils::set_args(std::move(test_args)); return session.run(); -} +} \ No newline at end of file diff --git a/tests/utils/domain.cpp b/tests/utils/domain.cpp index cd56e42..baa4c9e 100644 --- a/tests/utils/domain.cpp +++ b/tests/utils/domain.cpp @@ -12,1232 +12,1162 @@ 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 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 vertex_id( + const int xElementCount, + const int x, + const int y + ) { + return y * (xElementCount + 1) + 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_index( + const int xElementCount, + const int x, + const int y + ) { + return y * xElementCount + x; } - } -} -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}); + [[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))); } - } - /* - * 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 < + 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} + ); + } + } } - } - /* - * 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}); + 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} + ); + } + } } - } - /* - * 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]] + 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; } - } -} -[[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); + [[nodiscard]] + std::vector make_layered_attributes() { + constexpr int xElementCount = 5; + constexpr int yElementCount = 5; - mfem::Mesh mesh(2, (xElementCount + 1) * (yElementCount + 1), - xElementCount * yElementCount, - static_cast(boundaryEdges.size()), 2); + std::vector attributes(xElementCount * yElementCount, 3); - 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 = 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) { - const int lowerLeft = vertex_id(xElementCount, x, 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; - const int lowerRight = vertex_id(xElementCount, x + 1, y); + const std::vector attributes = make_layered_attributes(); - const int upperRight = vertex_id(xElementCount, x + 1, y + 1); + std::vector boundaries; - const int upperLeft = vertex_id(xElementCount, x, y + 1); + const auto isStellar = [](const int materialId) { return materialId == 1 || materialId == 2; }; - mesh.AddQuad(lowerLeft, lowerRight, upperRight, upperLeft, - cell_attribute(attributes, xElementCount, x, y)); + 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); } - } - for (const BoundaryEdge &boundary : boundaryEdges) { - mesh.AddBdrSegment(boundary.firstVertexId, boundary.secondVertexId, - boundary.attribute); - } + template void check_schema_is_valid(const mfem::Mesh &mesh) { + const auto validation = mean_field::utils::domain::validate_schema(mesh); - mesh.FinalizeTopology(false); - mesh.Finalize(false, false); + CHECK(validation.relationResults.size() == SchemaT::relationCount); - REQUIRE(mesh.GetNBE() == static_cast(boundaryEdges.size())); + for (const auto &relationResult : validation.relationResults) { + INFO("Relation index = " << relationResult.relationIndex); - return mesh; -} + INFO("Relation name = " << relationResult.relationName); -[[nodiscard]] -std::vector make_layered_attributes() { - constexpr int xElementCount = 5; - constexpr int yElementCount = 5; + INFO("Failure enum = " << static_cast(relationResult.result.failure)); - std::vector attributes(xElementCount * yElementCount, 3); + CHECK(relationResult.valid()); + } - for (int y = 1; y <= 3; ++y) { - for (int x = 1; x <= 3; ++x) { - attributes[static_cast(cell_index(xElementCount, x, y))] = 2; + CHECK(validation.valid()); } - } - attributes[static_cast(cell_index(xElementCount, 2, 2))] = 1; + 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>>; - return attributes; -} + [[nodiscard]] + stroid::config::MeshConfig make_stroid_config( + const int refinementLevels, + const int order, + const double flattening + ) { + stroid::config::MeshConfig config; -[[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.refinement_levels = refinementLevels; - const std::vector attributes = make_layered_attributes(); + config.order = order; - std::vector boundaries; + config.include_external_domain = true; - const auto isStellar = [](const int materialId) { - return materialId == 1 || materialId == 2; - }; + config.r_core = 0.25; - const auto isVacuum = [](const int materialId) { return materialId == 3; }; + config.r_star = 1.0; - if (includeStellarSurface) { - append_interface_boundaries(boundaries, attributes, xElementCount, - yElementCount, isStellar, isVacuum, - stellarSurfaceAttribute); - } + config.r_infinity = 4.0; - if (includeInfinitySurface) { - append_exterior_boundaries(boundaries, attributes, xElementCount, - yElementCount, isVacuum, - infinitySurfaceAttribute); - } + config.flattening = flattening; - return make_grid_mesh(xElementCount, yElementCount, attributes, boundaries); -} + config.core_id = 1; -template void check_schema_is_valid(const mfem::Mesh &mesh) { - const auto validation = - mean_field::utils::domain::validate_schema(mesh); + config.envelope_id = 2; - CHECK(validation.relationResults.size() == SchemaT::relationCount); + config.vacuum_id = 3; - for (const auto &relationResult : validation.relationResults) { - INFO("Relation index = " << relationResult.relationIndex); + config.surface_bdr_id = 1; - INFO("Relation name = " << relationResult.relationName); + config.inf_bdr_id = 2; - INFO("Failure enum = " << static_cast(relationResult.result.failure)); + config.optimization_methods = stroid::config::OptimizationMethods{.tmop = false, .smoothstep = true}; - 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; -} + 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< - mean_field::utils::domain::Stellar>); + STATIC_REQUIRE(mean_field::utils::domain::IsDomainSet); - 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< - mean_field::utils::domain::Stellar>); + STATIC_REQUIRE(mean_field::utils::domain::IsDomainOrSet); - 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>); + STATIC_REQUIRE(mean_field::utils::domain::IsBoundary); - 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< - mean_field::utils::domain::Envelope, 2>>); +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> + ); - 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>); + 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::Envelope, -7>, - 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> + ); - 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::StellarSurface, 1>, - mean_field::utils::domain::BoundaryAttribute< - mean_field::utils::domain::InfinitySurface, 2>>); + tags::unit &tags::mesh &tags::utils &tags::domain +) { + STATIC_REQUIRE( + 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_FALSE( + 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>>); + STATIC_REQUIRE( + domain_test_utils::CanFormBoundaryList< + mean_field::utils::domain::BoundaryAttribute, + mean_field::utils::domain::BoundaryAttribute> + ); - 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< - mean_field::utils::domain::StellarSurface>); + STATIC_REQUIRE_FALSE(domain_test_utils::CanFormDomainBoundary); - 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::StellarSurface, 1>, - mean_field::utils::domain::BoundaryAttribute< - mean_field::utils::domain::InfinitySurface, 2>>; + using CompleteBoundaries = mean_field::utils::domain::BoundaryList< + mean_field::utils::domain::BoundaryAttribute, + mean_field::utils::domain::BoundaryAttribute>; - using InfinityOnlyBoundary = mean_field::utils::domain::BoundaryList< - mean_field::utils::domain::BoundaryAttribute< - mean_field::utils::domain::InfinitySurface, 2>>; + using InfinityOnlyBoundary = mean_field::utils::domain::BoundaryList< + mean_field::utils::domain::BoundaryAttribute>; - using MissingEnvelopeRelation = mean_field::utils::domain::RelationList< - mean_field::utils::domain::Connected< - mean_field::utils::domain::Envelope>>; + using MissingEnvelopeRelation = mean_field::utils::domain::RelationList< + mean_field::utils::domain::Connected>; - 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>>; + using MissingBoundaryRelation = mean_field::utils::domain::RelationList>; - 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< - mean_field::utils::domain::Stellar>(1)); + STATIC_REQUIRE(SchemaT::template attribute_belongs_to(1)); - STATIC_REQUIRE(SchemaT::template attribute_belongs_to< - mean_field::utils::domain::Stellar>(2)); + STATIC_REQUIRE(SchemaT::template attribute_belongs_to(2)); - STATIC_REQUIRE_FALSE(SchemaT::template attribute_belongs_to< - mean_field::utils::domain::Stellar>(3)); + STATIC_REQUIRE_FALSE(SchemaT::template attribute_belongs_to(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 material_attribute() == - 1); + STATIC_REQUIRE(SchemaT::template material_attribute() == 1); - STATIC_REQUIRE(SchemaT::template material_attribute< - mean_field::utils::domain::Envelope>() == 2); + STATIC_REQUIRE(SchemaT::template material_attribute() == 2); - STATIC_REQUIRE(SchemaT::template material_attribute< - mean_field::utils::domain::Vacuum>() == 3); + STATIC_REQUIRE(SchemaT::template material_attribute() == 3); - STATIC_REQUIRE(SchemaT::template contains_boundary< - mean_field::utils::domain::StellarSurface>()); + STATIC_REQUIRE(SchemaT::template contains_boundary()); - STATIC_REQUIRE(SchemaT::template contains_boundary< - mean_field::utils::domain::InfinitySurface>()); + STATIC_REQUIRE(SchemaT::template contains_boundary()); - STATIC_REQUIRE(SchemaT::template boundary_attribute< - mean_field::utils::domain::StellarSurface>() == 1); + STATIC_REQUIRE(SchemaT::template boundary_attribute() == 1); - STATIC_REQUIRE(SchemaT::template boundary_attribute< - mean_field::utils::domain::InfinitySurface>() == 2); + STATIC_REQUIRE(SchemaT::template boundary_attribute() == 2); - CHECK(true); + CHECK(true); } -TEST_CASE("Domain Schema Builds Exact MFEM Attribute Markers", - tags::domain &tags::utils &tags::unit) { - using namespace mean_field::utils::domain; - using Schema = CoreEnvelopeVacuumDomainSchema; +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 mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); + const mfem::Mesh mesh = domain_test_utils::make_layered_mesh(); - const mfem::Array stellarMarker = - make_attribute_marker(mesh); - const mfem::Array vacuumMarker = - make_attribute_marker(mesh); - const mfem::Array allMarker = make_attribute_marker(mesh); + const mfem::Array stellarMarker = make_attribute_marker(mesh); + const mfem::Array vacuumMarker = make_attribute_marker(mesh); + const mfem::Array allMarker = make_attribute_marker(mesh); - REQUIRE(stellarMarker.Size() == 3); - REQUIRE(vacuumMarker.Size() == 3); - REQUIRE(allMarker.Size() == 3); + REQUIRE(stellarMarker.Size() == 3); + REQUIRE(vacuumMarker.Size() == 3); + REQUIRE(allMarker.Size() == 3); - CHECK(stellarMarker[0] == 1); - CHECK(stellarMarker[1] == 1); - CHECK(stellarMarker[2] == 0); + CHECK(stellarMarker[0] == 1); + CHECK(stellarMarker[1] == 1); + CHECK(stellarMarker[2] == 0); - CHECK(vacuumMarker[0] == 0); - CHECK(vacuumMarker[1] == 0); - CHECK(vacuumMarker[2] == 1); + CHECK(vacuumMarker[0] == 0); + CHECK(vacuumMarker[1] == 0); + CHECK(vacuumMarker[2] == 1); - CHECK(allMarker[0] == 1); - CHECK(allMarker[1] == 1); - CHECK(allMarker[2] == 1); + CHECK(allMarker[0] == 1); + CHECK(allMarker[1] == 1); + CHECK(allMarker[2] == 1); } -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( + "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 coreResult = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Connected>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto coreResult = mean_field::utils::domain:: + RelationValidator>::template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - REQUIRE(coreResult); - REQUIRE(coreResult.connectedDiagnostics.has_value()); + REQUIRE(coreResult); + REQUIRE(coreResult.connectedDiagnostics.has_value()); - CHECK(coreResult.connectedDiagnostics->domainElementCount == 1); + CHECK(coreResult.connectedDiagnostics->domainElementCount == 1); - CHECK(coreResult.connectedDiagnostics->visitedElementCount == 1); + CHECK(coreResult.connectedDiagnostics->visitedElementCount == 1); - 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); + const auto stellarResult = mean_field::utils::domain:: + RelationValidator>::template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - REQUIRE(stellarResult); - REQUIRE(stellarResult.connectedDiagnostics.has_value()); + REQUIRE(stellarResult); + REQUIRE(stellarResult.connectedDiagnostics.has_value()); - CHECK(stellarResult.connectedDiagnostics->domainElementCount == 9); + CHECK(stellarResult.connectedDiagnostics->domainElementCount == 9); - CHECK(stellarResult.connectedDiagnostics->visitedElementCount == 9); + CHECK(stellarResult.connectedDiagnostics->visitedElementCount == 9); } -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 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< - mean_field::utils::domain::Connected>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain:: + RelationValidator>::template validate< + mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK(result.failure == - mean_field::utils::domain::RelationValidationFailure::DomainAbsent); + CHECK(result.failure == mean_field::utils::domain::RelationValidationFailure::DomainAbsent); - REQUIRE(result.connectedDiagnostics.has_value()); + REQUIRE(result.connectedDiagnostics.has_value()); - CHECK(result.connectedDiagnostics->domainElementCount == 0); + CHECK(result.connectedDiagnostics->domainElementCount == 0); - CHECK(result.connectedDiagnostics->visitedElementCount == 0); + CHECK(result.connectedDiagnostics->visitedElementCount == 0); } -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 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 result = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Connected>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain:: + RelationValidator>::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::DomainDisconnected); - REQUIRE(result.connectedDiagnostics.has_value()); + REQUIRE(result.connectedDiagnostics.has_value()); - CHECK(result.connectedDiagnostics->domainElementCount == 2); + CHECK(result.connectedDiagnostics->domainElementCount == 2); - CHECK(result.connectedDiagnostics->visitedElementCount == 1); + CHECK(result.connectedDiagnostics->visitedElementCount == 1); - CHECK(result.connectedDiagnostics->elementId >= 0); + CHECK(result.connectedDiagnostics->elementId >= 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( + "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 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); + const auto coreResult = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed>:: + template validate(mesh); - CHECK(coreResult); + CHECK(coreResult); - const auto stellarResult = mean_field::utils::domain::RelationValidator< - mean_field::utils::domain::Inscribed>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto stellarResult = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed>:: + template validate(mesh); - CHECK(stellarResult); + CHECK(stellarResult); } -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 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< - mean_field::utils::domain::Core, - mean_field::utils::domain::Envelope>>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed>:: + template validate(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK( - result.failure == - mean_field::utils::domain::RelationValidationFailure::InnerDomainAbsent); + 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}, {}); +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); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed>:: + template validate(mesh); - CHECK_FALSE(result); + CHECK_FALSE(result); - CHECK( - result.failure == - mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent); + 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< - mean_field::utils::domain::Core, - mean_field::utils::domain::Envelope>>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed>:: + template validate(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< - mean_field::utils::domain::Core, - mean_field::utils::domain::Envelope>>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain::RelationValidator< + mean_field::utils::domain::Inscribed>:: + template validate(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< - 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); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(mesh); - CHECK(result); + CHECK(result); - /* - * 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); + /* + * Interface ordering is intentionally semantic rather + * than oriented. + */ + const auto reversedResult = mean_field::utils::domain::RelationValidator>:: + template validate(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< - mean_field::utils::domain::DomainBoundary< - mean_field::utils::domain::InfinitySurface, - mean_field::utils::domain::Vacuum>>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(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< - mean_field::utils::domain::DomainBoundary< - mean_field::utils::domain::InfinitySurface, - mean_field::utils::domain::Vacuum>>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(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< - mean_field::utils::domain::DomainBoundary< - mean_field::utils::domain::InfinitySurface, - mean_field::utils::domain::Vacuum>>:: - template validate< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(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< - 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); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(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< - 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); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(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< - 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); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(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< - 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); + const auto result = mean_field::utils::domain::RelationValidator>:: + template validate(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< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); + const auto validation = + mean_field::utils::domain::validate_schema(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()); - } -} - -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< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>(mesh); - - CHECK_FALSE(validation.valid()); - - REQUIRE(validation.relationResults.size() == 7); - - CHECK(validation.failed_relation_count() == 1); - - CHECK(validation.passed_relation_count() == 6); - - REQUIRE(validation.first_failed_relation_index().has_value()); - - CHECK(*validation.first_failed_relation_index() == 5); - - for (std::size_t relationIndex = 0; relationIndex < 7; ++relationIndex) { - CAPTURE(relationIndex); - - if (relationIndex == 5) { - CHECK_FALSE(validation.relationResults[relationIndex].valid()); - - CHECK(validation.relationResults[relationIndex].result.failure == - mean_field::utils::domain::RelationValidationFailure:: - DomainBoundaryExpectedFaceIsUntagged); - - continue; + CHECK(validation.relationResults[relationIndex].valid()); } - - 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( + "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); - for (const domain_test_utils::StroidCase &testCase : testCases) { - INFO("STROID case = " << testCase.name); + const auto validation = + mean_field::utils::domain::validate_schema(mesh); - INFO("Refinement levels = " << testCase.refinementLevels); + CHECK_FALSE(validation.valid()); - INFO("Order = " << testCase.order); + REQUIRE(validation.relationResults.size() == 7); - INFO("Flattening = " << testCase.flattening); + CHECK(validation.failed_relation_count() == 1); - const stroid::config::MeshConfig config = - domain_test_utils::make_stroid_config( - testCase.refinementLevels, testCase.order, testCase.flattening); + CHECK(validation.passed_relation_count() == 6); - stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); + REQUIRE(validation.first_failed_relation_index().has_value()); + + CHECK(*validation.first_failed_relation_index() == 5); + + for (std::size_t relationIndex = 0; relationIndex < 7; ++relationIndex) { + CAPTURE(relationIndex); + + if (relationIndex == 5) { + CHECK_FALSE(validation.relationResults[relationIndex].valid()); + + CHECK( + validation.relationResults[relationIndex].result.failure == + mean_field::utils::domain::RelationValidationFailure::DomainBoundaryExpectedFaceIsUntagged + ); + + 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} + }; + + for (const domain_test_utils::StroidCase &testCase : testCases) { + INFO("STROID case = " << testCase.name); + + INFO("Refinement levels = " << testCase.refinementLevels); + + INFO("Order = " << testCase.order); + + INFO("Flattening = " << 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); 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. + * The same semantic topology must validate when a mesh generator + * uses an entirely different attribute numbering convention. */ - domain_test_utils::check_schema_is_valid< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>( - *stroidMesh.reference_mesh); + domain_test_utils::check_schema_is_valid(*stroidMesh.reference_mesh); - domain_test_utils::check_schema_is_valid< - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema>( - *stroidMesh.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); } -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); +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}, {}); - config.core_id = 11; + const auto validation = + mean_field::utils::domain::validate_schema(mesh); - config.envelope_id = 17; + CHECK_FALSE(validation.valid()); - config.vacuum_id = 29; + REQUIRE(validation.relationResults.size() == 7); - config.surface_bdr_id = 101; + /* + * Connected + */ + CHECK_FALSE(validation.relationResults[2].valid()); - config.inf_bdr_id = 203; + CHECK( + validation.relationResults[2].result.failure == + mean_field::utils::domain::RelationValidationFailure::DomainAbsent + ); - stroid::StroidMesh stroidMesh = stroid::GenerateMesh(config); + /* + * Inscribed + */ + CHECK_FALSE(validation.relationResults[4].valid()); - 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); + CHECK( + validation.relationResults[4].result.failure == + mean_field::utils::domain::RelationValidationFailure::OuterDomainAbsent + ); }