diff --git a/CMakeLists.txt b/CMakeLists.txt index f3653b1..c524629 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,12 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT 0 CACHE STRING + "Uniform increment applied to every registered finite-element family order") +if (NOT MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT MATCHES "^[0-9]+$") + message(FATAL_ERROR "MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT must be a non-negative integer") +endif () + add_compile_options( -gdwarf-4 -Wno-unused-parameter @@ -34,6 +40,11 @@ pkg_check_modules(stroid REQUIRED IMPORTED_TARGET stroid) add_library(mean_field) +target_compile_definitions(mean_field + PUBLIC + MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT=${MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT} +) + target_include_directories(mean_field PUBLIC $ @@ -57,6 +68,8 @@ target_sources(mean_field libmeanfield/impl/integrators/viscosity.cpp libmeanfield/impl/mapping/domain_mapper.cpp libmeanfield/impl/mapping/transformations.cpp + libmeanfield/impl/deformation/nodal_radial_surface.cpp + libmeanfield/impl/deformation/radial_extensions.cpp libmeanfield/impl/operators/gravity_field.cpp libmeanfield/impl/operators/gravity_field_jacobian.cpp libmeanfield/impl/operators/kernels/gravity_kernels.cpp @@ -152,6 +165,13 @@ target_sources(mean_field libmeanfield/interface/surface/dependencies.cppm libmeanfield/interface/surface/compiled.cppm libmeanfield/interface/surface/compiler.cppm + libmeanfield/interface/deformation/descriptors.cppm + libmeanfield/interface/deformation/surface_prescription.cppm + libmeanfield/interface/deformation/nodal_radial_surface.cppm + libmeanfield/interface/deformation/interior_extension.cppm + libmeanfield/interface/deformation/vacuum_extension.cppm + libmeanfield/interface/deformation/radial_extensions.cppm + libmeanfield/interface/deformation/domain_deformation.cppm libmeanfield/interface/models/stellar_model.cppm libmeanfield/interface/operators/prepared_mass_normalization.cppm libmeanfield/interface/operators/prepared_centering_constraint.cppm @@ -231,6 +251,11 @@ add_executable(tests tests/operators/prepared_rotation_displacement_force_affine_deformation.cpp tests/operators/prepared_displacement_operator.cpp tests/models/stellar_model.cpp + tests/deformation/contracts.cpp + tests/deformation/surface_scalar_dof_map.cpp + tests/deformation/nodal_radial_surface.cpp + tests/deformation/radial_extensions.cpp + tests/deformation/domain_deformation.cpp tests/operators/prepared_mass_normalization.cpp tests/operators/prepared_stellar_equilibrium.cpp tests/utils/domain.cpp @@ -266,6 +291,7 @@ add_executable(stellar_null_space_experiments experiments/experiment_main.cpp experiments/rigid_motion_null_space.cpp experiments/gravity_completed_rigid_motion.cpp + experiments/coupled_gauge_modes.cpp ) target_link_libraries(stellar_null_space_experiments diff --git a/experiments/README.md b/experiments/README.md index 358404b..c631aa0 100644 --- a/experiments/README.md +++ b/experiments/README.md @@ -24,43 +24,119 @@ 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 +## Reduced stellar-surface conditioning 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. +`n = 3` Lane-Emden seed and probes only directions representable by the reduced +surface coordinates: uniform radial homology, three translation-like radial +dipoles, an axisymmetric oblate quadrupole, and a degree-12 zonal spherical +harmonic. Tangential, rotational, stellar-interior-only, and vacuum-only mesh +motions are deliberately absent because they are generated coordinates rather +than root unknowns. 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: +solves its gravity field, and completes each surface-mode case. The reachability +probe records the surface-to-volume lift amplification, complete root Jacobian +response by block, and centered-difference agreement at zero and half the +Keplerian angular speed. 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]" + --experiment-output reduced_surface_reachability.csv \ + --catch2 "[null_space][surface_modes][reachability]" ``` -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: +the gravity-gradient and gravity-potential variations accompanying each +reduced surface mode. It then measures the complete reduced root response: ```text mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \ - --experiment-output gravity_completed_null_space.csv \ - --catch2 "[null_space][gravity_completed]" + --experiment-output gravity_completed_surface_modes.csv \ + --catch2 "[null_space][surface_modes][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. +calculations from the reachability diagnostic. + +The surface-frequency probe injects normalized zonal spherical harmonics over +a range of angular degrees. For each degree it lifts the unit surface pattern +once, samples the coefficients of +`det(I + a grad(d))` at the production geometry-inspection points, and locates +the positive and negative critical fractional amplitudes without repeatedly +rebuilding trial geometries. It also records the minimum determinant at +fractional amplitudes `1e-4`, `1e-3`, and `1e-2`: + +```text +mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \ + --experiment-output surface_frequency_limits.csv \ + --catch2 "[surface_modes][frequency_limit]" +``` + +The coupled conditioning probe evaluates an extension-aware `n = 3` homology +direction together with the nonuniform reduced surface modes. Its density and +enthalpy tangents include the coordinate-composition terms generated by the +non-affine interior extension, and its gravity variation is completed through +the discrete mixed subsystem so the fixed-infinity exterior response is +consistent. The probe prepares the equilibrium once, reuses one restricted +gravity operator and preconditioner, and uses analytic Jacobian actions: + +```text +mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \ + --experiment-output coupled_surface_conditioning.csv \ + --catch2 "[null_space][surface_modes][conditioning]" +``` + +The CSV reports prescribed and gravity-completed responses by residual block, +the response normalized by the completed direction, lift conditioning where +applicable, and the convergence of each restricted gravity solve. + +The homology mass-cancellation experiment evaluates the signed decomposition + +```text +delta M = delta M_density + delta M_geometry +``` + +without solving gravity or preparing the complete coupled operator. The two +default-build cases provide the registered-order baseline and one uniform +spatial refinement: + +```text +mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \ + --experiment-output homology_mass_h0_p0.csv \ + --catch2 "[null_space][homology][mass_normalization][p_refinement]" + +mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \ + --experiment-output homology_mass_h1_p0.csv \ + --catch2 "[null_space][homology][mass_normalization][h_refinement]" +``` + +A reproducible one-level uniform polynomial refinement uses a separate build so +all registered field families and their quadrature policies see the same +compile-time order increment: + +```text +cmake -S . -B cmake-build-release-homebrew-p1 -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja \ + -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \ + -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ \ + -DUMFPACK_DIR=/opt/homebrew/lib/cmake/UMFPACK \ + -DXAD_DIR=/usr/local/lib/cmake/XAD \ + -Dhypre_DIR=/usr/local/lib/cmake/HYPRE \ + -Dmfem_DIR=/usr/local/lib/cmake/mfem \ + -DBoost_DIR=/opt/homebrew/anaconda3/lib/cmake/Boost-1.82.0 \ + -DMEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT=1 +cmake --build cmake-build-release-homebrew-p1 \ + --target stellar_null_space_experiments -j 8 +mpirun -np 1 \ + ./cmake-build-release-homebrew-p1/stellar_null_space_experiments \ + --experiment-output homology_mass_h0_p1.csv \ + --catch2 "[null_space][homology][mass_normalization][p_refinement]" +``` 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 diff --git a/experiments/coupled_gauge_modes.cpp b/experiments/coupled_gauge_modes.cpp new file mode 100644 index 0000000..1db0fa2 --- /dev/null +++ b/experiments/coupled_gauge_modes.cpp @@ -0,0 +1,694 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +import experiment; +import experiment.stellar_null_space; +import mean_field; +import test_helpers; + +namespace { + namespace null_space = experiment::null_space; + + struct GaugeMode final { + std::string name; + std::string family; + int axis{-1}; + bool requiresGravityCompletion{true}; + mfem::Vector direction; + }; + + class GravityUnknownJacobian final : public mfem::Operator { + public: + explicit GravityUnknownJacobian( + const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator + ) + : mfem::Operator( + stellarOperator.GetLayout().size(null_space::gravityGradientValue) + + stellarOperator.GetLayout().size(null_space::gravityPotentialValue) + ), + m_stellarOperator(stellarOperator), + m_gravityGradientSize(stellarOperator.GetLayout().size(null_space::gravityGradientValue)) { + MFEM_VERIFY(Width() == Height(), "The restricted gravity Jacobian must be square."); + } + + void Mult( + const mfem::Vector &gravityDirection, + mfem::Vector &gravityAction + ) const override { + MFEM_VERIFY(gravityDirection.Size() == Width(), "The restricted 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; + }; + + struct GravityCompletionResult final { + mfem::Vector direction; + double rightHandSideNorm{0.0}; + double residualNorm{0.0}; + double relativeResidual{0.0}; + double finalNorm{0.0}; + int iterations{0}; + bool solvePerformed{false}; + }; + + 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 + 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 = + null_space::const_residual_view(completeAction, layout, null_space::gravityGradientResidual); + const mfem::Vector potential = + null_space::const_residual_view(completeAction, layout, 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 + ); + null_space::assign_value_block(completeDirection, layout, null_space::gravityGradientValue, gravityGradient); + null_space::assign_value_block(completeDirection, layout, null_space::gravityPotentialValue, gravityPotential); + } + + [[nodiscard]] GravityCompletionResult solve_gravity_completion( + const mfem::Vector &prescribedAction, + const mean_field::operators::StellarEquilibriumLayout &layout, + const MPI_Comm communicator, + GravityUnknownJacobian &gravityJacobian, + mfem::MINRESSolver &gravitySolver + ) { + mfem::Vector rightHandSide = gravity_residual_blocks(prescribedAction, layout); + rightHandSide *= -1.0; + + GravityCompletionResult result; + result.direction.SetSize(gravityJacobian.Width()); + result.direction = 0.0; + result.rightHandSideNorm = null_space::global_norm(rightHandSide, communicator); + + const double skipThreshold = 100.0 * std::numeric_limits::epsilon(); + if (result.rightHandSideNorm <= skipThreshold) { + return result; + } + + gravitySolver.Mult(rightHandSide, result.direction); + REQUIRE(gravitySolver.GetConverged()); + + mfem::Vector action; + gravityJacobian.Mult(result.direction, action); + action -= rightHandSide; + + result.residualNorm = null_space::global_norm(action, communicator); + result.relativeResidual = result.residualNorm / result.rightHandSideNorm; + result.finalNorm = gravitySolver.GetFinalNorm(); + result.iterations = gravitySolver.GetNumIterations(); + result.solvePerformed = true; + + REQUIRE(std::isfinite(result.relativeResidual)); + return result; + } + + class ExtensionAwareHomologyScalarCoefficient final : public mfem::Coefficient { + public: + ExtensionAwareHomologyScalarCoefficient( + const mfem::ParGridFunction &baseField, + const mfem::ParGridFunction &coordinateVelocity, + const mfem::Vector &referenceCenter, + const double physicalScalingExponent + ) + : m_baseField(&baseField), + m_coordinateVelocity(&coordinateVelocity), + m_referenceCenter(&referenceCenter), + m_physicalScalingExponent(physicalScalingExponent) { + } + + double Eval( + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integrationPoint + ) override { + transformation.SetIntPoint(&integrationPoint); + + mfem::Vector referencePosition; + mfem::Vector coordinateVelocity; + mfem::Vector baseGradient; + transformation.Transform(integrationPoint, referencePosition); + m_coordinateVelocity->GetVectorValue(transformation, integrationPoint, coordinateVelocity); + m_baseField->GetGradient(transformation, baseGradient); + + coordinateVelocity -= referencePosition; + coordinateVelocity += *m_referenceCenter; + + return -m_physicalScalingExponent * m_baseField->GetValue(transformation, integrationPoint) + + baseGradient * coordinateVelocity; + } + + private: + const mfem::ParGridFunction *m_baseField; + const mfem::ParGridFunction *m_coordinateVelocity; + const mfem::Vector *m_referenceCenter; + double m_physicalScalingExponent; + }; + + [[nodiscard]] mfem::Vector project_extension_aware_homology_scalar( + mfem::ParFiniteElementSpace &finiteElementSpace, + const mean_field::field::FieldDofMap &fieldMap, + const mfem::Vector &baseReducedField, + const mfem::ParGridFunction &coordinateVelocity, + const mfem::Vector &referenceCenter, + const double physicalScalingExponent + ) { + mfem::ParGridFunction baseField(&finiteElementSpace); + baseField.SetFromTrueDofs(fieldMap.scatter(baseReducedField)); + + ExtensionAwareHomologyScalarCoefficient coefficient( + baseField, coordinateVelocity, referenceCenter, physicalScalingExponent + ); + mfem::ParGridFunction directionField(&finiteElementSpace); + directionField.ProjectCoefficient(coefficient); + + mfem::Vector directionTrue; + directionField.GetTrueDofs(directionTrue); + return fieldMap.gather(directionTrue); + } + + struct HomologyMassCancellation final { + double currentMass{0.0}; + double targetMass{0.0}; + double densityContribution{0.0}; + double geometryContribution{0.0}; + double completeDerivative{0.0}; + }; + + [[nodiscard]] HomologyMassCancellation measure_homology_mass_cancellation( + const mean_field::operators::PreparedMassNormalizationOperator &massOperator, + const mfem::Vector &densityDirection, + const mfem::Vector &volumeDirection + ) { + mfem::Vector densityAction; + mfem::Vector geometryAction; + mfem::Vector completeAction; + massOperator.ApplyDensityJacobianAction(densityDirection, densityAction); + massOperator.ApplyDisplacementJacobianAction(volumeDirection, geometryAction); + massOperator.ApplyCompleteJacobianAction(densityDirection, volumeDirection, completeAction); + + REQUIRE(densityAction.Size() == 1); + REQUIRE(geometryAction.Size() == 1); + REQUIRE(completeAction.Size() == 1); + + const double recomposedDerivative = densityAction(0) + geometryAction(0); + const double comparisonScale = std::max({1.0, std::abs(recomposedDerivative), std::abs(completeAction(0))}); + CHECK( + std::abs(completeAction(0) - recomposedDerivative) <= + 64.0 * std::numeric_limits::epsilon() * comparisonScale + ); + + return { + .currentMass = massOperator.GetCurrentMass(), + .targetMass = massOperator.GetTargetMass(), + .densityContribution = densityAction(0), + .geometryContribution = geometryAction(0), + .completeDerivative = completeAction(0) + }; + } + + void add_homology_mass_metrics( + std::map< + std::string, + double> &metrics, + const HomologyMassCancellation &cancellation + ) { + const double uncancelledMagnitude = + std::abs(cancellation.densityContribution) + std::abs(cancellation.geometryContribution); + const double targetScale = std::max(std::abs(cancellation.targetMass), std::numeric_limits::epsilon()); + + metrics.emplace("current_mass", cancellation.currentMass); + metrics.emplace("target_mass", cancellation.targetMass); + metrics.emplace("base_mass_residual", cancellation.currentMass - cancellation.targetMass); + metrics.emplace( + "relative_base_mass_residual", (cancellation.currentMass - cancellation.targetMass) / targetScale + ); + metrics.emplace("density_mass_derivative", cancellation.densityContribution); + metrics.emplace("geometry_mass_derivative", cancellation.geometryContribution); + metrics.emplace("complete_mass_derivative", cancellation.completeDerivative); + metrics.emplace("mass_derivative_uncancelled_magnitude", uncancelledMagnitude); + metrics.emplace( + "mass_derivative_relative_cancellation_error", + std::abs(cancellation.completeDerivative) / + std::max(uncancelledMagnitude, std::numeric_limits::epsilon()) + ); + metrics.emplace("complete_mass_derivative_per_target_mass", cancellation.completeDerivative / targetScale); + } + + [[nodiscard]] GaugeMode make_homology_mode( + null_space::N3Equilibrium &fixture, + const null_space::SurfaceMode &uniformRadialMode + ) { + const auto &layout = fixture.stellar_operator().GetLayout(); + const auto &state = fixture.state(); + + mfem::Vector direction(layout.value_offsets().Last()); + direction = 0.0; + + const mfem::Vector volumeDirection = fixture.lifted_surface_direction(uniformRadialMode.direction); + mfem::ParGridFunction coordinateVelocity(fixture.fem().displacementFes.get()); + coordinateVelocity.SetFromTrueDofs(volumeDirection); + + const mean_field::field::FieldDofMap densityMap = + mean_field::field::make_field_dof_map( + *fixture.fem().densityFes + ); + const mean_field::field::FieldDofMap enthalpyMap = + mean_field::field::make_field_dof_map( + *fixture.fem().enthalpyFes + ); + const mfem::Vector &referenceCenter = fixture.model().surfaceDeformationPrescription().referenceCenter(); + + const mfem::Vector densityDirection = project_extension_aware_homology_scalar( + *fixture.fem().densityFes, densityMap, + null_space::const_value_view(state, layout, null_space::densityValue), coordinateVelocity, referenceCenter, + 3.0 + ); + null_space::assign_value_block(direction, layout, null_space::densityValue, densityDirection); + + null_space::assign_value_block( + direction, layout, null_space::surfaceDeformationValue, + null_space::const_value_view(uniformRadialMode.direction, layout, null_space::surfaceDeformationValue) + ); + + /* + * A physical homology scales rho and h, while the power-law mesh + * extension moves interior coordinates non-affinely. The scalar + * tangents therefore contain the coordinate-composition term + * grad(f) dot (v - (X-Xc)) in addition to their physical scaling. + * Gravity is completed through the discrete mixed subsystem below, + * which also supplies the correct fixed-infinity exterior response. + */ + const mfem::Vector enthalpyDirection = project_extension_aware_homology_scalar( + *fixture.fem().enthalpyFes, enthalpyMap, + null_space::const_value_view(state, layout, null_space::enthalpyValue), coordinateVelocity, referenceCenter, + 1.0 + ); + null_space::assign_value_block(direction, layout, null_space::enthalpyValue, enthalpyDirection); + + null_space::value_view(direction, layout, null_space::bernoulliValue)(0) = + -null_space::const_value_view(state, layout, null_space::bernoulliValue)(0); + + return { + .name = "n3_homology", + .family = "homology", + .axis = -1, + .requiresGravityCompletion = true, + .direction = std::move(direction) + }; + } + + [[nodiscard]] std::vector make_gauge_modes(null_space::N3Equilibrium &fixture) { + auto surfaceModes = null_space::make_surface_modes(fixture); + const auto homologyMode = std::ranges::find_if(surfaceModes, [](const null_space::SurfaceMode &mode) { + return mode.kind == null_space::SurfaceModeKind::uniform_radial; + }); + MFEM_VERIFY(homologyMode != surfaceModes.end(), "The reduced surface modes do not contain homology."); + + std::vector modes; + modes.reserve(6); + modes.push_back(make_homology_mode(fixture, *homologyMode)); + + for (auto &surfaceMode : surfaceModes) { + if (surfaceMode.kind == null_space::SurfaceModeKind::uniform_radial) { + continue; + } + modes.push_back( + {.name = surfaceMode.name, + .family = null_space::surface_mode_kind_name(surfaceMode.kind), + .axis = surfaceMode.axis, + .requiresGravityCompletion = true, + .direction = std::move(surfaceMode.direction)} + ); + } + return modes; + } + + [[nodiscard]] long long global_nonzero_count( + const mfem::Vector &vector, + const MPI_Comm communicator + ) { + long long localCount = 0; + for (int index = 0; index < vector.Size(); ++index) { + if (vector(index) != 0.0) { + ++localCount; + } + } + + long long globalCount = 0; + MPI_Allreduce(&localCount, &globalCount, 1, MPI_LONG_LONG, MPI_SUM, communicator); + return globalCount; + } + + [[nodiscard]] mean_field::models::structure::StructureSeed make_n3_seed(null_space::Model &model) { + 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); + return model.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = radialSampleCount}); + } + + [[nodiscard]] mfem::Vector project_n3_density( + const mean_field::fem::FEM &fem, + const mean_field::models::structure::StructureSeed &seed + ) { + const auto interpolate = [](const mfem::Vector &radii, const mfem::Vector &values, const double radius) { + if (radius <= radii(0)) { + return values(0); + } + const int finalIndex = radii.Size() - 1; + if (radius >= 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) <= radius) { + lower = middle; + } else { + upper = middle; + } + } + const double fraction = (radius - 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 radius = position.Norml2(); + return radius >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.density, radius); + }); + mfem::ParGridFunction densityField(fem.densityFes.get()); + densityField.ProjectCoefficient(densityCoefficient); + + mfem::Vector densityTrue; + densityField.GetTrueDofs(densityTrue); + return densityTrue; + } + + void run_homology_mass_cancellation_experiment(const int hRefinementLevel) { + REQUIRE(hRefinementLevel >= 0); + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, hRefinementLevel); + REQUIRE(fem.okay()); + + null_space::Model model = null_space::make_model(); + const mean_field::models::structure::StructureSeed seed = make_n3_seed(model); + const mfem::Vector densityTrue = project_n3_density(fem, seed); + + auto deformation = model.compileDomainDeformation(fem); + const auto &surface = deformation.surfaceDeformationPrescription(); + mfem::Vector zeroSurfaceParameters(surface.parameterCount()); + mfem::Vector homologySurfaceDirection(surface.parameterCount()); + zeroSurfaceParameters = 0.0; + for (int parameter = 0; parameter < homologySurfaceDirection.Size(); ++parameter) { + homologySurfaceDirection(parameter) = surface.referenceRadius(parameter); + } + + mfem::Vector volumeDirection(deformation.volumeDisplacementSize()); + deformation.applyJacobian(zeroSurfaceParameters, homologySurfaceDirection, volumeDirection); + mfem::ParGridFunction coordinateVelocity(fem.displacementFes.get()); + coordinateVelocity.SetFromTrueDofs(volumeDirection); + + mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( + fem, *fem.domainMapperStateless + ); + const mean_field::field::FieldDofMap &densityMap = gravityContext.GetDensityMap(); + const mfem::Vector reducedDensity = densityMap.gather(densityTrue); + const mfem::Vector densityDirection = project_extension_aware_homology_scalar( + *fem.densityFes, densityMap, reducedDensity, coordinateVelocity, surface.referenceCenter(), 3.0 + ); + + mfem::Vector zeroDisplacement(fem.displacementFes->GetTrueVSize()); + mfem::Vector zeroGravityGradient(fem.gravityFluxFes->GetTrueVSize()); + mfem::Vector zeroGravityPotential(fem.gravityPotentialFes->GetTrueVSize()); + zeroDisplacement = 0.0; + zeroGravityGradient = 0.0; + zeroGravityPotential = 0.0; + + const mean_field::operators::MassNormalizationDependencies dependencies{ + .discretization = {.identity = 9101, .revision = 1}, + .density = {.identity = 9103, .revision = 1}, + .displacement = {.identity = 9109, .revision = 1}, + .targetMass = {.identity = 9127, .revision = 1} + }; + gravityContext.Prepare( + {.density = reducedDensity, + .displacement = gravityContext.GetDisplacementMap().gather(zeroDisplacement), + .gravity_gradient = gravityContext.GetGravityGradientMap().gather(zeroGravityGradient), + .gravity_potential = gravityContext.GetGravityPotentialMap().gather(zeroGravityPotential)}, + {.discretization = {.value = dependencies.discretization.revision}, + .displacement = {.value = dependencies.displacement.revision}, + .density = {.value = dependencies.density.revision}, + .gravity_gradient = {.value = 1}, + .gravity_potential = {.value = 1}} + ); + + mean_field::operators::PreparedMassNormalizationOperator massOperator( + fem, *fem.domainMapperStateless, gravityContext + ); + massOperator.Prepare({.targetMass = mean_field::utils::MASS}, dependencies); + + const mfem::Vector reducedVolumeDirection = gravityContext.GetDisplacementMap().gather(volumeDirection); + const HomologyMassCancellation cancellation = + measure_homology_mass_cancellation(massOperator, densityDirection, reducedVolumeDirection); + + std::map metrics{ + {"density_direction_norm", null_space::global_norm(densityDirection, fem.mesh->GetComm())}, + {"surface_direction_norm", null_space::global_norm(homologySurfaceDirection, fem.mesh->GetComm())}, + {"volume_direction_norm", null_space::global_norm(volumeDirection, fem.mesh->GetComm())}, + {"global_element_count", static_cast(fem.mesh->GetGlobalNE())}, + {"global_density_true_dof_count", static_cast(fem.densityFes->GlobalTrueVSize())}, + {"global_displacement_true_dof_count", static_cast(fem.displacementFes->GlobalTrueVSize())}, + {"global_surface_parameter_count", static_cast(surface.globalParameterCount())} + }; + add_homology_mass_metrics(metrics, cancellation); + + int rank = 0; + MPI_Comm_rank(fem.mesh->GetComm(), &rank); + if (rank == 0) { + const int pRefinementLevel = mean_field::field::uniformPolynomialOrderIncrement; + experiment::record_experiment_result( + "n3_homology_mass_cancellation", + "h" + std::to_string(hRefinementLevel) + "_p" + std::to_string(pRefinementLevel), + {{"h_refinement_level", std::to_string(hRefinementLevel)}, + {"p_refinement_level", std::to_string(pRefinementLevel)}, + {"density_polynomial_order", std::to_string(mean_field::field::Density::Scalar::familyOrder)}, + {"enthalpy_polynomial_order", std::to_string(mean_field::field::Enthalpy::Scalar::familyOrder)}, + {"displacement_polynomial_order", + std::to_string(mean_field::field::Displacement::Vector::familyOrder)}, + {"gravity_polynomial_order", std::to_string(mean_field::field::Gravity::Potential::familyOrder)}, + {"mesh_file", test_utils::setup_args().mesh_file}}, + std::move(metrics) + ); + } + } +} // namespace + +TEST_CASE( + "Coupled Stellar Equilibrium Homology And Reduced Surface Mode Responses", + "[null_space][surface_modes][conditioning][homology]" +) { + mean_field::utils::Args args = test_utils::setup_args(); + args.p.rtol = std::min(args.p.rtol, 1.0e-12); + args.p.atol = std::min(args.p.atol, 1.0e-13); + args.p.max_iters = std::max(args.p.max_iters, 1500); + + null_space::N3Equilibrium fixture(std::move(args)); + const MPI_Comm communicator = fixture.fem().mesh->GetComm(); + int rank = 0; + MPI_Comm_rank(communicator, &rank); + + const std::vector modes = make_gauge_modes(fixture); + const auto &stellarOperator = fixture.stellar_operator(); + const auto &layout = stellarOperator.GetLayout(); + + GravityUnknownJacobian gravityJacobian(stellarOperator); + mean_field::operators::ReducedGravityFieldPreconditioner gravityPreconditioner( + fixture.fem(), stellarOperator.GetGravityContext().GetGeometryContext() + ); + + mfem::MINRESSolver gravitySolver(communicator); + gravitySolver.SetOperator(gravityJacobian); + gravitySolver.SetPreconditioner(gravityPreconditioner); + gravitySolver.SetRelTol(1.0e-10); + gravitySolver.SetAbsTol(1.0e-12); + gravitySolver.SetMaxIter(1500); + gravitySolver.SetPrintLevel(0); + + for (std::size_t modeIndex = 0; modeIndex < modes.size(); ++modeIndex) { + const GaugeMode &mode = modes[modeIndex]; + null_space::report_progress( + communicator, + "evaluating " + mode.name + " (" + std::to_string(modeIndex + 1) + "/" + std::to_string(modes.size()) + ")" + ); + + const double prescribedInputNorm = null_space::global_norm(mode.direction, communicator); + REQUIRE(std::isfinite(prescribedInputNorm)); + REQUIRE(prescribedInputNorm > 0.0); + + const mfem::Vector prescribedAction = fixture.jacobian_action(mode.direction); + const double prescribedActionNorm = null_space::global_norm(prescribedAction, communicator); + + GravityCompletionResult completion; + completion.direction.SetSize(gravityJacobian.Width()); + completion.direction = 0.0; + + mfem::Vector completedDirection(mode.direction); + + if (mode.requiresGravityCompletion) { + completion = + solve_gravity_completion(prescribedAction, layout, communicator, gravityJacobian, gravitySolver); + + assign_gravity_completion( + completedDirection, layout, completion.direction, gravityJacobian.gravity_gradient_size() + ); + } + + const mfem::Vector completedAction = fixture.jacobian_action(completedDirection); + + const double completedInputNorm = null_space::global_norm(completedDirection, communicator); + const double completedActionNorm = null_space::global_norm(completedAction, communicator); + const double completionNorm = null_space::global_norm(completion.direction, communicator); + + REQUIRE(std::isfinite(prescribedActionNorm)); + REQUIRE(std::isfinite(completedInputNorm)); + REQUIRE(std::isfinite(completedActionNorm)); + REQUIRE(completedInputNorm > 0.0); + + std::map metrics{ + {"prescribed_input_norm", prescribedInputNorm}, + {"prescribed_action_norm", prescribedActionNorm}, + {"completed_input_norm", completedInputNorm}, + {"completed_action_norm", completedActionNorm}, + {"normalized_completed_response", completedActionNorm / completedInputNorm}, + {"gravity_completion_norm", completionNorm}, + {"gravity_solve_rhs_norm", completion.rightHandSideNorm}, + {"gravity_solve_residual_norm", completion.residualNorm}, + {"gravity_solve_relative_residual", completion.relativeResidual}, + {"gravity_solve_final_norm", completion.finalNorm}, + {"gravity_solve_iterations", static_cast(completion.iterations)}, + {"global_nonzero_input_dofs", static_cast(global_nonzero_count(mode.direction, communicator))} + }; + + if (mode.family == "homology") { + const mfem::Vector densityDirection = + null_space::const_value_view(mode.direction, layout, null_space::densityValue); + const mfem::Vector volumeDirection = fixture.lifted_surface_direction(mode.direction); + const HomologyMassCancellation massCancellation = measure_homology_mass_cancellation( + stellarOperator.GetMassNormalizationOperator(), densityDirection, volumeDirection + ); + add_homology_mass_metrics(metrics, massCancellation); + } + + if (completedActionNorm > 0.0) { + metrics.emplace("gravity_completion_reduction", prescribedActionNorm / completedActionNorm); + } + + add_block_metrics( + metrics, "prescribed_", null_space::residual_block_norms(prescribedAction, layout, communicator) + ); + add_block_metrics( + metrics, "completed_", null_space::residual_block_norms(completedAction, layout, communicator) + ); + + if (rank == 0) { + experiment::record_experiment_result( + "coupled_reduced_surface_mode_conditioning", mode.name, + {{"mode_family", mode.family}, + {"axis", std::to_string(mode.axis)}, + {"gravity_completion_requested", mode.requiresGravityCompletion ? "true" : "false"}, + {"gravity_solve_performed", completion.solvePerformed ? "true" : "false"}, + {"rotation_fraction_of_keplerian", "0.0"}, + {"mesh_file", test_utils::setup_args().mesh_file}, + {"local_state_dofs", std::to_string(stellarOperator.Width())}}, + std::move(metrics) + ); + } + } + + null_space::report_progress(communicator, "coupled reduced surface-mode probe complete; writing CSV output"); +} + +TEST_CASE( + "N3 Homology Mass Cancellation At The Registered Polynomial Order", + "[null_space][homology][mass_normalization][convergence][p_refinement]" +) { + run_homology_mass_cancellation_experiment(0); +} + +TEST_CASE( + "N3 Homology Mass Cancellation Under Uniform Spatial Refinement", + "[null_space][homology][mass_normalization][convergence][h_refinement]" +) { + run_homology_mass_cancellation_experiment(1); +} diff --git a/experiments/experiment_main.cpp b/experiments/experiment_main.cpp index d5e121d..d69ad8a 100644 --- a/experiments/experiment_main.cpp +++ b/experiments/experiment_main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -14,8 +15,6 @@ #include #include #include -#include - import mean_field; import test_helpers; @@ -23,7 +22,7 @@ import experiment; using namespace experiment; -static std::string escape_csv(const std::string& value) { +static std::string escape_csv(const std::string &value) { if (value.find_first_of(",\"\n") == std::string::npos) { return value; } @@ -51,11 +50,11 @@ static void write_experiment_csv() { std::set parameter_names; std::set metric_names; - for (const ExperimentResult& result : results) { - for (const auto& [name, value] : result.parameters) { + for (const ExperimentResult &result : results) { + for (const auto &[name, value] : result.parameters) { parameter_names.insert(name); } - for (const auto& [name, value] : result.metrics) { + for (const auto &[name, value] : result.metrics) { metric_names.insert(name); } } @@ -68,22 +67,22 @@ static void write_experiment_csv() { } output << "experiment,case"; - for (const std::string& name : parameter_names) { + for (const std::string &name : parameter_names) { output << ',' << escape_csv(name); } - for (const std::string& name : metric_names) { + for (const std::string &name : metric_names) { output << ',' << escape_csv(name); } output << '\n'; output << std::setprecision(17); - for (const ExperimentResult& result : results) { + for (const ExperimentResult &result : results) { output << escape_csv(result.experiment_name) << ',' << escape_csv(result.case_name); - for (const std::string& name : parameter_names) { + for (const std::string &name : parameter_names) { const auto iterator = result.parameters.find(name); output << ',' << (iterator == result.parameters.end() ? "" : escape_csv(iterator->second)); } - for (const std::string& name : metric_names) { + for (const std::string &name : metric_names) { const auto iterator = result.metrics.find(name); output << ','; if (iterator != result.metrics.end()) { @@ -104,24 +103,28 @@ public: return "Compact console reporter that writes structured experiment measurements to CSV."; } - void testCaseEnded(const Catch::TestCaseStats& statistics) override { + void testCaseEnded(const Catch::TestCaseStats &statistics) override { StreamingReporterBase::testCaseEnded(statistics); const bool passed = statistics.totals.assertions.allPassed(); - std::cout << (passed ? "PASS " : "FAIL ") - << statistics.testInfo->name - << " (" << statistics.totals.assertions.passed - << " assertions)\n"; + std::cout << (passed ? "PASS " : "FAIL ") << statistics.testInfo->name << " (" + << statistics.totals.assertions.passed << " assertions)\n"; } - void testRunEnded(const Catch::TestRunStats& statistics) override { + void testRunEnded(const Catch::TestRunStats &statistics) override { StreamingReporterBase::testRunEnded(statistics); write_experiment_csv(); } }; -CATCH_REGISTER_REPORTER("experiment", ExperimentReporter) +CATCH_REGISTER_REPORTER( + "experiment", + ExperimentReporter +) -int main(int argc, char* argv[]) { +int main( + int argc, + char *argv[] +) { fourdst::config::Config config; CLI::App app{"Mean Field accuracy experiments"}; @@ -149,39 +152,39 @@ int main(int argc, char* argv[]) { } } - std::vector configuration_argv; + std::vector configuration_argv; configuration_argv.reserve(configuration_arguments.size()); - for (const std::string& argument : configuration_arguments) { + for (const std::string &argument : configuration_arguments) { configuration_argv.push_back(argument.c_str()); } try { app.parse(static_cast(configuration_argv.size()), configuration_argv.data()); - } catch (const CLI::ParseError& error) { + } catch (const CLI::ParseError &error) { return app.exit(error); } std::vector catch_arguments{argv[0]}; - for (const std::string& argument : app.remaining()) { + for (const std::string &argument : app.remaining()) { catch_arguments.push_back(argument); } - for (const std::string& argument : catch_arguments_from_command_line) { + for (const std::string &argument : catch_arguments_from_command_line) { catch_arguments.push_back(argument); } bool has_reporter = false; - for (const std::string& argument : catch_arguments) { - has_reporter = has_reporter || argument == "-r" || argument == "--reporter" || - argument.starts_with("-r=") || argument.starts_with("--reporter="); + for (const std::string &argument : catch_arguments) { + has_reporter = has_reporter || argument == "-r" || argument == "--reporter" || argument.starts_with("-r=") || + argument.starts_with("--reporter="); } if (!has_reporter) { catch_arguments.emplace_back("--reporter"); catch_arguments.emplace_back("experiment"); } - std::vector catch_argv; + std::vector catch_argv; catch_argv.reserve(catch_arguments.size()); - for (const std::string& argument : catch_arguments) { + for (const std::string &argument : catch_arguments) { catch_argv.push_back(argument.c_str()); } diff --git a/experiments/experiment_results.cppm b/experiments/experiment_results.cppm index ca7c7a8..6b53ba9 100644 --- a/experiments/experiment_results.cppm +++ b/experiments/experiment_results.cppm @@ -18,7 +18,7 @@ export namespace experiment { class ExperimentRegistry { public: - static ExperimentRegistry& instance() { + static ExperimentRegistry &instance() { static ExperimentRegistry registry; return registry; } @@ -50,16 +50,20 @@ export namespace experiment { }; inline void record_experiment_result( - const std::string& experiment_name, - const std::string& case_name, - std::map parameters, - std::map metrics + const std::string &experiment_name, + const std::string &case_name, + std::map< + std::string, + std::string> parameters, + std::map< + std::string, + double> metrics ) { - ExperimentRegistry::instance().add_result({ - .experiment_name = experiment_name, - .case_name = case_name, - .parameters = std::move(parameters), - .metrics = std::move(metrics) - }); + ExperimentRegistry::instance().add_result( + {.experiment_name = experiment_name, + .case_name = case_name, + .parameters = std::move(parameters), + .metrics = std::move(metrics)} + ); } -} \ No newline at end of file +} // namespace experiment \ No newline at end of file diff --git a/experiments/gravity_accuracy_budget.cpp b/experiments/gravity_accuracy_budget.cpp index 9e8e7e9..ad88ad5 100644 --- a/experiments/gravity_accuracy_budget.cpp +++ b/experiments/gravity_accuracy_budget.cpp @@ -17,658 +17,589 @@ import experiment; using namespace experiment; struct AccuracyBudgetEnergies { - double binding{0.0}; - double virial{0.0}; + double binding{0.0}; + double virial{0.0}; }; struct AccuracyBudgetMetrics { - double direct_relative_residual{0.0}; - double gradient_relative_error{0.0}; - double gradient_projection_relative_error{0.0}; - double gradient_solution_projection_gap{0.0}; - double potential_relative_error{0.0}; - double potential_projection_relative_error{0.0}; - double potential_solution_projection_gap{0.0}; - double binding_relative_error{0.0}; - double virial_relative_error{0.0}; - double virial_consistency_error{0.0}; + double direct_relative_residual{0.0}; + double gradient_relative_error{0.0}; + double gradient_projection_relative_error{0.0}; + double gradient_solution_projection_gap{0.0}; + double potential_relative_error{0.0}; + double potential_projection_relative_error{0.0}; + double potential_solution_projection_gap{0.0}; + double binding_relative_error{0.0}; + double virial_relative_error{0.0}; + double virial_consistency_error{0.0}; }; -static double global_norm(const mfem::Vector &vector, MPI_Comm communicator) { - const double local_norm_squared = vector * vector; - double global_norm_squared = 0.0; - MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, - MPI_SUM, communicator); - return std::sqrt(global_norm_squared); +static double global_norm( + const mfem::Vector &vector, + MPI_Comm communicator +) { + const double local_norm_squared = vector * vector; + double global_norm_squared = 0.0; + MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator); + return std::sqrt(global_norm_squared); } -static double global_dot(const mfem::Vector &left, const mfem::Vector &right, - MPI_Comm communicator) { - const double local_dot = left * right; - double global_dot_product = 0.0; - MPI_Allreduce(&local_dot, &global_dot_product, 1, MPI_DOUBLE, MPI_SUM, - communicator); - return global_dot_product; +static double global_dot( + const mfem::Vector &left, + const mfem::Vector &right, + MPI_Comm communicator +) { + const double local_dot = left * right; + double global_dot_product = 0.0; + MPI_Allreduce(&local_dot, &global_dot_product, 1, MPI_DOUBLE, MPI_SUM, communicator); + return global_dot_product; } -static void zero_vacuum_density(const mean_field::fem::FEM &fem, - mfem::GridFunction &density) { - using DomainSchema = - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; +static void zero_vacuum_density( + const mean_field::fem::FEM &fem, + mfem::GridFunction &density +) { + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - const mean_field::field::FieldDofMap density_map = - mean_field::field::make_field_dof_map(*fem.densityFes); + const mean_field::field::FieldDofMap density_map = + mean_field::field::make_field_dof_map(*fem.densityFes); - mfem::Vector density_true; - density.GetTrueDofs(density_true); + mfem::Vector density_true; + density.GetTrueDofs(density_true); - const mfem::Vector supported_density = density_map.gather(density_true); - density_map.scatter(supported_density, density_true); - density.SetFromTrueDofs(density_true); + const mfem::Vector supported_density = density_map.gather(density_true); + density_map.scatter(supported_density, density_true); + density.SetFromTrueDofs(density_true); } static int diagnostic_quadrature_order(const mean_field::fem::FEM &fem) { - return 2 * std::max(fem.gravityPotentialFes->GetMaxElementOrder(), - fem.gravityFluxFes->GetMaxElementOrder()) + - 8; + return 2 * std::max(fem.gravityPotentialFes->GetMaxElementOrder(), fem.gravityFluxFes->GetMaxElementOrder()) + 8; } static mfem::Vector assemble_monopole_projection_rhs( - mean_field::fem::FEM &fem, const mfem::GridFunction &displacement, - const double mass, const double stellar_radius) { - *fem.displacement = displacement; + mean_field::fem::FEM &fem, + const mfem::GridFunction &displacement, + const double mass, + const double stellar_radius +) { + *fem.displacement = displacement; - mfem::Vector local_rhs(fem.gravityFluxFes->GetVSize()); - local_rhs = 0.0; + mfem::Vector local_rhs(fem.gravityFluxFes->GetVSize()); + local_rhs = 0.0; - const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; - const int quadrature_order = diagnostic_quadrature_order(fem); - mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate); + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + const int quadrature_order = diagnostic_quadrature_order(fem); + mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate + ); - for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { - const mfem::FiniteElement &gravity_element = - *fem.gravityFluxFes->GetFE(element_id); - mfem::ElementTransformation *transformation = - fem.mesh->GetElementTransformation(element_id); + for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { + const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id); + mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id); - mfem::Array gravity_dofs; - mfem::DofTransformation *gravity_transform = - fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); + mfem::Array gravity_dofs; + mfem::DofTransformation *gravity_transform = fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); - const int dof_count = gravity_element.GetDof(); - const int dimension = transformation->GetSpaceDim(); - mfem::Vector element_rhs(dof_count); - mfem::Vector physical_position(dimension); - mfem::Vector analytic_field(dimension); - mfem::Vector pulled_field(dimension); - mfem::DenseMatrix vector_shape(dof_count, dimension); - element_rhs = 0.0; + const int dof_count = gravity_element.GetDof(); + const int dimension = transformation->GetSpaceDim(); + mfem::Vector element_rhs(dof_count); + mfem::Vector physical_position(dimension); + mfem::Vector analytic_field(dimension); + mfem::Vector pulled_field(dimension); + mfem::DenseMatrix vector_shape(dof_count, dimension); + element_rhs = 0.0; - const mfem::IntegrationRule &rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); - ++quadrature_point_id) { - const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); - mean_field::mapping::MappingPointContext mapping_context; - MFEM_VERIFY( - mapping_evaluator.EvaluatePoint(*transformation, point, - mapping_context) == - mean_field::mapping::MappingStatus::valid, - "Invalid mapping in monopole projection RHS."); - physical_position = mapping_context.physical_position; + for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) { + const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); + mean_field::mapping::MappingPointContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) == + mean_field::mapping::MappingStatus::valid, + "Invalid mapping in monopole projection RHS." + ); + physical_position = mapping_context.physical_position; - const double radius = physical_position.Norml2(); - MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, - "Invalid radius in monopole projection RHS."); + const double radius = physical_position.Norml2(); + MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole projection RHS."); - analytic_field = physical_position; - if (transformation->Attribute == vacuum_attribute) { - analytic_field *= - mean_field::utils::G * mass / (radius * radius * radius); - } else { - analytic_field *= mean_field::utils::G * mass / - (stellar_radius * stellar_radius * stellar_radius); - } + analytic_field = physical_position; + if (transformation->Attribute == vacuum_attribute) { + analytic_field *= mean_field::utils::G * mass / (radius * radius * radius); + } else { + analytic_field *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius); + } - mapping_context.mapping_jacobian.MultTranspose(analytic_field, - pulled_field); + mapping_context.mapping_jacobian.MultTranspose(analytic_field, pulled_field); - transformation->SetIntPoint(&point); - gravity_element.CalcVShape(*transformation, vector_shape); - const double reference_weight = point.weight * transformation->Weight(); + transformation->SetIntPoint(&point); + gravity_element.CalcVShape(*transformation, vector_shape); + const double reference_weight = point.weight * transformation->Weight(); - for (int dof = 0; dof < dof_count; ++dof) { - for (int component = 0; component < dimension; ++component) { - element_rhs(dof) += reference_weight * vector_shape(dof, component) * - pulled_field(component); + for (int dof = 0; dof < dof_count; ++dof) { + for (int component = 0; component < dimension; ++component) { + element_rhs(dof) += reference_weight * vector_shape(dof, component) * pulled_field(component); + } + } } - } + + if (gravity_transform != nullptr) { + gravity_transform->TransformDual(element_rhs); + } + local_rhs.AddElementVector(gravity_dofs, element_rhs); } - if (gravity_transform != nullptr) { - gravity_transform->TransformDual(element_rhs); + mfem::Vector true_rhs(fem.gravityFluxFes->GetTrueVSize()); + true_rhs = 0.0; + const mfem::Operator *prolongation = fem.gravityFluxFes->GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->MultTranspose(local_rhs, true_rhs); + } else { + true_rhs = local_rhs; } - local_rhs.AddElementVector(gravity_dofs, element_rhs); - } - mfem::Vector true_rhs(fem.gravityFluxFes->GetTrueVSize()); - true_rhs = 0.0; - const mfem::Operator *prolongation = - fem.gravityFluxFes->GetProlongationMatrix(); - if (prolongation != nullptr) { - prolongation->MultTranspose(local_rhs, true_rhs); - } else { - true_rhs = local_rhs; - } - - return true_rhs; + return true_rhs; } -static mfem::Vector -project_monopole_gradient(mean_field::fem::FEM &fem, - const mfem::GridFunction &displacement, - const double mass, const double stellar_radius) { - mfem::Vector displacement_true; - displacement.GetTrueDofs(displacement_true); +static mfem::Vector project_monopole_gradient( + mean_field::fem::FEM &fem, + const mfem::GridFunction &displacement, + const double mass, + const double stellar_radius +) { + mfem::Vector displacement_true; + displacement.GetTrueDofs(displacement_true); - const mfem::Vector projection_rhs_true = - assemble_monopole_projection_rhs(fem, displacement, mass, stellar_radius); + const mfem::Vector projection_rhs_true = assemble_monopole_projection_rhs(fem, displacement, mass, stellar_radius); - mean_field::operators::PreparedMappedHDivMassOperator mass_operator( - fem, *fem.domainMapperStateless); - mass_operator.Prepare( - mass_operator.GetDisplacementMap().gather(displacement_true)); + mean_field::operators::PreparedMappedHDivMassOperator mass_operator(fem, *fem.domainMapperStateless); + mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true)); - const mfem::Vector projection_rhs = - mass_operator.GetFluxMap().gather(projection_rhs_true); + const mfem::Vector projection_rhs = mass_operator.GetFluxMap().gather(projection_rhs_true); - mfem::CGSolver solver(fem.gravityFluxFes->GetComm()); - solver.SetOperator(mass_operator); - solver.SetRelTol(1.0e-11); - solver.SetAbsTol(1.0e-13); - solver.SetMaxIter(4000); - solver.SetPrintLevel(0); + mfem::CGSolver solver(fem.gravityFluxFes->GetComm()); + solver.SetOperator(mass_operator); + solver.SetRelTol(1.0e-11); + solver.SetAbsTol(1.0e-13); + solver.SetMaxIter(4000); + solver.SetPrintLevel(0); - mfem::Vector projected_gradient_reduced( - mass_operator.GetFluxMap().reduced_size()); - projected_gradient_reduced = 0.0; - solver.Mult(projection_rhs, projected_gradient_reduced); + mfem::Vector projected_gradient_reduced(mass_operator.GetFluxMap().reduced_size()); + projected_gradient_reduced = 0.0; + solver.Mult(projection_rhs, projected_gradient_reduced); - mfem::Vector residual; - mass_operator.Mult(projected_gradient_reduced, residual); - residual -= projection_rhs; + mfem::Vector residual; + mass_operator.Mult(projected_gradient_reduced, residual); + residual -= projection_rhs; - const double relative_residual = - global_norm(residual, fem.gravityFluxFes->GetComm()) / - std::max(global_norm(projection_rhs, fem.gravityFluxFes->GetComm()), - std::numeric_limits::epsilon()); + const double relative_residual = + global_norm(residual, fem.gravityFluxFes->GetComm()) / + std::max(global_norm(projection_rhs, fem.gravityFluxFes->GetComm()), std::numeric_limits::epsilon()); - REQUIRE(std::isfinite(relative_residual)); - REQUIRE(relative_residual < 1.0e-8); - return mass_operator.GetFluxMap().scatter(projected_gradient_reduced); + REQUIRE(std::isfinite(relative_residual)); + REQUIRE(relative_residual < 1.0e-8); + return mass_operator.GetFluxMap().scatter(projected_gradient_reduced); } -static double mapped_hdiv_relative_gap(mean_field::fem::FEM &fem, - const mfem::GridFunction &displacement, - const mfem::Vector &calculated, - const mfem::Vector &reference) { - mfem::Vector displacement_true; - displacement.GetTrueDofs(displacement_true); +static double mapped_hdiv_relative_gap( + mean_field::fem::FEM &fem, + const mfem::GridFunction &displacement, + const mfem::Vector &calculated, + const mfem::Vector &reference +) { + mfem::Vector displacement_true; + displacement.GetTrueDofs(displacement_true); - mean_field::operators::PreparedMappedHDivMassOperator mass_operator( - fem, *fem.domainMapperStateless); - mass_operator.Prepare( - mass_operator.GetDisplacementMap().gather(displacement_true)); + mean_field::operators::PreparedMappedHDivMassOperator mass_operator(fem, *fem.domainMapperStateless); + mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true)); - mfem::Vector difference(calculated); - difference -= reference; - mfem::Vector difference_action; - mfem::Vector reference_action; - const mfem::Vector reduced_difference = - mass_operator.GetFluxMap().gather(difference); - const mfem::Vector reduced_reference = - mass_operator.GetFluxMap().gather(reference); - mass_operator.Mult(reduced_difference, difference_action); - mass_operator.Mult(reduced_reference, reference_action); + mfem::Vector difference(calculated); + difference -= reference; + mfem::Vector difference_action; + mfem::Vector reference_action; + const mfem::Vector reduced_difference = mass_operator.GetFluxMap().gather(difference); + const mfem::Vector reduced_reference = mass_operator.GetFluxMap().gather(reference); + mass_operator.Mult(reduced_difference, difference_action); + mass_operator.Mult(reduced_reference, reference_action); - const double difference_energy = global_dot( - reduced_difference, difference_action, fem.gravityFluxFes->GetComm()); - const double reference_energy = global_dot( - reduced_reference, reference_action, fem.gravityFluxFes->GetComm()); - MFEM_VERIFY(reference_energy > 0.0, - "Projected monopole field has zero mapped H(div) norm."); + const double difference_energy = global_dot(reduced_difference, difference_action, fem.gravityFluxFes->GetComm()); + const double reference_energy = global_dot(reduced_reference, reference_action, fem.gravityFluxFes->GetComm()); + MFEM_VERIFY(reference_energy > 0.0, "Projected monopole field has zero mapped H(div) norm."); - return std::sqrt(std::max(0.0, difference_energy) / reference_energy); + return std::sqrt(std::max(0.0, difference_energy) / reference_energy); } -static AccuracyBudgetEnergies -measure_stellar_energies(mean_field::fem::FEM &fem, - const mfem::GridFunction &density, - const mean_field::physics::GravitySolution &solution) { - const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; - const int quadrature_order = diagnostic_quadrature_order(fem); - mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate); - double local_binding = 0.0; - double local_virial = 0.0; +static AccuracyBudgetEnergies measure_stellar_energies( + mean_field::fem::FEM &fem, + const mfem::GridFunction &density, + const mean_field::physics::GravitySolution &solution +) { + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + const int quadrature_order = diagnostic_quadrature_order(fem); + mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate + ); + double local_binding = 0.0; + double local_virial = 0.0; - mfem::Vector physical_position(3); - mfem::Vector reference_field(3); - mfem::Vector physical_field(3); + mfem::Vector physical_position(3); + mfem::Vector reference_field(3); + mfem::Vector physical_field(3); - for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = - fem.mesh->GetElementTransformation(element_id); - if (transformation->Attribute == vacuum_attribute) { - continue; + for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id); + if (transformation->Attribute == vacuum_attribute) { + continue; + } + + const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + + for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) { + const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); + transformation->SetIntPoint(&point); + + mean_field::mapping::MappingPointContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) == + mean_field::mapping::MappingStatus::valid, + "Invalid mapping in energy diagnostic." + ); + physical_position = mapping_context.physical_position; + const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian; + const double mapping_determinant = mapping_context.mapping_determinant; + MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in energy diagnostic."); + + solution.gradPhi.GetVectorValue(element_id, point, reference_field); + mapping_jacobian.Mult(reference_field, physical_field); + physical_field /= mapping_determinant; + + const double weight = point.weight * transformation->Weight() * mapping_determinant; + const double rho = density.GetValue(element_id, point); + const double phi = solution.phi.GetValue(element_id, point); + local_binding += 0.5 * rho * phi * weight; + local_virial -= rho * (physical_position * physical_field) * weight; + } } - const mfem::IntegrationRule &rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - - for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); - ++quadrature_point_id) { - const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); - transformation->SetIntPoint(&point); - - mean_field::mapping::MappingPointContext mapping_context; - MFEM_VERIFY( - mapping_evaluator.EvaluatePoint(*transformation, point, - mapping_context) == - mean_field::mapping::MappingStatus::valid, - "Invalid mapping in energy diagnostic."); - physical_position = mapping_context.physical_position; - const mfem::DenseMatrix &mapping_jacobian = - mapping_context.mapping_jacobian; - const double mapping_determinant = - mapping_context.mapping_determinant; - MFEM_VERIFY(mapping_determinant > 0.0, - "Non-positive mapping determinant in energy diagnostic."); - - solution.gradPhi.GetVectorValue(element_id, point, reference_field); - mapping_jacobian.Mult(reference_field, physical_field); - physical_field /= mapping_determinant; - - const double weight = - point.weight * transformation->Weight() * mapping_determinant; - const double rho = density.GetValue(element_id, point); - const double phi = solution.phi.GetValue(element_id, point); - local_binding += 0.5 * rho * phi * weight; - local_virial -= rho * (physical_position * physical_field) * weight; - } - } - - AccuracyBudgetEnergies energies; - MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, - fem.densityFes->GetComm()); - MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, - fem.densityFes->GetComm()); - return energies; + AccuracyBudgetEnergies energies; + MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm()); + MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm()); + return energies; } static double reduced_gravity_relative_residual( - mean_field::fem::FEM &fem, const mfem::GridFunction &density, + mean_field::fem::FEM &fem, + const mfem::GridFunction &density, const mfem::GridFunction &displacement, - const mean_field::physics::GravitySolution &solution) { - using GravityFieldForm = mean_field::utils::blocks::gravity_field_form; + const mean_field::physics::GravitySolution &solution +) { + using GravityFieldForm = mean_field::utils::blocks::gravity_field_form; - constexpr auto gradient_block = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::gravity_field.gradient_term); - constexpr auto poisson_block = - mean_field::utils::blocks::get_residual_block( - mean_field::utils::blocks::gravity_field.poisson_term); + constexpr auto gradient_block = mean_field::utils::blocks::get_residual_block( + mean_field::utils::blocks::gravity_field.gradient_term + ); + constexpr auto poisson_block = mean_field::utils::blocks::get_residual_block( + mean_field::utils::blocks::gravity_field.poisson_term + ); - using DomainSchema = - mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - const mean_field::field::FieldDofMap density_map = - mean_field::field::make_field_dof_map(*fem.densityFes); - const mean_field::field::FieldDofMap displacement_map = - mean_field::field::make_field_dof_map(*fem.displacementFes); - const mean_field::field::FieldDofMap gravity_flux_map = - mean_field::field::make_field_dof_map(*fem.gravityFluxFes); - const mean_field::field::FieldDofMap gravity_potential_map = - mean_field::field::make_field_dof_map( - *fem.gravityPotentialFes); + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + const mean_field::field::FieldDofMap density_map = + mean_field::field::make_field_dof_map(*fem.densityFes); + const mean_field::field::FieldDofMap displacement_map = + mean_field::field::make_field_dof_map(*fem.displacementFes); + const mean_field::field::FieldDofMap gravity_flux_map = + mean_field::field::make_field_dof_map(*fem.gravityFluxFes); + const mean_field::field::FieldDofMap gravity_potential_map = + mean_field::field::make_field_dof_map(*fem.gravityPotentialFes); - const std::array value_sizes{ - density_map.reduced_size(), displacement_map.reduced_size(), - gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()}; - const std::array residual_sizes{ - gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()}; - const mean_field::utils::blocks::form_layout layout( - value_sizes, residual_sizes); + const std::array value_sizes{ + density_map.reduced_size(), displacement_map.reduced_size(), gravity_flux_map.reduced_size(), + gravity_potential_map.reduced_size() + }; + const std::array residual_sizes{ + gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size() + }; + const mean_field::utils::blocks::form_layout layout(value_sizes, residual_sizes); - mfem::Vector density_true; - mfem::Vector displacement_true; - mfem::Vector gradient_true; - mfem::Vector potential_true; - density.GetTrueDofs(density_true); - displacement.GetTrueDofs(displacement_true); - solution.gradPhi.GetTrueDofs(gradient_true); - solution.phi.GetTrueDofs(potential_true); + mfem::Vector density_true; + mfem::Vector displacement_true; + mfem::Vector gradient_true; + mfem::Vector potential_true; + density.GetTrueDofs(density_true); + displacement.GetTrueDofs(displacement_true); + solution.gradPhi.GetTrueDofs(gradient_true); + solution.phi.GetTrueDofs(potential_true); - mean_field::operators::context::gravity_field:: - GravityFieldLinearizationContext linearization_context( - fem, *fem.domainMapperStateless); - mean_field::operators::GravityFieldJacobianOperator jacobian( - fem, *fem.domainMapperStateless, linearization_context, - layout.value_offsets(), layout.residual_offsets()); - mean_field::operators::GravityFieldOperator field_operator( - fem, *fem.domainMapperStateless, linearization_context, - layout.value_offsets(), jacobian); - mean_field::operators::context::gravity_field::GravityFieldGeometryContext - geometry_context(fem, *fem.domainMapperStateless); - mean_field::operators::ReducedGravityFieldOperator reduced_operator( - field_operator, geometry_context, - displacement_map.gather(displacement_true)); + mean_field::operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( + fem, *fem.domainMapperStateless + ); + mean_field::operators::GravityFieldJacobianOperator jacobian( + fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() + ); + mean_field::operators::GravityFieldOperator field_operator( + fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), jacobian + ); + mean_field::operators::context::gravity_field::GravityFieldGeometryContext geometry_context( + fem, *fem.domainMapperStateless + ); + mean_field::operators::ReducedGravityFieldOperator reduced_operator( + field_operator, geometry_context, displacement_map.gather(displacement_true) + ); - mfem::Vector right_hand_side; - reduced_operator.BuildRightHandSide(density_map.gather(density_true), - right_hand_side); + mfem::Vector right_hand_side; + reduced_operator.BuildRightHandSide(density_map.gather(density_true), right_hand_side); - mfem::BlockVector state(layout.residual_offsets()); - state = 0.0; - state.GetBlock(gradient_block) = gravity_flux_map.gather(gradient_true); - state.GetBlock(poisson_block) = gravity_potential_map.gather(potential_true); + mfem::BlockVector state(layout.residual_offsets()); + state = 0.0; + state.GetBlock(gradient_block) = gravity_flux_map.gather(gradient_true); + state.GetBlock(poisson_block) = gravity_potential_map.gather(potential_true); - mfem::Vector residual; - reduced_operator.Mult(state, residual); - residual -= right_hand_side; + mfem::Vector residual; + reduced_operator.Mult(state, residual); + residual -= right_hand_side; - return global_norm(residual, fem.mesh->GetComm()) / - std::max(global_norm(right_hand_side, fem.mesh->GetComm()), - std::numeric_limits::epsilon()); + return global_norm(residual, fem.mesh->GetComm()) / + std::max(global_norm(right_hand_side, fem.mesh->GetComm()), std::numeric_limits::epsilon()); } -static AccuracyBudgetMetrics -measure_monopole_accuracy(mean_field::fem::FEM &fem, - const mfem::GridFunction &density, - const mfem::GridFunction &displacement, - const mean_field::physics::GravitySolution &solution, - const mfem::ParGridFunction &projected_potential, - const mfem::Vector &projected_gradient, - const double mass, const double stellar_radius) { - mfem::Vector solution_gradient; - solution.gradPhi.GetTrueDofs(solution_gradient); +static AccuracyBudgetMetrics measure_monopole_accuracy( + mean_field::fem::FEM &fem, + const mfem::GridFunction &density, + const mfem::GridFunction &displacement, + const mean_field::physics::GravitySolution &solution, + const mfem::ParGridFunction &projected_potential, + const mfem::Vector &projected_gradient, + const double mass, + const double stellar_radius +) { + mfem::Vector solution_gradient; + solution.gradPhi.GetTrueDofs(solution_gradient); - mfem::Vector solution_potential; - mfem::Vector projection_potential; - solution.phi.GetTrueDofs(solution_potential); - projected_potential.GetTrueDofs(projection_potential); + mfem::Vector solution_potential; + mfem::Vector projection_potential; + solution.phi.GetTrueDofs(solution_potential); + projected_potential.GetTrueDofs(projection_potential); - mfem::ParGridFunction projected_gradient_grid_function( - fem.gravityFluxFes.get()); - projected_gradient_grid_function.SetFromTrueDofs(projected_gradient); + mfem::ParGridFunction projected_gradient_grid_function(fem.gravityFluxFes.get()); + projected_gradient_grid_function.SetFromTrueDofs(projected_gradient); - double local_solution_gradient_error = 0.0; - double local_projection_gradient_error = 0.0; - double local_gradient_norm = 0.0; - double local_solution_potential_error = 0.0; - double local_projection_potential_error = 0.0; - double local_potential_norm = 0.0; + double local_solution_gradient_error = 0.0; + double local_projection_gradient_error = 0.0; + double local_gradient_norm = 0.0; + double local_solution_potential_error = 0.0; + double local_projection_potential_error = 0.0; + double local_potential_norm = 0.0; - const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; - const int quadrature_order = diagnostic_quadrature_order(fem); - mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( - *fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate); - mfem::Vector physical_position(3); - mfem::Vector analytic_gradient(3); - mfem::Vector solution_reference_gradient(3); - mfem::Vector projection_reference_gradient(3); - mfem::Vector solution_physical_gradient(3); - mfem::Vector projection_physical_gradient(3); + const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; + const int quadrature_order = diagnostic_quadrature_order(fem); + mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator( + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate + ); + mfem::Vector physical_position(3); + mfem::Vector analytic_gradient(3); + mfem::Vector solution_reference_gradient(3); + mfem::Vector projection_reference_gradient(3); + mfem::Vector solution_physical_gradient(3); + mfem::Vector projection_physical_gradient(3); - for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { - mfem::ElementTransformation *transformation = - fem.mesh->GetElementTransformation(element_id); - const mfem::IntegrationRule &rule = - mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); + for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { + mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id); + const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order); - for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); - ++quadrature_point_id) { - const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); - transformation->SetIntPoint(&point); - mean_field::mapping::MappingPointContext mapping_context; - MFEM_VERIFY( - mapping_evaluator.EvaluatePoint(*transformation, point, - mapping_context) == - mean_field::mapping::MappingStatus::valid, - "Invalid mapping in accuracy diagnostic."); - physical_position = mapping_context.physical_position; - const mfem::DenseMatrix &mapping_jacobian = - mapping_context.mapping_jacobian; - const double mapping_determinant = - mapping_context.mapping_determinant; - MFEM_VERIFY(mapping_determinant > 0.0, - "Non-positive mapping determinant in accuracy diagnostic."); + for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) { + const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id); + transformation->SetIntPoint(&point); + mean_field::mapping::MappingPointContext mapping_context; + MFEM_VERIFY( + mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) == + mean_field::mapping::MappingStatus::valid, + "Invalid mapping in accuracy diagnostic." + ); + physical_position = mapping_context.physical_position; + const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian; + const double mapping_determinant = mapping_context.mapping_determinant; + MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in accuracy diagnostic."); - const double radius = physical_position.Norml2(); - MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, - "Invalid radius in monopole diagnostic."); + const double radius = physical_position.Norml2(); + MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole diagnostic."); - analytic_gradient = physical_position; - double analytic_potential = 0.0; - if (transformation->Attribute == vacuum_attribute) { - analytic_gradient *= - mean_field::utils::G * mass / (radius * radius * radius); - analytic_potential = -mean_field::utils::G * mass / radius; - } else { - analytic_gradient *= mean_field::utils::G * mass / - (stellar_radius * stellar_radius * stellar_radius); - analytic_potential = - -mean_field::utils::G * mass * - (3.0 * stellar_radius * stellar_radius - radius * radius) / - (2.0 * stellar_radius * stellar_radius * stellar_radius); - } + analytic_gradient = physical_position; + double analytic_potential = 0.0; + if (transformation->Attribute == vacuum_attribute) { + analytic_gradient *= mean_field::utils::G * mass / (radius * radius * radius); + analytic_potential = -mean_field::utils::G * mass / radius; + } else { + analytic_gradient *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius); + analytic_potential = -mean_field::utils::G * mass * + (3.0 * stellar_radius * stellar_radius - radius * radius) / + (2.0 * stellar_radius * stellar_radius * stellar_radius); + } - solution.gradPhi.GetVectorValue(element_id, point, - solution_reference_gradient); - mapping_jacobian.Mult(solution_reference_gradient, - solution_physical_gradient); - solution_physical_gradient /= mapping_determinant; + solution.gradPhi.GetVectorValue(element_id, point, solution_reference_gradient); + mapping_jacobian.Mult(solution_reference_gradient, solution_physical_gradient); + solution_physical_gradient /= mapping_determinant; - projected_gradient_grid_function.GetVectorValue( - element_id, point, projection_reference_gradient); - mapping_jacobian.Mult(projection_reference_gradient, - projection_physical_gradient); - projection_physical_gradient /= mapping_determinant; + projected_gradient_grid_function.GetVectorValue(element_id, point, projection_reference_gradient); + mapping_jacobian.Mult(projection_reference_gradient, projection_physical_gradient); + projection_physical_gradient /= mapping_determinant; - const double solution_potential_value = - solution.phi.GetValue(element_id, point); - const double projection_potential_value = - projected_potential.GetValue(element_id, point); - const double weight = - point.weight * transformation->Weight() * mapping_determinant; + const double solution_potential_value = solution.phi.GetValue(element_id, point); + const double projection_potential_value = projected_potential.GetValue(element_id, point); + const double weight = point.weight * transformation->Weight() * mapping_determinant; - solution_physical_gradient -= analytic_gradient; - projection_physical_gradient -= analytic_gradient; - local_solution_gradient_error += - weight * (solution_physical_gradient * solution_physical_gradient); - local_projection_gradient_error += - weight * - (projection_physical_gradient * projection_physical_gradient); - local_gradient_norm += weight * (analytic_gradient * analytic_gradient); - local_solution_potential_error += - weight * (solution_potential_value - analytic_potential) * - (solution_potential_value - analytic_potential); - local_projection_potential_error += - weight * (projection_potential_value - analytic_potential) * - (projection_potential_value - analytic_potential); - local_potential_norm += weight * analytic_potential * analytic_potential; + solution_physical_gradient -= analytic_gradient; + projection_physical_gradient -= analytic_gradient; + local_solution_gradient_error += weight * (solution_physical_gradient * solution_physical_gradient); + local_projection_gradient_error += weight * (projection_physical_gradient * projection_physical_gradient); + local_gradient_norm += weight * (analytic_gradient * analytic_gradient); + local_solution_potential_error += weight * (solution_potential_value - analytic_potential) * + (solution_potential_value - analytic_potential); + local_projection_potential_error += weight * (projection_potential_value - analytic_potential) * + (projection_potential_value - analytic_potential); + local_potential_norm += weight * analytic_potential * analytic_potential; + } } - } - const std::array local_values{local_solution_gradient_error, - local_projection_gradient_error, - local_gradient_norm, - local_solution_potential_error, - local_projection_potential_error, - local_potential_norm}; - std::array global_values{}; - MPI_Allreduce(local_values.data(), global_values.data(), - static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, - fem.mesh->GetComm()); + const std::array local_values{ + local_solution_gradient_error, local_projection_gradient_error, local_gradient_norm, + local_solution_potential_error, local_projection_potential_error, local_potential_norm + }; + std::array global_values{}; + MPI_Allreduce( + local_values.data(), global_values.data(), static_cast(local_values.size()), MPI_DOUBLE, MPI_SUM, + fem.mesh->GetComm() + ); - const AccuracyBudgetEnergies energies = - measure_stellar_energies(fem, density, solution); - const double analytic_energy = - -3.0 * mean_field::utils::G * mass * mass / (5.0 * stellar_radius); + const AccuracyBudgetEnergies energies = measure_stellar_energies(fem, density, solution); + const double analytic_energy = -3.0 * mean_field::utils::G * mass * mass / (5.0 * stellar_radius); - REQUIRE(global_values[2] > 0.0); - REQUIRE(global_values[5] > 0.0); + REQUIRE(global_values[2] > 0.0); + REQUIRE(global_values[5] > 0.0); - AccuracyBudgetMetrics metrics; - metrics.direct_relative_residual = - reduced_gravity_relative_residual(fem, density, displacement, solution); - metrics.gradient_relative_error = - std::sqrt(global_values[0] / global_values[2]); - metrics.gradient_projection_relative_error = - std::sqrt(global_values[1] / global_values[2]); - metrics.gradient_solution_projection_gap = mapped_hdiv_relative_gap( - fem, displacement, solution_gradient, projected_gradient); - metrics.potential_relative_error = - std::sqrt(global_values[3] / global_values[5]); - metrics.potential_projection_relative_error = - std::sqrt(global_values[4] / global_values[5]); - mfem::Vector potential_difference(solution_potential); - potential_difference -= projection_potential; - const double projection_potential_norm = - global_norm(projection_potential, fem.gravityPotentialFes->GetComm()); - REQUIRE(projection_potential_norm > 0.0); - metrics.potential_solution_projection_gap = - global_norm(potential_difference, fem.gravityPotentialFes->GetComm()) / - projection_potential_norm; - metrics.binding_relative_error = - std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy); - metrics.virial_relative_error = - std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy); - metrics.virial_consistency_error = - std::abs(energies.binding - energies.virial) / - std::max(std::abs(energies.binding), - std::numeric_limits::epsilon()); - return metrics; + AccuracyBudgetMetrics metrics; + metrics.direct_relative_residual = reduced_gravity_relative_residual(fem, density, displacement, solution); + metrics.gradient_relative_error = std::sqrt(global_values[0] / global_values[2]); + metrics.gradient_projection_relative_error = std::sqrt(global_values[1] / global_values[2]); + metrics.gradient_solution_projection_gap = + mapped_hdiv_relative_gap(fem, displacement, solution_gradient, projected_gradient); + metrics.potential_relative_error = std::sqrt(global_values[3] / global_values[5]); + metrics.potential_projection_relative_error = std::sqrt(global_values[4] / global_values[5]); + mfem::Vector potential_difference(solution_potential); + potential_difference -= projection_potential; + const double projection_potential_norm = global_norm(projection_potential, fem.gravityPotentialFes->GetComm()); + REQUIRE(projection_potential_norm > 0.0); + metrics.potential_solution_projection_gap = + global_norm(potential_difference, fem.gravityPotentialFes->GetComm()) / projection_potential_norm; + metrics.binding_relative_error = std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy); + metrics.virial_relative_error = std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy); + metrics.virial_consistency_error = std::abs(energies.binding - energies.virial) / + std::max(std::abs(energies.binding), std::numeric_limits::epsilon()); + return metrics; } -static void run_monopole_case(const std::string &sweep_name, - const std::string &case_name, - mean_field::utils::Args args, - const double solver_tolerance, - const int quadrature_boost) { - args.p.rtol = solver_tolerance; - args.p.atol = std::min(args.p.atol, solver_tolerance * 1.0e-2); - args.p.max_iters = std::max(args.p.max_iters, 2000); - args.quadrature.global_boost = quadrature_boost; +static void run_monopole_case( + const std::string &sweep_name, + const std::string &case_name, + mean_field::utils::Args args, + const double solver_tolerance, + const int quadrature_boost +) { + args.p.rtol = solver_tolerance; + args.p.atol = std::min(args.p.atol, solver_tolerance * 1.0e-2); + args.p.max_iters = std::max(args.p.max_iters, 2000); + args.quadrature.global_boost = quadrature_boost; - mean_field::fem::FEM fem = - mean_field::fem::setup_fem(args.mesh_file, args, 0); - REQUIRE(fem.domainMapperStateless != nullptr); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.domainMapperStateless != nullptr); - const double stellar_radius = mean_field::utils::RADIUS; - const double mass = mean_field::utils::MASS; - const double density_value = mass / ((4.0 / 3.0) * M_PI * stellar_radius * - stellar_radius * stellar_radius); + const double stellar_radius = mean_field::utils::RADIUS; + const double mass = mean_field::utils::MASS; + const double density_value = mass / ((4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius); - mfem::ParGridFunction displacement(fem.displacementFes.get()); - displacement = 0.0; - *fem.displacement = 0.0; - mfem::GridFunction density(fem.densityFes.get()); - density = density_value; - zero_vacuum_density(fem, density); - mean_field::analysis::conserve_mass(fem, density, mass); - fem.com = mean_field::analysis::get_com(fem, density); - fem.Q = mean_field::physics::compute_quadrupole_moment_tensor(fem, density, - fem.com); + mfem::ParGridFunction displacement(fem.displacementFes.get()); + displacement = 0.0; + *fem.displacement = 0.0; + mfem::GridFunction density(fem.densityFes.get()); + density = density_value; + zero_vacuum_density(fem, density); + mean_field::analysis::conserve_mass(fem, density, mass); + fem.com = mean_field::analysis::get_com(fem, density); + fem.Q = mean_field::physics::compute_quadrupole_moment_tensor(fem, density, fem.com); - const mean_field::physics::GravitySolution solution = - mean_field::physics::solve_gravity_field(fem, args, density, - displacement); + const mean_field::physics::GravitySolution solution = + mean_field::physics::solve_gravity_field(fem, args, density, displacement); - auto analytic_potential = [mass, - stellar_radius](const mfem::Vector &position) { - const double radius = position.Norml2(); - if (radius >= stellar_radius) { - return -mean_field::utils::G * mass / radius; + auto analytic_potential = [mass, stellar_radius](const mfem::Vector &position) { + const double radius = position.Norml2(); + if (radius >= stellar_radius) { + return -mean_field::utils::G * mass / radius; + } + return -mean_field::utils::G * mass * (3.0 * stellar_radius * stellar_radius - radius * radius) / + (2.0 * stellar_radius * stellar_radius * stellar_radius); + }; + mean_field::mapping::PhysicalPositionFunctionCoefficient potential_coefficient( + *fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, analytic_potential + ); + mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get()); + projected_potential.ProjectCoefficient(potential_coefficient); + + const mfem::Vector projected_gradient = project_monopole_gradient(fem, displacement, mass, stellar_radius); + + const AccuracyBudgetMetrics metrics = measure_monopole_accuracy( + fem, density, displacement, solution, projected_potential, projected_gradient, mass, stellar_radius + ); + + REQUIRE(std::isfinite(metrics.direct_relative_residual)); + REQUIRE(std::isfinite(metrics.gradient_relative_error)); + REQUIRE(std::isfinite(metrics.potential_relative_error)); + REQUIRE(std::isfinite(metrics.virial_consistency_error)); + + record_experiment_result( + sweep_name, case_name, + {{"solver_rtol", std::to_string(solver_tolerance)}, + {"quadrature_global_boost", std::to_string(quadrature_boost)}, + {"mesh_file", args.mesh_file}}, + {{"direct_relative_residual", metrics.direct_relative_residual}, + {"gradient_relative_error", metrics.gradient_relative_error}, + {"gradient_projection_relative_error", metrics.gradient_projection_relative_error}, + {"gradient_solution_projection_gap", metrics.gradient_solution_projection_gap}, + {"potential_relative_error", metrics.potential_relative_error}, + {"potential_projection_relative_error", metrics.potential_projection_relative_error}, + {"potential_solution_projection_gap", metrics.potential_solution_projection_gap}, + {"binding_relative_error", metrics.binding_relative_error}, + {"virial_relative_error", metrics.virial_relative_error}, + {"virial_consistency_error", metrics.virial_consistency_error}} + ); +} + +TEST_CASE( + "Uniform Monopole Accuracy Budget: Solver Tolerance", + tags::gravity_analytic_accuracy +) { + const mean_field::utils::Args args = test_utils::setup_args(); + constexpr std::array solver_tolerances{1.0e-8, 1.0e-10, 1.0e-12, 1.0e-14}; + + for (const double solver_tolerance : solver_tolerances) { + run_monopole_case("solver_tolerance", "uniform_monopole", args, solver_tolerance, 0); } - return -mean_field::utils::G * mass * - (3.0 * stellar_radius * stellar_radius - radius * radius) / - (2.0 * stellar_radius * stellar_radius * stellar_radius); - }; - mean_field::mapping::PhysicalPositionFunctionCoefficient - potential_coefficient(*fem.domainMapperStateless, *fem.displacement, - *fem.compactificationCoordinate, - analytic_potential); - mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get()); - projected_potential.ProjectCoefficient(potential_coefficient); - - const mfem::Vector projected_gradient = - project_monopole_gradient(fem, displacement, mass, stellar_radius); - - const AccuracyBudgetMetrics metrics = measure_monopole_accuracy( - fem, density, displacement, solution, projected_potential, - projected_gradient, mass, stellar_radius); - - REQUIRE(std::isfinite(metrics.direct_relative_residual)); - REQUIRE(std::isfinite(metrics.gradient_relative_error)); - REQUIRE(std::isfinite(metrics.potential_relative_error)); - REQUIRE(std::isfinite(metrics.virial_consistency_error)); - - record_experiment_result( - sweep_name, case_name, - {{"solver_rtol", std::to_string(solver_tolerance)}, - {"quadrature_global_boost", std::to_string(quadrature_boost)}, - {"mesh_file", args.mesh_file}}, - {{"direct_relative_residual", metrics.direct_relative_residual}, - {"gradient_relative_error", metrics.gradient_relative_error}, - {"gradient_projection_relative_error", - metrics.gradient_projection_relative_error}, - {"gradient_solution_projection_gap", - metrics.gradient_solution_projection_gap}, - {"potential_relative_error", metrics.potential_relative_error}, - {"potential_projection_relative_error", - metrics.potential_projection_relative_error}, - {"potential_solution_projection_gap", - metrics.potential_solution_projection_gap}, - {"binding_relative_error", metrics.binding_relative_error}, - {"virial_relative_error", metrics.virial_relative_error}, - {"virial_consistency_error", metrics.virial_consistency_error}}); } -TEST_CASE("Uniform Monopole Accuracy Budget: Solver Tolerance", - tags::gravity_analytic_accuracy) { - const mean_field::utils::Args args = test_utils::setup_args(); - constexpr std::array solver_tolerances{1.0e-8, 1.0e-10, 1.0e-12, - 1.0e-14}; +TEST_CASE( + "Uniform Monopole Accuracy Budget: Quadrature", + tags::gravity_analytic_accuracy +) { + const mean_field::utils::Args args = test_utils::setup_args(); + constexpr std::array quadrature_boosts{0, 4, 8}; - for (const double solver_tolerance : solver_tolerances) { - run_monopole_case("solver_tolerance", "uniform_monopole", args, - solver_tolerance, 0); - } + for (const int quadrature_boost : quadrature_boosts) { + run_monopole_case("quadrature", "uniform_monopole", args, 1.0e-13, quadrature_boost); + } } -TEST_CASE("Uniform Monopole Accuracy Budget: Quadrature", - tags::gravity_analytic_accuracy) { - const mean_field::utils::Args args = test_utils::setup_args(); - constexpr std::array quadrature_boosts{0, 4, 8}; - - for (const int quadrature_boost : quadrature_boosts) { - run_monopole_case("quadrature", "uniform_monopole", args, 1.0e-13, - quadrature_boost); - } -} - -TEST_CASE("Uniform Monopole Accuracy Budget: Projection Decomposition", - tags::gravity_analytic_accuracy) { - run_monopole_case("projection_decomposition", "uniform_monopole", - test_utils::setup_args(), 1.0e-13, 0); +TEST_CASE( + "Uniform Monopole Accuracy Budget: Projection Decomposition", + tags::gravity_analytic_accuracy +) { + run_monopole_case("projection_decomposition", "uniform_monopole", test_utils::setup_args(), 1.0e-13, 0); } diff --git a/experiments/gravity_completed_rigid_motion.cpp b/experiments/gravity_completed_rigid_motion.cpp index 2e284fe..87376ad 100644 --- a/experiments/gravity_completed_rigid_motion.cpp +++ b/experiments/gravity_completed_rigid_motion.cpp @@ -45,10 +45,8 @@ namespace { ); m_stellarOperator.GetGravityOperator().ApplyGravityUnknowns( - gravityGradientDirection, - gravityPotentialDirection, - m_stellarOperator.GetGravityContext().GetGeometryContext(), - gravityAction + gravityGradientDirection, gravityPotentialDirection, + m_stellarOperator.GetGravityContext().GetGeometryContext(), gravityAction ); } @@ -113,23 +111,11 @@ namespace { ); } - 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]" + "Gravity-Completed Reduced Surface Mode Responses Of The Stellar Equilibrium Jacobian", + "[null_space][surface_modes][gravity_completed]" ) { mean_field::utils::Args args = test_utils::setup_args(); args.p.rtol = 1.0e-11; @@ -141,7 +127,7 @@ TEST_CASE( int rank = 0; MPI_Comm_rank(communicator, &rank); - const auto modes = experiment::null_space::make_rigid_modes(fixture); + const auto modes = experiment::null_space::make_surface_modes(fixture); constexpr std::array rotationFractions{0.0, 0.5}; const int totalCases = static_cast(rotationFractions.size() * modes.size()); int completedCases = 0; @@ -163,16 +149,16 @@ TEST_CASE( gravitySolver.SetMaxIter(2000); gravitySolver.SetPrintLevel(1); - for (const experiment::null_space::RigidMode &mode : modes) { + for (const experiment::null_space::SurfaceMode &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); + const mfem::Vector surfaceOnlyAction = fixture.jacobian_action(mode.direction); mfem::Vector gravityRightHandSide = - gravity_residual_blocks(displacementOnlyAction, fixture.stellar_operator().GetLayout()); + gravity_residual_blocks(surfaceOnlyAction, fixture.stellar_operator().GetLayout()); gravityRightHandSide *= -1.0; mfem::Vector gravityCompletion(gravityUnknownJacobian.Width()); @@ -201,25 +187,14 @@ TEST_CASE( 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; + const mfem::Vector completedAction = fixture.jacobian_action(completedDirection); std::map metrics{ - {"displacement_only_input_norm", experiment::null_space::global_norm(mode.direction, communicator)}, + {"surface_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)}, + {"surface_only_action_norm", experiment::null_space::global_norm(surfaceOnlyAction, communicator)}, + {"gravity_completed_action_norm", experiment::null_space::global_norm(completedAction, communicator)}, {"gravity_solve_rhs_norm", gravityRightHandSideNorm}, {"gravity_solve_residual_norm", gravitySolveResidualNorm}, {"gravity_solve_relative_residual", gravitySolveRelativeResidual}, @@ -228,29 +203,22 @@ TEST_CASE( }; add_block_metrics( - metrics, "displacement_only_", + metrics, "surface_only_", experiment::null_space::residual_block_norms( - displacementOnlyAction, fixture.stellar_operator().GetLayout(), communicator + surfaceOnlyAction, fixture.stellar_operator().GetLayout(), communicator ) ); add_block_metrics( - metrics, "gravity_completed_unpinned_", + metrics, "gravity_completed_", 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 + completedAction, 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"}, + "gravity_completed_reduced_surface_modes", mode.name, + {{"mode_kind", experiment::null_space::surface_mode_kind_name(mode.kind)}, {"axis", std::to_string(mode.axis)}, {"rotation_fraction_of_keplerian", std::to_string(rotationFraction)}, {"mesh_file", test_utils::setup_args().mesh_file}, @@ -262,12 +230,12 @@ TEST_CASE( ++completedCases; experiment::null_space::report_progress( communicator, "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) + - " gravity-completed rigid-mode cases" + " gravity-completed reduced surface-mode cases" ); } } experiment::null_space::report_progress( - communicator, "gravity-completed rigid-motion probe complete; writing CSV output" + communicator, "gravity-completed reduced surface-mode probe complete; writing CSV output" ); } diff --git a/experiments/rigid_motion_null_space.cpp b/experiments/rigid_motion_null_space.cpp index b41a1c8..6484260 100644 --- a/experiments/rigid_motion_null_space.cpp +++ b/experiments/rigid_motion_null_space.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include #include @@ -16,6 +18,32 @@ import mean_field; import test_helpers; namespace { + struct DeterminantPolynomial final { + double linear{0.0}; + double quadratic{0.0}; + double cubic{0.0}; + }; + + struct CriticalAmplitude final { + double magnitude{0.0}; + double determinant{1.0}; + bool searchLimitReached{false}; + }; + + struct SymmetricFiniteDifferenceStep final { + double step{0.0}; + double positiveMinimumDeterminant{0.0}; + double negativeMinimumDeterminant{0.0}; + }; + + struct PolynomialRoots final { + std::array values{ + std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN() + }; + int count{0}; + }; + [[nodiscard]] double relative_difference( const mfem::Vector &computed, const mfem::Vector &reference, @@ -43,11 +71,234 @@ namespace { metrics.emplace(prefix + experiment::null_space::residualBlockNames[block] + "_norm", norms[block]); } } + + [[nodiscard]] std::vector collect_determinant_polynomials( + const mean_field::fem::FEM &fem, + const mfem::Vector &unitVolumeDirection + ) { + MFEM_VERIFY(fem.mesh->SpaceDimension() == 3, "The spherical-harmonic frequency probe requires 3D geometry."); + + mfem::ParGridFunction displacement(fem.displacementFes.get()); + displacement.SetFromTrueDofs(unitVolumeDirection); + + std::vector polynomials; + polynomials.reserve(static_cast(fem.mesh->GetNE()) * 64); + for (int element = 0; element < fem.mesh->GetNE(); ++element) { + mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element); + const mfem::FiniteElement *finiteElement = fem.displacementFes->GetFE(element); + const int integrationOrder = + std::max(finiteElement->GetOrder() + 2, 2 * fem.mesh->SpaceDimension() * finiteElement->GetOrder()); + const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), integrationOrder); + + for (int point = 0; point < rule.GetNPoints(); ++point) { + transformation->SetIntPoint(&rule.IntPoint(point)); + mfem::DenseMatrix gradient; + displacement.GetVectorGradient(*transformation, gradient); + + double trace = 0.0; + double traceSquared = 0.0; + for (int row = 0; row < 3; ++row) { + trace += gradient(row, row); + for (int column = 0; column < 3; ++column) { + traceSquared += gradient(row, column) * gradient(column, row); + } + } + + polynomials.push_back( + {.linear = trace, .quadratic = 0.5 * (trace * trace - traceSquared), .cubic = gradient.Det()} + ); + } + } + return polynomials; + } + + [[nodiscard]] double global_minimum_determinant( + const std::vector &polynomials, + const double amplitude, + const MPI_Comm communicator + ) { + double localMinimum = std::numeric_limits::infinity(); + for (const DeterminantPolynomial &polynomial : polynomials) { + const double determinant = + 1.0 + + amplitude * (polynomial.linear + amplitude * (polynomial.quadratic + amplitude * polynomial.cubic)); + localMinimum = std::min(localMinimum, determinant); + } + + double globalMinimum = std::numeric_limits::infinity(); + MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator); + return globalMinimum; + } + + [[nodiscard]] double evaluate( + const DeterminantPolynomial &polynomial, + const double amplitude + ) { + return 1.0 + + amplitude * (polynomial.linear + amplitude * (polynomial.quadratic + amplitude * polynomial.cubic)); + } + + void append_root( + PolynomialRoots &roots, + const double root + ) { + if (roots.count < static_cast(roots.values.size()) && std::isfinite(root)) { + roots.values[static_cast(roots.count++)] = root; + } + } + + [[nodiscard]] PolynomialRoots real_roots(const DeterminantPolynomial &polynomial) { + PolynomialRoots roots; + const double coefficientScale = + std::max({1.0, std::abs(polynomial.linear), std::abs(polynomial.quadratic), std::abs(polynomial.cubic)}); + const double tolerance = 64.0 * std::numeric_limits::epsilon() * coefficientScale; + + if (std::abs(polynomial.cubic) <= tolerance) { + if (std::abs(polynomial.quadratic) <= tolerance) { + if (std::abs(polynomial.linear) > tolerance) { + append_root(roots, -1.0 / polynomial.linear); + } + return roots; + } + + const double discriminant = polynomial.linear * polynomial.linear - 4.0 * polynomial.quadratic; + const double discriminantTolerance = + 64.0 * std::numeric_limits::epsilon() * std::max(1.0, polynomial.linear * polynomial.linear); + if (discriminant < -discriminantTolerance) { + return roots; + } + + const double squareRoot = std::sqrt(std::max(0.0, discriminant)); + const double stableNumerator = -0.5 * (polynomial.linear + std::copysign(squareRoot, polynomial.linear)); + if (stableNumerator == 0.0) { + append_root(roots, -polynomial.linear / (2.0 * polynomial.quadratic)); + } else { + append_root(roots, stableNumerator / polynomial.quadratic); + if (squareRoot > std::sqrt(discriminantTolerance)) { + append_root(roots, 1.0 / stableNumerator); + } + } + return roots; + } + + const double quadratic = polynomial.quadratic / polynomial.cubic; + const double linear = polynomial.linear / polynomial.cubic; + const double constant = 1.0 / polynomial.cubic; + const double depressedLinear = linear - quadratic * quadratic / 3.0; + const double depressedConstant = + 2.0 * quadratic * quadratic * quadratic / 27.0 - quadratic * linear / 3.0 + constant; + const double halfConstant = 0.5 * depressedConstant; + const double thirdLinear = depressedLinear / 3.0; + const double discriminant = halfConstant * halfConstant + thirdLinear * thirdLinear * thirdLinear; + const double discriminantTolerance = + 128.0 * std::numeric_limits::epsilon() * + std::max({1.0, std::abs(halfConstant * halfConstant), std::abs(thirdLinear * thirdLinear * thirdLinear)}); + const double shift = quadratic / 3.0; + + if (discriminant > discriminantTolerance) { + const double squareRoot = std::sqrt(discriminant); + append_root(roots, std::cbrt(-halfConstant + squareRoot) + std::cbrt(-halfConstant - squareRoot) - shift); + } else if (std::abs(depressedLinear) <= tolerance || thirdLinear >= 0.0) { + append_root(roots, std::cbrt(-depressedConstant) - shift); + } else { + const double radius = 2.0 * std::sqrt(std::max(0.0, -thirdLinear)); + const double cosineArgument = std::clamp( + -halfConstant / std::sqrt(std::max(0.0, -thirdLinear * thirdLinear * thirdLinear)), -1.0, 1.0 + ); + const double phase = std::acos(cosineArgument) / 3.0; + constexpr double twoPiOverThree = 2.0943951023931954923; + for (int root = 0; root < 3; ++root) { + append_root(roots, radius * std::cos(phase - twoPiOverThree * static_cast(root)) - shift); + } + } + + for (int root = 0; root < roots.count; ++root) { + double &value = roots.values[static_cast(root)]; + for (int iteration = 0; iteration < 3; ++iteration) { + const double derivative = + polynomial.linear + value * (2.0 * polynomial.quadratic + 3.0 * value * polynomial.cubic); + if (std::abs(derivative) <= tolerance) { + break; + } + value -= evaluate(polynomial, value) / derivative; + } + } + return roots; + } + + [[nodiscard]] CriticalAmplitude find_critical_amplitude( + const std::vector &polynomials, + const double sign, + const MPI_Comm communicator + ) { + constexpr double maximumSearchMagnitude = 0.5; + MFEM_VERIFY(sign == 1.0 || sign == -1.0, "The critical-amplitude direction must be positive or negative."); + + double localCriticalMagnitude = std::numeric_limits::infinity(); + for (const DeterminantPolynomial &polynomial : polynomials) { + const PolynomialRoots roots = real_roots(polynomial); + for (int root = 0; root < roots.count; ++root) { + const double signedMagnitude = sign * roots.values[static_cast(root)]; + if (signedMagnitude > 0.0) { + localCriticalMagnitude = std::min(localCriticalMagnitude, signedMagnitude); + } + } + } + + double globalCriticalMagnitude = std::numeric_limits::infinity(); + MPI_Allreduce(&localCriticalMagnitude, &globalCriticalMagnitude, 1, MPI_DOUBLE, MPI_MIN, communicator); + + if (!std::isfinite(globalCriticalMagnitude) || globalCriticalMagnitude > maximumSearchMagnitude) { + return { + .magnitude = maximumSearchMagnitude, + .determinant = global_minimum_determinant(polynomials, sign * maximumSearchMagnitude, communicator), + .searchLimitReached = true + }; + } + + return { + .magnitude = globalCriticalMagnitude, + .determinant = global_minimum_determinant(polynomials, sign * globalCriticalMagnitude, communicator), + .searchLimitReached = false + }; + } + + [[nodiscard]] SymmetricFiniteDifferenceStep find_symmetric_finite_difference_step( + const mean_field::deformation::PreparedDomainDeformationRuntime &deformation, + const mfem::Vector &unitVolumeDirection + ) { + constexpr double requestedStep = 1.0e-4; + constexpr double minimumStep = 1.0e-10; + + mfem::Vector trialVolumeDirection(unitVolumeDirection.Size()); + for (double step = requestedStep; step >= minimumStep; step *= 0.25) { + trialVolumeDirection = unitVolumeDirection; + trialVolumeDirection *= step; + const mean_field::deformation::DomainDeformationGeometryReport positive = + deformation.inspectMappedGeometry(trialVolumeDirection); + + trialVolumeDirection *= -1.0; + const mean_field::deformation::DomainDeformationGeometryReport negative = + deformation.inspectMappedGeometry(trialVolumeDirection); + + if (positive.isOrientationPreserving() && negative.isOrientationPreserving()) { + return { + .step = step, + .positiveMinimumDeterminant = positive.minimumJacobianDeterminant, + .negativeMinimumDeterminant = negative.minimumJacobianDeterminant + }; + } + } + + throw std::domain_error( + "No symmetric orientation-preserving finite-difference step was found for the surface mode." + ); + } } // namespace TEST_CASE( - "Rigid Motion Responses Of The Stellar Equilibrium Jacobian", - "[null_space][rigid_motion]" + "Reduced Surface Mode Reachability And Stellar Equilibrium Linearization", + "[null_space][surface_modes][reachability][linearization]" ) { mean_field::utils::Args args = test_utils::setup_args(); args.p.rtol = 1.0e-12; @@ -59,9 +310,8 @@ TEST_CASE( int rank = 0; MPI_Comm_rank(communicator, &rank); - const auto modes = experiment::null_space::make_rigid_modes(fixture); + const auto modes = experiment::null_space::make_surface_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; @@ -69,59 +319,53 @@ TEST_CASE( 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(); + const mfem::Vector baseResidual = fixture.residual(); + REQUIRE(std::isfinite(experiment::null_space::global_norm(baseResidual, communicator))); - 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) { + for (const experiment::null_space::SurfaceMode &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); + const mfem::Vector action = fixture.jacobian_action(mode.direction); + const mfem::Vector liftedDirection = fixture.lifted_surface_direction(mode.direction); - 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); + const double inputNorm = experiment::null_space::global_norm(mode.direction, communicator); + const double actionNorm = experiment::null_space::global_norm(action, communicator); + const double liftNorm = experiment::null_space::global_norm(liftedDirection, communicator); + const SymmetricFiniteDifferenceStep coarseStep = find_symmetric_finite_difference_step( + fixture.stellar_operator().GetDomainDeformation(), liftedDirection + ); + const std::array finiteDifferenceSteps{coarseStep.step, 1.0e-2 * coarseStep.step}; REQUIRE(inputNorm > 0.0); - REQUIRE(std::isfinite(unpinnedNorm)); - REQUIRE(std::isfinite(constrainedNorm)); + REQUIRE(liftNorm > 0.0); + REQUIRE(std::isfinite(actionNorm)); 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)} + {"surface_parameter_input_norm", inputNorm}, + {"lifted_volume_displacement_norm", liftNorm}, + {"lift_amplification", liftNorm / inputNorm}, + {"root_jacobian_action_norm", actionNorm}, + {"root_action_per_surface_parameter_norm", actionNorm / inputNorm}, + {"root_action_per_lifted_volume_norm", actionNorm / liftNorm}, + {"base_residual_norm", experiment::null_space::global_norm(baseResidual, communicator)}, + {"surface_parameter_count", + static_cast(fixture.stellar_operator().GetDomainDeformation().parameterCount())}, + {"volume_displacement_count", + static_cast(fixture.stellar_operator().GetDomainDeformation().volumeDisplacementSize())}, + {"finite_difference_coarse_step", finiteDifferenceSteps[0]}, + {"finite_difference_fine_step", finiteDifferenceSteps[1]}, + {"coarse_step_positive_minimum_determinant", coarseStep.positiveMinimumDeterminant}, + {"coarse_step_negative_minimum_determinant", coarseStep.negativeMinimumDeterminant} }; add_block_metrics( - metrics, "unpinned_", + metrics, "root_", 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 + action, fixture.stellar_operator().GetLayout(), communicator ) ); @@ -129,21 +373,21 @@ TEST_CASE( mfem::Vector plusState(fixture.state()); plusState.Add(step, mode.direction); fixture.prepare(plusState, rotation); - const mfem::Vector plusResidual = fixture.unpinned_residual(); + const mfem::Vector plusResidual = fixture.residual(); mfem::Vector minusState(fixture.state()); minusState.Add(-step, mode.direction); fixture.prepare(minusState, rotation); - const mfem::Vector minusResidual = fixture.unpinned_residual(); + const mfem::Vector minusResidual = fixture.residual(); mfem::Vector finiteDifference(plusResidual); finiteDifference -= minusResidual; finiteDifference /= 2.0 * step; - const std::string stepName = step == finiteDifferenceSteps.front() ? "1e-4" : "1e-6"; + const std::string stepName = step == finiteDifferenceSteps.front() ? "coarse" : "fine"; metrics.emplace( "finite_difference_relative_error_" + stepName, - relative_difference(unpinnedAction, finiteDifference, communicator) + relative_difference(action, finiteDifference, communicator) ); } @@ -151,9 +395,8 @@ TEST_CASE( 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"}, + "reduced_surface_mode_reachability", mode.name, + {{"mode_kind", experiment::null_space::surface_mode_kind_name(mode.kind)}, {"axis", std::to_string(mode.axis)}, {"rotation_fraction_of_keplerian", std::to_string(rotationFraction)}, {"mesh_file", test_utils::setup_args().mesh_file}, @@ -164,11 +407,123 @@ TEST_CASE( ++completedCases; experiment::null_space::report_progress( - communicator, - "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) + " rigid-mode cases" + communicator, "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) + + " reduced surface-mode cases" ); } } - experiment::null_space::report_progress(communicator, "rigid-motion probe complete; writing CSV output"); + experiment::null_space::report_progress(communicator, "reduced surface-mode probe complete; writing CSV output"); +} + +TEST_CASE( + "Spherical Harmonic Surface Frequencies Preserve Orientation Up To Measured Critical Amplitudes", + "[surface_modes][frequency_limit][geometry][spherical_harmonic]" +) { + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + experiment::null_space::Model model = experiment::null_space::make_model(); + auto deformation = model.compileDomainDeformation(fem); + const auto &surface = deformation.surfaceDeformationPrescription(); + const MPI_Comm communicator = fem.mesh->GetComm(); + int rank = 0; + MPI_Comm_rank(communicator, &rank); + + constexpr std::array angularDegrees{0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20}; + mfem::Vector zeroParameters(surface.parameterCount()); + zeroParameters = 0.0; + + for (std::size_t degreeIndex = 0; degreeIndex < angularDegrees.size(); ++degreeIndex) { + const int angularDegree = angularDegrees[degreeIndex]; + experiment::null_space::report_progress( + communicator, "measuring zonal spherical-harmonic degree " + std::to_string(angularDegree) + " (" + + std::to_string(degreeIndex + 1) + "/" + std::to_string(angularDegrees.size()) + ")" + ); + + mfem::Vector parameters(surface.parameterCount()); + double localMaximumAngularMagnitude = 0.0; + for (int parameter = 0; parameter < parameters.Size(); ++parameter) { + const double angularValue = + experiment::null_space::zonal_legendre(angularDegree, surface.radialDirection(parameter, 2)); + parameters(parameter) = surface.referenceRadius(parameter) * angularValue; + localMaximumAngularMagnitude = std::max(localMaximumAngularMagnitude, std::abs(angularValue)); + } + + double globalMaximumAngularMagnitude = 0.0; + MPI_Allreduce( + &localMaximumAngularMagnitude, &globalMaximumAngularMagnitude, 1, MPI_DOUBLE, MPI_MAX, communicator + ); + REQUIRE(globalMaximumAngularMagnitude > 0.0); + parameters /= globalMaximumAngularMagnitude; + + mfem::Vector unitVolumeDirection(deformation.volumeDisplacementSize()); + deformation.applyJacobian(zeroParameters, parameters, unitVolumeDirection); + const std::vector determinantPolynomials = + collect_determinant_polynomials(fem, unitVolumeDirection); + + long long localSampleCount = static_cast(determinantPolynomials.size()); + long long globalSampleCount = 0; + MPI_Allreduce(&localSampleCount, &globalSampleCount, 1, MPI_LONG_LONG, MPI_SUM, communicator); + REQUIRE(globalSampleCount > 0); + + const CriticalAmplitude positiveCritical = find_critical_amplitude(determinantPolynomials, 1.0, communicator); + const CriticalAmplitude negativeCritical = find_critical_amplitude(determinantPolynomials, -1.0, communicator); + + const double determinantPositive1e4 = global_minimum_determinant(determinantPolynomials, 1.0e-4, communicator); + const double determinantNegative1e4 = global_minimum_determinant(determinantPolynomials, -1.0e-4, communicator); + const double determinantPositive1e3 = global_minimum_determinant(determinantPolynomials, 1.0e-3, communicator); + const double determinantNegative1e3 = global_minimum_determinant(determinantPolynomials, -1.0e-3, communicator); + const double determinantPositive1e2 = global_minimum_determinant(determinantPolynomials, 1.0e-2, communicator); + const double determinantNegative1e2 = global_minimum_determinant(determinantPolynomials, -1.0e-2, communicator); + + if (angularDegree == 12) { + mfem::Vector directInspectionDirection(unitVolumeDirection); + directInspectionDirection *= 1.0e-3; + const mean_field::deformation::DomainDeformationGeometryReport directInspection = + deformation.inspectMappedGeometry(directInspectionDirection); + const double comparisonScale = std::max( + {1.0, std::abs(directInspection.minimumJacobianDeterminant), std::abs(determinantPositive1e3)} + ); + CHECK( + std::abs(directInspection.minimumJacobianDeterminant - determinantPositive1e3) <= + 1.0e-11 * comparisonScale + ); + } + + REQUIRE(std::isfinite(positiveCritical.magnitude)); + REQUIRE(std::isfinite(negativeCritical.magnitude)); + REQUIRE(positiveCritical.magnitude > 0.0); + REQUIRE(negativeCritical.magnitude > 0.0); + + if (rank == 0) { + experiment::record_experiment_result( + "spherical_harmonic_surface_frequency_limit", "zonal_l" + std::to_string(angularDegree), + {{"angular_degree", std::to_string(angularDegree)}, + {"azimuthal_order", "0"}, + {"positive_limit_censored", positiveCritical.searchLimitReached ? "true" : "false"}, + {"negative_limit_censored", negativeCritical.searchLimitReached ? "true" : "false"}, + {"mesh_file", test_utils::setup_args().mesh_file}}, + {{"positive_critical_fractional_amplitude", positiveCritical.magnitude}, + {"negative_critical_fractional_amplitude", negativeCritical.magnitude}, + {"positive_critical_determinant", positiveCritical.determinant}, + {"negative_critical_determinant", negativeCritical.determinant}, + {"minimum_determinant_positive_1e-4", determinantPositive1e4}, + {"minimum_determinant_negative_1e-4", determinantNegative1e4}, + {"minimum_determinant_positive_1e-3", determinantPositive1e3}, + {"minimum_determinant_negative_1e-3", determinantNegative1e3}, + {"minimum_determinant_positive_1e-2", determinantPositive1e2}, + {"minimum_determinant_negative_1e-2", determinantNegative1e2}, + {"surface_parameter_norm", experiment::null_space::global_norm(parameters, communicator)}, + {"lifted_volume_displacement_norm", + experiment::null_space::global_norm(unitVolumeDirection, communicator)}, + {"global_geometry_sample_count", static_cast(globalSampleCount)}} + ); + } + } + + experiment::null_space::report_progress( + communicator, "spherical-harmonic frequency-limit probe complete; writing CSV output" + ); } diff --git a/experiments/stellar_null_space.cppm b/experiments/stellar_null_space.cppm index 5f81494..cf731ee 100644 --- a/experiments/stellar_null_space.cppm +++ b/experiments/stellar_null_space.cppm @@ -8,6 +8,7 @@ module; #include #include #include +#include #include #include @@ -18,14 +19,15 @@ import mean_field; import test_helpers; export namespace experiment::null_space { - using Form = mean_field::utils::blocks::barotropic_equilibrium_form; + using Form = mean_field::utils::blocks::surface_deformed_stellar_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 surfaceDeformationValue = mean_field::utils::blocks::get_value_block( + mean_field::utils::blocks::surface_deformation_field.parameters_term + ); constexpr auto gravityGradientValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialValue = @@ -42,8 +44,8 @@ export namespace experiment::null_space { 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 surfaceShapeResidual = mean_field::utils::blocks::get_residual_block( + mean_field::utils::blocks::surface_deformation_field.shape_equilibrium_term ); constexpr auto enthalpyResidual = mean_field::utils::blocks::get_residual_block(mean_field::utils::blocks::enthalpy_field.specific_term); @@ -52,7 +54,7 @@ export namespace experiment::null_space { ); inline constexpr std::array residualBlockNames{"gravity_gradient", "gravity_potential", "closure", - "displacement", "hydrostatic", "mass"}; + "surface_shape", "hydrostatic", "mass"}; template [[nodiscard]] mfem::Vector value_view( @@ -97,7 +99,9 @@ export namespace experiment::null_space { 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."); + MFEM_VERIFY( + source.Size() == layout.size(block), "Surface-mode experiment received a block with the wrong size." + ); value_view(vector, layout, block) = source; } @@ -118,27 +122,27 @@ export namespace experiment::null_space { int rank = 0; MPI_Comm_rank(communicator, &rank); if (rank == 0) { - std::cout << "[null-space experiment] " << message << std::endl; + std::cout << "[reduced-surface 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} + .discretization = {.identity = 2003, .revision = 1}, + .density = {.identity = 2011, .revision = 1}, + .surfaceDeformation = {.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.surfaceDeformation.revision; ++dependencies.gravityGradient.revision; ++dependencies.gravityPotential.revision; ++dependencies.enthalpy.revision; @@ -211,6 +215,10 @@ export namespace experiment::null_space { return m_fem; } + [[nodiscard]] Model &model() noexcept { + return m_model; + } + [[nodiscard]] mean_field::operators::PreparedStellarEquilibriumOperator &stellar_operator() noexcept { return m_operator; } @@ -248,66 +256,26 @@ export namespace experiment::null_space { 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 residual() const { + mfem::Vector result; + m_operator.BuildResidual(result); + return result; } - [[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); + [[nodiscard]] mfem::Vector jacobian_action(const mfem::Vector &direction) const { + mfem::Vector result; + m_operator.Mult(direction, result); + return result; + } - const mfem::Vector gravityDirection = pack_gravity_state( - reducedDensityDirection, displacementDirection, gravityGradientDirection, gravityPotentialDirection + [[nodiscard]] mfem::Vector lifted_surface_direction(const mfem::Vector &rootDirection) const { + const auto &layout = m_operator.GetLayout(); + const mfem::Vector surfaceDirection = const_value_view(rootDirection, layout, surfaceDeformationValue); + mfem::Vector volumeDirection(m_operator.GetDomainDeformation().volumeDisplacementSize()); + m_operator.GetDomainDeformation().applyJacobian( + m_operator.GetSurfaceDeformationParameters(), surfaceDirection, volumeDirection ); - - 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); + return volumeDirection; } private: @@ -375,12 +343,10 @@ export namespace experiment::null_space { 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); @@ -390,8 +356,10 @@ export namespace experiment::null_space { const mean_field::field::FieldDofMap enthalpyMap = mean_field::field::make_field_dof_map(*m_fem.enthalpyFes); + mfem::Vector surfaceParameters(layout.size(surfaceDeformationValue)); + surfaceParameters = 0.0; assign_value_block(m_state, layout, densityValue, densityMap.gather(densityTrue)); - assign_value_block(m_state, layout, displacementValue, displacementTrue); + assign_value_block(m_state, layout, surfaceDeformationValue, surfaceParameters); 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)); @@ -402,29 +370,6 @@ export namespace experiment::null_space { 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; @@ -434,68 +379,135 @@ export namespace experiment::null_space { mean_field::operators::StellarEquilibriumDependencies m_dependencies; }; - enum class RigidModeKind : std::uint8_t { translation, rotation }; + enum class SurfaceModeKind : std::uint8_t { + uniform_radial, + translation_like_dipole, + oblate_quadrupole, + spherical_harmonic + }; - struct RigidMode final { + struct SurfaceMode final { std::string name; - RigidModeKind kind; + SurfaceModeKind 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; + [[nodiscard]] inline const char *surface_mode_kind_name(const SurfaceModeKind kind) noexcept { + switch (kind) { + case SurfaceModeKind::uniform_radial: + return "uniform_radial"; + case SurfaceModeKind::translation_like_dipole: + return "translation_like_dipole"; + case SurfaceModeKind::oblate_quadrupole: + return "oblate_quadrupole"; + case SurfaceModeKind::spherical_harmonic: + return "spherical_harmonic"; + } + return "unknown"; + } - 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) - }; + [[nodiscard]] inline double zonal_legendre( + const int degree, + const double cosineOfPolarAngle + ) { + MFEM_VERIFY(degree >= 0, "A zonal spherical-harmonic degree must be non-negative."); + const double coordinate = std::clamp(cosineOfPolarAngle, -1.0, 1.0); + if (degree == 0) { + return 1.0; + } + if (degree == 1) { + return coordinate; } - 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); + double previousPrevious = 1.0; + double previous = coordinate; + for (int order = 2; order <= degree; ++order) { + const double current = ((2.0 * static_cast(order) - 1.0) * coordinate * previous - + (static_cast(order) - 1.0) * previousPrevious) / + static_cast(order); + previousPrevious = previous; + previous = current; + } + return previous; + } + [[nodiscard]] inline std::vector make_surface_modes(N3Equilibrium &fixture) { + const auto &layout = fixture.stellar_operator().GetLayout(); + auto deformation = fixture.model().compileDomainDeformation(fixture.fem()); + const auto &surface = deformation.surfaceDeformationPrescription(); + MFEM_VERIFY( + surface.parameterCount() == layout.size(surfaceDeformationValue), + "The diagnostic surface prescription does not match the root surface block." + ); + + const auto make_root_direction = [&layout](const mfem::Vector &surfaceDirection) { 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) - }; + assign_value_block(direction, layout, surfaceDeformationValue, surfaceDirection); + return direction; + }; + + std::vector modes; + modes.reserve(6); + + mfem::Vector uniform(surface.parameterCount()); + for (int parameter = 0; parameter < uniform.Size(); ++parameter) { + uniform(parameter) = surface.referenceRadius(parameter); } + modes.push_back( + {.name = "uniform_radial_homology", + .kind = SurfaceModeKind::uniform_radial, + .axis = -1, + .direction = make_root_direction(uniform)} + ); + + for (int axis = 0; axis < surface.spatialDimension(); ++axis) { + mfem::Vector dipole(surface.parameterCount()); + for (int parameter = 0; parameter < dipole.Size(); ++parameter) { + dipole(parameter) = surface.radialDirection(parameter, axis); + } + modes.push_back( + {.name = std::string("translation_like_dipole_") + static_cast('x' + axis), + .kind = SurfaceModeKind::translation_like_dipole, + .axis = axis, + .direction = make_root_direction(dipole)} + ); + } + + mfem::Vector quadrupole(surface.parameterCount()); + for (int parameter = 0; parameter < quadrupole.Size(); ++parameter) { + const double polarDirection = surface.radialDirection(parameter, 2); + quadrupole(parameter) = surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection); + } + modes.push_back( + {.name = "axisymmetric_oblate_quadrupole_z", + .kind = SurfaceModeKind::oblate_quadrupole, + .axis = 2, + .direction = make_root_direction(quadrupole)} + ); + + constexpr int diagnosticAngularDegree = 12; + mfem::Vector sphericalHarmonic(surface.parameterCount()); + double localMaximumMagnitude = 0.0; + for (int parameter = 0; parameter < sphericalHarmonic.Size(); ++parameter) { + const double angularValue = zonal_legendre(diagnosticAngularDegree, surface.radialDirection(parameter, 2)); + sphericalHarmonic(parameter) = surface.referenceRadius(parameter) * angularValue; + localMaximumMagnitude = std::max(localMaximumMagnitude, std::abs(angularValue)); + } + double globalMaximumMagnitude = 0.0; + MPI_Allreduce( + &localMaximumMagnitude, &globalMaximumMagnitude, 1, MPI_DOUBLE, MPI_MAX, fixture.fem().mesh->GetComm() + ); + MFEM_VERIFY(globalMaximumMagnitude > 0.0, "The spherical-harmonic surface mode has zero amplitude."); + sphericalHarmonic /= globalMaximumMagnitude; + modes.push_back( + {.name = "zonal_spherical_harmonic_l12", + .kind = SurfaceModeKind::spherical_harmonic, + .axis = -1, + .direction = make_root_direction(sphericalHarmonic)} + ); + return modes; } @@ -511,7 +523,7 @@ export namespace experiment::null_space { 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, surfaceShapeResidual), communicator), global_norm(const_residual_view(action, layout, enthalpyResidual), communicator), global_norm(const_residual_view(action, layout, massResidual), communicator) }; diff --git a/format b/format index f6e8e75..3f8376c 100755 --- a/format +++ b/format @@ -1,3 +1,3 @@ #!/bin/bash -find libmeanfield tests -type f \( -name '*.cpp' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {} -find libmeanfield tests -type f \( -name '*.cppm' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {} +find libmeanfield tests experiments -type f \( -name '*.cpp' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {} +find libmeanfield tests experiments -type f \( -name '*.cppm' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {} diff --git a/libmeanfield/impl/deformation/nodal_radial_surface.cpp b/libmeanfield/impl/deformation/nodal_radial_surface.cpp new file mode 100644 index 0000000..cc0bfea --- /dev/null +++ b/libmeanfield/impl/deformation/nodal_radial_surface.cpp @@ -0,0 +1,345 @@ +module; + +#include +#include +#include +#include + +#include + +module mean_field; + +import :deformation.nodal_radial_surface; + +namespace mean_field::deformation { + namespace { + [[nodiscard]] SurfaceDeformationDescriptor nodalRadialDescriptor(const int spatialDimension) noexcept { + return { + .name = "NodalRadialSurface", + .spatialDimension = spatialDimension, + .motionKind = SurfaceMotionKind::Radial, + .linearOnReferenceGeometry = true, + .requiresStarShapedReferenceSurface = true, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .translationTreatment = GeometricGaugeTreatment::Retained, + .orientationTreatment = GeometricGaugeTreatment::Retained + }; + } + + void requireFiniteVector( + const mfem::Vector &vector, + const char *message + ) { + for (int index = 0; index < vector.Size(); ++index) { + if (!std::isfinite(vector(index))) { + throw std::invalid_argument(message); + } + } + } + } // namespace + + SurfaceDeformationCompilationContext::SurfaceDeformationCompilationContext( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + field::ScalarBoundaryDofMap surfaceDofMap + ) + : m_scalarFiniteElementSpace(&scalarFiniteElementSpace), + m_surfaceDofMap(std::move(surfaceDofMap)) { + if (scalarFiniteElementSpace.Nonconforming()) { + throw std::invalid_argument( + "Surface deformation compilation currently requires a conforming scalar finite-element space." + ); + } + if (scalarFiniteElementSpace.GetVDim() != 1) { + throw std::invalid_argument("Surface deformation compilation requires a scalar finite-element space."); + } + if (scalarFiniteElementSpace.GetMesh() == nullptr) { + throw std::invalid_argument("Surface deformation compilation requires a finite-element mesh."); + } + if (m_surfaceDofMap.volume_true_dof_size() != scalarFiniteElementSpace.GetTrueVSize()) { + throw std::invalid_argument( + "The surface DOF map and scalar finite-element space have incompatible true-DOF sizes." + ); + } + if (m_surfaceDofMap.global_size() <= 0) { + throw std::invalid_argument("Surface deformation compilation requires at least one surface coordinate."); + } + } + + mfem::ParFiniteElementSpace &SurfaceDeformationCompilationContext::scalarFiniteElementSpace() const noexcept { + return *m_scalarFiniteElementSpace; + } + + const field::ScalarBoundaryDofMap &SurfaceDeformationCompilationContext::surfaceDofMap() const noexcept { + return m_surfaceDofMap; + } + + NodalRadialSurface::NodalRadialSurface(mfem::Vector referenceCenter) + : m_referenceCenter(std::move(referenceCenter)) { + validate(); + } + + const mfem::Vector &NodalRadialSurface::referenceCenter() const noexcept { + return m_referenceCenter; + } + + SurfaceDeformationDescriptor NodalRadialSurface::descriptor() const noexcept { + return nodalRadialDescriptor(m_referenceCenter.Size()); + } + + void NodalRadialSurface::validate() const { + if (m_referenceCenter.Size() <= 0) { + throw std::invalid_argument("NodalRadialSurface requires a non-empty reference center."); + } + requireFiniteVector(m_referenceCenter, "NodalRadialSurface reference-center coordinates must be finite."); + } + + PreparedNodalRadialSurface::PreparedNodalRadialSurface( + const SurfaceDeformationDescriptor descriptor, + mfem::Vector referenceCenter, + field::ScalarBoundaryDofMap surfaceDofMap, + mfem::Vector radialDirections, + mfem::Vector referenceRadii + ) + : m_descriptor(descriptor), + m_referenceCenter(std::move(referenceCenter)), + m_surfaceDofMap(std::move(surfaceDofMap)), + m_radialDirections(std::move(radialDirections)), + m_referenceRadii(std::move(referenceRadii)) { + } + + SurfaceDeformationDescriptor PreparedNodalRadialSurface::descriptor() const noexcept { + return m_descriptor; + } + + int PreparedNodalRadialSurface::parameterCount() const noexcept { + return m_surfaceDofMap.local_size(); + } + + long long PreparedNodalRadialSurface::globalParameterCount() const noexcept { + return m_surfaceDofMap.global_size(); + } + + long long PreparedNodalRadialSurface::globalParameterOffset() const noexcept { + return m_surfaceDofMap.global_offset(); + } + + int PreparedNodalRadialSurface::spatialDimension() const noexcept { + return m_descriptor.spatialDimension; + } + + int PreparedNodalRadialSurface::surfaceDisplacementSize() const noexcept { + return spatialDimension() * parameterCount(); + } + + long long PreparedNodalRadialSurface::globalSurfaceDisplacementSize() const noexcept { + return static_cast(spatialDimension()) * globalParameterCount(); + } + + long long PreparedNodalRadialSurface::globalSurfaceDisplacementOffset() const noexcept { + return static_cast(spatialDimension()) * globalParameterOffset(); + } + + int PreparedNodalRadialSurface::surfaceDisplacementDof( + const int parameterDof, + const int component + ) const { + if (parameterDof < 0 || parameterDof >= parameterCount()) { + throw std::out_of_range("Parameter DOF is outside PreparedNodalRadialSurface."); + } + if (component < 0 || component >= spatialDimension()) { + throw std::out_of_range("Surface-displacement component is outside PreparedNodalRadialSurface."); + } + return spatialDimension() * parameterDof + component; + } + + double PreparedNodalRadialSurface::radialDirection( + const int parameterDof, + const int component + ) const { + return m_radialDirections(surfaceDisplacementDof(parameterDof, component)); + } + + double PreparedNodalRadialSurface::referenceRadius(const int parameterDof) const { + if (parameterDof < 0 || parameterDof >= parameterCount()) { + throw std::out_of_range("Parameter DOF is outside PreparedNodalRadialSurface."); + } + return m_referenceRadii(parameterDof); + } + + const mfem::Vector &PreparedNodalRadialSurface::referenceCenter() const noexcept { + return m_referenceCenter; + } + + const field::ScalarBoundaryDofMap &PreparedNodalRadialSurface::surfaceDofMap() const noexcept { + return m_surfaceDofMap; + } + + void PreparedNodalRadialSurface::buildSurfaceDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &surfaceDisplacement + ) const { + requireParameterSize(parameters); + requireSurfaceDisplacementSize(surfaceDisplacement); + + for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) { + for (int component = 0; component < spatialDimension(); ++component) { + const int surfaceDof = spatialDimension() * parameterDof + component; + surfaceDisplacement(surfaceDof) = parameters(parameterDof) * m_radialDirections(surfaceDof); + } + } + } + + void PreparedNodalRadialSurface::applyJacobian( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + mfem::Vector &surfaceDisplacementDirection + ) const { + requireParameterSize(parameters); + requireParameterSize(parameterDirection); + requireSurfaceDisplacementSize(surfaceDisplacementDirection); + + for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) { + for (int component = 0; component < spatialDimension(); ++component) { + const int surfaceDof = spatialDimension() * parameterDof + component; + surfaceDisplacementDirection(surfaceDof) = + parameterDirection(parameterDof) * m_radialDirections(surfaceDof); + } + } + } + + void PreparedNodalRadialSurface::applyJacobianTranspose( + const mfem::Vector ¶meters, + const mfem::Vector &surfaceDisplacementDual, + mfem::Vector ¶meterDual + ) const { + requireParameterSize(parameters); + requireSurfaceDisplacementSize(surfaceDisplacementDual); + requireParameterSize(parameterDual); + + for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) { + double radialWork = 0.0; + for (int component = 0; component < spatialDimension(); ++component) { + const int surfaceDof = spatialDimension() * parameterDof + component; + radialWork += m_radialDirections(surfaceDof) * surfaceDisplacementDual(surfaceDof); + } + parameterDual(parameterDof) = radialWork; + } + } + + void PreparedNodalRadialSurface::applyPullbackDerivative( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + const mfem::Vector &surfaceDisplacementDual, + mfem::Vector ¶meterDualAction + ) const { + requireParameterSize(parameters); + requireParameterSize(parameterDirection); + requireSurfaceDisplacementSize(surfaceDisplacementDual); + requireParameterSize(parameterDualAction); + + parameterDualAction = 0.0; + } + + void PreparedNodalRadialSurface::requireParameterSize(const mfem::Vector ¶meters) const { + if (parameters.Size() != parameterCount()) { + throw std::invalid_argument( + std::format( + "Nodal radial parameter vector has size {}, but the prepared surface requires {}.", + parameters.Size(), parameterCount() + ) + ); + } + } + + void PreparedNodalRadialSurface::requireSurfaceDisplacementSize(const mfem::Vector &surfaceDisplacement) const { + if (surfaceDisplacement.Size() != surfaceDisplacementSize()) { + throw std::invalid_argument( + std::format( + "Surface displacement vector has size {}, but the prepared nodal radial surface requires {}.", + surfaceDisplacement.Size(), surfaceDisplacementSize() + ) + ); + } + } + + PreparedNodalRadialSurface compileSurfaceDeformationPrescription( + const NodalRadialSurface &prescription, + const SurfaceDeformationCompilationContext &context + ) { + prescription.validate(); + + mfem::ParFiniteElementSpace &scalarSpace = context.scalarFiniteElementSpace(); + const mfem::Mesh *mesh = scalarSpace.GetMesh(); + + if (mesh == nullptr) { + throw std::invalid_argument("Nodal radial surface compilation requires a reference mesh."); + } + if (prescription.referenceCenter().Size() != mesh->SpaceDimension()) { + throw std::invalid_argument( + std::format( + "NodalRadialSurface reference center has dimension {}, but the reference mesh has spatial " + "dimension {}.", + prescription.referenceCenter().Size(), mesh->SpaceDimension() + ) + ); + } + + const field::ScalarBoundaryDofMap &surfaceDofMap = context.surfaceDofMap(); + const int parameterCount = surfaceDofMap.local_size(); + const int spatialDimension = mesh->SpaceDimension(); + + mfem::Vector referencePositions(spatialDimension * parameterCount); + mfem::ParGridFunction coordinateField(&scalarSpace); + + for (int component = 0; component < spatialDimension; ++component) { + mfem::FunctionCoefficient coordinateCoefficient([component](const mfem::Vector &position) { + return position(component); + }); + + coordinateField.ProjectCoefficient(coordinateCoefficient); + + mfem::Vector coordinateTrueDofs; + coordinateField.GetTrueDofs(coordinateTrueDofs); + const mfem::Vector surfaceCoordinates = surfaceDofMap.gather(coordinateTrueDofs); + + for (int parameterDof = 0; parameterDof < parameterCount; ++parameterDof) { + referencePositions(spatialDimension * parameterDof + component) = surfaceCoordinates(parameterDof); + } + } + + mfem::Vector radialDirections(referencePositions.Size()); + mfem::Vector referenceRadii(parameterCount); + + for (int parameterDof = 0; parameterDof < parameterCount; ++parameterDof) { + double radiusSquared = 0.0; + + for (int component = 0; component < spatialDimension; ++component) { + const int surfaceDof = spatialDimension * parameterDof + component; + const double radialCoordinate = + referencePositions(surfaceDof) - prescription.referenceCenter()(component); + + radialDirections(surfaceDof) = radialCoordinate; + radiusSquared += radialCoordinate * radialCoordinate; + } + + const double radius = std::sqrt(radiusSquared); + if (!std::isfinite(radius) || radius <= 0.0) { + throw std::invalid_argument( + "Every nodal radial surface coordinate must have a finite positive distance from the reference " + "center." + ); + } + + referenceRadii(parameterDof) = radius; + for (int component = 0; component < spatialDimension; ++component) { + radialDirections(spatialDimension * parameterDof + component) /= radius; + } + } + + return PreparedNodalRadialSurface( + nodalRadialDescriptor(spatialDimension), prescription.referenceCenter(), surfaceDofMap, + std::move(radialDirections), std::move(referenceRadii) + ); + } +} // namespace mean_field::deformation diff --git a/libmeanfield/impl/deformation/radial_extensions.cpp b/libmeanfield/impl/deformation/radial_extensions.cpp new file mode 100644 index 0000000..0c68420 --- /dev/null +++ b/libmeanfield/impl/deformation/radial_extensions.cpp @@ -0,0 +1,1061 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +module mean_field; + +import :deformation.radial_extensions; + +namespace mean_field::deformation { + namespace { + struct LocatedBoundaryPoint final { + int boundaryElement{-1}; + mfem::IntegrationPoint integrationPoint; + double residualNorm{std::numeric_limits::infinity()}; + }; + + [[nodiscard]] InteriorDeformationExtensionDescriptor + powerLawInteriorDescriptor(const int spatialDimension) noexcept { + return { + .name = "PowerLawRadialInteriorExtension", + .spatialDimension = spatialDimension, + .linearOnReferenceGeometry = true, + .requiresRadialFoliation = true, + .requiresAuxiliarySolve = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .centerBehavior = InteriorCenterBehavior::FixedAtReferenceCenter + }; + } + + [[nodiscard]] VacuumDeformationExtensionDescriptor + fixedInfinityVacuumDescriptor(const int spatialDimension) noexcept { + return { + .name = "FixedInfinityRadialVacuumExtension", + .spatialDimension = spatialDimension, + .linearOnReferenceGeometry = true, + .requiresRadialFoliation = true, + .requiresAuxiliarySolve = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .outerBoundaryBehavior = VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity + }; + } + + [[nodiscard]] double logicalInfinityRadius(const mfem::Vector &position) { + double radius = 0.0; + for (int component = 0; component < position.Size(); ++component) { + radius = std::max(radius, std::abs(position(component))); + } + return radius; + } + + [[nodiscard]] double euclideanDistance( + const mfem::Vector &first, + const mfem::Vector &second + ) { + double squaredDistance = 0.0; + for (int component = 0; component < first.Size(); ++component) { + const double difference = first(component) - second(component); + squaredDistance += difference * difference; + } + return std::sqrt(squaredDistance); + } + + [[nodiscard]] mfem::Array buildTrueDofSupport( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + const mfem::Array &materialMarker + ) { + const mfem::Mesh *mesh = scalarFiniteElementSpace.GetMesh(); + if (mesh == nullptr) { + throw std::invalid_argument("Logical radial extension support compilation requires a mesh."); + } + if (materialMarker.Size() != mesh->attributes.Max()) { + throw std::invalid_argument( + "A logical radial extension material marker does not cover every material attribute." + ); + } + + mfem::Array localDofMarker(scalarFiniteElementSpace.GetVSize()); + localDofMarker = 0; + mfem::Array elementDofs; + + for (int element = 0; element < mesh->GetNE(); ++element) { + const int attribute = mesh->GetAttribute(element); + if (attribute <= 0 || attribute > materialMarker.Size() || materialMarker[attribute - 1] == 0) { + continue; + } + + scalarFiniteElementSpace.GetElementDofs(element, elementDofs); + for (const int encodedDof : elementDofs) { + localDofMarker[mfem::FiniteElementSpace::DecodeDof(encodedDof)] = 1; + } + } + + scalarFiniteElementSpace.Synchronize(localDofMarker); + mfem::Array trueDofMarker(scalarFiniteElementSpace.GetTrueVSize()); + trueDofMarker = 0; + + for (int localDof = 0; localDof < localDofMarker.Size(); ++localDof) { + if (localDofMarker[localDof] == 0) { + continue; + } + const int trueDof = scalarFiniteElementSpace.GetLocalTDofNumber(localDof); + if (trueDof >= 0) { + trueDofMarker[trueDof] = 1; + } + } + + return trueDofMarker; + } + + [[nodiscard]] mfem::Vector buildLogicalTrueDofPositions( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + mfem::ParMesh &logicalReferenceMesh + ) { + const mfem::Mesh *physicalMesh = scalarFiniteElementSpace.GetMesh(); + if (physicalMesh == nullptr || physicalMesh->GetNE() != logicalReferenceMesh.GetNE()) { + throw std::invalid_argument( + "Logical radial extension compilation requires paired physical and logical elements." + ); + } + + const int spatialDimension = logicalReferenceMesh.SpaceDimension(); + const int trueDofCount = scalarFiniteElementSpace.GetTrueVSize(); + mfem::Vector logicalPositions(spatialDimension * trueDofCount); + mfem::Array processed(trueDofCount); + processed = 0; + + mfem::Array elementDofs; + mfem::Vector logicalPosition(spatialDimension); + + for (int element = 0; element < physicalMesh->GetNE(); ++element) { + if (physicalMesh->GetElementGeometry(element) != logicalReferenceMesh.GetElementGeometry(element) || + physicalMesh->GetAttribute(element) != logicalReferenceMesh.GetAttribute(element)) { + throw std::invalid_argument("A physical element does not match its logical reference element."); + } + + const mfem::FiniteElement &finiteElement = *scalarFiniteElementSpace.GetFE(element); + const mfem::IntegrationRule &nodes = finiteElement.GetNodes(); + scalarFiniteElementSpace.GetElementDofs(element, elementDofs); + if (nodes.GetNPoints() != elementDofs.Size()) { + throw std::invalid_argument( + "The scalar companion basis is not nodal on a logical reference element." + ); + } + + mfem::ElementTransformation *logicalTransformation = + logicalReferenceMesh.GetElementTransformation(element); + if (logicalTransformation == nullptr) { + throw std::invalid_argument("A logical reference element has no transformation."); + } + + for (int localElementDof = 0; localElementDof < elementDofs.Size(); ++localElementDof) { + const int localDof = mfem::FiniteElementSpace::DecodeDof(elementDofs[localElementDof]); + const int trueDof = scalarFiniteElementSpace.GetLocalTDofNumber(localDof); + if (trueDof < 0) { + continue; + } + + logicalTransformation->Transform(nodes.IntPoint(localElementDof), logicalPosition); + if (processed[trueDof] != 0) { + mfem::Vector existingPosition( + logicalPositions.GetData() + spatialDimension * trueDof, spatialDimension + ); + const double scale = std::max(1.0, logicalInfinityRadius(logicalPosition)); + if (euclideanDistance(existingPosition, logicalPosition) > 1.0e-12 * scale) { + throw std::invalid_argument( + "A shared scalar DOF has inconsistent logical reference coordinates." + ); + } + continue; + } + + for (int component = 0; component < spatialDimension; ++component) { + logicalPositions(spatialDimension * trueDof + component) = logicalPosition(component); + } + processed[trueDof] = 1; + } + } + + for (int trueDof = 0; trueDof < trueDofCount; ++trueDof) { + if (processed[trueDof] == 0) { + throw std::invalid_argument("An owned scalar DOF has no logical reference coordinate."); + } + } + + return logicalPositions; + } + + [[nodiscard]] double boundaryLogicalRadius( + mfem::ParMesh &logicalReferenceMesh, + const int boundaryAttribute + ) { + double minimumRadius = std::numeric_limits::infinity(); + double maximumRadius = 0.0; + int sampleCount = 0; + mfem::Vector position(logicalReferenceMesh.SpaceDimension()); + + for (int boundaryElement = 0; boundaryElement < logicalReferenceMesh.GetNBE(); ++boundaryElement) { + if (logicalReferenceMesh.GetBdrAttribute(boundaryElement) != boundaryAttribute) { + continue; + } + + mfem::ElementTransformation *transformation = + logicalReferenceMesh.GetBdrElementTransformation(boundaryElement); + const int geometry = logicalReferenceMesh.GetBdrElementGeometry(boundaryElement); + const mfem::IntegrationRule *vertices = mfem::Geometries.GetVertices(geometry); + if (transformation == nullptr || vertices == nullptr) { + throw std::invalid_argument("A logical radial boundary element is incomplete."); + } + + for (int vertex = 0; vertex < vertices->GetNPoints(); ++vertex) { + transformation->Transform(vertices->IntPoint(vertex), position); + const double radius = logicalInfinityRadius(position); + minimumRadius = std::min(minimumRadius, radius); + maximumRadius = std::max(maximumRadius, radius); + ++sampleCount; + } + } + + if (sampleCount == 0 || !(minimumRadius > 0.0)) { + throw std::invalid_argument("A required logical radial boundary is absent."); + } + if (maximumRadius - minimumRadius > 1.0e-12 * std::max(1.0, maximumRadius)) { + throw std::invalid_argument("A logical radial boundary is not a constant L-infinity-radius surface."); + } + return 0.5 * (minimumRadius + maximumRadius); + } + + [[nodiscard]] LocatedBoundaryPoint locateLogicalBoundaryPoint( + mfem::ParMesh &logicalReferenceMesh, + const int boundaryAttribute, + const mfem::Vector &target + ) { + LocatedBoundaryPoint best; + const double residualTolerance = 2.0e-11 * std::max(1.0, logicalInfinityRadius(target)); + + for (int boundaryElement = 0; boundaryElement < logicalReferenceMesh.GetNBE(); ++boundaryElement) { + if (logicalReferenceMesh.GetBdrAttribute(boundaryElement) != boundaryAttribute) { + continue; + } + + const int geometry = logicalReferenceMesh.GetBdrElementGeometry(boundaryElement); + if (geometry != mfem::Geometry::SQUARE) { + throw std::invalid_argument( + "The STROID logical radial foliation currently requires quadrilateral boundary elements." + ); + } + + mfem::ElementTransformation *transformation = + logicalReferenceMesh.GetBdrElementTransformation(boundaryElement); + if (transformation == nullptr) { + throw std::invalid_argument("A logical boundary element has no transformation."); + } + + mfem::IntegrationPoint point = mfem::Geometries.GetCenter(geometry); + mfem::Vector transformed(target.Size()); + mfem::Vector residual(target.Size()); + bool nonsingular = true; + + for (int iteration = 0; iteration < 8; ++iteration) { + transformation->Transform(point, transformed); + residual = target; + residual -= transformed; + if (residual.Norml2() <= residualTolerance) { + break; + } + + transformation->SetIntPoint(&point); + const mfem::DenseMatrix &jacobian = transformation->Jacobian(); + double normal00 = 0.0; + double normal01 = 0.0; + double normal11 = 0.0; + double right0 = 0.0; + double right1 = 0.0; + + for (int component = 0; component < target.Size(); ++component) { + normal00 += jacobian(component, 0) * jacobian(component, 0); + normal01 += jacobian(component, 0) * jacobian(component, 1); + normal11 += jacobian(component, 1) * jacobian(component, 1); + right0 += jacobian(component, 0) * residual(component); + right1 += jacobian(component, 1) * residual(component); + } + + const double determinant = normal00 * normal11 - normal01 * normal01; + if (!(std::abs(determinant) > std::numeric_limits::epsilon())) { + nonsingular = false; + break; + } + + point.x += (normal11 * right0 - normal01 * right1) / determinant; + point.y += (normal00 * right1 - normal01 * right0) / determinant; + } + + if (!nonsingular || !mfem::Geometry::CheckPoint(geometry, point, 1.0e-10)) { + continue; + } + + transformation->Transform(point, transformed); + const double residualNorm = euclideanDistance(transformed, target); + if (residualNorm <= residualTolerance && residualNorm < best.residualNorm) { + best.boundaryElement = boundaryElement; + best.integrationPoint = point; + best.residualNorm = residualNorm; + } + } + + if (best.boundaryElement < 0) { + throw std::invalid_argument( + std::format( + "No logical boundary element contains a projected foliation point on boundary attribute {}.", + boundaryAttribute + ) + ); + } + return best; + } + + void appendSurfaceInterpolationRow( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + const field::ScalarBoundaryDofMap &stellarSurfaceDofMap, + const LocatedBoundaryPoint &locatedPoint, + std::vector &globalCoordinates, + std::vector &weights + ) { + mfem::Array boundaryDofs; + scalarFiniteElementSpace.GetBdrElementDofs(locatedPoint.boundaryElement, boundaryDofs); + const mfem::FiniteElement *boundaryElement = scalarFiniteElementSpace.GetBE(locatedPoint.boundaryElement); + if (boundaryElement == nullptr || boundaryElement->GetDof() != boundaryDofs.Size()) { + throw std::invalid_argument("The physical surface trace basis is incompatible with its boundary DOFs."); + } + + mfem::Vector shape(boundaryDofs.Size()); + boundaryElement->CalcShape(locatedPoint.integrationPoint, shape); + double partitionOfUnity = 0.0; + + for (int localShapeDof = 0; localShapeDof < boundaryDofs.Size(); ++localShapeDof) { + partitionOfUnity += shape(localShapeDof); + if (std::abs(shape(localShapeDof)) <= 64.0 * std::numeric_limits::epsilon()) { + continue; + } + + const int localDof = mfem::FiniteElementSpace::DecodeDof(boundaryDofs[localShapeDof]); + const int trueDof = scalarFiniteElementSpace.GetLocalTDofNumber(localDof); + if (trueDof < 0) { + throw std::invalid_argument( + "Logical surface interpolation across MPI ownership is not implemented yet." + ); + } + const std::optional surfaceDof = stellarSurfaceDofMap.local_boundary_dof(trueDof); + if (!surfaceDof.has_value()) { + throw std::invalid_argument( + "A logical surface interpolation basis DOF is absent from the compact surface map." + ); + } + + const long long globalCoordinate = stellarSurfaceDofMap.global_boundary_dof(*surfaceDof); + if (globalCoordinate > std::numeric_limits::max()) { + throw std::overflow_error("A global surface coordinate exceeds supported integer indexing."); + } + globalCoordinates.push_back(static_cast(globalCoordinate)); + weights.push_back(shape(localShapeDof)); + } + + if (std::abs(partitionOfUnity - 1.0) > 2.0e-12) { + throw std::invalid_argument("A logical surface interpolation row does not preserve constants."); + } + } + + [[nodiscard]] std::vector gatherCounts( + const int localCount, + MPI_Comm communicator + ) { + int communicatorSize = 0; + MPI_Comm_size(communicator, &communicatorSize); + std::vector counts(static_cast(communicatorSize)); + MPI_Allgather(&localCount, 1, MPI_INT, counts.data(), 1, MPI_INT, communicator); + return counts; + } + + [[nodiscard]] std::vector prefixOffsets(const std::vector &counts) { + std::vector offsets(counts.size()); + int offset = 0; + for (std::size_t rank = 0; rank < counts.size(); ++rank) { + offsets[rank] = offset; + offset += counts[rank]; + } + return offsets; + } + + void requireVectorSize( + const mfem::Vector &vector, + const int requiredSize, + const char *name + ) { + if (vector.Size() != requiredSize) { + throw std::invalid_argument( + std::format( + "{} has size {}, but the prepared logical radial extension requires {}.", name, vector.Size(), + requiredSize + ) + ); + } + } + + void gatherSurfaceDisplacement( + const mfem::Vector &localSurfaceDisplacement, + mfem::Vector &globalSurfaceDisplacement, + const std::vector &counts, + const std::vector &offsets, + MPI_Comm communicator + ) { + MPI_Allgatherv( + localSurfaceDisplacement.GetData(), localSurfaceDisplacement.Size(), MPI_DOUBLE, + globalSurfaceDisplacement.GetData(), counts.data(), offsets.data(), MPI_DOUBLE, communicator + ); + } + + void reduceSurfaceDual( + const mfem::Vector &localGlobalSurfaceDual, + mfem::Vector &globalSurfaceDual, + mfem::Vector &surfaceDisplacementDual, + const int globalSurfaceDisplacementOffset, + MPI_Comm communicator + ) { + MPI_Allreduce( + localGlobalSurfaceDual.GetData(), globalSurfaceDual.GetData(), localGlobalSurfaceDual.Size(), + MPI_DOUBLE, MPI_SUM, communicator + ); + for (int localDof = 0; localDof < surfaceDisplacementDual.Size(); ++localDof) { + surfaceDisplacementDual(localDof) = globalSurfaceDual(globalSurfaceDisplacementOffset + localDof); + } + } + + [[nodiscard]] int interpolationEntryIndex( + const std::vector &rowOffsets, + const int scalarTrueDof, + const int interpolationEntry + ) { + if (scalarTrueDof < 0 || scalarTrueDof + 1 >= static_cast(rowOffsets.size())) { + throw std::out_of_range("Scalar true DOF is outside the logical radial extension."); + } + const int entryCount = rowOffsets[scalarTrueDof + 1] - rowOffsets[scalarTrueDof]; + if (interpolationEntry < 0 || interpolationEntry >= entryCount) { + throw std::out_of_range("Surface interpolation entry is outside the logical radial extension row."); + } + return rowOffsets[scalarTrueDof] + interpolationEntry; + } + + [[nodiscard]] int mfemByNodesVectorDof( + const int scalarTrueDof, + const int component, + const int scalarTrueDofCount + ) noexcept { + return scalarTrueDof + component * scalarTrueDofCount; + } + + void applySparseForward( + const int spatialDimension, + const mfem::Array &support, + const mfem::Vector &radialWeights, + const std::vector &rowOffsets, + const std::vector &surfaceCoordinates, + const std::vector &surfaceWeights, + const mfem::Vector &globalSurfaceDisplacement, + mfem::Vector &volumeDisplacement + ) { + volumeDisplacement = 0.0; + for (int scalarTrueDof = 0; scalarTrueDof < support.Size(); ++scalarTrueDof) { + if (support[scalarTrueDof] == 0 || radialWeights(scalarTrueDof) == 0.0) { + continue; + } + for (int component = 0; component < spatialDimension; ++component) { + double interpolatedSurfaceDisplacement = 0.0; + for (int entry = rowOffsets[scalarTrueDof]; entry < rowOffsets[scalarTrueDof + 1]; ++entry) { + interpolatedSurfaceDisplacement += + surfaceWeights[entry] * + globalSurfaceDisplacement(spatialDimension * surfaceCoordinates[entry] + component); + } + volumeDisplacement(mfemByNodesVectorDof(scalarTrueDof, component, support.Size())) = + radialWeights(scalarTrueDof) * interpolatedSurfaceDisplacement; + } + } + } + + void applySparseTranspose( + const int spatialDimension, + const mfem::Array &support, + const mfem::Vector &radialWeights, + const std::vector &rowOffsets, + const std::vector &surfaceCoordinates, + const std::vector &surfaceWeights, + const mfem::Vector &volumeDual, + mfem::Vector &globalSurfaceDual + ) { + globalSurfaceDual = 0.0; + for (int scalarTrueDof = 0; scalarTrueDof < support.Size(); ++scalarTrueDof) { + if (support[scalarTrueDof] == 0 || radialWeights(scalarTrueDof) == 0.0) { + continue; + } + for (int entry = rowOffsets[scalarTrueDof]; entry < rowOffsets[scalarTrueDof + 1]; ++entry) { + for (int component = 0; component < spatialDimension; ++component) { + globalSurfaceDual(spatialDimension * surfaceCoordinates[entry] + component) += + radialWeights(scalarTrueDof) * surfaceWeights[entry] * + volumeDual(mfemByNodesVectorDof(scalarTrueDof, component, support.Size())); + } + } + } + } + } // namespace + + RadialDeformationExtensionCompilationContext::RadialDeformationExtensionCompilationContext( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + mfem::ParFiniteElementSpace &vectorFiniteElementSpace, + mfem::ParMesh &logicalReferenceMesh, + field::ScalarBoundaryDofMap stellarSurfaceDofMap, + field::ScalarBoundaryDofMap infinitySurfaceDofMap, + mfem::Array stellarMaterialMarker, + mfem::Array vacuumMaterialMarker, + const int stellarSurfaceBoundaryAttribute, + const int infinitySurfaceBoundaryAttribute + ) + : m_communicator(scalarFiniteElementSpace.GetComm()) { + const mfem::Mesh *physicalMesh = scalarFiniteElementSpace.GetMesh(); + if (physicalMesh == nullptr || vectorFiniteElementSpace.GetMesh() != physicalMesh) { + throw std::invalid_argument( + "Logical radial extension scalar and vector spaces must share one physical mesh." + ); + } + int communicatorSize = 0; + MPI_Comm_size(m_communicator, &communicatorSize); + if (communicatorSize != 1) { + throw std::invalid_argument( + "Logical radial surface interpolation currently supports one MPI rank; shared interpolation-row " + "ownership remains deferred." + ); + } + if (scalarFiniteElementSpace.Nonconforming() || vectorFiniteElementSpace.Nonconforming() || + logicalReferenceMesh.Nonconforming()) { + throw std::invalid_argument("Logical radial extension compilation currently requires conforming meshes."); + } + + m_spatialDimension = physicalMesh->SpaceDimension(); + m_scalarTrueDofCount = scalarFiniteElementSpace.GetTrueVSize(); + m_volumeDisplacementSize = vectorFiniteElementSpace.GetTrueVSize(); + if (logicalReferenceMesh.SpaceDimension() != m_spatialDimension || + logicalReferenceMesh.GetNE() != physicalMesh->GetNE() || + logicalReferenceMesh.GetNBE() != physicalMesh->GetNBE()) { + throw std::invalid_argument("The STROID logical reference mesh does not match the physical mesh topology."); + } + for (int boundaryElement = 0; boundaryElement < physicalMesh->GetNBE(); ++boundaryElement) { + if (logicalReferenceMesh.GetBdrAttribute(boundaryElement) != + physicalMesh->GetBdrAttribute(boundaryElement) || + logicalReferenceMesh.GetBdrElementGeometry(boundaryElement) != + physicalMesh->GetBdrElementGeometry(boundaryElement)) { + throw std::invalid_argument("The STROID logical and physical boundary elements do not correspond."); + } + } + if (scalarFiniteElementSpace.GetVDim() != 1 || vectorFiniteElementSpace.GetVDim() != m_spatialDimension || + vectorFiniteElementSpace.GetOrdering() != mfem::Ordering::byNODES || + m_volumeDisplacementSize != m_spatialDimension * m_scalarTrueDofCount || + vectorFiniteElementSpace.FEColl() != scalarFiniteElementSpace.FEColl()) { + throw std::invalid_argument( + "Logical radial extension compilation requires an MFEM byNODES vector space made from its scalar " + "companion basis." + ); + } + if (stellarSurfaceDofMap.volume_true_dof_size() != m_scalarTrueDofCount || + infinitySurfaceDofMap.volume_true_dof_size() != m_scalarTrueDofCount) { + throw std::invalid_argument("Logical radial boundary maps do not match the scalar companion space."); + } + + m_surfaceDisplacementSize = m_spatialDimension * stellarSurfaceDofMap.local_size(); + const long long globalSurfaceSize = m_spatialDimension * stellarSurfaceDofMap.global_size(); + const long long globalOffset = m_spatialDimension * stellarSurfaceDofMap.global_offset(); + if (globalSurfaceSize > std::numeric_limits::max() || globalOffset > std::numeric_limits::max()) { + throw std::overflow_error("The logical radial extension surface vector exceeds MPI integer indexing."); + } + m_globalSurfaceDisplacementSize = static_cast(globalSurfaceSize); + m_globalSurfaceDisplacementOffset = static_cast(globalOffset); + m_surfaceDisplacementCounts = gatherCounts(m_surfaceDisplacementSize, m_communicator); + m_surfaceDisplacementOffsets = prefixOffsets(m_surfaceDisplacementCounts); + + m_stellarSupport = buildTrueDofSupport(scalarFiniteElementSpace, stellarMaterialMarker); + m_vacuumSupport = buildTrueDofSupport(scalarFiniteElementSpace, vacuumMaterialMarker); + const mfem::Vector logicalPositions = + buildLogicalTrueDofPositions(scalarFiniteElementSpace, logicalReferenceMesh); + + m_stellarSurfaceLogicalRadius = boundaryLogicalRadius(logicalReferenceMesh, stellarSurfaceBoundaryAttribute); + m_infinitySurfaceLogicalRadius = boundaryLogicalRadius(logicalReferenceMesh, infinitySurfaceBoundaryAttribute); + if (!(m_infinitySurfaceLogicalRadius > m_stellarSurfaceLogicalRadius)) { + throw std::invalid_argument("Logical reference infinity must lie outside the logical stellar surface."); + } + + m_logicalRadius.SetSize(m_scalarTrueDofCount); + m_surfaceInterpolationRowOffsets.resize(static_cast(m_scalarTrueDofCount + 1)); + mfem::Vector logicalPosition(m_spatialDimension); + mfem::Vector stellarSurfaceTarget(m_spatialDimension); + mfem::Vector infinitySurfaceTarget(m_spatialDimension); + const double centerTolerance = 64.0 * std::numeric_limits::epsilon() * m_infinitySurfaceLogicalRadius; + const double radialTolerance = 128.0 * std::numeric_limits::epsilon() * m_infinitySurfaceLogicalRadius; + + for (int scalarTrueDof = 0; scalarTrueDof < m_scalarTrueDofCount; ++scalarTrueDof) { + m_surfaceInterpolationRowOffsets[scalarTrueDof] = static_cast(m_surfaceInterpolationWeights.size()); + for (int component = 0; component < m_spatialDimension; ++component) { + logicalPosition(component) = logicalPositions(m_spatialDimension * scalarTrueDof + component); + } + + const double radius = logicalInfinityRadius(logicalPosition); + m_logicalRadius(scalarTrueDof) = radius; + if (radius <= centerTolerance) { + continue; + } + if (m_stellarSupport[scalarTrueDof] != 0 && radius > m_stellarSurfaceLogicalRadius + radialTolerance) { + throw std::invalid_argument("A stellar DOF lies outside the logical stellar surface."); + } + if (m_vacuumSupport[scalarTrueDof] != 0 && (radius < m_stellarSurfaceLogicalRadius - radialTolerance || + radius > m_infinitySurfaceLogicalRadius + radialTolerance)) { + throw std::invalid_argument("A vacuum DOF lies outside the logical exterior interval."); + } + + for (int component = 0; component < m_spatialDimension; ++component) { + stellarSurfaceTarget(component) = logicalPosition(component) * m_stellarSurfaceLogicalRadius / radius; + } + const LocatedBoundaryPoint stellarSurfacePoint = + locateLogicalBoundaryPoint(logicalReferenceMesh, stellarSurfaceBoundaryAttribute, stellarSurfaceTarget); + appendSurfaceInterpolationRow( + scalarFiniteElementSpace, stellarSurfaceDofMap, stellarSurfacePoint, + m_surfaceInterpolationGlobalCoordinates, m_surfaceInterpolationWeights + ); + + if (m_vacuumSupport[scalarTrueDof] != 0) { + for (int component = 0; component < m_spatialDimension; ++component) { + infinitySurfaceTarget(component) = + logicalPosition(component) * m_infinitySurfaceLogicalRadius / radius; + } + static_cast(locateLogicalBoundaryPoint( + logicalReferenceMesh, infinitySurfaceBoundaryAttribute, infinitySurfaceTarget + )); + } + } + m_surfaceInterpolationRowOffsets[m_scalarTrueDofCount] = static_cast(m_surfaceInterpolationWeights.size()); + } + + int RadialDeformationExtensionCompilationContext::spatialDimension() const noexcept { + return m_spatialDimension; + } + + int RadialDeformationExtensionCompilationContext::surfaceDisplacementSize() const noexcept { + return m_surfaceDisplacementSize; + } + + int RadialDeformationExtensionCompilationContext::volumeDisplacementSize() const noexcept { + return m_volumeDisplacementSize; + } + + int RadialDeformationExtensionCompilationContext::scalarTrueDofCount() const noexcept { + return m_scalarTrueDofCount; + } + + double RadialDeformationExtensionCompilationContext::logicalRadius(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the logical radial compilation context."); + } + return m_logicalRadius(scalarTrueDof); + } + + double RadialDeformationExtensionCompilationContext::stellarSurfaceLogicalRadius() const noexcept { + return m_stellarSurfaceLogicalRadius; + } + + double RadialDeformationExtensionCompilationContext::infinitySurfaceLogicalRadius() const noexcept { + return m_infinitySurfaceLogicalRadius; + } + + int RadialDeformationExtensionCompilationContext::surfaceInterpolationEntryCount(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the logical radial compilation context."); + } + return m_surfaceInterpolationRowOffsets[scalarTrueDof + 1] - m_surfaceInterpolationRowOffsets[scalarTrueDof]; + } + + int RadialDeformationExtensionCompilationContext::surfaceGlobalCoordinate( + const int scalarTrueDof, + const int interpolationEntry + ) const { + return m_surfaceInterpolationGlobalCoordinates[interpolationEntryIndex( + m_surfaceInterpolationRowOffsets, scalarTrueDof, interpolationEntry + )]; + } + + double RadialDeformationExtensionCompilationContext::surfaceInterpolationWeight( + const int scalarTrueDof, + const int interpolationEntry + ) const { + return m_surfaceInterpolationWeights[interpolationEntryIndex( + m_surfaceInterpolationRowOffsets, scalarTrueDof, interpolationEntry + )]; + } + + PowerLawRadialInteriorExtension::PowerLawRadialInteriorExtension(const double radialPower) + : m_radialPower(radialPower) { + validate(); + } + + double PowerLawRadialInteriorExtension::radialPower() const noexcept { + return m_radialPower; + } + + InteriorDeformationExtensionDescriptor PowerLawRadialInteriorExtension::descriptor() const noexcept { + return powerLawInteriorDescriptor(3); + } + + void PowerLawRadialInteriorExtension::validate() const { + if (!std::isfinite(m_radialPower) || m_radialPower < 1.0) { + throw std::invalid_argument("PowerLawRadialInteriorExtension requires a finite radial power at least one."); + } + } + + PreparedPowerLawRadialInteriorExtension::PreparedPowerLawRadialInteriorExtension( + const PowerLawRadialInteriorExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ) + : m_descriptor(powerLawInteriorDescriptor(context.m_spatialDimension)), + m_radialPower(extension.radialPower()), + m_surfaceDisplacementSize(context.m_surfaceDisplacementSize), + m_interiorDisplacementSize(context.m_volumeDisplacementSize), + m_spatialDimension(context.m_spatialDimension), + m_globalSurfaceDisplacementSize(context.m_globalSurfaceDisplacementSize), + m_globalSurfaceDisplacementOffset(context.m_globalSurfaceDisplacementOffset), + m_communicator(context.m_communicator), + m_stellarSupport(context.m_stellarSupport), + m_radialWeights(context.m_scalarTrueDofCount), + m_surfaceInterpolationRowOffsets(context.m_surfaceInterpolationRowOffsets), + m_surfaceInterpolationGlobalCoordinates(context.m_surfaceInterpolationGlobalCoordinates), + m_surfaceInterpolationWeights(context.m_surfaceInterpolationWeights), + m_surfaceDisplacementCounts(context.m_surfaceDisplacementCounts), + m_surfaceDisplacementOffsets(context.m_surfaceDisplacementOffsets), + m_globalSurfaceDisplacementWorkspace(context.m_globalSurfaceDisplacementSize), + m_localGlobalSurfaceDualWorkspace(context.m_globalSurfaceDisplacementSize), + m_globalSurfaceDualWorkspace(context.m_globalSurfaceDisplacementSize) { + for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) { + if (m_stellarSupport[scalarTrueDof] == 0 || context.m_logicalRadius(scalarTrueDof) == 0.0) { + m_radialWeights(scalarTrueDof) = 0.0; + continue; + } + m_radialWeights(scalarTrueDof) = + std::pow(context.m_logicalRadius(scalarTrueDof) / context.m_stellarSurfaceLogicalRadius, m_radialPower); + } + } + + InteriorDeformationExtensionDescriptor PreparedPowerLawRadialInteriorExtension::descriptor() const noexcept { + return m_descriptor; + } + int PreparedPowerLawRadialInteriorExtension::surfaceDisplacementSize() const noexcept { + return m_surfaceDisplacementSize; + } + int PreparedPowerLawRadialInteriorExtension::interiorDisplacementSize() const noexcept { + return m_interiorDisplacementSize; + } + int PreparedPowerLawRadialInteriorExtension::scalarTrueDofCount() const noexcept { + return m_stellarSupport.Size(); + } + double PreparedPowerLawRadialInteriorExtension::radialPower() const noexcept { + return m_radialPower; + } + bool PreparedPowerLawRadialInteriorExtension::hasStellarSupport(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the prepared interior extension."); + } + return m_stellarSupport[scalarTrueDof] != 0; + } + double PreparedPowerLawRadialInteriorExtension::radialWeight(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the prepared interior extension."); + } + return m_radialWeights(scalarTrueDof); + } + int PreparedPowerLawRadialInteriorExtension::surfaceInterpolationEntryCount(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the prepared interior extension."); + } + return m_surfaceInterpolationRowOffsets[scalarTrueDof + 1] - m_surfaceInterpolationRowOffsets[scalarTrueDof]; + } + int PreparedPowerLawRadialInteriorExtension::surfaceGlobalCoordinate( + const int scalarTrueDof, + const int interpolationEntry + ) const { + return m_surfaceInterpolationGlobalCoordinates[interpolationEntryIndex( + m_surfaceInterpolationRowOffsets, scalarTrueDof, interpolationEntry + )]; + } + double PreparedPowerLawRadialInteriorExtension::surfaceInterpolationWeight( + const int scalarTrueDof, + const int interpolationEntry + ) const { + return m_surfaceInterpolationWeights[interpolationEntryIndex( + m_surfaceInterpolationRowOffsets, scalarTrueDof, interpolationEntry + )]; + } + + void PreparedPowerLawRadialInteriorExtension::requireSurfaceSize(const mfem::Vector &vector) const { + requireVectorSize(vector, surfaceDisplacementSize(), "Surface displacement"); + } + void PreparedPowerLawRadialInteriorExtension::requireInteriorSize(const mfem::Vector &vector) const { + requireVectorSize(vector, interiorDisplacementSize(), "Interior displacement"); + } + void PreparedPowerLawRadialInteriorExtension::applyForward( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &interiorDisplacement + ) const { + gatherSurfaceDisplacement( + surfaceDisplacement, m_globalSurfaceDisplacementWorkspace, m_surfaceDisplacementCounts, + m_surfaceDisplacementOffsets, m_communicator + ); + applySparseForward( + m_spatialDimension, m_stellarSupport, m_radialWeights, m_surfaceInterpolationRowOffsets, + m_surfaceInterpolationGlobalCoordinates, m_surfaceInterpolationWeights, + m_globalSurfaceDisplacementWorkspace, interiorDisplacement + ); + } + void PreparedPowerLawRadialInteriorExtension::applyTranspose( + const mfem::Vector &interiorDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const { + applySparseTranspose( + m_spatialDimension, m_stellarSupport, m_radialWeights, m_surfaceInterpolationRowOffsets, + m_surfaceInterpolationGlobalCoordinates, m_surfaceInterpolationWeights, interiorDisplacementDual, + m_localGlobalSurfaceDualWorkspace + ); + reduceSurfaceDual( + m_localGlobalSurfaceDualWorkspace, m_globalSurfaceDualWorkspace, surfaceDisplacementDual, + m_globalSurfaceDisplacementOffset, m_communicator + ); + } + void PreparedPowerLawRadialInteriorExtension::buildInteriorDisplacement( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &interiorDisplacement + ) const { + requireSurfaceSize(surfaceDisplacement); + requireInteriorSize(interiorDisplacement); + applyForward(surfaceDisplacement, interiorDisplacement); + } + void PreparedPowerLawRadialInteriorExtension::applyJacobian( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + mfem::Vector &interiorDisplacementDirection + ) const { + requireSurfaceSize(surfaceDisplacement); + requireSurfaceSize(surfaceDisplacementDirection); + requireInteriorSize(interiorDisplacementDirection); + applyForward(surfaceDisplacementDirection, interiorDisplacementDirection); + } + void PreparedPowerLawRadialInteriorExtension::applyJacobianTranspose( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &interiorDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const { + requireSurfaceSize(surfaceDisplacement); + requireInteriorSize(interiorDisplacementDual); + requireSurfaceSize(surfaceDisplacementDual); + applyTranspose(interiorDisplacementDual, surfaceDisplacementDual); + } + void PreparedPowerLawRadialInteriorExtension::applyPullbackDerivative( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + const mfem::Vector &interiorDisplacementDual, + mfem::Vector &surfaceDisplacementDualAction + ) const { + requireSurfaceSize(surfaceDisplacement); + requireSurfaceSize(surfaceDisplacementDirection); + requireInteriorSize(interiorDisplacementDual); + requireSurfaceSize(surfaceDisplacementDualAction); + surfaceDisplacementDualAction = 0.0; + } + PreparedPowerLawRadialInteriorExtension compileInteriorDeformationExtension( + const PowerLawRadialInteriorExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ) { + extension.validate(); + return PreparedPowerLawRadialInteriorExtension(extension, context); + } + + VacuumDeformationExtensionDescriptor FixedInfinityRadialVacuumExtension::descriptor() const noexcept { + return fixedInfinityVacuumDescriptor(3); + } + void FixedInfinityRadialVacuumExtension::validate() const { + } + + PreparedFixedInfinityRadialVacuumExtension::PreparedFixedInfinityRadialVacuumExtension( + const FixedInfinityRadialVacuumExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ) + : m_descriptor(fixedInfinityVacuumDescriptor(context.m_spatialDimension)), + m_surfaceDisplacementSize(context.m_surfaceDisplacementSize), + m_vacuumDisplacementSize(context.m_volumeDisplacementSize), + m_spatialDimension(context.m_spatialDimension), + m_globalSurfaceDisplacementSize(context.m_globalSurfaceDisplacementSize), + m_globalSurfaceDisplacementOffset(context.m_globalSurfaceDisplacementOffset), + m_communicator(context.m_communicator), + m_vacuumSupport(context.m_vacuumSupport), + m_radialWeights(context.m_scalarTrueDofCount), + m_surfaceInterpolationRowOffsets(context.m_surfaceInterpolationRowOffsets), + m_surfaceInterpolationGlobalCoordinates(context.m_surfaceInterpolationGlobalCoordinates), + m_surfaceInterpolationWeights(context.m_surfaceInterpolationWeights), + m_surfaceDisplacementCounts(context.m_surfaceDisplacementCounts), + m_surfaceDisplacementOffsets(context.m_surfaceDisplacementOffsets), + m_globalSurfaceDisplacementWorkspace(context.m_globalSurfaceDisplacementSize), + m_localGlobalSurfaceDualWorkspace(context.m_globalSurfaceDisplacementSize), + m_globalSurfaceDualWorkspace(context.m_globalSurfaceDisplacementSize) { + static_cast(extension); + for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) { + if (m_vacuumSupport[scalarTrueDof] == 0) { + m_radialWeights(scalarTrueDof) = 0.0; + continue; + } + m_radialWeights(scalarTrueDof) = + (context.m_infinitySurfaceLogicalRadius - context.m_logicalRadius(scalarTrueDof)) / + (context.m_infinitySurfaceLogicalRadius - context.m_stellarSurfaceLogicalRadius); + } + } + + VacuumDeformationExtensionDescriptor PreparedFixedInfinityRadialVacuumExtension::descriptor() const noexcept { + return m_descriptor; + } + int PreparedFixedInfinityRadialVacuumExtension::surfaceDisplacementSize() const noexcept { + return m_surfaceDisplacementSize; + } + int PreparedFixedInfinityRadialVacuumExtension::vacuumDisplacementSize() const noexcept { + return m_vacuumDisplacementSize; + } + int PreparedFixedInfinityRadialVacuumExtension::scalarTrueDofCount() const noexcept { + return m_vacuumSupport.Size(); + } + bool PreparedFixedInfinityRadialVacuumExtension::hasVacuumSupport(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the prepared vacuum extension."); + } + return m_vacuumSupport[scalarTrueDof] != 0; + } + double PreparedFixedInfinityRadialVacuumExtension::radialWeight(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the prepared vacuum extension."); + } + return m_radialWeights(scalarTrueDof); + } + int PreparedFixedInfinityRadialVacuumExtension::surfaceInterpolationEntryCount(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the prepared vacuum extension."); + } + return m_surfaceInterpolationRowOffsets[scalarTrueDof + 1] - m_surfaceInterpolationRowOffsets[scalarTrueDof]; + } + int PreparedFixedInfinityRadialVacuumExtension::surfaceGlobalCoordinate( + const int scalarTrueDof, + const int interpolationEntry + ) const { + return m_surfaceInterpolationGlobalCoordinates[interpolationEntryIndex( + m_surfaceInterpolationRowOffsets, scalarTrueDof, interpolationEntry + )]; + } + double PreparedFixedInfinityRadialVacuumExtension::surfaceInterpolationWeight( + const int scalarTrueDof, + const int interpolationEntry + ) const { + return m_surfaceInterpolationWeights[interpolationEntryIndex( + m_surfaceInterpolationRowOffsets, scalarTrueDof, interpolationEntry + )]; + } + + void PreparedFixedInfinityRadialVacuumExtension::requireSurfaceSize(const mfem::Vector &vector) const { + requireVectorSize(vector, surfaceDisplacementSize(), "Surface displacement"); + } + void PreparedFixedInfinityRadialVacuumExtension::requireVacuumSize(const mfem::Vector &vector) const { + requireVectorSize(vector, vacuumDisplacementSize(), "Vacuum displacement"); + } + void PreparedFixedInfinityRadialVacuumExtension::applyForward( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &vacuumDisplacement + ) const { + gatherSurfaceDisplacement( + surfaceDisplacement, m_globalSurfaceDisplacementWorkspace, m_surfaceDisplacementCounts, + m_surfaceDisplacementOffsets, m_communicator + ); + applySparseForward( + m_spatialDimension, m_vacuumSupport, m_radialWeights, m_surfaceInterpolationRowOffsets, + m_surfaceInterpolationGlobalCoordinates, m_surfaceInterpolationWeights, + m_globalSurfaceDisplacementWorkspace, vacuumDisplacement + ); + } + void PreparedFixedInfinityRadialVacuumExtension::applyTranspose( + const mfem::Vector &vacuumDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const { + applySparseTranspose( + m_spatialDimension, m_vacuumSupport, m_radialWeights, m_surfaceInterpolationRowOffsets, + m_surfaceInterpolationGlobalCoordinates, m_surfaceInterpolationWeights, vacuumDisplacementDual, + m_localGlobalSurfaceDualWorkspace + ); + reduceSurfaceDual( + m_localGlobalSurfaceDualWorkspace, m_globalSurfaceDualWorkspace, surfaceDisplacementDual, + m_globalSurfaceDisplacementOffset, m_communicator + ); + } + void PreparedFixedInfinityRadialVacuumExtension::buildVacuumDisplacement( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &vacuumDisplacement + ) const { + requireSurfaceSize(surfaceDisplacement); + requireVacuumSize(vacuumDisplacement); + applyForward(surfaceDisplacement, vacuumDisplacement); + } + void PreparedFixedInfinityRadialVacuumExtension::applyJacobian( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + mfem::Vector &vacuumDisplacementDirection + ) const { + requireSurfaceSize(surfaceDisplacement); + requireSurfaceSize(surfaceDisplacementDirection); + requireVacuumSize(vacuumDisplacementDirection); + applyForward(surfaceDisplacementDirection, vacuumDisplacementDirection); + } + void PreparedFixedInfinityRadialVacuumExtension::applyJacobianTranspose( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &vacuumDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const { + requireSurfaceSize(surfaceDisplacement); + requireVacuumSize(vacuumDisplacementDual); + requireSurfaceSize(surfaceDisplacementDual); + applyTranspose(vacuumDisplacementDual, surfaceDisplacementDual); + } + void PreparedFixedInfinityRadialVacuumExtension::applyPullbackDerivative( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + const mfem::Vector &vacuumDisplacementDual, + mfem::Vector &surfaceDisplacementDualAction + ) const { + requireSurfaceSize(surfaceDisplacement); + requireSurfaceSize(surfaceDisplacementDirection); + requireVacuumSize(vacuumDisplacementDual); + requireSurfaceSize(surfaceDisplacementDualAction); + surfaceDisplacementDualAction = 0.0; + } + PreparedFixedInfinityRadialVacuumExtension compileVacuumDeformationExtension( + const FixedInfinityRadialVacuumExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ) { + extension.validate(); + return PreparedFixedInfinityRadialVacuumExtension(extension, context); + } +} // namespace mean_field::deformation diff --git a/libmeanfield/impl/fem.cpp b/libmeanfield/impl/fem.cpp index c7bab33..d459850 100644 --- a/libmeanfield/impl/fem.cpp +++ b/libmeanfield/impl/fem.cpp @@ -45,15 +45,33 @@ namespace mean_field::fem { stroid::refinement::UniformRefinement(fem.smesh, extraRefine); } + if (fem.smesh.mesh == nullptr || fem.smesh.reference_mesh == nullptr) { + throw std::runtime_error("A STROID mesh requires paired physical and logical reference meshes."); + } + int mpiSize = 1; MPI_Comm_size(MPI_COMM_WORLD, &mpiSize); 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.logicalReferenceMesh = + std::make_unique(MPI_COMM_WORLD, *fem.smesh.reference_mesh, meshPartitioning.get(), 1); fem.mesh->EnsureNodes(); + if (fem.logicalReferenceMesh->GetNE() != fem.mesh->GetNE()) { + throw std::runtime_error("The physical and logical reference meshes have incompatible local elements."); + } + for (int element = 0; element < fem.mesh->GetNE(); ++element) { + if (fem.logicalReferenceMesh->GetElementGeometry(element) != fem.mesh->GetElementGeometry(element) || + fem.logicalReferenceMesh->GetAttribute(element) != fem.mesh->GetAttribute(element)) { + throw std::runtime_error( + "The physical and logical reference meshes do not preserve element correspondence." + ); + } + } + // ===================================================================== // Section 2: Exterior compactification coordinate // ===================================================================== @@ -185,21 +203,32 @@ namespace mean_field::fem { *fem.displacement = 0.0; + // --------------------------------------------------------------------- + // Surface deformation: scalar H1 coordinates on StellarSurface. + // + // This ambient scalar space exists only to define the surface basis + // and owned true-DOF topology. Interior scalar DOFs are not nonlinear + // unknowns. + // --------------------------------------------------------------------- + + fem.surfaceDeformationFes = + std::make_unique(fem.mesh.get(), fem.displacementFec.get()); + // --------------------------------------------------------------------- // Density: scalar discontinuous L2 // --------------------------------------------------------------------- - fem.densityFec = DensityField::make_fec(dimension); + fem.densityFec = DensityField::make_fec(dimension); - fem.densityFes = DensityField::make_fespace(*fem.mesh, *fem.densityFec); + fem.densityFes = DensityField::make_fespace(*fem.mesh, *fem.densityFec); // --------------------------------------------------------------------- // Specific enthalpy: scalar continuous H1 // --------------------------------------------------------------------- - fem.enthalpyFec = EnthalpyField::make_fec(dimension); + fem.enthalpyFec = EnthalpyField::make_fec(dimension); - fem.enthalpyFes = EnthalpyField::make_fespace(*fem.mesh, *fem.enthalpyFec); + fem.enthalpyFes = EnthalpyField::make_fespace(*fem.mesh, *fem.enthalpyFec); // ===================================================================== // Section 4: Multipole data diff --git a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp index 986afc9..826f56f 100644 --- a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp @@ -2,6 +2,7 @@ module; #include #include +#include #include #include @@ -21,29 +22,23 @@ 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, + const int surfaceDeformationParameterCount, const mean_field::field::FieldDofMap &gravityFluxMap, const mean_field::field::FieldDofMap &gravityPotentialMap, const mean_field::field::FieldDofMap &enthalpyMap ) { - using Form = mean_field::utils::blocks::barotropic_equilibrium_form; + using Form = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form; const std::array valueSizes{ - densityMap.reduced_size(), displacementMap.reduced_size(), gravityFluxMap.reduced_size(), - gravityPotentialMap.reduced_size(), enthalpyMap.reduced_size(), 1 + densityMap.reduced_size(), surfaceDeformationParameterCount, 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 + gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(), + surfaceDeformationParameterCount, enthalpyMap.reduced_size(), 1 }; return {valueSizes, residualSizes}; @@ -182,11 +177,13 @@ namespace { ); } - [[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions - make_gravity_revisions(const mean_field::operators::StellarEquilibriumDependencies &dependencies) { + [[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions make_gravity_revisions( + const mean_field::operators::StellarEquilibriumDependencies &dependencies, + const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement + ) { return { .discretization = {.value = dependencies.discretization.revision}, - .displacement = {.value = dependencies.displacement.revision}, + .displacement = {.value = generatedDisplacement.revision}, .density = {.value = dependencies.density.revision}, .gravity_gradient = {.value = dependencies.gravityGradient.revision}, .gravity_potential = {.value = dependencies.gravityPotential.revision} @@ -194,26 +191,28 @@ namespace { } [[nodiscard]] mean_field::operators::context::barotropic::BarotropicClosureDependencies - make_barotropic_closure_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) { + make_barotropic_closure_dependencies( + const mean_field::operators::StellarEquilibriumDependencies &dependencies, + const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement + ) { return { .discretization = {.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision}, .density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision}, .enthalpy = {.identity = dependencies.enthalpy.identity, .revision = dependencies.enthalpy.revision}, - .displacement = { - .identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision - } + .displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision} }; } - [[nodiscard]] mean_field::operators::DisplacementResidualDependencies - make_displacement_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) { + [[nodiscard]] mean_field::operators::DisplacementResidualDependencies make_displacement_dependencies( + const mean_field::operators::StellarEquilibriumDependencies &dependencies, + const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement + ) { return { .discretization = {.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision}, - .density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision}, - .displacement = - {.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision}, + .density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision}, + .displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision}, .gravityGradient = {.identity = dependencies.gravityGradient.identity, .revision = dependencies.gravityGradient.revision}, .enthalpy = {.identity = dependencies.enthalpy.identity, .revision = dependencies.enthalpy.revision}, @@ -222,7 +221,10 @@ namespace { } [[nodiscard]] mean_field::operators::context::hydrostatic::HydrostaticEquilibriumDependencies - make_hydrostatic_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) { + make_hydrostatic_dependencies( + const mean_field::operators::StellarEquilibriumDependencies &dependencies, + const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement + ) { return { .discretization = {.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision}, @@ -230,44 +232,48 @@ namespace { .gravityPotential = {.identity = dependencies.gravityPotential.identity, .revision = dependencies.gravityPotential.revision}, - .displacement = - {.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision}, - .rotation = {.identity = dependencies.rotation.identity, .revision = dependencies.rotation.revision}, + .displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision}, + .rotation = {.identity = dependencies.rotation.identity, .revision = dependencies.rotation.revision}, .bernoulliConstant = { .identity = dependencies.bernoulliConstant.identity, .revision = dependencies.bernoulliConstant.revision } }; } - [[nodiscard]] mean_field::operators::MassNormalizationDependencies - make_mass_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) { + [[nodiscard]] mean_field::operators::MassNormalizationDependencies make_mass_dependencies( + const mean_field::operators::StellarEquilibriumDependencies &dependencies, + const mean_field::operators::StellarEquilibriumDependencyStamp &generatedDisplacement + ) { return { .discretization = {.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision}, - .density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision}, - .displacement = - {.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision}, - .targetMass = {.identity = dependencies.targetMass.identity, .revision = dependencies.targetMass.revision} + .density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision}, + .displacement = {.identity = generatedDisplacement.identity, .revision = generatedDisplacement.revision}, + .targetMass = {.identity = dependencies.targetMass.identity, .revision = dependencies.targetMass.revision} }; } } // namespace namespace mean_field::operators { struct PreparedStellarEquilibriumOperator::ConstructionData { + deformation::PreparedDomainDeformationRuntime domainDeformation; field::FieldDofMap densityMap; field::FieldDofMap displacementMap; field::FieldDofMap gravityFluxMap; field::FieldDofMap gravityPotentialMap; field::FieldDofMap enthalpyMap; field::FieldBoundaryDofMap pressureSurfaceRows; - field::FieldPointDofMap centerDisplacementRows; StellarEquilibriumLayout layout; mfem::Array gravityStateOffsets; mfem::Array gravityResidualOffsets; - explicit ConstructionData(fem::FEM &f) - : densityMap( + ConstructionData( + fem::FEM &f, + deformation::PreparedDomainDeformationRuntime preparedDomainDeformation + ) + : domainDeformation(std::move(preparedDomainDeformation)), + densityMap( field::make_field_dof_map< field::Density, DomainSchema>(*f.densityFes) @@ -301,17 +307,9 @@ namespace mean_field::operators { enthalpyMap ) ), - centerDisplacementRows( - field::make_field_point_dof_map( - *f.displacementFes, - displacementMap, - make_computational_origin(*f.mesh), - 1.0e-12 - ) - ), layout(make_layout( densityMap, - displacementMap, + domainDeformation.parameterCount(), gravityFluxMap, gravityPotentialMap, enthalpyMap @@ -329,10 +327,12 @@ namespace mean_field::operators { } }; - PreparedStellarEquilibriumOperator::ConstructionData - PreparedStellarEquilibriumOperator::MakeConstructionData(fem::FEM &f) { + PreparedStellarEquilibriumOperator::ConstructionData PreparedStellarEquilibriumOperator::MakeConstructionData( + fem::FEM &f, + deformation::PreparedDomainDeformationRuntime domainDeformation + ) { verify_coupled_discretization(f); - return ConstructionData(f); + return ConstructionData(f, std::move(domainDeformation)); } PreparedStellarEquilibriumOperator::PreparedStellarEquilibriumOperator( @@ -340,7 +340,8 @@ namespace mean_field::operators { const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, const double targetMass, - const PressureSurfaceConstraintView surfaceConstraint + const PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation ) : PreparedStellarEquilibriumOperator( f, @@ -348,7 +349,10 @@ namespace mean_field::operators { equationOfState, targetMass, surfaceConstraint, - MakeConstructionData(f) + MakeConstructionData( + f, + std::move(domainDeformation) + ) ) { } @@ -408,7 +412,7 @@ namespace mean_field::operators { constructionData.pressureSurfaceRows, surfaceConstraint ), - m_centeringConstraintOperator(constructionData.centerDisplacementRows), + m_domainDeformation(std::move(constructionData.domainDeformation)), m_targetMass(targetMass) { MFEM_VERIFY( std::isfinite(m_targetMass) && m_targetMass > 0.0, @@ -420,12 +424,35 @@ namespace mean_field::operators { "PreparedStellarEquilibriumOperator has inconsistent block dimensions." ); + MFEM_VERIFY( + m_domainDeformation.volumeDisplacementSize() == constructionData.displacementMap.reduced_size(), + "The domain-deformation output does not match the coupled displacement discretization." + ); + + m_generatedDisplacementDependency.identity = + static_cast(reinterpret_cast(&m_domainDeformation)); + m_gravityState.SetSize(m_gravityStateOffsets.Last()); m_gravityDirection.SetSize(m_gravityStateOffsets.Last()); - m_gravityState = 0.0; - m_gravityDirection = 0.0; + m_surfaceDeformationParameters.SetSize(m_domainDeformation.parameterCount()); + m_generatedVolumeDisplacement.SetSize(m_domainDeformation.volumeDisplacementSize()); + m_fullMechanicalResidual.SetSize(m_domainDeformation.volumeDisplacementSize()); + m_volumeDisplacementDirection.SetSize(m_domainDeformation.volumeDisplacementSize()); + m_fullMechanicalAction.SetSize(m_domainDeformation.volumeDisplacementSize()); + m_surfaceShapeAction.SetSize(m_domainDeformation.parameterCount()); + m_pullbackDerivativeAction.SetSize(m_domainDeformation.parameterCount()); + + m_gravityState = 0.0; + m_gravityDirection = 0.0; + m_surfaceDeformationParameters = 0.0; + m_generatedVolumeDisplacement = 0.0; + m_fullMechanicalResidual = 0.0; + m_volumeDisplacementDirection = 0.0; + m_fullMechanicalAction = 0.0; + m_surfaceShapeAction = 0.0; + m_pullbackDerivativeAction = 0.0; } PreparedStellarEquilibriumReport PreparedStellarEquilibriumOperator::Prepare( @@ -448,8 +475,8 @@ namespace mean_field::operators { m_preparedDependencies.density, dependencies.density, "The density revision cannot move backwards." ); validate_dependency_transition( - m_preparedDependencies.displacement, dependencies.displacement, - "The displacement revision cannot move backwards." + m_preparedDependencies.surfaceDeformation, dependencies.surfaceDeformation, + "The surface-deformation revision cannot move backwards." ); validate_dependency_transition( m_preparedDependencies.gravityGradient, dependencies.gravityGradient, @@ -477,10 +504,10 @@ namespace mean_field::operators { m_isPrepared = false; - using Form = utils::blocks::barotropic_equilibrium_form; + using Form = utils::blocks::surface_deformed_stellar_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 surfaceDeformationValue = + utils::blocks::get_value_block(utils::blocks::surface_deformation_field.parameters_term); constexpr auto gravityGradientValue = utils::blocks::get_value_block(utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialValue = @@ -490,49 +517,65 @@ namespace mean_field::operators { constexpr auto bernoulliValue = utils::blocks::get_value_block(utils::blocks::barotropic_constant_field.mass_normalization_term); - const mfem::Vector reducedDensity = make_value_view(state, m_layout, densityValue); - const mfem::Vector displacement = make_value_view(state, m_layout, displacementValue); - const mfem::Vector gravityGradient = make_value_view(state, m_layout, gravityGradientValue); - const mfem::Vector gravityPotential = make_value_view(state, m_layout, gravityPotentialValue); - const mfem::Vector reducedEnthalpy = make_value_view(state, m_layout, enthalpyValue); - const mfem::Vector bernoulli = make_value_view(state, m_layout, bernoulliValue); + const mfem::Vector reducedDensity = make_value_view(state, m_layout, densityValue); + const mfem::Vector surfaceDeformationParameters = make_value_view(state, m_layout, surfaceDeformationValue); + const mfem::Vector gravityGradient = make_value_view(state, m_layout, gravityGradientValue); + const mfem::Vector gravityPotential = make_value_view(state, m_layout, gravityPotentialValue); + const mfem::Vector reducedEnthalpy = make_value_view(state, m_layout, enthalpyValue); + const mfem::Vector bernoulli = make_value_view(state, m_layout, bernoulliValue); - pack_gravity_vector( - m_gravityState, m_gravityStateOffsets, reducedDensity, displacement, gravityGradient, gravityPotential - ); + const bool generatedGeometryChanged = + !wasPrepared || dependencies.discretization != m_preparedDependencies.discretization || + dependencies.surfaceDeformation != m_preparedDependencies.surfaceDeformation; PreparedStellarEquilibriumReport report; + if (generatedGeometryChanged) { + m_surfaceDeformationParameters = surfaceDeformationParameters; + m_generatedGeometryReport = m_domainDeformation.buildValidatedVolumeDisplacement( + m_surfaceDeformationParameters, m_generatedVolumeDisplacement + ); + ++m_generatedDisplacementDependency.revision; + ++m_statistics.generatedGeometryBuilds; + report.generatedVolumeDisplacement = true; + } + report.generatedGeometry = m_generatedGeometryReport; + report.generatedDisplacement = m_generatedDisplacementDependency; - report.gravity = m_gravityOperator.Prepare(m_gravityState, make_gravity_revisions(dependencies)); + pack_gravity_vector( + m_gravityState, m_gravityStateOffsets, reducedDensity, m_generatedVolumeDisplacement, gravityGradient, + gravityPotential + ); + + report.gravity = m_gravityOperator.Prepare( + m_gravityState, make_gravity_revisions(dependencies, m_generatedDisplacementDependency) + ); report.barotropicClosure = m_barotropicClosureOperator.Prepare( - {.density = reducedDensity, .enthalpy = reducedEnthalpy, .displacement = displacement}, - make_barotropic_closure_dependencies(dependencies) + {.density = reducedDensity, .enthalpy = reducedEnthalpy, .displacement = m_generatedVolumeDisplacement}, + make_barotropic_closure_dependencies(dependencies, m_generatedDisplacementDependency) ); report.hydrostatic = m_hydrostaticOperator.Prepare( {.enthalpy = reducedEnthalpy, .gravityPotential = gravityPotential, - .displacement = displacement, + .displacement = m_generatedVolumeDisplacement, .bernoulliConstant = bernoulli(0)}, - make_hydrostatic_dependencies(dependencies), rotation + make_hydrostatic_dependencies(dependencies, m_generatedDisplacementDependency), rotation ); report.displacement = m_displacementOperator.Prepare( - {.enthalpy = reducedEnthalpy}, make_displacement_dependencies(dependencies), rotation + {.enthalpy = reducedEnthalpy}, + make_displacement_dependencies(dependencies, m_generatedDisplacementDependency), rotation ); - report.massNormalization = - m_massNormalizationOperator.Prepare({.targetMass = m_targetMass}, make_mass_dependencies(dependencies)); + report.massNormalization = m_massNormalizationOperator.Prepare( + {.targetMass = m_targetMass}, make_mass_dependencies(dependencies, m_generatedDisplacementDependency) + ); 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(); @@ -545,7 +588,7 @@ namespace mean_field::operators { } void PreparedStellarEquilibriumOperator::AssembleResidual() { - using Form = utils::blocks::barotropic_equilibrium_form; + using Form = utils::blocks::surface_deformed_stellar_equilibrium_form; constexpr auto gravityGradientResidual = utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); @@ -553,8 +596,8 @@ namespace mean_field::operators { 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 surfaceShapeResidual = + utils::blocks::get_residual_block(utils::blocks::surface_deformation_field.shape_equilibrium_term); constexpr auto enthalpyResidual = utils::blocks::get_residual_block(utils::blocks::enthalpy_field.specific_term); constexpr auto massResidual = @@ -562,14 +605,17 @@ namespace mean_field::operators { mfem::Vector gravity; mfem::Vector closure; - mfem::Vector displacement; + mfem::Vector surfaceShape; mfem::Vector hydrostatic; mfem::Vector mass; m_gravityOperator.Mult(m_gravityState, gravity); m_barotropicClosureOperator.BuildResidual(closure); - m_displacementOperator.BuildResidual(displacement); - m_centeringConstraintOperator.ApplyResidualRows(displacement); + m_displacementOperator.BuildResidual(m_fullMechanicalResidual); + surfaceShape.SetSize(m_domainDeformation.parameterCount()); + m_domainDeformation.applyJacobianTranspose( + m_surfaceDeformationParameters, m_fullMechanicalResidual, surfaceShape + ); m_hydrostaticOperator.BuildResidual(hydrostatic); m_surfaceConstraintOperator.ApplyResidualRows(hydrostatic); m_massNormalizationOperator.BuildResidual(mass); @@ -599,8 +645,8 @@ namespace mean_field::operators { m_cachedResidual, m_layout, densityResidual, closure, "The closure residual has the wrong size." ); assign_residual_block( - m_cachedResidual, m_layout, displacementResidual, displacement, - "The displacement residual has the wrong size." + m_cachedResidual, m_layout, surfaceShapeResidual, surfaceShape, + "The surface-shape residual has the wrong size." ); assign_residual_block( @@ -633,11 +679,11 @@ namespace mean_field::operators { direction, "PreparedStellarEquilibriumOperator received a non-finite Jacobian direction." ); - using Form = utils::blocks::barotropic_equilibrium_form; + using Form = utils::blocks::surface_deformed_stellar_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 surfaceDeformationValue = + utils::blocks::get_value_block(utils::blocks::surface_deformation_field.parameters_term); constexpr auto gravityGradientValue = utils::blocks::get_value_block(utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialValue = @@ -653,51 +699,61 @@ namespace mean_field::operators { 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 surfaceShapeResidual = + utils::blocks::get_residual_block(utils::blocks::surface_deformation_field.shape_equilibrium_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); - const mfem::Vector reducedDensityDirection = make_value_view(direction, m_layout, densityValue); - const mfem::Vector displacementDirection = make_value_view(direction, m_layout, displacementValue); - const mfem::Vector gravityGradientDirection = make_value_view(direction, m_layout, gravityGradientValue); - const mfem::Vector gravityPotentialDirection = make_value_view(direction, m_layout, gravityPotentialValue); - const mfem::Vector reducedEnthalpyDirection = make_value_view(direction, m_layout, enthalpyValue); - const mfem::Vector bernoulliDirection = make_value_view(direction, m_layout, bernoulliValue); + const mfem::Vector reducedDensityDirection = make_value_view(direction, m_layout, densityValue); + const mfem::Vector surfaceDeformationDirection = make_value_view(direction, m_layout, surfaceDeformationValue); + const mfem::Vector gravityGradientDirection = make_value_view(direction, m_layout, gravityGradientValue); + const mfem::Vector gravityPotentialDirection = make_value_view(direction, m_layout, gravityPotentialValue); + const mfem::Vector reducedEnthalpyDirection = make_value_view(direction, m_layout, enthalpyValue); + const mfem::Vector bernoulliDirection = make_value_view(direction, m_layout, bernoulliValue); + + m_domainDeformation.applyJacobian( + m_surfaceDeformationParameters, surfaceDeformationDirection, m_volumeDisplacementDirection + ); pack_gravity_vector( - m_gravityDirection, m_gravityStateOffsets, reducedDensityDirection, displacementDirection, + m_gravityDirection, m_gravityStateOffsets, reducedDensityDirection, m_volumeDisplacementDirection, gravityGradientDirection, gravityPotentialDirection ); mfem::Vector gravityAction; mfem::Vector closureAction; - mfem::Vector displacementAction; mfem::Vector hydrostaticAction; mfem::Vector massAction; m_gravityJacobianOperator.Mult(m_gravityDirection, gravityAction); m_barotropicClosureOperator.Mult( - reducedDensityDirection, reducedEnthalpyDirection, displacementDirection, closureAction + reducedDensityDirection, reducedEnthalpyDirection, m_volumeDisplacementDirection, closureAction ); m_displacementOperator.ApplyCompleteJacobianAction( - reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection, - displacementAction + reducedDensityDirection, m_volumeDisplacementDirection, gravityGradientDirection, reducedEnthalpyDirection, + m_fullMechanicalAction ); - m_centeringConstraintOperator.ApplyJacobianRows(displacementDirection, displacementAction); + m_domainDeformation.applyJacobianTranspose( + m_surfaceDeformationParameters, m_fullMechanicalAction, m_surfaceShapeAction + ); + m_domainDeformation.applyPullbackDerivative( + m_surfaceDeformationParameters, surfaceDeformationDirection, m_fullMechanicalResidual, + m_pullbackDerivativeAction + ); + m_surfaceShapeAction += m_pullbackDerivativeAction; m_hydrostaticOperator.ApplyCompleteJacobianAction( - reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection, + reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), m_volumeDisplacementDirection, hydrostaticAction ); m_surfaceConstraintOperator.ApplyJacobianRows(reducedEnthalpyDirection, hydrostaticAction); m_massNormalizationOperator.ApplyCompleteJacobianAction( - reducedDensityDirection, displacementDirection, massAction + reducedDensityDirection, m_volumeDisplacementDirection, massAction ); action.SetSize(Height()); @@ -725,8 +781,8 @@ namespace mean_field::operators { action, m_layout, densityResidual, closureAction, "The closure Jacobian action has the wrong size." ); assign_residual_block( - action, m_layout, displacementResidual, displacementAction, - "The displacement Jacobian action has the wrong size." + action, m_layout, surfaceShapeResidual, m_surfaceShapeAction, + "The surface-shape Jacobian action has the wrong size." ); assign_residual_block( @@ -743,8 +799,7 @@ 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_surfaceConstraintOperator.IsPrepared() && - m_centeringConstraintOperator.IsPrepared(); + m_massNormalizationOperator.IsPrepared() && m_surfaceConstraintOperator.IsPrepared(); } double PreparedStellarEquilibriumOperator::GetTargetMass() const noexcept { @@ -808,9 +863,30 @@ namespace mean_field::operators { return m_surfaceConstraintOperator; } - const PreparedCenteringConstraint & - PreparedStellarEquilibriumOperator::GetCenteringConstraintOperator() const noexcept { - return m_centeringConstraintOperator; + const deformation::PreparedDomainDeformationRuntime & + PreparedStellarEquilibriumOperator::GetDomainDeformation() const noexcept { + return m_domainDeformation; + } + + const mfem::Vector &PreparedStellarEquilibriumOperator::GetSurfaceDeformationParameters() const { + VerifyPrepared(); + return m_surfaceDeformationParameters; + } + + const mfem::Vector &PreparedStellarEquilibriumOperator::GetGeneratedVolumeDisplacement() const { + VerifyPrepared(); + return m_generatedVolumeDisplacement; + } + + const mfem::Vector &PreparedStellarEquilibriumOperator::GetFullMechanicalResidual() const { + VerifyPrepared(); + return m_fullMechanicalResidual; + } + + const StellarEquilibriumDependencyStamp & + PreparedStellarEquilibriumOperator::GetGeneratedDisplacementDependency() const { + VerifyPrepared(); + return m_generatedDisplacementDependency; } void PreparedStellarEquilibriumOperator::VerifyPrepared() const { diff --git a/libmeanfield/interface/deformation/descriptors.cppm b/libmeanfield/interface/deformation/descriptors.cppm new file mode 100644 index 0000000..0fa432a --- /dev/null +++ b/libmeanfield/interface/deformation/descriptors.cppm @@ -0,0 +1,111 @@ +module; + +#include +#include + +export module mean_field:deformation.descriptors; + +export namespace mean_field::deformation { + enum class SurfaceMotionKind : std::uint8_t { Radial, Normal, GeneralVector }; + + enum class GeometricGaugeTreatment : std::uint8_t { + Retained, + ExcludedByParameterization, + ConstrainedByPrescription + }; + + enum class InteriorCenterBehavior : std::uint8_t { Unspecified, FixedAtReferenceCenter, DeterminedBySurfaceMotion }; + + enum class VacuumOuterBoundaryBehavior : std::uint8_t { + Unspecified, + FixedAtReferenceInfinity, + DeterminedBySurfaceMotion + }; + + struct SurfaceDeformationDescriptor final { + std::string_view name; + int spatialDimension; + SurfaceMotionKind motionKind; + bool linearOnReferenceGeometry; + bool requiresStarShapedReferenceSurface; + bool hasExactDerivativeTranspose; + bool hasExactPullbackDerivative; + GeometricGaugeTreatment translationTreatment; + GeometricGaugeTreatment orientationTreatment; + + [[nodiscard]] constexpr bool isValid() const noexcept { + return !name.empty() && spatialDimension > 0; + } + + [[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept { + return hasExactDerivativeTranspose && hasExactPullbackDerivative; + } + + constexpr bool operator==(const SurfaceDeformationDescriptor &) const = default; + }; + + struct InteriorDeformationExtensionDescriptor final { + std::string_view name; + int spatialDimension; + bool linearOnReferenceGeometry; + bool requiresRadialFoliation; + bool requiresAuxiliarySolve; + bool hasExactDerivativeTranspose; + bool hasExactPullbackDerivative; + InteriorCenterBehavior centerBehavior; + + [[nodiscard]] constexpr bool isValid() const noexcept { + return !name.empty() && spatialDimension > 0 && centerBehavior != InteriorCenterBehavior::Unspecified; + } + + [[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept { + return hasExactDerivativeTranspose && hasExactPullbackDerivative; + } + + constexpr bool operator==(const InteriorDeformationExtensionDescriptor &) const = default; + }; + + struct VacuumDeformationExtensionDescriptor final { + std::string_view name; + int spatialDimension; + bool linearOnReferenceGeometry; + bool requiresRadialFoliation; + bool requiresAuxiliarySolve; + bool hasExactDerivativeTranspose; + bool hasExactPullbackDerivative; + VacuumOuterBoundaryBehavior outerBoundaryBehavior; + + [[nodiscard]] constexpr bool isValid() const noexcept { + return !name.empty() && spatialDimension > 0 && + outerBoundaryBehavior != VacuumOuterBoundaryBehavior::Unspecified; + } + + [[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept { + return hasExactDerivativeTranspose && hasExactPullbackDerivative; + } + + constexpr bool operator==(const VacuumDeformationExtensionDescriptor &) const = default; + }; + + struct DomainDeformationDescriptor final { + SurfaceDeformationDescriptor surfaceDeformation; + InteriorDeformationExtensionDescriptor stellarInteriorExtension; + VacuumDeformationExtensionDescriptor vacuumExtension; + bool linearOnReferenceGeometry; + bool requiresAuxiliarySolve; + bool hasExactDerivativeTranspose; + bool hasExactPullbackDerivative; + + [[nodiscard]] constexpr bool isValid() const noexcept { + return surfaceDeformation.isValid() && stellarInteriorExtension.isValid() && vacuumExtension.isValid() && + surfaceDeformation.spatialDimension == stellarInteriorExtension.spatialDimension && + surfaceDeformation.spatialDimension == vacuumExtension.spatialDimension; + } + + [[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept { + return hasExactDerivativeTranspose && hasExactPullbackDerivative; + } + + constexpr bool operator==(const DomainDeformationDescriptor &) const = default; + }; +} // namespace mean_field::deformation diff --git a/libmeanfield/interface/deformation/domain_deformation.cppm b/libmeanfield/interface/deformation/domain_deformation.cppm new file mode 100644 index 0000000..4198bbe --- /dev/null +++ b/libmeanfield/interface/deformation/domain_deformation.cppm @@ -0,0 +1,870 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +export module mean_field:deformation.domain_deformation; + +export import :deformation.interior_extension; +export import :deformation.nodal_radial_surface; +export import :deformation.radial_extensions; +export import :deformation.surface_prescription; +export import :deformation.vacuum_extension; +export import :fem; +export import :field.mfem; +export import :utils.domain; + +export namespace mean_field::deformation { + enum class VolumeDeformationOwner : std::uint8_t { StellarInterior, Vacuum }; + + struct DomainDeformationDiscretizationDependencies final { + const mfem::Mesh *physicalMeshIdentity{nullptr}; + const mfem::ParMesh *logicalReferenceMeshIdentity{nullptr}; + const mfem::ParFiniteElementSpace *surfaceScalarSpaceIdentity{nullptr}; + const mfem::ParFiniteElementSpace *volumeDisplacementSpaceIdentity{nullptr}; + long physicalMeshSequence{-1}; + long logicalReferenceMeshSequence{-1}; + long surfaceScalarSpaceSequence{-1}; + long volumeDisplacementSpaceSequence{-1}; + + [[nodiscard]] bool isCurrent() const noexcept { + return physicalMeshIdentity != nullptr && logicalReferenceMeshIdentity != nullptr && + surfaceScalarSpaceIdentity != nullptr && volumeDisplacementSpaceIdentity != nullptr && + physicalMeshIdentity->GetSequence() == physicalMeshSequence && + logicalReferenceMeshIdentity->GetSequence() == logicalReferenceMeshSequence && + surfaceScalarSpaceIdentity->GetSequence() == surfaceScalarSpaceSequence && + volumeDisplacementSpaceIdentity->GetSequence() == volumeDisplacementSpaceSequence; + } + }; + + struct DomainDeformationCompositionReport final { + int scalarTrueDofCount{0}; + int stellarInteriorOwnedScalarDofCount{0}; + int vacuumOwnedScalarDofCount{0}; + int sharedSurfaceScalarDofCount{0}; + + [[nodiscard]] constexpr int assignedScalarDofCount() const noexcept { + return stellarInteriorOwnedScalarDofCount + vacuumOwnedScalarDofCount; + } + + constexpr auto operator<=>(const DomainDeformationCompositionReport &) const = default; + }; + + struct DomainDeformationGeometryReport final { + double minimumJacobianDeterminant{std::numeric_limits::infinity()}; + + [[nodiscard]] bool isOrientationPreserving(const double determinantFloor = 0.0) const noexcept { + return std::isfinite(minimumJacobianDeterminant) && std::isfinite(determinantFloor) && + determinantFloor >= 0.0 && minimumJacobianDeterminant > determinantFloor; + } + }; + + struct PreparedDomainDeformationActionStatistics final { + std::uint64_t volumeBuildApplications{0}; + std::uint64_t jacobianApplications{0}; + std::uint64_t jacobianTransposeApplications{0}; + std::uint64_t pullbackDerivativeApplications{0}; + std::uint64_t geometryInspections{0}; + + constexpr auto operator<=>(const PreparedDomainDeformationActionStatistics &) const = default; + }; + + template + concept PreparedDomainDeformationOperator = requires( + const std::remove_cvref_t &preparedDeformation, + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + const mfem::Vector &volumeDisplacementDual, + mfem::Vector &volumeDisplacement, + mfem::Vector ¶meterDual + ) { + { preparedDeformation.descriptor() } noexcept -> std::same_as; + { preparedDeformation.parameterCount() } noexcept -> std::same_as; + { preparedDeformation.surfaceDisplacementSize() } noexcept -> std::same_as; + { preparedDeformation.volumeDisplacementSize() } noexcept -> std::same_as; + { preparedDeformation.buildVolumeDisplacement(parameters, volumeDisplacement) } -> std::same_as; + { preparedDeformation.applyJacobian(parameters, parameterDirection, volumeDisplacement) } -> std::same_as; + { + preparedDeformation.applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual) + } -> std::same_as; + { + preparedDeformation.applyPullbackDerivative( + parameters, parameterDirection, volumeDisplacementDual, parameterDual + ) + } -> std::same_as; + }; + + template < + PreparedSurfaceDeformationPrescription PreparedSurface, + PreparedInteriorDeformationExtension PreparedInterior, + PreparedVacuumDeformationExtension PreparedVacuum> + class PreparedDomainDeformation final { + public: + PreparedDomainDeformation( + PreparedSurface preparedSurface, + PreparedInterior preparedInterior, + PreparedVacuum preparedVacuum, + mfem::ParFiniteElementSpace &surfaceScalarSpace, + mfem::ParFiniteElementSpace &volumeDisplacementSpace, + mfem::ParMesh &logicalReferenceMesh + ) + : m_surface(std::move(preparedSurface)), + m_interior(std::move(preparedInterior)), + m_vacuum(std::move(preparedVacuum)), + m_volumeDisplacementSpace(&volumeDisplacementSpace), + m_descriptor(makeDescriptor( + m_surface, + m_interior, + m_vacuum + )), + m_surfaceDisplacementWorkspace(surfaceDisplacementSize()), + m_surfaceDirectionWorkspace(surfaceDisplacementSize()), + m_interiorVolumeWorkspace(volumeDisplacementSize()), + m_vacuumVolumeWorkspace(volumeDisplacementSize()), + m_interiorVolumeDualWorkspace(volumeDisplacementSize()), + m_vacuumVolumeDualWorkspace(volumeDisplacementSize()), + m_interiorSurfaceDualWorkspace(surfaceDisplacementSize()), + m_vacuumSurfaceDualWorkspace(surfaceDisplacementSize()), + m_surfaceDualWorkspace(surfaceDisplacementSize()), + m_interiorSurfacePullbackWorkspace(surfaceDisplacementSize()), + m_vacuumSurfacePullbackWorkspace(surfaceDisplacementSize()), + m_surfacePullbackWorkspace(surfaceDisplacementSize()), + m_parameterPullbackWorkspace(parameterCount()), + m_volumeGridFunctionWorkspace(std::make_unique(&volumeDisplacementSpace)) { + validateCompatibility(surfaceScalarSpace, volumeDisplacementSpace, logicalReferenceMesh); + compileOwnership(); + + const mfem::Mesh *physicalMesh = volumeDisplacementSpace.GetMesh(); + m_discretizationDependencies = { + .physicalMeshIdentity = physicalMesh, + .logicalReferenceMeshIdentity = &logicalReferenceMesh, + .surfaceScalarSpaceIdentity = &surfaceScalarSpace, + .volumeDisplacementSpaceIdentity = &volumeDisplacementSpace, + .physicalMeshSequence = physicalMesh->GetSequence(), + .logicalReferenceMeshSequence = logicalReferenceMesh.GetSequence(), + .surfaceScalarSpaceSequence = surfaceScalarSpace.GetSequence(), + .volumeDisplacementSpaceSequence = volumeDisplacementSpace.GetSequence() + }; + } + + PreparedDomainDeformation(const PreparedDomainDeformation &) = delete; + PreparedDomainDeformation &operator=(const PreparedDomainDeformation &) = delete; + PreparedDomainDeformation(PreparedDomainDeformation &&) noexcept = default; + PreparedDomainDeformation &operator=(PreparedDomainDeformation &&) noexcept = default; + + [[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept { + return m_descriptor; + } + + [[nodiscard]] int parameterCount() const noexcept { + return m_surface.parameterCount(); + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return m_surface.surfaceDisplacementSize(); + } + + [[nodiscard]] int volumeDisplacementSize() const noexcept { + return m_interior.interiorDisplacementSize(); + } + + [[nodiscard]] int scalarTrueDofCount() const noexcept { + return m_interior.scalarTrueDofCount(); + } + + [[nodiscard]] int spatialDimension() const noexcept { + return m_descriptor.surfaceDeformation.spatialDimension; + } + + [[nodiscard]] VolumeDeformationOwner volumeOwner(const int scalarTrueDof) const { + requireScalarTrueDof(scalarTrueDof); + return m_volumeOwners[static_cast(scalarTrueDof)]; + } + + [[nodiscard]] bool isSharedSurfaceDof(const int scalarTrueDof) const { + requireScalarTrueDof(scalarTrueDof); + return m_interior.hasStellarSupport(scalarTrueDof) && m_vacuum.hasVacuumSupport(scalarTrueDof); + } + + [[nodiscard]] const DomainDeformationCompositionReport &compositionReport() const noexcept { + return m_compositionReport; + } + + [[nodiscard]] const DomainDeformationDiscretizationDependencies &discretizationDependencies() const noexcept { + return m_discretizationDependencies; + } + + [[nodiscard]] bool matchesCurrentDiscretization() const noexcept { + return m_discretizationDependencies.isCurrent(); + } + + [[nodiscard]] const PreparedDomainDeformationActionStatistics &actionStatistics() const noexcept { + return m_actionStatistics; + } + + [[nodiscard]] const PreparedSurface &surfaceDeformationPrescription() const noexcept { + return m_surface; + } + + [[nodiscard]] const PreparedInterior &stellarInteriorExtension() const noexcept { + return m_interior; + } + + [[nodiscard]] const PreparedVacuum &vacuumExtension() const noexcept { + return m_vacuum; + } + + void buildVolumeDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &volumeDisplacement + ) const { + requireCurrentDiscretization(); + requireParameterSize(parameters); + requireVolumeSize(volumeDisplacement); + + m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace); + m_interior.buildInteriorDisplacement(m_surfaceDisplacementWorkspace, m_interiorVolumeWorkspace); + m_vacuum.buildVacuumDisplacement(m_surfaceDisplacementWorkspace, m_vacuumVolumeWorkspace); + mergeVolumeFields(m_interiorVolumeWorkspace, m_vacuumVolumeWorkspace, volumeDisplacement); + ++m_actionStatistics.volumeBuildApplications; + } + + void applyJacobian( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + mfem::Vector &volumeDisplacementDirection + ) const { + requireCurrentDiscretization(); + requireParameterSize(parameters); + requireParameterSize(parameterDirection); + requireVolumeSize(volumeDisplacementDirection); + + m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace); + m_surface.applyJacobian(parameters, parameterDirection, m_surfaceDirectionWorkspace); + m_interior.applyJacobian( + m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_interiorVolumeWorkspace + ); + m_vacuum.applyJacobian( + m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_vacuumVolumeWorkspace + ); + mergeVolumeFields(m_interiorVolumeWorkspace, m_vacuumVolumeWorkspace, volumeDisplacementDirection); + ++m_actionStatistics.jacobianApplications; + } + + void applyJacobianTranspose( + const mfem::Vector ¶meters, + const mfem::Vector &volumeDisplacementDual, + mfem::Vector ¶meterDual + ) const { + requireCurrentDiscretization(); + requireParameterSize(parameters); + requireVolumeSize(volumeDisplacementDual); + requireParameterSize(parameterDual); + + m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace); + splitVolumeDual(volumeDisplacementDual); + applyExtensionTransposes(); + m_surface.applyJacobianTranspose(parameters, m_surfaceDualWorkspace, parameterDual); + ++m_actionStatistics.jacobianTransposeApplications; + } + + void applyPullbackDerivative( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + const mfem::Vector &volumeDisplacementDual, + mfem::Vector ¶meterDualAction + ) const { + requireCurrentDiscretization(); + requireParameterSize(parameters); + requireParameterSize(parameterDirection); + requireVolumeSize(volumeDisplacementDual); + requireParameterSize(parameterDualAction); + + m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace); + m_surface.applyJacobian(parameters, parameterDirection, m_surfaceDirectionWorkspace); + splitVolumeDual(volumeDisplacementDual); + applyExtensionTransposes(); + + m_interior.applyPullbackDerivative( + m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_interiorVolumeDualWorkspace, + m_interiorSurfacePullbackWorkspace + ); + m_vacuum.applyPullbackDerivative( + m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_vacuumVolumeDualWorkspace, + m_vacuumSurfacePullbackWorkspace + ); + addSurfaceFields( + m_interiorSurfacePullbackWorkspace, m_vacuumSurfacePullbackWorkspace, m_surfacePullbackWorkspace + ); + + m_surface.applyJacobianTranspose(parameters, m_surfacePullbackWorkspace, parameterDualAction); + m_surface.applyPullbackDerivative( + parameters, parameterDirection, m_surfaceDualWorkspace, m_parameterPullbackWorkspace + ); + parameterDualAction += m_parameterPullbackWorkspace; + ++m_actionStatistics.pullbackDerivativeApplications; + } + + [[nodiscard]] DomainDeformationGeometryReport + inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const { + requireCurrentDiscretization(); + requireVolumeSize(volumeDisplacement); + + m_volumeGridFunctionWorkspace->SetFromTrueDofs(volumeDisplacement); + mfem::Mesh *mesh = m_volumeDisplacementSpace->GetMesh(); + double localMinimumDeterminant = std::numeric_limits::infinity(); + int localGeometryIsFinite = 1; + + for (int element = 0; element < mesh->GetNE(); ++element) { + mfem::ElementTransformation *transformation = mesh->GetElementTransformation(element); + const mfem::FiniteElement *finiteElement = m_volumeDisplacementSpace->GetFE(element); + // Positivity is a pointwise geometry requirement, not an + // integration-accuracy requirement. A rule only slightly + // above the displacement order can miss a narrow negative + // region of the determinant even when a downstream physics + // rule samples it. The determinant of a d-dimensional + // degree-p deformation gradient can vary at substantially + // higher order, so inspect at a conservative d*p scale. + const int geometryInspectionOrder = + std::max(finiteElement->GetOrder() + 2, 2 * spatialDimension() * finiteElement->GetOrder()); + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), geometryInspectionOrder); + + for (int point = 0; point < rule.GetNPoints(); ++point) { + transformation->SetIntPoint(&rule.IntPoint(point)); + mfem::DenseMatrix deformationGradient; + m_volumeGridFunctionWorkspace->GetVectorGradient(*transformation, deformationGradient); + for (int component = 0; component < spatialDimension(); ++component) { + deformationGradient(component, component) += 1.0; + } + const double determinant = deformationGradient.Det(); + if (!std::isfinite(determinant)) { + localGeometryIsFinite = 0; + } else { + localMinimumDeterminant = std::min(localMinimumDeterminant, determinant); + } + } + } + + double globalMinimumDeterminant = 0.0; + int globalGeometryIsFinite = 0; + MPI_Allreduce( + &localMinimumDeterminant, &globalMinimumDeterminant, 1, MPI_DOUBLE, MPI_MIN, + m_volumeDisplacementSpace->GetComm() + ); + MPI_Allreduce( + &localGeometryIsFinite, &globalGeometryIsFinite, 1, MPI_INT, MPI_MIN, + m_volumeDisplacementSpace->GetComm() + ); + if (globalGeometryIsFinite == 0) { + globalMinimumDeterminant = std::numeric_limits::quiet_NaN(); + } + ++m_actionStatistics.geometryInspections; + return {.minimumJacobianDeterminant = globalMinimumDeterminant}; + } + + [[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &volumeDisplacement, + const double determinantFloor = 0.0 + ) const { + if (!std::isfinite(determinantFloor) || determinantFloor < 0.0) { + throw std::invalid_argument("The mapped-geometry determinant floor must be finite and non-negative."); + } + buildVolumeDisplacement(parameters, volumeDisplacement); + const DomainDeformationGeometryReport report = inspectMappedGeometry(volumeDisplacement); + if (!report.isOrientationPreserving(determinantFloor)) { + throw std::domain_error("The prepared domain deformation inverts at least one volume element."); + } + return report; + } + + private: + [[nodiscard]] static DomainDeformationDescriptor makeDescriptor( + const PreparedSurface &surface, + const PreparedInterior &interior, + const PreparedVacuum &vacuum + ) noexcept { + const SurfaceDeformationDescriptor surfaceDescriptor = surface.descriptor(); + const InteriorDeformationExtensionDescriptor interiorDescriptor = interior.descriptor(); + const VacuumDeformationExtensionDescriptor vacuumDescriptor = vacuum.descriptor(); + return { + .surfaceDeformation = surfaceDescriptor, + .stellarInteriorExtension = interiorDescriptor, + .vacuumExtension = vacuumDescriptor, + .linearOnReferenceGeometry = surfaceDescriptor.linearOnReferenceGeometry && + interiorDescriptor.linearOnReferenceGeometry && + vacuumDescriptor.linearOnReferenceGeometry, + .requiresAuxiliarySolve = + interiorDescriptor.requiresAuxiliarySolve || vacuumDescriptor.requiresAuxiliarySolve, + .hasExactDerivativeTranspose = surfaceDescriptor.hasExactDerivativeTranspose && + interiorDescriptor.hasExactDerivativeTranspose && + vacuumDescriptor.hasExactDerivativeTranspose, + .hasExactPullbackDerivative = surfaceDescriptor.hasExactPullbackDerivative && + interiorDescriptor.hasExactPullbackDerivative && + vacuumDescriptor.hasExactPullbackDerivative + }; + } + + void validateCompatibility( + mfem::ParFiniteElementSpace &surfaceScalarSpace, + mfem::ParFiniteElementSpace &volumeDisplacementSpace, + mfem::ParMesh &logicalReferenceMesh + ) const { + const mfem::Mesh *physicalMesh = volumeDisplacementSpace.GetMesh(); + if (!m_descriptor.isValid()) { + throw std::invalid_argument("Prepared domain deformation descriptors are incompatible."); + } + if (!m_descriptor.supportsExactNewtonLinearization()) { + throw std::invalid_argument("Prepared domain deformation requires exact transpose and pullback paths."); + } + if (physicalMesh == nullptr || surfaceScalarSpace.GetMesh() != physicalMesh) { + throw std::invalid_argument("Prepared domain deformation spaces must share one physical mesh."); + } + if (logicalReferenceMesh.GetNE() != physicalMesh->GetNE() || + logicalReferenceMesh.GetNBE() != physicalMesh->GetNBE()) { + throw std::invalid_argument("Prepared domain deformation requires the paired logical reference mesh."); + } + if (m_surface.surfaceDisplacementSize() != m_interior.surfaceDisplacementSize() || + m_surface.surfaceDisplacementSize() != m_vacuum.surfaceDisplacementSize()) { + throw std::invalid_argument("Prepared deformation factors have incompatible surface trace sizes."); + } + if (m_interior.interiorDisplacementSize() != m_vacuum.vacuumDisplacementSize() || + m_interior.interiorDisplacementSize() != volumeDisplacementSpace.GetTrueVSize()) { + throw std::invalid_argument("Prepared deformation factors have incompatible volume vector sizes."); + } + if (m_interior.scalarTrueDofCount() != m_vacuum.scalarTrueDofCount() || + volumeDisplacementSpace.GetTrueVSize() != spatialDimension() * m_interior.scalarTrueDofCount()) { + throw std::invalid_argument("Prepared deformation factors have incompatible scalar volume topology."); + } + if (volumeDisplacementSpace.GetOrdering() != mfem::Ordering::byNODES) { + throw std::invalid_argument("Prepared domain deformation requires MFEM byNODES volume ordering."); + } + } + + void compileOwnership() { + m_volumeOwners.resize(static_cast(scalarTrueDofCount())); + m_compositionReport.scalarTrueDofCount = scalarTrueDofCount(); + + for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) { + const bool hasStellarSupport = m_interior.hasStellarSupport(scalarTrueDof); + const bool hasVacuumSupport = m_vacuum.hasVacuumSupport(scalarTrueDof); + if (!hasStellarSupport && !hasVacuumSupport) { + throw std::invalid_argument("A volume displacement DOF has no deformation-extension owner."); + } + if (hasStellarSupport) { + m_volumeOwners[static_cast(scalarTrueDof)] = VolumeDeformationOwner::StellarInterior; + ++m_compositionReport.stellarInteriorOwnedScalarDofCount; + if (hasVacuumSupport) { + ++m_compositionReport.sharedSurfaceScalarDofCount; + } + } else { + m_volumeOwners[static_cast(scalarTrueDof)] = VolumeDeformationOwner::Vacuum; + ++m_compositionReport.vacuumOwnedScalarDofCount; + } + } + } + + [[nodiscard]] int volumeVectorDof( + const int scalarTrueDof, + const int component + ) const noexcept { + return scalarTrueDof + component * scalarTrueDofCount(); + } + + void mergeVolumeFields( + const mfem::Vector &interiorVolume, + const mfem::Vector &vacuumVolume, + mfem::Vector &volume + ) const noexcept { + for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) { + const mfem::Vector &source = volumeOwner(scalarTrueDof) == VolumeDeformationOwner::StellarInterior + ? interiorVolume + : vacuumVolume; + for (int component = 0; component < spatialDimension(); ++component) { + const int vectorDof = volumeVectorDof(scalarTrueDof, component); + volume(vectorDof) = source(vectorDof); + } + } + } + + void splitVolumeDual(const mfem::Vector &volumeDual) const noexcept { + m_interiorVolumeDualWorkspace = 0.0; + m_vacuumVolumeDualWorkspace = 0.0; + for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) { + mfem::Vector &destination = volumeOwner(scalarTrueDof) == VolumeDeformationOwner::StellarInterior + ? m_interiorVolumeDualWorkspace + : m_vacuumVolumeDualWorkspace; + for (int component = 0; component < spatialDimension(); ++component) { + const int vectorDof = volumeVectorDof(scalarTrueDof, component); + destination(vectorDof) = volumeDual(vectorDof); + } + } + } + + void applyExtensionTransposes() const { + m_interior.applyJacobianTranspose( + m_surfaceDisplacementWorkspace, m_interiorVolumeDualWorkspace, m_interiorSurfaceDualWorkspace + ); + m_vacuum.applyJacobianTranspose( + m_surfaceDisplacementWorkspace, m_vacuumVolumeDualWorkspace, m_vacuumSurfaceDualWorkspace + ); + addSurfaceFields(m_interiorSurfaceDualWorkspace, m_vacuumSurfaceDualWorkspace, m_surfaceDualWorkspace); + } + + static void addSurfaceFields( + const mfem::Vector &interior, + const mfem::Vector &vacuum, + mfem::Vector &sum + ) { + sum = interior; + sum += vacuum; + } + + void requireCurrentDiscretization() const { + if (!matchesCurrentDiscretization()) { + throw std::logic_error("Prepared domain deformation discretization dependencies are stale."); + } + } + + void requireParameterSize(const mfem::Vector ¶meters) const { + if (parameters.Size() != parameterCount()) { + throw std::invalid_argument("Prepared domain deformation received an incompatible parameter vector."); + } + } + + void requireVolumeSize(const mfem::Vector &volume) const { + if (volume.Size() != volumeDisplacementSize()) { + throw std::invalid_argument("Prepared domain deformation received an incompatible volume vector."); + } + } + + void requireScalarTrueDof(const int scalarTrueDof) const { + if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) { + throw std::out_of_range("Scalar true DOF is outside the prepared domain deformation."); + } + } + + PreparedSurface m_surface; + PreparedInterior m_interior; + PreparedVacuum m_vacuum; + mfem::ParFiniteElementSpace *m_volumeDisplacementSpace; + DomainDeformationDescriptor m_descriptor; + DomainDeformationCompositionReport m_compositionReport; + DomainDeformationDiscretizationDependencies m_discretizationDependencies; + std::vector m_volumeOwners; + mutable PreparedDomainDeformationActionStatistics m_actionStatistics; + mutable mfem::Vector m_surfaceDisplacementWorkspace; + mutable mfem::Vector m_surfaceDirectionWorkspace; + mutable mfem::Vector m_interiorVolumeWorkspace; + mutable mfem::Vector m_vacuumVolumeWorkspace; + mutable mfem::Vector m_interiorVolumeDualWorkspace; + mutable mfem::Vector m_vacuumVolumeDualWorkspace; + mutable mfem::Vector m_interiorSurfaceDualWorkspace; + mutable mfem::Vector m_vacuumSurfaceDualWorkspace; + mutable mfem::Vector m_surfaceDualWorkspace; + mutable mfem::Vector m_interiorSurfacePullbackWorkspace; + mutable mfem::Vector m_vacuumSurfacePullbackWorkspace; + mutable mfem::Vector m_surfacePullbackWorkspace; + mutable mfem::Vector m_parameterPullbackWorkspace; + mutable std::unique_ptr m_volumeGridFunctionWorkspace; + }; + + template < + PreparedSurfaceDeformationPrescription PreparedSurface, + PreparedInteriorDeformationExtension PreparedInterior, + PreparedVacuumDeformationExtension PreparedVacuum> + [[nodiscard]] auto composePreparedDomainDeformation( + PreparedSurface preparedSurface, + PreparedInterior preparedInterior, + PreparedVacuum preparedVacuum, + mfem::ParFiniteElementSpace &surfaceScalarSpace, + mfem::ParFiniteElementSpace &volumeDisplacementSpace, + mfem::ParMesh &logicalReferenceMesh + ) { + return PreparedDomainDeformation{ + std::move(preparedSurface), std::move(preparedInterior), std::move(preparedVacuum), + surfaceScalarSpace, volumeDisplacementSpace, logicalReferenceMesh + }; + } + + class PreparedDomainDeformationRuntime final { + public: + template + requires(!std::same_as< + std::remove_cvref_t, + PreparedDomainDeformationRuntime>) + explicit PreparedDomainDeformationRuntime(PreparedDeformation &&preparedDeformation) + : m_implementation( + std::make_unique>>( + std::forward(preparedDeformation) + ) + ) { + } + + PreparedDomainDeformationRuntime(const PreparedDomainDeformationRuntime &) = delete; + PreparedDomainDeformationRuntime &operator=(const PreparedDomainDeformationRuntime &) = delete; + PreparedDomainDeformationRuntime(PreparedDomainDeformationRuntime &&) noexcept = default; + PreparedDomainDeformationRuntime &operator=(PreparedDomainDeformationRuntime &&) noexcept = default; + + [[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept { + return m_implementation->descriptor(); + } + + [[nodiscard]] int parameterCount() const noexcept { + return m_implementation->parameterCount(); + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return m_implementation->surfaceDisplacementSize(); + } + + [[nodiscard]] int volumeDisplacementSize() const noexcept { + return m_implementation->volumeDisplacementSize(); + } + + [[nodiscard]] bool matchesCurrentDiscretization() const noexcept { + return m_implementation->matchesCurrentDiscretization(); + } + + [[nodiscard]] DomainDeformationCompositionReport compositionReport() const noexcept { + return m_implementation->compositionReport(); + } + + [[nodiscard]] DomainDeformationDiscretizationDependencies discretizationDependencies() const noexcept { + return m_implementation->discretizationDependencies(); + } + + [[nodiscard]] PreparedDomainDeformationActionStatistics actionStatistics() const noexcept { + return m_implementation->actionStatistics(); + } + + void buildVolumeDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &volumeDisplacement + ) const { + m_implementation->buildVolumeDisplacement(parameters, volumeDisplacement); + } + + void applyJacobian( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + mfem::Vector &volumeDisplacementDirection + ) const { + m_implementation->applyJacobian(parameters, parameterDirection, volumeDisplacementDirection); + } + + void applyJacobianTranspose( + const mfem::Vector ¶meters, + const mfem::Vector &volumeDisplacementDual, + mfem::Vector ¶meterDual + ) const { + m_implementation->applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual); + } + + void applyPullbackDerivative( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + const mfem::Vector &volumeDisplacementDual, + mfem::Vector ¶meterDualAction + ) const { + m_implementation->applyPullbackDerivative( + parameters, parameterDirection, volumeDisplacementDual, parameterDualAction + ); + } + + [[nodiscard]] DomainDeformationGeometryReport + inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const { + return m_implementation->inspectMappedGeometry(volumeDisplacement); + } + + [[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &volumeDisplacement, + const double determinantFloor = 0.0 + ) const { + return m_implementation->buildValidatedVolumeDisplacement(parameters, volumeDisplacement, determinantFloor); + } + + private: + class Interface { + public: + virtual ~Interface() = default; + + [[nodiscard]] virtual DomainDeformationDescriptor descriptor() const noexcept = 0; + [[nodiscard]] virtual int parameterCount() const noexcept = 0; + [[nodiscard]] virtual int surfaceDisplacementSize() const noexcept = 0; + [[nodiscard]] virtual int volumeDisplacementSize() const noexcept = 0; + [[nodiscard]] virtual bool matchesCurrentDiscretization() const noexcept = 0; + [[nodiscard]] virtual DomainDeformationCompositionReport compositionReport() const noexcept = 0; + [[nodiscard]] virtual DomainDeformationDiscretizationDependencies + discretizationDependencies() const noexcept = 0; + [[nodiscard]] virtual PreparedDomainDeformationActionStatistics actionStatistics() const noexcept = 0; + virtual void buildVolumeDisplacement( + const mfem::Vector &, + mfem::Vector & + ) const = 0; + virtual void applyJacobian( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const = 0; + virtual void applyJacobianTranspose( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const = 0; + virtual void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const = 0; + [[nodiscard]] virtual DomainDeformationGeometryReport inspectMappedGeometry(const mfem::Vector &) const = 0; + [[nodiscard]] virtual DomainDeformationGeometryReport buildValidatedVolumeDisplacement( + const mfem::Vector &, + mfem::Vector &, + double + ) const = 0; + }; + + template class Implementation final : public Interface { + public: + explicit Implementation(PreparedDeformation preparedDeformation) + : m_preparedDeformation(std::move(preparedDeformation)) { + } + + [[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept override { + return m_preparedDeformation.descriptor(); + } + [[nodiscard]] int parameterCount() const noexcept override { + return m_preparedDeformation.parameterCount(); + } + [[nodiscard]] int surfaceDisplacementSize() const noexcept override { + return m_preparedDeformation.surfaceDisplacementSize(); + } + [[nodiscard]] int volumeDisplacementSize() const noexcept override { + return m_preparedDeformation.volumeDisplacementSize(); + } + [[nodiscard]] bool matchesCurrentDiscretization() const noexcept override { + return m_preparedDeformation.matchesCurrentDiscretization(); + } + [[nodiscard]] DomainDeformationCompositionReport compositionReport() const noexcept override { + return m_preparedDeformation.compositionReport(); + } + [[nodiscard]] DomainDeformationDiscretizationDependencies + discretizationDependencies() const noexcept override { + return m_preparedDeformation.discretizationDependencies(); + } + [[nodiscard]] PreparedDomainDeformationActionStatistics actionStatistics() const noexcept override { + return m_preparedDeformation.actionStatistics(); + } + void buildVolumeDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &volumeDisplacement + ) const override { + m_preparedDeformation.buildVolumeDisplacement(parameters, volumeDisplacement); + } + void applyJacobian( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + mfem::Vector &volumeDisplacementDirection + ) const override { + m_preparedDeformation.applyJacobian(parameters, parameterDirection, volumeDisplacementDirection); + } + void applyJacobianTranspose( + const mfem::Vector ¶meters, + const mfem::Vector &volumeDisplacementDual, + mfem::Vector ¶meterDual + ) const override { + m_preparedDeformation.applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual); + } + void applyPullbackDerivative( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + const mfem::Vector &volumeDisplacementDual, + mfem::Vector ¶meterDualAction + ) const override { + m_preparedDeformation.applyPullbackDerivative( + parameters, parameterDirection, volumeDisplacementDual, parameterDualAction + ); + } + [[nodiscard]] DomainDeformationGeometryReport + inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const override { + return m_preparedDeformation.inspectMappedGeometry(volumeDisplacement); + } + [[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &volumeDisplacement, + const double determinantFloor + ) const override { + return m_preparedDeformation.buildValidatedVolumeDisplacement( + parameters, volumeDisplacement, determinantFloor + ); + } + + private: + PreparedDeformation m_preparedDeformation; + }; + + std::unique_ptr m_implementation; + }; + + template < + utils::domain::IsSchema SchemaT = utils::domain::CoreEnvelopeVacuumDomainSchema, + SurfaceDeformationPrescription SurfacePrescription, + InteriorDeformationExtension InteriorExtension, + VacuumDeformationExtension VacuumExtension> + requires SurfaceDeformationCompilable< + SurfacePrescription, + SurfaceDeformationCompilationContext> && + InteriorDeformationExtensionCompilable< + InteriorExtension, + RadialDeformationExtensionCompilationContext> && + VacuumDeformationExtensionCompilable< + VacuumExtension, + RadialDeformationExtensionCompilationContext> + [[nodiscard]] auto compileDomainDeformation( + const SurfacePrescription &surfacePrescription, + const InteriorExtension &interiorExtension, + const VacuumExtension &vacuumExtension, + fem::FEM &finiteElementModel + ) { + if (!finiteElementModel.okay()) { + throw std::invalid_argument("Domain deformation compilation requires a complete finite-element model."); + } + + const field::ScalarBoundaryDofMap surfaceDofMap = + field::make_stellar_surface_scalar_dof_map(*finiteElementModel.surfaceDeformationFes); + const SurfaceDeformationCompilationContext surfaceContext{ + *finiteElementModel.surfaceDeformationFes, surfaceDofMap + }; + auto preparedSurface = compileSurfaceDeformationPrescription(surfacePrescription, surfaceContext); + + const RadialDeformationExtensionCompilationContext extensionContext = + makeRadialDeformationExtensionCompilationContext( + *finiteElementModel.surfaceDeformationFes, *finiteElementModel.displacementFes, + *finiteElementModel.logicalReferenceMesh + ); + auto preparedInterior = compileInteriorDeformationExtension(interiorExtension, extensionContext); + auto preparedVacuum = compileVacuumDeformationExtension(vacuumExtension, extensionContext); + + return composePreparedDomainDeformation( + std::move(preparedSurface), std::move(preparedInterior), std::move(preparedVacuum), + *finiteElementModel.surfaceDeformationFes, *finiteElementModel.displacementFes, + *finiteElementModel.logicalReferenceMesh + ); + } +} // namespace mean_field::deformation diff --git a/libmeanfield/interface/deformation/interior_extension.cppm b/libmeanfield/interface/deformation/interior_extension.cppm new file mode 100644 index 0000000..979cc7b --- /dev/null +++ b/libmeanfield/interface/deformation/interior_extension.cppm @@ -0,0 +1,63 @@ +module; + +#include +#include + +#include + +export module mean_field:deformation.interior_extension; + +export import :deformation.descriptors; + +export namespace mean_field::deformation { + template + concept PreparedInteriorDeformationExtension = requires( + const std::remove_cvref_t &preparedExtension, + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + const mfem::Vector &interiorDisplacementDual, + mfem::Vector &interiorDisplacement, + mfem::Vector &surfaceDisplacementDual + ) { + { preparedExtension.descriptor() } noexcept -> std::same_as; + { preparedExtension.surfaceDisplacementSize() } noexcept -> std::same_as; + { preparedExtension.interiorDisplacementSize() } noexcept -> std::same_as; + { preparedExtension.scalarTrueDofCount() } noexcept -> std::same_as; + { preparedExtension.hasStellarSupport(0) } -> std::same_as; + { + preparedExtension.buildInteriorDisplacement(surfaceDisplacement, interiorDisplacement) + } -> std::same_as; + { + preparedExtension.applyJacobian(surfaceDisplacement, surfaceDisplacementDirection, interiorDisplacement) + } -> std::same_as; + { + preparedExtension.applyJacobianTranspose( + surfaceDisplacement, interiorDisplacementDual, surfaceDisplacementDual + ) + } -> std::same_as; + { + preparedExtension.applyPullbackDerivative( + surfaceDisplacement, surfaceDisplacementDirection, interiorDisplacementDual, surfaceDisplacementDual + ) + } -> std::same_as; + }; + + template + concept InteriorDeformationExtension = requires(const std::remove_cvref_t &extension) { + typename std::remove_cvref_t::PreparedType; + requires PreparedInteriorDeformationExtension::PreparedType>; + { extension.descriptor() } noexcept -> std::same_as; + { extension.validate() } -> std::same_as; + }; + + template + concept InteriorDeformationExtensionCompilable = + InteriorDeformationExtension && requires( + const std::remove_cvref_t &extension, + const std::remove_cvref_t &context + ) { + { + compileInteriorDeformationExtension(extension, context) + } -> std::same_as::PreparedType>; + }; +} // namespace mean_field::deformation diff --git a/libmeanfield/interface/deformation/nodal_radial_surface.cppm b/libmeanfield/interface/deformation/nodal_radial_surface.cppm new file mode 100644 index 0000000..29373e7 --- /dev/null +++ b/libmeanfield/interface/deformation/nodal_radial_surface.cppm @@ -0,0 +1,138 @@ +module; + +#include + +export module mean_field:deformation.nodal_radial_surface; + +export import :deformation.surface_prescription; +export import :field.mfem; + +export namespace mean_field::deformation { + class PreparedNodalRadialSurface; + + class SurfaceDeformationCompilationContext final { + public: + SurfaceDeformationCompilationContext( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + field::ScalarBoundaryDofMap surfaceDofMap + ); + + [[nodiscard]] mfem::ParFiniteElementSpace &scalarFiniteElementSpace() const noexcept; + + [[nodiscard]] const field::ScalarBoundaryDofMap &surfaceDofMap() const noexcept; + + private: + mfem::ParFiniteElementSpace *m_scalarFiniteElementSpace; + field::ScalarBoundaryDofMap m_surfaceDofMap; + }; + + class NodalRadialSurface final { + public: + using PreparedType = PreparedNodalRadialSurface; + + explicit NodalRadialSurface(mfem::Vector referenceCenter); + + [[nodiscard]] const mfem::Vector &referenceCenter() const noexcept; + + [[nodiscard]] SurfaceDeformationDescriptor descriptor() const noexcept; + + void validate() const; + + private: + mfem::Vector m_referenceCenter; + }; + + class PreparedNodalRadialSurface final { + public: + [[nodiscard]] SurfaceDeformationDescriptor descriptor() const noexcept; + + [[nodiscard]] int parameterCount() const noexcept; + + [[nodiscard]] long long globalParameterCount() const noexcept; + + [[nodiscard]] long long globalParameterOffset() const noexcept; + + [[nodiscard]] int spatialDimension() const noexcept; + + [[nodiscard]] int surfaceDisplacementSize() const noexcept; + + [[nodiscard]] long long globalSurfaceDisplacementSize() const noexcept; + + [[nodiscard]] long long globalSurfaceDisplacementOffset() const noexcept; + + [[nodiscard]] int surfaceDisplacementDof( + int parameterDof, + int component + ) const; + + [[nodiscard]] double radialDirection( + int parameterDof, + int component + ) const; + + [[nodiscard]] double referenceRadius(int parameterDof) const; + + [[nodiscard]] const mfem::Vector &referenceCenter() const noexcept; + + [[nodiscard]] const field::ScalarBoundaryDofMap &surfaceDofMap() const noexcept; + + void buildSurfaceDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &surfaceDisplacement + ) const; + + void applyJacobian( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + mfem::Vector &surfaceDisplacementDirection + ) const; + + void applyJacobianTranspose( + const mfem::Vector ¶meters, + const mfem::Vector &surfaceDisplacementDual, + mfem::Vector ¶meterDual + ) const; + + void applyPullbackDerivative( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + const mfem::Vector &surfaceDisplacementDual, + mfem::Vector ¶meterDualAction + ) const; + + private: + friend PreparedNodalRadialSurface compileSurfaceDeformationPrescription( + const NodalRadialSurface &prescription, + const SurfaceDeformationCompilationContext &context + ); + + PreparedNodalRadialSurface( + SurfaceDeformationDescriptor descriptor, + mfem::Vector referenceCenter, + field::ScalarBoundaryDofMap surfaceDofMap, + mfem::Vector radialDirections, + mfem::Vector referenceRadii + ); + + void requireParameterSize(const mfem::Vector ¶meters) const; + + void requireSurfaceDisplacementSize(const mfem::Vector &surfaceDisplacement) const; + + SurfaceDeformationDescriptor m_descriptor; + mfem::Vector m_referenceCenter; + field::ScalarBoundaryDofMap m_surfaceDofMap; + mfem::Vector m_radialDirections; + mfem::Vector m_referenceRadii; + }; + + [[nodiscard]] PreparedNodalRadialSurface compileSurfaceDeformationPrescription( + const NodalRadialSurface &prescription, + const SurfaceDeformationCompilationContext &context + ); + + static_assert(SurfaceDeformationPrescription); + static_assert(PreparedSurfaceDeformationPrescription); + static_assert(SurfaceDeformationCompilable< + NodalRadialSurface, + SurfaceDeformationCompilationContext>); +} // namespace mean_field::deformation diff --git a/libmeanfield/interface/deformation/radial_extensions.cppm b/libmeanfield/interface/deformation/radial_extensions.cppm new file mode 100644 index 0000000..5021b9f --- /dev/null +++ b/libmeanfield/interface/deformation/radial_extensions.cppm @@ -0,0 +1,302 @@ +module; + +#include +#include + +#include +#include + +export module mean_field:deformation.radial_extensions; + +export import :deformation.interior_extension; +export import :deformation.vacuum_extension; +export import :field.mfem; +export import :utils.domain; + +export namespace mean_field::deformation { + class RadialDeformationExtensionCompilationContext final { + public: + RadialDeformationExtensionCompilationContext( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + mfem::ParFiniteElementSpace &vectorFiniteElementSpace, + mfem::ParMesh &logicalReferenceMesh, + field::ScalarBoundaryDofMap stellarSurfaceDofMap, + field::ScalarBoundaryDofMap infinitySurfaceDofMap, + mfem::Array stellarMaterialMarker, + mfem::Array vacuumMaterialMarker, + int stellarSurfaceBoundaryAttribute, + int infinitySurfaceBoundaryAttribute + ); + + [[nodiscard]] int spatialDimension() const noexcept; + [[nodiscard]] int surfaceDisplacementSize() const noexcept; + [[nodiscard]] int volumeDisplacementSize() const noexcept; + [[nodiscard]] int scalarTrueDofCount() const noexcept; + [[nodiscard]] double logicalRadius(int scalarTrueDof) const; + [[nodiscard]] double stellarSurfaceLogicalRadius() const noexcept; + [[nodiscard]] double infinitySurfaceLogicalRadius() const noexcept; + [[nodiscard]] int surfaceInterpolationEntryCount(int scalarTrueDof) const; + [[nodiscard]] int surfaceGlobalCoordinate( + int scalarTrueDof, + int interpolationEntry + ) const; + [[nodiscard]] double surfaceInterpolationWeight( + int scalarTrueDof, + int interpolationEntry + ) const; + + private: + friend class PreparedPowerLawRadialInteriorExtension; + friend class PreparedFixedInfinityRadialVacuumExtension; + + int m_spatialDimension{0}; + int m_scalarTrueDofCount{0}; + int m_volumeDisplacementSize{0}; + int m_surfaceDisplacementSize{0}; + int m_globalSurfaceDisplacementSize{0}; + int m_globalSurfaceDisplacementOffset{0}; + MPI_Comm m_communicator{MPI_COMM_NULL}; + mfem::Array m_stellarSupport; + mfem::Array m_vacuumSupport; + mfem::Vector m_logicalRadius; + double m_stellarSurfaceLogicalRadius{0.0}; + double m_infinitySurfaceLogicalRadius{0.0}; + std::vector m_surfaceInterpolationRowOffsets; + std::vector m_surfaceInterpolationGlobalCoordinates; + std::vector m_surfaceInterpolationWeights; + std::vector m_surfaceDisplacementCounts; + std::vector m_surfaceDisplacementOffsets; + }; + + template + requires( + SchemaT::template contains_domain() && + SchemaT::template contains_domain() && + SchemaT::template contains_boundary() && + SchemaT::template contains_boundary() + ) + [[nodiscard]] RadialDeformationExtensionCompilationContext makeRadialDeformationExtensionCompilationContext( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + mfem::ParFiniteElementSpace &vectorFiniteElementSpace, + mfem::ParMesh &logicalReferenceMesh + ) { + const mfem::Mesh *mesh = scalarFiniteElementSpace.GetMesh(); + MFEM_VERIFY(mesh != nullptr, "Radial deformation extension compilation requires an MFEM mesh."); + + return RadialDeformationExtensionCompilationContext( + scalarFiniteElementSpace, vectorFiniteElementSpace, logicalReferenceMesh, + field::make_scalar_boundary_dof_map(scalarFiniteElementSpace), + field::make_scalar_boundary_dof_map(scalarFiniteElementSpace), + utils::domain::make_attribute_marker(*mesh), + utils::domain::make_attribute_marker(*mesh), + SchemaT::template boundary_attribute(), + SchemaT::template boundary_attribute() + ); + } + + class PreparedPowerLawRadialInteriorExtension; + + class PowerLawRadialInteriorExtension final { + public: + using PreparedType = PreparedPowerLawRadialInteriorExtension; + + explicit PowerLawRadialInteriorExtension(double radialPower = 2.0); + [[nodiscard]] double radialPower() const noexcept; + [[nodiscard]] InteriorDeformationExtensionDescriptor descriptor() const noexcept; + void validate() const; + + private: + double m_radialPower; + }; + + class PreparedPowerLawRadialInteriorExtension final { + public: + [[nodiscard]] InteriorDeformationExtensionDescriptor descriptor() const noexcept; + [[nodiscard]] int surfaceDisplacementSize() const noexcept; + [[nodiscard]] int interiorDisplacementSize() const noexcept; + [[nodiscard]] int scalarTrueDofCount() const noexcept; + [[nodiscard]] double radialPower() const noexcept; + [[nodiscard]] bool hasStellarSupport(int scalarTrueDof) const; + [[nodiscard]] double radialWeight(int scalarTrueDof) const; + [[nodiscard]] int surfaceInterpolationEntryCount(int scalarTrueDof) const; + [[nodiscard]] int surfaceGlobalCoordinate( + int scalarTrueDof, + int interpolationEntry + ) const; + [[nodiscard]] double surfaceInterpolationWeight( + int scalarTrueDof, + int interpolationEntry + ) const; + + void buildInteriorDisplacement( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &interiorDisplacement + ) const; + void applyJacobian( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + mfem::Vector &interiorDisplacementDirection + ) const; + void applyJacobianTranspose( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &interiorDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const; + void applyPullbackDerivative( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + const mfem::Vector &interiorDisplacementDual, + mfem::Vector &surfaceDisplacementDualAction + ) const; + + private: + friend PreparedPowerLawRadialInteriorExtension compileInteriorDeformationExtension( + const PowerLawRadialInteriorExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ); + + PreparedPowerLawRadialInteriorExtension( + const PowerLawRadialInteriorExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ); + void requireSurfaceSize(const mfem::Vector &surfaceDisplacement) const; + void requireInteriorSize(const mfem::Vector &interiorDisplacement) const; + void applyForward( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &interiorDisplacement + ) const; + void applyTranspose( + const mfem::Vector &interiorDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const; + + InteriorDeformationExtensionDescriptor m_descriptor; + double m_radialPower{2.0}; + int m_surfaceDisplacementSize{0}; + int m_interiorDisplacementSize{0}; + int m_spatialDimension{0}; + int m_globalSurfaceDisplacementSize{0}; + int m_globalSurfaceDisplacementOffset{0}; + MPI_Comm m_communicator{MPI_COMM_NULL}; + mfem::Array m_stellarSupport; + mfem::Vector m_radialWeights; + std::vector m_surfaceInterpolationRowOffsets; + std::vector m_surfaceInterpolationGlobalCoordinates; + std::vector m_surfaceInterpolationWeights; + std::vector m_surfaceDisplacementCounts; + std::vector m_surfaceDisplacementOffsets; + mutable mfem::Vector m_globalSurfaceDisplacementWorkspace; + mutable mfem::Vector m_localGlobalSurfaceDualWorkspace; + mutable mfem::Vector m_globalSurfaceDualWorkspace; + }; + + [[nodiscard]] PreparedPowerLawRadialInteriorExtension compileInteriorDeformationExtension( + const PowerLawRadialInteriorExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ); + + class PreparedFixedInfinityRadialVacuumExtension; + + class FixedInfinityRadialVacuumExtension final { + public: + using PreparedType = PreparedFixedInfinityRadialVacuumExtension; + + [[nodiscard]] VacuumDeformationExtensionDescriptor descriptor() const noexcept; + void validate() const; + }; + + class PreparedFixedInfinityRadialVacuumExtension final { + public: + [[nodiscard]] VacuumDeformationExtensionDescriptor descriptor() const noexcept; + [[nodiscard]] int surfaceDisplacementSize() const noexcept; + [[nodiscard]] int vacuumDisplacementSize() const noexcept; + [[nodiscard]] int scalarTrueDofCount() const noexcept; + [[nodiscard]] bool hasVacuumSupport(int scalarTrueDof) const; + [[nodiscard]] double radialWeight(int scalarTrueDof) const; + [[nodiscard]] int surfaceInterpolationEntryCount(int scalarTrueDof) const; + [[nodiscard]] int surfaceGlobalCoordinate( + int scalarTrueDof, + int interpolationEntry + ) const; + [[nodiscard]] double surfaceInterpolationWeight( + int scalarTrueDof, + int interpolationEntry + ) const; + + void buildVacuumDisplacement( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &vacuumDisplacement + ) const; + void applyJacobian( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + mfem::Vector &vacuumDisplacementDirection + ) const; + void applyJacobianTranspose( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &vacuumDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const; + void applyPullbackDerivative( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + const mfem::Vector &vacuumDisplacementDual, + mfem::Vector &surfaceDisplacementDualAction + ) const; + + private: + friend PreparedFixedInfinityRadialVacuumExtension compileVacuumDeformationExtension( + const FixedInfinityRadialVacuumExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ); + + PreparedFixedInfinityRadialVacuumExtension( + const FixedInfinityRadialVacuumExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ); + void requireSurfaceSize(const mfem::Vector &surfaceDisplacement) const; + void requireVacuumSize(const mfem::Vector &vacuumDisplacement) const; + void applyForward( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &vacuumDisplacement + ) const; + void applyTranspose( + const mfem::Vector &vacuumDisplacementDual, + mfem::Vector &surfaceDisplacementDual + ) const; + + VacuumDeformationExtensionDescriptor m_descriptor; + int m_surfaceDisplacementSize{0}; + int m_vacuumDisplacementSize{0}; + int m_spatialDimension{0}; + int m_globalSurfaceDisplacementSize{0}; + int m_globalSurfaceDisplacementOffset{0}; + MPI_Comm m_communicator{MPI_COMM_NULL}; + mfem::Array m_vacuumSupport; + mfem::Vector m_radialWeights; + std::vector m_surfaceInterpolationRowOffsets; + std::vector m_surfaceInterpolationGlobalCoordinates; + std::vector m_surfaceInterpolationWeights; + std::vector m_surfaceDisplacementCounts; + std::vector m_surfaceDisplacementOffsets; + mutable mfem::Vector m_globalSurfaceDisplacementWorkspace; + mutable mfem::Vector m_localGlobalSurfaceDualWorkspace; + mutable mfem::Vector m_globalSurfaceDualWorkspace; + }; + + [[nodiscard]] PreparedFixedInfinityRadialVacuumExtension compileVacuumDeformationExtension( + const FixedInfinityRadialVacuumExtension &extension, + const RadialDeformationExtensionCompilationContext &context + ); + + static_assert(InteriorDeformationExtension); + static_assert(PreparedInteriorDeformationExtension); + static_assert(InteriorDeformationExtensionCompilable< + PowerLawRadialInteriorExtension, + RadialDeformationExtensionCompilationContext>); + static_assert(VacuumDeformationExtension); + static_assert(PreparedVacuumDeformationExtension); + static_assert(VacuumDeformationExtensionCompilable< + FixedInfinityRadialVacuumExtension, + RadialDeformationExtensionCompilationContext>); +} // namespace mean_field::deformation diff --git a/libmeanfield/interface/deformation/surface_prescription.cppm b/libmeanfield/interface/deformation/surface_prescription.cppm new file mode 100644 index 0000000..07623f0 --- /dev/null +++ b/libmeanfield/interface/deformation/surface_prescription.cppm @@ -0,0 +1,57 @@ +module; + +#include +#include + +#include + +export module mean_field:deformation.surface_prescription; + +export import :deformation.descriptors; + +export namespace mean_field::deformation { + template + concept PreparedSurfaceDeformationPrescription = requires( + const std::remove_cvref_t &preparedPrescription, + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + const mfem::Vector &surfaceDisplacementDual, + mfem::Vector &surfaceDisplacement, + mfem::Vector ¶meterDual + ) { + { preparedPrescription.descriptor() } noexcept -> std::same_as; + { preparedPrescription.parameterCount() } noexcept -> std::same_as; + { preparedPrescription.surfaceDisplacementSize() } noexcept -> std::same_as; + { preparedPrescription.buildSurfaceDisplacement(parameters, surfaceDisplacement) } -> std::same_as; + { + preparedPrescription.applyJacobian(parameters, parameterDirection, surfaceDisplacement) + } -> std::same_as; + { + preparedPrescription.applyJacobianTranspose(parameters, surfaceDisplacementDual, parameterDual) + } -> std::same_as; + { + preparedPrescription.applyPullbackDerivative( + parameters, parameterDirection, surfaceDisplacementDual, parameterDual + ) + } -> std::same_as; + }; + + template + concept SurfaceDeformationPrescription = requires(const std::remove_cvref_t &prescription) { + typename std::remove_cvref_t::PreparedType; + requires PreparedSurfaceDeformationPrescription::PreparedType>; + { prescription.descriptor() } noexcept -> std::same_as; + { prescription.validate() } -> std::same_as; + }; + + template + concept SurfaceDeformationCompilable = + SurfaceDeformationPrescription && requires( + const std::remove_cvref_t &prescription, + const std::remove_cvref_t &context + ) { + { + compileSurfaceDeformationPrescription(prescription, context) + } -> std::same_as::PreparedType>; + }; +} // namespace mean_field::deformation diff --git a/libmeanfield/interface/deformation/vacuum_extension.cppm b/libmeanfield/interface/deformation/vacuum_extension.cppm new file mode 100644 index 0000000..920e118 --- /dev/null +++ b/libmeanfield/interface/deformation/vacuum_extension.cppm @@ -0,0 +1,61 @@ +module; + +#include +#include + +#include + +export module mean_field:deformation.vacuum_extension; + +export import :deformation.descriptors; + +export namespace mean_field::deformation { + template + concept PreparedVacuumDeformationExtension = requires( + const std::remove_cvref_t &preparedExtension, + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDisplacementDirection, + const mfem::Vector &vacuumDisplacementDual, + mfem::Vector &vacuumDisplacement, + mfem::Vector &surfaceDisplacementDual + ) { + { preparedExtension.descriptor() } noexcept -> std::same_as; + { preparedExtension.surfaceDisplacementSize() } noexcept -> std::same_as; + { preparedExtension.vacuumDisplacementSize() } noexcept -> std::same_as; + { preparedExtension.scalarTrueDofCount() } noexcept -> std::same_as; + { preparedExtension.hasVacuumSupport(0) } -> std::same_as; + { preparedExtension.buildVacuumDisplacement(surfaceDisplacement, vacuumDisplacement) } -> std::same_as; + { + preparedExtension.applyJacobian(surfaceDisplacement, surfaceDisplacementDirection, vacuumDisplacement) + } -> std::same_as; + { + preparedExtension.applyJacobianTranspose( + surfaceDisplacement, vacuumDisplacementDual, surfaceDisplacementDual + ) + } -> std::same_as; + { + preparedExtension.applyPullbackDerivative( + surfaceDisplacement, surfaceDisplacementDirection, vacuumDisplacementDual, surfaceDisplacementDual + ) + } -> std::same_as; + }; + + template + concept VacuumDeformationExtension = requires(const std::remove_cvref_t &extension) { + typename std::remove_cvref_t::PreparedType; + requires PreparedVacuumDeformationExtension::PreparedType>; + { extension.descriptor() } noexcept -> std::same_as; + { extension.validate() } -> std::same_as; + }; + + template + concept VacuumDeformationExtensionCompilable = + VacuumDeformationExtension && requires( + const std::remove_cvref_t &extension, + const std::remove_cvref_t &context + ) { + { + compileVacuumDeformationExtension(extension, context) + } -> std::same_as::PreparedType>; + }; +} // namespace mean_field::deformation diff --git a/libmeanfield/interface/fem.cppm b/libmeanfield/interface/fem.cppm index d6897b0..3da9ea2 100644 --- a/libmeanfield/interface/fem.cppm +++ b/libmeanfield/interface/fem.cppm @@ -28,6 +28,7 @@ export namespace mean_field::fem { stroid::StroidMesh smesh; std::unique_ptr mesh; + std::unique_ptr logicalReferenceMesh; // ===================================================================== // Compile-time field descriptors @@ -63,6 +64,14 @@ export namespace mean_field::fem { std::unique_ptr displacement; + /* + * Scalar companion of the vector displacement space. Only its + * StellarSurface true DOFs become surface-deformation coordinates. + * Sharing displacementFec guarantees identical scalar basis + * functions without duplicating the finite-element collection. + */ + std::unique_ptr surfaceDeformationFes; + // ===================================================================== // Density field // ===================================================================== @@ -115,12 +124,13 @@ export namespace mean_field::fem { // ===================================================================== [[nodiscard]] bool okay() const { - return mesh != nullptr && + return mesh != nullptr && logicalReferenceMesh != nullptr && gravityPotentialFec != nullptr && gravityPotentialFes != nullptr && gravityFluxFec != nullptr && gravityFluxFes != nullptr && displacementFec != nullptr && displacementFes != nullptr && displacement != nullptr && + surfaceDeformationFes != nullptr && densityFec != nullptr && densityFes != nullptr && diff --git a/libmeanfield/interface/field/field_mfem.cppm b/libmeanfield/interface/field/field_mfem.cppm index d30eec2..723e9ff 100644 --- a/libmeanfield/interface/field/field_mfem.cppm +++ b/libmeanfield/interface/field/field_mfem.cppm @@ -5,6 +5,7 @@ module; #include #include #include +#include #include #include @@ -889,6 +890,198 @@ export namespace mean_field::field { mfem::Array m_trueToReduced; }; + /* + * Canonical coordinates on a scalar finite-element boundary. + * + * Unlike FieldBoundaryDofMap, this is not a row selection inside an + * existing physical field. It defines an independent dense coordinate + * vector whose entries are the locally owned scalar true DOFs on a + * semantic boundary. + * + * Local coordinates follow increasing MFEM true-DOF order. Global + * coordinates use the distributed-vector convention: ranks are ordered by + * communicator rank and each rank contributes its locally sorted block. + */ + class ScalarBoundaryDofMap final { + public: + ScalarBoundaryDofMap() = default; + + ScalarBoundaryDofMap( + const int volumeTrueDofSize, + const mfem::Array &boundaryTrueDofs, + const long long globalOffset, + const long long globalSize + ) + : m_boundaryDofs( + volumeTrueDofSize, + boundaryTrueDofs + ), + m_globalOffset(globalOffset), + m_globalSize(globalSize) { + if (m_globalOffset < 0) { + throw std::invalid_argument("ScalarBoundaryDofMap requires a non-negative global offset."); + } + if (m_globalSize < 0) { + throw std::invalid_argument("ScalarBoundaryDofMap requires a non-negative global size."); + } + if (m_globalOffset + local_size() > m_globalSize) { + throw std::invalid_argument( + "ScalarBoundaryDofMap local coordinates lie outside the global boundary coordinate vector." + ); + } + } + + [[nodiscard]] int volume_true_dof_size() const noexcept { + return m_boundaryDofs.full_size(); + } + + [[nodiscard]] int local_size() const noexcept { + return m_boundaryDofs.reduced_size(); + } + + [[nodiscard]] long long global_size() const noexcept { + return m_globalSize; + } + + [[nodiscard]] long long global_offset() const noexcept { + return m_globalOffset; + } + + [[nodiscard]] bool empty() const noexcept { + return local_size() == 0; + } + + [[nodiscard]] const mfem::Array &boundary_true_dofs() const noexcept { + return m_boundaryDofs.reduced_to_true(); + } + + [[nodiscard]] int volume_true_dof(const int localBoundaryDof) const { + return m_boundaryDofs.true_dof(localBoundaryDof); + } + + [[nodiscard]] std::optional local_boundary_dof(const int volumeTrueDof) const { + return m_boundaryDofs.reduced_dof(volumeTrueDof); + } + + [[nodiscard]] bool contains_volume_true_dof(const int volumeTrueDof) const { + return m_boundaryDofs.contains_true_dof(volumeTrueDof); + } + + [[nodiscard]] long long global_boundary_dof(const int localBoundaryDof) const { + if (localBoundaryDof < 0 || localBoundaryDof >= local_size()) { + throw std::out_of_range("Local boundary DOF is outside ScalarBoundaryDofMap."); + } + return m_globalOffset + localBoundaryDof; + } + + void gather( + const mfem::Vector &volumeTrueValues, + mfem::Vector &boundaryValues + ) const { + m_boundaryDofs.gather(volumeTrueValues, boundaryValues); + } + + [[nodiscard]] mfem::Vector gather(const mfem::Vector &volumeTrueValues) const { + return m_boundaryDofs.gather(volumeTrueValues); + } + + void scatter( + const mfem::Vector &boundaryValues, + mfem::Vector &volumeTrueValues + ) const { + m_boundaryDofs.scatter(boundaryValues, volumeTrueValues); + } + + [[nodiscard]] mfem::Vector scatter(const mfem::Vector &boundaryValues) const { + return m_boundaryDofs.scatter(boundaryValues); + } + + private: + FieldDofMap m_boundaryDofs; + long long m_globalOffset{0}; + long long m_globalSize{0}; + }; + + template < + utils::domain::IsBoundary BoundaryT, + utils::domain::IsSchema SchemaT> + requires(SchemaT::template contains_boundary()) + [[nodiscard]] ScalarBoundaryDofMap + make_scalar_boundary_dof_map(const mfem::ParFiniteElementSpace &finiteElementSpace) { + MFEM_VERIFY( + !finiteElementSpace.Nonconforming(), + "Scalar boundary true-DOF resolution currently requires a conforming mfem::ParFiniteElementSpace." + ); + MFEM_VERIFY(finiteElementSpace.GetVDim() == 1, "ScalarBoundaryDofMap requires a scalar finite-element space."); + + const mfem::Mesh *mesh = finiteElementSpace.GetMesh(); + MFEM_VERIFY(mesh != nullptr, "Scalar 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 scalar boundary vector DOF." + ); + boundaryVDofMarker[vdof] = 1; + } + } + + finiteElementSpace.Synchronize(boundaryVDofMarker); + + mfem::Array boundaryTrueDofMarker(finiteElementSpace.GetTrueVSize()); + boundaryTrueDofMarker = 0; + + for (int vdof = 0; vdof < boundaryVDofMarker.Size(); ++vdof) { + if (boundaryVDofMarker[vdof] == 0) { + continue; + } + + const int trueDof = finiteElementSpace.GetLocalTDofNumber(vdof); + if (trueDof < 0) { + continue; + } + + MFEM_VERIFY(trueDof < boundaryTrueDofMarker.Size(), "MFEM returned an invalid scalar boundary true DOF."); + boundaryTrueDofMarker[trueDof] = 1; + } + + mfem::Array boundaryTrueDofs; + mfem::FiniteElementSpace::MarkerToList(boundaryTrueDofMarker, boundaryTrueDofs); + + const long long localSize = boundaryTrueDofs.Size(); + long long globalSize = 0; + long long globalOffset = 0; + + MPI_Allreduce(&localSize, &globalSize, 1, MPI_LONG_LONG, MPI_SUM, finiteElementSpace.GetComm()); + MPI_Exscan(&localSize, &globalOffset, 1, MPI_LONG_LONG, MPI_SUM, finiteElementSpace.GetComm()); + + if (finiteElementSpace.GetMyRank() == 0) { + globalOffset = 0; + } + + MFEM_VERIFY(globalSize > 0, "The requested semantic boundary has no scalar true DOFs."); + + return ScalarBoundaryDofMap(finiteElementSpace.GetTrueVSize(), boundaryTrueDofs, globalOffset, globalSize); + } + + template + requires(SchemaT::template contains_boundary()) + [[nodiscard]] ScalarBoundaryDofMap + make_stellar_surface_scalar_dof_map(const mfem::ParFiniteElementSpace &finiteElementSpace) { + return make_scalar_boundary_dof_map(finiteElementSpace); + } + /* * Boundary rows expressed in a field's reduced solver ordering. * diff --git a/libmeanfield/interface/field/field_registry.cppm b/libmeanfield/interface/field/field_registry.cppm index bfd5118..94ec741 100644 --- a/libmeanfield/interface/field/field_registry.cppm +++ b/libmeanfield/interface/field/field_registry.cppm @@ -3,6 +3,10 @@ module; #include #include +#ifndef MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT +#define MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT 0 +#endif + export module mean_field:field.registry; export import :field.base; @@ -10,13 +14,16 @@ export import :quadrature.policy; export import :utils.domain; export namespace mean_field::field { + inline constexpr int uniformPolynomialOrderIncrement = MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT; + static_assert(uniformPolynomialOrderIncrement >= 0); + // ========================================================================= // Density // ========================================================================= struct Density { static constexpr std::string_view name = "density"; - static constexpr int scalarOrder = 2; + static constexpr int scalarOrder = 2 + uniformPolynomialOrderIncrement; using Support = DomainSupport; @@ -79,8 +86,8 @@ export namespace mean_field::field { struct Gravity { static constexpr std::string_view name = "gravity"; - static constexpr int potentialOrder = 2; - static constexpr int fluxOrder = 2; + static constexpr int potentialOrder = 2 + uniformPolynomialOrderIncrement; + static constexpr int fluxOrder = 2 + uniformPolynomialOrderIncrement; using Support = DomainSupport; @@ -147,7 +154,7 @@ export namespace mean_field::field { struct Displacement { static constexpr std::string_view name = "displacement"; - static constexpr int vectorOrder = 3; + static constexpr int vectorOrder = 3 + uniformPolynomialOrderIncrement; using Support = DomainSupport; @@ -229,7 +236,7 @@ export namespace mean_field::field { struct Enthalpy { static constexpr std::string_view name = "specific_enthalpy"; - static constexpr int scalarOrder = 3; + static constexpr int scalarOrder = 3 + uniformPolynomialOrderIncrement; using Support = DomainSupport; diff --git a/libmeanfield/interface/mean_field.cppm b/libmeanfield/interface/mean_field.cppm index 100ae3f..5060efd 100644 --- a/libmeanfield/interface/mean_field.cppm +++ b/libmeanfield/interface/mean_field.cppm @@ -65,6 +65,13 @@ export import :surface.constant; export import :surface.dependencies; export import :surface.compiled; export import :surface.compiler; +export import :deformation.descriptors; +export import :deformation.surface_prescription; +export import :deformation.nodal_radial_surface; +export import :deformation.interior_extension; +export import :deformation.vacuum_extension; +export import :deformation.radial_extensions; +export import :deformation.domain_deformation; export import :model.stellar; export import :operators.prepared_mass_normalization; export import :operators.prepared_centering_constraint; diff --git a/libmeanfield/interface/models/stellar_model.cppm b/libmeanfield/interface/models/stellar_model.cppm index 7915e9b..fdac159 100644 --- a/libmeanfield/interface/models/stellar_model.cppm +++ b/libmeanfield/interface/models/stellar_model.cppm @@ -5,8 +5,11 @@ module; #include #include +#include + export module mean_field:model.stellar; +export import :deformation.domain_deformation; export import :eos.runtime; export import :model.structure.base; export import :surface.compiler; @@ -35,38 +38,85 @@ export namespace mean_field::models { std::remove_cvref_t &>().equationOfState())>; template - concept SurfacePrescription = + concept SurfaceCondition = surface::ConstantPressureSurfaceType && surface::PressureSurfaceCompilable>; - template - requires SurfacePrescription> + template < + StructurePrescription Structure, + deformation::SurfaceDeformationPrescription SurfaceDeformation = deformation::NodalRadialSurface, + deformation::InteriorDeformationExtension StellarInteriorDeformation = + deformation::PowerLawRadialInteriorExtension, + deformation::VacuumDeformationExtension VacuumDeformation = deformation::FixedInfinityRadialVacuumExtension> + requires SurfaceCondition> class StellarModel final { public: - using StructurePrescriptionType = Structure; - using SurfacePrescriptionType = surface::ConstantPressureSurface; - using EquationOfStateType = StructureEquationOfStateT; + using StructurePrescriptionType = Structure; + using SurfaceConditionType = surface::ConstantPressureSurface; + using SurfaceDeformationPrescriptionType = SurfaceDeformation; + using StellarInteriorDeformationExtensionType = StellarInteriorDeformation; + using VacuumDeformationExtensionType = VacuumDeformation; + using EquationOfStateType = StructureEquationOfStateT; using SurfaceConstraintType = surface::CompiledPressureSurfaceConstraintT; template requires std::same_as< - std::remove_cvref_t, - Structure> + std::remove_cvref_t, + Structure> explicit StellarModel( StructureArgument &&structurePrescription, - const surface::ConstantPressureSurface surfacePrescription + const surface::ConstantPressureSurface surfaceCondition + ) + : StellarModel( + std::forward(structurePrescription), + surfaceCondition, + deformation::NodalRadialSurface{defaultReferenceCenter()}, + deformation::PowerLawRadialInteriorExtension{}, + deformation::FixedInfinityRadialVacuumExtension{} + ) { + } + + template < + typename StructureArgument, + typename SurfaceDeformationArgument, + typename StellarInteriorDeformationArgument, + typename VacuumDeformationArgument> + requires std::same_as< + std::remove_cvref_t, + Structure> && + std::same_as< + std::remove_cvref_t, + SurfaceDeformation> && + std::same_as< + std::remove_cvref_t, + StellarInteriorDeformation> && + std::same_as< + std::remove_cvref_t, + VacuumDeformation> + explicit StellarModel( + StructureArgument &&structurePrescription, + const surface::ConstantPressureSurface surfaceCondition, + SurfaceDeformationArgument &&surfaceDeformation, + StellarInteriorDeformationArgument &&stellarInteriorDeformation, + VacuumDeformationArgument &&vacuumDeformation ) : m_structurePrescription( std::make_unique(std::forward(structurePrescription)) ), - m_surfacePrescription(std::make_unique(surfacePrescription)), - m_compiledSurfaceConstraint( - std::make_unique(validateAndCompileSurface( - *m_structurePrescription, - *m_surfacePrescription - )) - ) { + m_surfaceCondition(std::make_unique(surfaceCondition)), + m_surfaceDeformation( + std::make_unique(std::forward(surfaceDeformation)) + ), + m_stellarInteriorDeformation( + std::make_unique( + std::forward(stellarInteriorDeformation) + ) + ), + m_vacuumDeformation( + std::make_unique(std::forward(vacuumDeformation)) + ), + m_compiledSurfaceConstraint(std::make_unique(validateAndCompileConfiguration())) { } ~StellarModel() = default; @@ -80,8 +130,20 @@ export namespace mean_field::models { return *m_structurePrescription; } - [[nodiscard]] const surface::ConstantPressureSurface &surfacePrescription() const noexcept { - return *m_surfacePrescription; + [[nodiscard]] const surface::ConstantPressureSurface &surfaceCondition() const noexcept { + return *m_surfaceCondition; + } + + [[nodiscard]] const SurfaceDeformation &surfaceDeformationPrescription() const noexcept { + return *m_surfaceDeformation; + } + + [[nodiscard]] const StellarInteriorDeformation &stellarInteriorDeformationExtension() const noexcept { + return *m_stellarInteriorDeformation; + } + + [[nodiscard]] const VacuumDeformation &vacuumDeformationExtension() const noexcept { + return *m_vacuumDeformation; } [[nodiscard]] const EquationOfStateType &equationOfState() const noexcept { @@ -100,20 +162,36 @@ export namespace mean_field::models { return *m_compiledSurfaceConstraint; } + [[nodiscard]] auto compileDomainDeformation(fem::FEM &finiteElementModel) const { + return deformation::compileDomainDeformation( + surfaceDeformationPrescription(), stellarInteriorDeformationExtension(), vacuumDeformationExtension(), + finiteElementModel + ); + } + private: - [[nodiscard]] static SurfaceConstraintType validateAndCompileSurface( - const Structure &structurePrescription, - const surface::ConstantPressureSurface &surfacePrescription - ) { - structurePrescription.validate(); + [[nodiscard]] static mfem::Vector defaultReferenceCenter() { + mfem::Vector center(3); + center = 0.0; + return center; + } + + [[nodiscard]] SurfaceConstraintType validateAndCompileConfiguration() const { + m_structurePrescription->validate(); + m_surfaceDeformation->validate(); + m_stellarInteriorDeformation->validate(); + m_vacuumDeformation->validate(); return surface::compilePressureSurfaceConstraint( - surfacePrescription, structurePrescription.equationOfState() + *m_surfaceCondition, m_structurePrescription->equationOfState() ); } std::unique_ptr m_structurePrescription; - std::unique_ptr m_surfacePrescription; + std::unique_ptr m_surfaceCondition; + std::unique_ptr m_surfaceDeformation; + std::unique_ptr m_stellarInteriorDeformation; + std::unique_ptr m_vacuumDeformation; std::unique_ptr m_compiledSurfaceConstraint; }; @@ -121,12 +199,42 @@ export namespace mean_field::models { StellarModel( Structure &&, surface::ConstantPressureSurface - ) -> StellarModel>; + ) + -> StellarModel< + std::remove_cvref_t, + deformation::NodalRadialSurface, + deformation::PowerLawRadialInteriorExtension, + deformation::FixedInfinityRadialVacuumExtension>; + + template < + typename Structure, + typename SurfaceDeformation, + typename StellarInteriorDeformation, + typename VacuumDeformation> + StellarModel( + Structure &&, + surface::ConstantPressureSurface, + SurfaceDeformation &&, + StellarInteriorDeformation &&, + VacuumDeformation && + ) + -> StellarModel< + std::remove_cvref_t, + std::remove_cvref_t, + std::remove_cvref_t, + std::remove_cvref_t>; namespace detail { template struct IsStellarModel : std::false_type { }; - template struct IsStellarModel> : std::true_type { }; + template < + typename Structure, + typename SurfaceDeformation, + typename StellarInteriorDeformation, + typename VacuumDeformation> + struct IsStellarModel< + StellarModel> + : std::true_type { }; } // namespace detail template @@ -143,7 +251,10 @@ export namespace mean_field::models { m_makeInitialSeed(&makeInitialSeedFor::StructurePrescriptionType>), m_targetMass(model.targetMass()), m_surfaceCondition(model.compiledSurfaceConstraint().descriptor()), - m_surfaceDependencies(model.compiledSurfaceConstraint().runtimeDependencies()) { + m_surfaceDependencies(model.compiledSurfaceConstraint().runtimeDependencies()), + m_surfaceDeformation(model.surfaceDeformationPrescription().descriptor()), + m_stellarInteriorDeformation(model.stellarInteriorDeformationExtension().descriptor()), + m_vacuumDeformation(model.vacuumDeformationExtension().descriptor()) { } [[nodiscard]] eos::EquationOfStateView equationOfState() const noexcept { @@ -166,6 +277,18 @@ export namespace mean_field::models { return m_surfaceDependencies; } + [[nodiscard]] deformation::SurfaceDeformationDescriptor surfaceDeformation() const noexcept { + return m_surfaceDeformation; + } + + [[nodiscard]] deformation::InteriorDeformationExtensionDescriptor stellarInteriorDeformation() const noexcept { + return m_stellarInteriorDeformation; + } + + [[nodiscard]] deformation::VacuumDeformationExtensionDescriptor vacuumDeformation() const noexcept { + return m_vacuumDeformation; + } + private: using MakeInitialSeedFunction = structure::StructureSeed (*)( const void *, @@ -186,5 +309,8 @@ export namespace mean_field::models { double m_targetMass; surface::PressureSurfaceDescriptor m_surfaceCondition; surface::RuntimeSurfaceConstraintDependencies m_surfaceDependencies; + deformation::SurfaceDeformationDescriptor m_surfaceDeformation; + deformation::InteriorDeformationExtensionDescriptor m_stellarInteriorDeformation; + deformation::VacuumDeformationExtensionDescriptor m_vacuumDeformation; }; } // namespace mean_field::models diff --git a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm index 108a4d7..c46f80b 100644 --- a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm +++ b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm @@ -4,6 +4,7 @@ module; #include #include #include +#include #include @@ -18,7 +19,6 @@ 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; @@ -37,7 +37,7 @@ export namespace mean_field::operators { struct StellarEquilibriumDependencies final { StellarEquilibriumDependencyStamp discretization; StellarEquilibriumDependencyStamp density; - StellarEquilibriumDependencyStamp displacement; + StellarEquilibriumDependencyStamp surfaceDeformation; StellarEquilibriumDependencyStamp gravityGradient; StellarEquilibriumDependencyStamp gravityPotential; StellarEquilibriumDependencyStamp enthalpy; @@ -55,17 +55,18 @@ export namespace mean_field::operators { PreparedDisplacementResidualReport displacement; PreparedMassNormalizationReport massNormalization; PreparedSurfaceConstraintReport surfaceConstraint; - PreparedCenteringConstraintReport centeringConstraint; + deformation::DomainDeformationGeometryReport generatedGeometry; + StellarEquilibriumDependencyStamp generatedDisplacement; + bool generatedVolumeDisplacement{false}; bool assembledResidual{false}; [[nodiscard]] bool DidAnyChildWork() const noexcept { return gravity.DidAnyWork() || barotropicClosure.DidAnyWork() || hydrostatic.DidAnyWork() || - displacement.DidAnyWork() || massNormalization.DidAnyWork() || surfaceConstraint.DidAnyWork() || - centeringConstraint.DidAnyWork(); + displacement.DidAnyWork() || massNormalization.DidAnyWork() || surfaceConstraint.DidAnyWork(); } [[nodiscard]] bool DidAnyWork() const noexcept { - return DidAnyChildWork() || assembledResidual; + return DidAnyChildWork() || generatedVolumeDisplacement || assembledResidual; } }; @@ -73,11 +74,13 @@ export namespace mean_field::operators { std::uint64_t residualAssemblies{0}; std::uint64_t residualApplications{0}; std::uint64_t jacobianApplications{0}; + std::uint64_t generatedGeometryBuilds{0}; constexpr auto operator<=>(const PreparedStellarEquilibriumStatistics &) const = default; }; - using StellarEquilibriumLayout = utils::blocks::form_layout; + using StellarEquilibriumLayout = + utils::blocks::form_layout; class PreparedStellarEquilibriumOperator final : public mfem::Operator { public: @@ -99,7 +102,8 @@ export namespace mean_field::operators { domainMapper, stellarModel.equationOfState(), stellarModel.targetMass(), - PressureSurfaceConstraintView{stellarModel.compiledSurfaceConstraint()} + PressureSurfaceConstraintView{stellarModel.compiledSurfaceConstraint()}, + deformation::PreparedDomainDeformationRuntime{stellarModel.compileDomainDeformation(f)} ) { } @@ -138,19 +142,27 @@ export namespace mean_field::operators { [[nodiscard]] const PreparedDisplacementResidualOperator &GetDisplacementOperator() const noexcept; [[nodiscard]] const PreparedMassNormalizationOperator &GetMassNormalizationOperator() const noexcept; [[nodiscard]] const PreparedPressureSurfaceConstraint &GetSurfaceConstraintOperator() const noexcept; - [[nodiscard]] const PreparedCenteringConstraint &GetCenteringConstraintOperator() const noexcept; + [[nodiscard]] const deformation::PreparedDomainDeformationRuntime &GetDomainDeformation() const noexcept; + [[nodiscard]] const mfem::Vector &GetSurfaceDeformationParameters() const; + [[nodiscard]] const mfem::Vector &GetGeneratedVolumeDisplacement() const; + [[nodiscard]] const mfem::Vector &GetFullMechanicalResidual() const; + [[nodiscard]] const StellarEquilibriumDependencyStamp &GetGeneratedDisplacementDependency() const; private: struct ConstructionData; - static ConstructionData MakeConstructionData(fem::FEM &f); + static ConstructionData MakeConstructionData( + fem::FEM &f, + deformation::PreparedDomainDeformationRuntime domainDeformation + ); PreparedStellarEquilibriumOperator( fem::FEM &f, const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, double targetMass, - PressureSurfaceConstraintView surfaceConstraint + PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation ); PreparedStellarEquilibriumOperator( @@ -177,9 +189,14 @@ export namespace mean_field::operators { PreparedDisplacementResidualOperator m_displacementOperator; PreparedMassNormalizationOperator m_massNormalizationOperator; PreparedPressureSurfaceConstraint m_surfaceConstraintOperator; - PreparedCenteringConstraint m_centeringConstraintOperator; + deformation::PreparedDomainDeformationRuntime m_domainDeformation; StellarEquilibriumDependencies m_preparedDependencies; + StellarEquilibriumDependencyStamp m_generatedDisplacementDependency; + deformation::DomainDeformationGeometryReport m_generatedGeometryReport; + mfem::Vector m_surfaceDeformationParameters; + mfem::Vector m_generatedVolumeDisplacement; + mfem::Vector m_fullMechanicalResidual; mfem::Vector m_cachedResidual; double m_targetMass{0.0}; @@ -189,5 +206,9 @@ export namespace mean_field::operators { mfem::Vector m_gravityState; mutable mfem::Vector m_gravityDirection; + mutable mfem::Vector m_volumeDisplacementDirection; + mutable mfem::Vector m_fullMechanicalAction; + mutable mfem::Vector m_surfaceShapeAction; + mutable mfem::Vector m_pullbackDerivativeAction; }; } // namespace mean_field::operators diff --git a/libmeanfield/interface/surface/constant.cppm b/libmeanfield/interface/surface/constant.cppm index 8478bb8..574d3ab 100644 --- a/libmeanfield/interface/surface/constant.cppm +++ b/libmeanfield/interface/surface/constant.cppm @@ -15,7 +15,7 @@ export namespace mean_field::surface { }; /* - * The only physical surface prescription currently supported by + * The only physical surface condition 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. diff --git a/libmeanfield/interface/utils/blocks.cppm b/libmeanfield/interface/utils/blocks.cppm index 13775dd..4a02245 100644 --- a/libmeanfield/interface/utils/blocks.cppm +++ b/libmeanfield/interface/utils/blocks.cppm @@ -59,6 +59,19 @@ export namespace mean_field::utils::blocks { static inline constexpr geometry geometry_term{}; }; + struct surface_deformation final : field { + struct parameters final : term { + struct value final : value_block_base { }; + }; + + struct shape_equilibrium final : term { + struct residual final : residual_block_base { }; + }; + + static inline constexpr parameters parameters_term{}; + static inline constexpr shape_equilibrium shape_equilibrium_term{}; + }; + struct gravity final : field { struct gradient final : term { struct value final : value_block_base { }; @@ -99,6 +112,7 @@ export namespace mean_field::utils::blocks { inline constexpr density density_field{}; inline constexpr displacement displacement_field{}; + inline constexpr surface_deformation surface_deformation_field{}; inline constexpr gravity gravity_field{}; inline constexpr enthalpy enthalpy_field{}; inline constexpr barotropic_constant barotropic_constant_field{}; @@ -422,6 +436,59 @@ export namespace mean_field::utils::blocks { density::mass::value, displacement::geometry::value>>; + // Root stellar-equilibrium coordinates: + // [rho, q, g, Phi, h, C], where q parameterizes the stellar surface. + // Full-volume displacement remains an internal coordinate of the child + // operators above and is generated from q by the domain-deformation map. + using surface_deformed_stellar_equilibrium_form = block_form< + type_list< + density::mass::value, + surface_deformation::parameters::value, + gravity::gradient::value, + gravity::poisson::value, + enthalpy::specific::value, + barotropic_constant::mass_normalization::value>, + type_list< + gravity::gradient::residual, + gravity::poisson::residual, + density::mass::residual, + surface_deformation::shape_equilibrium::residual, + enthalpy::specific::residual, + barotropic_constant::mass_normalization::residual>>; + + using surface_deformed_stellar_equilibrium_jacobian_form = type_list< + block_row< + gravity::gradient::residual, + gravity::gradient::value, + gravity::poisson::value, + surface_deformation::parameters::value>, + block_row< + gravity::poisson::residual, + gravity::gradient::value, + density::mass::value, + surface_deformation::parameters::value>, + block_row< + density::mass::residual, + density::mass::value, + enthalpy::specific::value, + surface_deformation::parameters::value>, + block_row< + surface_deformation::shape_equilibrium::residual, + density::mass::value, + surface_deformation::parameters::value, + gravity::gradient::value, + enthalpy::specific::value>, + block_row< + enthalpy::specific::residual, + enthalpy::specific::value, + gravity::poisson::value, + surface_deformation::parameters::value, + barotropic_constant::mass_normalization::value>, + block_row< + barotropic_constant::mass_normalization::residual, + density::mass::value, + surface_deformation::parameters::value>>; + // Columns: [d, h] // Rows: [R_d] using pressure_force_form = block_form< @@ -438,4 +505,8 @@ export namespace mean_field::utils::blocks { static_assert(valid_jacobian_form< barotropic_equilibrium_form, barotropic_equilibrium_jacobian_form>); + + static_assert(valid_jacobian_form< + surface_deformed_stellar_equilibrium_form, + surface_deformed_stellar_equilibrium_jacobian_form>); } // namespace mean_field::utils::blocks diff --git a/tests/deformation/contracts.cpp b/tests/deformation/contracts.cpp new file mode 100644 index 0000000..510df0f --- /dev/null +++ b/tests/deformation/contracts.cpp @@ -0,0 +1,608 @@ +#include + +#include + +#include + +import mean_field; +import test_helpers; + +namespace { + struct DeformationCompilationContext final { }; + + constexpr mean_field::deformation::SurfaceDeformationDescriptor surfaceDescriptor{ + .name = "TestRadialSurface", + .spatialDimension = 3, + .motionKind = mean_field::deformation::SurfaceMotionKind::Radial, + .linearOnReferenceGeometry = true, + .requiresStarShapedReferenceSurface = true, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .translationTreatment = mean_field::deformation::GeometricGaugeTreatment::ExcludedByParameterization, + .orientationTreatment = mean_field::deformation::GeometricGaugeTreatment::ExcludedByParameterization + }; + + constexpr mean_field::deformation::InteriorDeformationExtensionDescriptor interiorDescriptor{ + .name = "TestRadialInteriorExtension", + .spatialDimension = 3, + .linearOnReferenceGeometry = true, + .requiresRadialFoliation = true, + .requiresAuxiliarySolve = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .centerBehavior = mean_field::deformation::InteriorCenterBehavior::FixedAtReferenceCenter + }; + + constexpr mean_field::deformation::VacuumDeformationExtensionDescriptor vacuumDescriptor{ + .name = "TestFixedInfinityExtension", + .spatialDimension = 3, + .linearOnReferenceGeometry = true, + .requiresRadialFoliation = true, + .requiresAuxiliarySolve = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .outerBoundaryBehavior = mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity + }; + + template < + bool HasBuild = true, + bool HasJacobian = true, + bool HasJacobianTranspose = true, + bool HasPullbackDerivative = true> + class PreparedSurfaceFixture final { + public: + [[nodiscard]] mean_field::deformation::SurfaceDeformationDescriptor descriptor() const noexcept { + return surfaceDescriptor; + } + + [[nodiscard]] int parameterCount() const noexcept { + return 4; + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return 12; + } + + void buildSurfaceDisplacement( + const mfem::Vector &, + mfem::Vector & + ) const + requires HasBuild + { + } + + void applyJacobian( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobian + { + } + + void applyJacobianTranspose( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobianTranspose + { + } + + void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasPullbackDerivative + { + } + }; + + template + class SurfacePrescriptionFixture final { + public: + using PreparedType = Prepared; + + [[nodiscard]] mean_field::deformation::SurfaceDeformationDescriptor descriptor() const noexcept + requires HasDescriptor + { + return surfaceDescriptor; + } + + void validate() const + requires HasValidation + { + } + + static constexpr bool hasCompilation = HasCompilation; + }; + + template < + typename Prepared, + bool HasDescriptor, + bool HasValidation, + bool HasCompilation> + requires HasCompilation + Prepared compileSurfaceDeformationPrescription( + const SurfacePrescriptionFixture< + Prepared, + HasDescriptor, + HasValidation, + HasCompilation> &, + const DeformationCompilationContext & + ) { + return {}; + } + + template < + bool HasBuild = true, + bool HasJacobian = true, + bool HasJacobianTranspose = true, + bool HasPullbackDerivative = true, + bool HasScalarDofCount = true, + bool HasSupportQuery = true> + class PreparedInteriorExtensionFixture final { + public: + [[nodiscard]] mean_field::deformation::InteriorDeformationExtensionDescriptor descriptor() const noexcept { + return interiorDescriptor; + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return 12; + } + + [[nodiscard]] int interiorDisplacementSize() const noexcept { + return 24; + } + + [[nodiscard]] int scalarTrueDofCount() const noexcept + requires HasScalarDofCount + { + return 8; + } + + [[nodiscard]] bool hasStellarSupport(const int) const + requires HasSupportQuery + { + return true; + } + + void buildInteriorDisplacement( + const mfem::Vector &, + mfem::Vector & + ) const + requires HasBuild + { + } + + void applyJacobian( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobian + { + } + + void applyJacobianTranspose( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobianTranspose + { + } + + void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasPullbackDerivative + { + } + }; + + template + class InteriorExtensionFixture final { + public: + using PreparedType = Prepared; + + [[nodiscard]] mean_field::deformation::InteriorDeformationExtensionDescriptor descriptor() const noexcept + requires HasDescriptor + { + return interiorDescriptor; + } + + void validate() const + requires HasValidation + { + } + + static constexpr bool hasCompilation = HasCompilation; + }; + + template < + typename Prepared, + bool HasDescriptor, + bool HasValidation, + bool HasCompilation> + requires HasCompilation + Prepared compileInteriorDeformationExtension( + const InteriorExtensionFixture< + Prepared, + HasDescriptor, + HasValidation, + HasCompilation> &, + const DeformationCompilationContext & + ) { + return {}; + } + + template < + bool HasBuild = true, + bool HasJacobian = true, + bool HasJacobianTranspose = true, + bool HasPullbackDerivative = true, + bool HasScalarDofCount = true, + bool HasSupportQuery = true> + class PreparedVacuumExtensionFixture final { + public: + [[nodiscard]] mean_field::deformation::VacuumDeformationExtensionDescriptor descriptor() const noexcept { + return vacuumDescriptor; + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return 12; + } + + [[nodiscard]] int vacuumDisplacementSize() const noexcept { + return 30; + } + + [[nodiscard]] int scalarTrueDofCount() const noexcept + requires HasScalarDofCount + { + return 10; + } + + [[nodiscard]] bool hasVacuumSupport(const int) const + requires HasSupportQuery + { + return true; + } + + void buildVacuumDisplacement( + const mfem::Vector &, + mfem::Vector & + ) const + requires HasBuild + { + } + + void applyJacobian( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobian + { + } + + void applyJacobianTranspose( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobianTranspose + { + } + + void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasPullbackDerivative + { + } + }; + + template + class VacuumExtensionFixture final { + public: + using PreparedType = Prepared; + + [[nodiscard]] mean_field::deformation::VacuumDeformationExtensionDescriptor descriptor() const noexcept + requires HasDescriptor + { + return vacuumDescriptor; + } + + void validate() const + requires HasValidation + { + } + + static constexpr bool hasCompilation = HasCompilation; + }; + + template < + typename Prepared, + bool HasDescriptor, + bool HasValidation, + bool HasCompilation> + requires HasCompilation + Prepared compileVacuumDeformationExtension( + const VacuumExtensionFixture< + Prepared, + HasDescriptor, + HasValidation, + HasCompilation> &, + const DeformationCompilationContext & + ) { + return {}; + } + + using CompletePreparedSurface = PreparedSurfaceFixture<>; + using CompleteSurfacePrescription = SurfacePrescriptionFixture; + + using CompletePreparedInteriorExtension = PreparedInteriorExtensionFixture<>; + using CompleteInteriorExtension = InteriorExtensionFixture; + + using CompletePreparedVacuumExtension = PreparedVacuumExtensionFixture<>; + using CompleteVacuumExtension = VacuumExtensionFixture; + + constexpr mean_field::deformation::DomainDeformationDescriptor domainDescriptor{ + .surfaceDeformation = surfaceDescriptor, + .stellarInteriorExtension = interiorDescriptor, + .vacuumExtension = vacuumDescriptor, + .linearOnReferenceGeometry = true, + .requiresAuxiliarySolve = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true + }; + + template < + bool HasBuild = true, + bool HasJacobian = true, + bool HasJacobianTranspose = true, + bool HasPullbackDerivative = true> + class PreparedDomainDeformationFixture final { + public: + [[nodiscard]] mean_field::deformation::DomainDeformationDescriptor descriptor() const noexcept { + return domainDescriptor; + } + + [[nodiscard]] int parameterCount() const noexcept { + return 4; + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return 12; + } + + [[nodiscard]] int volumeDisplacementSize() const noexcept { + return 24; + } + + void buildVolumeDisplacement( + const mfem::Vector &, + mfem::Vector & + ) const + requires HasBuild + { + } + + void applyJacobian( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobian + { + } + + void applyJacobianTranspose( + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasJacobianTranspose + { + } + + void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector &, + const mfem::Vector &, + mfem::Vector & + ) const + requires HasPullbackDerivative + { + } + }; +} // namespace + +TEST_CASE( + "Surface Deformation Contracts Require Complete Forward And Pullback Operations", + tags::surface_deformation_type_contract +) { + STATIC_CHECK(mean_field::deformation::PreparedSurfaceDeformationPrescription); + STATIC_CHECK(mean_field::deformation::SurfaceDeformationPrescription); + STATIC_CHECK( + mean_field::deformation::SurfaceDeformationCompilable< + CompleteSurfacePrescription, DeformationCompilationContext> + ); + + STATIC_CHECK_FALSE(mean_field::deformation::PreparedSurfaceDeformationPrescription>); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedSurfaceDeformationPrescription> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedSurfaceDeformationPrescription> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedSurfaceDeformationPrescription> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::SurfaceDeformationPrescription< + SurfacePrescriptionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::SurfaceDeformationPrescription< + SurfacePrescriptionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::SurfaceDeformationCompilable< + SurfacePrescriptionFixture, DeformationCompilationContext> + ); +} + +TEST_CASE( + "Interior Extension Contracts Require Complete Forward And Pullback Operations", + tags::interior_deformation_extension_type_contract +) { + STATIC_CHECK(mean_field::deformation::PreparedInteriorDeformationExtension); + STATIC_CHECK(mean_field::deformation::InteriorDeformationExtension); + STATIC_CHECK( + mean_field::deformation::InteriorDeformationExtensionCompilable< + CompleteInteriorExtension, DeformationCompilationContext> + ); + + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedInteriorDeformationExtension> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedInteriorDeformationExtension> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedInteriorDeformationExtension< + PreparedInteriorExtensionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedInteriorDeformationExtension< + PreparedInteriorExtensionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedInteriorDeformationExtension< + PreparedInteriorExtensionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedInteriorDeformationExtension< + PreparedInteriorExtensionFixture> + ); + STATIC_CHECK_FALSE(mean_field::deformation::PreparedInteriorDeformationExtension); + STATIC_CHECK_FALSE( + mean_field::deformation::InteriorDeformationExtension< + InteriorExtensionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::InteriorDeformationExtensionCompilable< + InteriorExtensionFixture, + DeformationCompilationContext> + ); +} + +TEST_CASE( + "Vacuum Extension Contracts Require Complete Forward And Pullback Operations", + tags::vacuum_deformation_extension_type_contract +) { + STATIC_CHECK(mean_field::deformation::PreparedVacuumDeformationExtension); + STATIC_CHECK(mean_field::deformation::VacuumDeformationExtension); + STATIC_CHECK( + mean_field::deformation::VacuumDeformationExtensionCompilable< + CompleteVacuumExtension, DeformationCompilationContext> + ); + + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedVacuumDeformationExtension> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedVacuumDeformationExtension> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedVacuumDeformationExtension> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedVacuumDeformationExtension< + PreparedVacuumExtensionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedVacuumDeformationExtension< + PreparedVacuumExtensionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedVacuumDeformationExtension< + PreparedVacuumExtensionFixture> + ); + STATIC_CHECK_FALSE(mean_field::deformation::PreparedVacuumDeformationExtension); + STATIC_CHECK_FALSE( + mean_field::deformation::VacuumDeformationExtension< + VacuumExtensionFixture> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::VacuumDeformationExtensionCompilable< + VacuumExtensionFixture, DeformationCompilationContext> + ); +} + +TEST_CASE( + "Prepared Domain Deformation Contracts Require Complete Lift And Pullback Operations", + tags::domain_deformation_type_contract +) { + STATIC_CHECK(mean_field::deformation::PreparedDomainDeformationOperator>); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedDomainDeformationOperator> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedDomainDeformationOperator> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedDomainDeformationOperator> + ); + STATIC_CHECK_FALSE( + mean_field::deformation::PreparedDomainDeformationOperator< + PreparedDomainDeformationFixture> + ); +} + +TEST_CASE( + "Deformation Descriptors Report Geometry And Exact Linearization Capabilities", + tags::deformation_type_contract +) { + STATIC_CHECK(surfaceDescriptor.isValid()); + STATIC_CHECK(surfaceDescriptor.supportsExactNewtonLinearization()); + STATIC_CHECK(interiorDescriptor.isValid()); + STATIC_CHECK(interiorDescriptor.supportsExactNewtonLinearization()); + STATIC_CHECK(vacuumDescriptor.isValid()); + STATIC_CHECK(vacuumDescriptor.supportsExactNewtonLinearization()); + STATIC_CHECK(domainDescriptor.isValid()); + STATIC_CHECK(domainDescriptor.supportsExactNewtonLinearization()); + + constexpr auto invalidInteriorDescriptor = [] { + auto descriptor = interiorDescriptor; + descriptor.centerBehavior = mean_field::deformation::InteriorCenterBehavior::Unspecified; + return descriptor; + }(); + + constexpr auto invalidVacuumDescriptor = [] { + auto descriptor = vacuumDescriptor; + descriptor.outerBoundaryBehavior = mean_field::deformation::VacuumOuterBoundaryBehavior::Unspecified; + return descriptor; + }(); + + STATIC_CHECK_FALSE(invalidInteriorDescriptor.isValid()); + STATIC_CHECK_FALSE(invalidVacuumDescriptor.isValid()); + + CHECK(surfaceDescriptor.motionKind == mean_field::deformation::SurfaceMotionKind::Radial); + CHECK(interiorDescriptor.centerBehavior == mean_field::deformation::InteriorCenterBehavior::FixedAtReferenceCenter); + CHECK( + vacuumDescriptor.outerBoundaryBehavior == + mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity + ); +} diff --git a/tests/deformation/domain_deformation.cpp b/tests/deformation/domain_deformation.cpp new file mode 100644 index 0000000..22d31ce --- /dev/null +++ b/tests/deformation/domain_deformation.cpp @@ -0,0 +1,771 @@ +#include +#include +#include +#include +#include + +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace domain_deformation_test_utils { + namespace deformation = mean_field::deformation; + namespace domain = mean_field::utils::domain; + namespace field = mean_field::field; + + using Schema = domain::CoreEnvelopeVacuumDomainSchema; + + [[nodiscard]] mfem::Vector referenceCenter(const int spatialDimension) { + mfem::Vector center(spatialDimension); + center = 0.0; + return center; + } + + [[nodiscard]] auto makePreparedDomainDeformation(mean_field::fem::FEM &fem) { + const field::ScalarBoundaryDofMap surfaceDofMap = + field::make_stellar_surface_scalar_dof_map(*fem.surfaceDeformationFes); + const deformation::SurfaceDeformationCompilationContext surfaceContext{ + *fem.surfaceDeformationFes, surfaceDofMap + }; + deformation::PreparedNodalRadialSurface surface = deformation::compileSurfaceDeformationPrescription( + deformation::NodalRadialSurface{referenceCenter(fem.mesh->SpaceDimension())}, surfaceContext + ); + + const deformation::RadialDeformationExtensionCompilationContext extensionContext = + deformation::makeRadialDeformationExtensionCompilationContext( + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh + ); + deformation::PreparedPowerLawRadialInteriorExtension interior = + deformation::compileInteriorDeformationExtension( + deformation::PowerLawRadialInteriorExtension{}, extensionContext + ); + deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = deformation::compileVacuumDeformationExtension( + deformation::FixedInfinityRadialVacuumExtension{}, extensionContext + ); + + return deformation::composePreparedDomainDeformation( + std::move(surface), std::move(interior), std::move(vacuum), *fem.surfaceDeformationFes, + *fem.displacementFes, *fem.logicalReferenceMesh + ); + } + + [[nodiscard]] int volumeVectorDof( + const int scalarTrueDof, + const int component, + const int scalarTrueDofCount + ) { + return scalarTrueDof + component * scalarTrueDofCount; + } + + [[nodiscard]] double relativeError( + const mfem::Vector &actual, + const mfem::Vector &expected + ) { + REQUIRE(actual.Size() == expected.Size()); + mfem::Vector difference(actual); + difference -= expected; + return difference.Norml2() / std::max(expected.Norml2(), std::numeric_limits::epsilon()); + } + + [[nodiscard]] double globalInnerProduct( + const mfem::Vector &first, + const mfem::Vector &second, + MPI_Comm communicator + ) { + REQUIRE(first.Size() == second.Size()); + const double localValue = first * second; + double globalValue = 0.0; + MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, communicator); + return globalValue; + } + + class AnalyticNonlinearSurface final { + public: + [[nodiscard]] deformation::SurfaceDeformationDescriptor descriptor() const noexcept { + return { + .name = "AnalyticNonlinearSurface", + .spatialDimension = 3, + .motionKind = deformation::SurfaceMotionKind::Radial, + .linearOnReferenceGeometry = false, + .requiresStarShapedReferenceSurface = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .translationTreatment = deformation::GeometricGaugeTreatment::Retained, + .orientationTreatment = deformation::GeometricGaugeTreatment::Retained + }; + } + + [[nodiscard]] int parameterCount() const noexcept { + return 2; + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return 2; + } + + void buildSurfaceDisplacement( + const mfem::Vector ¶meters, + mfem::Vector &surfaceDisplacement + ) const { + surfaceDisplacement(0) = parameters(0) * parameters(0) + parameters(1); + surfaceDisplacement(1) = parameters(0) * parameters(1); + } + + void applyJacobian( + const mfem::Vector ¶meters, + const mfem::Vector ¶meterDirection, + mfem::Vector &surfaceDisplacementDirection + ) const { + surfaceDisplacementDirection(0) = 2.0 * parameters(0) * parameterDirection(0) + parameterDirection(1); + surfaceDisplacementDirection(1) = + parameters(1) * parameterDirection(0) + parameters(0) * parameterDirection(1); + } + + void applyJacobianTranspose( + const mfem::Vector ¶meters, + const mfem::Vector &surfaceDisplacementDual, + mfem::Vector ¶meterDual + ) const { + parameterDual(0) = + 2.0 * parameters(0) * surfaceDisplacementDual(0) + parameters(1) * surfaceDisplacementDual(1); + parameterDual(1) = surfaceDisplacementDual(0) + parameters(0) * surfaceDisplacementDual(1); + } + + void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector ¶meterDirection, + const mfem::Vector &surfaceDisplacementDual, + mfem::Vector ¶meterDualAction + ) const { + parameterDualAction(0) = 2.0 * parameterDirection(0) * surfaceDisplacementDual(0) + + parameterDirection(1) * surfaceDisplacementDual(1); + parameterDualAction(1) = parameterDirection(0) * surfaceDisplacementDual(1); + } + }; + + class AnalyticNonlinearExtensionKernel { + public: + AnalyticNonlinearExtensionKernel( + const int scalarTrueDofCount, + const double coefficientScale + ) + : m_scalarTrueDofCount(scalarTrueDofCount), + m_coefficientScale(coefficientScale) { + } + + [[nodiscard]] int scalarTrueDofCount() const noexcept { + return m_scalarTrueDofCount; + } + + [[nodiscard]] int volumeDisplacementSize() const noexcept { + return 3 * m_scalarTrueDofCount; + } + + [[nodiscard]] int sharedScalarDof() const noexcept { + return m_scalarTrueDofCount / 2; + } + + void build( + const mfem::Vector &surfaceDisplacement, + mfem::Vector &volumeDisplacement, + const bool stellar + ) const { + volumeDisplacement = 0.0; + for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) { + if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) { + continue; + } + double linearFirst = 0.0; + double linearSecond = 0.0; + double bilinear = 0.0; + coefficients(vectorDof, linearFirst, linearSecond, bilinear); + volumeDisplacement(vectorDof) = linearFirst * surfaceDisplacement(0) + + linearSecond * surfaceDisplacement(1) + + bilinear * surfaceDisplacement(0) * surfaceDisplacement(1); + } + } + + void applyJacobian( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &surfaceDirection, + mfem::Vector &volumeDirection, + const bool stellar + ) const { + volumeDirection = 0.0; + for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) { + if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) { + continue; + } + double linearFirst = 0.0; + double linearSecond = 0.0; + double bilinear = 0.0; + coefficients(vectorDof, linearFirst, linearSecond, bilinear); + volumeDirection(vectorDof) = (linearFirst + bilinear * surfaceDisplacement(1)) * surfaceDirection(0) + + (linearSecond + bilinear * surfaceDisplacement(0)) * surfaceDirection(1); + } + } + + void applyJacobianTranspose( + const mfem::Vector &surfaceDisplacement, + const mfem::Vector &volumeDual, + mfem::Vector &surfaceDual, + const bool stellar + ) const { + surfaceDual = 0.0; + for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) { + if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) { + continue; + } + double linearFirst = 0.0; + double linearSecond = 0.0; + double bilinear = 0.0; + coefficients(vectorDof, linearFirst, linearSecond, bilinear); + surfaceDual(0) += (linearFirst + bilinear * surfaceDisplacement(1)) * volumeDual(vectorDof); + surfaceDual(1) += (linearSecond + bilinear * surfaceDisplacement(0)) * volumeDual(vectorDof); + } + } + + void applyPullbackDerivative( + const mfem::Vector &surfaceDirection, + const mfem::Vector &volumeDual, + mfem::Vector &surfaceDualAction, + const bool stellar + ) const { + surfaceDualAction = 0.0; + for (int vectorDof = 0; vectorDof < volumeDisplacementSize(); ++vectorDof) { + if (!hasSupport(vectorDof % m_scalarTrueDofCount, stellar)) { + continue; + } + double linearFirst = 0.0; + double linearSecond = 0.0; + double bilinear = 0.0; + coefficients(vectorDof, linearFirst, linearSecond, bilinear); + surfaceDualAction(0) += bilinear * surfaceDirection(1) * volumeDual(vectorDof); + surfaceDualAction(1) += bilinear * surfaceDirection(0) * volumeDual(vectorDof); + } + } + + [[nodiscard]] bool hasSupport( + const int scalarTrueDof, + const bool stellar + ) const noexcept { + return stellar ? scalarTrueDof <= sharedScalarDof() : scalarTrueDof >= sharedScalarDof(); + } + + private: + void coefficients( + const int vectorDof, + double &linearFirst, + double &linearSecond, + double &bilinear + ) const noexcept { + linearFirst = m_coefficientScale * (0.01 + 0.001 * static_cast(vectorDof % 7)); + linearSecond = m_coefficientScale * (-0.02 + 0.002 * static_cast(vectorDof % 5)); + bilinear = m_coefficientScale * 0.0005 * static_cast(1 + vectorDof % 3); + } + + int m_scalarTrueDofCount; + double m_coefficientScale; + }; + + class AnalyticNonlinearInteriorExtension final { + public: + explicit AnalyticNonlinearInteriorExtension( + const int scalarTrueDofCount, + const bool supportEnabled = true + ) + : m_kernel( + scalarTrueDofCount, + 1.0 + ), + m_supportEnabled(supportEnabled) { + } + + [[nodiscard]] deformation::InteriorDeformationExtensionDescriptor descriptor() const noexcept { + return { + .name = "AnalyticNonlinearInteriorExtension", + .spatialDimension = 3, + .linearOnReferenceGeometry = false, + .requiresRadialFoliation = false, + .requiresAuxiliarySolve = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .centerBehavior = deformation::InteriorCenterBehavior::FixedAtReferenceCenter + }; + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return 2; + } + [[nodiscard]] int interiorDisplacementSize() const noexcept { + return m_kernel.volumeDisplacementSize(); + } + [[nodiscard]] int scalarTrueDofCount() const noexcept { + return m_kernel.scalarTrueDofCount(); + } + [[nodiscard]] bool hasStellarSupport(const int scalarTrueDof) const { + return m_supportEnabled && m_kernel.hasSupport(scalarTrueDof, true); + } + void buildInteriorDisplacement( + const mfem::Vector &surface, + mfem::Vector &volume + ) const { + m_kernel.build(surface, volume, true); + } + void applyJacobian( + const mfem::Vector &surface, + const mfem::Vector &direction, + mfem::Vector &volume + ) const { + m_kernel.applyJacobian(surface, direction, volume, true); + } + void applyJacobianTranspose( + const mfem::Vector &surface, + const mfem::Vector &volume, + mfem::Vector &dual + ) const { + m_kernel.applyJacobianTranspose(surface, volume, dual, true); + } + void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector &direction, + const mfem::Vector &volume, + mfem::Vector &dual + ) const { + m_kernel.applyPullbackDerivative(direction, volume, dual, true); + } + + private: + AnalyticNonlinearExtensionKernel m_kernel; + bool m_supportEnabled; + }; + + class AnalyticNonlinearVacuumExtension final { + public: + explicit AnalyticNonlinearVacuumExtension(const int scalarTrueDofCount) + : m_kernel( + scalarTrueDofCount, + -0.7 + ) { + } + + [[nodiscard]] deformation::VacuumDeformationExtensionDescriptor descriptor() const noexcept { + return { + .name = "AnalyticNonlinearVacuumExtension", + .spatialDimension = 3, + .linearOnReferenceGeometry = false, + .requiresRadialFoliation = false, + .requiresAuxiliarySolve = false, + .hasExactDerivativeTranspose = true, + .hasExactPullbackDerivative = true, + .outerBoundaryBehavior = deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity + }; + } + + [[nodiscard]] int surfaceDisplacementSize() const noexcept { + return 2; + } + [[nodiscard]] int vacuumDisplacementSize() const noexcept { + return m_kernel.volumeDisplacementSize(); + } + [[nodiscard]] int scalarTrueDofCount() const noexcept { + return m_kernel.scalarTrueDofCount(); + } + [[nodiscard]] bool hasVacuumSupport(const int scalarTrueDof) const { + return m_kernel.hasSupport(scalarTrueDof, false); + } + void buildVacuumDisplacement( + const mfem::Vector &surface, + mfem::Vector &volume + ) const { + m_kernel.build(surface, volume, false); + } + void applyJacobian( + const mfem::Vector &surface, + const mfem::Vector &direction, + mfem::Vector &volume + ) const { + m_kernel.applyJacobian(surface, direction, volume, false); + } + void applyJacobianTranspose( + const mfem::Vector &surface, + const mfem::Vector &volume, + mfem::Vector &dual + ) const { + m_kernel.applyJacobianTranspose(surface, volume, dual, false); + } + void applyPullbackDerivative( + const mfem::Vector &, + const mfem::Vector &direction, + const mfem::Vector &volume, + mfem::Vector &dual + ) const { + m_kernel.applyPullbackDerivative(direction, volume, dual, false); + } + + private: + AnalyticNonlinearExtensionKernel m_kernel; + }; +} // namespace domain_deformation_test_utils + +TEST_CASE( + "Prepared Domain Deformation Composes Surface Interior And Vacuum Maps With Explicit Ownership", + tags::domain_deformation_composition +) { + namespace deformation = mean_field::deformation; + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem); + STATIC_CHECK(deformation::PreparedDomainDeformationOperator); + + const deformation::DomainDeformationDescriptor descriptor = prepared.descriptor(); + REQUIRE(descriptor.isValid()); + CHECK(descriptor.linearOnReferenceGeometry); + CHECK_FALSE(descriptor.requiresAuxiliarySolve); + CHECK(descriptor.supportsExactNewtonLinearization()); + CHECK(prepared.parameterCount() == prepared.surfaceDeformationPrescription().parameterCount()); + CHECK(prepared.surfaceDisplacementSize() == prepared.surfaceDeformationPrescription().surfaceDisplacementSize()); + CHECK(prepared.volumeDisplacementSize() == fem.displacementFes->GetTrueVSize()); + CHECK(prepared.matchesCurrentDiscretization()); + + const deformation::DomainDeformationDiscretizationDependencies &dependencies = + prepared.discretizationDependencies(); + CHECK(dependencies.physicalMeshIdentity == fem.mesh.get()); + CHECK(dependencies.logicalReferenceMeshIdentity == fem.logicalReferenceMesh.get()); + CHECK(dependencies.surfaceScalarSpaceIdentity == fem.surfaceDeformationFes.get()); + CHECK(dependencies.volumeDisplacementSpaceIdentity == fem.displacementFes.get()); + CHECK(dependencies.isCurrent()); + + const deformation::DomainDeformationCompositionReport &composition = prepared.compositionReport(); + CHECK(composition.scalarTrueDofCount == prepared.scalarTrueDofCount()); + CHECK(composition.assignedScalarDofCount() == prepared.scalarTrueDofCount()); + CHECK(composition.stellarInteriorOwnedScalarDofCount > 0); + CHECK(composition.vacuumOwnedScalarDofCount > 0); + CHECK(composition.sharedSurfaceScalarDofCount > 0); + + int countedStellarOwners = 0; + int countedVacuumOwners = 0; + int countedSharedDofs = 0; + for (int scalarTrueDof = 0; scalarTrueDof < prepared.scalarTrueDofCount(); ++scalarTrueDof) { + if (prepared.volumeOwner(scalarTrueDof) == deformation::VolumeDeformationOwner::StellarInterior) { + ++countedStellarOwners; + } else { + ++countedVacuumOwners; + } + countedSharedDofs += prepared.isSharedSurfaceDof(scalarTrueDof) ? 1 : 0; + } + CHECK(countedStellarOwners == composition.stellarInteriorOwnedScalarDofCount); + CHECK(countedVacuumOwners == composition.vacuumOwnedScalarDofCount); + CHECK(countedSharedDofs == composition.sharedSurfaceScalarDofCount); + + mfem::Vector zeroParameters(prepared.parameterCount()); + zeroParameters = 0.0; + mfem::Vector composedVolume(prepared.volumeDisplacementSize()); + prepared.buildVolumeDisplacement(zeroParameters, composedVolume); + CHECK(composedVolume.Norml2() == 0.0); + + mfem::Vector parameters(prepared.parameterCount()); + for (int parameter = 0; parameter < parameters.Size(); ++parameter) { + const double index = static_cast(parameter + 1); + parameters(parameter) = 0.012 * std::sin(0.19 * index) - 0.004 * std::cos(0.31 * index); + } + prepared.buildVolumeDisplacement(parameters, composedVolume); + + mfem::Vector surfaceDisplacement(prepared.surfaceDisplacementSize()); + mfem::Vector interiorVolume(prepared.volumeDisplacementSize()); + mfem::Vector vacuumVolume(prepared.volumeDisplacementSize()); + prepared.surfaceDeformationPrescription().buildSurfaceDisplacement(parameters, surfaceDisplacement); + prepared.stellarInteriorExtension().buildInteriorDisplacement(surfaceDisplacement, interiorVolume); + prepared.vacuumExtension().buildVacuumDisplacement(surfaceDisplacement, vacuumVolume); + + constexpr double tolerance = 2.0e-12; + for (int scalarTrueDof = 0; scalarTrueDof < prepared.scalarTrueDofCount(); ++scalarTrueDof) { + for (int component = 0; component < prepared.spatialDimension(); ++component) { + const int volumeDof = + domain_deformation_test_utils::volumeVectorDof(scalarTrueDof, component, prepared.scalarTrueDofCount()); + const double expected = + prepared.volumeOwner(scalarTrueDof) == deformation::VolumeDeformationOwner::StellarInterior + ? interiorVolume(volumeDof) + : vacuumVolume(volumeDof); + CHECK(std::abs(composedVolume(volumeDof) - expected) <= tolerance); + if (prepared.isSharedSurfaceDof(scalarTrueDof)) { + CHECK(std::abs(interiorVolume(volumeDof) - vacuumVolume(volumeDof)) <= tolerance); + } + } + } + + const deformation::PreparedDomainDeformationActionStatistics &statistics = prepared.actionStatistics(); + CHECK(statistics.volumeBuildApplications == 2); + CHECK(statistics.jacobianApplications == 0); + CHECK(statistics.jacobianTransposeApplications == 0); + CHECK(statistics.pullbackDerivativeApplications == 0); + CHECK(statistics.geometryInspections == 0); + + mfem::Vector wrongParameters(prepared.parameterCount() + 1); + mfem::Vector wrongVolume(prepared.volumeDisplacementSize() + 1); + CHECK_THROWS_AS(prepared.buildVolumeDisplacement(wrongParameters, composedVolume), std::invalid_argument); + CHECK_THROWS_AS(prepared.buildVolumeDisplacement(parameters, wrongVolume), std::invalid_argument); + CHECK_THROWS_AS(prepared.volumeOwner(-1), std::out_of_range); +} + +TEST_CASE( + "Prepared Domain Deformation Jacobian Matches Centered Difference And Its Pullback Preserves Virtual Work", + tags::domain_deformation_linearization +) { + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem); + mfem::Vector parameters(prepared.parameterCount()); + mfem::Vector direction(prepared.parameterCount()); + for (int parameter = 0; parameter < parameters.Size(); ++parameter) { + const double index = static_cast(parameter + 1); + parameters(parameter) = 0.008 * std::sin(0.13 * index); + direction(parameter) = std::cos(0.17 * index) - 0.25 * std::sin(0.29 * index); + } + + constexpr double step = 1.0e-6; + mfem::Vector plusParameters(parameters); + mfem::Vector minusParameters(parameters); + plusParameters.Add(step, direction); + minusParameters.Add(-step, direction); + + mfem::Vector plusVolume(prepared.volumeDisplacementSize()); + mfem::Vector minusVolume(prepared.volumeDisplacementSize()); + mfem::Vector jacobianAction(prepared.volumeDisplacementSize()); + prepared.buildVolumeDisplacement(plusParameters, plusVolume); + prepared.buildVolumeDisplacement(minusParameters, minusVolume); + prepared.applyJacobian(parameters, direction, jacobianAction); + + mfem::Vector centeredDifference(plusVolume); + centeredDifference -= minusVolume; + centeredDifference /= 2.0 * step; + CHECK(domain_deformation_test_utils::relativeError(jacobianAction, centeredDifference) < 3.0e-10); + + mfem::Vector volumeDual(prepared.volumeDisplacementSize()); + for (int dof = 0; dof < volumeDual.Size(); ++dof) { + const double index = static_cast(dof + 1); + volumeDual(dof) = std::sin(0.07 * index) + 0.4 * std::cos(0.11 * index); + } + mfem::Vector parameterDual(prepared.parameterCount()); + prepared.applyJacobianTranspose(parameters, volumeDual, parameterDual); + + const double volumeWork = + domain_deformation_test_utils::globalInnerProduct(jacobianAction, volumeDual, fem.mesh->GetComm()); + const double parameterWork = + domain_deformation_test_utils::globalInnerProduct(direction, parameterDual, fem.mesh->GetComm()); + const double workScale = std::max({1.0, std::abs(volumeWork), std::abs(parameterWork)}); + CHECK(std::abs(volumeWork - parameterWork) <= 8.0e-13 * workScale); + + mfem::Vector pullbackAction(prepared.parameterCount()); + prepared.applyPullbackDerivative(parameters, direction, volumeDual, pullbackAction); + CHECK(pullbackAction.Norml2() == 0.0); + + mfem::Vector plusTranspose(prepared.parameterCount()); + mfem::Vector minusTranspose(prepared.parameterCount()); + prepared.applyJacobianTranspose(plusParameters, volumeDual, plusTranspose); + prepared.applyJacobianTranspose(minusParameters, volumeDual, minusTranspose); + mfem::Vector transposeCenteredDifference(plusTranspose); + transposeCenteredDifference -= minusTranspose; + transposeCenteredDifference /= 2.0 * step; + CHECK(transposeCenteredDifference.Norml2() <= 1.0e-12 * std::max(1.0, parameterDual.Norml2())); +} + +TEST_CASE( + "Nonlinear Domain Deformation Pullback Matches The Directional Derivative Of Its Complete Transpose", + tags::domain_deformation_linearization +) { + namespace deformation = mean_field::deformation; + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + const int scalarTrueDofCount = fem.displacementFes->GetTrueVSize() / fem.mesh->SpaceDimension(); + CHECK_THROWS_AS( + (deformation::composePreparedDomainDeformation( + domain_deformation_test_utils::AnalyticNonlinearSurface{}, + domain_deformation_test_utils::AnalyticNonlinearInteriorExtension{scalarTrueDofCount, false}, + domain_deformation_test_utils::AnalyticNonlinearVacuumExtension{scalarTrueDofCount}, + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh + )), + std::invalid_argument + ); + CHECK_THROWS_AS( + (deformation::composePreparedDomainDeformation( + domain_deformation_test_utils::AnalyticNonlinearSurface{}, + domain_deformation_test_utils::AnalyticNonlinearInteriorExtension{scalarTrueDofCount}, + domain_deformation_test_utils::AnalyticNonlinearVacuumExtension{scalarTrueDofCount - 1}, + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh + )), + std::invalid_argument + ); + auto prepared = deformation::composePreparedDomainDeformation( + domain_deformation_test_utils::AnalyticNonlinearSurface{}, + domain_deformation_test_utils::AnalyticNonlinearInteriorExtension{scalarTrueDofCount}, + domain_deformation_test_utils::AnalyticNonlinearVacuumExtension{scalarTrueDofCount}, *fem.surfaceDeformationFes, + *fem.displacementFes, *fem.logicalReferenceMesh + ); + REQUIRE_FALSE(prepared.descriptor().linearOnReferenceGeometry); + + mfem::Vector parameters(2); + parameters(0) = 0.37; + parameters(1) = -0.21; + mfem::Vector parameterDirection(2); + parameterDirection(0) = -0.42; + parameterDirection(1) = 0.63; + + mfem::Vector volumeDual(prepared.volumeDisplacementSize()); + for (int vectorDof = 0; vectorDof < volumeDual.Size(); ++vectorDof) { + const double index = static_cast(vectorDof + 1); + volumeDual(vectorDof) = std::sin(0.013 * index) - 0.3 * std::cos(0.021 * index); + } + + constexpr double step = 2.0e-6; + mfem::Vector plusParameters(parameters); + mfem::Vector minusParameters(parameters); + plusParameters.Add(step, parameterDirection); + minusParameters.Add(-step, parameterDirection); + + mfem::Vector plusVolume(prepared.volumeDisplacementSize()); + mfem::Vector minusVolume(prepared.volumeDisplacementSize()); + mfem::Vector jacobianAction(prepared.volumeDisplacementSize()); + prepared.buildVolumeDisplacement(plusParameters, plusVolume); + prepared.buildVolumeDisplacement(minusParameters, minusVolume); + prepared.applyJacobian(parameters, parameterDirection, jacobianAction); + mfem::Vector centeredJacobian(plusVolume); + centeredJacobian -= minusVolume; + centeredJacobian /= 2.0 * step; + CHECK(domain_deformation_test_utils::relativeError(jacobianAction, centeredJacobian) < 2.0e-10); + + mfem::Vector parameterDual(2); + prepared.applyJacobianTranspose(parameters, volumeDual, parameterDual); + const double volumeWork = + domain_deformation_test_utils::globalInnerProduct(jacobianAction, volumeDual, fem.mesh->GetComm()); + const double parameterWork = + domain_deformation_test_utils::globalInnerProduct(parameterDirection, parameterDual, fem.mesh->GetComm()); + CHECK( + std::abs(volumeWork - parameterWork) <= 2.0e-12 * std::max({1.0, std::abs(volumeWork), std::abs(parameterWork)}) + ); + + mfem::Vector pullbackAction(2); + prepared.applyPullbackDerivative(parameters, parameterDirection, volumeDual, pullbackAction); + mfem::Vector plusTranspose(2); + mfem::Vector minusTranspose(2); + prepared.applyJacobianTranspose(plusParameters, volumeDual, plusTranspose); + prepared.applyJacobianTranspose(minusParameters, volumeDual, minusTranspose); + mfem::Vector centeredPullback(plusTranspose); + centeredPullback -= minusTranspose; + centeredPullback /= 2.0 * step; + CHECK(domain_deformation_test_utils::relativeError(pullbackAction, centeredPullback) < 3.0e-9); + + const int sharedScalarDof = scalarTrueDofCount / 2; + REQUIRE(prepared.isSharedSurfaceDof(sharedScalarDof)); + CHECK(prepared.volumeOwner(sharedScalarDof) == deformation::VolumeDeformationOwner::StellarInterior); + + mfem::Vector surfaceDisplacement(2); + mfem::Vector interiorVolume(prepared.volumeDisplacementSize()); + mfem::Vector vacuumVolume(prepared.volumeDisplacementSize()); + mfem::Vector composedVolume(prepared.volumeDisplacementSize()); + prepared.surfaceDeformationPrescription().buildSurfaceDisplacement(parameters, surfaceDisplacement); + prepared.stellarInteriorExtension().buildInteriorDisplacement(surfaceDisplacement, interiorVolume); + prepared.vacuumExtension().buildVacuumDisplacement(surfaceDisplacement, vacuumVolume); + prepared.buildVolumeDisplacement(parameters, composedVolume); + for (int component = 0; component < prepared.spatialDimension(); ++component) { + const int vectorDof = sharedScalarDof + component * scalarTrueDofCount; + CHECK(composedVolume(vectorDof) == interiorVolume(vectorDof)); + CHECK(interiorVolume(vectorDof) != vacuumVolume(vectorDof)); + } +} + +TEST_CASE( + "Prepared Domain Deformation Accepts Orientation Preserving Shapes And Rejects Folded Volume Maps", + tags::domain_deformation_geometry +) { + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem); + mfem::Vector volumeDisplacement(prepared.volumeDisplacementSize()); + + mfem::Vector zeroParameters(prepared.parameterCount()); + zeroParameters = 0.0; + const mean_field::deformation::DomainDeformationGeometryReport referenceReport = + prepared.buildValidatedVolumeDisplacement(zeroParameters, volumeDisplacement, 0.99); + CHECK(std::abs(referenceReport.minimumJacobianDeterminant - 1.0) <= 64.0 * std::numeric_limits::epsilon()); + CHECK_THROWS_AS( + prepared.buildValidatedVolumeDisplacement(zeroParameters, volumeDisplacement, 1.01), std::domain_error + ); + CHECK_THROWS_AS( + prepared.buildValidatedVolumeDisplacement(zeroParameters, volumeDisplacement, -0.01), std::invalid_argument + ); + CHECK_THROWS_AS( + prepared.buildValidatedVolumeDisplacement( + zeroParameters, volumeDisplacement, std::numeric_limits::quiet_NaN() + ), + std::invalid_argument + ); + + mfem::Vector boundedParameters(prepared.parameterCount()); + boundedParameters = 0.02; + const mean_field::deformation::DomainDeformationGeometryReport boundedReport = + prepared.buildValidatedVolumeDisplacement(boundedParameters, volumeDisplacement); + CHECK(boundedReport.isOrientationPreserving()); + CHECK(boundedReport.minimumJacobianDeterminant > 0.0); + + mfem::Vector foldingParameters(prepared.parameterCount()); + foldingParameters = -2.0; + prepared.buildVolumeDisplacement(foldingParameters, volumeDisplacement); + const mean_field::deformation::DomainDeformationGeometryReport foldingReport = + prepared.inspectMappedGeometry(volumeDisplacement); + CHECK_FALSE(foldingReport.isOrientationPreserving()); + CHECK_THROWS_AS( + prepared.buildValidatedVolumeDisplacement(foldingParameters, volumeDisplacement), std::domain_error + ); + + mfem::Vector nonfiniteParameters(zeroParameters); + nonfiniteParameters(0) = std::numeric_limits::quiet_NaN(); + CHECK_THROWS_AS( + prepared.buildValidatedVolumeDisplacement(nonfiniteParameters, volumeDisplacement), std::domain_error + ); + + CHECK(prepared.actionStatistics().geometryInspections == 6); +} + +TEST_CASE( + "Prepared Domain Deformation Rejects Actions After Its Discretization Becomes Stale", + tags::domain_deformation_composition +) { + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + auto prepared = domain_deformation_test_utils::makePreparedDomainDeformation(fem); + mfem::Vector parameters(prepared.parameterCount()); + mfem::Vector parameterDirection(prepared.parameterCount()); + mfem::Vector parameterDual(prepared.parameterCount()); + mfem::Vector volume(prepared.volumeDisplacementSize()); + parameters = 0.0; + parameterDirection = 0.0; + volume = 0.0; + + REQUIRE(prepared.matchesCurrentDiscretization()); + fem.mesh->UniformRefinement(); + REQUIRE_FALSE(prepared.matchesCurrentDiscretization()); + CHECK_THROWS_AS(prepared.buildVolumeDisplacement(parameters, volume), std::logic_error); + CHECK_THROWS_AS(prepared.applyJacobian(parameters, parameterDirection, volume), std::logic_error); + CHECK_THROWS_AS(prepared.applyJacobianTranspose(parameters, volume, parameterDual), std::logic_error); + CHECK_THROWS_AS( + prepared.applyPullbackDerivative(parameters, parameterDirection, volume, parameterDual), std::logic_error + ); + CHECK_THROWS_AS(prepared.inspectMappedGeometry(volume), std::logic_error); +} diff --git a/tests/deformation/nodal_radial_surface.cpp b/tests/deformation/nodal_radial_surface.cpp new file mode 100644 index 0000000..a58e25e --- /dev/null +++ b/tests/deformation/nodal_radial_surface.cpp @@ -0,0 +1,536 @@ +#include +#include +#include +#include +#include + +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace nodal_radial_surface_test_utils { + namespace deformation = mean_field::deformation; + namespace domain = mean_field::utils::domain; + namespace field = mean_field::field; + + using Schema = domain::CoreEnvelopeVacuumDomainSchema; + + using PlanarBoundarySchema = domain::DomainSchema< + domain::MaterialList<>, + domain::BoundaryList>, + domain::RelationList<>>; + + [[nodiscard]] mfem::Vector referenceCenter(const int spatialDimension) { + mfem::Vector center(spatialDimension); + center = 0.0; + return center; + } + + [[nodiscard]] deformation::PreparedNodalRadialSurface makePreparedSurface(const mean_field::fem::FEM &fem) { + const field::ScalarBoundaryDofMap surfaceDofMap = + field::make_stellar_surface_scalar_dof_map(*fem.surfaceDeformationFes); + const deformation::SurfaceDeformationCompilationContext context{*fem.surfaceDeformationFes, surfaceDofMap}; + + return deformation::compileSurfaceDeformationPrescription( + deformation::NodalRadialSurface{referenceCenter(fem.mesh->SpaceDimension())}, context + ); + } + + [[nodiscard]] mfem::Vector projectReferenceSurfacePositions( + mfem::ParFiniteElementSpace &scalarFiniteElementSpace, + const field::ScalarBoundaryDofMap &surfaceDofMap + ) { + const mfem::Mesh *mesh = scalarFiniteElementSpace.GetMesh(); + REQUIRE(mesh != nullptr); + + const int spatialDimension = mesh->SpaceDimension(); + mfem::Vector positions(spatialDimension * surfaceDofMap.local_size()); + mfem::ParGridFunction coordinateField(&scalarFiniteElementSpace); + + for (int component = 0; component < spatialDimension; ++component) { + mfem::FunctionCoefficient coefficient([component](const mfem::Vector &position) { + return position(component); + }); + coordinateField.ProjectCoefficient(coefficient); + + mfem::Vector coordinateTrueDofs; + coordinateField.GetTrueDofs(coordinateTrueDofs); + const mfem::Vector surfaceCoordinates = surfaceDofMap.gather(coordinateTrueDofs); + + for (int surfaceDof = 0; surfaceDof < surfaceDofMap.local_size(); ++surfaceDof) { + positions(spatialDimension * surfaceDof + component) = surfaceCoordinates(surfaceDof); + } + } + + return positions; + } + + [[nodiscard]] double relativeError( + const mfem::Vector &actual, + const mfem::Vector &expected + ) { + REQUIRE(actual.Size() == expected.Size()); + + mfem::Vector difference(actual); + difference -= expected; + + return difference.Norml2() / std::max(expected.Norml2(), std::numeric_limits::epsilon()); + } + + [[nodiscard]] double globalMeanReferenceRadius(const deformation::PreparedNodalRadialSurface &surface) { + double localRadiusSum = 0.0; + for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) { + localRadiusSum += surface.referenceRadius(parameterDof); + } + + const long long localCount = surface.parameterCount(); + double globalRadiusSum = 0.0; + long long globalCount = 0; + MPI_Allreduce(&localRadiusSum, &globalRadiusSum, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD); + MPI_Allreduce(&localCount, &globalCount, 1, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD); + REQUIRE(globalCount > 0); + return globalRadiusSum / static_cast(globalCount); + } + + struct BinaryRochePotential final { + double primaryMass; + double companionMass; + double separation; + + [[nodiscard]] double operator()( + const double x, + const double y, + const double z + ) const { + const double primaryDistance = std::sqrt(x * x + y * y + z * z); + const double companionOffset = x - separation; + const double companionDistance = std::sqrt(companionOffset * companionOffset + y * y + z * z); + const double totalMass = primaryMass + companionMass; + const double centerOfMassX = separation * companionMass / totalMass; + const double angularSpeed2 = totalMass / (separation * separation * separation); + const double rotationRadius2 = (x - centerOfMassX) * (x - centerOfMassX) + y * y; + + return -primaryMass / primaryDistance - companionMass / companionDistance - + 0.5 * angularSpeed2 * rotationRadius2; + } + + [[nodiscard]] double firstRadialIntersection( + const double directionX, + const double directionY, + const double directionZ, + const double targetPotential, + const double referenceScale + ) const { + double lowerRadius = 1.0e-8 * referenceScale; + double lowerValue = + (*this)(lowerRadius * directionX, lowerRadius * directionY, lowerRadius * directionZ) - targetPotential; + REQUIRE(lowerValue < 0.0); + + double upperRadius = lowerRadius; + double upperValue = lowerValue; + constexpr int bracketSamples = 512; + for (int sample = 1; sample <= bracketSamples && upperValue <= 0.0; ++sample) { + upperRadius = 0.45 * separation * static_cast(sample) / bracketSamples; + upperValue = (*this)(upperRadius * directionX, upperRadius * directionY, upperRadius * directionZ) - + targetPotential; + } + REQUIRE(upperValue > 0.0); + + for (int iteration = 0; iteration < 80; ++iteration) { + const double middleRadius = 0.5 * (lowerRadius + upperRadius); + const double middleValue = + (*this)(middleRadius * directionX, middleRadius * directionY, middleRadius * directionZ) - + targetPotential; + if (middleValue > 0.0) { + upperRadius = middleRadius; + } else { + lowerRadius = middleRadius; + } + } + return 0.5 * (lowerRadius + upperRadius); + } + }; +} // namespace nodal_radial_surface_test_utils + +TEST_CASE( + "Nodal Radial Surface Satisfies The Surface Deformation Contract And Validates Its Reference Center", + tags::nodal_radial_surface_validation +) { + namespace deformation = mean_field::deformation; + namespace field = mean_field::field; + + STATIC_CHECK(deformation::SurfaceDeformationPrescription); + STATIC_CHECK(deformation::PreparedSurfaceDeformationPrescription); + STATIC_CHECK( + deformation::SurfaceDeformationCompilable< + deformation::NodalRadialSurface, deformation::SurfaceDeformationCompilationContext> + ); + + mfem::Vector center(3); + center = 0.0; + const deformation::NodalRadialSurface prescription{center}; + const deformation::SurfaceDeformationDescriptor descriptor = prescription.descriptor(); + + CHECK(descriptor.name == "NodalRadialSurface"); + CHECK(descriptor.spatialDimension == 3); + CHECK(descriptor.motionKind == deformation::SurfaceMotionKind::Radial); + CHECK(descriptor.linearOnReferenceGeometry); + CHECK(descriptor.requiresStarShapedReferenceSurface); + CHECK(descriptor.supportsExactNewtonLinearization()); + CHECK(descriptor.translationTreatment == deformation::GeometricGaugeTreatment::Retained); + CHECK(descriptor.orientationTreatment == deformation::GeometricGaugeTreatment::Retained); + + mfem::Vector emptyCenter; + CHECK_THROWS_AS(deformation::NodalRadialSurface{emptyCenter}, std::invalid_argument); + + mfem::Vector nonfiniteCenter(3); + nonfiniteCenter = 0.0; + nonfiniteCenter(1) = std::numeric_limits::quiet_NaN(); + CHECK_THROWS_AS(deformation::NodalRadialSurface{nonfiniteCenter}, std::invalid_argument); + + mfem::Mesh serialMesh = mfem::Mesh::MakeCartesian2D(4, 3, mfem::Element::QUADRILATERAL, true, 2.0, 1.5); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + mfem::H1_FECollection finiteElementCollection(2, mesh.Dimension()); + mfem::ParFiniteElementSpace finiteElementSpace(&mesh, &finiteElementCollection); + + const field::ScalarBoundaryDofMap surfaceDofMap = + field::make_stellar_surface_scalar_dof_map( + finiteElementSpace + ); + const deformation::SurfaceDeformationCompilationContext context{finiteElementSpace, surfaceDofMap}; + + CHECK_THROWS_AS(deformation::compileSurfaceDeformationPrescription(prescription, context), std::invalid_argument); +} + +TEST_CASE( + "Nodal Radial Surface Resolves Oblate Rotating And Binary Roche Envelopes Between Surface Nodes", + tags::nodal_radial_surface_analytic +) { + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + const mean_field::deformation::PreparedNodalRadialSurface surface = + nodal_radial_surface_test_utils::makePreparedSurface(fem); + REQUIRE(surface.spatialDimension() == 3); + + const double referenceScale = nodal_radial_surface_test_utils::globalMeanReferenceRadius(surface); + const double equatorialRadius = 1.08 * referenceScale; + const double polarRadius = 0.94 * referenceScale; + + mfem::Vector rotatingParameters(surface.parameterCount()); + for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) { + const double directionX = surface.radialDirection(parameterDof, 0); + const double directionY = surface.radialDirection(parameterDof, 1); + const double directionZ = surface.radialDirection(parameterDof, 2); + const double targetRadius = + 1.0 / std::sqrt( + (directionX * directionX + directionY * directionY) / (equatorialRadius * equatorialRadius) + + directionZ * directionZ / (polarRadius * polarRadius) + ); + rotatingParameters(parameterDof) = targetRadius - surface.referenceRadius(parameterDof); + } + + mfem::Vector rotatingDisplacement(surface.surfaceDisplacementSize()); + surface.buildSurfaceDisplacement(rotatingParameters, rotatingDisplacement); + + constexpr double geometricTolerance = 3.0e-12; + for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) { + double movedPosition[3]{}; + for (int component = 0; component < 3; ++component) { + const double direction = surface.radialDirection(parameterDof, component); + movedPosition[component] = surface.referenceRadius(parameterDof) * direction + + rotatingDisplacement(surface.surfaceDisplacementDof(parameterDof, component)); + } + const double ellipsoidLevel = (movedPosition[0] * movedPosition[0] + movedPosition[1] * movedPosition[1]) / + (equatorialRadius * equatorialRadius) + + movedPosition[2] * movedPosition[2] / (polarRadius * polarRadius); + CHECK(std::abs(ellipsoidLevel - 1.0) <= geometricTolerance); + } + + const nodal_radial_surface_test_utils::BinaryRochePotential rochePotential{ + .primaryMass = 1.0, .companionMass = 0.7, .separation = 4.0 * referenceScale + }; + const double targetPotential = rochePotential(0.0, 0.0, referenceScale); + + mfem::Vector rocheParameters(surface.parameterCount()); + for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) { + const double targetRadius = rochePotential.firstRadialIntersection( + surface.radialDirection(parameterDof, 0), surface.radialDirection(parameterDof, 1), + surface.radialDirection(parameterDof, 2), targetPotential, referenceScale + ); + rocheParameters(parameterDof) = targetRadius - surface.referenceRadius(parameterDof); + } + + mfem::Vector rocheDisplacement(surface.surfaceDisplacementSize()); + surface.buildSurfaceDisplacement(rocheParameters, rocheDisplacement); + + const double potentialTolerance = 2.0e-11 * std::max(1.0, std::abs(targetPotential)); + for (int parameterDof = 0; parameterDof < surface.parameterCount(); ++parameterDof) { + double movedPosition[3]{}; + for (int component = 0; component < 3; ++component) { + const double direction = surface.radialDirection(parameterDof, component); + movedPosition[component] = surface.referenceRadius(parameterDof) * direction + + rocheDisplacement(surface.surfaceDisplacementDof(parameterDof, component)); + } + CHECK( + std::abs(rochePotential(movedPosition[0], movedPosition[1], movedPosition[2]) - targetPotential) <= + potentialTolerance + ); + } + + auto domainDeformation = mean_field::deformation::compileDomainDeformation( + mean_field::deformation::NodalRadialSurface{ + nodal_radial_surface_test_utils::referenceCenter(fem.mesh->SpaceDimension()) + }, + mean_field::deformation::PowerLawRadialInteriorExtension{}, + mean_field::deformation::FixedInfinityRadialVacuumExtension{}, fem + ); + mfem::Vector volumeDisplacement(domainDeformation.volumeDisplacementSize()); + mfem::ParGridFunction displacementField(fem.displacementFes.get()); + mean_field::mapping::GridFunctionMappingEvaluator mappingEvaluator( + *fem.domainMapperStateless, displacementField, *fem.compactificationCoordinate + ); + + const auto sampleRepresentationError = [&](const mfem::Vector ¶meters, const auto &pointError) { + const auto geometry = domainDeformation.buildValidatedVolumeDisplacement(parameters, volumeDisplacement); + REQUIRE(geometry.isOrientationPreserving()); + displacementField.SetFromTrueDofs(volumeDisplacement); + mappingEvaluator.InvalidateCache(); + + double localMaximumError = 0.0; + double localErrorSquared = 0.0; + double localSurfaceArea = 0.0; + long long localSamples = 0; + const int stellarSurfaceAttribute = nodal_radial_surface_test_utils::Schema::template boundary_attribute< + nodal_radial_surface_test_utils::domain::StellarSurface>(); + + for (int boundaryElement = 0; boundaryElement < fem.mesh->GetNBE(); ++boundaryElement) { + if (fem.mesh->GetBdrAttribute(boundaryElement) != stellarSurfaceAttribute) { + continue; + } + + /* + * The stellar surface is a material interface, not an exterior + * boundary of the complete compactified mesh. Resolve the tagged + * boundary element to its underlying mesh face so this sampling + * path works for both internal interfaces and true exterior + * boundaries. + */ + const int face = fem.mesh->GetBdrElementFaceIndex(boundaryElement); + REQUIRE(face >= 0); + mfem::FaceElementTransformations *transformation = fem.mesh->GetFaceElementTransformations(face); + REQUIRE(transformation != nullptr); + REQUIRE(transformation->Elem1 != nullptr); + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), 2 * mean_field::field::Displacement::vectorOrder); + + for (int point = 0; point < rule.GetNPoints(); ++point) { + mean_field::mapping::FaceMappingContext context; + const mfem::IntegrationPoint &integrationPoint = rule.IntPoint(point); + REQUIRE( + mappingEvaluator.EvaluateFace( + *transformation, mean_field::mapping::FaceElementSide::element_1, integrationPoint, context + ) == mean_field::mapping::MappingStatus::valid + ); + + const double error = pointError(context.mapping.physical_position); + REQUIRE(std::isfinite(error)); + REQUIRE(context.physical_surface_weight > 0.0); + localMaximumError = std::max(localMaximumError, error); + localErrorSquared += error * error * context.physical_surface_weight; + localSurfaceArea += context.physical_surface_weight; + ++localSamples; + } + } + + double globalMaximumError = 0.0; + double globalErrorSquared = 0.0; + double globalSurfaceArea = 0.0; + long long globalSamples = 0; + MPI_Allreduce(&localMaximumError, &globalMaximumError, 1, MPI_DOUBLE, MPI_MAX, fem.mesh->GetComm()); + MPI_Allreduce(&localErrorSquared, &globalErrorSquared, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm()); + MPI_Allreduce(&localSurfaceArea, &globalSurfaceArea, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm()); + MPI_Allreduce(&localSamples, &globalSamples, 1, MPI_LONG_LONG, MPI_SUM, fem.mesh->GetComm()); + REQUIRE(globalSamples > 0); + REQUIRE(globalSurfaceArea > 0.0); + return std::array{globalMaximumError, std::sqrt(globalErrorSquared / globalSurfaceArea)}; + }; + + const auto rotatingErrors = + sampleRepresentationError(rotatingParameters, [equatorialRadius, polarRadius](const mfem::Vector &position) { + return std::abs( + (position(0) * position(0) + position(1) * position(1)) / (equatorialRadius * equatorialRadius) + + position(2) * position(2) / (polarRadius * polarRadius) - 1.0 + ); + }); + const auto rocheErrors = + sampleRepresentationError(rocheParameters, [&rochePotential, targetPotential](const mfem::Vector &position) { + return std::abs(rochePotential(position(0), position(1), position(2)) - targetPotential) / + std::max(1.0, std::abs(targetPotential)); + }); + + INFO("Between-node oblate surface maximum level-set error = " << rotatingErrors[0]); + INFO("Between-node oblate surface RMS level-set error = " << rotatingErrors[1]); + INFO("Between-node Roche surface maximum normalized potential error = " << rocheErrors[0]); + INFO("Between-node Roche surface RMS normalized potential error = " << rocheErrors[1]); + CHECK(rotatingErrors[0] < 5.0e-3); + CHECK(rotatingErrors[1] < 1.0e-3); + CHECK(rocheErrors[0] < 2.0e-2); + CHECK(rocheErrors[1] < 5.0e-3); + + const double companionFacingRadius = + rochePotential.firstRadialIntersection(1.0, 0.0, 0.0, targetPotential, referenceScale); + const double companionOpposingRadius = + rochePotential.firstRadialIntersection(-1.0, 0.0, 0.0, targetPotential, referenceScale); + CHECK(std::abs(companionFacingRadius - companionOpposingRadius) > 1.0e-3 * referenceScale); +} + +TEST_CASE( + "Nodal Radial Surface Produces The Requested Physical Radial Displacement At Every Surface Coordinate", + tags::nodal_radial_surface_analytic +) { + namespace deformation = mean_field::deformation; + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + const deformation::PreparedNodalRadialSurface prepared = nodal_radial_surface_test_utils::makePreparedSurface(fem); + + REQUIRE(prepared.parameterCount() > 0); + CHECK(prepared.spatialDimension() == fem.mesh->SpaceDimension()); + CHECK(prepared.surfaceDisplacementSize() == prepared.spatialDimension() * prepared.parameterCount()); + CHECK( + prepared.globalSurfaceDisplacementSize() == + static_cast(prepared.spatialDimension()) * prepared.globalParameterCount() + ); + CHECK( + prepared.globalSurfaceDisplacementOffset() == + static_cast(prepared.spatialDimension()) * prepared.globalParameterOffset() + ); + + mfem::Vector parameters(prepared.parameterCount()); + for (int parameterDof = 0; parameterDof < parameters.Size(); ++parameterDof) { + parameters(parameterDof) = 0.015 + 0.001 * static_cast(parameterDof % 7); + } + + mfem::Vector displacement(prepared.surfaceDisplacementSize()); + prepared.buildSurfaceDisplacement(parameters, displacement); + + const mfem::Vector referencePositions = nodal_radial_surface_test_utils::projectReferenceSurfacePositions( + *fem.surfaceDeformationFes, prepared.surfaceDofMap() + ); + + constexpr double tolerance = 2.0e-13; + + for (int parameterDof = 0; parameterDof < prepared.parameterCount(); ++parameterDof) { + double radiusSquared = 0.0; + double displacementNorm2 = 0.0; + double radialProjection = 0.0; + + for (int component = 0; component < prepared.spatialDimension(); ++component) { + const int surfaceDof = prepared.surfaceDisplacementDof(parameterDof, component); + const double radialCoordinate = referencePositions(surfaceDof) - prepared.referenceCenter()(component); + + radiusSquared += radialCoordinate * radialCoordinate; + } + + const double radius = std::sqrt(radiusSquared); + REQUIRE(radius > 0.0); + CHECK(std::abs(prepared.referenceRadius(parameterDof) - radius) <= tolerance * radius); + + for (int component = 0; component < prepared.spatialDimension(); ++component) { + const int surfaceDof = prepared.surfaceDisplacementDof(parameterDof, component); + const double radialCoordinate = referencePositions(surfaceDof) - prepared.referenceCenter()(component); + const double expectedDirection = radialCoordinate / radius; + const double expectedDisplacement = parameters(parameterDof) * expectedDirection; + + CHECK(std::abs(prepared.radialDirection(parameterDof, component) - expectedDirection) <= tolerance); + CHECK(std::abs(displacement(surfaceDof) - expectedDisplacement) <= tolerance); + + displacementNorm2 += displacement(surfaceDof) * displacement(surfaceDof); + radialProjection += displacement(surfaceDof) * expectedDirection; + } + + CHECK(std::abs(std::sqrt(displacementNorm2) - parameters(parameterDof)) <= tolerance); + CHECK(std::abs(radialProjection - parameters(parameterDof)) <= tolerance); + + double movedRadiusSquared = 0.0; + for (int component = 0; component < prepared.spatialDimension(); ++component) { + const int surfaceDof = prepared.surfaceDisplacementDof(parameterDof, component); + const double movedCoordinate = + referencePositions(surfaceDof) + displacement(surfaceDof) - prepared.referenceCenter()(component); + movedRadiusSquared += movedCoordinate * movedCoordinate; + } + CHECK(std::abs(std::sqrt(movedRadiusSquared) - (radius + parameters(parameterDof))) <= tolerance); + } +} + +TEST_CASE( + "Nodal Radial Surface Jacobian Matches Centered Difference And Its Transpose Preserves Virtual Work", + tags::nodal_radial_surface_linearization +) { + namespace deformation = mean_field::deformation; + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + const deformation::PreparedNodalRadialSurface prepared = nodal_radial_surface_test_utils::makePreparedSurface(fem); + + mfem::Vector parameters(prepared.parameterCount()); + mfem::Vector direction(prepared.parameterCount()); + for (int parameterDof = 0; parameterDof < prepared.parameterCount(); ++parameterDof) { + const double index = static_cast(parameterDof + 1); + parameters(parameterDof) = 0.013 * std::sin(0.37 * index); + direction(parameterDof) = std::cos(0.19 * index) - 0.21 * std::sin(0.43 * index); + } + + constexpr double step = 1.0e-6; + mfem::Vector plusParameters(parameters); + mfem::Vector minusParameters(parameters); + plusParameters.Add(step, direction); + minusParameters.Add(-step, direction); + + mfem::Vector plusDisplacement(prepared.surfaceDisplacementSize()); + mfem::Vector minusDisplacement(prepared.surfaceDisplacementSize()); + mfem::Vector jacobianAction(prepared.surfaceDisplacementSize()); + prepared.buildSurfaceDisplacement(plusParameters, plusDisplacement); + prepared.buildSurfaceDisplacement(minusParameters, minusDisplacement); + prepared.applyJacobian(parameters, direction, jacobianAction); + + mfem::Vector centeredDifference(plusDisplacement); + centeredDifference -= minusDisplacement; + centeredDifference /= 2.0 * step; + + CHECK(nodal_radial_surface_test_utils::relativeError(jacobianAction, centeredDifference) < 2.0e-11); + + mfem::Vector surfaceDual(prepared.surfaceDisplacementSize()); + for (int surfaceDof = 0; surfaceDof < surfaceDual.Size(); ++surfaceDof) { + const double index = static_cast(surfaceDof + 1); + surfaceDual(surfaceDof) = std::sin(0.23 * index) + 0.17 * std::cos(0.31 * index); + } + + mfem::Vector parameterDual(prepared.parameterCount()); + prepared.applyJacobianTranspose(parameters, surfaceDual, parameterDual); + + const double surfaceWork = jacobianAction * surfaceDual; + const double parameterWork = direction * parameterDual; + const double workScale = std::max({1.0, std::abs(surfaceWork), std::abs(parameterWork)}); + CHECK(std::abs(surfaceWork - parameterWork) <= 3.0e-14 * workScale); + + mfem::Vector pullbackDerivative(prepared.parameterCount()); + pullbackDerivative = 1.0; + prepared.applyPullbackDerivative(parameters, direction, surfaceDual, pullbackDerivative); + CHECK(pullbackDerivative.Norml2() == 0.0); + + mfem::Vector wrongParameters(prepared.parameterCount() + 1); + mfem::Vector wrongSurfaceDisplacement(prepared.surfaceDisplacementSize() + 1); + CHECK_THROWS_AS(prepared.buildSurfaceDisplacement(wrongParameters, plusDisplacement), std::invalid_argument); + CHECK_THROWS_AS(prepared.buildSurfaceDisplacement(parameters, wrongSurfaceDisplacement), std::invalid_argument); +} diff --git a/tests/deformation/radial_extensions.cpp b/tests/deformation/radial_extensions.cpp new file mode 100644 index 0000000..e1b987b --- /dev/null +++ b/tests/deformation/radial_extensions.cpp @@ -0,0 +1,491 @@ +#include +#include +#include +#include +#include + +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace radial_extension_test_utils { + namespace deformation = mean_field::deformation; + namespace domain = mean_field::utils::domain; + namespace field = mean_field::field; + + using Schema = domain::CoreEnvelopeVacuumDomainSchema; + + [[nodiscard]] mfem::Vector referenceCenter(const int spatialDimension) { + mfem::Vector center(spatialDimension); + center = 0.0; + return center; + } + + [[nodiscard]] deformation::PreparedNodalRadialSurface makePreparedSurface(const mean_field::fem::FEM &fem) { + const field::ScalarBoundaryDofMap surfaceDofMap = + field::make_stellar_surface_scalar_dof_map(*fem.surfaceDeformationFes); + const deformation::SurfaceDeformationCompilationContext context{*fem.surfaceDeformationFes, surfaceDofMap}; + return deformation::compileSurfaceDeformationPrescription( + deformation::NodalRadialSurface{referenceCenter(fem.mesh->SpaceDimension())}, context + ); + } + + [[nodiscard]] double globalInnerProduct( + const mfem::Vector &first, + const mfem::Vector &second, + MPI_Comm communicator + ) { + REQUIRE(first.Size() == second.Size()); + const double local = first * second; + double global = 0.0; + MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator); + return global; + } + + [[nodiscard]] double relativeError( + const mfem::Vector &actual, + const mfem::Vector &expected + ) { + REQUIRE(actual.Size() == expected.Size()); + mfem::Vector difference(actual); + difference -= expected; + return difference.Norml2() / std::max(expected.Norml2(), std::numeric_limits::epsilon()); + } + + [[nodiscard]] int mfemByNodesVectorDof( + const int scalarTrueDof, + const int component, + const int scalarTrueDofCount + ) { + return scalarTrueDof + component * scalarTrueDofCount; + } +} // namespace radial_extension_test_utils + +TEST_CASE( + "Radial Interior And Vacuum Extensions Advertise Closed Form Boundary Behavior", + tags::radial_deformation_extension_validation +) { + namespace deformation = mean_field::deformation; + + STATIC_CHECK(deformation::InteriorDeformationExtension); + STATIC_CHECK( + deformation::PreparedInteriorDeformationExtension + ); + STATIC_CHECK(deformation::VacuumDeformationExtension); + STATIC_CHECK( + deformation::PreparedVacuumDeformationExtension + ); + + const deformation::PowerLawRadialInteriorExtension interior; + const deformation::InteriorDeformationExtensionDescriptor interiorDescriptor = interior.descriptor(); + CHECK(interior.radialPower() == 2.0); + CHECK(interiorDescriptor.name == "PowerLawRadialInteriorExtension"); + CHECK(interiorDescriptor.linearOnReferenceGeometry); + CHECK(interiorDescriptor.requiresRadialFoliation); + CHECK_FALSE(interiorDescriptor.requiresAuxiliarySolve); + CHECK(interiorDescriptor.supportsExactNewtonLinearization()); + CHECK(interiorDescriptor.centerBehavior == deformation::InteriorCenterBehavior::FixedAtReferenceCenter); + + const deformation::FixedInfinityRadialVacuumExtension vacuum; + const deformation::VacuumDeformationExtensionDescriptor vacuumDescriptor = vacuum.descriptor(); + CHECK(vacuumDescriptor.name == "FixedInfinityRadialVacuumExtension"); + CHECK(vacuumDescriptor.linearOnReferenceGeometry); + CHECK(vacuumDescriptor.requiresRadialFoliation); + CHECK_FALSE(vacuumDescriptor.requiresAuxiliarySolve); + CHECK(vacuumDescriptor.supportsExactNewtonLinearization()); + CHECK(vacuumDescriptor.outerBoundaryBehavior == deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity); + + CHECK_THROWS_AS(deformation::PowerLawRadialInteriorExtension{0.5}, std::invalid_argument); + CHECK_THROWS_AS( + deformation::PowerLawRadialInteriorExtension{std::numeric_limits::infinity()}, std::invalid_argument + ); +} + +TEST_CASE( + "Radial Extensions Reproduce The Stellar Surface Fix Reference Infinity And Preserve Positive Volume Maps", + tags::radial_deformation_extension_analytic &tags::radial_deformation_extension_mapping +) { + namespace deformation = mean_field::deformation; + namespace domain = mean_field::utils::domain; + namespace field = mean_field::field; + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + const deformation::PreparedNodalRadialSurface surface = radial_extension_test_utils::makePreparedSurface(fem); + const deformation::RadialDeformationExtensionCompilationContext context = + deformation::makeRadialDeformationExtensionCompilationContext( + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh + ); + const deformation::PreparedPowerLawRadialInteriorExtension interior = + deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{}, context); + const deformation::PreparedPowerLawRadialInteriorExtension cubicInterior = + deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{3.0}, context); + const deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = + deformation::compileVacuumDeformationExtension(deformation::FixedInfinityRadialVacuumExtension{}, context); + + REQUIRE(interior.surfaceDisplacementSize() == surface.surfaceDisplacementSize()); + REQUIRE(vacuum.surfaceDisplacementSize() == surface.surfaceDisplacementSize()); + REQUIRE(interior.interiorDisplacementSize() == fem.displacementFes->GetTrueVSize()); + REQUIRE(vacuum.vacuumDisplacementSize() == fem.displacementFes->GetTrueVSize()); + + constexpr double surfaceAmplitude = 0.02; + mfem::Vector parameters(surface.parameterCount()); + parameters = surfaceAmplitude; + mfem::Vector surfaceDisplacement(surface.surfaceDisplacementSize()); + surface.buildSurfaceDisplacement(parameters, surfaceDisplacement); + + mfem::Vector interiorDisplacement(interior.interiorDisplacementSize()); + mfem::Vector vacuumDisplacement(vacuum.vacuumDisplacementSize()); + interior.buildInteriorDisplacement(surfaceDisplacement, interiorDisplacement); + vacuum.buildVacuumDisplacement(surfaceDisplacement, vacuumDisplacement); + + const int spatialDimension = fem.mesh->SpaceDimension(); + const field::ScalarBoundaryDofMap stellarSurfaceMap = + field::make_scalar_boundary_dof_map( + *fem.surfaceDeformationFes + ); + const field::ScalarBoundaryDofMap infinitySurfaceMap = + field::make_scalar_boundary_dof_map( + *fem.surfaceDeformationFes + ); + + CHECK_THROWS_AS( + deformation::RadialDeformationExtensionCompilationContext( + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh, stellarSurfaceMap, + stellarSurfaceMap, + domain::make_attribute_marker(*fem.mesh), + domain::make_attribute_marker(*fem.mesh), + radial_extension_test_utils::Schema::template boundary_attribute(), + radial_extension_test_utils::Schema::template boundary_attribute() + ), + std::invalid_argument + ); + + CHECK_THROWS_AS( + deformation::makeRadialDeformationExtensionCompilationContext( + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.mesh + ), + std::invalid_argument + ); + + const double stellarSurfaceRadius = context.stellarSurfaceLogicalRadius(); + const double infinitySurfaceRadius = context.infinitySurfaceLogicalRadius(); + REQUIRE(stellarSurfaceRadius > 0.0); + REQUIRE(infinitySurfaceRadius > stellarSurfaceRadius); + + constexpr double tolerance = 2.0e-11; + bool hasInterpolatedSurfacePoint = false; + for (int scalarDof = 0; scalarDof < interior.scalarTrueDofCount(); ++scalarDof) { + const double referenceRadius = context.logicalRadius(scalarDof); + const int interpolationEntryCount = context.surfaceInterpolationEntryCount(scalarDof); + + if (interpolationEntryCount == 0) { + CHECK(referenceRadius <= 64.0 * std::numeric_limits::epsilon() * infinitySurfaceRadius); + } else { + double interpolationWeightSum = 0.0; + for (int entry = 0; entry < interpolationEntryCount; ++entry) { + const int surfaceCoordinate = context.surfaceGlobalCoordinate(scalarDof, entry); + CHECK(surfaceCoordinate >= 0); + CHECK(surfaceCoordinate < stellarSurfaceMap.global_size()); + interpolationWeightSum += context.surfaceInterpolationWeight(scalarDof, entry); + } + CHECK(std::abs(interpolationWeightSum - 1.0) <= tolerance); + hasInterpolatedSurfacePoint |= interpolationEntryCount > 1; + } + + if (interior.hasStellarSupport(scalarDof)) { + const double expectedWeight = + referenceRadius == 0.0 ? 0.0 : std::pow(referenceRadius / stellarSurfaceRadius, 2.0); + CHECK(std::abs(interior.radialWeight(scalarDof) - expectedWeight) <= tolerance); + const double expectedCubicWeight = + referenceRadius == 0.0 ? 0.0 : std::pow(referenceRadius / stellarSurfaceRadius, 3.0); + CHECK(std::abs(cubicInterior.radialWeight(scalarDof) - expectedCubicWeight) <= tolerance); + } else { + CHECK(interior.radialWeight(scalarDof) == 0.0); + for (int component = 0; component < spatialDimension; ++component) { + const int volumeVectorDof = radial_extension_test_utils::mfemByNodesVectorDof( + scalarDof, component, interior.scalarTrueDofCount() + ); + CHECK(interiorDisplacement(volumeVectorDof) == 0.0); + } + } + + if (vacuum.hasVacuumSupport(scalarDof)) { + const double expectedWeight = + (infinitySurfaceRadius - referenceRadius) / (infinitySurfaceRadius - stellarSurfaceRadius); + CHECK(std::abs(vacuum.radialWeight(scalarDof) - expectedWeight) <= tolerance); + } else { + CHECK(vacuum.radialWeight(scalarDof) == 0.0); + for (int component = 0; component < spatialDimension; ++component) { + const int volumeVectorDof = radial_extension_test_utils::mfemByNodesVectorDof( + scalarDof, component, vacuum.scalarTrueDofCount() + ); + CHECK(vacuumDisplacement(volumeVectorDof) == 0.0); + } + } + } + CHECK(hasInterpolatedSurfacePoint); + + const double componentValues[3]{1.25, -0.75, 2.5}; + mfem::Vector constantSurfaceDisplacement(surface.surfaceDisplacementSize()); + for (int surfaceDof = 0; surfaceDof < surface.parameterCount(); ++surfaceDof) { + for (int component = 0; component < spatialDimension; ++component) { + constantSurfaceDisplacement(spatialDimension * surfaceDof + component) = componentValues[component]; + } + } + mfem::Vector constantInteriorDisplacement(interior.interiorDisplacementSize()); + interior.buildInteriorDisplacement(constantSurfaceDisplacement, constantInteriorDisplacement); + + mfem::Vector radialWeightTrueDofs(interior.scalarTrueDofCount()); + for (int scalarDof = 0; scalarDof < interior.scalarTrueDofCount(); ++scalarDof) { + radialWeightTrueDofs(scalarDof) = + interior.hasStellarSupport(scalarDof) ? interior.radialWeight(scalarDof) : 0.0; + } + mfem::ParGridFunction radialWeightField(fem.surfaceDeformationFes.get()); + mfem::ParGridFunction constantVectorField(fem.displacementFes.get()); + radialWeightField.SetFromTrueDofs(radialWeightTrueDofs); + constantVectorField.SetFromTrueDofs(constantInteriorDisplacement); + + const mfem::Array stellarMarker = + domain::make_attribute_marker(*fem.mesh); + int sampledStellarElement = -1; + for (int element = 0; element < fem.mesh->GetNE() && sampledStellarElement < 0; ++element) { + const int attribute = fem.mesh->GetAttribute(element); + if (attribute > 0 && attribute <= stellarMarker.Size() && stellarMarker[attribute - 1] != 0) { + sampledStellarElement = element; + } + } + REQUIRE(sampledStellarElement >= 0); + const mfem::IntegrationPoint &samplePoint = + mfem::Geometries.GetCenter(fem.mesh->GetElementBaseGeometry(sampledStellarElement)); + const double sampledRadialWeight = radialWeightField.GetValue(sampledStellarElement, samplePoint); + mfem::Vector sampledVector(spatialDimension); + constantVectorField.GetVectorValue(sampledStellarElement, samplePoint, sampledVector); + for (int component = 0; component < spatialDimension; ++component) { + CHECK(std::abs(sampledVector(component) - componentValues[component] * sampledRadialWeight) <= tolerance); + } + + mfem::Vector arbitrarySurfaceDisplacement(surface.surfaceDisplacementSize()); + for (int dof = 0; dof < arbitrarySurfaceDisplacement.Size(); ++dof) { + const double index = static_cast(dof + 1); + arbitrarySurfaceDisplacement(dof) = 0.03 * std::sin(0.29 * index) - 0.01 * std::cos(0.17 * index); + } + mfem::Vector arbitraryInteriorDisplacement(interior.interiorDisplacementSize()); + mfem::Vector arbitraryVacuumDisplacement(vacuum.vacuumDisplacementSize()); + interior.buildInteriorDisplacement(arbitrarySurfaceDisplacement, arbitraryInteriorDisplacement); + vacuum.buildVacuumDisplacement(arbitrarySurfaceDisplacement, arbitraryVacuumDisplacement); + + for (int surfaceDof = 0; surfaceDof < stellarSurfaceMap.local_size(); ++surfaceDof) { + const int scalarDof = stellarSurfaceMap.volume_true_dof(surfaceDof); + for (int component = 0; component < spatialDimension; ++component) { + const int surfaceVectorDof = spatialDimension * surfaceDof + component; + const int volumeVectorDof = + radial_extension_test_utils::mfemByNodesVectorDof(scalarDof, component, interior.scalarTrueDofCount()); + CHECK( + std::abs( + arbitraryInteriorDisplacement(volumeVectorDof) - arbitrarySurfaceDisplacement(surfaceVectorDof) + ) <= tolerance + ); + CHECK( + std::abs( + arbitraryVacuumDisplacement(volumeVectorDof) - arbitrarySurfaceDisplacement(surfaceVectorDof) + ) <= tolerance + ); + } + } + + for (int infinityDof = 0; infinityDof < infinitySurfaceMap.local_size(); ++infinityDof) { + const int scalarDof = infinitySurfaceMap.volume_true_dof(infinityDof); + for (int component = 0; component < spatialDimension; ++component) { + const int volumeVectorDof = + radial_extension_test_utils::mfemByNodesVectorDof(scalarDof, component, vacuum.scalarTrueDofCount()); + CHECK(std::abs(vacuumDisplacement(volumeVectorDof)) <= tolerance); + } + } + + mfem::Vector combinedDisplacement(interiorDisplacement); + for (int scalarDof = 0; scalarDof < vacuum.scalarTrueDofCount(); ++scalarDof) { + if (!vacuum.hasVacuumSupport(scalarDof) || interior.hasStellarSupport(scalarDof)) { + continue; + } + for (int component = 0; component < spatialDimension; ++component) { + const int volumeVectorDof = + radial_extension_test_utils::mfemByNodesVectorDof(scalarDof, component, vacuum.scalarTrueDofCount()); + combinedDisplacement(volumeVectorDof) = vacuumDisplacement(volumeVectorDof); + } + } + + mfem::ParGridFunction displacement(fem.displacementFes.get()); + displacement.SetFromTrueDofs(combinedDisplacement); + double localMinimumDeterminant = std::numeric_limits::infinity(); + for (int element = 0; element < fem.mesh->GetNE(); ++element) { + mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element); + const mfem::FiniteElement *finiteElement = fem.displacementFes->GetFE(element); + const mfem::IntegrationRule &rule = + mfem::IntRules.Get(transformation->GetGeometryType(), finiteElement->GetOrder() + 2); + + for (int point = 0; point < rule.GetNPoints(); ++point) { + transformation->SetIntPoint(&rule.IntPoint(point)); + mfem::DenseMatrix displacementGradient; + displacement.GetVectorGradient(*transformation, displacementGradient); + for (int component = 0; component < spatialDimension; ++component) { + displacementGradient(component, component) += 1.0; + } + localMinimumDeterminant = std::min(localMinimumDeterminant, displacementGradient.Det()); + } + } + double globalMinimumDeterminant = 0.0; + MPI_Allreduce(&localMinimumDeterminant, &globalMinimumDeterminant, 1, MPI_DOUBLE, MPI_MIN, fem.mesh->GetComm()); + CHECK(globalMinimumDeterminant > 0.0); +} + +TEST_CASE( + "Radial Extension Jacobians Match Centered Differences And Their Transposes Preserve Virtual Work", + tags::radial_deformation_extension_linearization +) { + namespace deformation = mean_field::deformation; + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + const deformation::RadialDeformationExtensionCompilationContext context = + deformation::makeRadialDeformationExtensionCompilationContext( + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh + ); + const deformation::PreparedPowerLawRadialInteriorExtension interior = + deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{}, context); + const deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = + deformation::compileVacuumDeformationExtension(deformation::FixedInfinityRadialVacuumExtension{}, context); + + mfem::Vector surface(interior.surfaceDisplacementSize()); + mfem::Vector direction(interior.surfaceDisplacementSize()); + for (int dof = 0; dof < surface.Size(); ++dof) { + const double index = static_cast(dof + 1); + surface(dof) = 0.01 * std::sin(0.17 * index); + direction(dof) = std::cos(0.13 * index) - 0.2 * std::sin(0.31 * index); + } + + constexpr double step = 1.0e-6; + mfem::Vector plusSurface(surface); + mfem::Vector minusSurface(surface); + plusSurface.Add(step, direction); + minusSurface.Add(-step, direction); + + auto checkLinearization = [&](const auto &prepared, const int volumeSize, const auto &build) { + mfem::Vector plus(volumeSize); + mfem::Vector minus(volumeSize); + mfem::Vector jacobian(volumeSize); + build(prepared, plusSurface, plus); + build(prepared, minusSurface, minus); + prepared.applyJacobian(surface, direction, jacobian); + + mfem::Vector centeredDifference(plus); + centeredDifference -= minus; + centeredDifference /= 2.0 * step; + CHECK(radial_extension_test_utils::relativeError(jacobian, centeredDifference) < 2.0e-10); + + mfem::Vector volumeDual(volumeSize); + for (int dof = 0; dof < volumeDual.Size(); ++dof) { + const double index = static_cast(dof + 1); + volumeDual(dof) = std::sin(0.07 * index) + 0.3 * std::cos(0.11 * index); + } + mfem::Vector surfaceDual(surface.Size()); + prepared.applyJacobianTranspose(surface, volumeDual, surfaceDual); + + const double volumeWork = + radial_extension_test_utils::globalInnerProduct(jacobian, volumeDual, fem.mesh->GetComm()); + const double surfaceWork = + radial_extension_test_utils::globalInnerProduct(direction, surfaceDual, fem.mesh->GetComm()); + const double scale = std::max({1.0, std::abs(volumeWork), std::abs(surfaceWork)}); + CHECK(std::abs(volumeWork - surfaceWork) <= 5.0e-13 * scale); + + mfem::Vector pullback(surface.Size()); + pullback = 1.0; + prepared.applyPullbackDerivative(surface, direction, volumeDual, pullback); + CHECK(pullback.Norml2() == 0.0); + }; + + checkLinearization( + interior, interior.interiorDisplacementSize(), + [](const auto &prepared, const mfem::Vector &input, mfem::Vector &output) { + prepared.buildInteriorDisplacement(input, output); + } + ); + checkLinearization( + vacuum, vacuum.vacuumDisplacementSize(), + [](const auto &prepared, const mfem::Vector &input, mfem::Vector &output) { + prepared.buildVacuumDisplacement(input, output); + } + ); + + mfem::Vector wrongSurface(surface.Size() + 1); + mfem::Vector interiorOutput(interior.interiorDisplacementSize()); + mfem::Vector vacuumOutput(vacuum.vacuumDisplacementSize()); + CHECK_THROWS_AS(interior.buildInteriorDisplacement(wrongSurface, interiorOutput), std::invalid_argument); + CHECK_THROWS_AS(vacuum.buildVacuumDisplacement(wrongSurface, vacuumOutput), std::invalid_argument); +} + +TEST_CASE( + "Logical Radial Deformation Remains Conforming And Orientation Preserving After Mesh Refinement", + tags::radial_deformation_extension_mapping +) { + namespace deformation = mean_field::deformation; + + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 1); + REQUIRE(fem.okay()); + + deformation::PreparedNodalRadialSurface surface = radial_extension_test_utils::makePreparedSurface(fem); + const deformation::RadialDeformationExtensionCompilationContext context = + deformation::makeRadialDeformationExtensionCompilationContext( + *fem.surfaceDeformationFes, *fem.displacementFes, *fem.logicalReferenceMesh + ); + deformation::PreparedPowerLawRadialInteriorExtension interior = + deformation::compileInteriorDeformationExtension(deformation::PowerLawRadialInteriorExtension{}, context); + deformation::PreparedFixedInfinityRadialVacuumExtension vacuum = + deformation::compileVacuumDeformationExtension(deformation::FixedInfinityRadialVacuumExtension{}, context); + auto prepared = deformation::composePreparedDomainDeformation( + std::move(surface), std::move(interior), std::move(vacuum), *fem.surfaceDeformationFes, *fem.displacementFes, + *fem.logicalReferenceMesh + ); + + mfem::Vector parameters(prepared.parameterCount()); + mfem::Vector direction(prepared.parameterCount()); + for (int parameter = 0; parameter < parameters.Size(); ++parameter) { + const double index = static_cast(parameter + 1); + const double polarDirection = prepared.surfaceDeformationPrescription().radialDirection(parameter, 2); + const double quadrupoleValue = 0.5 * (3.0 * polarDirection * polarDirection - 1.0); + parameters(parameter) = 0.006 - 0.001 * quadrupoleValue; + direction(parameter) = std::sin(0.07 * index) - 0.4 * std::cos(0.13 * index); + } + + mfem::Vector volumeDisplacement(prepared.volumeDisplacementSize()); + prepared.buildVolumeDisplacement(parameters, volumeDisplacement); + const deformation::DomainDeformationGeometryReport geometry = prepared.inspectMappedGeometry(volumeDisplacement); + CAPTURE(geometry.minimumJacobianDeterminant); + REQUIRE(geometry.isOrientationPreserving()); + + mfem::Vector jacobianAction(prepared.volumeDisplacementSize()); + mfem::Vector volumeDual(prepared.volumeDisplacementSize()); + for (int dof = 0; dof < volumeDual.Size(); ++dof) { + const double index = static_cast(dof + 1); + volumeDual(dof) = std::cos(0.017 * index) + 0.2 * std::sin(0.023 * index); + } + prepared.applyJacobian(parameters, direction, jacobianAction); + mfem::Vector parameterDual(prepared.parameterCount()); + prepared.applyJacobianTranspose(parameters, volumeDual, parameterDual); + const double volumeWork = + radial_extension_test_utils::globalInnerProduct(jacobianAction, volumeDual, fem.mesh->GetComm()); + const double parameterWork = + radial_extension_test_utils::globalInnerProduct(direction, parameterDual, fem.mesh->GetComm()); + CHECK( + std::abs(volumeWork - parameterWork) <= 2.0e-12 * std::max({1.0, std::abs(volumeWork), std::abs(parameterWork)}) + ); +} diff --git a/tests/deformation/surface_scalar_dof_map.cpp b/tests/deformation/surface_scalar_dof_map.cpp new file mode 100644 index 0000000..25826bd --- /dev/null +++ b/tests/deformation/surface_scalar_dof_map.cpp @@ -0,0 +1,302 @@ +#include +#include +#include +#include +#include + +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace surface_scalar_dof_test_utils { + namespace domain = mean_field::utils::domain; + namespace field = mean_field::field; + + using DefaultSchema = domain::CoreEnvelopeVacuumDomainSchema; + + using FirstBoundarySchema = domain::DomainSchema< + domain::MaterialList<>, + domain::BoundaryList>, + domain::RelationList<>>; + + using SecondBoundarySchema = domain::DomainSchema< + domain::MaterialList<>, + domain::BoundaryList>, + domain::RelationList<>>; + + using MissingBoundarySchema = + domain::DomainSchema, domain::BoundaryList<>, domain::RelationList<>>; + + [[nodiscard]] mfem::Array make_array(const std::initializer_list values) { + mfem::Array result(static_cast(values.size())); + int index = 0; + for (const int value : values) { + result[index++] = value; + } + return result; + } + + [[nodiscard]] mfem::Array expected_boundary_true_dofs( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const int boundaryAttribute + ) { + const mfem::Mesh *mesh = finiteElementSpace.GetMesh(); + REQUIRE(mesh != nullptr); + REQUIRE(boundaryAttribute > 0); + REQUIRE(boundaryAttribute <= mesh->bdr_attributes.Max()); + + mfem::Array boundaryMarker(mesh->bdr_attributes.Max()); + boundaryMarker = 0; + boundaryMarker[boundaryAttribute - 1] = 1; + + mfem::Array expected; + finiteElementSpace.GetEssentialTrueDofs(boundaryMarker, expected); + return expected; + } + + void check_equal( + const mfem::Array &actual, + const mfem::Array &expected + ) { + REQUIRE(actual.Size() == expected.Size()); + for (int index = 0; index < actual.Size(); ++index) { + CAPTURE(index); + CHECK(actual[index] == expected[index]); + } + } + + [[nodiscard]] mfem::Mesh make_boundary_mesh() { + return mfem::Mesh::MakeCartesian2D(6, 4, mfem::Element::QUADRILATERAL, true, 3.0, 2.0); + } + + template + concept CanMakeStellarSurfaceMap = requires(const mfem::ParFiniteElementSpace &finiteElementSpace) { + field::make_stellar_surface_scalar_dof_map(finiteElementSpace); + }; +} // namespace surface_scalar_dof_test_utils + +TEST_CASE( + "Scalar Boundary DOF Map Preserves Canonical Surface Coordinate Indexing", + tags::surface_deformation_dof_unit +) { + namespace field = mean_field::field; + + const field::ScalarBoundaryDofMap map(8, surface_scalar_dof_test_utils::make_array({1, 3, 6}), 5, 12); + + CHECK(map.volume_true_dof_size() == 8); + CHECK(map.local_size() == 3); + CHECK(map.global_offset() == 5); + CHECK(map.global_size() == 12); + CHECK_FALSE(map.empty()); + + CHECK(map.volume_true_dof(0) == 1); + CHECK(map.volume_true_dof(1) == 3); + CHECK(map.volume_true_dof(2) == 6); + CHECK(map.global_boundary_dof(0) == 5); + CHECK(map.global_boundary_dof(1) == 6); + CHECK(map.global_boundary_dof(2) == 7); + + REQUIRE(map.local_boundary_dof(1).has_value()); + REQUIRE(map.local_boundary_dof(3).has_value()); + REQUIRE(map.local_boundary_dof(6).has_value()); + CHECK(*map.local_boundary_dof(1) == 0); + CHECK(*map.local_boundary_dof(3) == 1); + CHECK(*map.local_boundary_dof(6) == 2); + CHECK_FALSE(map.local_boundary_dof(0).has_value()); + + mfem::Vector volumeValues(8); + for (int trueDof = 0; trueDof < volumeValues.Size(); ++trueDof) { + volumeValues(trueDof) = 10.0 + trueDof; + } + + const mfem::Vector boundaryValues = map.gather(volumeValues); + REQUIRE(boundaryValues.Size() == map.local_size()); + CHECK(boundaryValues(0) == 11.0); + CHECK(boundaryValues(1) == 13.0); + CHECK(boundaryValues(2) == 16.0); + + const mfem::Vector scattered = map.scatter(boundaryValues); + REQUIRE(scattered.Size() == map.volume_true_dof_size()); + for (int trueDof = 0; trueDof < scattered.Size(); ++trueDof) { + const std::optional localBoundaryDof = map.local_boundary_dof(trueDof); + if (localBoundaryDof.has_value()) { + CHECK(scattered(trueDof) == boundaryValues(*localBoundaryDof)); + } else { + CHECK(scattered(trueDof) == 0.0); + } + } + + CHECK_THROWS_AS( + (field::ScalarBoundaryDofMap(8, surface_scalar_dof_test_utils::make_array({3, 1}), 0, 2)), std::invalid_argument + ); + CHECK_THROWS_AS( + (field::ScalarBoundaryDofMap(8, surface_scalar_dof_test_utils::make_array({1, 3, 6}), -1, 3)), + std::invalid_argument + ); + CHECK_THROWS_AS( + (field::ScalarBoundaryDofMap(8, surface_scalar_dof_test_utils::make_array({1, 3, 6}), 4, 6)), + std::invalid_argument + ); + CHECK_THROWS_AS(map.global_boundary_dof(-1), std::out_of_range); + CHECK_THROWS_AS(map.global_boundary_dof(map.local_size()), std::out_of_range); +} + +TEST_CASE( + "Stellar Surface Coordinates Use The Scalar Displacement Basis And Only Surface DOFs", + tags::surface_deformation_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 fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + REQUIRE(fem.surfaceDeformationFes != nullptr); + REQUIRE(fem.displacementFes != nullptr); + CHECK(fem.surfaceDeformationFes->GetVDim() == 1); + CHECK(fem.surfaceDeformationFes->FEColl() == fem.displacementFes->FEColl()); + CHECK(fem.surfaceDeformationFes.get() != fem.enthalpyFes.get()); + + const field::ScalarBoundaryDofMap surfaceCoordinates = + field::make_stellar_surface_scalar_dof_map( + *fem.surfaceDeformationFes + ); + + const mfem::Array expected = surface_scalar_dof_test_utils::expected_boundary_true_dofs( + *fem.surfaceDeformationFes, + surface_scalar_dof_test_utils::DefaultSchema::template boundary_attribute() + ); + surface_scalar_dof_test_utils::check_equal(surfaceCoordinates.boundary_true_dofs(), expected); + + CHECK(surfaceCoordinates.global_size() > 0); + CHECK(surfaceCoordinates.global_size() < fem.surfaceDeformationFes->GlobalTrueVSize()); + + const field::FieldDofMap displacementMap = + field::make_field_dof_map( + *fem.displacementFes + ); + const field::FieldBoundaryDofMap vectorSurface = field::make_field_boundary_dof_map< + field::Displacement, domain::StellarSurface, surface_scalar_dof_test_utils::DefaultSchema>( + *fem.displacementFes, displacementMap + ); + + long long localVectorSurfaceSize = vectorSurface.size(); + long long globalVectorSurfaceSize = 0; + MPI_Allreduce( + &localVectorSurfaceSize, &globalVectorSurfaceSize, 1, MPI_LONG_LONG, MPI_SUM, + fem.surfaceDeformationFes->GetComm() + ); + + CHECK( + globalVectorSurfaceSize == static_cast(fem.mesh->SpaceDimension()) * surfaceCoordinates.global_size() + ); +} + +TEST_CASE( + "Scalar Boundary DOF Resolution Uses Semantic Schema Boundary Attributes", + tags::surface_deformation_dof_schema +) { + namespace domain = mean_field::utils::domain; + namespace field = mean_field::field; + + STATIC_CHECK( + surface_scalar_dof_test_utils::CanMakeStellarSurfaceMap + ); + STATIC_CHECK_FALSE( + surface_scalar_dof_test_utils::CanMakeStellarSurfaceMap + ); + + mfem::Mesh serialMesh = surface_scalar_dof_test_utils::make_boundary_mesh(); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + mfem::H1_FECollection finiteElementCollection(2, mesh.Dimension()); + mfem::ParFiniteElementSpace finiteElementSpace(&mesh, &finiteElementCollection); + + const field::ScalarBoundaryDofMap firstBoundary = + field::make_stellar_surface_scalar_dof_map( + finiteElementSpace + ); + const field::ScalarBoundaryDofMap secondBoundary = + field::make_stellar_surface_scalar_dof_map( + finiteElementSpace + ); + + const mfem::Array expectedFirst = surface_scalar_dof_test_utils::expected_boundary_true_dofs( + finiteElementSpace, + surface_scalar_dof_test_utils::FirstBoundarySchema::template boundary_attribute() + ); + const mfem::Array expectedSecond = surface_scalar_dof_test_utils::expected_boundary_true_dofs( + finiteElementSpace, + surface_scalar_dof_test_utils::SecondBoundarySchema::template boundary_attribute() + ); + + surface_scalar_dof_test_utils::check_equal(firstBoundary.boundary_true_dofs(), expectedFirst); + surface_scalar_dof_test_utils::check_equal(secondBoundary.boundary_true_dofs(), expectedSecond); + + bool localMapsDiffer = firstBoundary.local_size() != secondBoundary.local_size(); + if (!localMapsDiffer) { + for (int localDof = 0; localDof < firstBoundary.local_size(); ++localDof) { + if (firstBoundary.volume_true_dof(localDof) != secondBoundary.volume_true_dof(localDof)) { + localMapsDiffer = true; + break; + } + } + } + + int localDifference = localMapsDiffer ? 1 : 0; + int globalDifference = 0; + MPI_Allreduce(&localDifference, &globalDifference, 1, MPI_INT, MPI_MAX, finiteElementSpace.GetComm()); + CHECK(globalDifference == 1); +} + +TEST_CASE( + "Scalar Boundary Coordinates Have Deterministic Contiguous Parallel Ordering", + tags::surface_deformation_dof_parallel +) { + namespace field = mean_field::field; + + mfem::Mesh serialMesh = surface_scalar_dof_test_utils::make_boundary_mesh(); + mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); + mfem::H1_FECollection finiteElementCollection(2, mesh.Dimension()); + mfem::ParFiniteElementSpace finiteElementSpace(&mesh, &finiteElementCollection); + + const field::ScalarBoundaryDofMap first = + field::make_stellar_surface_scalar_dof_map( + finiteElementSpace + ); + const field::ScalarBoundaryDofMap second = + field::make_stellar_surface_scalar_dof_map( + finiteElementSpace + ); + + surface_scalar_dof_test_utils::check_equal(first.boundary_true_dofs(), second.boundary_true_dofs()); + CHECK(first.global_offset() == second.global_offset()); + CHECK(first.global_size() == second.global_size()); + + for (int localDof = 0; localDof < first.local_size(); ++localDof) { + CAPTURE(localDof); + CHECK(first.global_boundary_dof(localDof) == first.global_offset() + localDof); + if (localDof > 0) { + CHECK(first.volume_true_dof(localDof - 1) < first.volume_true_dof(localDof)); + } + } + + int communicatorSize = 1; + int communicatorRank = 0; + MPI_Comm_size(finiteElementSpace.GetComm(), &communicatorSize); + MPI_Comm_rank(finiteElementSpace.GetComm(), &communicatorRank); + + const long long localSize = first.local_size(); + std::vector localSizes(static_cast(communicatorSize)); + MPI_Allgather(&localSize, 1, MPI_LONG_LONG, localSizes.data(), 1, MPI_LONG_LONG, finiteElementSpace.GetComm()); + + const long long expectedOffset = std::accumulate(localSizes.begin(), localSizes.begin() + communicatorRank, 0LL); + const long long expectedGlobalSize = std::accumulate(localSizes.begin(), localSizes.end(), 0LL); + + CHECK(first.global_offset() == expectedOffset); + CHECK(first.global_size() == expectedGlobalSize); +} diff --git a/tests/models/stellar_model.cpp b/tests/models/stellar_model.cpp index b9b4f87..c0914bf 100644 --- a/tests/models/stellar_model.cpp +++ b/tests/models/stellar_model.cpp @@ -8,6 +8,8 @@ #include +#include + import mean_field; import test_helpers; @@ -96,19 +98,18 @@ namespace { } // namespace TEST_CASE( - "Stellar Model Owns Structure And Surface Prescriptions", - tags::stellar_model_type_contract + "Stellar Model Owns Structure Prescription And Surface Condition", + tags::stellar_model_type_contract &tags::surface_condition_type_contract ) { STATIC_CHECK(mean_field::models::StructurePrescription); STATIC_CHECK(mean_field::models::StructurePrescription); STATIC_CHECK_FALSE(mean_field::models::StructurePrescription); STATIC_CHECK( - mean_field::models::SurfacePrescription< - mean_field::surface::ConstantPressureSurface, mean_field::eos::Polytrope> + mean_field::models::SurfaceCondition ); STATIC_CHECK_FALSE( - mean_field::models::SurfacePrescription + mean_field::models::SurfaceCondition ); STATIC_CHECK_FALSE(std::derived_from); @@ -141,14 +142,68 @@ TEST_CASE( decltype(model.structurePrescription()), const mean_field::models::structure::PolytropicStructure &> ); STATIC_CHECK( - std::same_as + std::same_as ); STATIC_CHECK(std::same_as); + STATIC_CHECK( + std::same_as< + typename PolytropicStellarModel::SurfaceDeformationPrescriptionType, + mean_field::deformation::NodalRadialSurface> + ); + STATIC_CHECK( + std::same_as< + typename PolytropicStellarModel::StellarInteriorDeformationExtensionType, + mean_field::deformation::PowerLawRadialInteriorExtension> + ); + STATIC_CHECK( + std::same_as< + typename PolytropicStellarModel::VacuumDeformationExtensionType, + mean_field::deformation::FixedInfinityRadialVacuumExtension> + ); 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}); + CHECK(model.surfaceCondition().targetPressure() == mean_field::eos::PressureValue{0.0}); + CHECK(model.surfaceDeformationPrescription().descriptor().name == "NodalRadialSurface"); + CHECK(model.stellarInteriorDeformationExtension().radialPower() == 2.0); + CHECK(model.vacuumDeformationExtension().descriptor().name == "FixedInfinityRadialVacuumExtension"); +} + +TEST_CASE( + "Stellar Model Owns Explicit Surface Interior And Vacuum Deformation Policies", + tags::stellar_model_deformation_ownership +) { + mfem::Vector referenceCenter(3); + referenceCenter(0) = 0.125; + referenceCenter(1) = -0.25; + referenceCenter(2) = 0.375; + + mean_field::models::StellarModel model{ + 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::deformation::NodalRadialSurface{referenceCenter}, + mean_field::deformation::PowerLawRadialInteriorExtension{3.0}, + mean_field::deformation::FixedInfinityRadialVacuumExtension{} + }; + + CHECK(model.surfaceDeformationPrescription().referenceCenter()(0) == referenceCenter(0)); + CHECK(model.surfaceDeformationPrescription().referenceCenter()(1) == referenceCenter(1)); + CHECK(model.surfaceDeformationPrescription().referenceCenter()(2) == referenceCenter(2)); + CHECK(model.stellarInteriorDeformationExtension().radialPower() == 3.0); + CHECK( + model.vacuumDeformationExtension().descriptor().outerBoundaryBehavior == + mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity + ); + + const auto *surfaceDeformationAddress = &model.surfaceDeformationPrescription(); + const auto *interiorDeformationAddress = &model.stellarInteriorDeformationExtension(); + const auto *vacuumDeformationAddress = &model.vacuumDeformationExtension(); + auto movedModel = std::move(model); + + CHECK(&movedModel.surfaceDeformationPrescription() == surfaceDeformationAddress); + CHECK(&movedModel.stellarInteriorDeformationExtension() == interiorDeformationAddress); + CHECK(&movedModel.vacuumDeformationExtension() == vacuumDeformationAddress); } TEST_CASE( @@ -184,7 +239,7 @@ TEST_CASE( const mean_field::models::structure::PolytropicStructure *structureAddress = &originalModel.structurePrescription(); - const mean_field::surface::ConstantPressureSurface *surfaceAddress = &originalModel.surfacePrescription(); + const mean_field::surface::ConstantPressureSurface *surfaceAddress = &originalModel.surfaceCondition(); const mean_field::eos::Polytrope *equationOfStateAddress = &originalModel.equationOfState(); @@ -193,7 +248,7 @@ TEST_CASE( mean_field::models::StellarModel movedModel{std::move(originalModel)}; CHECK(&movedModel.structurePrescription() == structureAddress); - CHECK(&movedModel.surfacePrescription() == surfaceAddress); + CHECK(&movedModel.surfaceCondition() == surfaceAddress); CHECK(&movedModel.equationOfState() == equationOfStateAddress); CHECK(&movedModel.compiledSurfaceConstraint() == compiledSurfaceConstraintAddress); CHECK(movedModel.targetMass() == 1.0); @@ -221,7 +276,7 @@ TEST_CASE( } TEST_CASE( - "Stellar Model Supports Custom Structure And Surface Prescriptions", + "Stellar Model Supports Custom Structure Prescriptions And Surface Conditions", tags::barotrope &tags::unit &tags::model ) { const auto tracker = std::make_shared(); @@ -261,9 +316,12 @@ TEST_CASE( const mean_field::models::structure::PolytropicStructure *sourceStructureAddress = &sourceModel.structurePrescription(); - const mean_field::surface::ConstantPressureSurface *sourceSurfaceAddress = &sourceModel.surfacePrescription(); + const mean_field::surface::ConstantPressureSurface *sourceSurfaceAddress = &sourceModel.surfaceCondition(); const mean_field::eos::Polytrope *sourceEquationOfStateAddress = &sourceModel.equationOfState(); + const auto *sourceSurfaceDeformationAddress = &sourceModel.surfaceDeformationPrescription(); + const auto *sourceInteriorDeformationAddress = &sourceModel.stellarInteriorDeformationExtension(); + const auto *sourceVacuumDeformationAddress = &sourceModel.vacuumDeformationExtension(); const mean_field::eos::PressureValue sourceTargetPressure = sourceModel.compiledSurfaceConstraint().targetPressure(); @@ -272,10 +330,16 @@ TEST_CASE( CHECK(&destinationModel.structurePrescription() == sourceStructureAddress); - CHECK(&destinationModel.surfacePrescription() == sourceSurfaceAddress); + CHECK(&destinationModel.surfaceCondition() == sourceSurfaceAddress); CHECK(&destinationModel.equationOfState() == sourceEquationOfStateAddress); + CHECK(&destinationModel.surfaceDeformationPrescription() == sourceSurfaceDeformationAddress); + + CHECK(&destinationModel.stellarInteriorDeformationExtension() == sourceInteriorDeformationAddress); + + CHECK(&destinationModel.vacuumDeformationExtension() == sourceVacuumDeformationAddress); + CHECK(destinationModel.targetMass() == 1.25); CHECK(destinationModel.compiledSurfaceConstraint().targetPressure() == sourceTargetPressure); @@ -307,6 +371,13 @@ TEST_CASE( CHECK(views[0].targetMass() == 1.0); CHECK(views[1].targetMass() == 2.5); CHECK(views[1].surfaceCondition().targetPressure == 0.375); + CHECK(views[0].surfaceDeformation().name == "NodalRadialSurface"); + CHECK(views[0].surfaceDeformation().motionKind == mean_field::deformation::SurfaceMotionKind::Radial); + CHECK(views[0].stellarInteriorDeformation().name == "PowerLawRadialInteriorExtension"); + CHECK( + views[0].vacuumDeformation().outerBoundaryBehavior == + mean_field::deformation::VacuumOuterBoundaryBehavior::FixedAtReferenceInfinity + ); REQUIRE(views[1].surfaceDependencies().stateFields.size() == 1); CHECK( views[1].surfaceDependencies().residualRowField == @@ -350,6 +421,9 @@ TEST_CASE( REQUIRE(pressure.has_value()); CHECK(view.targetMass() == movedModel.targetMass()); + CHECK(view.surfaceDeformation() == movedModel.surfaceDeformationPrescription().descriptor()); + CHECK(view.stellarInteriorDeformation() == movedModel.stellarInteriorDeformationExtension().descriptor()); + CHECK(view.vacuumDeformation() == movedModel.vacuumDeformationExtension().descriptor()); CHECK( pressure->value() == mean_field::eos::evaluate( movedModel.equationOfState(), mean_field::eos::DensityValue{0.7} @@ -358,3 +432,48 @@ TEST_CASE( ); CHECK(seed.radius.Size() == 8); } + +TEST_CASE( + "Stellar Model Compiles Its Deformation Policies Against The Finite Element Discretization", + tags::stellar_model_deformation_compilation +) { + mean_field::utils::Args args = test_utils::setup_args(); + mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(fem.okay()); + + mfem::Vector referenceCenter(fem.mesh->SpaceDimension()); + referenceCenter = 0.0; + const mean_field::models::StellarModel model{ + 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::deformation::NodalRadialSurface{referenceCenter}, + mean_field::deformation::PowerLawRadialInteriorExtension{3.0}, + mean_field::deformation::FixedInfinityRadialVacuumExtension{} + }; + + auto prepared = model.compileDomainDeformation(fem); + STATIC_CHECK(mean_field::deformation::PreparedDomainDeformationOperator); + CHECK(prepared.matchesCurrentDiscretization()); + CHECK(prepared.stellarInteriorExtension().radialPower() == 3.0); + CHECK(prepared.discretizationDependencies().physicalMeshIdentity == fem.mesh.get()); + CHECK(prepared.discretizationDependencies().logicalReferenceMeshIdentity == fem.logicalReferenceMesh.get()); + CHECK(prepared.parameterCount() == prepared.surfaceDeformationPrescription().parameterCount()); + CHECK(prepared.volumeDisplacementSize() == fem.displacementFes->GetTrueVSize()); + + STATIC_CHECK_FALSE(std::is_copy_constructible_v); + STATIC_CHECK(std::is_nothrow_move_constructible_v); + mean_field::deformation::PreparedDomainDeformationRuntime runtime{std::move(prepared)}; + STATIC_CHECK(mean_field::deformation::PreparedDomainDeformationOperator); + mean_field::deformation::PreparedDomainDeformationRuntime movedRuntime{std::move(runtime)}; + + CHECK(movedRuntime.matchesCurrentDiscretization()); + CHECK(movedRuntime.parameterCount() > 0); + CHECK(movedRuntime.volumeDisplacementSize() == fem.displacementFes->GetTrueVSize()); + CHECK(movedRuntime.compositionReport().assignedScalarDofCount() == fem.surfaceDeformationFes->GetTrueVSize()); + + mfem::Vector zeroParameters(movedRuntime.parameterCount()); + mfem::Vector volumeDisplacement(movedRuntime.volumeDisplacementSize()); + zeroParameters = 0.0; + movedRuntime.buildVolumeDisplacement(zeroParameters, volumeDisplacement); + CHECK(volumeDisplacement.Norml2() == 0.0); +} diff --git a/tests/operators/prepared_stellar_equilibrium.cpp b/tests/operators/prepared_stellar_equilibrium.cpp index 13d4374..ab1a740 100644 --- a/tests/operators/prepared_stellar_equilibrium.cpp +++ b/tests/operators/prepared_stellar_equilibrium.cpp @@ -388,21 +388,21 @@ namespace stellar_equilibrium_test_utils { [[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} + .discretization = {.identity = 1009, .revision = 3}, + .density = {.identity = 1013, .revision = 5}, + .surfaceDeformation = {.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.surfaceDeformation.revision; ++dependencies.gravityGradient.revision; ++dependencies.gravityPotential.revision; ++dependencies.enthalpy.revision; @@ -424,7 +424,9 @@ namespace stellar_equilibrium_test_utils { assign_value_block(state, layout, densityValue, reducedDensity); } - assign_value_block(state, layout, displacementValue, project_displacement(f, 0.63)); + mfem::Vector surfaceDeformation(layout.size(displacementValue)); + surfaceDeformation = 0.0; + assign_value_block(state, layout, displacementValue, surfaceDeformation); assign_value_block(state, layout, gravityGradientValue, project_gravity_gradient(f, 0.43)); assign_value_block(state, layout, gravityPotentialValue, project_gravity_potential(f, 0.47)); @@ -454,7 +456,11 @@ namespace stellar_equilibrium_test_utils { assign_value_block(direction, layout, densityValue, reducedDensityDirection); } - assign_value_block(direction, layout, displacementValue, project_displacement_direction(f, 0.79)); + mfem::Vector surfaceDeformationDirection(layout.size(displacementValue)); + for (int parameter = 0; parameter < surfaceDeformationDirection.Size(); ++parameter) { + surfaceDeformationDirection(parameter) = 0.11 * std::cos(0.41 * static_cast(parameter) + 0.79); + } + assign_value_block(direction, layout, displacementValue, surfaceDeformationDirection); assign_value_block(direction, layout, gravityGradientValue, project_gravity_direction(f, 0.83)); assign_value_block(direction, layout, gravityPotentialValue, project_potential_direction(f, 0.89)); @@ -497,7 +503,7 @@ namespace stellar_equilibrium_test_utils { ) { 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 &displacement = stellarOperator.GetGeneratedVolumeDisplacement(); const mfem::Vector gravityGradient = const_value_view(state, layout, gravityGradientValue); const mfem::Vector gravityPotential = const_value_view(state, layout, gravityPotentialValue); @@ -513,7 +519,10 @@ namespace stellar_equilibrium_test_utils { stellarOperator.GetGravityOperator().Mult(gravityState, gravity); stellarOperator.GetBarotropicClosureOperator().BuildResidual(closure); stellarOperator.GetDisplacementOperator().BuildResidual(displacementResidualValue); - stellarOperator.GetCenteringConstraintOperator().ApplyResidualRows(displacementResidualValue); + mfem::Vector surfaceShapeResidualValue(stellarOperator.GetDomainDeformation().parameterCount()); + stellarOperator.GetDomainDeformation().applyJacobianTranspose( + stellarOperator.GetSurfaceDeformationParameters(), displacementResidualValue, surfaceShapeResidualValue + ); stellarOperator.GetHydrostaticOperator().BuildResidual(hydrostatic); stellarOperator.GetSurfaceConstraintOperator().ApplyResidualRows(hydrostatic); stellarOperator.GetMassNormalizationOperator().BuildResidual(mass); @@ -536,7 +545,7 @@ namespace stellar_equilibrium_test_utils { residual_view(result, layout, densityResidual) = closure; - residual_view(result, layout, displacementResidual) = displacementResidualValue; + residual_view(result, layout, displacementResidual) = surfaceShapeResidualValue; residual_view(result, layout, enthalpyResidual) = hydrostatic; @@ -551,8 +560,12 @@ namespace stellar_equilibrium_test_utils { ) { 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 reducedDensityDirection = const_value_view(direction, layout, densityValue); + const mfem::Vector surfaceDeformationDirection = const_value_view(direction, layout, displacementValue); + mfem::Vector displacementDirection(stellarOperator.GetDomainDeformation().volumeDisplacementSize()); + stellarOperator.GetDomainDeformation().applyJacobian( + stellarOperator.GetSurfaceDeformationParameters(), surfaceDeformationDirection, displacementDirection + ); 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); @@ -578,7 +591,16 @@ namespace stellar_equilibrium_test_utils { reducedDensityDirection, displacementDirection, gravityGradientDirection, reducedEnthalpyDirection, displacementAction ); - stellarOperator.GetCenteringConstraintOperator().ApplyJacobianRows(displacementDirection, displacementAction); + mfem::Vector surfaceShapeAction(stellarOperator.GetDomainDeformation().parameterCount()); + stellarOperator.GetDomainDeformation().applyJacobianTranspose( + stellarOperator.GetSurfaceDeformationParameters(), displacementAction, surfaceShapeAction + ); + mfem::Vector pullbackDerivative(stellarOperator.GetDomainDeformation().parameterCount()); + stellarOperator.GetDomainDeformation().applyPullbackDerivative( + stellarOperator.GetSurfaceDeformationParameters(), surfaceDeformationDirection, + stellarOperator.GetFullMechanicalResidual(), pullbackDerivative + ); + surfaceShapeAction += pullbackDerivative; stellarOperator.GetHydrostaticOperator().ApplyCompleteJacobianAction( reducedEnthalpyDirection, gravityPotentialDirection, bernoulliDirection(0), displacementDirection, @@ -608,7 +630,7 @@ namespace stellar_equilibrium_test_utils { residual_view(result, layout, densityResidual) = closureAction; - residual_view(result, layout, displacementResidual) = displacementAction; + residual_view(result, layout, displacementResidual) = surfaceShapeAction; residual_view(result, layout, enthalpyResidual) = hydrostaticAction; @@ -644,8 +666,8 @@ namespace stellar_equilibrium_test_utils { } // 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 + "Prepared Stellar Equilibrium Uses Surface Parameters For Its Root Geometry Block", + tags::reduced_stellar_geometry &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); @@ -660,12 +682,22 @@ TEST_CASE( ); CHECK(stellarOperator.GetTargetMass() == stellarModel.targetMass()); + CHECK(stellarOperator.GetDomainDeformation().matchesCurrentDiscretization()); + const mean_field::field::ScalarBoundaryDofMap surfaceDeformationMap = + mean_field::field::make_stellar_surface_scalar_dof_map( + *f.surfaceDeformationFes + ); + CHECK(stellarOperator.GetDomainDeformation().parameterCount() == surfaceDeformationMap.local_size()); + CHECK(stellarOperator.GetDomainDeformation().volumeDisplacementSize() == f.displacementFes->GetTrueVSize()); const auto &layout = stellarOperator.GetLayout(); const stellar_equilibrium_test_utils::FieldMaps maps(f); CHECK(layout.size(stellar_equilibrium_test_utils::densityValue) == maps.density.reduced_size()); - CHECK(layout.size(stellar_equilibrium_test_utils::displacementValue) == maps.displacement.reduced_size()); + CHECK( + layout.size(stellar_equilibrium_test_utils::displacementValue) == + stellarOperator.GetDomainDeformation().parameterCount() + ); 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()); @@ -676,7 +708,10 @@ TEST_CASE( 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::displacementResidual) == + stellarOperator.GetDomainDeformation().parameterCount() + ); CHECK(layout.size(stellar_equilibrium_test_utils::enthalpyResidual) == maps.enthalpy.reduced_size()); CHECK(layout.size(stellar_equilibrium_test_utils::massResidual) == 1); @@ -715,9 +750,8 @@ TEST_CASE( } TEST_CASE( - "Prepared Stellar Equilibrium Jacobian Is The Exact Restricted Full " - "Child Jacobian", - tags::barotrope_prepared_jacobian_accuracy &tags::field + "Prepared Stellar Equilibrium Jacobian Is The Lifted And Pulled Back Full Child Jacobian", + tags::barotrope_prepared_jacobian_accuracy &tags::reduced_stellar_geometry &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); @@ -834,8 +868,8 @@ TEST_CASE( } TEST_CASE( - "Prepared Stellar Equilibrium Replaces Center Force Rows With A Translational Centering Constraint", - tags::translational_centering_enforcement + "Reduced Stellar Geometry Uses Surface Parameters And Generates An Orientation Preserving Volume Map", + tags::reduced_stellar_geometry &tags::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); @@ -847,75 +881,25 @@ TEST_CASE( f, *f.domainMapperStateless, stellarModel ); - const auto &layout = stellarOperator.GetLayout(); - mfem::Vector state = stellar_equilibrium_test_utils::make_state(f, layout); + const auto &layout = stellarOperator.GetLayout(); + const 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( + 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()); + const int surfaceParameterCount = stellarOperator.GetDomainDeformation().parameterCount(); + const int volumeDisplacementSize = stellarOperator.GetDomainDeformation().volumeDisplacementSize(); - 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); - } + CHECK(layout.size(stellar_equilibrium_test_utils::displacementValue) == surfaceParameterCount); + CHECK(layout.size(stellar_equilibrium_test_utils::displacementResidual) == surfaceParameterCount); + CHECK(surfaceParameterCount < volumeDisplacementSize); + CHECK(stellarOperator.GetSurfaceDeformationParameters().Size() == surfaceParameterCount); + CHECK(stellarOperator.GetGeneratedVolumeDisplacement().Size() == volumeDisplacementSize); + CHECK(report.generatedVolumeDisplacement); + CHECK(report.generatedGeometry.isOrientationPreserving()); + CHECK(report.generatedDisplacement.identity != 0); + CHECK(report.generatedDisplacement.revision == 1); } TEST_CASE( @@ -949,7 +933,7 @@ TEST_CASE( CHECK(report.displacement.DidAnyWork()); CHECK(report.massNormalization.DidAnyWork()); CHECK(report.surfaceConstraint.DidAnyWork()); - CHECK(report.centeringConstraint.DidAnyWork()); + CHECK(report.generatedVolumeDisplacement); CHECK(report.assembledResidual); CHECK(stellarOperator.IsPrepared()); @@ -1227,7 +1211,7 @@ TEST_CASE( TEST_CASE( "Prepared Stellar Equilibrium Complete Jacobian Matches Every " "Coupled Centered Difference Block", - tags::barotrope &tags::prepared &tags::jacobian &tags::accuracy &tags::geometry + tags::barotrope &tags::prepared &tags::jacobian &tags::accuracy &tags::reduced_stellar_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); @@ -1398,7 +1382,7 @@ TEST_CASE( TEST_CASE( "Prepared Stellar Equilibrium Selectively Invalidates Rows And Never " "Reprepares In Krylov Mult", - tags::barotrope &tags::prepared &tags::contexts &tags::mfem_operators + tags::barotrope &tags::prepared &tags::contexts &tags::mfem_operators &tags::reduced_stellar_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); @@ -1423,10 +1407,14 @@ TEST_CASE( stellarOperator.GetDisplacementOperator().GetResidualPreparationCount(); const std::uint64_t massPreparations = stellarOperator.GetMassNormalizationOperator().GetPreparationCount(); const std::uint64_t rootAssemblies = stellarOperator.GetStatistics().residualAssemblies; + const std::uint64_t geometryBuilds = stellarOperator.GetStatistics().generatedGeometryBuilds; + const auto generatedDisplacement = stellarOperator.GetGeneratedDisplacementDependency(); const auto repeated = stellarOperator.Prepare(state, dependencies, rotation); CHECK_FALSE(repeated.DidAnyWork()); CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies); + CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds); + CHECK(stellarOperator.GetGeneratedDisplacementDependency() == generatedDisplacement); const mfem::Vector direction = stellar_equilibrium_test_utils::make_direction(f, layout); mfem::Vector action; @@ -1440,6 +1428,26 @@ TEST_CASE( CHECK(stellarOperator.GetMassNormalizationOperator().GetPreparationCount() == massPreparations); CHECK(stellarOperator.GetStatistics().residualAssemblies == rootAssemblies); CHECK(stellarOperator.GetStatistics().jacobianApplications == 3); + CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds); + + mfem::Vector surfaceDeformation = + stellar_equilibrium_test_utils::value_view(state, layout, stellar_equilibrium_test_utils::displacementValue); + for (int parameter = 0; parameter < surfaceDeformation.Size(); ++parameter) { + surfaceDeformation(parameter) += 1.0e-4; + } + ++dependencies.surfaceDeformation.revision; + + const auto surfaceDeformationReport = stellarOperator.Prepare(state, dependencies, rotation); + CHECK(surfaceDeformationReport.generatedVolumeDisplacement); + CHECK(surfaceDeformationReport.generatedGeometry.isOrientationPreserving()); + CHECK(surfaceDeformationReport.generatedDisplacement.identity == generatedDisplacement.identity); + CHECK(surfaceDeformationReport.generatedDisplacement.revision == generatedDisplacement.revision + 1); + CHECK(stellarOperator.GetStatistics().generatedGeometryBuilds == geometryBuilds + 1); + CHECK(surfaceDeformationReport.gravity.DidAnyWork()); + CHECK(surfaceDeformationReport.barotropicClosure.DidAnyWork()); + CHECK(surfaceDeformationReport.hydrostatic.DidAnyWork()); + CHECK(surfaceDeformationReport.displacement.DidAnyWork()); + CHECK(surfaceDeformationReport.massNormalization.DidAnyWork()); 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)); @@ -1476,8 +1484,79 @@ TEST_CASE( } TEST_CASE( - "Prepared Stellar Equilibrium Matches The Analytic N1 Lane Emden " - "State Up To The Mixed Projection Floor", + "Accepted Reduced Geometries Remain Valid Across Prepared Stellar Physics Quadrature Rules", + tags::reduced_stellar_geometry &tags::prepared &tags::geometry &tags::self_consistency +) { + 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); + const auto stellarModel = stellar_equilibrium_test_utils::make_stellar_model(barotrope, 1.15); + auto parameterGeometry = stellarModel.compileDomainDeformation(f); + const auto &surface = parameterGeometry.surfaceDeformationPrescription(); + + 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(); + + mfem::Vector uniformExpansion(surface.parameterCount()); + mfem::Vector uniformContraction(surface.parameterCount()); + mfem::Vector oblateSurface(surface.parameterCount()); + uniformExpansion = 1.0e-2; + uniformContraction = -1.0e-2; + for (int parameter = 0; parameter < surface.parameterCount(); ++parameter) { + const double polarDirection = surface.radialDirection(parameter, 2); + oblateSurface(parameter) = + 1.0e-2 * surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection); + } + + const std::array shapes{ + &uniformExpansion, + &uniformContraction, + &oblateSurface, + }; + + for (int shape = 0; shape < static_cast(shapes.size()); ++shape) { + stellar_equilibrium_test_utils::assign_value_block( + state, layout, stellar_equilibrium_test_utils::displacementValue, *shapes[shape] + ); + if (shape > 0) { + ++dependencies.surfaceDeformation.revision; + } + + const auto report = + stellarOperator.Prepare(state, dependencies, stellar_equilibrium_test_utils::make_zero_rotation()); + + CAPTURE(shape); + CHECK(report.generatedVolumeDisplacement); + CHECK(report.generatedGeometry.isOrientationPreserving()); + CHECK(std::isfinite(report.generatedGeometry.minimumJacobianDeterminant)); + + const auto independentGeometry = stellarOperator.GetDomainDeformation().inspectMappedGeometry( + stellarOperator.GetGeneratedVolumeDisplacement() + ); + CHECK(independentGeometry.isOrientationPreserving()); + CHECK( + std::abs( + independentGeometry.minimumJacobianDeterminant - report.generatedGeometry.minimumJacobianDeterminant + ) <= 64.0 * std::numeric_limits::epsilon() * + std::max(1.0, std::abs(report.generatedGeometry.minimumJacobianDeterminant)) + ); + + mfem::Vector residual; + stellarOperator.BuildResidual(residual); + for (int entry = 0; entry < residual.Size(); ++entry) { + REQUIRE(std::isfinite(residual(entry))); + } + } +} + +TEST_CASE( + "Reduced Stellar Equilibrium Preserves The Analytic N1 Floor Virtual Work And Rotational Shape Descent", tags::barotrope &tags::prepared &tags::analytic_comparison &tags::accuracy &tags::gravity &tags::hydro &tags::residuals ) { @@ -1910,6 +1989,159 @@ TEST_CASE( CHECK(analyticMassError < 5.0e-5 * targetMass); CHECK(analyticMassError < 0.10 * perturbedMassError); + + /* + * Return to the analytic spherical state before testing the reduced + * mechanical dual and its rotational shape response. + */ + stellar_equilibrium_test_utils::increment_all_state_revisions(dependencies); + stellarOperator.Prepare(analyticState, dependencies, zeroRotation); + + auto parameterGeometry = stellarModel.compileDomainDeformation(f); + const auto &surface = parameterGeometry.surfaceDeformationPrescription(); + + mfem::Vector oblateSurfaceDirection(surface.parameterCount()); + for (int parameter = 0; parameter < surface.parameterCount(); ++parameter) { + const double polarDirection = surface.radialDirection(parameter, 2); + oblateSurfaceDirection(parameter) = + surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection); + } + + const mfem::Vector surfaceParameters = stellar_equilibrium_test_utils::const_value_view( + analyticState, layout, stellar_equilibrium_test_utils::displacementValue + ); + mfem::Vector liftedOblateDirection(parameterGeometry.volumeDisplacementSize()); + parameterGeometry.applyJacobian(surfaceParameters, oblateSurfaceDirection, liftedOblateDirection); + + mfem::Vector nonrotatingResidual; + stellarOperator.BuildResidual(nonrotatingResidual); + const mfem::Vector nonrotatingShapeResidual = stellar_equilibrium_test_utils::const_residual_view( + nonrotatingResidual, layout, stellar_equilibrium_test_utils::displacementResidual + ); + + const double reducedVirtualWork = + gravity_prepared_test_utils::global_dot(oblateSurfaceDirection, nonrotatingShapeResidual, f.mesh->GetComm()); + const double volumeVirtualWork = gravity_prepared_test_utils::global_dot( + liftedOblateDirection, stellarOperator.GetFullMechanicalResidual(), f.mesh->GetComm() + ); + const double virtualWorkScale = std::max({1.0, std::abs(reducedVirtualWork), std::abs(volumeVirtualWork)}); + + INFO("Reduced mechanical virtual work = " << reducedVirtualWork); + INFO("Lifted volume mechanical virtual work = " << volumeVirtualWork); + CHECK(std::abs(reducedVirtualWork - volumeVirtualWork) <= 2.0e-12 * virtualWorkScale); + + const double keplerianAngularSpeed = + std::sqrt(mean_field::utils::G * targetMass / (stellarRadius * stellarRadius * stellarRadius)); + const double angularSpeed = 0.25 * 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); + + ++dependencies.rotation.revision; + stellarOperator.Prepare(analyticState, dependencies, rotation); + + mfem::Vector rotatingSphericalResidual; + stellarOperator.BuildResidual(rotatingSphericalResidual); + const mfem::Vector rotatingShapeResidual = stellar_equilibrium_test_utils::const_residual_view( + rotatingSphericalResidual, layout, stellar_equilibrium_test_utils::displacementResidual + ); + mfem::Vector rotationInducedShapeResidual(rotatingShapeResidual); + rotationInducedShapeResidual -= nonrotatingShapeResidual; + + const double rotationInducedWork = gravity_prepared_test_utils::global_dot( + rotationInducedShapeResidual, oblateSurfaceDirection, f.mesh->GetComm() + ); + const double rotationInducedNorm = + stellar_equilibrium_test_utils::global_norm(rotationInducedShapeResidual, f.mesh->GetComm()); + const double oblateDirectionNorm = + stellar_equilibrium_test_utils::global_norm(oblateSurfaceDirection, f.mesh->GetComm()); + const double rotationWorkScale = rotationInducedNorm * oblateDirectionNorm; + + INFO("Rotation-induced reduced shape residual norm = " << rotationInducedNorm); + INFO("Rotation-induced work against the oblate surface direction = " << rotationInducedWork); + REQUIRE(rotationInducedNorm > 0.0); + REQUIRE(oblateDirectionNorm > 0.0); + REQUIRE(rotationWorkScale > 0.0); + CHECK(rotationInducedWork < -1.0e-3 * rotationWorkScale); + + mfem::Vector oblateDirection(layout.value_offsets().Last()); + oblateDirection = 0.0; + stellar_equilibrium_test_utils::assign_value_block( + oblateDirection, layout, stellar_equilibrium_test_utils::displacementValue, oblateSurfaceDirection + ); + + const auto deformationStatisticsBefore = stellarOperator.GetDomainDeformation().actionStatistics(); + mfem::Vector oblateJacobianAction; + stellarOperator.Mult(oblateDirection, oblateJacobianAction); + const auto deformationStatisticsAfter = stellarOperator.GetDomainDeformation().actionStatistics(); + CHECK( + deformationStatisticsAfter.pullbackDerivativeApplications == + deformationStatisticsBefore.pullbackDerivativeApplications + 1 + ); + + const mfem::Vector oblateShapeJacobianAction = stellar_equilibrium_test_utils::const_residual_view( + oblateJacobianAction, layout, stellar_equilibrium_test_utils::displacementResidual + ); + /* + * Test descent for the rotation-induced departure from the nonrotating + * state. The analytic finite-element state has a nonzero projection floor, + * so minimizing the absolute residual would mix that unrelated floor into + * the rotational response. The sign of the best correction along this + * single trial coordinate is deliberately not prescribed: a shape-only + * correction holds the thermodynamic and gravity unknowns fixed, whereas + * the physical oblate equilibrium is a coupled response of every block. + */ + const double residualDirectionalDerivative = gravity_prepared_test_utils::global_dot( + rotationInducedShapeResidual, oblateShapeJacobianAction, f.mesh->GetComm() + ); + const double jacobianDirectionNormSquared = gravity_prepared_test_utils::global_dot( + oblateShapeJacobianAction, oblateShapeJacobianAction, f.mesh->GetComm() + ); + + REQUIRE(std::isfinite(residualDirectionalDerivative)); + REQUIRE(jacobianDirectionNormSquared > 0.0); + REQUIRE(std::abs(residualDirectionalDerivative) > 1.0e-12 * rotationWorkScale); + + const double optimalLinearizedAmplitude = -residualDirectionalDerivative / jacobianDirectionNormSquared; + const double appliedShapeAmplitude = + std::copysign(std::min(0.25 * std::abs(optimalLinearizedAmplitude), 2.0e-2), optimalLinearizedAmplitude); + REQUIRE(appliedShapeAmplitude != 0.0); + + mfem::Vector predictedShapeResidual(rotationInducedShapeResidual); + predictedShapeResidual.Add(appliedShapeAmplitude, oblateShapeJacobianAction); + const double predictedShapeNorm = + stellar_equilibrium_test_utils::global_norm(predictedShapeResidual, f.mesh->GetComm()); + CHECK(predictedShapeNorm < rotationInducedNorm); + + mfem::Vector correctedShapeState(analyticState); + stellar_equilibrium_test_utils::value_view( + correctedShapeState, layout, stellar_equilibrium_test_utils::displacementValue + ) + .Add(appliedShapeAmplitude, oblateSurfaceDirection); + ++dependencies.surfaceDeformation.revision; + stellarOperator.Prepare(correctedShapeState, dependencies, rotation); + + mfem::Vector nonlinearCorrectedResidual; + stellarOperator.BuildResidual(nonlinearCorrectedResidual); + mfem::Vector nonlinearShapeDeparture( + stellar_equilibrium_test_utils::const_residual_view( + nonlinearCorrectedResidual, layout, stellar_equilibrium_test_utils::displacementResidual + ) + ); + nonlinearShapeDeparture -= nonrotatingShapeResidual; + const double nonlinearShapeDepartureNorm = + stellar_equilibrium_test_utils::global_norm(nonlinearShapeDeparture, f.mesh->GetComm()); + + INFO("Optimal linearized shape amplitude = " << optimalLinearizedAmplitude); + INFO("Applied shape amplitude = " << appliedShapeAmplitude); + INFO("Rotation-induced shape residual norm = " << rotationInducedNorm); + INFO("Predicted corrected rotational departure norm = " << predictedShapeNorm); + INFO("Nonlinear corrected rotational departure norm = " << nonlinearShapeDepartureNorm); + CHECK(nonlinearShapeDepartureNorm < rotationInducedNorm); } TEST_CASE( @@ -2558,7 +2790,7 @@ TEST_CASE( displacementBlock.Add(appliedOblateAmplitude, oblateDisplacement); } - ++dependencies.displacement.revision; + ++dependencies.surfaceDeformation.revision; stellarOperator.Prepare(oblateState, dependencies, rotation); diff --git a/tests/physics/gravity_monopole_accuracy.cpp b/tests/physics/gravity_monopole_accuracy.cpp index e0ff24e..685f428 100644 --- a/tests/physics/gravity_monopole_accuracy.cpp +++ b/tests/physics/gravity_monopole_accuracy.cpp @@ -988,7 +988,7 @@ TEST_CASE( 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-6; CHECK(binding_error < virial_target); CHECK(virial_error < virial_target); diff --git a/tests/surface/constant_surface_compilation.cpp b/tests/surface/constant_surface_compilation.cpp index 4660250..18106cd 100644 --- a/tests/surface/constant_surface_compilation.cpp +++ b/tests/surface/constant_surface_compilation.cpp @@ -191,7 +191,7 @@ namespace { TEST_CASE( "Constant Pressure Surface Prescribes Only A Pressure Quantity", - tags::surface_prescription_type_contract + tags::surface_condition_type_contract ) { STATIC_CHECK(std::same_as); STATIC_CHECK(std::constructible_from); diff --git a/tests/test_helpers.cppm b/tests/test_helpers.cppm index 868d949..c2a74ff 100644 --- a/tests/test_helpers.cppm +++ b/tests/test_helpers.cppm @@ -303,22 +303,68 @@ export namespace field_dof_test_utils { } // 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 deformation = geometry & solver & make_tag("deformation"); + inline constexpr auto deformation_type_contract = deformation & unit & make_tag("type_contract"); + inline constexpr auto surface_deformation = deformation & surface & make_tag("surface_deformation"); + inline constexpr auto interior_deformation_extension = deformation & make_tag("interior_extension"); + inline constexpr auto vacuum_deformation_extension = deformation & make_tag("vacuum_extension"); + inline constexpr auto deformation_pullback = deformation & make_tag("pullback"); + inline constexpr auto surface_deformation_type_contract = deformation & surface & unit & + make_tag("surface_deformation") & + make_tag("type_contract") & make_tag("pullback"); + inline constexpr auto interior_deformation_extension_type_contract = + deformation & unit & make_tag("interior_extension") & make_tag("type_contract") & make_tag("pullback"); + inline constexpr auto vacuum_deformation_extension_type_contract = + deformation & unit & make_tag("vacuum_extension") & make_tag("type_contract") & make_tag("pullback"); + inline constexpr auto surface_deformation_dof = + surface & geometry & mesh & make_tag("surface_deformation") & make_tag("dof"); + inline constexpr auto surface_deformation_dof_unit = surface_deformation_dof & unit; + inline constexpr auto surface_deformation_dof_topology = + surface_deformation_dof & integration & make_tag("topology"); + inline constexpr auto surface_deformation_dof_schema = surface_deformation_dof & integration & make_tag("schema"); + inline constexpr auto surface_deformation_dof_parallel = + surface_deformation_dof & integration & make_tag("parallel"); + inline constexpr auto nodal_radial_surface = + surface & geometry & make_tag("surface_deformation") & make_tag("nodal_radial"); + inline constexpr auto nodal_radial_surface_validation = nodal_radial_surface & unit & make_tag("validation"); + inline constexpr auto nodal_radial_surface_analytic = + nodal_radial_surface & integration & accuracy & make_tag("analytic_comparison"); + inline constexpr auto nodal_radial_surface_linearization = + nodal_radial_surface & integration & make_tag("jacobian") & make_tag("adjoint"); + inline constexpr auto radial_deformation_extension = deformation & mapping & make_tag("radial_extension"); + inline constexpr auto radial_deformation_extension_validation = + radial_deformation_extension & unit & make_tag("validation"); + inline constexpr auto radial_deformation_extension_analytic = + radial_deformation_extension & integration & accuracy & make_tag("analytic_comparison"); + inline constexpr auto radial_deformation_extension_linearization = + radial_deformation_extension & integration & make_tag("jacobian") & make_tag("adjoint"); + inline constexpr auto radial_deformation_extension_mapping = + radial_deformation_extension & integration & make_tag("determinant"); + inline constexpr auto domain_deformation = deformation & mapping & solver & make_tag("domain_deformation"); + inline constexpr auto domain_deformation_type_contract = domain_deformation & unit & make_tag("type_contract"); + inline constexpr auto domain_deformation_composition = domain_deformation & integration & make_tag("composition"); + inline constexpr auto domain_deformation_linearization = + domain_deformation & integration & make_tag("jacobian") & make_tag("adjoint"); + inline constexpr auto domain_deformation_geometry = + domain_deformation & integration & geometry & make_tag("determinant"); + inline constexpr auto reduced_stellar_geometry = + domain_deformation & integration & physics & make_tag("reduced_coordinates"); inline constexpr auto field = sub_tag(mesh & physics, "field"); inline constexpr auto field_dof = field & make_tag("dof"); @@ -412,10 +458,13 @@ export namespace tags { 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 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 stellar_model_deformation_ownership = model & deformation & unit & make_tag("ownership"); + inline constexpr auto stellar_model_deformation_compilation = + model & deformation & integration & make_tag("compilation"); + inline constexpr auto surface_condition_type_contract = + surface & physics & unit & make_tag("condition") & 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; diff --git a/tests/utils/blocks.cpp b/tests/utils/blocks.cpp index 04c6a5e..a304606 100644 --- a/tests/utils/blocks.cpp +++ b/tests/utils/blocks.cpp @@ -526,6 +526,41 @@ TEST_CASE( CHECK(true); } +TEST_CASE( + "Reduced Stellar Equilibrium Form Encodes Surface Shape Coordinates And Couplings", + tags::reduced_stellar_geometry &tags::unit &tags::utils +) { + using form = blocks::surface_deformed_stellar_equilibrium_form; + using jacobian = blocks::surface_deformed_stellar_equilibrium_jacobian_form; + + constexpr auto surface_parameters = + blocks::get_value_block(blocks::surface_deformation_field.parameters_term); + constexpr auto surface_shape = + blocks::get_residual_block(blocks::surface_deformation_field.shape_equilibrium_term); + + STATIC_REQUIRE(form::value_block_count == 6); + STATIC_REQUIRE(form::residual_block_count == 6); + STATIC_REQUIRE(static_cast(surface_parameters) == 1); + STATIC_REQUIRE(static_cast(surface_shape) == 3); + STATIC_REQUIRE((blocks::valid_jacobian_form)); + + STATIC_REQUIRE( + blocks::has_jacobian_coupling_v< + blocks::gravity::gradient::residual, blocks::surface_deformation::parameters::value, jacobian> + ); + STATIC_REQUIRE( + blocks::has_jacobian_coupling_v< + blocks::surface_deformation::shape_equilibrium::residual, blocks::surface_deformation::parameters::value, + jacobian> + ); + STATIC_REQUIRE_FALSE(blocks::contains_type_v); + STATIC_REQUIRE_FALSE( + blocks::contains_type_v + ); + + CHECK(true); +} + TEST_CASE( "Block Validators Reject Structurally Malformed Forms", tags::unit &tags::solver &tags::utils @@ -623,4 +658,4 @@ TEST_CASE( const std::array invalid_value_sizes{11, 13, 17, 19, 23, 2}; CHECK_THROWS(blocks::form_layout(invalid_value_sizes, residual_sizes)); -} \ No newline at end of file +}