From 25510008ddf9810eaf9821149734d526c76b087a Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Wed, 2 Sep 2026 17:01:50 -0400 Subject: [PATCH] perf(jacobian-action): major updates to jacobian action application by removing redudant quadrature work. ~5x increase in speed --- CMakeLists.txt | 32 + experiments/README.md | 39 ++ experiments/coupled_gauge_modes.cpp | 87 +++ experiments/preconditioning_diagnostics.cpp | 455 +++++++++++++ libmeanfield/impl/mapping/domain_mapper.cpp | 72 +- libmeanfield/impl/mapping/transformations.cpp | 28 +- libmeanfield/impl/models/polytropic.cpp | 234 +------ .../impl/operators/gravity_field_jacobian.cpp | 11 +- .../kernels/barotropic_closure_kernels.cpp | 27 +- .../operators/kernels/gravity_kernels.cpp | 4 +- .../kernels/pressure_force_kernels.cpp | 14 +- .../operators/prepared_barotropic_closure.cpp | 114 +++- ...ed_central_density_stellar_equilibrium.cpp | 223 ++++++ .../prepared_gravity_displacement_force.cpp | 301 ++++++++- .../operators/prepared_gravity_source.cpp | 114 ++++ .../impl/operators/prepared_hdiv_mass.cpp | 303 ++++++++- .../prepared_hydrostatic_equilibrium.cpp | 7 +- .../operators/prepared_mass_normalization.cpp | 198 +++++- .../operators/prepared_pressure_force.cpp | 58 +- .../prepared_rotation_displacement_force.cpp | 266 +++++++- .../prepared_stellar_equilibrium.cpp | 318 ++++----- libmeanfield/impl/physics/gravity.cpp | 35 +- libmeanfield/impl/seed/lane_emden.cpp | 248 +++++++ .../seed/stellar_equilibrium_projection.cpp | 209 ++++++ .../solver/preconditioning_diagnostics.cpp | 638 ++++++++++++++++++ .../interface/dimensions/quantities.cppm | 312 +++++++++ libmeanfield/interface/eos/concepts.cppm | 4 +- libmeanfield/interface/eos/polytropic.cppm | 98 +-- .../interface/eos/pressure_surface.cppm | 22 +- libmeanfield/interface/eos/quantities.cppm | 146 +--- libmeanfield/interface/eos/relations.cppm | 12 +- .../equilibrium/stellar_discretization.cppm | 64 ++ .../interface/field/field_registry.cppm | 23 + .../interface/mapping/domain_mapper.cppm | 18 +- libmeanfield/interface/mean_field.cppm | 15 + .../compiled_fixed_central_density.cppm | 95 +++ .../interface/models/compiled_fixed_mass.cppm | 115 ++++ .../interface/models/specifications.cppm | 481 +++++++++++++ .../models/structure/polytropic.cppm | 37 +- .../interface/models/typed_stellar_model.cppm | 66 ++ .../prepared_barotropic_closure_operator.cppm | 17 + .../operators/prepared_central_density.cppm | 264 ++++++++ ...d_central_density_stellar_equilibrium.cppm | 117 ++++ .../operators/prepared_constraint.cppm | 37 + .../prepared_gravity_displacement_force.cppm | 47 ++ .../operators/prepared_gravity_source.cppm | 19 + .../operators/prepared_hdiv_mass.cppm | 36 + .../prepared_mass_normalization.cppm | 62 ++ .../prepared_rotation_displacement_force.cppm | 35 + .../prepared_stellar_equilibrium.cppm | 50 +- .../prepared_surface_constraint.cppm | 14 +- .../interface/operators/root_manifest.cppm | 601 +++++++++++++++++ .../stellar_equilibrium_problem.cppm | 210 ++++++ .../operators/stellar_equilibrium_system.cppm | 26 + libmeanfield/interface/physics/gravity.cppm | 13 + libmeanfield/interface/seed/lane_emden.cppm | 120 ++++ .../seed/stellar_equilibrium_projection.cppm | 136 ++++ .../solver/preconditioning_diagnostics.cppm | 259 +++++++ libmeanfield/interface/surface/compiled.cppm | 4 +- libmeanfield/interface/surface/constant.cppm | 13 +- libmeanfield/interface/utils/blocks.cppm | 108 ++- tests/models/model_specifications.cpp | 196 ++++++ tests/models/typed_stellar_model.cpp | 108 +++ tests/operators/prepared_central_density.cpp | 164 +++++ ...ed_central_density_stellar_equilibrium.cpp | 193 ++++++ .../operators/prepared_mass_normalization.cpp | 72 ++ .../prepared_stellar_equilibrium.cpp | 93 +-- tests/operators/root_manifest.cpp | 204 ++++++ .../operators/stellar_equilibrium_system.cpp | 163 +++++ tests/physics/dimensional_quantities.cpp | 119 ++++ tests/seed/lane_emden.cpp | 237 +++++++ tests/seed/stellar_equilibrium_projection.cpp | 189 ++++++ tests/solver/preconditioning_diagnostics.cpp | 282 ++++++++ tests/test_helpers.cppm | 30 +- 74 files changed, 8967 insertions(+), 814 deletions(-) create mode 100644 experiments/preconditioning_diagnostics.cpp create mode 100644 libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp create mode 100644 libmeanfield/impl/seed/lane_emden.cpp create mode 100644 libmeanfield/impl/seed/stellar_equilibrium_projection.cpp create mode 100644 libmeanfield/impl/solver/preconditioning_diagnostics.cpp create mode 100644 libmeanfield/interface/dimensions/quantities.cppm create mode 100644 libmeanfield/interface/equilibrium/stellar_discretization.cppm create mode 100644 libmeanfield/interface/models/compiled_fixed_central_density.cppm create mode 100644 libmeanfield/interface/models/compiled_fixed_mass.cppm create mode 100644 libmeanfield/interface/models/specifications.cppm create mode 100644 libmeanfield/interface/models/typed_stellar_model.cppm create mode 100644 libmeanfield/interface/operators/prepared_central_density.cppm create mode 100644 libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm create mode 100644 libmeanfield/interface/operators/prepared_constraint.cppm create mode 100644 libmeanfield/interface/operators/root_manifest.cppm create mode 100644 libmeanfield/interface/operators/stellar_equilibrium_problem.cppm create mode 100644 libmeanfield/interface/operators/stellar_equilibrium_system.cppm create mode 100644 libmeanfield/interface/seed/lane_emden.cppm create mode 100644 libmeanfield/interface/seed/stellar_equilibrium_projection.cppm create mode 100644 libmeanfield/interface/solver/preconditioning_diagnostics.cppm create mode 100644 tests/models/model_specifications.cpp create mode 100644 tests/models/typed_stellar_model.cpp create mode 100644 tests/operators/prepared_central_density.cpp create mode 100644 tests/operators/prepared_central_density_stellar_equilibrium.cpp create mode 100644 tests/operators/root_manifest.cpp create mode 100644 tests/operators/stellar_equilibrium_system.cpp create mode 100644 tests/physics/dimensional_quantities.cpp create mode 100644 tests/seed/lane_emden.cpp create mode 100644 tests/seed/stellar_equilibrium_projection.cpp create mode 100644 tests/solver/preconditioning_diagnostics.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c524629..edccfb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(stroid REQUIRED IMPORTED_TARGET stroid) +pkg_check_modules(eigen3 REQUIRED IMPORTED_TARGET eigen3) add_library(mean_field) @@ -92,7 +93,11 @@ target_sources(mean_field libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp libmeanfield/impl/operators/prepared_displacement_operator.cpp libmeanfield/impl/models/polytropic.cpp + libmeanfield/impl/seed/lane_emden.cpp + libmeanfield/impl/seed/stellar_equilibrium_projection.cpp + libmeanfield/impl/solver/preconditioning_diagnostics.cpp libmeanfield/impl/operators/prepared_mass_normalization.cpp + libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp ) @@ -125,6 +130,7 @@ target_sources(mean_field libmeanfield/interface/quadrature/policy.cppm libmeanfield/interface/quadrature/mfem.cppm libmeanfield/interface/solver/fields.cppm + libmeanfield/interface/solver/preconditioning_diagnostics.cppm libmeanfield/interface/operators/gravity_field.cppm libmeanfield/interface/operators/gravity_field_jacobian.cppm libmeanfield/interface/operators/kernels/gravity_kernels.cppm @@ -151,6 +157,7 @@ target_sources(mean_field libmeanfield/interface/operators/kernels/rotation_displacement_force_kernels.cppm libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm libmeanfield/interface/operators/prepared_displacement_operator.cppm + libmeanfield/interface/dimensions/quantities.cppm libmeanfield/interface/eos/quantities.cppm libmeanfield/interface/eos/relations.cppm libmeanfield/interface/eos/concepts.cppm @@ -158,9 +165,14 @@ target_sources(mean_field libmeanfield/interface/eos/pressure_surface.cppm libmeanfield/interface/eos/runtime.cppm libmeanfield/interface/eos/polytropic.cppm + libmeanfield/interface/seed/lane_emden.cppm libmeanfield/interface/models/structure/structure_base.cppm libmeanfield/interface/models/structure/polytropic.cppm libmeanfield/interface/models/structure_profile.cppm + libmeanfield/interface/models/specifications.cppm + libmeanfield/interface/models/typed_stellar_model.cppm + libmeanfield/interface/models/compiled_fixed_mass.cppm + libmeanfield/interface/models/compiled_fixed_central_density.cppm libmeanfield/interface/surface/constant.cppm libmeanfield/interface/surface/dependencies.cppm libmeanfield/interface/surface/compiled.cppm @@ -173,10 +185,18 @@ target_sources(mean_field libmeanfield/interface/deformation/radial_extensions.cppm libmeanfield/interface/deformation/domain_deformation.cppm libmeanfield/interface/models/stellar_model.cppm + libmeanfield/interface/operators/root_manifest.cppm + libmeanfield/interface/operators/prepared_constraint.cppm libmeanfield/interface/operators/prepared_mass_normalization.cppm + libmeanfield/interface/operators/prepared_central_density.cppm libmeanfield/interface/operators/prepared_centering_constraint.cppm libmeanfield/interface/operators/prepared_surface_constraint.cppm libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm + libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm + libmeanfield/interface/equilibrium/stellar_discretization.cppm + libmeanfield/interface/operators/stellar_equilibrium_problem.cppm + libmeanfield/interface/seed/stellar_equilibrium_projection.cppm + libmeanfield/interface/operators/stellar_equilibrium_system.cppm ) @@ -187,6 +207,7 @@ target_link_libraries(mean_field mfem PkgConfig::stroid ) +target_link_libraries(mean_field PRIVATE PkgConfig::eigen3) add_library(test_mod) target_sources(test_mod @@ -209,6 +230,9 @@ pkg_check_modules(fourdst_config REQUIRED IMPORTED_TARGET fourdst_config) add_executable(tests tests/test_main.cpp tests/physics/gravity.cpp + tests/physics/dimensional_quantities.cpp + tests/seed/lane_emden.cpp + tests/seed/stellar_equilibrium_projection.cpp tests/geometry/volume.cpp tests/quadrature/policy.cpp tests/integrators/centrifugal.cpp @@ -250,7 +274,13 @@ add_executable(tests tests/operators/prepared_rotation_displacement_force_analytic.cpp tests/operators/prepared_rotation_displacement_force_affine_deformation.cpp tests/operators/prepared_displacement_operator.cpp + tests/operators/root_manifest.cpp + tests/operators/prepared_central_density.cpp + tests/operators/prepared_central_density_stellar_equilibrium.cpp + tests/models/model_specifications.cpp + tests/models/typed_stellar_model.cpp tests/models/stellar_model.cpp + tests/operators/stellar_equilibrium_system.cpp tests/deformation/contracts.cpp tests/deformation/surface_scalar_dof_map.cpp tests/deformation/nodal_radial_surface.cpp @@ -263,6 +293,7 @@ add_executable(tests tests/field/field_registry.cpp tests/field/field_mfem.cpp tests/field/field_dof_map.cpp + tests/solver/preconditioning_diagnostics.cpp ) target_link_libraries(tests PRIVATE mean_field test_mod Catch2::Catch2 Boost::boost) @@ -283,6 +314,7 @@ target_link_libraries(experiment_mod add_executable(experiments experiments/experiment_main.cpp experiments/gravity_accuracy_budget.cpp + experiments/preconditioning_diagnostics.cpp ) target_link_libraries(experiments PRIVATE mean_field test_mod experiment_mod Catch2::Catch2 Boost::boost) diff --git a/experiments/README.md b/experiments/README.md index c631aa0..b183bb0 100644 --- a/experiments/README.md +++ b/experiments/README.md @@ -147,3 +147,42 @@ The executable needs the same dependencies, generated module mapping, and configuration registration as the existing Catch2 test executable. Add `experiment_main.cpp` and `gravity_accuracy_budget.cpp` as a second executable next to that target; do not add them to the ordinary test executable. + +## P0 preconditioning baseline + +The P0 diagnostic establishes the unpreconditioned reference for the complete, +central-density-closed `n = 3` stellar equilibrium Jacobian. It uses an identity +inverse preconditioner with FGMRES, recomputes the true residual independently, +records every residual block, and counts and times Jacobian and preconditioner +applications. A separate fixed-operator Arnoldi measurement acts explicitly on +the right-preconditioned product `J M^-1`. Its singular-value ratio is a +projected Krylov-space condition proxy, not the condition number of the full +Jacobian. The same output records Ritz values, clustering about one, +nonnormality, and the real extent of the projected field of values. + +The extended baseline preserves the fixed 40-iteration FGMRES budget used by +the original P0 run and increases the Arnoldi dimension from 12 to 48. It writes +the complete reported FGMRES residual history, block-relative and +manifest-scaled final residuals, the fraction of the squared residual in each +physics block, timings for construction/projection/preparation/direct-residual +measurement, and separate Arnoldi operator and orthogonalization timings. Live +progress messages delimit every expensive phase and report every fourth +Arnoldi application. The CSV records whether it came from a Debug or Release +build. + +Run the focused synthetic verification tests with: + +```text +./cmake-build-debug-homebrew/tests "[preconditioning][diagnostics][unit]" +``` + +Run the performance and spectral measurement separately with: + +```text +mpirun -np 1 ./cmake-build-release-homebrew/experiments \ + --experiment-output preconditioning_p0_identity_extended.csv \ + --catch2 "[preconditioning][diagnostics][baseline]" +``` + +Set `MEANFIELD_SINGLE_JACOBIAN_BENCHMARK=1` to stop after the initial prepared +Jacobian timing instead of running FGMRES and Arnoldi. diff --git a/experiments/coupled_gauge_modes.cpp b/experiments/coupled_gauge_modes.cpp index 1db0fa2..81a07af 100644 --- a/experiments/coupled_gauge_modes.cpp +++ b/experiments/coupled_gauge_modes.cpp @@ -679,6 +679,93 @@ TEST_CASE( null_space::report_progress(communicator, "coupled reduced surface-mode probe complete; writing CSV output"); } +TEST_CASE( + "Fixed Central Density Phase Couples To The N3 Homology Tangent", + "[null_space][homology][central_density][phase]" +) { + mean_field::utils::Args args = test_utils::setup_args(); + null_space::N3Equilibrium fixture(std::move(args)); + const MPI_Comm communicator = fixture.fem().mesh->GetComm(); + + const std::vector modes = make_gauge_modes(fixture); + const auto homology = std::ranges::find_if(modes, [](const GaugeMode &mode) { return mode.family == "homology"; }); + REQUIRE(homology != modes.end()); + + const auto &layout = fixture.stellar_operator().GetLayout(); + const mfem::Vector enthalpy = null_space::const_value_view(fixture.state(), layout, null_space::enthalpyValue); + const mfem::Vector enthalpyDirection = + null_space::const_value_view(homology->direction, layout, null_space::enthalpyValue); + + const mean_field::field::FieldDofMap enthalpyMap = + mean_field::field::make_field_dof_map( + *fixture.fem().enthalpyFes + ); + mfem::Vector origin(fixture.fem().mesh->SpaceDimension()); + origin = 0.0; + mean_field::field::FieldPointDofMap centerDof = + mean_field::field::make_field_point_dof_map( + *fixture.fem().enthalpyFes, enthalpyMap, origin, 1.0e-12 + ); + + double localCentralEnthalpy = 0.0; + for (const int reducedDof : centerDof.reduced_dofs()) { + localCentralEnthalpy += enthalpy(reducedDof); + } + double centralEnthalpy = 0.0; + MPI_Allreduce(&localCentralEnthalpy, ¢ralEnthalpy, 1, MPI_DOUBLE, MPI_SUM, communicator); + REQUIRE(std::isfinite(centralEnthalpy)); + REQUIRE(centralEnthalpy > 0.0); + + const auto &equationOfState = fixture.model().equationOfState(); + const mean_field::eos::DensityValue targetDensity = mean_field::eos::evaluate( + equationOfState, mean_field::eos::SpecificEnthalpyValue{centralEnthalpy} + ); + const mean_field::models::CompiledFixedCentralDensity compiled = + mean_field::models::compileConstraint(mean_field::models::FixedCentralDensity{targetDensity}, equationOfState); + mean_field::operators::PreparedCentralDensityConstraint phase(std::move(centerDof), communicator); + phase.Prepare(compiled, enthalpy, 0.0, {.enthalpy = {.identity = 3251, .revision = 1}}); + + mfem::Vector enthalpyAction(enthalpy.Size()); + mfem::Vector phaseAction(1); + enthalpyAction = 0.0; + phaseAction = 0.0; + phase.ApplyJacobian( + {.enthalpyVariation = enthalpyDirection, .borderVariation = 0.0}, + {.enthalpyAction = enthalpyAction, .phaseAction = phaseAction} + ); + + const double couplingScale = std::max(1.0, std::abs(compiled.targetEnthalpy().value())); + const double enthalpyDirectionNorm = null_space::global_norm(enthalpyDirection, communicator); + const double homologyDirectionNorm = null_space::global_norm(homology->direction, communicator); + const double absolutePhaseCoupling = std::abs(phaseAction(0)); + INFO("Central enthalpy = " << centralEnthalpy); + INFO("N3 homology phase coupling = " << phaseAction(0)); + REQUIRE(std::isfinite(phaseAction(0))); + REQUIRE(enthalpyDirectionNorm > 0.0); + REQUIRE(homologyDirectionNorm > 0.0); + CHECK(absolutePhaseCoupling > 100.0 * std::numeric_limits::epsilon() * couplingScale); + + int rank = 0; + MPI_Comm_rank(communicator, &rank); + if (rank == 0) { + experiment::record_experiment_result( + "fixed_central_density_homology_coupling", "n3_homology", + {{"mesh_file", test_utils::setup_args().mesh_file}, + {"local_state_dofs", std::to_string(fixture.stellar_operator().Width())}}, + {{"target_density", compiled.targetDensity().value()}, + {"target_enthalpy", compiled.targetEnthalpy().value()}, + {"central_enthalpy", centralEnthalpy}, + {"homology_phase_action", phaseAction(0)}, + {"absolute_phase_coupling", absolutePhaseCoupling}, + {"target_scaled_phase_coupling", absolutePhaseCoupling / couplingScale}, + {"enthalpy_direction_norm", enthalpyDirectionNorm}, + {"enthalpy_normalized_phase_coupling", absolutePhaseCoupling / enthalpyDirectionNorm}, + {"homology_direction_norm", homologyDirectionNorm}, + {"state_normalized_phase_coupling", absolutePhaseCoupling / homologyDirectionNorm}} + ); + } +} + TEST_CASE( "N3 Homology Mass Cancellation At The Registered Polynomial Order", "[null_space][homology][mass_normalization][convergence][p_refinement]" diff --git a/experiments/preconditioning_diagnostics.cpp b/experiments/preconditioning_diagnostics.cpp new file mode 100644 index 0000000..33819f4 --- /dev/null +++ b/experiments/preconditioning_diagnostics.cpp @@ -0,0 +1,455 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +import mean_field; +import test_helpers; +import experiment; + +namespace { + using Clock = std::chrono::steady_clock; + + [[nodiscard]] const char *build_configuration() noexcept { +#ifdef NDEBUG + return "release"; +#else + return "debug"; +#endif + } + + [[nodiscard]] double maximum_rank_seconds( + const Clock::time_point start, + const MPI_Comm communicator + ) { + const double localSeconds = std::chrono::duration(Clock::now() - start).count(); + double maximumSeconds{0.0}; + MPI_Allreduce(&localSeconds, &maximumSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator); + return maximumSeconds; + } + + void announce( + const MPI_Comm communicator, + const std::string &message + ) { + int rank{0}; + MPI_Comm_rank(communicator, &rank); + if (rank == 0) { + std::cout << message << std::endl; + } + } + + class ArnoldiProgressOperator final : public mfem::Operator { + public: + ArnoldiProgressOperator( + const mfem::Operator &operation, + const MPI_Comm communicator, + const int expectedApplications, + const int reportingInterval + ) + : mfem::Operator( + operation.Height(), + operation.Width() + ), + m_operation(&operation), + m_communicator(communicator), + m_expectedApplications(expectedApplications), + m_reportingInterval(reportingInterval) { + } + + void Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const override { + m_operation->Mult(input, output); + ++m_completedApplications; + if (m_completedApplications == 1 || m_completedApplications == m_expectedApplications || + m_completedApplications % m_reportingInterval == 0) { + announce( + m_communicator, "Arnoldi progress: " + std::to_string(m_completedApplications) + "/" + + std::to_string(m_expectedApplications) + " Jacobian applications" + ); + } + } + + private: + const mfem::Operator *m_operation; + MPI_Comm m_communicator; + int m_expectedApplications; + int m_reportingInterval; + mutable int m_completedApplications{0}; + }; + + [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() { + return { + .discretization = {.identity = 8101, .revision = 1}, + .density = {.identity = 8103, .revision = 1}, + .surfaceDeformation = {.identity = 8107, .revision = 1}, + .gravityGradient = {.identity = 8111, .revision = 1}, + .gravityPotential = {.identity = 8117, .revision = 1}, + .enthalpy = {.identity = 8123, .revision = 1}, + .bernoulliConstant = {.identity = 8129, .revision = 1}, + .rotation = {.identity = 8131, .revision = 1}, + .targetMass = {.identity = 8137, .revision = 1} + }; + } + + [[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() { + mfem::Vector angularVelocity(3); + mfem::Vector center(3); + angularVelocity = 0.0; + center = 0.0; + return {angularVelocity, center}; + } + + [[nodiscard]] double global_norm( + const mfem::Vector &vector, + const MPI_Comm communicator + ) { + const double localSquaredNorm = vector * vector; + double globalSquaredNorm{0.0}; + MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, communicator); + return std::sqrt(std::max(globalSquaredNorm, 0.0)); + } + + [[nodiscard]] mfem::Vector make_block_balanced_direction( + const int stateSize, + const std::span valueBlocks, + const MPI_Comm communicator + ) { + mfem::Vector direction(stateSize); + direction = 0.0; + + for (const mean_field::operators::RootBlockDescriptor &block : valueBlocks) { + mfem::Vector values(direction.GetData() + block.offset, block.size); + for (int index = 0; index < values.Size(); ++index) { + const double ordinal = static_cast(block.canonicalIndex + 1); + values(index) = std::sin(0.6180339887498948 * static_cast(index + 1) + ordinal); + } + const double norm = global_norm(values, communicator); + if (norm > 0.0) { + values /= norm; + } + } + return direction; + } + + void require_finite(const double value) { + REQUIRE(std::isfinite(value)); + } + + [[nodiscard]] std::map< + std::string, + std::string> + common_parameters( + const std::string &measurement, + const int stateSize + ) { + return { + {"build_configuration", build_configuration()}, + {"equation_of_state", "Polytrope(n=3)"}, + {"experiment_schema", "p0_extended_v2"}, + {"linearization_state", "projected_lane_emden"}, + {"measurement", measurement}, + {"mesh_file", test_utils::setup_args().mesh_file}, + {"preconditioner", "identity"}, + {"preconditioned_product", "J M^-1"}, + {"root_dimension", std::to_string(stateSize)} + }; + } +} // namespace + +TEST_CASE( + "Stellar Equilibrium P0 Identity Preconditioning Baseline", + "[preconditioning][diagnostics][baseline][spectrum]" +) { + using namespace mean_field; + + constexpr int arnoldiDimension = 48; + const MPI_Comm world = MPI_COMM_WORLD; + const Clock::time_point experimentStart = Clock::now(); + announce(world, "P0 extended baseline: constructing the finite-element discretization"); + + const Clock::time_point finiteElementSetupStart = Clock::now(); + utils::Args args = test_utils::setup_args(); + fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(finiteElementModel.okay()); + const MPI_Comm communicator = finiteElementModel.mesh->GetComm(); + const double finiteElementSetupSeconds = maximum_rank_seconds(finiteElementSetupStart, communicator); + announce( + communicator, "P0 extended baseline: finite-element setup completed in " + + std::to_string(finiteElementSetupSeconds) + " seconds" + ); + + constexpr double stellarRadius = utils::RADIUS; + constexpr double targetMass = utils::MASS; + const Clock::time_point calibrationStart = Clock::now(); + const seed::DimensionlessLaneEmdenSolution dimensionlessProfile = seed::integrateLaneEmden(3.0, 10.0); + REQUIRE(dimensionlessProfile.firstZeroCoordinate.has_value()); + const double surfaceCoordinate = *dimensionlessProfile.firstZeroCoordinate; + const double surfaceDerivative = + dimensionlessProfile.thetaDerivative(dimensionlessProfile.thetaDerivative.Size() - 1); + const double dimensionlessMass = -surfaceCoordinate * surfaceCoordinate * surfaceDerivative; + REQUIRE(dimensionlessMass > 0.0); + + const double massScale = targetMass / (4.0 * std::numbers::pi_v * dimensionlessMass); + const double polytropicConstant = std::numbers::pi_v * utils::G * std::pow(massScale, 2.0 / 3.0); + const double radialScale = stellarRadius / surfaceCoordinate; + const double centralDensity = + std::pow(polytropicConstant / (std::numbers::pi_v * utils::G * radialScale * radialScale), 1.5); + const double calibrationSeconds = maximum_rank_seconds(calibrationStart, communicator); + + const Clock::time_point problemConstructionStart = Clock::now(); + const auto stellarModel = model::StellarModel( + eos::Polytrope({.n = 3.0, .K = polytropicConstant}), + surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}), + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}}) + ); + auto problem = equilibrium::discretize(stellarModel, finiteElementModel); + const double problemConstructionSeconds = maximum_rank_seconds(problemConstructionStart, communicator); + announce(communicator, "P0 extended baseline: projecting the Lane-Emden seed"); + + const Clock::time_point seedProjectionStart = Clock::now(); + const auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 4096})); + const double seedProjectionSeconds = maximum_rank_seconds(seedProjectionStart, communicator); + announce(communicator, "P0 extended baseline: preparing the complete equilibrium operator"); + + const Clock::time_point operatorPreparationStart = Clock::now(); + const operators::PreparedCentralDensityStellarEquilibriumReport preparation = + problem.Prepare(projected.values, make_dependencies(), make_zero_rotation()); + REQUIRE(preparation.assembledResidual); + const double operatorPreparationSeconds = maximum_rank_seconds(operatorPreparationStart, communicator); + + const mfem::Operator &rawJacobian = problem.GetLinearizationOperator(); + mfem::Vector knownDirection = + make_block_balanced_direction(problem.StateSize(), problem.GetManifest().valueBlocks(), communicator); + mfem::Vector rightHandSide(problem.EquationSize()); + const Clock::time_point applicationStart = Clock::now(); + rawJacobian.Mult(knownDirection, rightHandSide); + const double applicationSeconds = maximum_rank_seconds(applicationStart, communicator); + REQUIRE(rightHandSide.Size() == problem.EquationSize()); + require_finite(global_norm(rightHandSide, communicator)); + announce( + communicator, "P0 extended baseline: first prepared Jacobian application completed in " + + std::to_string(applicationSeconds) + " seconds" + ); + + if (std::getenv("MEANFIELD_SINGLE_JACOBIAN_BENCHMARK") != nullptr) { + int rank{0}; + MPI_Comm_rank(communicator, &rank); + if (rank == 0) { + std::cout << "Single prepared Jacobian application: " << applicationSeconds << " seconds\n"; + } + return; + } + + solver::IdentityPreconditioner identity(problem.StateSize()); + solver::InstrumentedOperator instrumentedJacobian(rawJacobian); + solver::InstrumentedPreconditioner instrumentedPreconditioner(identity); + solver::ResidualHistoryMonitor monitor; + mfem::FGMRESSolver krylov(communicator); + krylov.SetPreconditioner(instrumentedPreconditioner); + krylov.SetOperator(instrumentedJacobian); + krylov.SetMonitor(monitor); + krylov.SetRelTol(1.0e-8); + krylov.SetAbsTol(1.0e-12); + krylov.SetMaxIter(40); + krylov.SetKDim(20); + krylov.SetPrintLevel(1); + + mfem::Vector solution(problem.StateSize()); + solution = 0.0; + const operators::PreparedStellarEquilibriumStatistics statisticsBeforeSolve = + problem.GetPreparedOperator().GetPhysicalOperator().GetStatistics(); + announce(communicator, "P0 extended baseline: starting the 40-iteration identity-preconditioned FGMRES solve"); + const Clock::time_point solveStart = Clock::now(); + krylov.Mult(rightHandSide, solution); + const double localSolveSeconds = std::chrono::duration(Clock::now() - solveStart).count(); + const operators::PreparedStellarEquilibriumStatistics statisticsAfterSolve = + problem.GetPreparedOperator().GetPhysicalOperator().GetStatistics(); + + announce(communicator, "P0 extended baseline: independently reconstructing the true residual"); + const Clock::time_point directResidualStart = Clock::now(); + const solver::LinearSolveMeasurement solveMeasurement = solver::measureLinearSolve( + krylov, rawJacobian, rightHandSide, solution, problem.GetManifest().residualBlocks(), + instrumentedJacobian.GetStatistics(), instrumentedPreconditioner.GetStatistics(), + instrumentedPreconditioner.GetLifecycleStatistics(), monitor, localSolveSeconds, communicator + ); + const double directResidualMeasurementSeconds = maximum_rank_seconds(directResidualStart, communicator); + require_finite(solveMeasurement.directResidual.relativeResidual); + require_finite(solveMeasurement.solveSecondsMaximumRank); + + std::map solveMetrics{ + {"solver_converged", solveMeasurement.solverConverged ? 1.0 : 0.0}, + {"outer_iterations", static_cast(solveMeasurement.outerIterations)}, + {"reported_initial_residual_norm", solveMeasurement.solverReportedInitialNorm}, + {"reported_final_residual_norm", solveMeasurement.solverReportedFinalNorm}, + {"reported_residual_reduction", solveMeasurement.solverReportedResidualReduction}, + {"true_residual_norm", solveMeasurement.directResidual.trueResidualNorm}, + {"true_relative_residual", solveMeasurement.directResidual.relativeResidual}, + {"rhs_norm", solveMeasurement.directResidual.rightHandSideNorm}, + {"true_residual_digits_per_jacobian_application", + solveMeasurement.trueResidualDigitsReducedPerJacobianApplication}, + {"finite_element_setup_seconds", finiteElementSetupSeconds}, + {"lane_emden_calibration_seconds", calibrationSeconds}, + {"equilibrium_problem_construction_seconds", problemConstructionSeconds}, + {"seed_projection_seconds", seedProjectionSeconds}, + {"operator_preparation_seconds", operatorPreparationSeconds}, + {"initial_jacobian_application_seconds", applicationSeconds}, + {"direct_residual_measurement_seconds", directResidualMeasurementSeconds}, + {"solve_seconds_maximum_rank", solveMeasurement.solveSecondsMaximumRank}, + {"jacobian_applications", static_cast(solveMeasurement.jacobian.applications)}, + {"jacobian_application_seconds", solveMeasurement.jacobian.totalSeconds}, + {"jacobian_maximum_application_seconds", solveMeasurement.jacobian.maximumSeconds}, + {"inverse_preconditioner_applications", + static_cast(solveMeasurement.inversePreconditioner.applications)}, + {"inverse_preconditioner_application_seconds", solveMeasurement.inversePreconditioner.totalSeconds}, + {"inverse_preconditioner_maximum_application_seconds", solveMeasurement.inversePreconditioner.maximumSeconds}, + {"inverse_preconditioner_setups", static_cast(solveMeasurement.inversePreconditionerLifecycle.setups)}, + {"inverse_preconditioner_refreshes", + static_cast(solveMeasurement.inversePreconditionerLifecycle.refreshes)}, + {"inverse_preconditioner_setup_seconds", solveMeasurement.inversePreconditionerLifecycle.setupSeconds}, + {"inverse_preconditioner_refresh_seconds", solveMeasurement.inversePreconditionerLifecycle.refreshSeconds}, + {"prepared_residual_assemblies_during_solve", + static_cast(statisticsAfterSolve.residualAssemblies - statisticsBeforeSolve.residualAssemblies)}, + {"prepared_geometry_builds_during_solve", + static_cast( + statisticsAfterSolve.generatedGeometryBuilds - statisticsBeforeSolve.generatedGeometryBuilds + )}, + {"prepared_jacobian_applications_during_solve", + static_cast(statisticsAfterSolve.jacobianApplications - statisticsBeforeSolve.jacobianApplications)} + }; + for (const solver::ResidualBlockMeasurement &block : solveMeasurement.directResidual.blocks) { + const std::string prefix = "residual_block." + block.stableId; + solveMetrics[prefix + ".descriptor_scale"] = block.descriptorScale; + solveMetrics[prefix + ".rhs_norm"] = block.rightHandSideNorm; + solveMetrics[prefix + ".true_norm"] = block.trueResidualNorm; + solveMetrics[prefix + ".block_relative_residual"] = block.blockRelativeResidual; + solveMetrics[prefix + ".scaled_rhs_norm"] = block.scaledRightHandSideNorm; + solveMetrics[prefix + ".scaled_true_norm"] = block.scaledTrueResidualNorm; + solveMetrics[prefix + ".fraction_global_squared_residual"] = block.fractionOfGlobalSquaredResidualNorm; + solveMetrics[prefix + ".global_relative_contribution"] = block.contributionToGlobalRelativeResidual; + } + experiment::record_experiment_result( + "stellar_preconditioning_p0", "identity_linear_solve", common_parameters("linear_solve", problem.StateSize()), + std::move(solveMetrics) + ); + + const double reportedInitialDenominator = std::max(solveMeasurement.solverReportedInitialNorm, 1.0e-300); + for (std::size_t sample = 0; sample < solveMeasurement.reportedResidualHistory.size(); ++sample) { + const solver::IterationResidualMeasurement &residual = solveMeasurement.reportedResidualHistory[sample]; + experiment::record_experiment_result( + "stellar_preconditioning_p0", "identity_fgmres_history_" + std::to_string(sample), + common_parameters("fgmres_residual_history", problem.StateSize()), + {{"history_sample", static_cast(sample)}, + {"iteration", static_cast(residual.iteration)}, + {"reported_residual_norm", residual.reportedNorm}, + {"reported_relative_residual", residual.reportedNorm / reportedInitialDenominator}, + {"final_measurement", residual.final ? 1.0 : 0.0}} + ); + } + + instrumentedJacobian.ResetStatistics(); + instrumentedPreconditioner.ResetStatistics(); + solver::FixedRightPreconditionedOperator rightPreconditionedProduct( + instrumentedJacobian, instrumentedPreconditioner + ); + ArnoldiProgressOperator progressOperator(rightPreconditionedProduct, communicator, arnoldiDimension, 4); + announce( + communicator, + "P0 extended baseline: starting the " + std::to_string(arnoldiDimension) + "-vector Arnoldi measurement" + ); + const solver::ArnoldiSpectralMeasurement spectrum = solver::measureArnoldiSpectrum( + progressOperator, knownDirection, communicator, + {.krylovDimension = arnoldiDimension, + .breakdownRelativeTolerance = 1.0e-13, + .ritzConvergenceRelativeTolerance = 1.0e-7, + .reorthogonalize = true} + ); + require_finite(spectrum.projectedLargestSingularValue); + require_finite(spectrum.centroidRealPart); + require_finite(spectrum.rmsClusterRadius); + + experiment::record_experiment_result( + "stellar_preconditioning_p0", "identity_arnoldi_summary", + common_parameters("arnoldi_summary", problem.StateSize()), + {{"requested_krylov_dimension", static_cast(spectrum.requestedDimension)}, + {"achieved_krylov_dimension", static_cast(spectrum.achievedDimension)}, + {"invariant_subspace_found", spectrum.invariantSubspaceFound ? 1.0 : 0.0}, + {"operator_applications", static_cast(spectrum.operatorApplications)}, + {"arnoldi_operator_application_seconds", spectrum.operatorApplicationSecondsMaximumRank}, + {"arnoldi_operator_maximum_application_seconds", spectrum.operatorMaximumApplicationSecondsMaximumRank}, + {"arnoldi_measurement_seconds", spectrum.measurementSecondsMaximumRank}, + {"arnoldi_nonapplication_seconds", spectrum.nonApplicationSecondsMaximumRank}, + {"experiment_elapsed_through_arnoldi_seconds", maximum_rank_seconds(experimentStart, communicator)}, + {"converged_ritz_values", static_cast(spectrum.convergedRitzValueCount)}, + {"negative_real_part_ritz_values", static_cast(spectrum.negativeRealPartCount)}, + {"projected_largest_singular_value", spectrum.projectedLargestSingularValue}, + {"projected_smallest_singular_value", spectrum.projectedSmallestSingularValue}, + {"projected_condition_proxy", spectrum.projectedConditionProxy}, + {"ritz_centroid_real", spectrum.centroidRealPart}, + {"ritz_centroid_imaginary", spectrum.centroidImaginaryPart}, + {"ritz_rms_distance_from_one", spectrum.rmsDistanceFromOne}, + {"ritz_rms_cluster_radius", spectrum.rmsClusterRadius}, + {"ritz_minimum_magnitude", spectrum.minimumMagnitude}, + {"ritz_maximum_magnitude", spectrum.maximumMagnitude}, + {"ritz_minimum_real_part", spectrum.minimumRealPart}, + {"ritz_maximum_real_part", spectrum.maximumRealPart}, + {"ritz_maximum_absolute_imaginary_part", spectrum.maximumAbsoluteImaginaryPart}, + {"ritz_conjugate_pair_defect", spectrum.conjugatePairDefect}, + {"projected_departure_from_normality", spectrum.projectedDepartureFromNormality}, + {"projected_field_of_values_minimum_real_part", spectrum.projectedFieldOfValuesMinimumRealPart}, + {"projected_field_of_values_maximum_real_part", spectrum.projectedFieldOfValuesMaximumRealPart}, + {"measured_jacobian_applications", static_cast(instrumentedJacobian.GetStatistics().applications)}, + {"measured_jacobian_application_seconds", instrumentedJacobian.GetStatistics().totalSeconds}, + {"measured_jacobian_maximum_application_seconds", instrumentedJacobian.GetStatistics().maximumSeconds}, + {"measured_inverse_preconditioner_applications", + static_cast(instrumentedPreconditioner.GetStatistics().applications)}, + {"measured_inverse_preconditioner_application_seconds", + instrumentedPreconditioner.GetStatistics().totalSeconds}} + ); + + std::vector orderedRitzValues = spectrum.ritzValues; + std::ranges::sort(orderedRitzValues, [](const auto &left, const auto &right) { + if (left.realPart != right.realPart) { + return left.realPart < right.realPart; + } + return left.imaginaryPart < right.imaginaryPart; + }); + for (std::size_t index = 0; index < orderedRitzValues.size(); ++index) { + const solver::RitzValueMeasurement &ritz = orderedRitzValues[index]; + experiment::record_experiment_result( + "stellar_preconditioning_p0", "identity_ritz_" + std::to_string(index), + common_parameters("ritz_value", problem.StateSize()), + {{"ritz_index", static_cast(index)}, + {"ritz_real", ritz.realPart}, + {"ritz_imaginary", ritz.imaginaryPart}, + {"ritz_magnitude", ritz.magnitude}, + {"ritz_distance_from_one", ritz.distanceFromOne}, + {"ritz_residual_estimate", ritz.residualEstimate}, + {"ritz_relative_residual_estimate", ritz.relativeResidualEstimate}, + {"ritz_converged", ritz.converged ? 1.0 : 0.0}} + ); + } + + int rank{0}; + MPI_Comm_rank(communicator, &rank); + if (rank == 0) { + std::cout << "P0 identity baseline: " << solveMeasurement.outerIterations << " FGMRES iterations, " + << spectrum.achievedDimension << " Arnoldi vectors, true relative residual " + << solveMeasurement.directResidual.relativeResidual << '\n'; + } +} diff --git a/libmeanfield/impl/mapping/domain_mapper.cpp b/libmeanfield/impl/mapping/domain_mapper.cpp index 61e5581..3d0fcd1 100644 --- a/libmeanfield/impl/mapping/domain_mapper.cpp +++ b/libmeanfield/impl/mapping/domain_mapper.cpp @@ -489,7 +489,8 @@ namespace mean_field::mapping { mfem::ElementTransformation &transformation, const mfem::IntegrationPoint &integration_point, Workspace &workspace, - CompactificationPointData &point_data + CompactificationPointData &point_data, + const mfem::DenseMatrix *inverse_mesh_jacobian ) const { const mfem::FiniteElement &element = compactification.GetElement(); const mfem::Vector &dofs = compactification.GetDofs(); @@ -509,13 +510,17 @@ namespace mean_field::mapping { return MappingStatus::non_finite_input; } - transformation.SetIntPoint(&integration_point); - workspace.m_compactification_shape.SetSize(dof_count); workspace.m_compactification_dshape.SetSize(dof_count, m_options.dimension); element.CalcShape(integration_point, workspace.m_compactification_shape); - element.CalcPhysDShape(transformation, workspace.m_compactification_dshape); + if (inverse_mesh_jacobian != nullptr) { + workspace.m_reference_dshape.SetSize(dof_count, m_options.dimension); + element.CalcDShape(integration_point, workspace.m_reference_dshape); + mfem::Mult(workspace.m_reference_dshape, *inverse_mesh_jacobian, workspace.m_compactification_dshape); + } else { + element.CalcPhysDShape(transformation, workspace.m_compactification_dshape); + } point_data.coordinate = dofs * workspace.m_compactification_shape; point_data.coordinate_gradient.SetSize(m_options.dimension); @@ -539,10 +544,9 @@ namespace mean_field::mapping { const mfem::IntegrationPoint &integration_point, Workspace &workspace, mfem::Vector &value, - mfem::DenseMatrix &jacobian + mfem::DenseMatrix &jacobian, + const mfem::DenseMatrix *inverse_mesh_jacobian ) const { - transformation.SetIntPoint(&integration_point); - const mfem::FiniteElement &element = field.GetElement(); const mfem::DenseMatrix &dof_matrix = field.GetDofMatrix(); @@ -550,7 +554,13 @@ namespace mean_field::mapping { workspace.m_mesh_dshape.SetSize(element.GetDof(), m_options.dimension); element.CalcShape(integration_point, workspace.m_shape); - element.CalcPhysDShape(transformation, workspace.m_mesh_dshape); + if (inverse_mesh_jacobian != nullptr) { + workspace.m_reference_dshape.SetSize(element.GetDof(), m_options.dimension); + element.CalcDShape(integration_point, workspace.m_reference_dshape); + mfem::Mult(workspace.m_reference_dshape, *inverse_mesh_jacobian, workspace.m_mesh_dshape); + } else { + element.CalcPhysDShape(transformation, workspace.m_mesh_dshape); + } value.SetSize(m_options.dimension); dof_matrix.MultTranspose(workspace.m_shape, value); @@ -586,7 +596,7 @@ namespace mean_field::mapping { EvaluateField( element_data.displacement, transformation, integration_point, workspace, workspace.m_field_value, - workspace.m_field_jacobian + workspace.m_field_jacobian, nullptr ); if (!vector_is_finite(context.reference_position) || !vector_is_finite(workspace.m_field_value) || @@ -608,7 +618,7 @@ namespace mean_field::mapping { if (context.compactified) { const MappingStatus coordinate_status = EvaluateCompactificationCoordinate( element_data.compactification, transformation, integration_point, workspace, - workspace.m_compactification_point + workspace.m_compactification_point, nullptr ); if (coordinate_status != MappingStatus::valid) @@ -663,7 +673,6 @@ namespace mean_field::mapping { if (point_status != MappingStatus::valid) return point_status; - transformation.SetIntPoint(&integration_point); mfem::Mult(context.mapping.mapping_jacobian, transformation.Jacobian(), workspace.m_full_element_jacobian); context.quadrature.J_inv.SetSize(m_options.dimension, m_options.dimension); @@ -766,6 +775,21 @@ namespace mean_field::mapping { const MappingPointContext &base_context, Workspace &workspace, MappingPointVariation &variation + ) const { + return EvaluatePointVariationImpl( + element_data, direction, transformation, integration_point, base_context, workspace, variation, nullptr + ); + } + + MappingStatus DomainMapper::EvaluatePointVariationImpl( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + const MappingPointContext &base_context, + Workspace &workspace, + MappingPointVariation &variation, + const mfem::DenseMatrix *inverse_mesh_jacobian ) const { ValidateElementData(element_data); const ElementMappingData direction_data{ @@ -786,8 +810,13 @@ namespace mean_field::mapping { "domain." ); + if (inverse_mesh_jacobian == nullptr) { + transformation.SetIntPoint(&integration_point); + } + EvaluateField( - direction, transformation, integration_point, workspace, workspace.m_field_value, workspace.m_field_jacobian + direction, transformation, integration_point, workspace, workspace.m_field_value, + workspace.m_field_jacobian, inverse_mesh_jacobian ); if (!vector_is_finite(workspace.m_field_value) || !matrix_is_finite(workspace.m_field_jacobian)) @@ -799,7 +828,7 @@ namespace mean_field::mapping { if (base_context.compactified) { const MappingStatus coordinate_status = EvaluateCompactificationCoordinate( element_data.compactification, transformation, integration_point, workspace, - workspace.m_compactification_point + workspace.m_compactification_point, inverse_mesh_jacobian ); if (coordinate_status != MappingStatus::valid) @@ -872,27 +901,28 @@ namespace mean_field::mapping { Workspace &workspace, VolumeMappingVariation &variation ) const { - const MappingStatus point_status = EvaluatePointVariation( + mfem::Mult(base_context.quadrature.J_inv, base_context.mapping.mapping_jacobian, workspace.m_matrix_temp_2); + + const MappingStatus point_status = EvaluatePointVariationImpl( element_data, direction, transformation, integration_point, base_context.mapping, workspace, - variation.mapping + variation.mapping, &workspace.m_matrix_temp_2 ); if (point_status != MappingStatus::valid) return point_status; - transformation.SetIntPoint(&integration_point); mfem::Mult( - variation.mapping.mapping_jacobian_variation, transformation.Jacobian(), workspace.m_full_element_jacobian + base_context.quadrature.J_inv, variation.mapping.mapping_jacobian_variation, workspace.m_matrix_temp_1 ); - mfem::Mult(base_context.quadrature.J_inv, workspace.m_full_element_jacobian, workspace.m_matrix_temp_1); variation.inverse_element_jacobian_variation.SetSize(m_options.dimension, m_options.dimension); mfem::Mult( - workspace.m_matrix_temp_1, base_context.quadrature.J_inv, variation.inverse_element_jacobian_variation + workspace.m_matrix_temp_1, base_context.mapping.inverse_mapping_jacobian, + variation.inverse_element_jacobian_variation ); variation.inverse_element_jacobian_variation *= -1.0; - variation.weight_variation = - integration_point.weight * transformation.Weight() * variation.mapping.mapping_determinant_variation; + variation.weight_variation = base_context.quadrature.weight / base_context.mapping.mapping_determinant * + variation.mapping.mapping_determinant_variation; if (!matrix_is_finite(variation.inverse_element_jacobian_variation) || !std::isfinite(variation.weight_variation)) diff --git a/libmeanfield/impl/mapping/transformations.cpp b/libmeanfield/impl/mapping/transformations.cpp index c0936a7..c5bb25d 100644 --- a/libmeanfield/impl/mapping/transformations.cpp +++ b/libmeanfield/impl/mapping/transformations.cpp @@ -217,16 +217,22 @@ namespace mean_field::mapping { ); MFEM_VERIFY(std::isfinite(determinant_variation), "The mapping determinant variation must be finite."); - mfem::DenseMatrix determinant_correction(dimension, dimension); - ComputeHDivMassTensor(context, determinant_correction); - determinant_correction *= determinant_variation / determinant; + const mfem::DenseMatrix &jacobian = context.mapping_jacobian; + const mfem::DenseMatrix &jacobianVariation = variation.mapping_jacobian_variation; + const double inverseDeterminant = 1.0 / determinant; + const double determinantScale = determinant_variation * inverseDeterminant; - mfem::DenseMatrix right_jacobian_variation(dimension, dimension); - mfem::MultAtB(context.mapping_jacobian, variation.mapping_jacobian_variation, right_jacobian_variation); - mfem::MultAtB(variation.mapping_jacobian_variation, context.mapping_jacobian, mass_tensor_variation); - - mass_tensor_variation += right_jacobian_variation; - mass_tensor_variation *= 1 / determinant; - mass_tensor_variation -= determinant_correction; + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + double gram{0.0}; + double gramVariation{0.0}; + for (int inner = 0; inner < dimension; ++inner) { + gram += jacobian(inner, row) * jacobian(inner, column); + gramVariation += jacobian(inner, row) * jacobianVariation(inner, column) + + jacobianVariation(inner, row) * jacobian(inner, column); + } + mass_tensor_variation(row, column) = inverseDeterminant * (gramVariation - determinantScale * gram); + } + } } -} // namespace mean_field::mapping \ No newline at end of file +} // namespace mean_field::mapping diff --git a/libmeanfield/impl/models/polytropic.cpp b/libmeanfield/impl/models/polytropic.cpp index d676865..fa663ac 100644 --- a/libmeanfield/impl/models/polytropic.cpp +++ b/libmeanfield/impl/models/polytropic.cpp @@ -1,8 +1,12 @@ module; + #include #include -#include +#include +#include + module mean_field; + import :model.structure.polytropic; namespace mean_field::models::structure { @@ -24,64 +28,18 @@ namespace mean_field::models::structure { } StructureSeed PolytropicStructure::makeInitialSeed(const StructureSeedRequest &request) const { - validateSeedRequest(request); + const seed::RadialProfile profile = seed::generateLaneEmdenProfile( + m_equationOfState, dimensions::DensityValue{request.centralDensity}, request.radialSampleCount + ); - const double polytropicIndex = m_equationOfState.polytropic_index(); - const std::vector laneEmdenSolution = solveLaneEmden(polytropicIndex); - const double surfaceCoordinate = laneEmdenSolution.back().coordinate; - const double centralEnthalpy = - eos::evaluate(m_equationOfState, eos::DensityValue{request.centralDensity}) - .value(); - const double radialScaleSquared = - centralEnthalpy / (4.0 * std::numbers::pi_v * mean_field::utils::G * request.centralDensity); - - if (!std::isfinite(radialScaleSquared) || radialScaleSquared <= 0.0) { - throw std::runtime_error( - "The polytropic Lane-Emden radial scale is not " - "finite and positive." - ); - } - - const double radialScale = std::sqrt(radialScaleSquared); - - StructureSeed seed; - - seed.radius.SetSize(request.radialSampleCount); - seed.density.SetSize(request.radialSampleCount); - seed.enthalpy.SetSize(request.radialSampleCount); - - seed.stellarRadius = radialScale * surfaceCoordinate; - seed.centralDensity = request.centralDensity; - seed.centralEnthalpy = centralEnthalpy; - - std::size_t interpolationIndex = 0; - - for (int sampleIndex = 0; sampleIndex < request.radialSampleCount; ++sampleIndex) { - const double sampleFraction = - static_cast(sampleIndex) / static_cast(request.radialSampleCount - 1); - - const double dimensionlessRadius = sampleFraction * surfaceCoordinate; - const double laneEmdenValue = - interpolateLaneEmdenValue(laneEmdenSolution, dimensionlessRadius, interpolationIndex); - const double density = request.centralDensity * std::pow(laneEmdenValue, polytropicIndex); - - seed.radius(sampleIndex) = radialScale * dimensionlessRadius; - seed.density(sampleIndex) = density; - seed.enthalpy(sampleIndex) = - eos::evaluate(m_equationOfState, eos::DensityValue{density}).value(); - } - - seed.radius(0) = 0.0; - seed.density(0) = request.centralDensity; - seed.enthalpy(0) = centralEnthalpy; - - const int surfaceIndex = request.radialSampleCount - 1; - - seed.radius(surfaceIndex) = seed.stellarRadius; - seed.density(surfaceIndex) = 0.0; - seed.enthalpy(surfaceIndex) = 0.0; - - return seed; + return { + .radius = profile.radius, + .density = profile.density, + .enthalpy = profile.specificEnthalpy, + .stellarRadius = profile.stellarRadius.value(), + .centralDensity = profile.centralDensity.value(), + .centralEnthalpy = profile.centralSpecificEnthalpy.value() + }; } void PolytropicStructure::validate() const { @@ -90,8 +48,7 @@ namespace mean_field::models::structure { if (!std::isfinite(polytropicIndex) || polytropicIndex < 1.0 || polytropicIndex >= 5.0) { throw std::invalid_argument( std::format( - "PolytropicStructure requires a finite-radius " - "polytrope with 1 <= n < 5. Instead n = {} was " + "PolytropicStructure requires a finite-radius polytrope with 1 <= n < 5. Instead n = {} was " "provided.", polytropicIndex ) @@ -101,163 +58,10 @@ namespace mean_field::models::structure { if (!std::isfinite(m_targetMass) || m_targetMass <= 0.0) { throw std::invalid_argument( std::format( - "The target stellar mass must be finite and " - "positive. Instead a value of {} was provided.", + "The target stellar mass must be finite and positive. Instead a value of {} was provided.", m_targetMass ) ); } } - - void PolytropicStructure::validateSeedRequest(const StructureSeedRequest &request) { - if (!std::isfinite(request.centralDensity) || request.centralDensity <= 0.0) { - throw std::invalid_argument( - std::format( - "The seed central density must be finite and " - "positive. Instead a value of {} was provided.", - request.centralDensity - ) - ); - } - - if (request.radialSampleCount < 2) { - throw std::invalid_argument( - std::format( - "A polytropic seed requires at least two radial " - "samples. Instead {} samples were requested.", - request.radialSampleCount - ) - ); - } - } - - PolytropicStructure::LaneEmdenDerivative PolytropicStructure::evaluateLaneEmdenRhs( - const double coordinate, - const double value, - const double derivative, - const double polytropicIndex - ) { - const double nonnegativeValue = std::max(value, 0.0); - - return { - .value = derivative, - .derivative = -2.0 * derivative / coordinate - std::pow(nonnegativeValue, polytropicIndex) - }; - } - - PolytropicStructure::LaneEmdenPoint PolytropicStructure::takeLaneEmdenStep( - const LaneEmdenPoint &point, - const double step, - const double polytropicIndex - ) { - const LaneEmdenDerivative first = - evaluateLaneEmdenRhs(point.coordinate, point.value, point.derivative, polytropicIndex); - - const LaneEmdenDerivative second = evaluateLaneEmdenRhs( - point.coordinate + 0.5 * step, point.value + 0.5 * step * first.value, - point.derivative + 0.5 * step * first.derivative, polytropicIndex - ); - - const LaneEmdenDerivative third = evaluateLaneEmdenRhs( - point.coordinate + 0.5 * step, point.value + 0.5 * step * second.value, - point.derivative + 0.5 * step * second.derivative, polytropicIndex - ); - - const LaneEmdenDerivative fourth = evaluateLaneEmdenRhs( - point.coordinate + step, point.value + step * third.value, point.derivative + step * third.derivative, - polytropicIndex - ); - - return { - .coordinate = point.coordinate + step, - .value = point.value + step / 6.0 * (first.value + 2.0 * second.value + 2.0 * third.value + fourth.value), - .derivative = - point.derivative + - step / 6.0 * (first.derivative + 2.0 * second.derivative + 2.0 * third.derivative + fourth.derivative) - }; - } - - std::vector PolytropicStructure::solveLaneEmden(const double polytropicIndex) { - constexpr double initialCoordinate = 1.0e-6; - constexpr double integrationStep = 1.0e-3; - constexpr int maximumStepCount = 2'000'000; - - const double coordinateSquared = initialCoordinate * initialCoordinate; - - const double coordinateCubed = coordinateSquared * initialCoordinate; - - const double coordinateFourth = coordinateSquared * coordinateSquared; - - LaneEmdenPoint point{ - .coordinate = initialCoordinate, - .value = 1.0 - coordinateSquared / 6.0 + polytropicIndex * coordinateFourth / 120.0, - .derivative = -initialCoordinate / 3.0 + polytropicIndex * coordinateCubed / 30.0 - }; - - std::vector solution; - solution.reserve(8192); - solution.push_back({.coordinate = 0.0, .value = 1.0, .derivative = 0.0}); - solution.push_back(point); - - for (int stepIndex = 0; stepIndex < maximumStepCount; ++stepIndex) { - LaneEmdenPoint nextPoint = takeLaneEmdenStep(point, integrationStep, polytropicIndex); - - if (!std::isfinite(nextPoint.value)) { - throw std::runtime_error( - "The Lane-Emden integration produced a non-finite " - "solution before reaching the stellar surface." - ); - } - - if (nextPoint.value <= 0.0) { - const double rootFraction = point.value / (point.value - nextPoint.value); - - solution.push_back( - {.coordinate = point.coordinate + rootFraction * (nextPoint.coordinate - point.coordinate), - .value = 0.0, - .derivative = point.derivative + rootFraction * (nextPoint.derivative - point.derivative)} - ); - - return solution; - } - - solution.push_back(nextPoint); - point = nextPoint; - } - - throw std::runtime_error( - "The Lane-Emden integration did not reach its first zero " - "within the configured step limit." - ); - } - - double PolytropicStructure::interpolateLaneEmdenValue( - const std::vector &solution, - const double coordinate, - std::size_t &lowerIndex - ) { - while (lowerIndex + 1 < solution.size() && solution[lowerIndex + 1].coordinate < coordinate) { - ++lowerIndex; - } - - if (lowerIndex + 1 >= solution.size()) { - return 0.0; - } - - const LaneEmdenPoint &lower = solution[lowerIndex]; - const LaneEmdenPoint &upper = solution[lowerIndex + 1]; - - const double interval = upper.coordinate - lower.coordinate; - - if (interval <= 0.0) { - throw std::runtime_error( - "The Lane-Emden interpolation grid is not strictly " - "increasing." - ); - } - - const double fraction = (coordinate - lower.coordinate) / interval; - - return std::clamp(lower.value + fraction * (upper.value - lower.value), 0.0, 1.0); - } -}; // namespace mean_field::models::structure +} // namespace mean_field::models::structure diff --git a/libmeanfield/impl/operators/gravity_field_jacobian.cpp b/libmeanfield/impl/operators/gravity_field_jacobian.cpp index 5d09a92..818f9f9 100644 --- a/libmeanfield/impl/operators/gravity_field_jacobian.cpp +++ b/libmeanfield/impl/operators/gravity_field_jacobian.cpp @@ -3,7 +3,6 @@ module; module mean_field; import :operators.gravity_field_jacobian; -import :operators.kernels.gravity_field; import :utils.blocks; namespace { @@ -202,7 +201,6 @@ namespace mean_field::operators { const context::gravity_field::GravityFieldGeometryContext &geometry_context = m_linearization_context.GetGeometryContext(); const mfem::Vector &density = m_linearization_context.GetDensityTrue(); - const mfem::Vector &displacement = geometry_context.GetDisplacementTrue(); const mfem::Vector &gravity_gradient = m_linearization_context.GetGravityGradientTrue(); const mfem::Vector density_direction = make_read_only_value_view(direction, m_state_offsets, density_block); @@ -244,14 +242,13 @@ namespace mean_field::operators { geometry_context.GetMassOperator().Mult(gravity_gradient_direction, gravity_gradient_action); geometry_context.GetSourceOperator().Mult(density_direction, source_action); - kernels::apply_mapped_hdiv_mass_variation( - m_fem, m_domain_mapper, gravity_gradient, displacement, displacement_direction_true, - mass_variation_action_true + geometry_context.GetMassOperator().MultDisplacementVariationTrue( + gravity_gradient, displacement_direction_true, mass_variation_action_true ); flux_map.gather(mass_variation_action_true, mass_variation_action); - kernels::apply_mapped_source_variation( - m_fem, m_domain_mapper, density, displacement, displacement_direction_true, source_variation_action_true + geometry_context.GetSourceOperator().MultDisplacementVariationTrue( + density, displacement_direction_true, source_variation_action_true ); potential_map.gather(source_variation_action_true, source_variation_action); diff --git a/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp b/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp index ad3aa07..c66fd7d 100644 --- a/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/barotropic_closure_kernels.cpp @@ -12,10 +12,11 @@ import :field.registry; import :utils.domain; namespace { - namespace eos = mean_field::eos; + namespace dimensions = mean_field::dimensions; + namespace eos = mean_field::eos; - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; - using ClosureDomain = mean_field::field::FieldDomainT; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using ClosureDomain = mean_field::field::FieldDomainT; enum class ClosureAction { residual, density, enthalpy }; @@ -339,7 +340,9 @@ namespace { const double density = elementDensityInput * densityShape; const double equationOfStateDensity = - eos::evaluate(barotrope, eos::SpecificEnthalpyValue{baseEnthalpy}) + eos::evaluate( + barotrope, dimensions::SpecificEnthalpyValue{baseEnthalpy} + ) .value(); integrand = density - equationOfStateDensity; @@ -348,7 +351,7 @@ namespace { const double densityDerivative = eos::partialDerivative( - barotrope, eos::SpecificEnthalpyValue{baseEnthalpy} + barotrope, dimensions::SpecificEnthalpyValue{baseEnthalpy} ) .value(); @@ -640,16 +643,18 @@ namespace mean_field::operators::kernels { enthalpyElement.CalcShape(integrationPoint, enthalpyShape); - const double densityValue = elementBaseDensity * densityShape; + const double densityValue = elementBaseDensity * densityShape; - const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; + const double enthalpyValue = elementBaseEnthalpy * enthalpyShape; - const double equationOfStateDensity = - eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpyValue}).value(); + const double equationOfStateDensity = eos::evaluate( + barotrope, dimensions::SpecificEnthalpyValue{enthalpyValue} + ) + .value(); - const double closureValue = densityValue - equationOfStateDensity; + const double closureValue = densityValue - equationOfStateDensity; - const double geometryActionValue = closureValue * mappingVariation.weight_variation; + const double geometryActionValue = closureValue * mappingVariation.weight_variation; MFEM_VERIFY( std::isfinite(closureValue) && std::isfinite(geometryActionValue), diff --git a/libmeanfield/impl/operators/kernels/gravity_kernels.cpp b/libmeanfield/impl/operators/kernels/gravity_kernels.cpp index bec9823..ac7124d 100644 --- a/libmeanfield/impl/operators/kernels/gravity_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/gravity_kernels.cpp @@ -490,6 +490,8 @@ namespace mean_field::operators::kernels { mfem::DenseMatrix gravity_gradient_shape; mfem::DenseMatrix mass_tensor_variation; + mapping::VolumeMappingContext mapping_context; + mapping::VolumeMappingVariation mapping_variation; for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { const mfem::FiniteElement &gravity_gradient_element = *f.gravityFluxFes->GetFE(element_id); @@ -555,7 +557,6 @@ namespace mean_field::operators::kernels { const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); transformation->SetIntPoint(&integration_point); - mapping::VolumeMappingContext mapping_context; const mapping::MappingStatus status = domain_mapper.EvaluateVolume( mapping_data, *transformation, integration_point, workspace, mapping_context ); @@ -568,7 +569,6 @@ namespace mean_field::operators::kernels { << ", status: " << static_cast(status) ); - mapping::VolumeMappingVariation mapping_variation; const mapping::MappingStatus variation_status = domain_mapper.EvaluateVolumeVariation( mapping_data, displacement_variation_data, *transformation, integration_point, mapping_context, workspace, mapping_variation diff --git a/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp b/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp index b1b3b42..f3d7261 100644 --- a/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp +++ b/libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp @@ -12,9 +12,10 @@ module mean_field; import :operators.kernels.pressure_force; namespace { - namespace eos = mean_field::eos; + namespace dimensions = mean_field::dimensions; + namespace eos = mean_field::eos; - using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; [[nodiscard]] bool is_vacuum_attribute(const int attribute) { return DomainSchema::template attribute_belongs_to(attribute); @@ -416,14 +417,15 @@ namespace { if (pressureForceAction == PressureForceAction::residual || pressureForceAction == PressureForceAction::displacement) { - pressureFactor = - eos::evaluate(barotrope, eos::SpecificEnthalpyValue{enthalpyValue}) - .value(); + pressureFactor = eos::evaluate( + barotrope, dimensions::SpecificEnthalpyValue{enthalpyValue} + ) + .value(); } else { const double enthalpyVariationValue = elementEnthalpyVariation * enthalpyShape; pressureFactor = eos::partialDerivative( - barotrope, eos::SpecificEnthalpyValue{enthalpyValue} + barotrope, dimensions::SpecificEnthalpyValue{enthalpyValue} ) .value() * enthalpyVariationValue; diff --git a/libmeanfield/impl/operators/prepared_barotropic_closure.cpp b/libmeanfield/impl/operators/prepared_barotropic_closure.cpp index 391b7e5..e7dcb9c 100644 --- a/libmeanfield/impl/operators/prepared_barotropic_closure.cpp +++ b/libmeanfield/impl/operators/prepared_barotropic_closure.cpp @@ -287,7 +287,6 @@ namespace mean_field::operators { mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); - mfem::Array displacementDofs; mfem::Array compactificationDofs; mfem::Vector elementBaseDensity; @@ -311,18 +310,19 @@ namespace mean_field::operators { m_elements.emplace_back(); ElementPAData &data = m_elements.back(); + data.elementId = elementId; data.densityDofTransformation = m_fem.densityFes->GetElementDofs(elementId, data.densityDofs); data.enthalpyDofTransformation = m_fem.enthalpyFes->GetElementDofs(elementId, data.enthalpyDofs); - mfem::DofTransformation *displacementDofTransformation = - m_fem.displacementFes->GetElementVDofs(elementId, displacementDofs); + data.displacementDofTransformation = + m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs); mfem::DofTransformation *compactificationDofTransformation = m_fem.compactificationFes->GetElementDofs(elementId, compactificationDofs); baseDensityLocal.GetSubVector(data.densityDofs, elementBaseDensity); baseEnthalpyLocal.GetSubVector(data.enthalpyDofs, elementBaseEnthalpy); - displacementLocal.GetSubVector(displacementDofs, elementDisplacement); + displacementLocal.GetSubVector(data.displacementDofs, elementDisplacement); m_fem.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); if (data.densityDofTransformation != nullptr) { @@ -331,8 +331,8 @@ namespace mean_field::operators { if (data.enthalpyDofTransformation != nullptr) { data.enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy); } - if (displacementDofTransformation != nullptr) { - displacementDofTransformation->InvTransformPrimal(elementDisplacement); + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(elementDisplacement); } if (compactificationDofTransformation != nullptr) { compactificationDofTransformation->InvTransformPrimal(elementCompactification); @@ -363,6 +363,9 @@ namespace mean_field::operators { data.densityBasis.SetSize(quadraturePointCount, densityDofCount); data.enthalpyBasis.SetSize(quadraturePointCount, enthalpyDofCount); + data.inverseElementJacobians.SetSize( + quadraturePointCount, m_fem.mesh->Dimension() * m_fem.mesh->Dimension() + ); data.weightedResidual.SetSize(quadraturePointCount); data.quadratureWeights.SetSize(quadraturePointCount); data.weightedEnthalpyDerivative.SetSize(quadraturePointCount); @@ -388,6 +391,18 @@ namespace mean_field::operators { << ", quadrature point: " << quadraturePoint << ", status: " << static_cast(mappingStatus) ); + MFEM_VERIFY( + !mappingContext.mapping.compactified, + "Prepared barotropic closure support unexpectedly includes a compactified element." + ); + + for (int row = 0; row < m_fem.mesh->Dimension(); ++row) { + for (int column = 0; column < m_fem.mesh->Dimension(); ++column) { + data.inverseElementJacobians(quadraturePoint, row * m_fem.mesh->Dimension() + column) = + mappingContext.quadrature.J_inv(row, column); + } + } + densityElement.CalcShape(integrationPoint, densityShape); enthalpyElement.CalcShape(integrationPoint, enthalpyShape); @@ -401,7 +416,7 @@ namespace mean_field::operators { const double density = elementBaseDensity * densityShape; const double enthalpy = elementBaseEnthalpy * enthalpyShape; const double quadratureWeight = mappingContext.quadrature.weight; - const eos::SpecificEnthalpyValue specificEnthalpy{enthalpy}; + const dimensions::SpecificEnthalpyValue specificEnthalpy{enthalpy}; const double eosDensity = eos::evaluate(m_equationOfState, specificEnthalpy).value(); const double enthalpyDerivative = @@ -485,10 +500,7 @@ namespace mean_field::operators { ApplyThermodynamicActionFull(m_densityVariationTrue, m_enthalpyVariationTrue, m_fullThermodynamicAction); - kernels::apply_barotropic_closure_displacement_action( - m_fem, m_domainMapper, m_equationOfState, m_baseDensityTrue, m_baseEnthalpyTrue, m_baseDisplacementTrue, - m_displacementVariationTrue, m_fullDisplacementAction - ); + ApplyDisplacementActionFull(m_displacementVariationTrue, m_fullDisplacementAction); MFEM_VERIFY( m_fullThermodynamicAction.Size() == m_densityMap.full_size() && @@ -589,6 +601,86 @@ namespace mean_field::operators { local_to_true(*m_fem.densityFes, localAction, actionTrue); } + void PreparedBarotropicClosureOperator::ApplyDisplacementActionFull( + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionTrue + ) const { + MFEM_VERIFY( + displacementVariationTrue.Size() == m_displacementMap.full_size(), + "The full displacement variation has the wrong size." + ); + + true_to_local(*m_fem.displacementFes, displacementVariationTrue, m_displacementVariationLocal); + + m_localDisplacementAction.SetSize(m_fem.densityFes->GetVSize()); + m_localDisplacementAction = 0.0; + + const int dimension = m_fem.mesh->Dimension(); + + for (const ElementPAData &data : m_elements) { + m_displacementVariationLocal.GetSubVector(data.displacementDofs, m_elementDisplacementVariation); + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(m_elementDisplacementVariation); + } + + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, m_elementDisplacementVariation); + const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix(); + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); + MFEM_VERIFY( + transformation != nullptr, + "Prepared barotropic closure displacement action received a null element transformation." + ); + const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(data.elementId); + const mfem::FiniteElement &enthalpyElement = *m_fem.enthalpyFes->GetFE(data.elementId); + const mfem::IntegrationRule &integrationRule = + get_eos_rule(m_fem, m_equationOfState, densityElement, enthalpyElement, *transformation); + + MFEM_VERIFY( + data.inverseElementJacobians.Height() == integrationRule.GetNPoints() && + data.inverseElementJacobians.Width() == dimension * dimension, + "Prepared barotropic closure inverse-Jacobian data has an incompatible size." + ); + + m_referenceDShape.SetSize(displacementElement.GetDof(), dimension); + m_referenceDisplacementJacobian.SetSize(dimension, dimension); + m_quadratureDisplacementAction.SetSize(integrationRule.GetNPoints()); + + for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadraturePoint); + displacementElement.CalcDShape(integrationPoint, m_referenceDShape); + mfem::MultAtB(directionDofs, m_referenceDShape, m_referenceDisplacementJacobian); + + double logarithmicJacobianVariation{0.0}; + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + logarithmicJacobianVariation += + data.inverseElementJacobians(quadraturePoint, row * dimension + column) * + m_referenceDisplacementJacobian(column, row); + } + } + + m_quadratureDisplacementAction(quadraturePoint) = + data.weightedResidual(quadraturePoint) * logarithmicJacobianVariation; + MFEM_VERIFY( + std::isfinite(m_quadratureDisplacementAction(quadraturePoint)), + "Prepared barotropic closure displacement action encountered a non-finite quadrature value." + ); + } + + m_elementDisplacementAction.SetSize(data.densityDofs.Size()); + data.densityBasis.MultTranspose(m_quadratureDisplacementAction, m_elementDisplacementAction); + + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->TransformDual(m_elementDisplacementAction); + } + m_localDisplacementAction.AddElementVector(data.densityDofs, m_elementDisplacementAction); + } + + local_to_true(*m_fem.densityFes, m_localDisplacementAction, actionTrue); + } + bool PreparedBarotropicClosureOperator::IsPrepared() const noexcept { return m_isPrepared && m_context.IsPrepared(); } diff --git a/libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp b/libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp new file mode 100644 index 0000000..1e19088 --- /dev/null +++ b/libmeanfield/impl/operators/prepared_central_density_stellar_equilibrium.cpp @@ -0,0 +1,223 @@ +module; + +#include +#include +#include +#include +#include + +#include + +module mean_field; + +import :operators.prepared_central_density_stellar_equilibrium; + +namespace { + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + using PhysicalForm = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form; + using BorderedForm = mean_field::operators::CentralDensityStellarEquilibriumForm; + + [[nodiscard]] std::array< + int, + BorderedForm::value_block_count> + make_value_sizes(const mean_field::operators::StellarEquilibriumLayout &physicalLayout) { + std::array sizes{}; + for (int block = 0; block < PhysicalForm::value_block_count; ++block) { + sizes[block] = physicalLayout.value_offsets()[block + 1] - physicalLayout.value_offsets()[block]; + } + sizes[PhysicalForm::value_block_count] = 1; + return sizes; + } + + [[nodiscard]] std::array< + int, + BorderedForm::residual_block_count> + make_residual_sizes(const mean_field::operators::StellarEquilibriumLayout &physicalLayout) { + std::array sizes{}; + for (int block = 0; block < PhysicalForm::residual_block_count; ++block) { + sizes[block] = physicalLayout.residual_offsets()[block + 1] - physicalLayout.residual_offsets()[block]; + } + sizes[PhysicalForm::residual_block_count] = 1; + return sizes; + } + + [[nodiscard]] mean_field::operators::CentralDensityDependencies + make_phase_dependencies(const mean_field::operators::StellarEquilibriumDependencies &dependencies) { + return {.enthalpy = {.identity = dependencies.enthalpy.identity, .revision = dependencies.enthalpy.revision}}; + } + + void validate_finite_scalar( + const double value, + const char *message + ) { + MFEM_VERIFY(std::isfinite(value), message); + } +} // namespace + +namespace mean_field::operators { + field::FieldPointDofMap PreparedCentralDensityStellarEquilibriumOperator::MakeCenterDofMap(const fem::FEM &f) { + MFEM_VERIFY( + f.mesh != nullptr && f.enthalpyFes != nullptr, + "The central-density phase requires the mesh and enthalpy finite-element space." + ); + const field::FieldDofMap enthalpyMap = field::make_field_dof_map(*f.enthalpyFes); + mfem::Vector origin(f.mesh->SpaceDimension()); + origin = 0.0; + return field::make_field_point_dof_map(*f.enthalpyFes, enthalpyMap, origin, 1.0e-12); + } + + PreparedCentralDensityStellarEquilibriumOperator::PreparedCentralDensityStellarEquilibriumOperator( + fem::FEM &f, + std::unique_ptr physicalOperator, + models::CompiledFixedCentralDensity centralDensity, + field::FieldPointDofMap centerDof + ) + : mfem::Operator( + physicalOperator->Height() + 1, + physicalOperator->Width() + 1 + ), + m_physicalOperator(std::move(physicalOperator)), + m_centralDensity(std::move(centralDensity)), + m_phaseConstraint( + std::move(centerDof), + f.mesh->GetComm() + ), + m_rootManifest( + make_value_sizes(m_physicalOperator->GetLayout()), + make_residual_sizes(m_physicalOperator->GetLayout()), + m_physicalOperator->GetTargetMass(), + m_physicalOperator->GetSurfaceConstraintOperator().GetPhysicalCondition().targetPressure, + m_physicalOperator->GetSurfaceConstraintOperator().GetSurfaceRows().size(), + CentralDensityManifestInput{ + .targetDensity = m_centralDensity.targetDensity().value(), + .targetEnthalpy = m_centralDensity.targetEnthalpy().value(), + .centerDofCount = 1 + } + ) { + MFEM_VERIFY( + Width() == m_rootManifest.layout().value_offsets().Last() && + Height() == m_rootManifest.layout().residual_offsets().Last(), + "The central-density bordered root has inconsistent dimensions." + ); + } + + PreparedCentralDensityStellarEquilibriumReport PreparedCentralDensityStellarEquilibriumOperator::Prepare( + const mfem::Vector &state, + const StellarEquilibriumDependencies &dependencies, + const physics::RigidRotation &rotation + ) { + MFEM_VERIFY(state.Size() == Width(), "The central-density bordered root received a state with the wrong size."); + const auto stateView = m_rootManifest.stateView(state); + const mfem::Vector enthalpy = stateView.block(utils::blocks::enthalpy_field.specific_term); + const mfem::Vector border = stateView.block(utils::blocks::fixed_central_density_phase.central_value_term); + validate_finite_scalar(border(0), "The central-density bordered root received a non-finite border value."); + + mfem::Vector physicalState(const_cast(state.GetData()), m_physicalOperator->Width()); + + m_isPrepared = false; + PreparedCentralDensityStellarEquilibriumReport report; + report.physical = m_physicalOperator->Prepare(physicalState, dependencies, rotation); + report.phase = + m_phaseConstraint.Prepare(m_centralDensity, enthalpy, border(0), make_phase_dependencies(dependencies)); + + if (report.physical.assembledResidual || report.phase.DidAnyWork() || m_cachedResidual.Size() != Height()) { + AssembleResidual(); + report.assembledResidual = true; + } + + m_isPrepared = true; + return report; + } + + void PreparedCentralDensityStellarEquilibriumOperator::AssembleResidual() { + mfem::Vector physicalResidual; + m_physicalOperator->BuildResidual(physicalResidual); + + m_cachedResidual.SetSize(Height()); + m_cachedResidual = 0.0; + mfem::Vector physicalDestination(m_cachedResidual.GetData(), physicalResidual.Size()); + physicalDestination = physicalResidual; + + const auto residualView = m_rootManifest.residualView(m_cachedResidual); + mfem::Vector enthalpyResidual = residualView.block(utils::blocks::enthalpy_field.specific_term); + mfem::Vector phaseResidual = residualView.block(utils::blocks::fixed_central_density_phase.central_value_term); + m_phaseConstraint.AddResidual(enthalpyResidual, phaseResidual); + } + + void PreparedCentralDensityStellarEquilibriumOperator::BuildResidual(mfem::Vector &residual) const { + VerifyPrepared(); + residual = m_cachedResidual; + } + + void PreparedCentralDensityStellarEquilibriumOperator::Mult( + const mfem::Vector &direction, + mfem::Vector &action + ) const { + VerifyPrepared(); + MFEM_VERIFY( + direction.Size() == Width(), "The central-density bordered root received a direction with the wrong size." + ); + const auto directionView = m_rootManifest.directionView(direction); + const mfem::Vector enthalpyDirection = directionView.block(utils::blocks::enthalpy_field.specific_term); + const mfem::Vector borderDirection = + directionView.block(utils::blocks::fixed_central_density_phase.central_value_term); + validate_finite_scalar( + borderDirection(0), "The central-density bordered root received a non-finite border direction." + ); + + mfem::Vector physicalDirection(const_cast(direction.GetData()), m_physicalOperator->Width()); + mfem::Vector physicalAction; + m_physicalOperator->Mult(physicalDirection, physicalAction); + + action.SetSize(Height()); + action = 0.0; + mfem::Vector physicalDestination(action.GetData(), physicalAction.Size()); + physicalDestination = physicalAction; + + const auto actionView = m_rootManifest.residualView(action); + mfem::Vector enthalpyAction = actionView.block(utils::blocks::enthalpy_field.specific_term); + mfem::Vector phaseAction = actionView.block(utils::blocks::fixed_central_density_phase.central_value_term); + m_phaseConstraint.ApplyJacobian( + {.enthalpyVariation = enthalpyDirection, .borderVariation = borderDirection(0)}, + {.enthalpyAction = enthalpyAction, .phaseAction = phaseAction} + ); + } + + bool PreparedCentralDensityStellarEquilibriumOperator::IsPrepared() const noexcept { + return m_isPrepared && m_physicalOperator->IsPrepared() && m_phaseConstraint.IsPrepared(); + } + + const CentralDensityStellarEquilibriumLayout & + PreparedCentralDensityStellarEquilibriumOperator::GetLayout() const noexcept { + return m_rootManifest.layout(); + } + + const CentralDensityStellarEquilibriumRootManifest & + PreparedCentralDensityStellarEquilibriumOperator::GetRootManifest() const noexcept { + return m_rootManifest; + } + + const PreparedStellarEquilibriumOperator & + PreparedCentralDensityStellarEquilibriumOperator::GetPhysicalOperator() const noexcept { + return *m_physicalOperator; + } + + const PreparedCentralDensityConstraint & + PreparedCentralDensityStellarEquilibriumOperator::GetCentralDensityConstraint() const noexcept { + return m_phaseConstraint; + } + + RootConstraintReport PreparedCentralDensityStellarEquilibriumOperator::GetFixedMassReport() const { + VerifyPrepared(); + return m_physicalOperator->GetFixedMassReport(); + } + + CentralDensityConstraintReport PreparedCentralDensityStellarEquilibriumOperator::GetCentralDensityReport() const { + VerifyPrepared(); + return m_phaseConstraint.GetConstraintReport(); + } + + void PreparedCentralDensityStellarEquilibriumOperator::VerifyPrepared() const { + MFEM_VERIFY(IsPrepared(), "The central-density bordered root must be prepared before application."); + } +} // namespace mean_field::operators diff --git a/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp b/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp index 8d0bdd6..ca912d2 100644 --- a/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp +++ b/libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp @@ -8,6 +8,8 @@ import :operators.kernels.gravity_displacement_force; import :operators.prepared_gravity_displacement_force; namespace { + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + [[nodiscard]] bool relevant_revisions_match( const mean_field::operators::context::gravity_field::GravityFieldRevisions &left, const mean_field::operators::context::gravity_field::GravityFieldRevisions &right @@ -15,6 +17,71 @@ namespace { return left.discretization == right.discretization && left.displacement == right.displacement && left.density == right.density && left.gravity_gradient == right.gravity_gradient; } + + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); + } + + void true_to_local( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, + mfem::Vector &localVector + ) { + localVector.SetSize(finiteElementSpace.GetVSize()); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } + } + + void local_to_true( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, + mfem::Vector &trueVector + ) { + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } + } + + [[nodiscard]] int vector_dof_index( + const mfem::Ordering::Type ordering, + const int scalarDof, + const int component, + const int scalarDofCount, + const int dimension + ) { + if (ordering == mfem::Ordering::byNODES) { + return scalarDof + component * scalarDofCount; + } + MFEM_VERIFY(ordering == mfem::Ordering::byVDIM, "Unsupported displacement ordering."); + return scalarDof * dimension + component; + } + + [[nodiscard]] const mfem::IntegrationRule &get_gravity_force_rule( + const mean_field::fem::FEM &f, + const mfem::ElementTransformation &transformation + ) { + using DisplacementField = mean_field::field::Field; + const mean_field::quadrature::Query query = + DisplacementField::make_query( + mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, + mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general + ); + const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY( + rule.integration_rule != nullptr, + "The quadrature policy did not return a gravity-displacement-force integration rule." + ); + return *rule.integration_rule; + } } // namespace namespace mean_field::operators { @@ -41,6 +108,122 @@ namespace mean_field::operators { ); } + void PreparedGravityDisplacementForceOperator::PrepareElementData() { + m_elements.clear(); + m_elements.reserve(m_fem.mesh->GetNE()); + + mfem::Vector baseDensityLocal; + mfem::Vector baseGravityGradientLocal; + mfem::Vector baseDisplacementLocal; + true_to_local(*m_fem.densityFes, m_gravityContext.GetDensityTrue(), baseDensityLocal); + true_to_local(*m_fem.gravityFluxFes, m_gravityContext.GetGravityGradientTrue(), baseGravityGradientLocal); + true_to_local( + *m_fem.displacementFes, m_gravityContext.GetGeometryContext().GetDisplacementTrue(), baseDisplacementLocal + ); + + mapping::DomainMapper::Workspace workspace(m_domainMapper.GetDimension()); + mapping::VolumeMappingContext mappingContext; + mfem::Array compactificationDofs; + mfem::Vector elementBaseDensity; + mfem::Vector elementBaseGravityGradient; + mfem::Vector elementBaseDisplacement; + mfem::Vector elementCompactification; + mfem::Vector densityShape; + mfem::Vector baseGravityReferenceValue; + mfem::DenseMatrix gravityGradientShape; + + const int dimension = m_domainMapper.GetDimension(); + + for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId); + MFEM_VERIFY(transformation != nullptr, "Prepared gravity force received a null transformation."); + if (is_vacuum_attribute(transformation->Attribute)) { + continue; + } + + m_elements.emplace_back(); + ElementPAData &data = m_elements.back(); + data.elementId = elementId; + data.densityDofTransformation = m_fem.densityFes->GetElementDofs(elementId, data.densityDofs); + data.gravityGradientDofTransformation = + m_fem.gravityFluxFes->GetElementVDofs(elementId, data.gravityGradientDofs); + data.displacementDofTransformation = + m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs); + mfem::DofTransformation *compactificationDofTransformation = + m_fem.compactificationFes->GetElementDofs(elementId, compactificationDofs); + + baseDensityLocal.GetSubVector(data.densityDofs, elementBaseDensity); + baseGravityGradientLocal.GetSubVector(data.gravityGradientDofs, elementBaseGravityGradient); + baseDisplacementLocal.GetSubVector(data.displacementDofs, elementBaseDisplacement); + m_fem.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal(elementBaseDensity); + } + if (data.gravityGradientDofTransformation != nullptr) { + data.gravityGradientDofTransformation->InvTransformPrimal(elementBaseGravityGradient); + } + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(elementBaseDisplacement); + } + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal(elementCompactification); + } + + const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(elementId); + const mfem::FiniteElement &gravityGradientElement = *m_fem.gravityFluxFes->GetFE(elementId); + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(elementId); + const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(elementId); + data.integrationRule = &get_gravity_force_rule(m_fem, *transformation); + + const mapping::ElementDisplacementData displacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementBaseDisplacement); + const mapping::ElementCompactificationData compactificationData( + compactificationElement, elementCompactification + ); + const mapping::ElementMappingData mappingData{ + .displacement = displacementData, .compactification = compactificationData + }; + + const int quadraturePointCount = data.integrationRule->GetNPoints(); + data.mappingJacobians.SetSize(quadraturePointCount, dimension * dimension); + data.inverseMeshJacobians.SetSize(quadraturePointCount, dimension * dimension); + data.baseGravityReferenceValues.SetSize(quadraturePointCount, dimension); + data.baseDensityValues.SetSize(quadraturePointCount); + data.referenceWeights.SetSize(quadraturePointCount); + densityShape.SetSize(densityElement.GetDof()); + gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension); + baseGravityReferenceValue.SetSize(dimension); + + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); + const mapping::MappingStatus status = m_domainMapper.EvaluateVolume( + mappingData, *transformation, integrationPoint, workspace, mappingContext + ); + MFEM_VERIFY( + status == mapping::MappingStatus::valid && !mappingContext.mapping.compactified, + "Prepared gravity force encountered an invalid stellar mapping." + ); + + densityElement.CalcShape(integrationPoint, densityShape); + gravityGradientElement.CalcVShape(*transformation, gravityGradientShape); + gravityGradientShape.MultTranspose(elementBaseGravityGradient, baseGravityReferenceValue); + data.baseDensityValues(quadraturePoint) = elementBaseDensity * densityShape; + data.referenceWeights(quadraturePoint) = integrationPoint.weight * transformation->Weight(); + + const mfem::DenseMatrix &inverseMeshJacobian = transformation->InverseJacobian(); + for (int row = 0; row < dimension; ++row) { + data.baseGravityReferenceValues(quadraturePoint, row) = baseGravityReferenceValue(row); + for (int column = 0; column < dimension; ++column) { + const int entry = row * dimension + column; + data.mappingJacobians(quadraturePoint, entry) = + mappingContext.mapping.mapping_jacobian(row, column); + data.inverseMeshJacobians(quadraturePoint, entry) = inverseMeshJacobian(row, column); + } + } + } + } + } + PreparedGravityDisplacementForceReport PreparedGravityDisplacementForceOperator::Prepare() { MFEM_VERIFY( m_gravityContext.IsPrepared(), "PreparedGravityDisplacementForceOperator requires the shared " @@ -59,6 +242,7 @@ namespace mean_field::operators { ); m_cachedResidual.SetSize(m_gravityContext.GetDisplacementMap().reduced_size()); m_gravityContext.GetDisplacementMap().gather(m_actionTrue, m_cachedResidual); + PrepareElementData(); m_preparedRevisions = requestedRevisions; ++m_residualPreparationCount; @@ -130,6 +314,117 @@ namespace mean_field::operators { ++m_displacementJacobianStatistics.applications; } + void PreparedGravityDisplacementForceOperator::ApplyPreparedCompleteJacobianActionTrue( + const mfem::Vector &densityVariationTrue, + const mfem::Vector &displacementVariationTrue, + const mfem::Vector &gravityGradientVariationTrue, + mfem::Vector &actionTrue + ) const { + true_to_local(*m_fem.densityFes, densityVariationTrue, m_densityVariationLocal); + true_to_local(*m_fem.gravityFluxFes, gravityGradientVariationTrue, m_gravityGradientVariationLocal); + true_to_local(*m_fem.displacementFes, displacementVariationTrue, m_displacementVariationLocal); + m_localAction.SetSize(m_fem.displacementFes->GetVSize()); + m_localAction = 0.0; + + const int dimension = m_domainMapper.GetDimension(); + const mfem::Ordering::Type ordering = m_fem.displacementFes->GetOrdering(); + + for (const ElementPAData &data : m_elements) { + MFEM_VERIFY(data.integrationRule != nullptr, "Prepared gravity force has no integration rule."); + + m_densityVariationLocal.GetSubVector(data.densityDofs, m_elementDensityVariation); + m_gravityGradientVariationLocal.GetSubVector(data.gravityGradientDofs, m_elementGravityGradientVariation); + m_displacementVariationLocal.GetSubVector(data.displacementDofs, m_elementDisplacementVariation); + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal(m_elementDensityVariation); + } + if (data.gravityGradientDofTransformation != nullptr) { + data.gravityGradientDofTransformation->InvTransformPrimal(m_elementGravityGradientVariation); + } + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(m_elementDisplacementVariation); + } + + const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(data.elementId); + const mfem::FiniteElement &gravityGradientElement = *m_fem.gravityFluxFes->GetFE(data.elementId); + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); + MFEM_VERIFY(transformation != nullptr, "Prepared gravity force received a null transformation."); + + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, m_elementDisplacementVariation); + const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix(); + const int scalarDisplacementDofCount = displacementElement.GetDof(); + + m_densityShape.SetSize(densityElement.GetDof()); + m_displacementShape.SetSize(scalarDisplacementDofCount); + m_gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension); + m_referenceDisplacementDShape.SetSize(scalarDisplacementDofCount, dimension); + m_referenceDisplacementJacobian.SetSize(dimension, dimension); + m_displacementJacobianVariation.SetSize(dimension, dimension); + m_mappingJacobian.SetSize(dimension, dimension); + m_inverseMeshJacobian.SetSize(dimension, dimension); + m_baseGravityReferenceValue.SetSize(dimension); + m_gravityVariationReferenceValue.SetSize(dimension); + m_mappedBaseGravity.SetSize(dimension); + m_mappedGravityVariation.SetSize(dimension); + m_mappedGeometryVariation.SetSize(dimension); + m_forceValue.SetSize(dimension); + m_elementAction.SetSize(data.displacementDofs.Size()); + m_elementAction = 0.0; + + for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); + densityElement.CalcShape(integrationPoint, m_densityShape); + displacementElement.CalcShape(integrationPoint, m_displacementShape); + displacementElement.CalcDShape(integrationPoint, m_referenceDisplacementDShape); + mfem::MultAtB(directionDofs, m_referenceDisplacementDShape, m_referenceDisplacementJacobian); + + transformation->SetIntPoint(&integrationPoint); + gravityGradientElement.CalcVShape(*transformation, m_gravityGradientShape); + m_gravityGradientShape.MultTranspose( + m_elementGravityGradientVariation, m_gravityVariationReferenceValue + ); + + for (int row = 0; row < dimension; ++row) { + m_baseGravityReferenceValue(row) = data.baseGravityReferenceValues(quadraturePoint, row); + for (int column = 0; column < dimension; ++column) { + const int entry = row * dimension + column; + m_mappingJacobian(row, column) = data.mappingJacobians(quadraturePoint, entry); + m_inverseMeshJacobian(row, column) = data.inverseMeshJacobians(quadraturePoint, entry); + } + } + mfem::Mult(m_referenceDisplacementJacobian, m_inverseMeshJacobian, m_displacementJacobianVariation); + m_mappingJacobian.Mult(m_baseGravityReferenceValue, m_mappedBaseGravity); + m_mappingJacobian.Mult(m_gravityVariationReferenceValue, m_mappedGravityVariation); + m_displacementJacobianVariation.Mult(m_baseGravityReferenceValue, m_mappedGeometryVariation); + + const double densityVariationValue = m_elementDensityVariation * m_densityShape; + const double baseDensityValue = data.baseDensityValues(quadraturePoint); + m_forceValue = 0.0; + m_forceValue.Add(densityVariationValue, m_mappedBaseGravity); + m_forceValue.Add(baseDensityValue, m_mappedGravityVariation); + m_forceValue.Add(baseDensityValue, m_mappedGeometryVariation); + m_forceValue *= data.referenceWeights(quadraturePoint); + + for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) { + for (int component = 0; component < dimension; ++component) { + const int vectorDof = + vector_dof_index(ordering, scalarDof, component, scalarDisplacementDofCount, dimension); + m_elementAction(vectorDof) += m_displacementShape(scalarDof) * m_forceValue(component); + } + } + } + + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->TransformDual(m_elementAction); + } + m_localAction.AddElementVector(data.displacementDofs, m_elementAction); + } + + local_to_true(*m_fem.displacementFes, m_localAction, actionTrue); + } + void PreparedGravityDisplacementForceOperator::ApplyCompleteJacobianAction( const mfem::Vector &densityVariation, const mfem::Vector &displacementVariation, @@ -145,10 +440,8 @@ namespace mean_field::operators { m_gravityContext.GetGravityGradientMap().scatter(gravityGradientVariation, m_gravityGradientVariationTrue); m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue); - kernels::apply_gravity_displacement_force_complete_action( - m_fem, m_domainMapper, m_gravityContext.GetDensityTrue(), m_densityVariationTrue, - m_gravityContext.GetGravityGradientTrue(), m_gravityGradientVariationTrue, m_displacementVariationTrue, - m_gravityContext.GetGeometryContext().GetDisplacementTrue(), m_actionTrue + ApplyPreparedCompleteJacobianActionTrue( + m_densityVariationTrue, m_displacementVariationTrue, m_gravityGradientVariationTrue, m_actionTrue ); action.SetSize(m_gravityContext.GetDisplacementMap().reduced_size()); m_gravityContext.GetDisplacementMap().gather(m_actionTrue, action); diff --git a/libmeanfield/impl/operators/prepared_gravity_source.cpp b/libmeanfield/impl/operators/prepared_gravity_source.cpp index c4279b7..045a9da 100644 --- a/libmeanfield/impl/operators/prepared_gravity_source.cpp +++ b/libmeanfield/impl/operators/prepared_gravity_source.cpp @@ -174,9 +174,15 @@ namespace { "non-finite mapping determinant." ); + m_inverse_element_jacobian = mapping_context.quadrature.J_inv; + return 4.0 * std::numbers::pi * mean_field::utils::G * mapping_determinant; } + [[nodiscard]] const mfem::DenseMatrix &GetInverseElementJacobian() const noexcept { + return m_inverse_element_jacobian; + } + private: void LoadElement(const int element_id) { if (element_id == m_cached_element_id) { @@ -230,6 +236,7 @@ namespace { std::unique_ptr m_compactification_data; mean_field::mapping::DomainMapper::Workspace m_workspace; + mfem::DenseMatrix m_inverse_element_jacobian; int m_cached_element_id{-1}; }; } // namespace @@ -336,6 +343,9 @@ namespace mean_field::operators { data.potential_dof_transformation = m_fem.gravityPotentialFes->GetElementDofs(element_id, data.potential_dofs); + data.displacement_dof_transformation = + m_fem.displacementFes->GetElementVDofs(element_id, data.displacement_dofs); + const mfem::FiniteElement &density_element = *m_fem.densityFes->GetFE(element_id); const mfem::FiniteElement &potential_element = *m_fem.gravityPotentialFes->GetFE(element_id); @@ -344,6 +354,7 @@ namespace mean_field::operators { const mfem::IntegrationRule &integration_rule = get_source_rule(m_fem, density_element, potential_element, transformation); + data.integration_rule = &integration_rule; const int quadrature_point_count = integration_rule.GetNPoints(); @@ -355,6 +366,9 @@ namespace mean_field::operators { data.potential_basis.SetSize(quadrature_point_count, potential_dof_count); + const int dimension = m_fem.mesh->Dimension(); + data.inverse_element_jacobians.SetSize(quadrature_point_count, dimension * dimension); + data.quadrature_data.SetSize(quadrature_point_count); mfem::Vector density_shape(density_dof_count); @@ -381,6 +395,14 @@ namespace mean_field::operators { const double coefficient_value = source_coefficient.Eval(transformation, integration_point); + const mfem::DenseMatrix &inverse_element_jacobian = source_coefficient.GetInverseElementJacobian(); + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + data.inverse_element_jacobians(quadrature_point, row * dimension + column) = + inverse_element_jacobian(row, column); + } + } + transformation.SetIntPoint(&integration_point); const double quadrature_value = integration_point.weight * transformation.Weight() * coefficient_value; @@ -463,6 +485,98 @@ namespace mean_field::operators { m_potential_map.gather(m_action_true, action); } + void PreparedMappedGravitySourceOperator::MultDisplacementVariationTrue( + const mfem::Vector &densityTrue, + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionVariationTrue + ) const { + MFEM_VERIFY( + m_is_prepared, + "PreparedMappedGravitySourceOperator must be prepared before applying a displacement variation." + ); + MFEM_VERIFY( + densityTrue.Size() == m_fem.densityFes->GetTrueVSize(), "The full density vector has the wrong size." + ); + MFEM_VERIFY( + displacementVariationTrue.Size() == m_fem.displacementFes->GetTrueVSize(), + "The full displacement variation has the wrong size." + ); + + true_to_local(*m_fem.densityFes, densityTrue, m_density_local); + true_to_local(*m_fem.displacementFes, displacementVariationTrue, m_displacement_variation_local); + + m_local_variation_action.SetSize(m_fem.gravityPotentialFes->GetVSize()); + m_local_variation_action = 0.0; + + const int dimension = m_fem.mesh->Dimension(); + + for (const ElementPAData &data : m_elements) { + MFEM_VERIFY( + data.integration_rule != nullptr, + "Prepared gravity source displacement variation has no integration rule." + ); + + m_density_local.GetSubVector(data.density_dofs, m_element_density); + m_displacement_variation_local.GetSubVector(data.displacement_dofs, m_element_displacement_variation); + + if (data.density_dof_transformation != nullptr) { + data.density_dof_transformation->InvTransformPrimal(m_element_density); + } + if (data.displacement_dof_transformation != nullptr) { + data.displacement_dof_transformation->InvTransformPrimal(m_element_displacement_variation); + } + + const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(data.element_id); + const mapping::ElementDisplacementData direction_data = mapping::ElementDisplacementDataFromElementVDofs( + displacement_element, m_element_displacement_variation + ); + const mfem::DenseMatrix &direction_dofs = direction_data.GetDofMatrix(); + + MFEM_VERIFY( + data.inverse_element_jacobians.Height() == data.integration_rule->GetNPoints() && + data.inverse_element_jacobians.Width() == dimension * dimension, + "Prepared gravity source inverse-Jacobian data has an incompatible size." + ); + + m_reference_displacement_dshape.SetSize(displacement_element.GetDof(), dimension); + m_reference_displacement_jacobian.SetSize(dimension, dimension); + m_quadrature_variation_action.SetSize(data.integration_rule->GetNPoints()); + data.density_basis.Mult(m_element_density, m_quadrature_variation_action); + + for (int quadrature_point = 0; quadrature_point < data.integration_rule->GetNPoints(); ++quadrature_point) { + const mfem::IntegrationPoint &integration_point = data.integration_rule->IntPoint(quadrature_point); + displacement_element.CalcDShape(integration_point, m_reference_displacement_dshape); + mfem::MultAtB(direction_dofs, m_reference_displacement_dshape, m_reference_displacement_jacobian); + + double logarithmic_jacobian_variation{0.0}; + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + logarithmic_jacobian_variation += + data.inverse_element_jacobians(quadrature_point, row * dimension + column) * + m_reference_displacement_jacobian(column, row); + } + } + + m_quadrature_variation_action(quadrature_point) *= + data.quadrature_data(quadrature_point) * logarithmic_jacobian_variation; + MFEM_VERIFY( + std::isfinite(m_quadrature_variation_action(quadrature_point)), + "Prepared gravity source displacement variation encountered a non-finite quadrature value." + ); + } + + m_element_variation_action.SetSize(data.potential_dofs.Size()); + data.potential_basis.MultTranspose(m_quadrature_variation_action, m_element_variation_action); + + if (data.potential_dof_transformation != nullptr) { + data.potential_dof_transformation->TransformDual(m_element_variation_action); + } + m_local_variation_action.AddElementVector(data.potential_dofs, m_element_variation_action); + } + + local_to_true(*m_fem.gravityPotentialFes, m_local_variation_action, actionVariationTrue); + } + void PreparedMappedGravitySourceOperator::MultTranspose( const mfem::Vector &potential, mfem::Vector &action diff --git a/libmeanfield/impl/operators/prepared_hdiv_mass.cpp b/libmeanfield/impl/operators/prepared_hdiv_mass.cpp index db655cb..330bd8f 100644 --- a/libmeanfield/impl/operators/prepared_hdiv_mass.cpp +++ b/libmeanfield/impl/operators/prepared_hdiv_mass.cpp @@ -35,6 +35,128 @@ namespace { } } + void local_to_true( + const mfem::ParFiniteElementSpace &finite_element_space, + const mfem::Vector &local_vector, + mfem::Vector &true_vector + ) { + true_vector.SetSize(finite_element_space.GetTrueVSize()); + true_vector = 0.0; + const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->MultTranspose(local_vector, true_vector); + } else { + true_vector = local_vector; + } + } + + mean_field::quadrature::MappingKind get_mapping_kind( + const mean_field::mapping::DomainMapper &domain_mapper, + const mfem::ElementTransformation &transformation + ) { + return domain_mapper.IsCompactifiedElement(transformation) ? mean_field::quadrature::MappingKind::kelvin + : mean_field::quadrature::MappingKind::general; + } + + const mfem::IntegrationRule &get_hdiv_mass_rule( + const mean_field::fem::FEM &f, + const mean_field::mapping::DomainMapper &domain_mapper, + const mfem::FiniteElement &element, + const mfem::ElementTransformation &transformation + ) { + using GravityField = mean_field::field::Field; + const mean_field::quadrature::Query query = + GravityField::make_query( + mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {}, + mean_field::utils::DOMAINS::ALL, get_mapping_kind(domain_mapper, transformation) + ); + const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY( + resolution.integration_rule != nullptr, + "The quadrature policy did not return an H(div) mass integration rule." + ); + return *resolution.integration_rule; + } + + int frozen_mapping_width(const int dimension) { + return 3 * dimension + 4 * dimension * dimension + 3; + } + + void freeze_mapping_context( + const mean_field::mapping::VolumeMappingContext &context, + const int quadrature_point, + mfem::DenseMatrix &data + ) { + const int dimension = context.mapping.reference_position.Size(); + const int displacement_jacobian_start = 3 * dimension; + const int mapping_jacobian_start = displacement_jacobian_start + dimension * dimension; + const int inverse_mapping_start = mapping_jacobian_start + dimension * dimension; + const int inverse_element_start = inverse_mapping_start + dimension * dimension; + const int scalar_start = inverse_element_start + dimension * dimension; + + for (int component = 0; component < dimension; ++component) { + data(quadrature_point, component) = context.mapping.reference_position(component); + data(quadrature_point, dimension + component) = context.mapping.displaced_position(component); + data(quadrature_point, 2 * dimension + component) = context.mapping.physical_position(component); + } + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + const int entry = row * dimension + column; + data(quadrature_point, displacement_jacobian_start + entry) = + context.mapping.displacement_jacobian(row, column); + data(quadrature_point, mapping_jacobian_start + entry) = context.mapping.mapping_jacobian(row, column); + data(quadrature_point, inverse_mapping_start + entry) = + context.mapping.inverse_mapping_jacobian(row, column); + data(quadrature_point, inverse_element_start + entry) = context.quadrature.J_inv(row, column); + } + } + data(quadrature_point, scalar_start) = context.mapping.mapping_determinant; + data(quadrature_point, scalar_start + 1) = context.quadrature.weight; + data(quadrature_point, scalar_start + 2) = context.mapping.compactified ? 1.0 : 0.0; + } + + void thaw_mapping_context( + const mfem::DenseMatrix &data, + const int quadrature_point, + const int dimension, + mean_field::mapping::VolumeMappingContext &context + ) { + const int displacement_jacobian_start = 3 * dimension; + const int mapping_jacobian_start = displacement_jacobian_start + dimension * dimension; + const int inverse_mapping_start = mapping_jacobian_start + dimension * dimension; + const int inverse_element_start = inverse_mapping_start + dimension * dimension; + const int scalar_start = inverse_element_start + dimension * dimension; + + context.mapping.reference_position.SetSize(dimension); + context.mapping.displaced_position.SetSize(dimension); + context.mapping.physical_position.SetSize(dimension); + context.mapping.displacement_jacobian.SetSize(dimension, dimension); + context.mapping.mapping_jacobian.SetSize(dimension, dimension); + context.mapping.inverse_mapping_jacobian.SetSize(dimension, dimension); + context.quadrature.J_inv.SetSize(dimension, dimension); + + for (int component = 0; component < dimension; ++component) { + context.mapping.reference_position(component) = data(quadrature_point, component); + context.mapping.displaced_position(component) = data(quadrature_point, dimension + component); + context.mapping.physical_position(component) = data(quadrature_point, 2 * dimension + component); + } + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + const int entry = row * dimension + column; + context.mapping.displacement_jacobian(row, column) = + data(quadrature_point, displacement_jacobian_start + entry); + context.mapping.mapping_jacobian(row, column) = data(quadrature_point, mapping_jacobian_start + entry); + context.mapping.inverse_mapping_jacobian(row, column) = + data(quadrature_point, inverse_mapping_start + entry); + context.quadrature.J_inv(row, column) = data(quadrature_point, inverse_element_start + entry); + } + } + context.mapping.mapping_determinant = data(quadrature_point, scalar_start); + context.mapping.compactified = data(quadrature_point, scalar_start + 2) != 0.0; + context.quadrature.detJ = context.mapping.mapping_determinant; + context.quadrature.weight = data(quadrature_point, scalar_start + 1); + } + int find_representative_element( const mean_field::fem::FEM &f, const mfem::Array &marker @@ -238,7 +360,8 @@ namespace mean_field::operators { field::make_field_dof_map< field::Displacement, DomainSchema>(*f.displacementFes) - ) { + ), + m_variationWorkspace(domain_mapper.GetDimension()) { MFEM_VERIFY(f.mesh != nullptr, "PreparedMappedHDivMassOperator requires a mesh."); MFEM_VERIFY( f.gravityFluxFes != nullptr, "PreparedMappedHDivMassOperator requires the " @@ -285,6 +408,78 @@ namespace mean_field::operators { validate_uniform_domain_discretization(f, m_vacuum_marker, vacuum_element_id); } + void PreparedMappedHDivMassOperator::PrepareVariationData() { + m_variationElements.clear(); + m_variationElements.reserve(m_fem.mesh->GetNE()); + + mfem::Vector displacementLocal; + true_to_local(*m_fem.displacementFes, m_displacement_true, displacementLocal); + + mfem::Vector elementDisplacement; + mfem::Vector elementCompactification; + mapping::VolumeMappingContext mappingContext; + + for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) { + m_variationElements.emplace_back(); + ElementVariationData &data = m_variationElements.back(); + data.elementId = elementId; + + data.gravityGradientDofTransformation = + m_fem.gravityFluxFes->GetElementVDofs(elementId, data.gravityGradientDofs); + data.displacementDofTransformation = + m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs); + mfem::DofTransformation *compactificationDofTransformation = + m_fem.compactificationFes->GetElementDofs(elementId, data.compactificationDofs); + + displacementLocal.GetSubVector(data.displacementDofs, elementDisplacement); + m_fem.compactificationCoordinate->GetSubVector(data.compactificationDofs, elementCompactification); + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(elementDisplacement); + } + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal(elementCompactification); + } + data.baseDisplacement = elementDisplacement; + data.compactification = elementCompactification; + + const mfem::FiniteElement &gravityGradientElement = *m_fem.gravityFluxFes->GetFE(elementId); + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(elementId); + const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(elementId); + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId); + MFEM_VERIFY( + transformation != nullptr, "Prepared H(div) variation data received a null element transformation." + ); + + data.integrationRule = &get_hdiv_mass_rule(m_fem, m_domain_mapper, gravityGradientElement, *transformation); + data.frozenMappingData.SetSize( + data.integrationRule->GetNPoints(), frozen_mapping_width(m_domain_mapper.GetDimension()) + ); + + const mapping::ElementDisplacementData displacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement); + const mapping::ElementCompactificationData compactificationData( + compactificationElement, data.compactification + ); + const mapping::ElementMappingData mappingData{ + .displacement = displacementData, .compactification = compactificationData + }; + + for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); + const mapping::MappingStatus status = m_domain_mapper.EvaluateVolume( + mappingData, *transformation, integrationPoint, m_variationWorkspace, mappingContext + ); + MFEM_VERIFY( + status == mapping::MappingStatus::valid, + "Prepared H(div) variation data encountered an invalid mapping. Element: " + << elementId << ", quadrature point: " << quadraturePoint + << ", status: " << static_cast(status) + ); + freeze_mapping_context(mappingContext, quadraturePoint, data.frozenMappingData); + } + } + } + void PreparedMappedHDivMassOperator::Prepare(const mfem::Vector &displacement) { MFEM_VERIFY( displacement.Size() == m_displacement_map.reduced_size(), @@ -346,6 +541,8 @@ namespace mean_field::operators { m_stellar_mass_form->Assemble(); m_vacuum_mass_form->Assemble(); + PrepareVariationData(); + m_is_prepared = true; ++m_preparation_count; } @@ -378,6 +575,110 @@ namespace mean_field::operators { m_flux_map.gather(m_action_true, action); } + void PreparedMappedHDivMassOperator::MultDisplacementVariationTrue( + const mfem::Vector &gravityGradientTrue, + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionVariationTrue + ) const { + MFEM_VERIFY( + m_is_prepared, "PreparedMappedHDivMassOperator must be prepared before applying a displacement variation." + ); + MFEM_VERIFY( + gravityGradientTrue.Size() == m_fem.gravityFluxFes->GetTrueVSize(), + "The full gravity-gradient vector has the wrong size." + ); + MFEM_VERIFY( + displacementVariationTrue.Size() == m_fem.displacementFes->GetTrueVSize(), + "The full displacement variation has the wrong size." + ); + + true_to_local(*m_fem.gravityFluxFes, gravityGradientTrue, m_gravityGradientLocal); + true_to_local(*m_fem.displacementFes, displacementVariationTrue, m_displacementVariationLocal); + m_localVariationAction.SetSize(m_fem.gravityFluxFes->GetVSize()); + m_localVariationAction = 0.0; + + const int dimension = m_domain_mapper.GetDimension(); + + for (const ElementVariationData &data : m_variationElements) { + MFEM_VERIFY( + data.integrationRule != nullptr && + data.frozenMappingData.Height() == data.integrationRule->GetNPoints() && + data.frozenMappingData.Width() == frozen_mapping_width(dimension), + "Prepared H(div) variation data is incomplete." + ); + + m_gravityGradientLocal.GetSubVector(data.gravityGradientDofs, m_elementGravityGradient); + m_displacementVariationLocal.GetSubVector(data.displacementDofs, m_elementDisplacementVariation); + if (data.gravityGradientDofTransformation != nullptr) { + data.gravityGradientDofTransformation->InvTransformPrimal(m_elementGravityGradient); + } + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(m_elementDisplacementVariation); + } + + const mfem::FiniteElement &gravityGradientElement = *m_fem.gravityFluxFes->GetFE(data.elementId); + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); + const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); + MFEM_VERIFY( + transformation != nullptr, + "Prepared H(div) displacement variation received a null element transformation." + ); + + const mapping::ElementDisplacementData baseDisplacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement); + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, m_elementDisplacementVariation); + const mapping::ElementCompactificationData compactificationData( + compactificationElement, data.compactification + ); + const mapping::ElementMappingData mappingData{ + .displacement = baseDisplacementData, .compactification = compactificationData + }; + + m_elementVariationAction.SetSize(gravityGradientElement.GetDof()); + m_elementVariationAction = 0.0; + m_gravityGradientValue.SetSize(dimension); + m_massTensorVariationAction.SetSize(dimension); + m_gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension); + m_massTensorVariation.SetSize(dimension, dimension); + + for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); + thaw_mapping_context(data.frozenMappingData, quadraturePoint, dimension, m_baseMappingContext); + + const mapping::MappingStatus status = m_domain_mapper.EvaluateVolumeVariation( + mappingData, directionData, *transformation, integrationPoint, m_baseMappingContext, + m_variationWorkspace, m_mappingVariation + ); + MFEM_VERIFY( + status == mapping::MappingStatus::valid, + "Prepared H(div) displacement variation encountered an invalid mapping variation. Element: " + << data.elementId << ", quadrature point: " << quadraturePoint + << ", status: " << static_cast(status) + ); + + mapping::ComputeHDivMassTensorVariation( + m_baseMappingContext.mapping, m_mappingVariation.mapping, m_massTensorVariation + ); + + transformation->SetIntPoint(&integrationPoint); + gravityGradientElement.CalcVShape(*transformation, m_gravityGradientShape); + m_gravityGradientShape.MultTranspose(m_elementGravityGradient, m_gravityGradientValue); + m_massTensorVariation.Mult(m_gravityGradientValue, m_massTensorVariationAction); + const double referenceWeight = integrationPoint.weight * transformation->Weight(); + m_gravityGradientShape.AddMult(m_massTensorVariationAction, m_elementVariationAction, referenceWeight); + } + + if (data.gravityGradientDofTransformation != nullptr) { + data.gravityGradientDofTransformation->TransformDual(m_elementVariationAction); + } + m_localVariationAction.AddElementVector(data.gravityGradientDofs, m_elementVariationAction); + } + + local_to_true(*m_fem.gravityFluxFes, m_localVariationAction, actionVariationTrue); + } + void PreparedMappedHDivMassOperator::AssembleDiagonal(mfem::Vector &diagonal) const { mfem::Vector true_diagonal; AssembleTrueDiagonal(true_diagonal); diff --git a/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp b/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp index 473be01..7bb8236 100644 --- a/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp @@ -992,6 +992,7 @@ namespace mean_field::operators { mfem::Vector elementDisplacementVariation; mfem::Vector weightedQuadratureVariation; mfem::Vector elementAction; + mapping::VolumeMappingVariation variation; for (const ElementPAData &data : m_elements) { MFEM_VERIFY( @@ -1040,11 +1041,7 @@ namespace mean_field::operators { for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); - transformation->SetIntPoint(&integrationPoint); - - mapping::VolumeMappingVariation variation; - - const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolumeVariation( + const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolumeVariation( mappingData, directionData, *transformation, integrationPoint, data.baseMappingContexts[quadraturePoint], workspace, variation ); diff --git a/libmeanfield/impl/operators/prepared_mass_normalization.cpp b/libmeanfield/impl/operators/prepared_mass_normalization.cpp index a10b463..0ae5a4f 100644 --- a/libmeanfield/impl/operators/prepared_mass_normalization.cpp +++ b/libmeanfield/impl/operators/prepared_mass_normalization.cpp @@ -42,6 +42,25 @@ namespace { } } + void local_to_true( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, + mfem::Vector &trueVector + ) { + MFEM_VERIFY(localVector.Size() == finiteElementSpace.GetVSize(), "Local vector has the wrong size."); + + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; + + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } + } + const mfem::IntegrationRule &get_mass_normalization_rule( const mean_field::fem::FEM &f, const mfem::FiniteElement &densityElement, @@ -212,6 +231,13 @@ namespace mean_field::operators { return report; } + PreparedMassNormalizationReport PreparedMassNormalizationOperator::Prepare( + const models::CompiledFixedMass &constraint, + const MassNormalizationDependencies &dependencies + ) { + return Prepare({.targetMass = constraint.targetMass().value()}, dependencies); + } + void PreparedMassNormalizationOperator::BuildStaticPlan() { m_elements.clear(); m_elements.reserve(m_fem.mesh->GetNE()); @@ -432,6 +458,7 @@ namespace mean_field::operators { true_to_local(*m_fem.displacementFes, displacementVariation, displacementVariationLocal); mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); + mapping::VolumeMappingVariation variation; mfem::Vector elementDisplacementVariation; double localAction = 0.0; @@ -464,8 +491,6 @@ namespace mean_field::operators { mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); for (const QuadraturePointData &point : data.quadraturePoints) { - mapping::VolumeMappingVariation variation; - const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation( mappingData, directionData, *transformation, point.integrationPoint, point.mappingContext, workspace, variation @@ -558,6 +583,137 @@ namespace mean_field::operators { ++m_actionStatistics.completeApplications; } + void PreparedMassNormalizationOperator::ApplyJacobian( + const FixedMassJacobianInput &input, + mfem::Vector &action + ) const { + ApplyCompleteJacobianAction(input.densityVariation, input.displacementVariation, action); + } + + void PreparedMassNormalizationOperator::AssembleDensityTransposeAction( + const double residualDual, + mfem::Vector &densityDual + ) const { + mfem::Vector localDual(m_fem.densityFes->GetVSize()); + localDual = 0.0; + + mfem::Vector elementDual; + + for (const ElementPAData &data : m_elements) { + elementDual.SetSize(data.densityDofs.Size()); + elementDual = 0.0; + + for (const QuadraturePointData &point : data.quadraturePoints) { + elementDual.Add(residualDual * point.mappingContext.quadrature.weight, point.densityShape); + } + + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->TransformDual(elementDual); + } + + localDual.AddElementVector(data.densityDofs, elementDual); + } + + mfem::Vector trueDual; + local_to_true(*m_fem.densityFes, localDual, trueDual); + + densityDual.SetSize(m_gravityContext.GetDensityMap().reduced_size()); + m_gravityContext.GetDensityMap().gather(trueDual, densityDual); + } + + void PreparedMassNormalizationOperator::AssembleDisplacementTransposeAction( + const double residualDual, + mfem::Vector &displacementDual + ) const { + mfem::Vector localDual(m_fem.displacementFes->GetVSize()); + localDual = 0.0; + + mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); + mapping::VolumeMappingVariation variation; + mfem::Vector elementDirection; + mfem::Vector elementDual; + + for (const ElementPAData &data : m_elements) { + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); + const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId); + + const mapping::ElementDisplacementData baseDisplacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement); + const mapping::ElementCompactificationData compactificationData( + compactificationElement, data.compactification + ); + const mapping::ElementMappingData mappingData{ + .displacement = baseDisplacementData, .compactification = compactificationData + }; + + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId); + + elementDirection.SetSize(data.displacementDofs.Size()); + elementDual.SetSize(data.displacementDofs.Size()); + elementDual = 0.0; + + for (int elementDof = 0; elementDof < elementDirection.Size(); ++elementDof) { + elementDirection = 0.0; + elementDirection(elementDof) = 1.0; + + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDirection); + + double elementDofAction = 0.0; + + for (const QuadraturePointData &point : data.quadraturePoints) { + const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation( + mappingData, directionData, *transformation, point.integrationPoint, point.mappingContext, + workspace, variation + ); + + MFEM_VERIFY( + status == mapping::MappingStatus::valid, + "Stateless mapping variation failed in the mass-normalization transpose action. Element: " + << data.elementId << ", status: " << static_cast(status) + ); + + elementDofAction += point.density * variation.weight_variation; + } + + elementDual(elementDof) = residualDual * elementDofAction; + } + + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->TransformDual(elementDual); + } + + localDual.AddElementVector(data.displacementDofs, elementDual); + } + + mfem::Vector trueDual; + local_to_true(*m_fem.displacementFes, localDual, trueDual); + + displacementDual.SetSize(m_gravityContext.GetDisplacementMap().reduced_size()); + m_gravityContext.GetDisplacementMap().gather(trueDual, displacementDual); + } + + void PreparedMassNormalizationOperator::ApplyCompleteJacobianTransposeAction( + const double residualDual, + mfem::Vector &densityDual, + mfem::Vector &displacementDual + ) const { + VerifyPrepared(); + MFEM_VERIFY(std::isfinite(residualDual), "Mass-normalization transpose action received a non-finite dual."); + + AssembleDensityTransposeAction(residualDual, densityDual); + AssembleDisplacementTransposeAction(residualDual, displacementDual); + ++m_actionStatistics.transposeApplications; + } + + void PreparedMassNormalizationOperator::ApplyJacobianTranspose( + const mfem::Vector &residualDual, + FixedMassJacobianTransposeOutput output + ) const { + MFEM_VERIFY(residualDual.Size() == 1, "Fixed-mass transpose action requires one residual dual value."); + ApplyCompleteJacobianTransposeAction(residualDual(0), output.densityDual, output.displacementDual); + } + double PreparedMassNormalizationOperator::GlobalSum(const double localValue) const { double globalValue = 0.0; MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_fem.mesh->GetComm()); @@ -720,6 +876,44 @@ namespace mean_field::operators { action(m_layout.offset(massResidual)) = massAction(0); } + void PreparedMassNormalizationJacobianOperator::MultTranspose( + const mfem::Vector &residualDual, + mfem::Vector &stateDual + ) const { + MFEM_VERIFY( + m_preparedOperator.IsPrepared(), + "Prepared mass-normalization MFEM adapter requires a prepared row operator." + ); + MFEM_VERIFY( + residualDual.Size() == Height(), + "Prepared mass-normalization MFEM adapter received a residual dual with the wrong size." + ); + + using Form = utils::blocks::barotropic_equilibrium_form; + constexpr auto densityValue = utils::blocks::get_value_block
(utils::blocks::density_field.mass_term); + constexpr auto displacementValue = + utils::blocks::get_value_block(utils::blocks::displacement_field.geometry_term); + constexpr auto massResidual = + utils::blocks::get_residual_block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); + + mfem::Vector densityDual; + mfem::Vector displacementDual; + m_preparedOperator.ApplyCompleteJacobianTransposeAction( + residualDual(m_layout.offset(massResidual)), densityDual, displacementDual + ); + + stateDual.SetSize(Width()); + stateDual = 0.0; + + mfem::Vector densityBlock(stateDual.GetData() + m_layout.offset(densityValue), m_layout.size(densityValue)); + densityBlock = densityDual; + + mfem::Vector displacementBlock( + stateDual.GetData() + m_layout.offset(displacementValue), m_layout.size(displacementValue) + ); + displacementBlock = displacementDual; + } + const MassNormalizationLayout &PreparedMassNormalizationJacobianOperator::GetLayout() const noexcept { return m_layout; } diff --git a/libmeanfield/impl/operators/prepared_pressure_force.cpp b/libmeanfield/impl/operators/prepared_pressure_force.cpp index c0a5e11..faf5dc1 100644 --- a/libmeanfield/impl/operators/prepared_pressure_force.cpp +++ b/libmeanfield/impl/operators/prepared_pressure_force.cpp @@ -619,7 +619,7 @@ namespace mean_field::operators { for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { const double enthalpy = quadratureEnthalpy(quadraturePoint); - const eos::SpecificEnthalpyValue specificEnthalpy{enthalpy}; + const dimensions::SpecificEnthalpyValue specificEnthalpy{enthalpy}; const double pressure = eos::evaluate(m_equationOfState, specificEnthalpy).value(); @@ -813,11 +813,13 @@ namespace mean_field::operators { localAction = 0.0; - mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension()); - mfem::Vector elementDisplacementVariation; mfem::Vector elementAction; + mfem::DenseMatrix referenceDisplacementDShape; + mfem::DenseMatrix referenceDisplacementJacobian; + mfem::DenseMatrix inverseElementJacobianVariation; + mfem::DenseMatrix matrixTemporary; mfem::DenseMatrix physicalTestGradientVariation; const int dimension = m_fem.mesh->Dimension(); @@ -850,13 +852,11 @@ namespace mean_field::operators { const mapping::ElementDisplacementData directionData = mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacementVariation); - const mapping::ElementMappingData mappingData{ - .displacement = *data.baseDisplacementData, .compactification = *data.compactificationData - }; + const int quadraturePointCount = data.integrationRule->GetNPoints(); - const int quadraturePointCount = data.integrationRule->GetNPoints(); + const int scalarDisplacementDofCount = displacementElement.GetDof(); - const int scalarDisplacementDofCount = displacementElement.GetDof(); + const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix(); MFEM_VERIFY( static_cast(data.baseMappingContexts.size()) == quadraturePointCount && @@ -869,31 +869,32 @@ namespace mean_field::operators { elementAction = 0.0; + referenceDisplacementDShape.SetSize(scalarDisplacementDofCount, dimension); + referenceDisplacementJacobian.SetSize(dimension, dimension); + inverseElementJacobianVariation.SetSize(dimension, dimension); + matrixTemporary.SetSize(dimension, dimension); physicalTestGradientVariation.SetSize(scalarDisplacementDofCount, dimension); for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); - transformation->SetIntPoint(&integrationPoint); + displacementElement.CalcDShape(integrationPoint, referenceDisplacementDShape); + mfem::MultAtB(directionDofs, referenceDisplacementDShape, referenceDisplacementJacobian); - mapping::VolumeMappingVariation variation; + const mfem::DenseMatrix &inverseElementJacobian = + data.baseMappingContexts[quadraturePoint].quadrature.J_inv; + mfem::Mult(inverseElementJacobian, referenceDisplacementJacobian, matrixTemporary); - const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolumeVariation( - mappingData, directionData, *transformation, integrationPoint, - data.baseMappingContexts[quadraturePoint], workspace, variation - ); + double logarithmicJacobianVariation{0.0}; + for (int component = 0; component < dimension; ++component) { + logarithmicJacobianVariation += matrixTemporary(component, component); + } - MFEM_VERIFY( - mappingStatus == mapping::MappingStatus::valid, - "Stateless mapping variation failed while applying " - "the prepared pressure-force displacement Jacobian. " - "Element: " - << data.elementId << ", attribute: " << transformation->Attribute - << ", quadrature point: " << quadraturePoint << ", status: " << static_cast(mappingStatus) - ); + mfem::Mult(matrixTemporary, inverseElementJacobian, inverseElementJacobianVariation); + inverseElementJacobianVariation *= -1.0; mfem::Mult( - data.referenceTestGradients[quadraturePoint], variation.inverse_element_jacobian_variation, + data.referenceTestGradients[quadraturePoint], inverseElementJacobianVariation, physicalTestGradientVariation ); @@ -905,12 +906,13 @@ namespace mean_field::operators { displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension ); - const double gradientWeightVariation = - data.quadratureWeights(quadraturePoint) * - physicalTestGradientVariation(scalarDof, component) + - variation.weight_variation * physicalTestGradient(scalarDof, component); + const double gradientWeightVariation = data.quadratureWeights(quadraturePoint) * + physicalTestGradientVariation(scalarDof, component) + + data.quadratureWeights(quadraturePoint) * + logarithmicJacobianVariation * + physicalTestGradient(scalarDof, component); - const double contribution = data.pressure(quadraturePoint) * gradientWeightVariation; + const double contribution = data.pressure(quadraturePoint) * gradientWeightVariation; MFEM_VERIFY( std::isfinite(gradientWeightVariation) && std::isfinite(contribution), diff --git a/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp b/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp index 894777f..abfdfd7 100644 --- a/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp +++ b/libmeanfield/impl/operators/prepared_rotation_displacement_force.cpp @@ -1,5 +1,6 @@ module; +#include #include module mean_field; @@ -7,6 +8,75 @@ module mean_field; import :operators.kernels.rotational_displacement_force; import :operators.prepared_rotational_displacement_force; +namespace { + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + + [[nodiscard]] bool is_vacuum_attribute(const int attribute) { + return DomainSchema::template attribute_belongs_to(attribute); + } + + void true_to_local( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &trueVector, + mfem::Vector &localVector + ) { + localVector.SetSize(finiteElementSpace.GetVSize()); + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->Mult(trueVector, localVector); + } else { + localVector = trueVector; + } + } + + void local_to_true( + const mfem::ParFiniteElementSpace &finiteElementSpace, + const mfem::Vector &localVector, + mfem::Vector &trueVector + ) { + trueVector.SetSize(finiteElementSpace.GetTrueVSize()); + trueVector = 0.0; + const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->MultTranspose(localVector, trueVector); + } else { + trueVector = localVector; + } + } + + [[nodiscard]] int vector_dof_index( + const mfem::Ordering::Type ordering, + const int scalarDof, + const int component, + const int scalarDofCount, + const int dimension + ) { + if (ordering == mfem::Ordering::byNODES) { + return scalarDof + component * scalarDofCount; + } + MFEM_VERIFY(ordering == mfem::Ordering::byVDIM, "Unsupported displacement ordering."); + return scalarDof * dimension + component; + } + + [[nodiscard]] const mfem::IntegrationRule &get_rotation_force_rule( + const mean_field::fem::FEM &f, + const mfem::ElementTransformation &transformation + ) { + using DisplacementField = mean_field::field::Field; + const mean_field::quadrature::Query query = + DisplacementField::make_query( + mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array{1}, + mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general + ); + const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType()); + MFEM_VERIFY( + rule.integration_rule != nullptr, + "The quadrature policy did not return a rotational-displacement-force integration rule." + ); + return *rule.integration_rule; + } +} // namespace + namespace mean_field::operators { PreparedRotationalDisplacementForceOperator::PreparedRotationalDisplacementForceOperator( const fem::FEM &f, @@ -49,6 +119,105 @@ namespace mean_field::operators { ); } + void PreparedRotationalDisplacementForceOperator::PrepareElementData() { + MFEM_VERIFY(m_rotation.has_value(), "Prepared rotational force has no frozen rotation state."); + + m_elements.clear(); + m_elements.reserve(m_fem.mesh->GetNE()); + + mfem::Vector baseDensityLocal; + mfem::Vector baseDisplacementLocal; + true_to_local(*m_fem.densityFes, m_context.GetBaseDensityTrue(), baseDensityLocal); + true_to_local(*m_fem.displacementFes, m_context.GetDisplacementTrue(), baseDisplacementLocal); + + mapping::DomainMapper::Workspace workspace(m_domainMapper.GetDimension()); + mapping::VolumeMappingContext mappingContext; + mfem::Array compactificationDofs; + mfem::Vector elementBaseDensity; + mfem::Vector elementBaseDisplacement; + mfem::Vector elementCompactification; + mfem::Vector densityShape; + mfem::Vector potentialGradient; + + const int dimension = m_domainMapper.GetDimension(); + + for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) { + mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId); + MFEM_VERIFY(transformation != nullptr, "Prepared rotational force received a null transformation."); + if (is_vacuum_attribute(transformation->Attribute)) { + continue; + } + + m_elements.emplace_back(); + ElementPAData &data = m_elements.back(); + data.elementId = elementId; + data.densityDofTransformation = m_fem.densityFes->GetElementDofs(elementId, data.densityDofs); + data.displacementDofTransformation = + m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs); + mfem::DofTransformation *compactificationDofTransformation = + m_fem.compactificationFes->GetElementDofs(elementId, compactificationDofs); + + baseDensityLocal.GetSubVector(data.densityDofs, elementBaseDensity); + baseDisplacementLocal.GetSubVector(data.displacementDofs, elementBaseDisplacement); + m_fem.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification); + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal(elementBaseDensity); + } + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(elementBaseDisplacement); + } + if (compactificationDofTransformation != nullptr) { + compactificationDofTransformation->InvTransformPrimal(elementCompactification); + } + + const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(elementId); + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(elementId); + const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(elementId); + data.integrationRule = &get_rotation_force_rule(m_fem, *transformation); + + const mapping::ElementDisplacementData displacementData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementBaseDisplacement); + const mapping::ElementCompactificationData compactificationData( + compactificationElement, elementCompactification + ); + const mapping::ElementMappingData mappingData{ + .displacement = displacementData, .compactification = compactificationData + }; + + const int quadraturePointCount = data.integrationRule->GetNPoints(); + data.inverseElementJacobians.SetSize(quadraturePointCount, dimension * dimension); + data.centrifugalAccelerations.SetSize(quadraturePointCount, dimension); + data.baseDensityValues.SetSize(quadraturePointCount); + data.quadratureWeights.SetSize(quadraturePointCount); + densityShape.SetSize(densityElement.GetDof()); + potentialGradient.SetSize(dimension); + + for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); + const mapping::MappingStatus status = m_domainMapper.EvaluateVolume( + mappingData, *transformation, integrationPoint, workspace, mappingContext + ); + MFEM_VERIFY( + status == mapping::MappingStatus::valid && !mappingContext.mapping.compactified, + "Prepared rotational force encountered an invalid stellar mapping." + ); + + densityElement.CalcShape(integrationPoint, densityShape); + m_rotation->potential_gradient(mappingContext.mapping.physical_position, potentialGradient); + data.baseDensityValues(quadraturePoint) = elementBaseDensity * densityShape; + data.quadratureWeights(quadraturePoint) = mappingContext.quadrature.weight; + + for (int row = 0; row < dimension; ++row) { + data.centrifugalAccelerations(quadraturePoint, row) = -potentialGradient(row); + for (int column = 0; column < dimension; ++column) { + data.inverseElementJacobians(quadraturePoint, row * dimension + column) = + mappingContext.quadrature.J_inv(row, column); + } + } + } + } + } + PreparedRotationalDisplacementForceReport PreparedRotationalDisplacementForceOperator::Prepare( const context::rotational_displacement_force::RotationalDisplacementForceStateView &state, const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies, @@ -83,6 +252,7 @@ namespace mean_field::operators { ); m_cachedResidual.SetSize(m_context.GetDisplacementMap().reduced_size()); m_context.GetDisplacementMap().gather(m_actionTrue, m_cachedResidual); + PrepareElementData(); ++m_residualPreparationCount; report.preparedResidual = true; @@ -143,6 +313,97 @@ namespace mean_field::operators { ++m_displacementJacobianStatistics.applications; } + void PreparedRotationalDisplacementForceOperator::ApplyPreparedCompleteJacobianActionTrue( + const mfem::Vector &densityVariationTrue, + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionTrue + ) const { + true_to_local(*m_fem.densityFes, densityVariationTrue, m_densityVariationLocal); + true_to_local(*m_fem.displacementFes, displacementVariationTrue, m_displacementVariationLocal); + m_localAction.SetSize(m_fem.displacementFes->GetVSize()); + m_localAction = 0.0; + + const int dimension = m_domainMapper.GetDimension(); + const mfem::Ordering::Type ordering = m_fem.displacementFes->GetOrdering(); + + for (const ElementPAData &data : m_elements) { + MFEM_VERIFY(data.integrationRule != nullptr, "Prepared rotational force has no integration rule."); + + m_densityVariationLocal.GetSubVector(data.densityDofs, m_elementDensityVariation); + m_displacementVariationLocal.GetSubVector(data.displacementDofs, m_elementDisplacementVariation); + if (data.densityDofTransformation != nullptr) { + data.densityDofTransformation->InvTransformPrimal(m_elementDensityVariation); + } + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->InvTransformPrimal(m_elementDisplacementVariation); + } + + const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(data.elementId); + const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId); + const mapping::ElementDisplacementData directionData = + mapping::ElementDisplacementDataFromElementVDofs(displacementElement, m_elementDisplacementVariation); + const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix(); + const int scalarDisplacementDofCount = displacementElement.GetDof(); + + m_densityShape.SetSize(densityElement.GetDof()); + m_displacementShape.SetSize(scalarDisplacementDofCount); + m_referenceDisplacementDShape.SetSize(scalarDisplacementDofCount, dimension); + m_referenceDisplacementJacobian.SetSize(dimension, dimension); + m_physicalPositionVariation.SetSize(dimension); + m_centrifugalAcceleration.SetSize(dimension); + m_centrifugalAccelerationVariation.SetSize(dimension); + m_weightedForce.SetSize(dimension); + m_elementAction.SetSize(data.displacementDofs.Size()); + m_elementAction = 0.0; + + for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) { + const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint); + densityElement.CalcShape(integrationPoint, m_densityShape); + displacementElement.CalcShape(integrationPoint, m_displacementShape); + displacementElement.CalcDShape(integrationPoint, m_referenceDisplacementDShape); + mfem::MultAtB(directionDofs, m_referenceDisplacementDShape, m_referenceDisplacementJacobian); + directionDofs.MultTranspose(m_displacementShape, m_physicalPositionVariation); + m_rotation->potential_gradient_directional_derivative( + m_physicalPositionVariation, m_centrifugalAccelerationVariation + ); + m_centrifugalAccelerationVariation *= -1.0; + + double logarithmicJacobianVariation{0.0}; + for (int row = 0; row < dimension; ++row) { + m_centrifugalAcceleration(row) = data.centrifugalAccelerations(quadraturePoint, row); + for (int column = 0; column < dimension; ++column) { + logarithmicJacobianVariation += + data.inverseElementJacobians(quadraturePoint, row * dimension + column) * + m_referenceDisplacementJacobian(column, row); + } + } + + const double densityVariationValue = m_elementDensityVariation * m_densityShape; + const double baseDensityValue = data.baseDensityValues(quadraturePoint); + m_weightedForce = 0.0; + m_weightedForce.Add(densityVariationValue, m_centrifugalAcceleration); + m_weightedForce.Add(baseDensityValue, m_centrifugalAccelerationVariation); + m_weightedForce.Add(baseDensityValue * logarithmicJacobianVariation, m_centrifugalAcceleration); + m_weightedForce *= data.quadratureWeights(quadraturePoint); + + for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) { + for (int component = 0; component < dimension; ++component) { + const int vectorDof = + vector_dof_index(ordering, scalarDof, component, scalarDisplacementDofCount, dimension); + m_elementAction(vectorDof) += m_displacementShape(scalarDof) * m_weightedForce(component); + } + } + } + + if (data.displacementDofTransformation != nullptr) { + data.displacementDofTransformation->TransformDual(m_elementAction); + } + m_localAction.AddElementVector(data.displacementDofs, m_elementAction); + } + + local_to_true(*m_fem.displacementFes, m_localAction, actionTrue); + } + void PreparedRotationalDisplacementForceOperator::ApplyCompleteJacobianAction( const mfem::Vector &densityVariation, const mfem::Vector &displacementVariation, @@ -155,10 +416,7 @@ namespace mean_field::operators { m_context.GetDensityMap().scatter(densityVariation, m_densityVariationTrue); m_context.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue); - kernels::apply_rotational_displacement_force_complete_action( - m_fem, m_domainMapper, *m_rotation, m_context.GetBaseDensityTrue(), m_densityVariationTrue, - m_displacementVariationTrue, m_context.GetDisplacementTrue(), m_actionTrue - ); + ApplyPreparedCompleteJacobianActionTrue(m_densityVariationTrue, m_displacementVariationTrue, m_actionTrue); action.SetSize(m_context.GetDisplacementMap().reduced_size()); m_context.GetDisplacementMap().gather(m_actionTrue, action); diff --git a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp index 826f56f..1e57f4c 100644 --- a/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp +++ b/libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp @@ -22,26 +22,34 @@ namespace { ); } - [[nodiscard]] mean_field::operators::StellarEquilibriumLayout make_layout( + using StellarRootForm = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form; + + [[nodiscard]] std::array< + int, + StellarRootForm::value_block_count> + make_value_sizes( const mean_field::field::FieldDofMap &densityMap, 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::surface_deformed_stellar_equilibrium_form; + return {densityMap.reduced_size(), surfaceDeformationParameterCount, gravityFluxMap.reduced_size(), + gravityPotentialMap.reduced_size(), enthalpyMap.reduced_size(), 1}; + } - const std::array valueSizes{ - 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(), - surfaceDeformationParameterCount, enthalpyMap.reduced_size(), 1 - }; - - return {valueSizes, residualSizes}; + [[nodiscard]] std::array< + int, + StellarRootForm::residual_block_count> + make_residual_sizes( + const mean_field::field::FieldDofMap &densityMap, + const int surfaceDeformationParameterCount, + const mean_field::field::FieldDofMap &gravityFluxMap, + const mean_field::field::FieldDofMap &gravityPotentialMap, + const mean_field::field::FieldDofMap &enthalpyMap + ) { + return {gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(), + surfaceDeformationParameterCount, enthalpyMap.reduced_size(), 1}; } [[nodiscard]] mfem::Array make_gravity_state_offsets( @@ -70,47 +78,6 @@ namespace { return offsets; } - template - [[nodiscard]] mfem::Vector make_value_view( - const mfem::Vector &vector, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::value_block block - ) { - MFEM_VERIFY( - vector.Size() == layout.value_offsets().Last(), - "The coupled vector does not match the stellar-equilibrium value layout." - ); - - return mfem::Vector(const_cast(vector.GetData()) + layout.offset(block), layout.size(block)); - } - - template - [[nodiscard]] mfem::Vector make_residual_view( - mfem::Vector &vector, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::residual_block block - ) { - MFEM_VERIFY( - vector.Size() == layout.residual_offsets().Last(), - "The coupled vector does not match the stellar-equilibrium residual layout." - ); - - return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block)); - } - - template - void assign_residual_block( - mfem::Vector &coupledResidual, - const mean_field::operators::StellarEquilibriumLayout &layout, - const mean_field::utils::blocks::residual_block block, - const mfem::Vector &blockResidual, - const char *message - ) { - MFEM_VERIFY(layout.size(block) == blockResidual.Size(), message); - mfem::Vector destination = make_residual_view(coupledResidual, layout, block); - destination = blockResidual; - } - void assign_gravity_block( mfem::Vector &gravityState, const mfem::Array &offsets, @@ -264,7 +231,8 @@ namespace mean_field::operators { field::FieldDofMap enthalpyMap; field::FieldBoundaryDofMap pressureSurfaceRows; - StellarEquilibriumLayout layout; + std::array valueSizes; + std::array residualSizes; mfem::Array gravityStateOffsets; mfem::Array gravityResidualOffsets; @@ -307,7 +275,14 @@ namespace mean_field::operators { enthalpyMap ) ), - layout(make_layout( + valueSizes(make_value_sizes( + densityMap, + domainDeformation.parameterCount(), + gravityFluxMap, + gravityPotentialMap, + enthalpyMap + )), + residualSizes(make_residual_sizes( densityMap, domainDeformation.parameterCount(), gravityFluxMap, @@ -339,7 +314,7 @@ namespace mean_field::operators { fem::FEM &f, const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, - const double targetMass, + models::CompiledFixedMass fixedMassConstraint, const PressureSurfaceConstraintView surfaceConstraint, deformation::PreparedDomainDeformationRuntime domainDeformation ) @@ -347,7 +322,7 @@ namespace mean_field::operators { f, domainMapper, equationOfState, - targetMass, + std::move(fixedMassConstraint), surfaceConstraint, MakeConstructionData( f, @@ -360,15 +335,31 @@ namespace mean_field::operators { fem::FEM &f, const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, - const double targetMass, + models::CompiledFixedMass fixedMassConstraint, const PressureSurfaceConstraintView surfaceConstraint, ConstructionData constructionData ) : mfem::Operator( - constructionData.layout.residual_offsets().Last(), - constructionData.layout.value_offsets().Last() + StellarEquilibriumLayout( + constructionData.valueSizes, + constructionData.residualSizes + ) + .residual_offsets() + .Last(), + StellarEquilibriumLayout( + constructionData.valueSizes, + constructionData.residualSizes + ) + .value_offsets() + .Last() + ), + m_rootManifest( + constructionData.valueSizes, + constructionData.residualSizes, + fixedMassConstraint.targetMass().value(), + surfaceConstraint.descriptor().targetPressure, + constructionData.pressureSurfaceRows.size() ), - m_layout(constructionData.layout), m_gravityStateOffsets(constructionData.gravityStateOffsets), m_gravityContext( f, @@ -413,14 +404,11 @@ namespace mean_field::operators { surfaceConstraint ), m_domainDeformation(std::move(constructionData.domainDeformation)), - m_targetMass(targetMass) { - MFEM_VERIFY( - std::isfinite(m_targetMass) && m_targetMass > 0.0, - "PreparedStellarEquilibriumOperator requires a finite, positive target mass." - ); + m_fixedMassConstraint(std::move(fixedMassConstraint)) { MFEM_VERIFY( - Width() == m_layout.value_offsets().Last() && Height() == m_layout.residual_offsets().Last(), + Width() == m_rootManifest.layout().value_offsets().Last() && + Height() == m_rootManifest.layout().residual_offsets().Last(), "PreparedStellarEquilibriumOperator has inconsistent block dimensions." ); @@ -502,27 +490,18 @@ namespace mean_field::operators { ); } - m_isPrepared = false; + m_isPrepared = false; - 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 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 = - utils::blocks::get_value_block(utils::blocks::gravity_field.poisson_term); - constexpr auto enthalpyValue = - utils::blocks::get_value_block(utils::blocks::enthalpy_field.specific_term); - constexpr auto bernoulliValue = - utils::blocks::get_value_block(utils::blocks::barotropic_constant_field.mass_normalization_term); + const auto rootState = m_rootManifest.stateView(state); - 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); + const mfem::Vector reducedDensity = rootState.block(utils::blocks::density_field.mass_term); + const mfem::Vector surfaceDeformationParameters = + rootState.block(utils::blocks::surface_deformation_field.parameters_term); + const mfem::Vector gravityGradient = rootState.block(utils::blocks::gravity_field.gradient_term); + const mfem::Vector gravityPotential = rootState.block(utils::blocks::gravity_field.poisson_term); + const mfem::Vector reducedEnthalpy = rootState.block(utils::blocks::enthalpy_field.specific_term); + const mfem::Vector bernoulli = + rootState.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); const bool generatedGeometryChanged = !wasPrepared || dependencies.discretization != m_preparedDependencies.discretization || @@ -569,7 +548,7 @@ namespace mean_field::operators { ); report.massNormalization = m_massNormalizationOperator.Prepare( - {.targetMass = m_targetMass}, make_mass_dependencies(dependencies, m_generatedDisplacementDependency) + m_fixedMassConstraint, make_mass_dependencies(dependencies, m_generatedDisplacementDependency) ); report.surfaceConstraint = m_surfaceConstraintOperator.Prepare( @@ -588,21 +567,6 @@ namespace mean_field::operators { } void PreparedStellarEquilibriumOperator::AssembleResidual() { - using Form = utils::blocks::surface_deformed_stellar_equilibrium_form; - - constexpr auto gravityGradientResidual = - utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravityPotentialResidual = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - constexpr auto densityResidual = - utils::blocks::get_residual_block(utils::blocks::density_field.mass_term); - constexpr auto 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); - mfem::Vector gravity; mfem::Vector closure; mfem::Vector surfaceShape; @@ -621,41 +585,29 @@ namespace mean_field::operators { m_massNormalizationOperator.BuildResidual(mass); m_cachedResidual.SetSize(Height()); - m_cachedResidual = 0.0; + m_cachedResidual = 0.0; + const auto residualView = m_rootManifest.residualView(m_cachedResidual); MFEM_VERIFY( - gravity.Size() == m_layout.size(gravityGradientResidual) + m_layout.size(gravityPotentialResidual), + gravity.Size() == residualView.block(utils::blocks::gravity_field.gradient_term).Size() + + residualView.block(utils::blocks::gravity_field.poisson_term).Size(), "The gravity residual has the wrong size." ); - mfem::Vector gravityGradient(gravity.GetData(), m_layout.size(gravityGradientResidual)); + mfem::Vector gravityGradient( + gravity.GetData(), residualView.block(utils::blocks::gravity_field.gradient_term).Size() + ); mfem::Vector gravityPotential( - gravity.GetData() + m_layout.size(gravityGradientResidual), m_layout.size(gravityPotentialResidual) + gravity.GetData() + gravityGradient.Size(), + residualView.block(utils::blocks::gravity_field.poisson_term).Size() ); - assign_residual_block( - m_cachedResidual, m_layout, gravityGradientResidual, gravityGradient, - "The gravity-gradient residual has the wrong size." - ); - assign_residual_block( - m_cachedResidual, m_layout, gravityPotentialResidual, gravityPotential, - "The gravity-potential residual has the wrong size." - ); - assign_residual_block( - m_cachedResidual, m_layout, densityResidual, closure, "The closure residual has the wrong size." - ); - assign_residual_block( - m_cachedResidual, m_layout, surfaceShapeResidual, surfaceShape, - "The surface-shape residual has the wrong size." - ); - - assign_residual_block( - m_cachedResidual, m_layout, enthalpyResidual, hydrostatic, "The hydrostatic residual has the wrong size." - ); - - assign_residual_block( - m_cachedResidual, m_layout, massResidual, mass, "The mass-normalization residual has the wrong size." - ); + residualView.assign(utils::blocks::gravity_field.gradient_term, gravityGradient); + residualView.assign(utils::blocks::gravity_field.poisson_term, gravityPotential); + residualView.assign(utils::blocks::density_field.mass_term, closure); + residualView.assign(utils::blocks::surface_deformation_field.shape_equilibrium_term, surfaceShape); + residualView.assign(utils::blocks::enthalpy_field.specific_term, hydrostatic); + residualView.assign(utils::blocks::fixed_total_mass_constraint.mass_normalization_term, mass); ++m_statistics.residualAssemblies; } @@ -679,39 +631,16 @@ namespace mean_field::operators { direction, "PreparedStellarEquilibriumOperator received a non-finite Jacobian direction." ); - using Form = utils::blocks::surface_deformed_stellar_equilibrium_form; + const auto rootDirection = m_rootManifest.directionView(direction); - constexpr auto densityValue = utils::blocks::get_value_block(utils::blocks::density_field.mass_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 = - utils::blocks::get_value_block(utils::blocks::gravity_field.poisson_term); - constexpr auto enthalpyValue = - utils::blocks::get_value_block(utils::blocks::enthalpy_field.specific_term); - constexpr auto bernoulliValue = - utils::blocks::get_value_block(utils::blocks::barotropic_constant_field.mass_normalization_term); - - constexpr auto gravityGradientResidual = - utils::blocks::get_residual_block(utils::blocks::gravity_field.gradient_term); - constexpr auto gravityPotentialResidual = - utils::blocks::get_residual_block(utils::blocks::gravity_field.poisson_term); - constexpr auto densityResidual = - utils::blocks::get_residual_block(utils::blocks::density_field.mass_term); - constexpr auto 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 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); + const mfem::Vector reducedDensityDirection = rootDirection.block(utils::blocks::density_field.mass_term); + const mfem::Vector surfaceDeformationDirection = + rootDirection.block(utils::blocks::surface_deformation_field.parameters_term); + const mfem::Vector gravityGradientDirection = rootDirection.block(utils::blocks::gravity_field.gradient_term); + const mfem::Vector gravityPotentialDirection = rootDirection.block(utils::blocks::gravity_field.poisson_term); + const mfem::Vector reducedEnthalpyDirection = rootDirection.block(utils::blocks::enthalpy_field.specific_term); + const mfem::Vector bernoulliDirection = + rootDirection.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); m_domainDeformation.applyJacobian( m_surfaceDeformationParameters, surfaceDeformationDirection, m_volumeDisplacementDirection @@ -757,41 +686,29 @@ namespace mean_field::operators { ); action.SetSize(Height()); - action = 0.0; + action = 0.0; + const auto actionView = m_rootManifest.residualView(action); MFEM_VERIFY( - gravityAction.Size() == m_layout.size(gravityGradientResidual) + m_layout.size(gravityPotentialResidual), + gravityAction.Size() == actionView.block(utils::blocks::gravity_field.gradient_term).Size() + + actionView.block(utils::blocks::gravity_field.poisson_term).Size(), "The gravity Jacobian action has the wrong size." ); - mfem::Vector gravityGradientAction(gravityAction.GetData(), m_layout.size(gravityGradientResidual)); + mfem::Vector gravityGradientAction( + gravityAction.GetData(), actionView.block(utils::blocks::gravity_field.gradient_term).Size() + ); mfem::Vector gravityPotentialAction( - gravityAction.GetData() + m_layout.size(gravityGradientResidual), m_layout.size(gravityPotentialResidual) + gravityAction.GetData() + gravityGradientAction.Size(), + actionView.block(utils::blocks::gravity_field.poisson_term).Size() ); - assign_residual_block( - action, m_layout, gravityGradientResidual, gravityGradientAction, - "The gravity-gradient Jacobian action has the wrong size." - ); - assign_residual_block( - action, m_layout, gravityPotentialResidual, gravityPotentialAction, - "The gravity-potential Jacobian action has the wrong size." - ); - assign_residual_block( - action, m_layout, densityResidual, closureAction, "The closure Jacobian action has the wrong size." - ); - assign_residual_block( - action, m_layout, surfaceShapeResidual, m_surfaceShapeAction, - "The surface-shape Jacobian action has the wrong size." - ); - - assign_residual_block( - action, m_layout, enthalpyResidual, hydrostaticAction, "The hydrostatic Jacobian action has the wrong size." - ); - - assign_residual_block( - action, m_layout, massResidual, massAction, "The mass-normalization Jacobian action has the wrong size." - ); + actionView.assign(utils::blocks::gravity_field.gradient_term, gravityGradientAction); + actionView.assign(utils::blocks::gravity_field.poisson_term, gravityPotentialAction); + actionView.assign(utils::blocks::density_field.mass_term, closureAction); + actionView.assign(utils::blocks::surface_deformation_field.shape_equilibrium_term, m_surfaceShapeAction); + actionView.assign(utils::blocks::enthalpy_field.specific_term, hydrostaticAction); + actionView.assign(utils::blocks::fixed_total_mass_constraint.mass_normalization_term, massAction); ++m_statistics.jacobianApplications; } @@ -803,11 +720,30 @@ namespace mean_field::operators { } double PreparedStellarEquilibriumOperator::GetTargetMass() const noexcept { - return m_targetMass; + return m_fixedMassConstraint.targetMass().value(); } const StellarEquilibriumLayout &PreparedStellarEquilibriumOperator::GetLayout() const noexcept { - return m_layout; + return m_rootManifest.layout(); + } + + const StellarEquilibriumRootManifest &PreparedStellarEquilibriumOperator::GetRootManifest() const noexcept { + return m_rootManifest; + } + + RootStateView + PreparedStellarEquilibriumOperator::GetRootStateView(const mfem::Vector &state) const { + return m_rootManifest.stateView(state); + } + + ResidualView + PreparedStellarEquilibriumOperator::GetResidualView(mfem::Vector &residual) const { + return m_rootManifest.residualView(residual); + } + + RootConstraintReport PreparedStellarEquilibriumOperator::GetFixedMassReport() const { + VerifyPrepared(); + return m_rootManifest.fixedMassReport(m_massNormalizationOperator.GetCurrentMass()); } const StellarEquilibriumDependencies &PreparedStellarEquilibriumOperator::GetDependencies() const { diff --git a/libmeanfield/impl/physics/gravity.cpp b/libmeanfield/impl/physics/gravity.cpp index ce5cf31..ad65a88 100644 --- a/libmeanfield/impl/physics/gravity.cpp +++ b/libmeanfield/impl/physics/gravity.cpp @@ -102,7 +102,7 @@ namespace mean_field::physics { GravitySolution solve_gravity_field( fem::FEM &f, - const utils::Args &args, + const GravitySolveOptions &options, const mfem::GridFunction &rho, const mfem::GridFunction &displacement ) { @@ -132,7 +132,15 @@ namespace mean_field::physics { "Vec_H1 " "space." ); - MFEM_VERIFY(args.p.max_iters > 0, "Gravity solve requires a positive MINRES iteration limit."); + MFEM_VERIFY( + std::isfinite(options.relativeTolerance) && options.relativeTolerance >= 0.0, + "Gravity solve requires a finite, nonnegative relative tolerance." + ); + MFEM_VERIFY( + std::isfinite(options.absoluteTolerance) && options.absoluteTolerance >= 0.0, + "Gravity solve requires a finite, nonnegative absolute tolerance." + ); + MFEM_VERIFY(options.maximumIterations > 0, "Gravity solve requires a positive MINRES iteration limit."); using form = utils::blocks::gravity_field_form; @@ -206,9 +214,9 @@ namespace mean_field::physics { mfem::MINRESSolver minres(f.mesh->GetComm()); minres.SetOperator(reduced_operator); minres.SetPreconditioner(reduced_preconditioner); - minres.SetRelTol(args.p.rtol); - minres.SetAbsTol(args.p.atol); - minres.SetMaxIter(args.p.max_iters); + minres.SetRelTol(options.relativeTolerance); + minres.SetAbsTol(options.absoluteTolerance); + minres.SetMaxIter(options.maximumIterations); // minres.SetPrintLevel(args.verbose ? 1 : 0); minres.SetPrintLevel(0); minres.Mult(right_hand_side, gravity_state); @@ -222,4 +230,21 @@ namespace mean_field::physics { return solution; } + + GravitySolution solve_gravity_field( + fem::FEM &f, + const utils::Args &args, + const mfem::GridFunction &rho, + const mfem::GridFunction &displacement + ) { + return solve_gravity_field( + f, + GravitySolveOptions{ + .relativeTolerance = args.p.rtol, + .absoluteTolerance = args.p.atol, + .maximumIterations = args.p.max_iters + }, + rho, displacement + ); + } } // namespace mean_field::physics diff --git a/libmeanfield/impl/seed/lane_emden.cpp b/libmeanfield/impl/seed/lane_emden.cpp new file mode 100644 index 0000000..74b4880 --- /dev/null +++ b/libmeanfield/impl/seed/lane_emden.cpp @@ -0,0 +1,248 @@ +module; + +#include +#include +#include +#include +#include +#include + +#include + +module mean_field; + +import :seed.lane_emden; +import :utils.misc; + +namespace { + struct LaneEmdenPoint final { + double coordinate{0.0}; + double value{0.0}; + double derivative{0.0}; + }; + + struct LaneEmdenDerivative final { + double value{0.0}; + double derivative{0.0}; + }; + + [[nodiscard]] LaneEmdenDerivative evaluate_lane_emden_rhs( + const double coordinate, + const double value, + const double derivative, + const double polytropicIndex + ) { + const double nonnegativeValue = std::max(value, 0.0); + return { + .value = derivative, + .derivative = -2.0 * derivative / coordinate - std::pow(nonnegativeValue, polytropicIndex) + }; + } + + [[nodiscard]] LaneEmdenPoint take_lane_emden_step( + const LaneEmdenPoint &point, + const double step, + const double polytropicIndex + ) { + const LaneEmdenDerivative first = + evaluate_lane_emden_rhs(point.coordinate, point.value, point.derivative, polytropicIndex); + const LaneEmdenDerivative second = evaluate_lane_emden_rhs( + point.coordinate + 0.5 * step, point.value + 0.5 * step * first.value, + point.derivative + 0.5 * step * first.derivative, polytropicIndex + ); + const LaneEmdenDerivative third = evaluate_lane_emden_rhs( + point.coordinate + 0.5 * step, point.value + 0.5 * step * second.value, + point.derivative + 0.5 * step * second.derivative, polytropicIndex + ); + const LaneEmdenDerivative fourth = evaluate_lane_emden_rhs( + point.coordinate + step, point.value + step * third.value, point.derivative + step * third.derivative, + polytropicIndex + ); + + return { + .coordinate = point.coordinate + step, + .value = point.value + step / 6.0 * (first.value + 2.0 * second.value + 2.0 * third.value + fourth.value), + .derivative = + point.derivative + + step / 6.0 * (first.derivative + 2.0 * second.derivative + 2.0 * third.derivative + fourth.derivative) + }; + } + + [[nodiscard]] std::vector solve_lane_emden( + const double polytropicIndex, + const double coordinateLimit, + const double integrationStep + ) { + if (!std::isfinite(polytropicIndex) || polytropicIndex < 0.0) { + throw std::invalid_argument("Lane-Emden integration requires a finite, nonnegative polytropic index."); + } + if (!std::isfinite(coordinateLimit) || coordinateLimit <= 0.0) { + throw std::invalid_argument("The Lane-Emden coordinate limit must be finite and positive."); + } + if (!std::isfinite(integrationStep) || integrationStep <= 0.0) { + throw std::invalid_argument("The Lane-Emden integration step must be finite and positive."); + } + + constexpr int maximumStepCount = 2'000'000; + if (std::ceil(coordinateLimit / integrationStep) > static_cast(maximumStepCount)) { + throw std::invalid_argument("The requested Lane-Emden interval exceeds the integration step limit."); + } + + const double initialCoordinate = std::min(1.0e-6, coordinateLimit); + + const double coordinateSquared = initialCoordinate * initialCoordinate; + const double coordinateCubed = coordinateSquared * initialCoordinate; + const double coordinateFourth = coordinateSquared * coordinateSquared; + + LaneEmdenPoint point{ + .coordinate = initialCoordinate, + .value = 1.0 - coordinateSquared / 6.0 + polytropicIndex * coordinateFourth / 120.0, + .derivative = -initialCoordinate / 3.0 + polytropicIndex * coordinateCubed / 30.0 + }; + + std::vector solution; + solution.reserve(8192); + solution.push_back({.coordinate = 0.0, .value = 1.0, .derivative = 0.0}); + solution.push_back(point); + + for (int stepIndex = 0; stepIndex < maximumStepCount && point.coordinate < coordinateLimit; ++stepIndex) { + const double step = std::min(integrationStep, coordinateLimit - point.coordinate); + LaneEmdenPoint nextPoint = take_lane_emden_step(point, step, polytropicIndex); + if (!std::isfinite(nextPoint.value)) { + throw std::runtime_error( + "The Lane-Emden integration produced a non-finite solution before reaching its termination." + ); + } + if (nextPoint.value <= 0.0) { + const double rootFraction = point.value / (point.value - nextPoint.value); + solution.push_back( + {.coordinate = point.coordinate + rootFraction * (nextPoint.coordinate - point.coordinate), + .value = 0.0, + .derivative = point.derivative + rootFraction * (nextPoint.derivative - point.derivative)} + ); + return solution; + } + solution.push_back(nextPoint); + point = nextPoint; + } + + if (point.coordinate < coordinateLimit) { + throw std::runtime_error("The Lane-Emden integration exceeded its step limit."); + } + return solution; + } + + [[nodiscard]] double interpolate_lane_emden_value( + const std::vector &solution, + const double coordinate, + std::size_t &lowerIndex + ) { + while (lowerIndex + 1 < solution.size() && solution[lowerIndex + 1].coordinate < coordinate) { + ++lowerIndex; + } + if (lowerIndex + 1 >= solution.size()) { + return 0.0; + } + + const LaneEmdenPoint &lower = solution[lowerIndex]; + const LaneEmdenPoint &upper = solution[lowerIndex + 1]; + const double interval = upper.coordinate - lower.coordinate; + if (interval <= 0.0) { + throw std::runtime_error("The Lane-Emden interpolation grid is not strictly increasing."); + } + const double fraction = (coordinate - lower.coordinate) / interval; + return std::clamp(lower.value + fraction * (upper.value - lower.value), 0.0, 1.0); + } +} // namespace + +namespace mean_field::seed { + DimensionlessLaneEmdenSolution integrateLaneEmden( + const double polytropicIndex, + const double coordinateLimit, + const double integrationStep + ) { + const std::vector points = solve_lane_emden(polytropicIndex, coordinateLimit, integrationStep); + + DimensionlessLaneEmdenSolution solution{ + .coordinate = mfem::Vector(static_cast(points.size())), + .theta = mfem::Vector(static_cast(points.size())), + .thetaDerivative = mfem::Vector(static_cast(points.size())), + .firstZeroCoordinate = std::nullopt + }; + for (int index = 0; index < static_cast(points.size()); ++index) { + solution.coordinate(index) = points[static_cast(index)].coordinate; + solution.theta(index) = points[static_cast(index)].value; + solution.thetaDerivative(index) = points[static_cast(index)].derivative; + } + if (points.back().value == 0.0) { + solution.firstZeroCoordinate = points.back().coordinate; + } + return solution; + } + + RadialProfile generateLaneEmdenProfile( + const eos::Polytrope &equationOfState, + const dimensions::DensityValue centralDensity, + const int radialSampleCount + ) { + if (!std::isfinite(centralDensity.value()) || centralDensity.value() <= 0.0) { + throw std::invalid_argument("A Lane-Emden seed central density must be finite and positive."); + } + if (radialSampleCount < 2) { + throw std::invalid_argument("A Lane-Emden seed requires at least two radial samples."); + } + + const double polytropicIndex = equationOfState.polytropic_index(); + if (!std::isfinite(polytropicIndex) || polytropicIndex < 1.0 || polytropicIndex >= 5.0) { + throw std::invalid_argument("Lane-Emden seeds require a finite-radius polytrope with 1 <= n < 5."); + } + + constexpr double seedCoordinateLimit = 2'000.0; + constexpr double integrationStep = 1.0e-3; + const std::vector solution = solve_lane_emden(polytropicIndex, seedCoordinateLimit, integrationStep); + if (solution.back().value != 0.0) { + throw std::runtime_error("The Lane-Emden integration did not reach its first zero within the step limit."); + } + const double surfaceCoordinate = solution.back().coordinate; + const dimensions::SpecificEnthalpyValue centralEnthalpy = + eos::evaluate(equationOfState, centralDensity); + const double radialScaleSquared = centralEnthalpy.value() / (4.0 * std::numbers::pi_v * + mean_field::utils::G * centralDensity.value()); + if (!std::isfinite(radialScaleSquared) || radialScaleSquared <= 0.0) { + throw std::runtime_error("The polytropic Lane-Emden radial scale is not finite and positive."); + } + + const double radialScale = std::sqrt(radialScaleSquared); + RadialProfile profile{ + .radius = mfem::Vector(radialSampleCount), + .density = mfem::Vector(radialSampleCount), + .specificEnthalpy = mfem::Vector(radialSampleCount), + .stellarRadius = dimensions::LengthValue{radialScale * surfaceCoordinate}, + .centralDensity = centralDensity, + .centralSpecificEnthalpy = centralEnthalpy + }; + + std::size_t interpolationIndex = 0; + for (int sampleIndex = 0; sampleIndex < radialSampleCount; ++sampleIndex) { + const double fraction = static_cast(sampleIndex) / static_cast(radialSampleCount - 1); + const double dimensionlessRadius = fraction * surfaceCoordinate; + const double laneEmdenValue = + interpolate_lane_emden_value(solution, dimensionlessRadius, interpolationIndex); + const dimensions::DensityValue density{centralDensity.value() * std::pow(laneEmdenValue, polytropicIndex)}; + + profile.radius(sampleIndex) = radialScale * dimensionlessRadius; + profile.density(sampleIndex) = density.value(); + profile.specificEnthalpy(sampleIndex) = + eos::evaluate(equationOfState, density).value(); + } + + profile.radius(0) = 0.0; + profile.density(0) = centralDensity.value(); + profile.specificEnthalpy(0) = centralEnthalpy.value(); + const int surfaceIndex = radialSampleCount - 1; + profile.radius(surfaceIndex) = profile.stellarRadius.value(); + profile.density(surfaceIndex) = 0.0; + profile.specificEnthalpy(surfaceIndex) = 0.0; + return profile; + } +} // namespace mean_field::seed diff --git a/libmeanfield/impl/seed/stellar_equilibrium_projection.cpp b/libmeanfield/impl/seed/stellar_equilibrium_projection.cpp new file mode 100644 index 0000000..e0e80b1 --- /dev/null +++ b/libmeanfield/impl/seed/stellar_equilibrium_projection.cpp @@ -0,0 +1,209 @@ +module; + +#include +#include +#include +#include + +#include +#include + +module mean_field; + +import :field.mfem; +import :seed.stellar_equilibrium_projection; +import :utils.domain; +import :utils.misc; + +namespace { + using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; + + void validate_profile(const mean_field::seed::RadialProfile &profile) { + const int sampleCount = profile.radius.Size(); + if (sampleCount < 2 || profile.density.Size() != sampleCount || + profile.specificEnthalpy.Size() != sampleCount) { + throw std::invalid_argument("A radial seed projection requires equally sized profiles with two samples."); + } + if (!std::isfinite(profile.stellarRadius.value()) || profile.stellarRadius.value() <= 0.0 || + !std::isfinite(profile.centralDensity.value()) || profile.centralDensity.value() <= 0.0 || + !std::isfinite(profile.centralSpecificEnthalpy.value()) || profile.centralSpecificEnthalpy.value() <= 0.0) { + throw std::invalid_argument("A radial seed projection requires finite, positive physical scales."); + } + + for (int index = 0; index < sampleCount; ++index) { + if (!std::isfinite(profile.radius(index)) || !std::isfinite(profile.density(index)) || + !std::isfinite(profile.specificEnthalpy(index)) || profile.density(index) < 0.0 || + profile.specificEnthalpy(index) < 0.0) { + throw std::invalid_argument("A radial seed projection received a non-finite or negative profile."); + } + if (index > 0 && profile.radius(index) <= profile.radius(index - 1)) { + throw std::invalid_argument("A radial seed projection requires strictly increasing radii."); + } + } + + const int surfaceIndex = sampleCount - 1; + const double radialScale = std::max(profile.stellarRadius.value(), 1.0); + if (std::abs(profile.radius(0)) > 64.0 * std::numeric_limits::epsilon() * radialScale || + std::abs(profile.radius(surfaceIndex) - profile.stellarRadius.value()) > + 64.0 * std::numeric_limits::epsilon() * radialScale || + profile.density(0) != profile.centralDensity.value() || + profile.specificEnthalpy(0) != profile.centralSpecificEnthalpy.value() || + profile.density(surfaceIndex) != 0.0 || profile.specificEnthalpy(surfaceIndex) != 0.0) { + throw std::invalid_argument("A radial seed projection received inconsistent center or surface metadata."); + } + } + + [[nodiscard]] double interpolate_profile( + const mfem::Vector &radius, + const mfem::Vector &values, + const double requestedRadius + ) { + if (requestedRadius <= radius(0)) { + return values(0); + } + const int finalIndex = radius.Size() - 1; + if (requestedRadius >= radius(finalIndex)) { + return values(finalIndex); + } + + int lowerIndex = 0; + int upperIndex = finalIndex; + while (upperIndex - lowerIndex > 1) { + const int middleIndex = lowerIndex + (upperIndex - lowerIndex) / 2; + if (radius(middleIndex) <= requestedRadius) { + lowerIndex = middleIndex; + } else { + upperIndex = middleIndex; + } + } + + const double fraction = (requestedRadius - radius(lowerIndex)) / (radius(upperIndex) - radius(lowerIndex)); + return (1.0 - fraction) * values(lowerIndex) + fraction * values(upperIndex); + } + + struct SurfaceRadiusRange final { + double minimum; + double maximum; + }; + + [[nodiscard]] SurfaceRadiusRange measure_surface_radius(const mean_field::fem::FEM &finiteElementModel) { + if (finiteElementModel.surfaceDeformationFes == nullptr) { + throw std::invalid_argument("Radial seed projection requires the surface-deformation space."); + } + + mfem::ParFiniteElementSpace &surfaceSpace = *finiteElementModel.surfaceDeformationFes; + const mean_field::field::ScalarBoundaryDofMap surfaceMap = + mean_field::field::make_stellar_surface_scalar_dof_map(surfaceSpace); + mfem::Vector radiusSquared(surfaceMap.local_size()); + radiusSquared = 0.0; + + mfem::ParGridFunction coordinateField(&surfaceSpace); + for (int component = 0; component < surfaceSpace.GetMesh()->SpaceDimension(); ++component) { + mfem::FunctionCoefficient coordinateCoefficient([component](const mfem::Vector &position) { + return position(component); + }); + coordinateField.ProjectCoefficient(coordinateCoefficient); + mfem::Vector coordinateTrue; + coordinateField.GetTrueDofs(coordinateTrue); + const mfem::Vector surfaceCoordinate = surfaceMap.gather(coordinateTrue); + for (int index = 0; index < radiusSquared.Size(); ++index) { + radiusSquared(index) += surfaceCoordinate(index) * surfaceCoordinate(index); + } + } + + double localMinimum = std::numeric_limits::infinity(); + double localMaximum = 0.0; + for (int index = 0; index < radiusSquared.Size(); ++index) { + const double radius = std::sqrt(radiusSquared(index)); + localMinimum = std::min(localMinimum, radius); + localMaximum = std::max(localMaximum, radius); + } + + double globalMinimum = 0.0; + double globalMaximum = 0.0; + MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, surfaceSpace.GetComm()); + MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, surfaceSpace.GetComm()); + if (!std::isfinite(globalMinimum) || !std::isfinite(globalMaximum) || globalMinimum <= 0.0 || + globalMaximum < globalMinimum) { + throw std::runtime_error("The stellar surface has no finite, positive radial extent."); + } + return {.minimum = globalMinimum, .maximum = globalMaximum}; + } +} // namespace + +namespace mean_field::seed::detail { + ProjectedRadialFields projectRadialFields( + const equilibrium::StellarDiscretization &discretization, + const RadialProfile &profile, + const dimensions::MassValue targetMass, + const dimensions::PressureValue targetSurfacePressure, + const StellarEquilibriumProjectionOptions &options + ) { + validate_profile(profile); + if (!std::isfinite(options.surfaceRadiusRelativeTolerance) || options.surfaceRadiusRelativeTolerance < 0.0) { + throw std::invalid_argument("The surface-radius projection tolerance must be finite and nonnegative."); + } + if (targetSurfacePressure.value() != 0.0) { + throw std::invalid_argument("A Lane-Emden radial seed requires a zero-pressure isobaric surface."); + } + + fem::FEM &finiteElementModel = discretization.finiteElementModel(); + const SurfaceRadiusRange surfaceRadius = measure_surface_radius(finiteElementModel); + const double targetRadius = profile.stellarRadius.value(); + const double comparisonScale = std::max({targetRadius, surfaceRadius.maximum, 1.0e-300}); + const double relativeMismatch = + std::max(std::abs(surfaceRadius.minimum - targetRadius), std::abs(surfaceRadius.maximum - targetRadius)) / + comparisonScale; + if (relativeMismatch > options.surfaceRadiusRelativeTolerance) { + throw std::invalid_argument( + "The radial seed surface does not coincide with the spherical reference discretization." + ); + } + + if (finiteElementModel.densityFes == nullptr || finiteElementModel.enthalpyFes == nullptr || + finiteElementModel.displacementFes == nullptr || finiteElementModel.gravityFluxFes == nullptr || + finiteElementModel.gravityPotentialFes == nullptr) { + throw std::invalid_argument("Radial seed projection requires the complete equilibrium discretization."); + } + + mfem::FunctionCoefficient densityCoefficient([&profile](const mfem::Vector &position) { + return interpolate_profile(profile.radius, profile.density, position.Norml2()); + }); + mfem::FunctionCoefficient enthalpyCoefficient([&profile](const mfem::Vector &position) { + return interpolate_profile(profile.radius, profile.specificEnthalpy, position.Norml2()); + }); + + mfem::ParGridFunction densityField(finiteElementModel.densityFes.get()); + mfem::ParGridFunction enthalpyField(finiteElementModel.enthalpyFes.get()); + mfem::ParGridFunction displacementField(finiteElementModel.displacementFes.get()); + densityField = 0.0; + enthalpyField = 0.0; + displacementField = 0.0; + densityField.ProjectCoefficient(densityCoefficient); + enthalpyField.ProjectCoefficient(enthalpyCoefficient); + + const physics::GravitySolution gravitySolution = + physics::solve_gravity_field(finiteElementModel, options.gravity, densityField, displacementField); + + const field::FieldDofGridFunctionAdapter densityAdapter = + field::make_field_dof_grid_function_adapter(*finiteElementModel.densityFes); + const field::FieldDofGridFunctionAdapter enthalpyAdapter = + field::make_field_dof_grid_function_adapter(*finiteElementModel.enthalpyFes); + const field::FieldDofGridFunctionAdapter gravityFluxAdapter = + field::make_field_dof_grid_function_adapter( + *finiteElementModel.gravityFluxFes + ); + const field::FieldDofGridFunctionAdapter gravityPotentialAdapter = + field::make_field_dof_grid_function_adapter( + *finiteElementModel.gravityPotentialFes + ); + + return { + .density = densityAdapter.gather(densityField), + .gravityGradient = gravityFluxAdapter.gather(gravitySolution.gradPhi), + .gravityPotential = gravityPotentialAdapter.gather(gravitySolution.phi), + .specificEnthalpy = enthalpyAdapter.gather(enthalpyField), + .bernoulliConstant = -utils::G * targetMass.value() / targetRadius + }; + } +} // namespace mean_field::seed::detail diff --git a/libmeanfield/impl/solver/preconditioning_diagnostics.cpp b/libmeanfield/impl/solver/preconditioning_diagnostics.cpp new file mode 100644 index 0000000..58a9b45 --- /dev/null +++ b/libmeanfield/impl/solver/preconditioning_diagnostics.cpp @@ -0,0 +1,638 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +module mean_field; + +import :solver.preconditioning_diagnostics; + +namespace { + using Clock = std::chrono::steady_clock; + + [[nodiscard]] double seconds_between( + const Clock::time_point start, + const Clock::time_point finish + ) { + return std::chrono::duration(finish - start).count(); + } + + void verify_finite_vector( + 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); + } + } + } + + [[nodiscard]] double global_dot( + const mfem::Vector &left, + const mfem::Vector &right, + const MPI_Comm communicator + ) { + if (communicator == MPI_COMM_NULL) { + throw std::invalid_argument("Preconditioning diagnostics require a valid MPI communicator."); + } + if (left.Size() != right.Size()) { + throw std::invalid_argument("A distributed inner product received vectors with different sizes."); + } + + const double localValue = left * right; + double globalValue = 0.0; + MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, communicator); + return globalValue; + } + + [[nodiscard]] double global_norm( + const mfem::Vector &vector, + const MPI_Comm communicator + ) { + return std::sqrt(std::max(global_dot(vector, vector, communicator), 0.0)); + } + + [[nodiscard]] mean_field::solver::OperatorApplicationStatistics maximum_rank_statistics( + const mean_field::solver::OperatorApplicationStatistics &local, + const MPI_Comm communicator + ) { + unsigned long long localApplications = static_cast(local.applications); + unsigned long long maximumApplications{0}; + MPI_Allreduce(&localApplications, &maximumApplications, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, communicator); + + mean_field::solver::OperatorApplicationStatistics result; + result.applications = static_cast(maximumApplications); + MPI_Allreduce(&local.totalSeconds, &result.totalSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator); + MPI_Allreduce(&local.maximumSeconds, &result.maximumSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator); + return result; + } + + [[nodiscard]] double maximum_rank_value( + const double localValue, + const MPI_Comm communicator + ) { + double result = 0.0; + MPI_Allreduce(&localValue, &result, 1, MPI_DOUBLE, MPI_MAX, communicator); + return result; + } + + [[nodiscard]] mean_field::solver::PreconditionerLifecycleStatistics maximum_rank_lifecycle_statistics( + const mean_field::solver::PreconditionerLifecycleStatistics &local, + const MPI_Comm communicator + ) { + unsigned long long localSetups = static_cast(local.setups); + unsigned long long localRefreshes = static_cast(local.refreshes); + unsigned long long maximumSetups{0}; + unsigned long long maximumRefreshes{0}; + MPI_Allreduce(&localSetups, &maximumSetups, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, communicator); + MPI_Allreduce(&localRefreshes, &maximumRefreshes, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, communicator); + + mean_field::solver::PreconditionerLifecycleStatistics result; + result.setups = static_cast(maximumSetups); + result.refreshes = static_cast(maximumRefreshes); + MPI_Allreduce(&local.setupSeconds, &result.setupSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator); + MPI_Allreduce(&local.refreshSeconds, &result.refreshSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator); + return result; + } + + [[nodiscard]] Eigen::MatrixXd copy_hessenberg( + const Eigen::MatrixXd &source, + const int rowCount, + const int columnCount + ) { + return source.topLeftCorner(rowCount, columnCount); + } +} // namespace + +namespace mean_field::solver { + InstrumentedOperator::InstrumentedOperator(const mfem::Operator &operation) + : mfem::Operator( + operation.Height(), + operation.Width() + ), + m_operation(std::addressof(operation)) { + } + + void InstrumentedOperator::Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const { + const Clock::time_point start = Clock::now(); + m_operation->Mult(input, output); + const double elapsed = seconds_between(start, Clock::now()); + + ++m_statistics.applications; + m_statistics.totalSeconds += elapsed; + m_statistics.maximumSeconds = std::max(m_statistics.maximumSeconds, elapsed); + } + + void InstrumentedOperator::ResetStatistics() const noexcept { + m_statistics = {}; + } + + const OperatorApplicationStatistics &InstrumentedOperator::GetStatistics() const noexcept { + return m_statistics; + } + + const mfem::Operator &InstrumentedOperator::GetOperation() const noexcept { + return *m_operation; + } + + InstrumentedPreconditioner::InstrumentedPreconditioner(mfem::Solver &preconditioner) + : mfem::Solver( + preconditioner.Height(), + preconditioner.Width(), + preconditioner.iterative_mode + ), + m_preconditioner(std::addressof(preconditioner)) { + } + + void InstrumentedPreconditioner::SetOperator(const mfem::Operator &operation) { + const Clock::time_point start = Clock::now(); + m_preconditioner->SetOperator(operation); + m_lifecycleStatistics.setupSeconds += seconds_between(start, Clock::now()); + ++m_lifecycleStatistics.setups; + if (m_preconditioner->Height() != Height() || m_preconditioner->Width() != Width()) { + throw std::invalid_argument("An instrumented preconditioner changed dimensions during SetOperator."); + } + } + + void InstrumentedPreconditioner::Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const { + const Clock::time_point start = Clock::now(); + m_preconditioner->Mult(input, output); + const double elapsed = seconds_between(start, Clock::now()); + + ++m_statistics.applications; + m_statistics.totalSeconds += elapsed; + m_statistics.maximumSeconds = std::max(m_statistics.maximumSeconds, elapsed); + } + + void InstrumentedPreconditioner::ResetStatistics() const noexcept { + m_statistics = {}; + } + + const OperatorApplicationStatistics &InstrumentedPreconditioner::GetStatistics() const noexcept { + return m_statistics; + } + + const PreconditionerLifecycleStatistics &InstrumentedPreconditioner::GetLifecycleStatistics() const noexcept { + return m_lifecycleStatistics; + } + + const mfem::Solver &InstrumentedPreconditioner::GetPreconditioner() const noexcept { + return *m_preconditioner; + } + + IdentityPreconditioner::IdentityPreconditioner(const int size) : mfem::Solver(size) { + if (size <= 0) { + throw std::invalid_argument("An identity preconditioner requires a positive dimension."); + } + } + + void IdentityPreconditioner::SetOperator(const mfem::Operator &operation) { + if (operation.Height() != Height() || operation.Width() != Width()) { + throw std::invalid_argument("The identity preconditioner received an incompatible operator."); + } + } + + void IdentityPreconditioner::Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const { + if (input.Size() != Width()) { + throw std::invalid_argument("The identity preconditioner received an input with the wrong size."); + } + output = input; + } + + FixedRightPreconditionedOperator::FixedRightPreconditionedOperator( + const mfem::Operator &jacobian, + const mfem::Solver &inversePreconditioner + ) + : mfem::Operator( + jacobian.Height(), + inversePreconditioner.Width() + ), + m_jacobian(std::addressof(jacobian)), + m_inversePreconditioner(std::addressof(inversePreconditioner)), + m_preconditionedDirection(inversePreconditioner.Height()) { + if (jacobian.Height() != jacobian.Width()) { + throw std::invalid_argument("A preconditioned stellar Jacobian must be square."); + } + if (inversePreconditioner.Height() != jacobian.Width() || inversePreconditioner.Width() != jacobian.Height()) { + throw std::invalid_argument("The inverse preconditioner does not map residuals into Jacobian states."); + } + if (Height() != Width()) { + throw std::invalid_argument("The fixed right-preconditioned product must be square."); + } + } + + void FixedRightPreconditionedOperator::Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const { + if (input.Size() != Width()) { + throw std::invalid_argument("The right-preconditioned operator received an input with the wrong size."); + } + m_inversePreconditioner->Mult(input, m_preconditionedDirection); + m_jacobian->Mult(m_preconditionedDirection, output); + } + + const mfem::Operator &FixedRightPreconditionedOperator::GetJacobian() const noexcept { + return *m_jacobian; + } + + const mfem::Solver &FixedRightPreconditionedOperator::GetInversePreconditioner() const noexcept { + return *m_inversePreconditioner; + } + + void ResidualHistoryMonitor::Reset() { + mfem::IterativeSolverMonitor::Reset(); + m_history.clear(); + } + + void ResidualHistoryMonitor::MonitorResidual( + const int iteration, + const double norm, + const mfem::Vector &, + const bool final + ) { + m_history.push_back({.iteration = iteration, .reportedNorm = norm, .final = final}); + } + + const std::vector &ResidualHistoryMonitor::GetHistory() const noexcept { + return m_history; + } + + DirectResidualMeasurement measureDirectResidual( + const mfem::Operator &jacobian, + const mfem::Vector &rightHandSide, + const mfem::Vector &solution, + const std::span residualBlocks, + const MPI_Comm communicator, + const double denominatorFloor + ) { + if (jacobian.Height() != jacobian.Width() || rightHandSide.Size() != jacobian.Height() || + solution.Size() != jacobian.Width()) { + throw std::invalid_argument("Direct residual measurement received incompatible linear-system dimensions."); + } + if (!std::isfinite(denominatorFloor) || denominatorFloor <= 0.0) { + throw std::invalid_argument("The direct-residual denominator floor must be finite and positive."); + } + verify_finite_vector(rightHandSide, "Direct residual measurement received a non-finite right-hand side."); + verify_finite_vector(solution, "Direct residual measurement received a non-finite solution."); + + int expectedOffset = 0; + for (const operators::RootBlockDescriptor &block : residualBlocks) { + if (block.kind != operators::RootBlockKind::residual || block.offset != expectedOffset || block.size < 0 || + block.offset + block.size > jacobian.Height() || !std::isfinite(block.scale) || block.scale <= 0.0) { + throw std::invalid_argument("Residual block descriptors do not form the canonical equation layout."); + } + expectedOffset += block.size; + } + if (expectedOffset != jacobian.Height()) { + throw std::invalid_argument("Residual block descriptors do not cover the complete equation vector."); + } + + mfem::Vector action(jacobian.Height()); + jacobian.Mult(solution, action); + if (action.Size() != rightHandSide.Size()) { + throw std::runtime_error("The Jacobian returned an action with the wrong size."); + } + mfem::Vector trueResidual(rightHandSide); + trueResidual -= action; + verify_finite_vector(trueResidual, "Direct residual measurement produced a non-finite residual."); + + DirectResidualMeasurement measurement; + measurement.rightHandSideNorm = global_norm(rightHandSide, communicator); + measurement.trueResidualNorm = global_norm(trueResidual, communicator); + const double denominator = std::max(measurement.rightHandSideNorm, denominatorFloor); + measurement.relativeResidual = measurement.trueResidualNorm / denominator; + measurement.blocks.reserve(residualBlocks.size()); + + for (const operators::RootBlockDescriptor &block : residualBlocks) { + const mfem::Vector blockRightHandSide( + const_cast(rightHandSide.GetData()) + block.offset, block.size + ); + const mfem::Vector blockResidual(trueResidual.GetData() + block.offset, block.size); + const double blockRightHandSideNorm = global_norm(blockRightHandSide, communicator); + const double blockResidualNorm = global_norm(blockResidual, communicator); + const double blockDenominator = std::max(blockRightHandSideNorm, denominatorFloor); + const double globalResidualFraction = + measurement.trueResidualNorm > denominatorFloor + ? blockResidualNorm * blockResidualNorm / + (measurement.trueResidualNorm * measurement.trueResidualNorm) + : 0.0; + measurement.blocks.push_back( + {.stableId = std::string(block.stableId), + .size = block.size, + .descriptorScale = block.scale, + .rightHandSideNorm = blockRightHandSideNorm, + .trueResidualNorm = blockResidualNorm, + .blockRelativeResidual = blockResidualNorm / blockDenominator, + .scaledRightHandSideNorm = blockRightHandSideNorm / block.scale, + .scaledTrueResidualNorm = blockResidualNorm / block.scale, + .contributionToGlobalRelativeResidual = blockResidualNorm / denominator, + .fractionOfGlobalSquaredResidualNorm = globalResidualFraction} + ); + } + return measurement; + } + + LinearSolveMeasurement measureLinearSolve( + const mfem::IterativeSolver &iterativeSolver, + const mfem::Operator &jacobian, + const mfem::Vector &rightHandSide, + const mfem::Vector &solution, + const std::span residualBlocks, + const OperatorApplicationStatistics &jacobianStatistics, + const OperatorApplicationStatistics &inversePreconditionerStatistics, + const PreconditionerLifecycleStatistics &inversePreconditionerLifecycle, + const ResidualHistoryMonitor &monitor, + const double localSolveSeconds, + const MPI_Comm communicator, + const double denominatorFloor + ) { + if (!std::isfinite(localSolveSeconds) || localSolveSeconds < 0.0) { + throw std::invalid_argument("A linear-solve duration must be finite and nonnegative."); + } + + const DirectResidualMeasurement directResidual = + measureDirectResidual(jacobian, rightHandSide, solution, residualBlocks, communicator, denominatorFloor); + const double reportedInitial = iterativeSolver.GetInitialNorm(); + const double reportedFinal = iterativeSolver.GetFinalNorm(); + const double reportedReduction = + std::abs(reportedInitial) > denominatorFloor ? std::abs(reportedFinal) / std::abs(reportedInitial) : 0.0; + double digitsPerJacobianApplication = 0.0; + if (jacobianStatistics.applications > 0 && directResidual.relativeResidual >= 0.0 && + std::isfinite(directResidual.relativeResidual)) { + digitsPerJacobianApplication = -std::log10(std::max(directResidual.relativeResidual, denominatorFloor)) / + static_cast(jacobianStatistics.applications); + } + + return { + .solverConverged = iterativeSolver.GetConverged(), + .outerIterations = iterativeSolver.GetNumIterations(), + .solverReportedInitialNorm = reportedInitial, + .solverReportedFinalNorm = reportedFinal, + .solverReportedResidualReduction = reportedReduction, + .trueResidualDigitsReducedPerJacobianApplication = digitsPerJacobianApplication, + .solveSecondsMaximumRank = maximum_rank_value(localSolveSeconds, communicator), + .jacobian = maximum_rank_statistics(jacobianStatistics, communicator), + .inversePreconditioner = maximum_rank_statistics(inversePreconditionerStatistics, communicator), + .inversePreconditionerLifecycle = + maximum_rank_lifecycle_statistics(inversePreconditionerLifecycle, communicator), + .directResidual = directResidual, + .reportedResidualHistory = monitor.GetHistory() + }; + } + + ArnoldiSpectralMeasurement measureArnoldiSpectrum( + const mfem::Operator &operation, + const mfem::Vector &initialDirection, + const MPI_Comm communicator, + const ArnoldiOptions &options + ) { + if (operation.Height() != operation.Width() || operation.Width() <= 0) { + throw std::invalid_argument("Arnoldi diagnostics require a nonempty square operator."); + } + if (initialDirection.Size() != operation.Width()) { + throw std::invalid_argument("The Arnoldi initial direction has the wrong size."); + } + if (options.krylovDimension <= 0 || !std::isfinite(options.breakdownRelativeTolerance) || + options.breakdownRelativeTolerance < 0.0 || !std::isfinite(options.ritzConvergenceRelativeTolerance) || + options.ritzConvergenceRelativeTolerance < 0.0) { + throw std::invalid_argument("Arnoldi diagnostic options are invalid."); + } + verify_finite_vector(initialDirection, "Arnoldi diagnostics received a non-finite initial direction."); + + const Clock::time_point measurementStart = Clock::now(); + OperatorApplicationStatistics localApplicationStatistics; + + const double initialNorm = global_norm(initialDirection, communicator); + if (!std::isfinite(initialNorm) || initialNorm <= 0.0) { + throw std::invalid_argument("Arnoldi diagnostics require a nonzero initial direction."); + } + + const int requestedDimension = std::min(options.krylovDimension, operation.Width()); + Eigen::MatrixXd hessenberg = Eigen::MatrixXd::Zero(requestedDimension + 1, requestedDimension); + std::vector basis; + basis.reserve(static_cast(requestedDimension + 1)); + basis.emplace_back(initialDirection); + basis.back() /= initialNorm; + + int achievedDimension{0}; + bool invariantSubspaceFound{false}; + + for (int column = 0; column < requestedDimension; ++column) { + mfem::Vector candidate(operation.Height()); + const Clock::time_point applicationStart = Clock::now(); + operation.Mult(basis[static_cast(column)], candidate); + const double applicationSeconds = seconds_between(applicationStart, Clock::now()); + ++localApplicationStatistics.applications; + localApplicationStatistics.totalSeconds += applicationSeconds; + localApplicationStatistics.maximumSeconds = + std::max(localApplicationStatistics.maximumSeconds, applicationSeconds); + if (candidate.Size() != operation.Height()) { + throw std::runtime_error("The Arnoldi operator returned a vector with the wrong size."); + } + verify_finite_vector(candidate, "The Arnoldi operator produced a non-finite vector."); + const double unorthogonalizedNorm = global_norm(candidate, communicator); + + const int passCount = options.reorthogonalize ? 2 : 1; + for (int pass = 0; pass < passCount; ++pass) { + for (int row = 0; row <= column; ++row) { + const double projection = global_dot(basis[static_cast(row)], candidate, communicator); + hessenberg(row, column) += projection; + candidate.Add(-projection, basis[static_cast(row)]); + } + } + + const double nextNorm = global_norm(candidate, communicator); + hessenberg(column + 1, column) = nextNorm; + achievedDimension = column + 1; + const double breakdownScale = std::max(unorthogonalizedNorm, 1.0); + if (nextNorm <= options.breakdownRelativeTolerance * breakdownScale) { + invariantSubspaceFound = true; + break; + } + if (column + 1 < requestedDimension) { + candidate /= nextNorm; + basis.push_back(std::move(candidate)); + } + } + + if (achievedDimension <= 0) { + throw std::runtime_error("Arnoldi diagnostics did not construct a Krylov projection."); + } + + const Eigen::MatrixXd projected = copy_hessenberg(hessenberg, achievedDimension, achievedDimension); + const Eigen::MatrixXd projectedRectangular = + copy_hessenberg(hessenberg, achievedDimension + 1, achievedDimension); + + Eigen::EigenSolver eigenSolver(projected, true); + if (eigenSolver.info() != Eigen::Success) { + throw std::runtime_error("The projected Arnoldi eigenproblem did not converge."); + } + Eigen::JacobiSVD singularValueDecomposition(projectedRectangular); + if (singularValueDecomposition.info() != Eigen::Success) { + throw std::runtime_error("The projected Arnoldi singular-value problem did not converge."); + } + + ArnoldiSpectralMeasurement measurement; + measurement.requestedDimension = requestedDimension; + measurement.achievedDimension = achievedDimension; + measurement.invariantSubspaceFound = invariantSubspaceFound; + const OperatorApplicationStatistics globalApplicationStatistics = + maximum_rank_statistics(localApplicationStatistics, communicator); + measurement.operatorApplications = globalApplicationStatistics.applications; + measurement.operatorApplicationSecondsMaximumRank = globalApplicationStatistics.totalSeconds; + measurement.operatorMaximumApplicationSecondsMaximumRank = globalApplicationStatistics.maximumSeconds; + measurement.ritzValues.reserve(static_cast(achievedDimension)); + + const Eigen::VectorXd singularValues = singularValueDecomposition.singularValues(); + measurement.projectedLargestSingularValue = singularValues(0); + measurement.projectedSmallestSingularValue = singularValues(singularValues.size() - 1); + measurement.projectedConditionProxy = + measurement.projectedSmallestSingularValue > 0.0 + ? measurement.projectedLargestSingularValue / measurement.projectedSmallestSingularValue + : std::numeric_limits::infinity(); + + const double finalSubdiagonal = hessenberg(achievedDimension, achievedDimension - 1); + std::complex centroid{0.0, 0.0}; + const auto eigenvalues = eigenSolver.eigenvalues(); + const auto eigenvectors = eigenSolver.eigenvectors(); + for (int index = 0; index < achievedDimension; ++index) { + const std::complex eigenvalue = eigenvalues(index); + const double eigenvectorNorm = eigenvectors.col(index).norm(); + const double residualEstimate = + eigenvectorNorm > 0.0 + ? std::abs(finalSubdiagonal * eigenvectors(achievedDimension - 1, index)) / eigenvectorNorm + : std::numeric_limits::infinity(); + const double convergenceScale = std::max(std::abs(eigenvalue), 1.0); + const double relativeResidualEstimate = residualEstimate / convergenceScale; + const bool converged = relativeResidualEstimate <= options.ritzConvergenceRelativeTolerance; + + measurement.ritzValues.push_back( + {.realPart = eigenvalue.real(), + .imaginaryPart = eigenvalue.imag(), + .magnitude = std::abs(eigenvalue), + .distanceFromOne = std::abs(eigenvalue - std::complex{1.0, 0.0}), + .residualEstimate = residualEstimate, + .relativeResidualEstimate = relativeResidualEstimate, + .converged = converged} + ); + centroid += eigenvalue; + measurement.convergedRitzValueCount += converged ? 1 : 0; + measurement.negativeRealPartCount += eigenvalue.real() < 0.0 ? 1 : 0; + } + centroid /= static_cast(achievedDimension); + measurement.centroidRealPart = centroid.real(); + measurement.centroidImaginaryPart = centroid.imag(); + + measurement.minimumMagnitude = std::numeric_limits::infinity(); + measurement.minimumRealPart = std::numeric_limits::infinity(); + measurement.maximumRealPart = -std::numeric_limits::infinity(); + double squaredDistanceFromOne{0.0}; + double squaredClusterRadius{0.0}; + for (const RitzValueMeasurement &ritz : measurement.ritzValues) { + const std::complex value{ritz.realPart, ritz.imaginaryPart}; + measurement.minimumMagnitude = std::min(measurement.minimumMagnitude, ritz.magnitude); + measurement.maximumMagnitude = std::max(measurement.maximumMagnitude, ritz.magnitude); + measurement.minimumRealPart = std::min(measurement.minimumRealPart, ritz.realPart); + measurement.maximumRealPart = std::max(measurement.maximumRealPart, ritz.realPart); + measurement.maximumAbsoluteImaginaryPart = + std::max(measurement.maximumAbsoluteImaginaryPart, std::abs(ritz.imaginaryPart)); + squaredDistanceFromOne += ritz.distanceFromOne * ritz.distanceFromOne; + squaredClusterRadius += std::norm(value - centroid); + + double pairDefect = std::numeric_limits::infinity(); + for (const RitzValueMeasurement &candidate : measurement.ritzValues) { + pairDefect = std::min( + pairDefect, + std::abs(std::complex{candidate.realPart, candidate.imaginaryPart} - std::conj(value)) + ); + } + measurement.conjugatePairDefect = std::max(measurement.conjugatePairDefect, pairDefect); + } + measurement.rmsDistanceFromOne = std::sqrt(squaredDistanceFromOne / achievedDimension); + measurement.rmsClusterRadius = std::sqrt(squaredClusterRadius / achievedDimension); + + const double projectedFrobeniusSquared = projected.squaredNorm(); + if (projectedFrobeniusSquared > 0.0) { + const Eigen::MatrixXd normalityCommutator = + projected.transpose() * projected - projected * projected.transpose(); + measurement.projectedDepartureFromNormality = normalityCommutator.norm() / projectedFrobeniusSquared; + } + + const Eigen::MatrixXd hermitianPart = 0.5 * (projected + projected.transpose()); + Eigen::SelfAdjointEigenSolver fieldOfValuesSolver(hermitianPart); + if (fieldOfValuesSolver.info() != Eigen::Success) { + throw std::runtime_error("The projected field-of-values problem did not converge."); + } + measurement.projectedFieldOfValuesMinimumRealPart = fieldOfValuesSolver.eigenvalues().minCoeff(); + measurement.projectedFieldOfValuesMaximumRealPart = fieldOfValuesSolver.eigenvalues().maxCoeff(); + const double localMeasurementSeconds = seconds_between(measurementStart, Clock::now()); + const double localNonApplicationSeconds = + std::max(localMeasurementSeconds - localApplicationStatistics.totalSeconds, 0.0); + measurement.measurementSecondsMaximumRank = maximum_rank_value(localMeasurementSeconds, communicator); + measurement.nonApplicationSecondsMaximumRank = maximum_rank_value(localNonApplicationSeconds, communicator); + return measurement; + } + + std::vector selectRitzValues( + const ArnoldiSpectralMeasurement &measurement, + const RitzValueOrdering ordering, + const int count + ) { + if (count < 0) { + throw std::invalid_argument("The requested Ritz-value count must be nonnegative."); + } + + std::vector selected; + selected.reserve(measurement.ritzValues.size()); + for (const RitzValueMeasurement &value : measurement.ritzValues) { + if (value.converged) { + selected.push_back(value); + } + } + + std::ranges::sort(selected, [ordering](const RitzValueMeasurement &left, const RitzValueMeasurement &right) { + switch (ordering) { + case RitzValueOrdering::closest_to_zero: + return left.magnitude < right.magnitude; + case RitzValueOrdering::farthest_from_one: + return left.distanceFromOne > right.distanceFromOne; + case RitzValueOrdering::smallest_real_part: + return left.realPart < right.realPart; + case RitzValueOrdering::largest_magnitude: + return left.magnitude > right.magnitude; + } + return false; + }); + if (static_cast(selected.size()) > count) { + selected.resize(static_cast(count)); + } + return selected; + } +} // namespace mean_field::solver diff --git a/libmeanfield/interface/dimensions/quantities.cppm b/libmeanfield/interface/dimensions/quantities.cppm new file mode 100644 index 0000000..f254c1a --- /dev/null +++ b/libmeanfield/interface/dimensions/quantities.cppm @@ -0,0 +1,312 @@ +module; + +#include +#include +#include +#include + +export module mean_field:dimensions.quantities; + +export namespace mean_field::dimensions { + /* + * QuantityValue provides semantic strong typing for scalar physical + * values expressed in the unit system selected by a model. It does not + * perform dimensional algebra or unit conversion. + */ + struct PhysicalQuantity { }; + + struct ThermodynamicQuantity : PhysicalQuantity { }; + + template + concept PhysicalQuantityType = + std::same_as> && std::derived_from; + + template + concept ThermodynamicQuantityType = + PhysicalQuantityType && std::derived_from; + + namespace quantity { + struct Dimensionless final : PhysicalQuantity { + static constexpr std::string_view identifier = "dimensionless"; + }; + + struct Mass final : PhysicalQuantity { + static constexpr std::string_view identifier = "mass"; + }; + + struct Length final : PhysicalQuantity { + static constexpr std::string_view identifier = "length"; + }; + + struct Time final : PhysicalQuantity { + static constexpr std::string_view identifier = "time"; + }; + + struct Area final : PhysicalQuantity { + static constexpr std::string_view identifier = "area"; + }; + + struct Volume final : PhysicalQuantity { + static constexpr std::string_view identifier = "volume"; + }; + + struct Density final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "density"; + }; + + struct SurfaceDensity final : PhysicalQuantity { + static constexpr std::string_view identifier = "surface_density"; + }; + + struct NumberDensity final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "number_density"; + }; + + struct Pressure final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "pressure"; + }; + + struct Temperature final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "temperature"; + }; + + struct Entropy final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "entropy"; + }; + + struct SpecificEntropy final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "specific_entropy"; + }; + + struct ChemicalPotential final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "chemical_potential"; + }; + + struct Energy final : PhysicalQuantity { + static constexpr std::string_view identifier = "energy"; + }; + + struct InternalEnergy final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "internal_energy"; + }; + + struct SpecificEnergy final : PhysicalQuantity { + static constexpr std::string_view identifier = "specific_energy"; + }; + + struct SpecificInternalEnergy final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "specific_internal_energy"; + }; + + struct SpecificEnthalpy final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "specific_enthalpy"; + }; + + struct EnergyDensity final : ThermodynamicQuantity { + static constexpr std::string_view identifier = "energy_density"; + }; + + struct GravitationalPotential final : PhysicalQuantity { + static constexpr std::string_view identifier = "gravitational_potential"; + }; + + struct Velocity final : PhysicalQuantity { + static constexpr std::string_view identifier = "velocity"; + }; + + struct Acceleration final : PhysicalQuantity { + static constexpr std::string_view identifier = "acceleration"; + }; + + struct Frequency final : PhysicalQuantity { + static constexpr std::string_view identifier = "frequency"; + }; + + struct AngularVelocity final : PhysicalQuantity { + static constexpr std::string_view identifier = "angular_velocity"; + }; + + struct Momentum final : PhysicalQuantity { + static constexpr std::string_view identifier = "momentum"; + }; + + struct AngularMomentum final : PhysicalQuantity { + static constexpr std::string_view identifier = "angular_momentum"; + }; + + struct MomentOfInertia final : PhysicalQuantity { + static constexpr std::string_view identifier = "moment_of_inertia"; + }; + + struct Force final : PhysicalQuantity { + static constexpr std::string_view identifier = "force"; + }; + + struct Torque final : PhysicalQuantity { + static constexpr std::string_view identifier = "torque"; + }; + + struct Power final : PhysicalQuantity { + static constexpr std::string_view identifier = "power"; + }; + + struct Luminosity final : PhysicalQuantity { + static constexpr std::string_view identifier = "luminosity"; + }; + + struct MassFlowRate final : PhysicalQuantity { + static constexpr std::string_view identifier = "mass_flow_rate"; + }; + + struct Opacity final : PhysicalQuantity { + static constexpr std::string_view identifier = "opacity"; + }; + + struct DynamicViscosity final : PhysicalQuantity { + static constexpr std::string_view identifier = "dynamic_viscosity"; + }; + + struct KinematicViscosity final : PhysicalQuantity { + static constexpr std::string_view identifier = "kinematic_viscosity"; + }; + + struct MagneticFluxDensity final : PhysicalQuantity { + static constexpr std::string_view identifier = "magnetic_flux_density"; + }; + } // namespace quantity + + template + concept Numeric = std::integral || std::floating_point; + + template class QuantityValue final { + public: + explicit constexpr QuantityValue(const double value) noexcept : m_value(value) { + } + + [[nodiscard]] constexpr double value() const noexcept { + return m_value; + } + + [[nodiscard]] friend constexpr bool operator==( + const QuantityValue &, + const QuantityValue & + ) noexcept = default; + + friend constexpr QuantityValue operator+( + const QuantityValue &lhs, + const QuantityValue &rhs + ) noexcept { + return QuantityValue{lhs.m_value + rhs.m_value}; + } + + friend constexpr QuantityValue operator-( + const QuantityValue &lhs, + const QuantityValue &rhs + ) noexcept { + return QuantityValue{lhs.m_value - rhs.m_value}; + } + + template + friend constexpr QuantityValue operator*( + const QuantityValue &lhs, + const Scalar rhs + ) noexcept { + return QuantityValue{lhs.m_value * static_cast(rhs)}; + } + + template + friend constexpr QuantityValue operator*( + const Scalar lhs, + const QuantityValue &rhs + ) noexcept { + return QuantityValue{static_cast(lhs) * rhs.m_value}; + } + + template + friend constexpr QuantityValue operator/( + const QuantityValue &lhs, + const Scalar rhs + ) noexcept { + return QuantityValue{lhs.m_value / static_cast(rhs)}; + } + + template + friend constexpr std::partial_ordering operator<=>( + const QuantityValue &lhs, + const Scalar rhs + ) noexcept { + return lhs.m_value <=> static_cast(rhs); + } + + template + friend constexpr std::partial_ordering operator<=>( + const Scalar lhs, + const QuantityValue &rhs + ) noexcept { + return static_cast(lhs) <=> rhs.m_value; + } + + friend constexpr std::partial_ordering operator<=>( + const QuantityValue &lhs, + const QuantityValue &rhs + ) noexcept { + return lhs.m_value <=> rhs.m_value; + } + + private: + double m_value; + }; + + template struct IsQuantityValue : std::false_type { }; + + template struct IsQuantityValue> : std::true_type { }; + + template + concept QuantityValueType = IsQuantityValue>::value; + + template struct QuantityOf; + + template struct QuantityOf> { + using Type = Quantity; + }; + + template using QuantityOfT = typename QuantityOf>::Type; + + using DimensionlessValue = QuantityValue; + using MassValue = QuantityValue; + using LengthValue = QuantityValue; + using TimeValue = QuantityValue; + using AreaValue = QuantityValue; + using VolumeValue = QuantityValue; + using DensityValue = QuantityValue; + using SurfaceDensityValue = QuantityValue; + using NumberDensityValue = QuantityValue; + using PressureValue = QuantityValue; + using TemperatureValue = QuantityValue; + using EntropyValue = QuantityValue; + using SpecificEntropyValue = QuantityValue; + using ChemicalPotentialValue = QuantityValue; + using EnergyValue = QuantityValue; + using InternalEnergyValue = QuantityValue; + using SpecificEnergyValue = QuantityValue; + using SpecificInternalEnergyValue = QuantityValue; + using SpecificEnthalpyValue = QuantityValue; + using EnergyDensityValue = QuantityValue; + using GravitationalPotentialValue = QuantityValue; + using VelocityValue = QuantityValue; + using AccelerationValue = QuantityValue; + using FrequencyValue = QuantityValue; + using AngularVelocityValue = QuantityValue; + using MomentumValue = QuantityValue; + using AngularMomentumValue = QuantityValue; + using MomentOfInertiaValue = QuantityValue; + using ForceValue = QuantityValue; + using TorqueValue = QuantityValue; + using PowerValue = QuantityValue; + using LuminosityValue = QuantityValue; + using MassFlowRateValue = QuantityValue; + using OpacityValue = QuantityValue; + using DynamicViscosityValue = QuantityValue; + using KinematicViscosityValue = QuantityValue; + using MagneticFluxDensityValue = QuantityValue; +} // namespace mean_field::dimensions diff --git a/libmeanfield/interface/eos/concepts.cppm b/libmeanfield/interface/eos/concepts.cppm index f3088d5..04ea715 100644 --- a/libmeanfield/interface/eos/concepts.cppm +++ b/libmeanfield/interface/eos/concepts.cppm @@ -91,10 +91,10 @@ export namespace mean_field::eos { template concept BarotropicClosureEquationOfState = EquationOfStateModel && SupportsRelation && - SupportsPartialDerivative; + SupportsPartialDerivative; template concept PressureForceEquationOfState = EquationOfStateModel && SupportsRelation && - SupportsPartialDerivative; + SupportsPartialDerivative; } // namespace mean_field::eos diff --git a/libmeanfield/interface/eos/polytropic.cppm b/libmeanfield/interface/eos/polytropic.cppm index 65f8990..4d0f692 100644 --- a/libmeanfield/interface/eos/polytropic.cppm +++ b/libmeanfield/interface/eos/polytropic.cppm @@ -8,6 +8,11 @@ export import :eos.evaluation; export namespace mean_field::eos { class Polytrope final { public: + struct Parameters final { + double n; + double K; + }; + using Relations = RelationCatalog< PressureFromDensity, PressureFromSpecificEnthalpy, @@ -15,6 +20,13 @@ export namespace mean_field::eos { SpecificEnthalpyFromPressure, DensityFromSpecificEnthalpy>; + explicit Polytrope(const Parameters parameters) + : Polytrope( + parameters.n, + parameters.K + ) { + } + Polytrope( const double polytropic_index, const double polytropic_constant @@ -56,125 +68,131 @@ export namespace mean_field::eos { return m_enthalpy_scale; } - [[nodiscard]] PressureValue evaluate( + [[nodiscard]] dimensions::PressureValue evaluate( PressureFromDensity, - const DensityValue density + const dimensions::DensityValue density ) const { validate_nonnegativity(density.value(), "density"); if (density.value() == 0.0) { - return PressureValue{0.0}; + return dimensions::PressureValue{0.0}; } - return PressureValue{m_polytropic_constant * std::pow(density.value(), 1.0 + 1.0 / m_polytropic_index)}; + return dimensions::PressureValue{ + m_polytropic_constant * std::pow(density.value(), 1.0 + 1.0 / m_polytropic_index) + }; } - [[nodiscard]] SpecificEnthalpyValue evaluate( + [[nodiscard]] dimensions::SpecificEnthalpyValue evaluate( SpecificEnthalpyFromDensity, - const DensityValue density + const dimensions::DensityValue density ) const { validate_nonnegativity(density.value(), "density"); if (density.value() == 0.0) { - return SpecificEnthalpyValue{0.0}; + return dimensions::SpecificEnthalpyValue{0.0}; } - return SpecificEnthalpyValue{m_enthalpy_scale * std::pow(density.value(), 1.0 / m_polytropic_index)}; + return dimensions::SpecificEnthalpyValue{ + m_enthalpy_scale * std::pow(density.value(), 1.0 / m_polytropic_index) + }; } - [[nodiscard]] DensityValue evaluate( + [[nodiscard]] dimensions::DensityValue evaluate( DensityFromSpecificEnthalpy, - const SpecificEnthalpyValue specificEnthalpy + const dimensions::SpecificEnthalpyValue specificEnthalpy ) const { validate_finite(specificEnthalpy.value(), "specific enthalpy"); if (specificEnthalpy.value() <= 0.0) { - return DensityValue{0.0}; + return dimensions::DensityValue{0.0}; } - return DensityValue{std::pow(specificEnthalpy.value() / m_enthalpy_scale, m_polytropic_index)}; + return dimensions::DensityValue{std::pow(specificEnthalpy.value() / m_enthalpy_scale, m_polytropic_index)}; } - [[nodiscard]] PressureValue evaluate( + [[nodiscard]] dimensions::PressureValue evaluate( PressureFromSpecificEnthalpy, - const SpecificEnthalpyValue specificEnthalpy + const dimensions::SpecificEnthalpyValue specificEnthalpy ) const { - const DensityValue density = evaluate(DensityFromSpecificEnthalpy{}, specificEnthalpy); + const dimensions::DensityValue density = evaluate(DensityFromSpecificEnthalpy{}, specificEnthalpy); if (specificEnthalpy.value() <= 0.0) { - return PressureValue{0.0}; + return dimensions::PressureValue{0.0}; } - return PressureValue{density.value() * specificEnthalpy.value() / (m_polytropic_index + 1.0)}; + return dimensions::PressureValue{density.value() * specificEnthalpy.value() / (m_polytropic_index + 1.0)}; } - [[nodiscard]] SpecificEnthalpyValue evaluate( + [[nodiscard]] dimensions::SpecificEnthalpyValue evaluate( SpecificEnthalpyFromPressure, - const PressureValue pressure + const dimensions::PressureValue pressure ) const { validate_nonnegativity(pressure.value(), "pressure"); if (pressure.value() == 0.0) { - return SpecificEnthalpyValue{0.0}; + return dimensions::SpecificEnthalpyValue{0.0}; } const double indexPlusOne = m_polytropic_index + 1.0; - return SpecificEnthalpyValue{ + return dimensions::SpecificEnthalpyValue{ indexPlusOne * std::pow(m_polytropic_constant, m_polytropic_index / indexPlusOne) * std::pow(pressure.value(), 1.0 / indexPlusOne) }; } [[nodiscard]] PartialDerivative< - quantity::Density, - quantity::SpecificEnthalpy> + dimensions::quantity::Density, + dimensions::quantity::SpecificEnthalpy> partialDerivative( DensityFromSpecificEnthalpy, - WithRespectTo, - const SpecificEnthalpyValue specificEnthalpy + WithRespectTo, + const dimensions::SpecificEnthalpyValue specificEnthalpy ) const { validate_finite(specificEnthalpy.value(), "specific enthalpy"); if (specificEnthalpy.value() < 0.0) { - return PartialDerivative{0.0}; + return PartialDerivative{0.0}; } if (specificEnthalpy.value() == 0.0) { - return PartialDerivative{ + return PartialDerivative{ m_polytropic_index == 1.0 ? 1.0 / m_enthalpy_scale : 0.0 }; } - return PartialDerivative{ + return PartialDerivative{ m_polytropic_index / m_enthalpy_scale * std::pow(specificEnthalpy.value() / m_enthalpy_scale, m_polytropic_index - 1.0) }; } [[nodiscard]] PartialDerivative< - quantity::Pressure, - quantity::SpecificEnthalpy> + dimensions::quantity::Pressure, + dimensions::quantity::SpecificEnthalpy> partialDerivative( PressureFromSpecificEnthalpy, - WithRespectTo, - const SpecificEnthalpyValue specificEnthalpy + WithRespectTo, + const dimensions::SpecificEnthalpyValue specificEnthalpy ) const { - const DensityValue density = evaluate(DensityFromSpecificEnthalpy{}, specificEnthalpy); + const dimensions::DensityValue density = evaluate(DensityFromSpecificEnthalpy{}, specificEnthalpy); - return PartialDerivative{density.value()}; + return PartialDerivative{ + density.value() + }; } [[nodiscard]] PartialDerivative< - quantity::Pressure, - quantity::Density> + dimensions::quantity::Pressure, + dimensions::quantity::Density> partialDerivative( PressureFromDensity, - WithRespectTo, - const DensityValue density + WithRespectTo, + const dimensions::DensityValue density ) const { validate_nonnegativity(density.value(), "density"); if (density.value() == 0.0) { - return PartialDerivative{0.0}; + return PartialDerivative{0.0}; } - return PartialDerivative{ + return PartialDerivative{ m_polytropic_constant * (1.0 + 1.0 / m_polytropic_index) * std::pow(density.value(), 1.0 / m_polytropic_index) }; diff --git a/libmeanfield/interface/eos/pressure_surface.cppm b/libmeanfield/interface/eos/pressure_surface.cppm index b9df808..3a1d371 100644 --- a/libmeanfield/interface/eos/pressure_surface.cppm +++ b/libmeanfield/interface/eos/pressure_surface.cppm @@ -13,10 +13,10 @@ export namespace mean_field::eos { ThermodynamicQuantityType InputQuantity, typename SurfaceState> [[nodiscard]] constexpr auto pressureSurfaceRelationInput( - const PressureValue targetPressure, + const dimensions::PressureValue targetPressure, const SurfaceState &state ) { - if constexpr (std::same_as) { + if constexpr (std::same_as) { return targetPressure; } else { return state.value(InputQuantity{}); @@ -30,9 +30,9 @@ export namespace mean_field::eos { template < typename EquationOfState, typename SurfaceState> - [[nodiscard]] static QuantityValue requiredCarrierValue( + [[nodiscard]] static dimensions::QuantityValue requiredCarrierValue( const EquationOfState &equationOfState, - const PressureValue targetPressure, + const dimensions::PressureValue targetPressure, const SurfaceState &state ) { return evaluate( @@ -47,11 +47,11 @@ export namespace mean_field::eos { typename SurfaceVariation> [[nodiscard]] static double inputJacobianContribution( const EquationOfState &equationOfState, - const PressureValue targetPressure, + const dimensions::PressureValue targetPressure, const SurfaceState &state, const SurfaceVariation &variation ) { - if constexpr (std::same_as) { + if constexpr (std::same_as) { return 0.0; } else { const auto derivative = partialDerivative( @@ -67,7 +67,7 @@ export namespace mean_field::eos { typename SurfaceVariation> [[nodiscard]] static double carrierCorrectionJacobianAction( const EquationOfState &equationOfState, - const PressureValue targetPressure, + const dimensions::PressureValue targetPressure, const SurfaceState &state, const SurfaceVariation &variation ) { @@ -92,18 +92,18 @@ export namespace mean_field::eos { ResolvedPressureSurfaceRelation( const EquationOfState &equationOfState, - const PressureValue targetPressure + const dimensions::PressureValue targetPressure ) noexcept : m_equationOfState(std::addressof(equationOfState)), m_targetPressure(targetPressure) { } - [[nodiscard]] PressureValue targetPressure() const noexcept { + [[nodiscard]] dimensions::PressureValue targetPressure() const noexcept { return m_targetPressure; } template - [[nodiscard]] QuantityValue requiredCarrierValue(const SurfaceState &state) const { + [[nodiscard]] dimensions::QuantityValue requiredCarrierValue(const SurfaceState &state) const { return detail::PressureSurfaceRelationOperations::requiredCarrierValue( *m_equationOfState, m_targetPressure, state ); @@ -123,6 +123,6 @@ export namespace mean_field::eos { private: const EquationOfState *m_equationOfState; - PressureValue m_targetPressure; + dimensions::PressureValue m_targetPressure; }; } // namespace mean_field::eos diff --git a/libmeanfield/interface/eos/quantities.cppm b/libmeanfield/interface/eos/quantities.cppm index 3e47cc8..d21b1a3 100644 --- a/libmeanfield/interface/eos/quantities.cppm +++ b/libmeanfield/interface/eos/quantities.cppm @@ -2,132 +2,42 @@ module; #include #include -#include -#include export module mean_field:eos.quantities; +export import :dimensions.quantities; export namespace mean_field::eos { - struct ThermodynamicQuantity { }; + // Compatibility names for the thermodynamic subset now owned by the + // general dimensions partition. + using ThermodynamicQuantity = dimensions::ThermodynamicQuantity; template - concept ThermodynamicQuantityType = - std::same_as> && std::derived_from; + concept ThermodynamicQuantityType = dimensions::ThermodynamicQuantityType; namespace quantity { - struct Density final : ThermodynamicQuantity { - static constexpr std::string_view identifier = "density"; - }; - - struct Pressure final : ThermodynamicQuantity { - static constexpr std::string_view identifier = "pressure"; - }; - - struct SpecificEnthalpy final : ThermodynamicQuantity { - static constexpr std::string_view identifier = "specific_enthalpy"; - }; + using Density = dimensions::quantity::Density; + using Pressure = dimensions::quantity::Pressure; + using SpecificEnthalpy = dimensions::quantity::SpecificEnthalpy; } // namespace quantity template - concept Numeric = std::integral || std::floating_point; + concept Numeric = dimensions::Numeric; - template class QuantityValue final { - public: - explicit constexpr QuantityValue(const double value) noexcept : m_value(value) { - } + template using QuantityValue = dimensions::QuantityValue; - [[nodiscard]] constexpr double value() const noexcept { - return m_value; - } + using DensityValue = dimensions::DensityValue; + using PressureValue = dimensions::PressureValue; + using SpecificEnthalpyValue = dimensions::SpecificEnthalpyValue; - [[nodiscard]] friend constexpr bool operator==( - const QuantityValue &, - const QuantityValue & - ) noexcept = default; - - friend constexpr QuantityValue operator+( - const QuantityValue &lhs, - const QuantityValue &rhs - ) noexcept { - return QuantityValue{lhs.m_value + rhs.m_value}; - } - - friend constexpr QuantityValue operator-( - const QuantityValue &lhs, - const QuantityValue &rhs - ) noexcept { - return QuantityValue{lhs.m_value - rhs.m_value}; - } - - template - friend constexpr QuantityValue operator*( - const QuantityValue &lhs, - rhsT rhs - ) noexcept { - return QuantityValue{lhs.m_value * static_cast(rhs)}; - } - - template - friend constexpr QuantityValue operator*( - lhsT lhs, - const QuantityValue &rhs - ) noexcept { - return QuantityValue{static_cast(lhs) * rhs.m_value}; - } - - template - friend constexpr QuantityValue operator/( - const QuantityValue &lhs, - rhsT rhs - ) noexcept { - return QuantityValue{lhs.m_value / static_cast(rhs)}; - } - - template - friend constexpr std::partial_ordering operator<=>( - const QuantityValue &lhs, - compT rhs - ) noexcept { - return lhs.m_value <=> static_cast(rhs); - } - - template - friend constexpr std::partial_ordering operator<=>( - compT lhs, - const QuantityValue &rhs - ) noexcept { - return static_cast(lhs) <=> rhs.m_value; - } - - friend constexpr std::partial_ordering operator<=>( - const QuantityValue &lhs, - const QuantityValue &rhs - ) noexcept { - return lhs.m_value <=> rhs.m_value; - } - - private: - double m_value; - }; - - using DensityValue = QuantityValue; - using PressureValue = QuantityValue; - using SpecificEnthalpyValue = QuantityValue; - - template struct IsQuantityValue : std::false_type { }; - - template struct IsQuantityValue> : std::true_type { }; + template using IsQuantityValue = dimensions::IsQuantityValue; template - concept QuantityValueType = IsQuantityValue>::value; + concept QuantityValueType = + dimensions::QuantityValueType && ThermodynamicQuantityType>; - template struct QuantityOf; + template using QuantityOf = dimensions::QuantityOf; - template struct QuantityOf> { - using Type = Quantity; - }; - - template using QuantityOfT = typename QuantityOf>::Type; + template using QuantityOfT = dimensions::QuantityOfT; template class PartialDerivative final { @@ -163,7 +73,7 @@ export namespace mean_field::eos { InputQuantity> &rhs ) noexcept; - template + template friend constexpr PartialDerivative< OutputQuantity, InputQuantity> @@ -171,21 +81,21 @@ export namespace mean_field::eos { const PartialDerivative< OutputQuantity, InputQuantity> &, - rhsT + Scalar ) noexcept; - template + template friend constexpr PartialDerivative< OutputQuantity, InputQuantity> operator*( - lhsT, + Scalar, const PartialDerivative< OutputQuantity, InputQuantity> & ) noexcept; - template + template friend constexpr PartialDerivative< OutputQuantity, InputQuantity> @@ -193,22 +103,22 @@ export namespace mean_field::eos { const PartialDerivative< OutputQuantity, InputQuantity> &, - rhsT + Scalar ) noexcept; - template + template friend constexpr std::partial_ordering operator<=>( const PartialDerivative< OutputQuantity, InputQuantity> &lhs, - cmpT rhs + Scalar rhs ) noexcept { return lhs.m_value <=> static_cast(rhs); } - template + template friend constexpr std::partial_ordering operator<=>( - cmpT lhs, + Scalar lhs, const PartialDerivative< OutputQuantity, InputQuantity> &rhs diff --git a/libmeanfield/interface/eos/relations.cppm b/libmeanfield/interface/eos/relations.cppm index d8bf825..4d0e0da 100644 --- a/libmeanfield/interface/eos/relations.cppm +++ b/libmeanfield/interface/eos/relations.cppm @@ -85,9 +85,11 @@ export namespace mean_field::eos { template using RelationInputT = typename detail::QuantityAt::Type; - using PressureFromDensity = Relation; - using PressureFromSpecificEnthalpy = Relation; - using SpecificEnthalpyFromDensity = Relation; - using SpecificEnthalpyFromPressure = Relation; - using DensityFromSpecificEnthalpy = Relation; + using PressureFromDensity = Relation; + using PressureFromSpecificEnthalpy = + Relation; + using SpecificEnthalpyFromDensity = Relation; + using SpecificEnthalpyFromPressure = + Relation; + using DensityFromSpecificEnthalpy = Relation; } // namespace mean_field::eos diff --git a/libmeanfield/interface/equilibrium/stellar_discretization.cppm b/libmeanfield/interface/equilibrium/stellar_discretization.cppm new file mode 100644 index 0000000..0bc0e64 --- /dev/null +++ b/libmeanfield/interface/equilibrium/stellar_discretization.cppm @@ -0,0 +1,64 @@ +module; + +#include +#include + +export module mean_field:equilibrium.stellar_discretization; + +export import :fem; +export import :mapping.domain_mapper; + +export namespace mean_field::equilibrium { + /* + * An explicit, non-owning view of the numerical discretization used by a + * stellar equilibrium problem. The referenced FEM and mapper must outlive + * every problem and structure that uses this view. + * + * Ownership cannot move here yet because FEM currently also contains + * mutable field workspaces. Separating those workspaces is a prerequisite + * for shared discretization ownership by solved Structure objects. + */ + class StellarDiscretization final { + public: + explicit StellarDiscretization(fem::FEM &finiteElementModel) + : StellarDiscretization( + finiteElementModel, + RequireDomainMapper(finiteElementModel) + ) { + } + + StellarDiscretization( + fem::FEM &finiteElementModel, + const mapping::DomainMapper &domainMapper + ) + : m_finiteElementModel(std::addressof(finiteElementModel)), + m_domainMapper(std::addressof(domainMapper)) { + if (!finiteElementModel.okay()) { + throw std::invalid_argument("A stellar discretization requires a complete finite-element model."); + } + } + + [[nodiscard]] fem::FEM &finiteElementModel() const noexcept { + return *m_finiteElementModel; + } + + [[nodiscard]] const mapping::DomainMapper &domainMapper() const noexcept { + return *m_domainMapper; + } + + [[nodiscard]] bool isCurrent() const noexcept { + return m_finiteElementModel != nullptr && m_domainMapper != nullptr && m_finiteElementModel->okay(); + } + + private: + [[nodiscard]] static const mapping::DomainMapper &RequireDomainMapper(const fem::FEM &finiteElementModel) { + if (finiteElementModel.domainMapperStateless == nullptr) { + throw std::invalid_argument("A stellar discretization requires a domain mapper."); + } + return *finiteElementModel.domainMapperStateless; + } + + fem::FEM *m_finiteElementModel; + const mapping::DomainMapper *m_domainMapper; + }; +} // namespace mean_field::equilibrium diff --git a/libmeanfield/interface/field/field_registry.cppm b/libmeanfield/interface/field/field_registry.cppm index 94ec741..1c1de11 100644 --- a/libmeanfield/interface/field/field_registry.cppm +++ b/libmeanfield/interface/field/field_registry.cppm @@ -208,6 +208,9 @@ export namespace mean_field::field { using FormList = TypeList; }; + // Current realization of MultiplierFor. This remains a + // barotrope-specific field representation: the specification compiler, + // rather than the universal state registry, decides when it is present. struct BarotropicConstant { static constexpr std::string_view name = "barotropic_constant"; @@ -226,6 +229,26 @@ export namespace mean_field::field { static_assert(constraintsAreValid); }; + // Solver border generated by FixedCentralDensity. This is deliberately a + // non-spatial numerical coordinate rather than a physical stellar field. + struct CentralDensityBorder { + static constexpr std::string_view name = "central_density_border"; + + using Support = NonSpatialSupport; + + struct Scalar final : GlobalScalarQ { + static constexpr std::string_view symbol = "lambda_rho_c"; + }; + + using Quantities = TypeList; + using Constraints = TypeList<>; + using FormList = TypeList<>; + + static constexpr bool constraintsAreValid = validate_constraints(Constraints{}); + + static_assert(constraintsAreValid); + }; + // ========================================================================= // Specific enthalpy // diff --git a/libmeanfield/interface/mapping/domain_mapper.cppm b/libmeanfield/interface/mapping/domain_mapper.cppm index ae87580..c025419 100644 --- a/libmeanfield/interface/mapping/domain_mapper.cppm +++ b/libmeanfield/interface/mapping/domain_mapper.cppm @@ -78,6 +78,7 @@ export namespace mean_field::mapping { int m_dimension; mfem::Vector m_shape; + mfem::DenseMatrix m_reference_dshape; mfem::DenseMatrix m_mesh_dshape; mfem::Vector m_field_value; mfem::DenseMatrix m_field_jacobian; @@ -178,7 +179,8 @@ export namespace mean_field::mapping { const mfem::IntegrationPoint &integration_point, Workspace &workspace, mfem::Vector &value, - mfem::DenseMatrix &jacobian + mfem::DenseMatrix &jacobian, + const mfem::DenseMatrix *inverse_mesh_jacobian ) const; [[nodiscard]] MappingStatus EvaluateCompactificationCoordinate( @@ -186,7 +188,19 @@ export namespace mean_field::mapping { mfem::ElementTransformation &transformation, const mfem::IntegrationPoint &integration_point, Workspace &workspace, - CompactificationPointData &point_data + CompactificationPointData &point_data, + const mfem::DenseMatrix *inverse_mesh_jacobian + ) const; + + [[nodiscard]] MappingStatus EvaluatePointVariationImpl( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + const MappingPointContext &base_context, + Workspace &workspace, + MappingPointVariation &variation, + const mfem::DenseMatrix *inverse_mesh_jacobian ) const; [[nodiscard]] static mfem::ElementTransformation &SelectFaceElementTransformation( diff --git a/libmeanfield/interface/mean_field.cppm b/libmeanfield/interface/mean_field.cppm index 5060efd..19bb3d1 100644 --- a/libmeanfield/interface/mean_field.cppm +++ b/libmeanfield/interface/mean_field.cppm @@ -25,6 +25,7 @@ export import :integrators.viscosity; export import :quadrature.policy; export import :quadrature.mfem; export import :solver.fields; +export import :solver.preconditioning_diagnostics; export import :utils.blocks; export import :operators.gravity_field; export import :operators.gravity_field_jacobian; @@ -51,9 +52,14 @@ export import :operators.context.rotational_displacement_force; export import :operators.kernels.rotational_displacement_force; export import :operators.prepared_rotational_displacement_force; export import :operators.prepared_displacement_residual; +export import :dimensions.quantities; export import :model.structure_profile; export import :model.structure.base; export import :model.structure.polytropic; +export import :model.specifications; +export import :model.typed_stellar; +export import :model.compiled_fixed_mass; +export import :model.compiled_fixed_central_density; export import :eos.quantities; export import :eos.relations; export import :eos.concepts; @@ -61,6 +67,7 @@ export import :eos.evaluation; export import :eos.pressure_surface; export import :eos.runtime; export import :eos.polytrope; +export import :seed.lane_emden; export import :surface.constant; export import :surface.dependencies; export import :surface.compiled; @@ -73,7 +80,15 @@ export import :deformation.vacuum_extension; export import :deformation.radial_extensions; export import :deformation.domain_deformation; export import :model.stellar; +export import :operators.root_manifest; +export import :operators.prepared_constraint; export import :operators.prepared_mass_normalization; +export import :operators.prepared_central_density; export import :operators.prepared_centering_constraint; export import :operators.prepared_surface_constraint; export import :operators.prepared_stellar_equilibrium; +export import :operators.prepared_central_density_stellar_equilibrium; +export import :equilibrium.stellar_discretization; +export import :operators.stellar_equilibrium_problem; +export import :seed.stellar_equilibrium_projection; +export import :operators.stellar_equilibrium_system; diff --git a/libmeanfield/interface/models/compiled_fixed_central_density.cppm b/libmeanfield/interface/models/compiled_fixed_central_density.cppm new file mode 100644 index 0000000..bad5d88 --- /dev/null +++ b/libmeanfield/interface/models/compiled_fixed_central_density.cppm @@ -0,0 +1,95 @@ +module; + +#include +#include + +export module mean_field:model.compiled_fixed_central_density; + +export import :eos.polytrope; +export import :field.registry; +export import :model.compiled_fixed_mass; +export import :utils.blocks; + +export namespace mean_field::models { + struct CentralDensityLayoutRequest final { + using SpecificationType = FixedCentralDensity; + using GeneratedValueType = BorderFor; + using GeneratedResidualType = ResidualFor; + using ValueBlockType = utils::blocks::fixed_central_density::central_value::value; + using ResidualBlockType = utils::blocks::fixed_central_density::central_value::residual; + using TermType = utils::blocks::fixed_central_density::central_value; + using StateValueBlockTypes = ModelTypeList; + + static constexpr ConstraintRowInjection rowInjection = ConstraintRowInjection::solver_border; + static constexpr std::size_t valueArity = GeneratedValueType::scalarArity; + static constexpr std::size_t residualArity = GeneratedResidualType::scalarArity; + + template [[nodiscard]] static consteval auto valueBlock() { + return utils::blocks::get_value_block(TermType{}); + } + + template [[nodiscard]] static consteval auto residualBlock() { + return utils::blocks::get_residual_block(TermType{}); + } + }; + + class CompiledFixedCentralDensity final { + public: + using SpecificationType = FixedCentralDensity; + using LayoutRequest = CentralDensityLayoutRequest; + using BorderType = typename LayoutRequest::GeneratedValueType; + using ResidualType = typename LayoutRequest::GeneratedResidualType; + using CarrierField = field::Enthalpy; + using BorderField = field::CentralDensityBorder; + + CompiledFixedCentralDensity( + const FixedCentralDensity specification, + const eos::Polytrope &equationOfState + ) + : m_specification(specification), + m_equationOfState(equationOfState), + m_targetEnthalpy( + eos::evaluate( + m_equationOfState, + m_specification.targetDensity() + ) + ) { + } + + [[nodiscard]] const FixedCentralDensity &specification() const noexcept { + return m_specification; + } + + [[nodiscard]] dimensions::DensityValue targetDensity() const noexcept { + return m_specification.targetDensity(); + } + + [[nodiscard]] dimensions::SpecificEnthalpyValue targetEnthalpy() const noexcept { + return m_targetEnthalpy; + } + + [[nodiscard]] dimensions::DensityValue + densityFromEnthalpy(const dimensions::SpecificEnthalpyValue enthalpy) const { + return eos::evaluate(m_equationOfState, enthalpy); + } + + [[nodiscard]] static consteval LayoutRequest layoutRequest() noexcept { + return {}; + } + + private: + FixedCentralDensity m_specification; + eos::Polytrope m_equationOfState; + dimensions::SpecificEnthalpyValue m_targetEnthalpy; + }; + + [[nodiscard]] inline CompiledFixedCentralDensity compileConstraint( + const FixedCentralDensity specification, + const eos::Polytrope &equationOfState + ) { + return {specification, equationOfState}; + } + + static_assert(ConstraintLayoutRequestType); + static_assert(CompiledConstraint); +} // namespace mean_field::models diff --git a/libmeanfield/interface/models/compiled_fixed_mass.cppm b/libmeanfield/interface/models/compiled_fixed_mass.cppm new file mode 100644 index 0000000..59b37af --- /dev/null +++ b/libmeanfield/interface/models/compiled_fixed_mass.cppm @@ -0,0 +1,115 @@ +module; + +#include +#include + +export module mean_field:model.compiled_fixed_mass; + +export import :field.registry; +export import :model.specifications; +export import :utils.blocks; + +export namespace mean_field::models { + enum class ConstraintRowInjection { append, solver_border }; + + template < + ModelSpecification Specification, + typename GeneratedValue, + typename GeneratedResidual, + typename ValueBlock, + typename ResidualBlock, + typename Term, + typename... StateValueBlocks> + struct ConstraintLayoutRequest final { + using SpecificationType = Specification; + using GeneratedValueType = GeneratedValue; + using GeneratedResidualType = GeneratedResidual; + using ValueBlockType = ValueBlock; + using ResidualBlockType = ResidualBlock; + using TermType = Term; + using StateValueBlockTypes = ModelTypeList; + + static constexpr ConstraintRowInjection rowInjection = ConstraintRowInjection::append; + static constexpr std::size_t valueArity = GeneratedValue::scalarArity; + static constexpr std::size_t residualArity = GeneratedResidual::scalarArity; + + template [[nodiscard]] static consteval auto valueBlock() { + return utils::blocks::get_value_block(Term{}); + } + + template [[nodiscard]] static consteval auto residualBlock() { + return utils::blocks::get_residual_block(Term{}); + } + }; + + template + concept ConstraintLayoutRequestType = requires { + typename std::remove_cvref_t::SpecificationType; + typename std::remove_cvref_t::GeneratedValueType; + typename std::remove_cvref_t::GeneratedResidualType; + typename std::remove_cvref_t::ValueBlockType; + typename std::remove_cvref_t::ResidualBlockType; + typename std::remove_cvref_t::StateValueBlockTypes; + requires ModelSpecification::SpecificationType>; + requires std::remove_cvref_t::valueArity == std::remove_cvref_t::residualArity; + }; + + using FixedMassLayoutRequest = ConstraintLayoutRequest< + FixedTotalMass, + MultiplierFor, + ResidualFor, + utils::blocks::fixed_total_mass::mass_normalization::value, + utils::blocks::fixed_total_mass::mass_normalization::residual, + utils::blocks::fixed_total_mass::mass_normalization, + utils::blocks::density::mass::value, + utils::blocks::displacement::geometry::value>; + + class CompiledFixedMass final { + public: + using SpecificationType = FixedTotalMass; + using LayoutRequest = FixedMassLayoutRequest; + using MultiplierType = typename LayoutRequest::GeneratedValueType; + using ResidualType = typename LayoutRequest::GeneratedResidualType; + + // In the current barotropic formulation, the multiplier generated by + // FixedTotalMass is realized by the historical scalar C field. + using MultiplierField = field::BarotropicConstant; + + explicit CompiledFixedMass(const FixedTotalMass specification) noexcept : m_specification(specification) { + } + + [[nodiscard]] const FixedTotalMass &specification() const noexcept { + return m_specification; + } + + [[nodiscard]] dimensions::MassValue targetMass() const noexcept { + return m_specification.targetMass(); + } + + [[nodiscard]] static consteval LayoutRequest layoutRequest() noexcept { + return {}; + } + + private: + FixedTotalMass m_specification; + }; + + template + concept CompiledConstraint = requires(const std::remove_cvref_t &constraint) { + typename std::remove_cvref_t::SpecificationType; + typename std::remove_cvref_t::LayoutRequest; + requires ModelSpecification::SpecificationType>; + requires ConstraintLayoutRequestType::LayoutRequest>; + { + constraint.specification() + } noexcept -> std::same_as::SpecificationType &>; + { constraint.layoutRequest() } noexcept -> std::same_as::LayoutRequest>; + }; + + [[nodiscard]] inline CompiledFixedMass compileConstraint(const FixedTotalMass specification) noexcept { + return CompiledFixedMass{specification}; + } + + static_assert(ConstraintLayoutRequestType); + static_assert(CompiledConstraint); +} // namespace mean_field::models diff --git a/libmeanfield/interface/models/specifications.cppm b/libmeanfield/interface/models/specifications.cppm new file mode 100644 index 0000000..541fc7d --- /dev/null +++ b/libmeanfield/interface/models/specifications.cppm @@ -0,0 +1,481 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +export module mean_field:model.specifications; + +export import :eos.polytrope; +export import :surface.constant; + +export namespace mean_field::models { + enum class SpecificationRole { + constitutive_law, + boundary_condition, + invariant, + phase_condition, + gauge_choice, + rotation_law + }; + + struct SpecificationKey final { + SpecificationRole role; + std::size_t ordinal; + + constexpr auto operator<=>(const SpecificationKey &) const = default; + }; + + struct SpecificationDescriptor final { + std::string_view name; + SpecificationRole role; + SpecificationKey key; + std::size_t generatedValueArity; + std::size_t generatedResidualArity; + + constexpr bool operator==(const SpecificationDescriptor &) const = default; + }; + + enum class EquilibriumSystemCompilation { + complete_equilibrium_system, + equation_contributions_only, + + // Transitional spellings retained while internal solver code is + // migrated to physics-facing equilibrium-system terminology. + isolated_root = complete_equilibrium_system, + assembly_only = equation_contributions_only + }; + + using ModelCompilationClass = EquilibriumSystemCompilation; + + struct RuntimeSpecificationDescriptor final { + SpecificationDescriptor specification; + std::size_t canonicalIndex; + bool hasRootCompiler; + + constexpr bool operator==(const RuntimeSpecificationDescriptor &) const = default; + }; + + template struct SpecificationTraits; + + template + concept ModelSpecification = requires { + typename std::remove_cvref_t::Parameters; + { SpecificationTraits>::name } -> std::convertible_to; + { SpecificationTraits>::role } -> std::convertible_to; + { SpecificationTraits>::key } -> std::convertible_to; + } && std::constructible_from, typename std::remove_cvref_t::Parameters>; + + class FixedTotalMass final { + public: + struct Parameters final { + dimensions::MassValue Mtotal; + }; + + using TargetValue = dimensions::MassValue; + + explicit FixedTotalMass(const Parameters parameters) : FixedTotalMass(parameters.Mtotal) { + } + + explicit FixedTotalMass(const TargetValue targetMass) : m_targetMass(targetMass) { + if (!std::isfinite(targetMass.value()) || targetMass.value() <= 0.0) { + throw std::invalid_argument( + std::format( + "The fixed total mass must be finite and positive. Instead M = {} was provided.", + targetMass.value() + ) + ); + } + } + + [[nodiscard]] TargetValue targetMass() const noexcept { + return m_targetMass; + } + + private: + TargetValue m_targetMass; + }; + + class FixedCentralDensity final { + public: + struct Parameters final { + dimensions::DensityValue RhoC; + }; + + using TargetValue = dimensions::DensityValue; + + explicit FixedCentralDensity(const Parameters parameters) : FixedCentralDensity(parameters.RhoC) { + } + + explicit FixedCentralDensity(const TargetValue targetDensity) : m_targetDensity(targetDensity) { + if (!std::isfinite(targetDensity.value()) || targetDensity.value() <= 0.0) { + throw std::invalid_argument( + std::format( + "The fixed central density must be finite and positive. Instead rho_c = {} was provided.", + targetDensity.value() + ) + ); + } + } + + [[nodiscard]] TargetValue targetDensity() const noexcept { + return m_targetDensity; + } + + private: + TargetValue m_targetDensity; + }; + + template <> struct SpecificationTraits { + static constexpr std::string_view name = "Polytrope"; + static constexpr SpecificationRole role = SpecificationRole::constitutive_law; + static constexpr SpecificationKey key{role, 0}; + }; + + template <> struct SpecificationTraits { + static constexpr std::string_view name = "IsobaricSurface"; + static constexpr SpecificationRole role = SpecificationRole::boundary_condition; + static constexpr SpecificationKey key{role, 0}; + }; + + template <> struct SpecificationTraits { + static constexpr std::string_view name = "FixedTotalMass"; + static constexpr SpecificationRole role = SpecificationRole::invariant; + static constexpr SpecificationKey key{role, 0}; + }; + + template <> struct SpecificationTraits { + static constexpr std::string_view name = "FixedCentralDensity"; + static constexpr SpecificationRole role = SpecificationRole::phase_condition; + static constexpr SpecificationKey key{role, 0}; + }; + + template struct ModelTypeList final { + static constexpr std::size_t size = sizeof...(Types); + }; + + template struct ModelTypeListContains; + + template + struct ModelTypeListContains> + : std::bool_constant<(std::same_as || ...)> { }; + + template + inline constexpr bool modelTypeListContains = ModelTypeListContains::value; + + template struct ResidualFor final { + using SpecificationType = Specification; + + static constexpr std::size_t scalarArity = 1; + }; + + template struct MultiplierFor final { + using SpecificationType = Specification; + + static constexpr std::size_t scalarArity = 1; + }; + + template struct BorderFor final { + using SpecificationType = Specification; + + static constexpr std::size_t scalarArity = 1; + }; + + template struct SpecificationContribution { + using GeneratedValues = ModelTypeList<>; + using GeneratedResiduals = ModelTypeList<>; + + static constexpr bool isDefined = false; + static constexpr bool hasRootCompiler = false; + }; + + template <> struct SpecificationContribution { + using GeneratedValues = ModelTypeList<>; + using GeneratedResiduals = ModelTypeList<>; + + static constexpr bool isDefined = true; + static constexpr bool hasRootCompiler = true; + }; + + template <> struct SpecificationContribution { + using GeneratedValues = ModelTypeList<>; + using GeneratedResiduals = ModelTypeList<>; + + static constexpr bool isDefined = true; + static constexpr bool hasRootCompiler = true; + }; + + template <> struct SpecificationContribution { + using GeneratedValues = ModelTypeList>; + using GeneratedResiduals = ModelTypeList>; + + static constexpr bool isDefined = true; + static constexpr bool hasRootCompiler = true; + }; + + template <> struct SpecificationContribution { + using GeneratedValues = ModelTypeList>; + using GeneratedResiduals = ModelTypeList>; + + static constexpr bool isDefined = true; + static constexpr bool hasRootCompiler = true; + }; + + template + concept ResolvedModelSpecification = + ModelSpecification && SpecificationContribution>::isDefined; + + namespace detail { + template struct SpecificationSetStorage final { + static constexpr std::size_t size = sizeof...(Specifications); + }; + + template struct ConcatenateModelTypeLists; + + template <> struct ConcatenateModelTypeLists<> { + using Type = ModelTypeList<>; + }; + + template struct ConcatenateModelTypeLists> { + using Type = ModelTypeList; + }; + + template + struct ConcatenateModelTypeLists, ModelTypeList, Remaining...> { + using Type = typename ConcatenateModelTypeLists, Remaining...>::Type; + }; + + template struct InsertSpecification; + + template + struct InsertSpecification> { + using Type = SpecificationSetStorage; + }; + + template + struct InsertSpecification> { + private: + using InsertedTail = typename InsertSpecification>::Type; + + template struct PrependSpecification; + + template + struct PrependSpecification> { + using Type = SpecificationSetStorage; + }; + + public: + using Type = std::conditional_t< + (SpecificationTraits::key < SpecificationTraits::key), + SpecificationSetStorage, + typename PrependSpecification::Type>; + }; + + template struct CanonicalizeSpecifications; + + template struct CanonicalizeSpecifications { + using Type = Set; + }; + + template + struct CanonicalizeSpecifications { + using Inserted = typename InsertSpecification::Type; + using Type = typename CanonicalizeSpecifications::Type; + }; + + template + using CanonicalSpecificationSet = + typename CanonicalizeSpecifications, Specifications...>::Type; + + template < + ModelSpecification Head, + ModelSpecification... Tail> + consteval bool specificationKeyIsUnique() { + return ((SpecificationTraits::key != SpecificationTraits::key) && ...); + } + + template struct SpecificationKeysAreUnique; + + template <> struct SpecificationKeysAreUnique<> : std::true_type { }; + + template + struct SpecificationKeysAreUnique + : std::bool_constant< + specificationKeyIsUnique() && SpecificationKeysAreUnique::value> { }; + + template + inline constexpr std::size_t specificationRoleCount = + (std::size_t{0} + ... + (SpecificationTraits::role == Role ? 1 : 0)); + + template struct ModelTypeListScalarArity; + + template + struct ModelTypeListScalarArity> + : std::integral_constant { }; + + template + inline constexpr bool isOneOf = (std::same_as || ...); + + template + inline constexpr std::size_t typeCount = + (std::size_t{0} + ... + + (std::same_as> ? std::size_t{1} : std::size_t{0})); + + template struct ArgumentsMatchCanonicalSpecifications; + + template + struct ArgumentsMatchCanonicalSpecifications, Arguments...> + : std::bool_constant< + sizeof...(CanonicalSpecifications) == sizeof...(Arguments) && + (isOneOf, CanonicalSpecifications...> && ...) && + ((typeCount == 1) && ...)> { }; + } // namespace detail + + template + inline constexpr bool specificationKeysAreUnique = detail::SpecificationKeysAreUnique::value; + + template + concept ValidModelSpecificationPack = + (ResolvedModelSpecification && ...) && specificationKeysAreUnique && + detail::specificationRoleCount == 1; + + template + requires specificationKeysAreUnique + using SpecificationSet = detail::CanonicalSpecificationSet; + + template struct SpecificationOperatorSignature; + + template + struct SpecificationOperatorSignature> final { + using GeneratedValues = typename detail::ConcatenateModelTypeLists< + typename SpecificationContribution::GeneratedValues...>::Type; + + using GeneratedResiduals = typename detail::ConcatenateModelTypeLists< + typename SpecificationContribution::GeneratedResiduals...>::Type; + + static constexpr std::size_t generatedValueArity = detail::ModelTypeListScalarArity::value; + + static constexpr std::size_t generatedResidualArity = + detail::ModelTypeListScalarArity::value; + + static constexpr bool symbolicallySquare = generatedValueArity == generatedResidualArity; + }; + + template + [[nodiscard]] consteval SpecificationDescriptor specificationDescriptor() { + using Contribution = SpecificationContribution; + + return { + .name = SpecificationTraits::name, + .role = SpecificationTraits::role, + .key = SpecificationTraits::key, + .generatedValueArity = detail::ModelTypeListScalarArity::value, + .generatedResidualArity = detail::ModelTypeListScalarArity::value + }; + } + + namespace detail { + template class SpecifiedModel; + + template + class SpecifiedModel> final { + public: + using SpecificationTypes = SpecificationSetStorage; + using OperatorSignature = SpecificationOperatorSignature; + + static constexpr bool symbolicallySquare = OperatorSignature::symbolicallySquare; + static constexpr bool hasCompleteRootCompiler = + (SpecificationContribution::hasRootCompiler && ...); + static constexpr EquilibriumSystemCompilation compilationClass = + symbolicallySquare && hasCompleteRootCompiler + ? EquilibriumSystemCompilation::complete_equilibrium_system + : EquilibriumSystemCompilation::equation_contributions_only; + + template + requires ArgumentsMatchCanonicalSpecifications< + SpecificationTypes, + Arguments...>::value + explicit SpecifiedModel(Arguments &&...arguments) + : m_specifications( + std::get( + std::tuple...>{std::forward(arguments)...} + )... + ) { + } + + template + requires isOneOf< + Specification, + Specifications...> + [[nodiscard]] const Specification &specification() const noexcept { + return std::get(m_specifications); + } + + template + static constexpr bool containsSpecification = isOneOf; + + [[nodiscard]] static constexpr std::span + runtimeSpecificationDescriptors() noexcept { + return runtimeDescriptors; + } + + private: + inline static constexpr std::array + runtimeDescriptors = [] { + std::array descriptors{}; + std::size_t index = 0; + ((descriptors[index] = + {.specification = specificationDescriptor(), + .canonicalIndex = index, + .hasRootCompiler = SpecificationContribution::hasRootCompiler}, + ++index), + ...); + return descriptors; + }(); + + std::tuple m_specifications; + }; + } // namespace detail + + template + requires ValidModelSpecificationPack && + SpecificationOperatorSignature>::symbolicallySquare + using Model = detail::SpecifiedModel>; + + template + concept SpecifiedModelType = requires { + typename std::remove_cvref_t::SpecificationTypes; + typename std::remove_cvref_t::OperatorSignature; + requires std::remove_cvref_t::symbolicallySquare; + { std::remove_cvref_t::compilationClass } -> std::convertible_to; + { + std::remove_cvref_t::runtimeSpecificationDescriptors() + } -> std::same_as>; + }; + + static_assert(ModelSpecification); + static_assert(ModelSpecification); + static_assert(ModelSpecification); + static_assert(ModelSpecification); + static_assert(ResolvedModelSpecification); + static_assert(ResolvedModelSpecification); + static_assert(ResolvedModelSpecification); + static_assert(ResolvedModelSpecification); +} // namespace mean_field::models + +export namespace mean_field::integral { + using FixedTotalMass = models::FixedTotalMass; +} + +export namespace mean_field::constraint { + using FixedCentralDensity = models::FixedCentralDensity; +} diff --git a/libmeanfield/interface/models/structure/polytropic.cppm b/libmeanfield/interface/models/structure/polytropic.cppm index 812f6c9..97bd805 100644 --- a/libmeanfield/interface/models/structure/polytropic.cppm +++ b/libmeanfield/interface/models/structure/polytropic.cppm @@ -1,13 +1,12 @@ module; -#include - #include export module mean_field:model.structure.polytropic; export import :eos.polytrope; export import :model.structure.base; +export import :seed.lane_emden; import :utils.misc; @@ -28,40 +27,6 @@ export namespace mean_field::models::structure { void validate() const; private: - struct LaneEmdenPoint { - double coordinate{0.0}; - double value{0.0}; - double derivative{0.0}; - }; - - struct LaneEmdenDerivative { - double value{0.0}; - double derivative{0.0}; - }; - - static void validateSeedRequest(const StructureSeedRequest &request); - - [[nodiscard]] static LaneEmdenDerivative evaluateLaneEmdenRhs( - double coordinate, - double value, - double derivative, - double polytropicIndex - ); - - [[nodiscard]] static LaneEmdenPoint takeLaneEmdenStep( - const LaneEmdenPoint &point, - double step, - double polytropicIndex - ); - - [[nodiscard]] static std::vector solveLaneEmden(double polytropicIndex); - - [[nodiscard]] static double interpolateLaneEmdenValue( - const std::vector &solution, - double coordinate, - std::size_t &lowerIndex - ); - eos::Polytrope m_equationOfState; double m_targetMass; }; diff --git a/libmeanfield/interface/models/typed_stellar_model.cppm b/libmeanfield/interface/models/typed_stellar_model.cppm new file mode 100644 index 0000000..a7cfc1f --- /dev/null +++ b/libmeanfield/interface/models/typed_stellar_model.cppm @@ -0,0 +1,66 @@ +module; + +#include +#include +#include +#include +#include + +export module mean_field:model.typed_stellar; + +export import :model.specifications; + +export namespace mean_field::model { + template class StellarModel; + + template + class StellarModel> final { + public: + using SpecificationTypes = models::detail::SpecificationSetStorage; + using OperatorSignature = models::SpecificationOperatorSignature; + using Storage = models::Model; + + static constexpr std::size_t specificationCount = sizeof...(CanonicalSpecifications); + static constexpr bool symbolicallySquare = Storage::symbolicallySquare; + static constexpr bool hasCompleteEquilibriumCompiler = Storage::hasCompleteRootCompiler; + static constexpr models::EquilibriumSystemCompilation compilationClass = Storage::compilationClass; + + template + requires std::constructible_from< + Storage, + Arguments...> + explicit StellarModel(Arguments &&...arguments) : m_specifications(std::forward(arguments)...) { + } + + template + requires Storage::template + containsSpecification [[nodiscard]] const Specification &specification() const noexcept { + return m_specifications.template specification(); + } + + template + static constexpr bool containsSpecification = Storage::template containsSpecification; + + [[nodiscard]] static constexpr std::span + runtimeSpecificationDescriptors() noexcept { + return Storage::runtimeSpecificationDescriptors(); + } + + private: + Storage m_specifications; + }; + + template + requires models::ValidModelSpecificationPack...> + StellarModel(Specifications &&...) + -> StellarModel...>>; + + namespace detail { + template struct IsStellarModel : std::false_type { }; + + template struct IsStellarModel> : std::true_type { }; + } // namespace detail + + template + concept StellarModelType = detail::IsStellarModel>::value; +} // namespace mean_field::model diff --git a/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm b/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm index 08c8dd6..e71ce22 100644 --- a/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm +++ b/libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm @@ -84,15 +84,24 @@ export namespace mean_field::operators { mfem::Vector &actionTrue ) const; + void ApplyDisplacementActionFull( + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionTrue + ) const; + struct ElementPAData { + int elementId{-1}; mfem::Array densityDofs; mfem::Array enthalpyDofs; + mfem::Array displacementDofs; mfem::DofTransformation *densityDofTransformation{nullptr}; mfem::DofTransformation *enthalpyDofTransformation{nullptr}; + mfem::DofTransformation *displacementDofTransformation{nullptr}; mfem::DenseMatrix densityBasis; mfem::DenseMatrix enthalpyBasis; + mfem::DenseMatrix inverseElementJacobians; mfem::Vector weightedResidual; mfem::Vector quadratureWeights; @@ -122,6 +131,14 @@ export namespace mean_field::operators { mutable mfem::Vector m_fullDisplacementAction; mutable mfem::Vector m_fullResidual; + mutable mfem::Vector m_displacementVariationLocal; + mutable mfem::Vector m_localDisplacementAction; + mutable mfem::Vector m_elementDisplacementVariation; + mutable mfem::Vector m_quadratureDisplacementAction; + mutable mfem::Vector m_elementDisplacementAction; + mutable mfem::DenseMatrix m_referenceDShape; + mutable mfem::DenseMatrix m_referenceDisplacementJacobian; + std::uint64_t m_preparationCount{0}; bool m_isPrepared{false}; }; diff --git a/libmeanfield/interface/operators/prepared_central_density.cppm b/libmeanfield/interface/operators/prepared_central_density.cppm new file mode 100644 index 0000000..67547bd --- /dev/null +++ b/libmeanfield/interface/operators/prepared_central_density.cppm @@ -0,0 +1,264 @@ +module; + +#include +#include +#include +#include +#include +#include + +#include + +export module mean_field:operators.prepared_central_density; + +export import :field.mfem; +export import :model.compiled_fixed_central_density; + +export namespace mean_field::operators { + struct CentralDensityDependencyStamp final { + std::uint64_t identity{0}; + std::uint64_t revision{0}; + + constexpr auto operator<=>(const CentralDensityDependencyStamp &) const = default; + }; + + struct CentralDensityDependencies final { + CentralDensityDependencyStamp enthalpy; + + constexpr auto operator<=>(const CentralDensityDependencies &) const = default; + }; + + struct PreparedCentralDensityReport final { + bool refreshedCentralEnthalpy{false}; + bool refreshedBorder{false}; + bool assembledResidual{false}; + + [[nodiscard]] bool DidAnyWork() const noexcept { + return refreshedCentralEnthalpy || refreshedBorder || assembledResidual; + } + + constexpr auto operator<=>(const PreparedCentralDensityReport &) const = default; + }; + + struct CentralDensityConstraintReport final { + double targetDensity; + double achievedDensity; + double targetEnthalpy; + double achievedEnthalpy; + double enthalpyResidual; + double scaledResidual; + }; + + struct CentralDensityJacobianInput final { + const mfem::Vector &enthalpyVariation; + double borderVariation; + }; + + struct CentralDensityJacobianOutput final { + mfem::Vector &enthalpyAction; + mfem::Vector &phaseAction; + }; + + struct CentralDensityJacobianTransposeInput final { + const mfem::Vector &enthalpyResidualDual; + double phaseResidualDual; + }; + + struct CentralDensityJacobianTransposeOutput final { + mfem::Vector &enthalpyDual; + mfem::Vector &borderDual; + }; + + /* + * Bordered central-density phase condition + * + * R_c(h) = h(0) - h(rho_c,target), + * R_h <- R_h + lambda_c e_c. + * + * The point functional e_c selects the unique scalar H1 vertex at the + * computational origin. Its coordinate transpose supplies the border + * column, so this contribution is algebraically symmetric before any + * independent scaling is applied by a solver. + */ + class PreparedCentralDensityConstraint final { + public: + PreparedCentralDensityConstraint( + field::FieldPointDofMap centerDof, + const MPI_Comm communicator + ) + : m_centerDof(std::move(centerDof)), + m_communicator(communicator) { + } + + PreparedCentralDensityReport Prepare( + const models::CompiledFixedCentralDensity &constraint, + const mfem::Vector &enthalpy, + const double border, + const CentralDensityDependencies &dependencies + ) { + MFEM_VERIFY( + enthalpy.Size() == m_centerDof.field_size(), + "The central-density phase received an enthalpy vector with the wrong size." + ); + MFEM_VERIFY(std::isfinite(border), "The central-density phase received a non-finite border value."); + + const bool wasPrepared = m_isPrepared; + PreparedCentralDensityReport report; + + if (!wasPrepared || dependencies.enthalpy != m_preparedDependencies.enthalpy) { + double localCentralEnthalpy = 0.0; + for (const int reducedDof : m_centerDof.reduced_dofs()) { + const double value = enthalpy(reducedDof); + MFEM_VERIFY(std::isfinite(value), "The central enthalpy is non-finite."); + localCentralEnthalpy += value; + } + m_centralEnthalpy = GlobalSum(localCentralEnthalpy); + report.refreshedCentralEnthalpy = true; + } + + if (!wasPrepared || border != m_border) { + m_border = border; + report.refreshedBorder = true; + } + + const bool targetChanged = + !m_constraint.has_value() || constraint.targetDensity() != m_constraint->targetDensity(); + if (targetChanged) { + m_constraint = constraint; + } + + if (report.refreshedCentralEnthalpy || report.refreshedBorder || targetChanged) { + m_cachedPhaseResidual = m_centralEnthalpy - m_constraint->targetEnthalpy().value(); + report.assembledResidual = true; + } + + m_preparedDependencies = dependencies; + m_isPrepared = true; + ++m_preparationCount; + return report; + } + + void AddResidual( + mfem::Vector &enthalpyResidual, + mfem::Vector &phaseResidual + ) const { + VerifyPrepared(); + VerifyOutputSizes(enthalpyResidual, phaseResidual); + for (const int reducedDof : m_centerDof.reduced_dofs()) { + enthalpyResidual(reducedDof) += m_border; + } + phaseResidual(0) = m_cachedPhaseResidual; + } + + void ApplyJacobian( + const CentralDensityJacobianInput &input, + CentralDensityJacobianOutput output + ) const { + VerifyPrepared(); + MFEM_VERIFY( + input.enthalpyVariation.Size() == m_centerDof.field_size(), + "The central-density Jacobian received an enthalpy direction with the wrong size." + ); + VerifyOutputSizes(output.enthalpyAction, output.phaseAction); + + double localPhaseAction = 0.0; + for (const int reducedDof : m_centerDof.reduced_dofs()) { + output.enthalpyAction(reducedDof) += input.borderVariation; + localPhaseAction += input.enthalpyVariation(reducedDof); + } + output.phaseAction(0) = GlobalSum(localPhaseAction); + ++m_jacobianApplicationCount; + } + + void ApplyJacobianTranspose( + const CentralDensityJacobianTransposeInput &input, + CentralDensityJacobianTransposeOutput output + ) const { + VerifyPrepared(); + MFEM_VERIFY( + input.enthalpyResidualDual.Size() == m_centerDof.field_size(), + "The central-density transpose received an enthalpy residual dual with the wrong size." + ); + MFEM_VERIFY( + output.enthalpyDual.Size() == m_centerDof.field_size() && output.borderDual.Size() == 1, + "The central-density transpose received output vectors with the wrong size." + ); + + double localBorderDual = 0.0; + for (const int reducedDof : m_centerDof.reduced_dofs()) { + output.enthalpyDual(reducedDof) += input.phaseResidualDual; + localBorderDual += input.enthalpyResidualDual(reducedDof); + } + output.borderDual(0) += GlobalSum(localBorderDual); + ++m_transposeApplicationCount; + } + + [[nodiscard]] CentralDensityConstraintReport GetConstraintReport() const { + VerifyPrepared(); + const double targetEnthalpy = m_constraint->targetEnthalpy().value(); + const double scale = std::max(std::abs(targetEnthalpy), 1.0e-300); + return { + .targetDensity = m_constraint->targetDensity().value(), + .achievedDensity = + m_constraint->densityFromEnthalpy(dimensions::SpecificEnthalpyValue{m_centralEnthalpy}).value(), + .targetEnthalpy = targetEnthalpy, + .achievedEnthalpy = m_centralEnthalpy, + .enthalpyResidual = m_cachedPhaseResidual, + .scaledResidual = m_cachedPhaseResidual / scale + }; + } + + [[nodiscard]] bool IsPrepared() const noexcept { + return m_isPrepared; + } + + [[nodiscard]] const field::FieldPointDofMap &GetCenterDof() const noexcept { + return m_centerDof; + } + + [[nodiscard]] std::uint64_t GetPreparationCount() const noexcept { + return m_preparationCount; + } + + [[nodiscard]] std::uint64_t GetJacobianApplicationCount() const noexcept { + return m_jacobianApplicationCount; + } + + [[nodiscard]] std::uint64_t GetTransposeApplicationCount() const noexcept { + return m_transposeApplicationCount; + } + + private: + [[nodiscard]] double GlobalSum(const double localValue) const { + double globalValue = 0.0; + MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_communicator); + return globalValue; + } + + void VerifyOutputSizes( + const mfem::Vector &enthalpyOutput, + const mfem::Vector &phaseOutput + ) const { + MFEM_VERIFY( + enthalpyOutput.Size() == m_centerDof.field_size() && phaseOutput.Size() == 1, + "The central-density phase received output vectors with the wrong size." + ); + } + + void VerifyPrepared() const { + MFEM_VERIFY(m_isPrepared, "The central-density phase must be prepared before application."); + } + + field::FieldPointDofMap m_centerDof; + MPI_Comm m_communicator; + std::optional m_constraint; + CentralDensityDependencies m_preparedDependencies; + double m_centralEnthalpy{0.0}; + double m_border{0.0}; + double m_cachedPhaseResidual{0.0}; + std::uint64_t m_preparationCount{0}; + mutable std::uint64_t m_jacobianApplicationCount{0}; + mutable std::uint64_t m_transposeApplicationCount{0}; + bool m_isPrepared{false}; + }; +} // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm new file mode 100644 index 0000000..b189395 --- /dev/null +++ b/libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm @@ -0,0 +1,117 @@ +module; + +#include +#include +#include +#include + +#include + +export module mean_field:operators.prepared_central_density_stellar_equilibrium; + +export import :model.compiled_fixed_central_density; +export import :operators.prepared_central_density; +export import :operators.prepared_stellar_equilibrium; + +export namespace mean_field::operators { + using CentralDensityStellarEquilibriumSpecificationModel = model::StellarModel< + models:: + SpecificationSet>; + + using CentralDensityStellarEquilibriumForm = utils::blocks::central_density_bordered_stellar_equilibrium_form; + using CentralDensityStellarEquilibriumJacobianForm = + utils::blocks::central_density_bordered_stellar_equilibrium_jacobian_form; + using CentralDensityStellarEquilibriumLayout = utils::blocks::form_layout; + using CentralDensityStellarEquilibriumSystemManifest = EquilibriumSystemManifest< + CentralDensityStellarEquilibriumSpecificationModel, + CentralDensityStellarEquilibriumForm, + CentralDensityStellarEquilibriumJacobianForm>; + + using CentralDensityStellarEquilibriumRootManifest = CentralDensityStellarEquilibriumSystemManifest; + + struct PreparedCentralDensityStellarEquilibriumReport final { + PreparedStellarEquilibriumReport physical; + PreparedCentralDensityReport phase; + bool assembledResidual{false}; + + [[nodiscard]] bool DidAnyWork() const noexcept { + return physical.DidAnyWork() || phase.DidAnyWork() || assembledResidual; + } + }; + + class PreparedCentralDensityStellarEquilibriumOperator final : public mfem::Operator { + public: + PreparedCentralDensityStellarEquilibriumOperator( + fem::FEM &f, + const mapping::DomainMapper &domainMapper, + const eos::Polytrope &equationOfState, + models::CompiledFixedMass fixedMassConstraint, + PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation, + models::CompiledFixedCentralDensity centralDensity + ) + : PreparedCentralDensityStellarEquilibriumOperator( + f, + std::make_unique( + f, + domainMapper, + equationOfState, + std::move(fixedMassConstraint), + surfaceConstraint, + std::move(domainDeformation) + ), + std::move(centralDensity), + MakeCenterDofMap(f) + ) { + } + + PreparedCentralDensityStellarEquilibriumOperator(const PreparedCentralDensityStellarEquilibriumOperator &) = + delete; + PreparedCentralDensityStellarEquilibriumOperator & + operator=(const PreparedCentralDensityStellarEquilibriumOperator &) = delete; + PreparedCentralDensityStellarEquilibriumOperator(PreparedCentralDensityStellarEquilibriumOperator &&) = delete; + PreparedCentralDensityStellarEquilibriumOperator & + operator=(PreparedCentralDensityStellarEquilibriumOperator &&) = delete; + + PreparedCentralDensityStellarEquilibriumReport Prepare( + const mfem::Vector &state, + const StellarEquilibriumDependencies &dependencies, + const physics::RigidRotation &rotation + ); + + void BuildResidual(mfem::Vector &residual) const; + + void Mult( + const mfem::Vector &direction, + mfem::Vector &action + ) const override; + + [[nodiscard]] bool IsPrepared() const noexcept; + [[nodiscard]] const CentralDensityStellarEquilibriumLayout &GetLayout() const noexcept; + [[nodiscard]] const CentralDensityStellarEquilibriumRootManifest &GetRootManifest() const noexcept; + [[nodiscard]] const PreparedStellarEquilibriumOperator &GetPhysicalOperator() const noexcept; + [[nodiscard]] const PreparedCentralDensityConstraint &GetCentralDensityConstraint() const noexcept; + [[nodiscard]] RootConstraintReport GetFixedMassReport() const; + [[nodiscard]] CentralDensityConstraintReport GetCentralDensityReport() const; + + private: + static field::FieldPointDofMap MakeCenterDofMap(const fem::FEM &f); + + PreparedCentralDensityStellarEquilibriumOperator( + fem::FEM &f, + std::unique_ptr physicalOperator, + models::CompiledFixedCentralDensity centralDensity, + field::FieldPointDofMap centerDof + ); + + void AssembleResidual(); + void VerifyPrepared() const; + + std::unique_ptr m_physicalOperator; + models::CompiledFixedCentralDensity m_centralDensity; + PreparedCentralDensityConstraint m_phaseConstraint; + CentralDensityStellarEquilibriumRootManifest m_rootManifest; + mfem::Vector m_cachedResidual; + bool m_isPrepared{false}; + }; +} // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/prepared_constraint.cppm b/libmeanfield/interface/operators/prepared_constraint.cppm new file mode 100644 index 0000000..0e91a3c --- /dev/null +++ b/libmeanfield/interface/operators/prepared_constraint.cppm @@ -0,0 +1,37 @@ +module; + +#include +#include + +#include + +export module mean_field:operators.prepared_constraint; + +export import :model.compiled_fixed_mass; + +export namespace mean_field::operators { + template + concept PreparedConstraint = requires( + std::remove_cvref_t &prepared, + const std::remove_cvref_t &constPrepared, + const typename std::remove_cvref_t::CompiledConstraintType &constraint, + const typename std::remove_cvref_t::Dependencies &dependencies, + const typename std::remove_cvref_t::JacobianInput &jacobianInput, + typename std::remove_cvref_t::JacobianTransposeOutput transposeOutput, + const mfem::Vector &residualDual, + mfem::Vector &result + ) { + typename std::remove_cvref_t::SpecificationType; + typename std::remove_cvref_t::CompiledConstraintType; + typename std::remove_cvref_t::Dependencies; + typename std::remove_cvref_t::Report; + typename std::remove_cvref_t::JacobianInput; + typename std::remove_cvref_t::JacobianTransposeOutput; + requires models::CompiledConstraint::CompiledConstraintType>; + { prepared.Prepare(constraint, dependencies) } -> std::same_as::Report>; + { constPrepared.BuildResidual(result) } -> std::same_as; + { constPrepared.ApplyJacobian(jacobianInput, result) } -> std::same_as; + { constPrepared.ApplyJacobianTranspose(residualDual, transposeOutput) } -> std::same_as; + { constPrepared.IsPrepared() } noexcept -> std::same_as; + }; +} // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm b/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm index 20e92c7..afebff8 100644 --- a/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm +++ b/libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm @@ -2,6 +2,7 @@ module; #include #include +#include #include @@ -105,6 +106,29 @@ export namespace mean_field::operators { private: void VerifyPrepared() const; + void PrepareElementData(); + void ApplyPreparedCompleteJacobianActionTrue( + const mfem::Vector &densityVariationTrue, + const mfem::Vector &displacementVariationTrue, + const mfem::Vector &gravityGradientVariationTrue, + mfem::Vector &actionTrue + ) const; + + struct ElementPAData { + int elementId{-1}; + mfem::Array densityDofs; + mfem::Array gravityGradientDofs; + mfem::Array displacementDofs; + mfem::DofTransformation *densityDofTransformation{nullptr}; + mfem::DofTransformation *gravityGradientDofTransformation{nullptr}; + mfem::DofTransformation *displacementDofTransformation{nullptr}; + const mfem::IntegrationRule *integrationRule{nullptr}; + mfem::DenseMatrix mappingJacobians; + mfem::DenseMatrix inverseMeshJacobians; + mfem::DenseMatrix baseGravityReferenceValues; + mfem::Vector baseDensityValues; + mfem::Vector referenceWeights; + }; const fem::FEM &m_fem; const mapping::DomainMapper &m_domainMapper; @@ -112,11 +136,34 @@ export namespace mean_field::operators { context::gravity_field::GravityFieldRevisions m_preparedRevisions; mfem::Vector m_cachedResidual; + std::vector m_elements; mutable mfem::Vector m_densityVariationTrue; mutable mfem::Vector m_gravityGradientVariationTrue; mutable mfem::Vector m_displacementVariationTrue; mutable mfem::Vector m_actionTrue; + mutable mfem::Vector m_densityVariationLocal; + mutable mfem::Vector m_gravityGradientVariationLocal; + mutable mfem::Vector m_displacementVariationLocal; + mutable mfem::Vector m_localAction; + mutable mfem::Vector m_elementDensityVariation; + mutable mfem::Vector m_elementGravityGradientVariation; + mutable mfem::Vector m_elementDisplacementVariation; + mutable mfem::Vector m_elementAction; + mutable mfem::Vector m_densityShape; + mutable mfem::Vector m_displacementShape; + mutable mfem::Vector m_baseGravityReferenceValue; + mutable mfem::Vector m_gravityVariationReferenceValue; + mutable mfem::Vector m_mappedBaseGravity; + mutable mfem::Vector m_mappedGravityVariation; + mutable mfem::Vector m_mappedGeometryVariation; + mutable mfem::Vector m_forceValue; + mutable mfem::DenseMatrix m_gravityGradientShape; + mutable mfem::DenseMatrix m_referenceDisplacementDShape; + mutable mfem::DenseMatrix m_referenceDisplacementJacobian; + mutable mfem::DenseMatrix m_displacementJacobianVariation; + mutable mfem::DenseMatrix m_mappingJacobian; + mutable mfem::DenseMatrix m_inverseMeshJacobian; std::uint64_t m_residualPreparationCount{0}; mutable std::uint64_t m_residualApplicationCount{0}; diff --git a/libmeanfield/interface/operators/prepared_gravity_source.cppm b/libmeanfield/interface/operators/prepared_gravity_source.cppm index 391a4d6..0b362a8 100644 --- a/libmeanfield/interface/operators/prepared_gravity_source.cppm +++ b/libmeanfield/interface/operators/prepared_gravity_source.cppm @@ -22,6 +22,11 @@ export namespace mean_field::operators { const mfem::Vector &density, mfem::Vector &action ) const override; + void MultDisplacementVariationTrue( + const mfem::Vector &densityTrue, + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionVariationTrue + ) const; [[nodiscard]] bool IsPrepared() const noexcept; [[nodiscard]] std::uint64_t GetPreparationCount() const noexcept; @@ -41,13 +46,18 @@ export namespace mean_field::operators { mfem::Array density_dofs; mfem::Array potential_dofs; + mfem::Array displacement_dofs; mfem::DofTransformation *density_dof_transformation{nullptr}; mfem::DofTransformation *potential_dof_transformation{nullptr}; + mfem::DofTransformation *displacement_dof_transformation{nullptr}; + + const mfem::IntegrationRule *integration_rule{nullptr}; // Rows are quadrature points; columns are element DOFs. mfem::DenseMatrix density_basis; mfem::DenseMatrix potential_basis; + mfem::DenseMatrix inverse_element_jacobians; // Contains quadrature weight, mesh Jacobian, mapped Jacobian, // and 4*pi*G. @@ -67,6 +77,15 @@ export namespace mean_field::operators { mutable mfem::Vector m_density_true; mutable mfem::Vector m_potential_true; mutable mfem::Vector m_action_true; + mutable mfem::Vector m_density_local; + mutable mfem::Vector m_displacement_variation_local; + mutable mfem::Vector m_local_variation_action; + mutable mfem::Vector m_element_density; + mutable mfem::Vector m_element_displacement_variation; + mutable mfem::Vector m_quadrature_variation_action; + mutable mfem::Vector m_element_variation_action; + mutable mfem::DenseMatrix m_reference_displacement_dshape; + mutable mfem::DenseMatrix m_reference_displacement_jacobian; mfem::Vector m_displacement_true; std::uint64_t m_preparation_count{0}; diff --git a/libmeanfield/interface/operators/prepared_hdiv_mass.cppm b/libmeanfield/interface/operators/prepared_hdiv_mass.cppm index fa2fb6b..46d5df7 100644 --- a/libmeanfield/interface/operators/prepared_hdiv_mass.cppm +++ b/libmeanfield/interface/operators/prepared_hdiv_mass.cppm @@ -2,6 +2,7 @@ module; #include #include #include +#include export module mean_field:operators.prepared_hdiv_mass; export import :fem; @@ -21,6 +22,11 @@ export namespace mean_field::operators { const mfem::Vector &gravity_gradient, mfem::Vector &action ) const override; + void MultDisplacementVariationTrue( + const mfem::Vector &gravityGradientTrue, + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionVariationTrue + ) const; void AssembleDiagonal(mfem::Vector &diagonal) const override; void AssembleTrueDiagonal(mfem::Vector &diagonal) const; @@ -31,6 +37,21 @@ export namespace mean_field::operators { [[nodiscard]] const field::FieldDofMap &GetDisplacementMap() const noexcept; private: + struct ElementVariationData { + int elementId{-1}; + mfem::Array gravityGradientDofs; + mfem::Array displacementDofs; + mfem::Array compactificationDofs; + mfem::DofTransformation *gravityGradientDofTransformation{nullptr}; + mfem::DofTransformation *displacementDofTransformation{nullptr}; + mfem::Vector baseDisplacement; + mfem::Vector compactification; + const mfem::IntegrationRule *integrationRule{nullptr}; + mfem::DenseMatrix frozenMappingData; + }; + + void PrepareVariationData(); + const fem::FEM &m_fem; const mapping::DomainMapper &m_domain_mapper; @@ -48,6 +69,21 @@ export namespace mean_field::operators { mutable mfem::Vector m_action_true; mutable mfem::Vector m_domain_action_true; mfem::Vector m_displacement_true; + std::vector m_variationElements; + + mutable mapping::DomainMapper::Workspace m_variationWorkspace; + mutable mapping::VolumeMappingContext m_baseMappingContext; + mutable mapping::VolumeMappingVariation m_mappingVariation; + mutable mfem::Vector m_gravityGradientLocal; + mutable mfem::Vector m_displacementVariationLocal; + mutable mfem::Vector m_localVariationAction; + mutable mfem::Vector m_elementGravityGradient; + mutable mfem::Vector m_elementDisplacementVariation; + mutable mfem::Vector m_elementVariationAction; + mutable mfem::Vector m_gravityGradientValue; + mutable mfem::Vector m_massTensorVariationAction; + mutable mfem::DenseMatrix m_gravityGradientShape; + mutable mfem::DenseMatrix m_massTensorVariation; std::uint64_t m_preparation_count{0}; bool m_is_prepared{false}; }; diff --git a/libmeanfield/interface/operators/prepared_mass_normalization.cppm b/libmeanfield/interface/operators/prepared_mass_normalization.cppm index 4d10afe..c18ac15 100644 --- a/libmeanfield/interface/operators/prepared_mass_normalization.cppm +++ b/libmeanfield/interface/operators/prepared_mass_normalization.cppm @@ -9,7 +9,9 @@ export module mean_field:operators.prepared_mass_normalization; export import :fem; export import :mapping.domain_mapper; +export import :model.compiled_fixed_mass; export import :operators.context.gravity_field; +export import :operators.prepared_constraint; export import :utils.blocks; export namespace mean_field::operators { @@ -33,6 +35,16 @@ export namespace mean_field::operators { double targetMass{0.0}; }; + struct FixedMassJacobianInput final { + const mfem::Vector &densityVariation; + const mfem::Vector &displacementVariation; + }; + + struct FixedMassJacobianTransposeOutput final { + mfem::Vector &densityDual; + mfem::Vector &displacementDual; + }; + struct PreparedMassNormalizationReport final { bool rebuiltStaticPlan{false}; bool refreshedGeometry{false}; @@ -43,12 +55,15 @@ export namespace mean_field::operators { [[nodiscard]] bool DidAnyWork() const noexcept { return rebuiltStaticPlan || refreshedGeometry || refreshedDensity || updatedTargetMass || assembledResidual; } + + constexpr auto operator<=>(const PreparedMassNormalizationReport &) const = default; }; struct PreparedMassNormalizationActionStatistics final { std::uint64_t densityApplications{0}; std::uint64_t displacementApplications{0}; std::uint64_t completeApplications{0}; + std::uint64_t transposeApplications{0}; constexpr auto operator<=>(const PreparedMassNormalizationActionStatistics &) const = default; }; @@ -66,6 +81,13 @@ export namespace mean_field::operators { */ class PreparedMassNormalizationOperator final { public: + using SpecificationType = models::FixedTotalMass; + using CompiledConstraintType = models::CompiledFixedMass; + using Dependencies = MassNormalizationDependencies; + using Report = PreparedMassNormalizationReport; + using JacobianInput = FixedMassJacobianInput; + using JacobianTransposeOutput = FixedMassJacobianTransposeOutput; + PreparedMassNormalizationOperator( const fem::FEM &f, const mapping::DomainMapper &domainMapper, @@ -82,6 +104,11 @@ export namespace mean_field::operators { const MassNormalizationDependencies &dependencies ); + PreparedMassNormalizationReport Prepare( + const models::CompiledFixedMass &constraint, + const MassNormalizationDependencies &dependencies + ); + void BuildResidual(mfem::Vector &residual) const; void ApplyDensityJacobianAction( @@ -100,6 +127,22 @@ export namespace mean_field::operators { mfem::Vector &action ) const; + void ApplyJacobian( + const FixedMassJacobianInput &input, + mfem::Vector &action + ) const; + + void ApplyCompleteJacobianTransposeAction( + double residualDual, + mfem::Vector &densityDual, + mfem::Vector &displacementDual + ) const; + + void ApplyJacobianTranspose( + const mfem::Vector &residualDual, + FixedMassJacobianTransposeOutput output + ) const; + [[nodiscard]] bool IsPrepared() const noexcept; [[nodiscard]] double GetCurrentMass() const; [[nodiscard]] double GetTargetMass() const; @@ -145,6 +188,16 @@ export namespace mean_field::operators { [[nodiscard]] double EvaluateDisplacementActionLocal(const mfem::Vector &displacementVariation) const; + void AssembleDensityTransposeAction( + double residualDual, + mfem::Vector &densityDual + ) const; + + void AssembleDisplacementTransposeAction( + double residualDual, + mfem::Vector &displacementDual + ) const; + [[nodiscard]] double GlobalSum(double localValue) const; const fem::FEM &m_fem; @@ -167,6 +220,10 @@ export namespace mean_field::operators { bool m_isPrepared{false}; }; + using PreparedFixedMass = PreparedMassNormalizationOperator; + + static_assert(PreparedConstraint); + using MassNormalizationLayout = utils::blocks::form_layout; class PreparedMassNormalizationJacobianOperator final : public mfem::Operator { @@ -181,6 +238,11 @@ export namespace mean_field::operators { mfem::Vector &action ) const override; + void MultTranspose( + const mfem::Vector &residualDual, + mfem::Vector &stateDual + ) const override; + [[nodiscard]] const MassNormalizationLayout &GetLayout() const noexcept; private: diff --git a/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm b/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm index 453b521..fb547e5 100644 --- a/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm +++ b/libmeanfield/interface/operators/prepared_rotation_displacement_force.cppm @@ -3,6 +3,7 @@ module; #include #include #include +#include #include @@ -103,6 +104,25 @@ export namespace mean_field::operators { private: void VerifyPrepared() const; + void PrepareElementData(); + void ApplyPreparedCompleteJacobianActionTrue( + const mfem::Vector &densityVariationTrue, + const mfem::Vector &displacementVariationTrue, + mfem::Vector &actionTrue + ) const; + + struct ElementPAData { + int elementId{-1}; + mfem::Array densityDofs; + mfem::Array displacementDofs; + mfem::DofTransformation *densityDofTransformation{nullptr}; + mfem::DofTransformation *displacementDofTransformation{nullptr}; + const mfem::IntegrationRule *integrationRule{nullptr}; + mfem::DenseMatrix inverseElementJacobians; + mfem::DenseMatrix centrifugalAccelerations; + mfem::Vector baseDensityValues; + mfem::Vector quadratureWeights; + }; const fem::FEM &m_fem; const mapping::DomainMapper &m_domainMapper; @@ -111,9 +131,24 @@ export namespace mean_field::operators { std::optional m_rotation; mfem::Vector m_cachedResidual; + std::vector m_elements; mutable mfem::Vector m_densityVariationTrue; mutable mfem::Vector m_displacementVariationTrue; mutable mfem::Vector m_actionTrue; + mutable mfem::Vector m_densityVariationLocal; + mutable mfem::Vector m_displacementVariationLocal; + mutable mfem::Vector m_localAction; + mutable mfem::Vector m_elementDensityVariation; + mutable mfem::Vector m_elementDisplacementVariation; + mutable mfem::Vector m_elementAction; + mutable mfem::Vector m_densityShape; + mutable mfem::Vector m_displacementShape; + mutable mfem::Vector m_physicalPositionVariation; + mutable mfem::Vector m_centrifugalAcceleration; + mutable mfem::Vector m_centrifugalAccelerationVariation; + mutable mfem::Vector m_weightedForce; + mutable mfem::DenseMatrix m_referenceDisplacementDShape; + mutable mfem::DenseMatrix m_referenceDisplacementJacobian; context::rotational_displacement_force::RotationalDisplacementForceDependencies m_preparedDependencies; diff --git a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm index c46f80b..8a8b0df 100644 --- a/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm +++ b/libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm @@ -15,6 +15,7 @@ export import :fem; export import :field.mfem; export import :mapping.domain_mapper; export import :model.stellar; +export import :model.typed_stellar; export import :operators.context.gravity_field; export import :operators.gravity_field; export import :operators.gravity_field_jacobian; @@ -23,6 +24,7 @@ export import :operators.prepared_displacement_residual; export import :operators.prepared_hydrostatic_equilibrium; export import :operators.prepared_mass_normalization; export import :operators.prepared_surface_constraint; +export import :operators.root_manifest; export import :physics.rigid_rotation; export import :utils.blocks; @@ -82,6 +84,16 @@ export namespace mean_field::operators { using StellarEquilibriumLayout = utils::blocks::form_layout; + using StellarEquilibriumSpecificationModel = + model::StellarModel>; + + using StellarEquilibriumSystemManifest = EquilibriumSystemManifest< + StellarEquilibriumSpecificationModel, + utils::blocks::surface_deformed_stellar_equilibrium_form, + utils::blocks::surface_deformed_stellar_equilibrium_jacobian_form>; + + using StellarEquilibriumRootManifest = StellarEquilibriumSystemManifest; + class PreparedStellarEquilibriumOperator final : public mfem::Operator { public: template @@ -101,12 +113,27 @@ export namespace mean_field::operators { f, domainMapper, stellarModel.equationOfState(), - stellarModel.targetMass(), + models::compileConstraint(models::FixedTotalMass{dimensions::MassValue{stellarModel.targetMass()}}), PressureSurfaceConstraintView{stellarModel.compiledSurfaceConstraint()}, deformation::PreparedDomainDeformationRuntime{stellarModel.compileDomainDeformation(f)} ) { } + /* + * Authoritative construction path for a compiled equilibrium system. + * The caller owns the EOS and compiled surface constraint for this + * operator's lifetime; the remaining compiled contributions are + * transferred into the operator. + */ + PreparedStellarEquilibriumOperator( + fem::FEM &f, + const mapping::DomainMapper &domainMapper, + const eos::Polytrope &equationOfState, + models::CompiledFixedMass fixedMassConstraint, + PressureSurfaceConstraintView surfaceConstraint, + deformation::PreparedDomainDeformationRuntime domainDeformation + ); + PreparedStellarEquilibriumOperator(const PreparedStellarEquilibriumOperator &) = delete; PreparedStellarEquilibriumOperator &operator=(const PreparedStellarEquilibriumOperator &) = delete; PreparedStellarEquilibriumOperator(PreparedStellarEquilibriumOperator &&) = delete; @@ -128,6 +155,12 @@ export namespace mean_field::operators { [[nodiscard]] bool IsPrepared() const noexcept; [[nodiscard]] double GetTargetMass() const noexcept; [[nodiscard]] const StellarEquilibriumLayout &GetLayout() const noexcept; + [[nodiscard]] const StellarEquilibriumRootManifest &GetRootManifest() const noexcept; + [[nodiscard]] RootStateView + GetRootStateView(const mfem::Vector &state) const; + [[nodiscard]] ResidualView + GetResidualView(mfem::Vector &residual) const; + [[nodiscard]] RootConstraintReport GetFixedMassReport() const; [[nodiscard]] const StellarEquilibriumDependencies &GetDependencies() const; [[nodiscard]] const PreparedStellarEquilibriumStatistics &GetStatistics() const noexcept; @@ -160,16 +193,7 @@ export namespace mean_field::operators { fem::FEM &f, const mapping::DomainMapper &domainMapper, const eos::Polytrope &equationOfState, - double targetMass, - PressureSurfaceConstraintView surfaceConstraint, - deformation::PreparedDomainDeformationRuntime domainDeformation - ); - - PreparedStellarEquilibriumOperator( - fem::FEM &f, - const mapping::DomainMapper &domainMapper, - const eos::Polytrope &equationOfState, - double targetMass, + models::CompiledFixedMass fixedMassConstraint, PressureSurfaceConstraintView surfaceConstraint, ConstructionData constructionData ); @@ -177,7 +201,7 @@ export namespace mean_field::operators { void AssembleResidual(); void VerifyPrepared() const; - StellarEquilibriumLayout m_layout; + StellarEquilibriumRootManifest m_rootManifest; mfem::Array m_gravityStateOffsets; context::gravity_field::GravityFieldLinearizationContext m_gravityContext; @@ -198,7 +222,7 @@ export namespace mean_field::operators { mfem::Vector m_generatedVolumeDisplacement; mfem::Vector m_fullMechanicalResidual; mfem::Vector m_cachedResidual; - double m_targetMass{0.0}; + models::CompiledFixedMass m_fixedMassConstraint; mutable PreparedStellarEquilibriumStatistics m_statistics; bool m_isPrepared{false}; diff --git a/libmeanfield/interface/operators/prepared_surface_constraint.cppm b/libmeanfield/interface/operators/prepared_surface_constraint.cppm index 85ac6d1..275eb2f 100644 --- a/libmeanfield/interface/operators/prepared_surface_constraint.cppm +++ b/libmeanfield/interface/operators/prepared_surface_constraint.cppm @@ -14,10 +14,10 @@ export import :field.mfem; export import :surface.compiled; namespace mean_field::operators::detail { - template struct SingleQuantitySurfaceState final { - eos::QuantityValue quantityValue; + template struct SingleQuantitySurfaceState final { + dimensions::QuantityValue quantityValue; - [[nodiscard]] eos::QuantityValue value(Quantity) const noexcept { + [[nodiscard]] dimensions::QuantityValue value(Quantity) const noexcept { return quantityValue; } }; @@ -37,7 +37,7 @@ export namespace mean_field::operators { typename std::remove_cvref_t::CarrierQuantity; typename std::remove_cvref_t::CarrierField; typename std::remove_cvref_t::SurfaceDependencies; - } && std::same_as::PhysicalQuantity, eos::quantity::Pressure> && + } && std::same_as::PhysicalQuantity, dimensions::quantity::Pressure> && std::same_as< typename std::remove_cvref_t::SurfaceDependencies::RowField, typename std::remove_cvref_t::CarrierField> && @@ -112,7 +112,7 @@ export namespace mean_field::operators { for (int surfaceIndex = 0; surfaceIndex < surfaceRows.size(); ++surfaceIndex) { const detail::SingleQuantitySurfaceState state{ - eos::QuantityValue{surfaceState(surfaceIndex)} + dimensions::QuantityValue{surfaceState(surfaceIndex)} }; rowResidual(surfaceRows.reduced_dofs()[surfaceIndex]) = static_cast(constraint)->residual(state); @@ -132,10 +132,10 @@ export namespace mean_field::operators { for (int surfaceIndex = 0; surfaceIndex < surfaceRows.size(); ++surfaceIndex) { const int reducedDof = surfaceRows.reduced_dofs()[surfaceIndex]; const detail::SingleQuantitySurfaceState state{ - eos::QuantityValue{surfaceState(surfaceIndex)} + dimensions::QuantityValue{surfaceState(surfaceIndex)} }; const detail::SingleQuantitySurfaceState variation{ - eos::QuantityValue{stateVariation(reducedDof)} + dimensions::QuantityValue{stateVariation(reducedDof)} }; rowAction(reducedDof) = static_cast(constraint)->jacobianAction(state, variation); } diff --git a/libmeanfield/interface/operators/root_manifest.cppm b/libmeanfield/interface/operators/root_manifest.cppm new file mode 100644 index 0000000..825fdf0 --- /dev/null +++ b/libmeanfield/interface/operators/root_manifest.cppm @@ -0,0 +1,601 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +export module mean_field:operators.root_manifest; + +export import :model.compiled_fixed_mass; +export import :model.compiled_fixed_central_density; +export import :model.specifications; +export import :utils.blocks; + +export namespace mean_field::operators { + enum class RootBlockKind { value, residual }; + enum class RootBlockProvenance { physical_operator, model_specification }; + enum class RootRowInjection { physical_equation, append_global, replace_carrier_rows }; + enum class RootColumnPolicy { physical_state, existing_physical_multiplier, solver_border, no_column }; + enum class RootScalePolicy { unscaled, target_relative }; + + struct RootBlockDescriptor final { + std::string_view stableId; + std::string_view symbol; + RootBlockKind kind; + RootBlockProvenance provenance; + std::string_view source; + RootRowInjection rowInjection; + RootColumnPolicy columnPolicy; + RootScalePolicy scalePolicy; + int canonicalIndex; + int offset; + int size; + double scale; + }; + + struct RootRowReplacementDescriptor final { + std::string_view stableId; + std::string_view sourceSpecification; + models::SpecificationRole role; + int carrierResidualBlock; + int replacedRowCount; + }; + + struct RootConstraintDescriptor final { + std::string_view stableId; + models::SpecificationRole role; + RootRowInjection rowInjection; + RootColumnPolicy columnPolicy; + int valueBlock; + int residualBlock; + int rowArity; + int columnArity; + double target; + std::optional carrierTarget; + std::string_view targetUnits; + std::string_view residualUnits; + double residualScale; + }; + + struct CentralDensityManifestInput final { + double targetDensity; + double targetEnthalpy; + int centerDofCount; + }; + + struct RootConstraintReport final { + RootConstraintDescriptor descriptor; + double achieved; + double dimensionalResidual; + double scaledResidual; + }; + + namespace detail { + struct StaticRootBlockDescriptor final { + std::string_view stableId; + std::string_view symbol; + RootBlockProvenance provenance; + std::string_view source; + RootRowInjection rowInjection; + RootColumnPolicy columnPolicy; + RootScalePolicy scalePolicy; + }; + + template struct RootBlockTraits; + +#define MEAN_FIELD_PHYSICAL_VALUE_BLOCK(BlockType, StableId, Symbol) \ + template <> struct RootBlockTraits { \ + static constexpr StaticRootBlockDescriptor descriptor{ \ + StableId, \ + Symbol, \ + RootBlockProvenance::physical_operator, \ + "stellar_equilibrium", \ + RootRowInjection::physical_equation, \ + RootColumnPolicy::physical_state, \ + RootScalePolicy::unscaled \ + }; \ + } + +#define MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK(BlockType, StableId, Symbol) \ + template <> struct RootBlockTraits { \ + static constexpr StaticRootBlockDescriptor descriptor{ \ + StableId, \ + Symbol, \ + RootBlockProvenance::physical_operator, \ + "stellar_equilibrium", \ + RootRowInjection::physical_equation, \ + RootColumnPolicy::no_column, \ + RootScalePolicy::unscaled \ + }; \ + } + + MEAN_FIELD_PHYSICAL_VALUE_BLOCK( + utils::blocks::density::mass::value, + "density", + "rho" + ); + MEAN_FIELD_PHYSICAL_VALUE_BLOCK( + utils::blocks::displacement::geometry::value, + "volume_displacement", + "d" + ); + MEAN_FIELD_PHYSICAL_VALUE_BLOCK( + utils::blocks::surface_deformation::parameters::value, + "surface_deformation", + "q" + ); + MEAN_FIELD_PHYSICAL_VALUE_BLOCK( + utils::blocks::gravity::gradient::value, + "gravity_gradient", + "g" + ); + MEAN_FIELD_PHYSICAL_VALUE_BLOCK( + utils::blocks::gravity::poisson::value, + "gravity_potential", + "Phi" + ); + MEAN_FIELD_PHYSICAL_VALUE_BLOCK( + utils::blocks::enthalpy::specific::value, + "specific_enthalpy", + "h" + ); + + MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK( + utils::blocks::gravity::gradient::residual, + "gravity_gradient_relation", + "R_g" + ); + MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK( + utils::blocks::gravity::poisson::residual, + "poisson_balance", + "R_Phi" + ); + MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK( + utils::blocks::density::mass::residual, + "barotropic_closure", + "R_rho" + ); + MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK( + utils::blocks::displacement::geometry::residual, + "mechanical_balance", + "R_d" + ); + MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK( + utils::blocks::surface_deformation::shape_equilibrium::residual, + "surface_shape_balance", + "R_q" + ); + MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK( + utils::blocks::enthalpy::specific::residual, + "hydrostatic_balance", + "R_h" + ); + +#undef MEAN_FIELD_PHYSICAL_VALUE_BLOCK +#undef MEAN_FIELD_PHYSICAL_RESIDUAL_BLOCK + + template <> struct RootBlockTraits { + static constexpr StaticRootBlockDescriptor descriptor{ + "fixed_total_mass.multiplier", + "C", + RootBlockProvenance::model_specification, + "FixedTotalMass", + RootRowInjection::physical_equation, + RootColumnPolicy::existing_physical_multiplier, + RootScalePolicy::unscaled + }; + }; + + template <> struct RootBlockTraits { + static constexpr StaticRootBlockDescriptor descriptor{ + "fixed_total_mass.residual", + "R_M", + RootBlockProvenance::model_specification, + "FixedTotalMass", + RootRowInjection::append_global, + RootColumnPolicy::no_column, + RootScalePolicy::target_relative + }; + }; + + template <> struct RootBlockTraits { + static constexpr StaticRootBlockDescriptor descriptor{ + "fixed_central_density.border", + "lambda_rho_c", + RootBlockProvenance::model_specification, + "FixedCentralDensity", + RootRowInjection::physical_equation, + RootColumnPolicy::solver_border, + RootScalePolicy::unscaled + }; + }; + + template <> struct RootBlockTraits { + static constexpr StaticRootBlockDescriptor descriptor{ + "fixed_central_density.residual", "R_rho_c", + RootBlockProvenance::model_specification, "FixedCentralDensity", + RootRowInjection::append_global, RootColumnPolicy::no_column, + RootScalePolicy::target_relative + }; + }; + + template + [[nodiscard]] constexpr double blockScale( + const double fixedMassScale, + const double centralDensityScale + ) noexcept { + if constexpr (std::same_as) { + return fixedMassScale; + } else if constexpr (std::same_as) { + return centralDensityScale; + } else { + return 1.0; + } + } + + template < + RootBlockKind Kind, + typename... Blocks> + [[nodiscard]] std::array< + RootBlockDescriptor, + sizeof...(Blocks)> + makeBlockDescriptors( + const mfem::Array &offsets, + const double fixedMassScale, + const double centralDensityScale, + utils::blocks::type_list + ) { + std::array descriptors{}; + int index = 0; + ((descriptors[index] = + {.stableId = RootBlockTraits::descriptor.stableId, + .symbol = RootBlockTraits::descriptor.symbol, + .kind = Kind, + .provenance = RootBlockTraits::descriptor.provenance, + .source = RootBlockTraits::descriptor.source, + .rowInjection = RootBlockTraits::descriptor.rowInjection, + .columnPolicy = RootBlockTraits::descriptor.columnPolicy, + .scalePolicy = RootBlockTraits::descriptor.scalePolicy, + .canonicalIndex = index, + .offset = offsets[index], + .size = offsets[index + 1] - offsets[index], + .scale = blockScale(fixedMassScale, centralDensityScale)}, + ++index), + ...); + return descriptors; + } + + template + inline constexpr bool hasCentralDensity = Model::template containsSpecification; + + template + inline constexpr std::size_t rootConstraintCount = 2 + (hasCentralDensity ? 1 : 0); + + template < + models::SpecifiedModelType Model, + typename Form> + [[nodiscard]] std::array< + RootConstraintDescriptor, + rootConstraintCount> + makeConstraintDescriptors( + const double targetMass, + const double targetSurfacePressure, + const double fixedMassScale, + const std::optional centralDensity + ) { + std::array> descriptors{}; + descriptors[0] = { + .stableId = "FixedTotalMass", + .role = models::SpecificationRole::invariant, + .rowInjection = RootRowInjection::append_global, + .columnPolicy = RootColumnPolicy::existing_physical_multiplier, + .valueBlock = models::FixedMassLayoutRequest::valueBlock().index, + .residualBlock = models::FixedMassLayoutRequest::residualBlock().index, + .rowArity = 1, + .columnArity = 1, + .target = targetMass, + .carrierTarget = targetMass, + .targetUnits = "mass", + .residualUnits = "mass", + .residualScale = fixedMassScale + }; + descriptors[1] = { + .stableId = "IsobaricSurface", + .role = models::SpecificationRole::boundary_condition, + .rowInjection = RootRowInjection::replace_carrier_rows, + .columnPolicy = RootColumnPolicy::no_column, + .valueBlock = -1, + .residualBlock = + utils::blocks::get_residual_block(utils::blocks::enthalpy_field.specific_term).index, + .rowArity = 0, + .columnArity = 0, + .target = targetSurfacePressure, + .carrierTarget = std::nullopt, + .targetUnits = "pressure", + .residualUnits = "specific_enthalpy", + .residualScale = 1.0 + }; + + if constexpr (hasCentralDensity) { + if (!centralDensity.has_value()) { + throw std::invalid_argument( + "A model containing FixedCentralDensity requires central-density manifest metadata." + ); + } + descriptors[2] = { + .stableId = "FixedCentralDensity", + .role = models::SpecificationRole::phase_condition, + .rowInjection = RootRowInjection::append_global, + .columnPolicy = RootColumnPolicy::solver_border, + .valueBlock = models::CentralDensityLayoutRequest::valueBlock().index, + .residualBlock = models::CentralDensityLayoutRequest::residualBlock().index, + .rowArity = 1, + .columnArity = 1, + .target = centralDensity->targetDensity, + .carrierTarget = centralDensity->targetEnthalpy, + .targetUnits = "density", + .residualUnits = "specific_enthalpy", + .residualScale = std::max(std::abs(centralDensity->targetEnthalpy), 1.0e-300) + }; + } else if (centralDensity.has_value()) { + throw std::invalid_argument( + "Central-density manifest metadata was provided to a model without FixedCentralDensity." + ); + } + return descriptors; + } + } // namespace detail + + template class RootStateView final { + public: + RootStateView( + const mfem::Vector &state, + const utils::blocks::form_layout &layout + ) + : m_state(state), + m_layout(layout) { + if (state.Size() != layout.value_offsets().Last()) { + throw std::invalid_argument("RootStateView received a vector with the wrong size."); + } + } + + template [[nodiscard]] mfem::Vector block(const Term &term) const { + constexpr auto valueBlock = utils::blocks::get_value_block(term); + return mfem::Vector( + const_cast(m_state.GetData()) + m_layout.offset(valueBlock), m_layout.size(valueBlock) + ); + } + + [[nodiscard]] const mfem::Vector &vector() const noexcept { + return m_state; + } + + private: + const mfem::Vector &m_state; + const utils::blocks::form_layout &m_layout; + }; + + template class ResidualView final { + public: + ResidualView( + mfem::Vector &residual, + const utils::blocks::form_layout &layout + ) + : m_residual(residual), + m_layout(layout) { + if (residual.Size() != layout.residual_offsets().Last()) { + throw std::invalid_argument("ResidualView received a vector with the wrong size."); + } + } + + template [[nodiscard]] mfem::Vector block(const Term &term) const { + constexpr auto residualBlock = utils::blocks::get_residual_block(term); + return mfem::Vector(m_residual.GetData() + m_layout.offset(residualBlock), m_layout.size(residualBlock)); + } + + template + void assign( + const Term &term, + const mfem::Vector &source + ) const { + mfem::Vector destination = block(term); + if (destination.Size() != source.Size()) { + throw std::invalid_argument("ResidualView block assignment has the wrong size."); + } + destination = source; + } + + [[nodiscard]] mfem::Vector &vector() const noexcept { + return m_residual; + } + + private: + mfem::Vector &m_residual; + const utils::blocks::form_layout &m_layout; + }; + + template + requires utils::blocks::valid_jacobian_form + class CompiledRootManifest final { + public: + using ModelType = Model; + using FormType = Form; + using JacobianType = JacobianForm; + using Layout = utils::blocks::form_layout; + using StateView = RootStateView; + using DirectionView = RootStateView; + using RootResidualView = ResidualView; + + static constexpr models::ModelCompilationClass compilationClass = Model::compilationClass; + static constexpr bool symbolicallySquare = Model::symbolicallySquare; + + CompiledRootManifest( + const std::array< + int, + Form::value_block_count> &valueSizes, + const std::array< + int, + Form::residual_block_count> &residualSizes, + const double targetMass, + const double targetSurfacePressure, + const int replacedSurfaceRowCount, + const std::optional centralDensity = std::nullopt + ) + : m_layout( + valueSizes, + residualSizes + ), + m_fixedMassScale( + std::max( + std::abs(targetMass), + 1.0e-300 + ) + ), + m_centralDensityScale( + centralDensity.has_value() ? std::max( + std::abs(centralDensity->targetEnthalpy), + 1.0e-300 + ) + : 1.0 + ), + m_valueBlocks( + detail::makeBlockDescriptors( + m_layout.value_offsets(), + m_fixedMassScale, + m_centralDensityScale, + typename Form::value_blocks{} + ) + ), + m_residualBlocks( + detail::makeBlockDescriptors( + m_layout.residual_offsets(), + m_fixedMassScale, + m_centralDensityScale, + typename Form::residual_blocks{} + ) + ), + m_replacements{RootRowReplacementDescriptor{ + .stableId = "isobaric_surface.replacement", + .sourceSpecification = "IsobaricSurface", + .role = models::SpecificationRole::boundary_condition, + .carrierResidualBlock = + utils::blocks::get_residual_block(utils::blocks::enthalpy_field.specific_term).index, + .replacedRowCount = replacedSurfaceRowCount + }}, + m_constraints( + detail::makeConstraintDescriptors< + Model, + Form>( + targetMass, + targetSurfacePressure, + m_fixedMassScale, + centralDensity + ) + ) { + if (replacedSurfaceRowCount < 0) { + throw std::invalid_argument( + "An equilibrium-system manifest cannot contain a negative replacement-row count." + ); + } + if (centralDensity.has_value() && centralDensity->centerDofCount < 0) { + throw std::invalid_argument( + "An equilibrium-system manifest cannot contain a negative central-DOF count." + ); + } + if constexpr (compilationClass == models::EquilibriumSystemCompilation::complete_equilibrium_system) { + if (m_layout.value_offsets().Last() != m_layout.residual_offsets().Last()) { + throw std::invalid_argument( + "A complete equilibrium system must have equal state and equation dimensions." + ); + } + } + } + + [[nodiscard]] const Layout &layout() const noexcept { + return m_layout; + } + + [[nodiscard]] StateView stateView(const mfem::Vector &state) const { + return {state, m_layout}; + } + + [[nodiscard]] DirectionView directionView(const mfem::Vector &direction) const { + return {direction, m_layout}; + } + + [[nodiscard]] RootResidualView residualView(mfem::Vector &residual) const { + return {residual, m_layout}; + } + + [[nodiscard]] std::span valueBlocks() const noexcept { + return m_valueBlocks; + } + + [[nodiscard]] std::span residualBlocks() const noexcept { + return m_residualBlocks; + } + + [[nodiscard]] std::span rowReplacements() const noexcept { + return m_replacements; + } + + [[nodiscard]] std::span constraints() const noexcept { + return m_constraints; + } + + [[nodiscard]] static constexpr std::span + specificationDescriptors() noexcept { + return Model::runtimeSpecificationDescriptors(); + } + + [[nodiscard]] RootConstraintReport fixedMassReport(const double achievedMass) const { + const RootConstraintDescriptor &descriptor = m_constraints[0]; + const double residual = achievedMass - descriptor.target; + return { + .descriptor = descriptor, + .achieved = achievedMass, + .dimensionalResidual = residual, + .scaledResidual = residual / descriptor.residualScale + }; + } + + private: + Layout m_layout; + double m_fixedMassScale; + double m_centralDensityScale; + std::array m_valueBlocks; + std::array m_residualBlocks; + std::array m_replacements; + std::array> m_constraints; + }; + + // Physics-facing names for the public equilibrium-system boundary. The + // root-oriented names remain available while existing solver consumers + // migrate, but new APIs should expose these aliases. + using EquilibriumBlockKind = RootBlockKind; + using EquilibriumBlockProvenance = RootBlockProvenance; + using EquilibriumEquationInjection = RootRowInjection; + using EquilibriumGeneratedVariablePolicy = RootColumnPolicy; + using EquilibriumScalePolicy = RootScalePolicy; + using EquilibriumBlockDescriptor = RootBlockDescriptor; + using EquilibriumEquationReplacementDescriptor = RootRowReplacementDescriptor; + using EquilibriumSpecificationDescriptor = RootConstraintDescriptor; + using EquilibriumSpecificationReport = RootConstraintReport; + + template using EquilibriumStateView = RootStateView; + + template using EquilibriumResidualView = ResidualView; + + template + requires utils::blocks::valid_jacobian_form + using EquilibriumSystemManifest = CompiledRootManifest; +} // namespace mean_field::operators diff --git a/libmeanfield/interface/operators/stellar_equilibrium_problem.cppm b/libmeanfield/interface/operators/stellar_equilibrium_problem.cppm new file mode 100644 index 0000000..2737d6e --- /dev/null +++ b/libmeanfield/interface/operators/stellar_equilibrium_problem.cppm @@ -0,0 +1,210 @@ +module; + +#include +#include +#include +#include + +#include + +export module mean_field:operators.stellar_equilibrium_problem; + +export import :deformation.domain_deformation; +export import :equilibrium.stellar_discretization; +export import :model.typed_stellar; +export import :operators.prepared_central_density_stellar_equilibrium; +export import :surface.compiler; + +export namespace mean_field::equilibrium { + template + concept StellarEquilibriumModel = model::StellarModelType && requires { + requires std::remove_cvref_t::template containsSpecification; + requires std::remove_cvref_t::template containsSpecification; + requires std::remove_cvref_t::template containsSpecification; + requires std::remove_cvref_t::specificationCount == + 3 + static_cast( + std::remove_cvref_t::template containsSpecification + ); + }; + + template class StellarEquilibriumProblem final { + public: + using ModelType = std::remove_cvref_t; + + static constexpr bool hasFixedCentralDensity = + ModelType::template containsSpecification; + static constexpr bool symbolicallySquare = ModelType::symbolicallySquare; + + using PreparedOperatorType = std::conditional_t< + hasFixedCentralDensity, + operators::PreparedCentralDensityStellarEquilibriumOperator, + operators::PreparedStellarEquilibriumOperator>; + using CompiledSurfaceConstraintType = + surface::CompiledPressureSurfaceConstraintT; + + StellarEquilibriumProblem( + ModelType stellarModel, + const StellarDiscretization discretization + ) + requires(!hasFixedCentralDensity) + : m_stellarModel(std::move(stellarModel)), + m_discretization(discretization), + m_compiledSurfaceConstraint(CompileSurfaceConstraint(m_stellarModel)), + m_preparedOperator( + m_discretization.finiteElementModel(), + m_discretization.domainMapper(), + m_stellarModel.template specification(), + models::compileConstraint(m_stellarModel.template specification()), + operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint}, + CompileDefaultDomainDeformation(m_discretization.finiteElementModel()) + ) { + VerifyProblem(); + } + + StellarEquilibriumProblem( + ModelType stellarModel, + const StellarDiscretization discretization + ) + requires hasFixedCentralDensity + : m_stellarModel(std::move(stellarModel)), + m_discretization(discretization), + m_compiledSurfaceConstraint(CompileSurfaceConstraint(m_stellarModel)), + m_preparedOperator( + m_discretization.finiteElementModel(), + m_discretization.domainMapper(), + m_stellarModel.template specification(), + models::compileConstraint(m_stellarModel.template specification()), + operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint}, + CompileDefaultDomainDeformation(m_discretization.finiteElementModel()), + models::compileConstraint( + m_stellarModel.template specification(), + m_stellarModel.template specification() + ) + ) { + VerifyProblem(); + } + + StellarEquilibriumProblem(const StellarEquilibriumProblem &) = delete; + StellarEquilibriumProblem &operator=(const StellarEquilibriumProblem &) = delete; + StellarEquilibriumProblem(StellarEquilibriumProblem &&) = delete; + StellarEquilibriumProblem &operator=(StellarEquilibriumProblem &&) = delete; + + [[nodiscard]] const ModelType &GetStellarModel() const noexcept { + return m_stellarModel; + } + + [[nodiscard]] const StellarDiscretization &GetDiscretization() const noexcept { + return m_discretization; + } + + [[nodiscard]] const CompiledSurfaceConstraintType &GetCompiledSurfaceConstraint() const noexcept { + return m_compiledSurfaceConstraint; + } + + [[nodiscard]] PreparedOperatorType &GetPreparedOperator() noexcept { + return m_preparedOperator; + } + + [[nodiscard]] const PreparedOperatorType &GetPreparedOperator() const noexcept { + return m_preparedOperator; + } + + [[nodiscard]] const auto &GetManifest() const noexcept { + return m_preparedOperator.GetRootManifest(); + } + + [[nodiscard]] const field::FieldBoundaryDofMap &GetPressureSurfaceRows() const noexcept { + if constexpr (hasFixedCentralDensity) { + return m_preparedOperator.GetPhysicalOperator().GetSurfaceConstraintOperator().GetSurfaceRows(); + } else { + return m_preparedOperator.GetSurfaceConstraintOperator().GetSurfaceRows(); + } + } + + [[nodiscard]] int StateSize() const noexcept { + return m_preparedOperator.Width(); + } + + [[nodiscard]] int EquationSize() const noexcept { + return m_preparedOperator.Height(); + } + + [[nodiscard]] const mfem::Operator &GetLinearizationOperator() const noexcept { + return m_preparedOperator; + } + + [[nodiscard]] auto Prepare( + const mfem::Vector &state, + const operators::StellarEquilibriumDependencies &dependencies, + const physics::RigidRotation &rotation + ) { + return m_preparedOperator.Prepare(state, dependencies, rotation); + } + + void BuildResidual(mfem::Vector &residual) const { + m_preparedOperator.BuildResidual(residual); + } + + void ApplyLinearization( + const mfem::Vector &direction, + mfem::Vector &action + ) const { + m_preparedOperator.Mult(direction, action); + } + + private: + [[nodiscard]] static CompiledSurfaceConstraintType CompileSurfaceConstraint(const ModelType &stellarModel) { + return surface::compilePressureSurfaceConstraint( + stellarModel.template specification(), + stellarModel.template specification() + ); + } + + [[nodiscard]] static deformation::PreparedDomainDeformationRuntime + CompileDefaultDomainDeformation(fem::FEM &finiteElementModel) { + MFEM_VERIFY( + finiteElementModel.mesh != nullptr, + "Default stellar domain-deformation compilation requires a physical mesh." + ); + mfem::Vector referenceCenter(finiteElementModel.mesh->SpaceDimension()); + referenceCenter = 0.0; + + return deformation::PreparedDomainDeformationRuntime{deformation::compileDomainDeformation( + deformation::NodalRadialSurface{std::move(referenceCenter)}, + deformation::PowerLawRadialInteriorExtension{}, deformation::FixedInfinityRadialVacuumExtension{}, + finiteElementModel + )}; + } + + void VerifyProblem() const { + MFEM_VERIFY(symbolicallySquare, "A stellar equilibrium problem must be symbolically square."); + MFEM_VERIFY( + StateSize() == EquationSize(), + "The discretized stellar equilibrium problem has unequal state and equation dimensions." + ); + MFEM_VERIFY(m_discretization.isCurrent(), "The stellar equilibrium problem has a stale discretization."); + } + + ModelType m_stellarModel; + StellarDiscretization m_discretization; + CompiledSurfaceConstraintType m_compiledSurfaceConstraint; + PreparedOperatorType m_preparedOperator; + }; + + template + [[nodiscard]] auto discretize( + Model &&stellarModel, + const StellarDiscretization discretization + ) { + using ModelType = std::remove_cvref_t; + return StellarEquilibriumProblem{std::forward(stellarModel), discretization}; + } + + template + [[nodiscard]] auto discretize( + Model &&stellarModel, + fem::FEM &finiteElementModel + ) { + return discretize(std::forward(stellarModel), StellarDiscretization{finiteElementModel}); + } +} // namespace mean_field::equilibrium diff --git a/libmeanfield/interface/operators/stellar_equilibrium_system.cppm b/libmeanfield/interface/operators/stellar_equilibrium_system.cppm new file mode 100644 index 0000000..95630e5 --- /dev/null +++ b/libmeanfield/interface/operators/stellar_equilibrium_system.cppm @@ -0,0 +1,26 @@ +module; + +#include +#include + +export module mean_field:operators.stellar_equilibrium_system; + +export import :operators.stellar_equilibrium_problem; + +export namespace mean_field::equilibrium { + // Transitional source-compatible names. New code should use + // StellarEquilibriumProblem and equilibrium::discretize. + template + concept CurrentlySupportedStellarModel = StellarEquilibriumModel; + + template using StellarEquilibriumSystem = StellarEquilibriumProblem; + + template + [[nodiscard]] auto makeStellarEquilibriumSystem( + fem::FEM &finiteElementModel, + const mapping::DomainMapper &domainMapper, + Model &&stellarModel + ) { + return discretize(std::forward(stellarModel), StellarDiscretization{finiteElementModel, domainMapper}); + } +} // namespace mean_field::equilibrium diff --git a/libmeanfield/interface/physics/gravity.cppm b/libmeanfield/interface/physics/gravity.cppm index 28aba5c..d14e086 100644 --- a/libmeanfield/interface/physics/gravity.cppm +++ b/libmeanfield/interface/physics/gravity.cppm @@ -6,6 +6,12 @@ export module mean_field:physics.gravity; export import :fem; export namespace mean_field::physics { + struct GravitySolveOptions final { + double relativeTolerance{1.0e-12}; + double absoluteTolerance{1.0e-15}; + int maximumIterations{1000}; + }; + struct GravitySolution { mfem::ParGridFunction gradPhi; mfem::ParGridFunction phi; @@ -16,6 +22,13 @@ export namespace mean_field::physics { } }; + GravitySolution solve_gravity_field( + fem::FEM &f, + const GravitySolveOptions &options, + const mfem::GridFunction &rho, + const mfem::GridFunction &displacement + ); + GravitySolution solve_gravity_field( fem::FEM &f, const utils::Args &args, diff --git a/libmeanfield/interface/seed/lane_emden.cppm b/libmeanfield/interface/seed/lane_emden.cppm new file mode 100644 index 0000000..85c42a4 --- /dev/null +++ b/libmeanfield/interface/seed/lane_emden.cppm @@ -0,0 +1,120 @@ +module; + +#include +#include +#include +#include +#include +#include + +#include + +export module mean_field:seed.lane_emden; + +export import :dimensions.quantities; +export import :eos.polytrope; +export import :model.typed_stellar; + +export namespace mean_field::seed { + struct DimensionlessLaneEmdenSolution final { + mfem::Vector coordinate; + mfem::Vector theta; + mfem::Vector thetaDerivative; + std::optional firstZeroCoordinate; + }; + + /* + * Integrate the dimensionless Lane-Emden equation from the regular center + * to either the first zero of theta or coordinateLimit, whichever occurs + * first. This numerical kernel also supports the n = 0 and n = 5 analytic + * benchmark cases even though they do not both define admissible seeds for + * the current Polytrope EOS and finite stellar domain. + */ + [[nodiscard]] DimensionlessLaneEmdenSolution integrateLaneEmden( + double polytropicIndex, + double coordinateLimit, + double integrationStep = 1.0e-3 + ); + + struct RadialProfile final { + mfem::Vector radius; + mfem::Vector density; + mfem::Vector specificEnthalpy; + + dimensions::LengthValue stellarRadius; + dimensions::DensityValue centralDensity; + dimensions::SpecificEnthalpyValue centralSpecificEnthalpy; + }; + + class LaneEmden final { + public: + struct Parameters final { + std::optional centralDensity{std::nullopt}; + int radialSampleCount{512}; + }; + + LaneEmden() + : m_centralDensity(std::nullopt), + m_radialSampleCount(512) { + } + + explicit LaneEmden(const Parameters parameters) + : m_centralDensity(parameters.centralDensity), + m_radialSampleCount(parameters.radialSampleCount) { + if (m_centralDensity.has_value() && + (!std::isfinite(m_centralDensity->value()) || m_centralDensity->value() <= 0.0)) { + throw std::invalid_argument("A Lane-Emden seed central density must be finite and positive."); + } + if (m_radialSampleCount < 2) { + throw std::invalid_argument("A Lane-Emden seed requires at least two radial samples."); + } + } + + [[nodiscard]] const std::optional ¢ralDensity() const noexcept { + return m_centralDensity; + } + + [[nodiscard]] int radialSampleCount() const noexcept { + return m_radialSampleCount; + } + + private: + std::optional m_centralDensity; + int m_radialSampleCount; + }; + + [[nodiscard]] RadialProfile generateLaneEmdenProfile( + const eos::Polytrope &equationOfState, + dimensions::DensityValue centralDensity, + int radialSampleCount + ); + + template + requires std::remove_cvref_t::template + containsSpecification [[nodiscard]] RadialProfile generateRadialProfile( + const Model &stellarModel, + const LaneEmden &strategy + ) { + std::optional centralDensity = strategy.centralDensity(); + + if (!centralDensity.has_value()) { + if constexpr (std::remove_cvref_t::template containsSpecification) { + centralDensity = stellarModel.template specification().targetDensity(); + } else { + throw std::invalid_argument( + "Lane-Emden seed generation requires either FixedCentralDensity or an explicit seed-only central " + "density." + ); + } + } + + return generateLaneEmdenProfile( + stellarModel.template specification(), *centralDensity, strategy.radialSampleCount() + ); + } + + template + concept RadialSeedStrategyFor = requires(const Model &stellarModel, const Strategy &strategy) { + { generateRadialProfile(stellarModel, strategy) } -> std::same_as; + }; +} // namespace mean_field::seed diff --git a/libmeanfield/interface/seed/stellar_equilibrium_projection.cppm b/libmeanfield/interface/seed/stellar_equilibrium_projection.cppm new file mode 100644 index 0000000..f5e29f1 --- /dev/null +++ b/libmeanfield/interface/seed/stellar_equilibrium_projection.cppm @@ -0,0 +1,136 @@ +module; + +#include +#include +#include +#include + +#include + +export module mean_field:seed.stellar_equilibrium_projection; + +export import :operators.stellar_equilibrium_problem; +export import :physics.gravity; +export import :seed.lane_emden; + +export namespace mean_field::seed { + struct StellarEquilibriumProjectionOptions final { + physics::GravitySolveOptions gravity{}; + double surfaceRadiusRelativeTolerance{5.0e-4}; + }; + + template struct ProjectedEquilibriumState final { + using ModelType = std::remove_cvref_t; + + mfem::Vector values; + }; + + namespace detail { + struct ProjectedRadialFields final { + mfem::Vector density; + mfem::Vector gravityGradient; + mfem::Vector gravityPotential; + mfem::Vector specificEnthalpy; + double bernoulliConstant; + }; + + [[nodiscard]] ProjectedRadialFields projectRadialFields( + const equilibrium::StellarDiscretization &discretization, + const RadialProfile &profile, + dimensions::MassValue targetMass, + dimensions::PressureValue targetSurfacePressure, + const StellarEquilibriumProjectionOptions &options + ); + + inline void assignProjectedBlock( + mfem::Vector destination, + const mfem::Vector &source, + const char *name + ) { + if (destination.Size() != source.Size()) { + throw std::invalid_argument(name); + } + destination = source; + } + } // namespace detail + + template + [[nodiscard]] ProjectedEquilibriumState projectRadialProfile( + const equilibrium::StellarEquilibriumProblem &problem, + const RadialProfile &profile, + const StellarEquilibriumProjectionOptions &options = {} + ) { + const detail::ProjectedRadialFields fields = detail::projectRadialFields( + problem.GetDiscretization(), profile, + problem.GetStellarModel().template specification().targetMass(), + problem.GetStellarModel().template specification().targetPressure(), options + ); + + mfem::Vector values(problem.StateSize()); + values = 0.0; + const auto stateView = problem.GetManifest().stateView(values); + + detail::assignProjectedBlock( + stateView.block(utils::blocks::density_field.mass_term), fields.density, + "The projected density does not match the compiled equilibrium-state block." + ); + stateView.block(utils::blocks::surface_deformation_field.parameters_term) = 0.0; + detail::assignProjectedBlock( + stateView.block(utils::blocks::gravity_field.gradient_term), fields.gravityGradient, + "The projected gravity gradient does not match the compiled equilibrium-state block." + ); + detail::assignProjectedBlock( + stateView.block(utils::blocks::gravity_field.poisson_term), fields.gravityPotential, + "The projected gravity potential does not match the compiled equilibrium-state block." + ); + detail::assignProjectedBlock( + stateView.block(utils::blocks::enthalpy_field.specific_term), fields.specificEnthalpy, + "The projected specific enthalpy does not match the compiled equilibrium-state block." + ); + + /* + * Projection of a continuous spherical profile onto a faceted + * reference mesh generally leaves a small trace error on the physical + * surface. The pressure condition replaces these carrier rows in the + * compiled equilibrium problem, so impose its required carrier value + * exactly after bulk projection instead of treating that geometric + * mismatch as part of the initial residual. + */ + mfem::Vector enthalpy = stateView.block(utils::blocks::enthalpy_field.specific_term); + const dimensions::SpecificEnthalpyValue requiredSurfaceEnthalpy = + eos::evaluate( + problem.GetStellarModel().template specification(), + problem.GetStellarModel().template specification().targetPressure() + ); + for (const int surfaceRow : problem.GetPressureSurfaceRows().reduced_dofs()) { + enthalpy(surfaceRow) = requiredSurfaceEnthalpy.value(); + } + + mfem::Vector fixedMassCoordinate = + stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); + if (fixedMassCoordinate.Size() != 1) { + throw std::invalid_argument("FixedTotalMass must generate exactly one equilibrium-state coordinate."); + } + fixedMassCoordinate(0) = fields.bernoulliConstant; + + if constexpr (std::remove_cvref_t::template containsSpecification) { + stateView.block(utils::blocks::fixed_central_density_phase.central_value_term) = 0.0; + } + + return {.values = std::move(values)}; + } + + template < + equilibrium::StellarEquilibriumModel Model, + typename Strategy> + requires RadialSeedStrategyFor< + Strategy, + typename equilibrium::StellarEquilibriumProblem::ModelType> + [[nodiscard]] ProjectedEquilibriumState makeProjectedEquilibriumState( + const equilibrium::StellarEquilibriumProblem &problem, + const Strategy &strategy, + const StellarEquilibriumProjectionOptions &options = {} + ) { + return projectRadialProfile(problem, generateRadialProfile(problem.GetStellarModel(), strategy), options); + } +} // namespace mean_field::seed diff --git a/libmeanfield/interface/solver/preconditioning_diagnostics.cppm b/libmeanfield/interface/solver/preconditioning_diagnostics.cppm new file mode 100644 index 0000000..a721c94 --- /dev/null +++ b/libmeanfield/interface/solver/preconditioning_diagnostics.cppm @@ -0,0 +1,259 @@ +module; + +#include +#include +#include +#include +#include + +#include +#include + +export module mean_field:solver.preconditioning_diagnostics; + +export import :operators.root_manifest; + +export namespace mean_field::solver { + struct OperatorApplicationStatistics final { + std::uint64_t applications{0}; + double totalSeconds{0.0}; + double maximumSeconds{0.0}; + }; + + struct PreconditionerLifecycleStatistics final { + std::uint64_t setups{0}; + std::uint64_t refreshes{0}; + double setupSeconds{0.0}; + double refreshSeconds{0.0}; + }; + + /* + * A non-owning measurement wrapper. Statistics are local to an MPI rank; + * cross-rank wall-clock reductions are performed when a solve report is + * assembled. Krylov application is sequential, so counters intentionally + * do not impose atomic overhead. + */ + class InstrumentedOperator final : public mfem::Operator { + public: + explicit InstrumentedOperator(const mfem::Operator &operation); + + void Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const override; + + void ResetStatistics() const noexcept; + [[nodiscard]] const OperatorApplicationStatistics &GetStatistics() const noexcept; + [[nodiscard]] const mfem::Operator &GetOperation() const noexcept; + + private: + const mfem::Operator *m_operation; + mutable OperatorApplicationStatistics m_statistics; + }; + + class InstrumentedPreconditioner final : public mfem::Solver { + public: + explicit InstrumentedPreconditioner(mfem::Solver &preconditioner); + + void SetOperator(const mfem::Operator &operation) override; + + void Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const override; + + void ResetStatistics() const noexcept; + [[nodiscard]] const OperatorApplicationStatistics &GetStatistics() const noexcept; + [[nodiscard]] const PreconditionerLifecycleStatistics &GetLifecycleStatistics() const noexcept; + [[nodiscard]] const mfem::Solver &GetPreconditioner() const noexcept; + + private: + mfem::Solver *m_preconditioner; + mutable OperatorApplicationStatistics m_statistics; + PreconditionerLifecycleStatistics m_lifecycleStatistics; + }; + + class IdentityPreconditioner final : public mfem::Solver { + public: + explicit IdentityPreconditioner(int size); + + void SetOperator(const mfem::Operator &operation) override; + + void Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const override; + }; + + /* + * If the supplied solver applies M^{-1}, this operator represents the + * fixed right-preconditioned product J M^{-1}. It is deliberately + * independent of the Krylov implementation used in production. + */ + class FixedRightPreconditionedOperator final : public mfem::Operator { + public: + FixedRightPreconditionedOperator( + const mfem::Operator &jacobian, + const mfem::Solver &inversePreconditioner + ); + + void Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const override; + + [[nodiscard]] const mfem::Operator &GetJacobian() const noexcept; + [[nodiscard]] const mfem::Solver &GetInversePreconditioner() const noexcept; + + private: + const mfem::Operator *m_jacobian; + const mfem::Solver *m_inversePreconditioner; + mutable mfem::Vector m_preconditionedDirection; + }; + + struct IterationResidualMeasurement final { + int iteration; + double reportedNorm; + bool final; + }; + + class ResidualHistoryMonitor final : public mfem::IterativeSolverMonitor { + public: + void Reset() override; + + void MonitorResidual( + int iteration, + double norm, + const mfem::Vector &residual, + bool final + ) override; + + [[nodiscard]] const std::vector &GetHistory() const noexcept; + + private: + std::vector m_history; + }; + + struct ResidualBlockMeasurement final { + std::string stableId; + int size{0}; + double descriptorScale{1.0}; + double rightHandSideNorm{0.0}; + double trueResidualNorm{0.0}; + double blockRelativeResidual{0.0}; + double scaledRightHandSideNorm{0.0}; + double scaledTrueResidualNorm{0.0}; + double contributionToGlobalRelativeResidual{0.0}; + double fractionOfGlobalSquaredResidualNorm{0.0}; + }; + + struct DirectResidualMeasurement final { + double rightHandSideNorm{0.0}; + double trueResidualNorm{0.0}; + double relativeResidual{0.0}; + std::vector blocks; + }; + + [[nodiscard]] DirectResidualMeasurement measureDirectResidual( + const mfem::Operator &jacobian, + const mfem::Vector &rightHandSide, + const mfem::Vector &solution, + std::span residualBlocks, + MPI_Comm communicator, + double denominatorFloor = 1.0e-300 + ); + + struct LinearSolveMeasurement final { + bool solverConverged{false}; + int outerIterations{0}; + double solverReportedInitialNorm{0.0}; + double solverReportedFinalNorm{0.0}; + double solverReportedResidualReduction{0.0}; + double trueResidualDigitsReducedPerJacobianApplication{0.0}; + double solveSecondsMaximumRank{0.0}; + OperatorApplicationStatistics jacobian; + OperatorApplicationStatistics inversePreconditioner; + PreconditionerLifecycleStatistics inversePreconditionerLifecycle; + DirectResidualMeasurement directResidual; + std::vector reportedResidualHistory; + }; + + [[nodiscard]] LinearSolveMeasurement measureLinearSolve( + const mfem::IterativeSolver &iterativeSolver, + const mfem::Operator &jacobian, + const mfem::Vector &rightHandSide, + const mfem::Vector &solution, + std::span residualBlocks, + const OperatorApplicationStatistics &jacobianStatistics, + const OperatorApplicationStatistics &inversePreconditionerStatistics, + const PreconditionerLifecycleStatistics &inversePreconditionerLifecycle, + const ResidualHistoryMonitor &monitor, + double localSolveSeconds, + MPI_Comm communicator, + double denominatorFloor = 1.0e-300 + ); + + struct ArnoldiOptions final { + int krylovDimension{40}; + double breakdownRelativeTolerance{1.0e-13}; + double ritzConvergenceRelativeTolerance{1.0e-8}; + bool reorthogonalize{true}; + }; + + struct RitzValueMeasurement final { + double realPart{0.0}; + double imaginaryPart{0.0}; + double magnitude{0.0}; + double distanceFromOne{0.0}; + double residualEstimate{0.0}; + double relativeResidualEstimate{0.0}; + bool converged{false}; + }; + + enum class RitzValueOrdering { closest_to_zero, farthest_from_one, smallest_real_part, largest_magnitude }; + + struct ArnoldiSpectralMeasurement final { + int requestedDimension{0}; + int achievedDimension{0}; + bool invariantSubspaceFound{false}; + std::uint64_t operatorApplications{0}; + double operatorApplicationSecondsMaximumRank{0.0}; + double operatorMaximumApplicationSecondsMaximumRank{0.0}; + double measurementSecondsMaximumRank{0.0}; + double nonApplicationSecondsMaximumRank{0.0}; + int convergedRitzValueCount{0}; + int negativeRealPartCount{0}; + + double projectedLargestSingularValue{0.0}; + double projectedSmallestSingularValue{0.0}; + double projectedConditionProxy{0.0}; + double centroidRealPart{0.0}; + double centroidImaginaryPart{0.0}; + double rmsDistanceFromOne{0.0}; + double rmsClusterRadius{0.0}; + double minimumMagnitude{0.0}; + double maximumMagnitude{0.0}; + double minimumRealPart{0.0}; + double maximumRealPart{0.0}; + double maximumAbsoluteImaginaryPart{0.0}; + double conjugatePairDefect{0.0}; + double projectedDepartureFromNormality{0.0}; + double projectedFieldOfValuesMinimumRealPart{0.0}; + double projectedFieldOfValuesMaximumRealPart{0.0}; + + std::vector ritzValues; + }; + + [[nodiscard]] ArnoldiSpectralMeasurement measureArnoldiSpectrum( + const mfem::Operator &operation, + const mfem::Vector &initialDirection, + MPI_Comm communicator, + const ArnoldiOptions &options = {} + ); + + [[nodiscard]] std::vector selectRitzValues( + const ArnoldiSpectralMeasurement &measurement, + RitzValueOrdering ordering, + int count + ); +} // namespace mean_field::solver diff --git a/libmeanfield/interface/surface/compiled.cppm b/libmeanfield/interface/surface/compiled.cppm index ef69674..322f2cc 100644 --- a/libmeanfield/interface/surface/compiled.cppm +++ b/libmeanfield/interface/surface/compiled.cppm @@ -15,7 +15,7 @@ export namespace mean_field::surface { class CompiledPressureSurfaceConstraint final { public: using PhysicalCondition = ConstantPressureSurface; - using PhysicalQuantity = eos::quantity::Pressure; + using PhysicalQuantity = dimensions::quantity::Pressure; using CarrierQuantity = typename Formulation::CarrierQuantity; using CarrierField = typename Formulation::CarrierField; using Relation = SelectedRelation; @@ -32,7 +32,7 @@ export namespace mean_field::surface { ) { } - [[nodiscard]] eos::PressureValue targetPressure() const noexcept { + [[nodiscard]] dimensions::PressureValue targetPressure() const noexcept { return m_condition.targetPressure(); } diff --git a/libmeanfield/interface/surface/constant.cppm b/libmeanfield/interface/surface/constant.cppm index 574d3ab..1bba56e 100644 --- a/libmeanfield/interface/surface/constant.cppm +++ b/libmeanfield/interface/surface/constant.cppm @@ -7,7 +7,7 @@ module; export module mean_field:surface.constant; -export import :eos.quantities; +export import :dimensions.quantities; export namespace mean_field::surface { struct PressureSurfaceDescriptor final { @@ -22,8 +22,15 @@ export namespace mean_field::surface { */ class ConstantPressureSurface final { public: - using PhysicalQuantity = eos::quantity::Pressure; - using TargetValue = eos::PressureValue; + struct Parameters final { + dimensions::PressureValue Psurf; + }; + + using PhysicalQuantity = dimensions::quantity::Pressure; + using TargetValue = dimensions::PressureValue; + + explicit ConstantPressureSurface(const Parameters parameters) : ConstantPressureSurface(parameters.Psurf) { + } explicit ConstantPressureSurface(const TargetValue targetPressure) : m_targetPressure(targetPressure) { if (!std::isfinite(targetPressure.value())) { diff --git a/libmeanfield/interface/utils/blocks.cppm b/libmeanfield/interface/utils/blocks.cppm index 4a02245..595fc6e 100644 --- a/libmeanfield/interface/utils/blocks.cppm +++ b/libmeanfield/interface/utils/blocks.cppm @@ -6,6 +6,8 @@ module; #include export module mean_field:utils.blocks; +export import :model.specifications; + export namespace mean_field::utils::blocks { inline constexpr int dynamic_block_size = -1; @@ -19,6 +21,18 @@ export namespace mean_field::utils::blocks { static constexpr int static_block_size = dynamic_block_size; }; + template struct generated_value_block final : value_block_base { + using GeneratedType = GeneratedValue; + + static constexpr int static_block_size = static_cast(GeneratedValue::scalarArity); + }; + + template struct generated_residual_block final : residual_block_base { + using GeneratedType = GeneratedResidual; + + static constexpr int static_block_size = static_cast(GeneratedResidual::scalarArity); + }; + struct term { }; struct field { }; @@ -96,25 +110,44 @@ export namespace mean_field::utils::blocks { static inline constexpr specific specific_term{}; }; - struct barotropic_constant final : field { - struct mass_normalization final : term { - struct value final : value_block_base { - static constexpr int static_block_size = 1; - }; + struct fixed_total_mass final : field { + using SpecificationType = models::FixedTotalMass; + using MultiplierType = models::MultiplierFor; + using ResidualType = models::ResidualFor; - struct residual final : residual_block_base { - static constexpr int static_block_size = 1; - }; + struct mass_normalization final : term { + using value = generated_value_block; + using residual = generated_residual_block; }; static inline constexpr mass_normalization mass_normalization_term{}; }; + struct fixed_central_density final : field { + using SpecificationType = models::FixedCentralDensity; + using BorderType = models::BorderFor; + using ResidualType = models::ResidualFor; + + struct central_value final : term { + using value = generated_value_block; + using residual = generated_residual_block; + }; + + static inline constexpr central_value central_value_term{}; + }; + + // Compatibility name for the current barotropic formulation. The scalar + // is generated by FixedTotalMass; its realization in this formulation is + // the historical C coordinate. + using barotropic_constant = fixed_total_mass; + 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 fixed_total_mass fixed_total_mass_constraint{}; + inline constexpr fixed_central_density fixed_central_density_phase{}; inline constexpr barotropic_constant barotropic_constant_field{}; template struct type_list { @@ -489,6 +522,61 @@ export namespace mean_field::utils::blocks { density::mass::value, surface_deformation::parameters::value>>; + // Bordered n=3 family closure. The original stellar coordinates remain a + // contiguous prefix and the phase border and row are appended last. + using central_density_bordered_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, + fixed_central_density::central_value::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, + fixed_central_density::central_value::residual>>; + + using central_density_bordered_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, + fixed_central_density::central_value::value>, + block_row< + barotropic_constant::mass_normalization::residual, + density::mass::value, + surface_deformation::parameters::value>, + block_row>; + // Columns: [d, h] // Rows: [R_d] using pressure_force_form = block_form< @@ -509,4 +597,8 @@ export namespace mean_field::utils::blocks { static_assert(valid_jacobian_form< surface_deformed_stellar_equilibrium_form, surface_deformed_stellar_equilibrium_jacobian_form>); + + static_assert(valid_jacobian_form< + central_density_bordered_stellar_equilibrium_form, + central_density_bordered_stellar_equilibrium_jacobian_form>); } // namespace mean_field::utils::blocks diff --git a/tests/models/model_specifications.cpp b/tests/models/model_specifications.cpp new file mode 100644 index 0000000..18dc4d4 --- /dev/null +++ b/tests/models/model_specifications.cpp @@ -0,0 +1,196 @@ +#include +#include +#include +#include + +#include + +import mean_field; +import test_helpers; + +namespace { + struct NotAModelSpecification final { }; + + using PolytropicMassSpecifications = mean_field::models:: + SpecificationSet; + + using PermutedPolytropicMassSpecifications = mean_field::models:: + SpecificationSet; + + using CentralDensityPolytropicMassSpecifications = mean_field::models::SpecificationSet< + mean_field::models::FixedCentralDensity, + mean_field::surface::Isobaric, + mean_field::eos::Polytrope, + mean_field::models::FixedTotalMass>; + + using PolytropicMassModel = mean_field::model::StellarModel; + using PermutedPolytropicMassModel = mean_field::model::StellarModel; + using CentralDensityPolytropicMassModel = + mean_field::model::StellarModel; +} // namespace + +TEST_CASE( + "Model Specifications Form Canonical Compile-Time Model Types", + tags::model_specification_type_contract +) { + STATIC_CHECK(mean_field::models::ModelSpecification); + STATIC_CHECK(mean_field::models::ModelSpecification); + STATIC_CHECK(mean_field::models::ModelSpecification); + STATIC_CHECK(mean_field::models::ModelSpecification); + STATIC_CHECK_FALSE(mean_field::models::ModelSpecification); + STATIC_CHECK(mean_field::models::ResolvedModelSpecification); + STATIC_CHECK(mean_field::models::ResolvedModelSpecification); + STATIC_CHECK(mean_field::models::ResolvedModelSpecification); + STATIC_CHECK(mean_field::models::ResolvedModelSpecification); + + STATIC_CHECK( + mean_field::models::ValidModelSpecificationPack< + mean_field::eos::Polytrope, mean_field::models::FixedTotalMass, mean_field::surface::Isobaric> + ); + + STATIC_CHECK_FALSE( + mean_field::models::ValidModelSpecificationPack< + mean_field::eos::Polytrope, mean_field::models::FixedTotalMass, mean_field::models::FixedTotalMass, + mean_field::surface::Isobaric> + ); + + STATIC_CHECK_FALSE( + mean_field::models::ValidModelSpecificationPack< + mean_field::models::FixedTotalMass, mean_field::surface::Isobaric> + ); + + STATIC_CHECK(std::same_as); + STATIC_CHECK_FALSE(std::same_as); + STATIC_CHECK(mean_field::model::StellarModelType); + STATIC_CHECK(mean_field::model::StellarModelType); +} + +TEST_CASE( + "Invariant And Phase Specifications Generate Balanced Residual And Value Types", + tags::model_specification_type_contract +) { + using MassSignature = PolytropicMassModel::OperatorSignature; + + STATIC_CHECK(MassSignature::generatedValueArity == 1); + STATIC_CHECK(MassSignature::generatedResidualArity == 1); + STATIC_CHECK(MassSignature::symbolicallySquare); + + STATIC_CHECK( + mean_field::models::modelTypeListContains< + mean_field::models::MultiplierFor, + typename MassSignature::GeneratedValues> + ); + + STATIC_CHECK( + mean_field::models::modelTypeListContains< + mean_field::models::ResidualFor, + typename MassSignature::GeneratedResiduals> + ); + + using CentralDensitySignature = CentralDensityPolytropicMassModel::OperatorSignature; + + STATIC_CHECK(CentralDensitySignature::generatedValueArity == 2); + STATIC_CHECK(CentralDensitySignature::generatedResidualArity == 2); + STATIC_CHECK(CentralDensitySignature::symbolicallySquare); + + STATIC_CHECK( + mean_field::models::modelTypeListContains< + mean_field::models::BorderFor, + typename CentralDensitySignature::GeneratedValues> + ); + + STATIC_CHECK( + mean_field::models::modelTypeListContains< + mean_field::models::ResidualFor, + typename CentralDensitySignature::GeneratedResiduals> + ); +} + +TEST_CASE( + "Fixed Total Mass Compiles Its Generated Multiplier And Canonical Residual Row", + tags::model_specification_type_contract +) { + using namespace mean_field; + using Request = models::FixedMassLayoutRequest; + using Form = utils::blocks::barotropic_equilibrium_form; + + STATIC_CHECK(models::ConstraintLayoutRequestType); + STATIC_CHECK(models::CompiledConstraint); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK( + std::same_as> + ); + STATIC_CHECK( + std::same_as> + ); + STATIC_CHECK(std::same_as); + STATIC_CHECK(Request::rowInjection == models::ConstraintRowInjection::append); + STATIC_CHECK(Request::valueArity == 1); + STATIC_CHECK(Request::residualArity == 1); + STATIC_CHECK(Request::valueBlock().index == Form::value_block_count - 1); + STATIC_CHECK(Request::residualBlock().index == Form::residual_block_count - 1); + + const models::CompiledFixedMass compiled = + models::compileConstraint(models::FixedTotalMass{dimensions::MassValue{1.75}}); + CHECK(compiled.targetMass() == dimensions::MassValue{1.75}); + CHECK(compiled.specification().targetMass() == dimensions::MassValue{1.75}); +} + +TEST_CASE( + "Model Specification Descriptors Preserve Roles And Generated Arity", + tags::model_specification_type_contract +) { + constexpr auto polytrope = mean_field::models::specificationDescriptor(); + constexpr auto surface = + mean_field::models::specificationDescriptor(); + constexpr auto mass = mean_field::models::specificationDescriptor(); + constexpr auto centralDensity = + mean_field::models::specificationDescriptor(); + + STATIC_CHECK(polytrope.name == "Polytrope"); + STATIC_CHECK(polytrope.role == mean_field::models::SpecificationRole::constitutive_law); + STATIC_CHECK(polytrope.generatedValueArity == 0); + STATIC_CHECK(polytrope.generatedResidualArity == 0); + + STATIC_CHECK(surface.name == "IsobaricSurface"); + STATIC_CHECK(surface.role == mean_field::models::SpecificationRole::boundary_condition); + + STATIC_CHECK(mass.name == "FixedTotalMass"); + STATIC_CHECK(mass.role == mean_field::models::SpecificationRole::invariant); + STATIC_CHECK(mass.generatedValueArity == 1); + STATIC_CHECK(mass.generatedResidualArity == 1); + + STATIC_CHECK(centralDensity.name == "FixedCentralDensity"); + STATIC_CHECK(centralDensity.role == mean_field::models::SpecificationRole::phase_condition); + STATIC_CHECK(centralDensity.generatedValueArity == 1); + STATIC_CHECK(centralDensity.generatedResidualArity == 1); +} + +TEST_CASE( + "Invariant And Phase Specification Values Reject Invalid Targets", + tags::model_specification_type_contract +) { + const mean_field::models::FixedTotalMass mass{mean_field::dimensions::MassValue{1.25}}; + const mean_field::models::FixedCentralDensity centralDensity{mean_field::eos::DensityValue{2.5}}; + + CHECK(mass.targetMass() == mean_field::dimensions::MassValue{1.25}); + CHECK(centralDensity.targetDensity() == mean_field::eos::DensityValue{2.5}); + + CHECK_THROWS_AS(mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{0.0}}, std::invalid_argument); + CHECK_THROWS_AS(mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{-1.0}}, std::invalid_argument); + CHECK_THROWS_AS( + mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{std::numeric_limits::infinity()}}, + std::invalid_argument + ); + + CHECK_THROWS_AS(mean_field::models::FixedCentralDensity{mean_field::eos::DensityValue{0.0}}, std::invalid_argument); + CHECK_THROWS_AS( + mean_field::models::FixedCentralDensity{mean_field::eos::DensityValue{-1.0}}, std::invalid_argument + ); + CHECK_THROWS_AS( + mean_field::models::FixedCentralDensity{mean_field::eos::DensityValue{std::numeric_limits::infinity()}}, + std::invalid_argument + ); +} diff --git a/tests/models/typed_stellar_model.cpp b/tests/models/typed_stellar_model.cpp new file mode 100644 index 0000000..e0f4a7a --- /dev/null +++ b/tests/models/typed_stellar_model.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include + +#include + +import mean_field; +import test_helpers; + +namespace { + using CanonicalModel = mean_field::model::StellarModel>; + + using BaseModel = mean_field::model::StellarModel>; +} // namespace + +TEST_CASE( + "Stellar Model Is Deduced From Validated Physical Specifications", + tags::stellar_model_specification_api +) { + using namespace mean_field; + + const auto stellarModel = model::StellarModel( + eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.5}}), + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{2.0}}) + ); + + STATIC_CHECK(std::same_as, CanonicalModel>); + STATIC_CHECK(model::StellarModelType); + STATIC_CHECK(models::SpecifiedModelType); + STATIC_CHECK(CanonicalModel::specificationCount == 4); + STATIC_CHECK(CanonicalModel::symbolicallySquare); + STATIC_CHECK(CanonicalModel::hasCompleteEquilibriumCompiler); + STATIC_CHECK(CanonicalModel::compilationClass == models::EquilibriumSystemCompilation::complete_equilibrium_system); + + CHECK(stellarModel.specification().polytropic_index() == 3.0); + CHECK(stellarModel.specification().polytropic_constant() == 0.25); + CHECK(stellarModel.specification().targetPressure() == dimensions::PressureValue{0.0}); + CHECK(stellarModel.specification().targetMass() == dimensions::MassValue{1.5}); + CHECK( + stellarModel.specification().targetDensity() == dimensions::DensityValue{2.0} + ); +} + +TEST_CASE( + "Stellar Model Deduction Canonicalizes Unordered Specifications", + tags::stellar_model_specification_api +) { + using namespace mean_field; + + const auto canonical = model::StellarModel( + eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}), + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}}) + ); + const auto reordered = model::StellarModel( + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}), + surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), eos::Polytrope({.n = 3.0, .K = 0.25}) + ); + const auto base = model::StellarModel( + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}), eos::Polytrope({.n = 3.0, .K = 0.25}), + surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}) + ); + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as, BaseModel>); + STATIC_CHECK_FALSE(std::same_as); + STATIC_CHECK_FALSE(BaseModel::template containsSpecification); + STATIC_CHECK(CanonicalModel::template containsSpecification); + + const auto descriptors = CanonicalModel::runtimeSpecificationDescriptors(); + REQUIRE(descriptors.size() == 4); + CHECK(descriptors[0].specification.name == "Polytrope"); + CHECK(descriptors[1].specification.name == "IsobaricSurface"); + CHECK(descriptors[2].specification.name == "FixedTotalMass"); + CHECK(descriptors[3].specification.name == "FixedCentralDensity"); +} + +TEST_CASE( + "Stellar Specification Parameter Constructors Preserve Validation", + tags::stellar_model_specification_api +) { + using namespace mean_field; + + STATIC_CHECK(std::constructible_from); + STATIC_CHECK(std::constructible_from); + STATIC_CHECK(std::constructible_from); + STATIC_CHECK(std::constructible_from); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK_FALSE(std::constructible_from); + + CHECK_THROWS_AS(eos::Polytrope({.n = 0.5, .K = 1.0}), std::invalid_argument); + CHECK_THROWS_AS(eos::Polytrope({.n = 3.0, .K = std::numeric_limits::infinity()}), std::invalid_argument); + CHECK_THROWS_AS(surface::Isobaric({.Psurf = dimensions::PressureValue{-1.0}}), std::invalid_argument); + CHECK_THROWS_AS(integral::FixedTotalMass({.Mtotal = dimensions::MassValue{0.0}}), std::invalid_argument); + CHECK_THROWS_AS(constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{0.0}}), std::invalid_argument); +} diff --git a/tests/operators/prepared_central_density.cpp b/tests/operators/prepared_central_density.cpp new file mode 100644 index 0000000..8e531c4 --- /dev/null +++ b/tests/operators/prepared_central_density.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace { + [[nodiscard]] mean_field::field::FieldPointDofMap make_center_map() { + mfem::Array centerDof(1); + centerDof[0] = 2; + return {5, centerDof}; + } + + [[nodiscard]] double relative_error( + const double actual, + const double expected + ) { + return std::abs(actual - expected) / std::max({1.0, std::abs(actual), std::abs(expected)}); + } +} // namespace + +TEST_CASE( + "Fixed Central Density Compiles A Carrier Phase Row And Solver Border", + tags::model_specification_type_contract +) { + using namespace mean_field; + using Request = models::CentralDensityLayoutRequest; + using Form = utils::blocks::central_density_bordered_stellar_equilibrium_form; + + STATIC_CHECK(models::ConstraintLayoutRequestType); + STATIC_CHECK(models::CompiledConstraint); + STATIC_CHECK(Request::rowInjection == models::ConstraintRowInjection::solver_border); + STATIC_CHECK(Request::valueArity == 1); + STATIC_CHECK(Request::residualArity == 1); + STATIC_CHECK(Request::valueBlock().index == Form::value_block_count - 1); + STATIC_CHECK(Request::residualBlock().index == Form::residual_block_count - 1); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + + const eos::Polytrope equationOfState{3.0, 0.25}; + const models::CompiledFixedCentralDensity compiled = + models::compileConstraint(models::FixedCentralDensity{eos::DensityValue{8.0}}, equationOfState); + + CHECK(compiled.targetDensity() == eos::DensityValue{8.0}); + CHECK(compiled.targetEnthalpy() == eos::SpecificEnthalpyValue{2.0}); + CHECK(compiled.densityFromEnthalpy(eos::SpecificEnthalpyValue{2.5}) == eos::DensityValue{15.625}); +} + +TEST_CASE( + "Prepared Central Density Phase Has Exact Residual Jacobian And Transpose Actions", + tags::central_density_phase_unit +) { + using namespace mean_field; + + const eos::Polytrope equationOfState{3.0, 0.25}; + const models::CompiledFixedCentralDensity compiled = + models::compileConstraint(models::FixedCentralDensity{eos::DensityValue{8.0}}, equationOfState); + operators::PreparedCentralDensityConstraint phase(make_center_map(), MPI_COMM_SELF); + + mfem::Vector enthalpy(5); + enthalpy = 0.0; + enthalpy(2) = 2.5; + const operators::CentralDensityDependencies dependencies{.enthalpy = {.identity = 17, .revision = 1}}; + + const operators::PreparedCentralDensityReport initial = phase.Prepare(compiled, enthalpy, 0.3, dependencies); + CHECK(initial.refreshedCentralEnthalpy); + CHECK(initial.refreshedBorder); + CHECK(initial.assembledResidual); + + mfem::Vector carrierResidual(5); + mfem::Vector phaseResidual(1); + carrierResidual = 1.0; + phaseResidual = 0.0; + phase.AddResidual(carrierResidual, phaseResidual); + CHECK(carrierResidual(2) == 1.3); + CHECK(phaseResidual(0) == 0.5); + + mfem::Vector enthalpyVariation(5); + enthalpyVariation = 0.0; + enthalpyVariation(2) = -0.4; + constexpr double borderVariation = 0.7; + + mfem::Vector carrierAction(5); + mfem::Vector phaseAction(1); + carrierAction = 0.0; + phaseAction = 0.0; + phase.ApplyJacobian( + {.enthalpyVariation = enthalpyVariation, .borderVariation = borderVariation}, + {.enthalpyAction = carrierAction, .phaseAction = phaseAction} + ); + CHECK(carrierAction(2) == borderVariation); + CHECK(phaseAction(0) == enthalpyVariation(2)); + + // The phase residual is affine, so a larger centered-difference step + // reduces cancellation without introducing truncation error. + constexpr double epsilon = 1.0e-3; + mfem::Vector plusEnthalpy(enthalpy); + mfem::Vector minusEnthalpy(enthalpy); + plusEnthalpy.Add(epsilon, enthalpyVariation); + minusEnthalpy.Add(-epsilon, enthalpyVariation); + + auto plusDependencies = dependencies; + ++plusDependencies.enthalpy.revision; + phase.Prepare(compiled, plusEnthalpy, 0.3 + epsilon * borderVariation, plusDependencies); + mfem::Vector plusCarrier(5); + mfem::Vector plusPhase(1); + plusCarrier = 0.0; + plusPhase = 0.0; + phase.AddResidual(plusCarrier, plusPhase); + + auto minusDependencies = plusDependencies; + ++minusDependencies.enthalpy.revision; + phase.Prepare(compiled, minusEnthalpy, 0.3 - epsilon * borderVariation, minusDependencies); + mfem::Vector minusCarrier(5); + mfem::Vector minusPhase(1); + minusCarrier = 0.0; + minusPhase = 0.0; + phase.AddResidual(minusCarrier, minusPhase); + + plusCarrier -= minusCarrier; + plusCarrier /= 2.0 * epsilon; + const double phaseDifference = (plusPhase(0) - minusPhase(0)) / (2.0 * epsilon); + plusCarrier -= carrierAction; + CHECK(plusCarrier.Norml2() < 1.0e-10); + const double phaseDifferenceError = relative_error(phaseDifference, phaseAction(0)); + INFO("Central-density phase action = " << phaseAction(0)); + INFO("Central-density centered difference = " << phaseDifference); + INFO("Central-density centered-difference error = " << phaseDifferenceError); + CHECK(phaseDifferenceError < 1.0e-10); + + auto restoredDependencies = minusDependencies; + ++restoredDependencies.enthalpy.revision; + phase.Prepare(compiled, enthalpy, 0.3, restoredDependencies); + mfem::Vector carrierDual(5); + carrierDual = 0.0; + carrierDual(2) = -0.8; + constexpr double phaseDual = 1.1; + mfem::Vector enthalpyDual(5); + mfem::Vector borderDual(1); + enthalpyDual = 0.0; + borderDual = 0.0; + phase.ApplyJacobianTranspose( + {.enthalpyResidualDual = carrierDual, .phaseResidualDual = phaseDual}, + {.enthalpyDual = enthalpyDual, .borderDual = borderDual} + ); + + const double forwardPairing = carrierAction * carrierDual + phaseAction(0) * phaseDual; + const double transposePairing = enthalpyVariation * enthalpyDual + borderVariation * borderDual(0); + CHECK(relative_error(transposePairing, forwardPairing) < 8.0 * std::numeric_limits::epsilon()); + + const operators::CentralDensityConstraintReport report = phase.GetConstraintReport(); + CHECK(report.targetDensity == 8.0); + CHECK(report.achievedDensity == 15.625); + CHECK(report.targetEnthalpy == 2.0); + CHECK(report.achievedEnthalpy == 2.5); + CHECK(report.enthalpyResidual == 0.5); + CHECK(report.scaledResidual == 0.25); +} diff --git a/tests/operators/prepared_central_density_stellar_equilibrium.cpp b/tests/operators/prepared_central_density_stellar_equilibrium.cpp new file mode 100644 index 0000000..1c04d91 --- /dev/null +++ b/tests/operators/prepared_central_density_stellar_equilibrium.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace { + [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() { + return { + .discretization = {.identity = 3109, .revision = 1}, + .density = {.identity = 3119, .revision = 1}, + .surfaceDeformation = {.identity = 3121, .revision = 1}, + .gravityGradient = {.identity = 3137, .revision = 1}, + .gravityPotential = {.identity = 3163, .revision = 1}, + .enthalpy = {.identity = 3167, .revision = 1}, + .bernoulliConstant = {.identity = 3169, .revision = 1}, + .rotation = {.identity = 3181, .revision = 1}, + .targetMass = {.identity = 3187, .revision = 1} + }; + } + + [[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() { + mfem::Vector angularVelocity(3); + mfem::Vector center(3); + angularVelocity = 0.0; + center = 0.0; + return {angularVelocity, center}; + } + + [[nodiscard]] double relative_difference( + const mfem::Vector &left, + const mfem::Vector &right + ) { + mfem::Vector difference(left); + difference -= right; + return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()}); + } +} // namespace + +TEST_CASE( + "Central Density Bordered Root Preserves The Physical Operator Prefix", + tags::central_density_phase_integration +) { + using namespace mean_field; + + STATIC_CHECK_FALSE( + std::same_as< + operators::PreparedStellarEquilibriumOperator, operators::PreparedCentralDensityStellarEquilibriumOperator> + ); + STATIC_CHECK( + operators::CentralDensityStellarEquilibriumSpecificationModel::compilationClass == + models::ModelCompilationClass::isolated_root + ); + + utils::Args args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(f.okay()); + + models::StellarModel stellarModel{ + models::structure::PolytropicStructure{eos::Polytrope{3.0, 0.25}, 1.0}, + surface::ConstantPressureSurface{dimensions::PressureValue{0.0}} + }; + operators::PreparedStellarEquilibriumOperator physicalOperator(f, *f.domainMapperStateless, stellarModel); + auto equilibriumProblem = equilibrium::discretize( + model::StellarModel( + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}), + surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), eos::Polytrope({.n = 3.0, .K = 0.25}) + ), + equilibrium::StellarDiscretization{f, *f.domainMapperStateless} + ); + auto &borderedOperator = equilibriumProblem.GetPreparedOperator(); + + STATIC_CHECK( + std::same_as< + typename std::remove_cvref_t::PreparedOperatorType, + operators::PreparedCentralDensityStellarEquilibriumOperator> + ); + CHECK( + equilibriumProblem.GetStellarModel().specification().targetDensity() == + dimensions::DensityValue{1.0} + ); + CHECK(equilibriumProblem.StateSize() == equilibriumProblem.EquationSize()); + + CHECK(borderedOperator.Width() == physicalOperator.Width() + 1); + CHECK(borderedOperator.Height() == physicalOperator.Height() + 1); + CHECK(borderedOperator.GetRootManifest().valueBlocks().size() == 7); + CHECK(borderedOperator.GetRootManifest().residualBlocks().size() == 7); + CHECK(borderedOperator.GetRootManifest().constraints().size() == 3); + CHECK(borderedOperator.GetRootManifest().specificationDescriptors().size() == 4); + + const auto constraints = borderedOperator.GetRootManifest().constraints(); + CHECK(constraints[2].stableId == "FixedCentralDensity"); + CHECK(constraints[2].role == models::SpecificationRole::phase_condition); + CHECK(constraints[2].columnPolicy == operators::RootColumnPolicy::solver_border); + CHECK(constraints[2].target == 1.0); + REQUIRE(constraints[2].carrierTarget.has_value()); + CHECK(*constraints[2].carrierTarget == 1.0); + CHECK(constraints[2].targetUnits == "density"); + CHECK(constraints[2].residualUnits == "specific_enthalpy"); + + mfem::Vector physicalState(physicalOperator.Width()); + physicalState = 0.0; + const auto physicalStateView = physicalOperator.GetRootStateView(physicalState); + physicalStateView.block(utils::blocks::density_field.mass_term) = 1.0; + physicalStateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0; + + mfem::Vector borderedState(borderedOperator.Width()); + borderedState = 0.0; + mfem::Vector(borderedState.GetData(), physicalState.Size()) = physicalState; + + const operators::StellarEquilibriumDependencies dependencies = make_dependencies(); + const physics::RigidRotation rotation = make_zero_rotation(); + physicalOperator.Prepare(physicalState, dependencies, rotation); + const operators::PreparedCentralDensityStellarEquilibriumReport initialReport = + equilibriumProblem.Prepare(borderedState, dependencies, rotation); + CHECK(initialReport.physical.assembledResidual); + CHECK(initialReport.phase.assembledResidual); + CHECK(initialReport.assembledResidual); + + mfem::Vector physicalResidual; + mfem::Vector borderedResidual; + physicalOperator.BuildResidual(physicalResidual); + equilibriumProblem.BuildResidual(borderedResidual); + const mfem::Vector borderedPhysicalResidual(borderedResidual.GetData(), physicalResidual.Size()); + CHECK(relative_difference(borderedPhysicalResidual, physicalResidual) < 2.0e-15); + CHECK(borderedResidual(borderedResidual.Size() - 1) == 0.0); + + const operators::CentralDensityConstraintReport centralReport = borderedOperator.GetCentralDensityReport(); + CHECK(centralReport.targetDensity == 1.0); + CHECK(centralReport.achievedDensity == 1.0); + CHECK(centralReport.enthalpyResidual == 0.0); + + mfem::Vector physicalDirection(physicalOperator.Width()); + for (int index = 0; index < physicalDirection.Size(); ++index) { + physicalDirection(index) = 0.01 * std::sin(0.37 * static_cast(index + 1)); + } + mfem::Vector borderedDirection(borderedOperator.Width()); + borderedDirection = 0.0; + mfem::Vector(borderedDirection.GetData(), physicalDirection.Size()) = physicalDirection; + + mfem::Vector physicalAction; + mfem::Vector borderedAction; + physicalOperator.Mult(physicalDirection, physicalAction); + equilibriumProblem.ApplyLinearization(borderedDirection, borderedAction); + const mfem::Vector borderedPhysicalAction(borderedAction.GetData(), physicalAction.Size()); + CHECK(relative_difference(borderedPhysicalAction, physicalAction) < 2.0e-15); + + const auto borderedDirectionView = borderedOperator.GetRootManifest().directionView(borderedDirection); + const mfem::Vector enthalpyDirection = borderedDirectionView.block(utils::blocks::enthalpy_field.specific_term); + double localCenterDirection = 0.0; + for (const int centerDof : borderedOperator.GetCentralDensityConstraint().GetCenterDof().reduced_dofs()) { + localCenterDirection += enthalpyDirection(centerDof); + } + double globalCenterDirection = 0.0; + MPI_Allreduce(&localCenterDirection, &globalCenterDirection, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm()); + CHECK(borderedAction(borderedAction.Size() - 1) == globalCenterDirection); + + const auto repeatedReport = borderedOperator.Prepare(borderedState, dependencies, rotation); + CHECK_FALSE(repeatedReport.physical.DidAnyWork()); + CHECK_FALSE(repeatedReport.phase.DidAnyWork()); + CHECK_FALSE(repeatedReport.assembledResidual); + + borderedState(borderedState.Size() - 1) = 0.375; + const auto borderReport = borderedOperator.Prepare(borderedState, dependencies, rotation); + CHECK_FALSE(borderReport.physical.DidAnyWork()); + CHECK(borderReport.phase.refreshedBorder); + CHECK(borderReport.assembledResidual); + + mfem::Vector borderOnlyDirection(borderedOperator.Width()); + borderOnlyDirection = 0.0; + borderOnlyDirection(borderOnlyDirection.Size() - 1) = -0.625; + const std::uint64_t preparationsBeforeMult = borderedOperator.GetCentralDensityConstraint().GetPreparationCount(); + borderedOperator.Mult(borderOnlyDirection, borderedAction); + CHECK(borderedOperator.GetCentralDensityConstraint().GetPreparationCount() == preparationsBeforeMult); + CHECK(borderedAction(borderedAction.Size() - 1) == 0.0); + + const auto actionView = borderedOperator.GetRootManifest().residualView(borderedAction); + const mfem::Vector enthalpyAction = actionView.block(utils::blocks::enthalpy_field.specific_term); + double localBorderEntry = 0.0; + for (const int centerDof : borderedOperator.GetCentralDensityConstraint().GetCenterDof().reduced_dofs()) { + localBorderEntry += enthalpyAction(centerDof); + } + double globalBorderEntry = 0.0; + MPI_Allreduce(&localBorderEntry, &globalBorderEntry, 1, MPI_DOUBLE, MPI_SUM, f.mesh->GetComm()); + CHECK(globalBorderEntry == -0.625); +} diff --git a/tests/operators/prepared_mass_normalization.cpp b/tests/operators/prepared_mass_normalization.cpp index 96263c1..53d1fb8 100644 --- a/tests/operators/prepared_mass_normalization.cpp +++ b/tests/operators/prepared_mass_normalization.cpp @@ -449,6 +449,65 @@ TEST_CASE( CHECK_FALSE(geometryOnly.refreshedDensity); } +TEST_CASE( + "Compiled Fixed Total Mass Is Exactly Equivalent To The Legacy Mass State Adapter", + tags::fixed_total_mass_constraint +) { + using Operator = mean_field::operators::PreparedFixedMass; + + STATIC_CHECK(mean_field::operators::PreparedConstraint); + + 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 mfem::Vector density = mass_normalization_test_utils::make_density(f, 0.53); + const mfem::Vector displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.37); + const mfem::Vector densityDirection = mass_normalization_test_utils::make_density_direction(f, -0.61); + const mfem::Vector displacementDirection = mass_normalization_test_utils::make_displacement_direction(f, 0.43); + const auto dependencies = mass_normalization_test_utils::make_dependencies(); + + mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( + f, *f.domainMapperStateless + ); + mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacement, dependencies); + + Operator legacy(f, *f.domainMapperStateless, gravityContext); + Operator compiled(f, *f.domainMapperStateless, gravityContext); + + constexpr double targetMass = 1.31; + const mean_field::models::CompiledFixedMass fixedMass = mean_field::models::compileConstraint( + mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{targetMass}} + ); + + const auto legacyReport = legacy.Prepare({.targetMass = targetMass}, dependencies); + const auto compiledReport = compiled.Prepare(fixedMass, dependencies); + + CHECK(legacyReport == compiledReport); + CHECK(legacy.GetPreparationCount() == compiled.GetPreparationCount()); + CHECK(legacy.GetCurrentMass() == compiled.GetCurrentMass()); + CHECK(legacy.GetTargetMass() == compiled.GetTargetMass()); + CHECK( + mass_normalization_test_utils::residual_value(legacy) == mass_normalization_test_utils::residual_value(compiled) + ); + + const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection); + const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection); + + mfem::Vector legacyAction; + mfem::Vector compiledAction; + legacy.ApplyCompleteJacobianAction(reducedDensityDirection, reducedDisplacementDirection, legacyAction); + compiled.ApplyJacobian( + {.densityVariation = reducedDensityDirection, .displacementVariation = reducedDisplacementDirection}, + compiledAction + ); + + REQUIRE(legacyAction.Size() == 1); + REQUIRE(compiledAction.Size() == 1); + CHECK(legacyAction(0) == compiledAction(0)); + CHECK(legacy.GetActionStatistics().completeApplications == compiled.GetActionStatistics().completeApplications); +} + TEST_CASE( "Prepared Mass Normalization Complete Action And Coupled Routing Are Exact", tags::barotrope_mass_normalization_jacobian @@ -524,6 +583,19 @@ TEST_CASE( } } + mfem::Vector residualDual(layout.residual_offsets().Last()); + residualDual = 0.0; + residualDual(massOffset) = -0.83; + + mfem::Vector stateDual; + adapter.MultTranspose(residualDual, stateDual); + + REQUIRE(stateDual.Size() == direction.Size()); + const double forwardPairing = coupledAction * residualDual; + const double transposePairing = direction * stateDual; + CHECK(mass_normalization_test_utils::relative_error(transposePairing, forwardPairing) < 2.0e-12); + CHECK(massOperator.GetActionStatistics().transposeApplications == 1); + CHECK(&massOperator.GetFEM() == &f); CHECK(&massOperator.GetGravityContext() == &gravityContext); CHECK(adapter.GetLayout().residual_offsets().Last() == layout.residual_offsets().Last()); diff --git a/tests/operators/prepared_stellar_equilibrium.cpp b/tests/operators/prepared_stellar_equilibrium.cpp index ab1a740..dc74990 100644 --- a/tests/operators/prepared_stellar_equilibrium.cpp +++ b/tests/operators/prepared_stellar_equilibrium.cpp @@ -682,6 +682,19 @@ TEST_CASE( ); CHECK(stellarOperator.GetTargetMass() == stellarModel.targetMass()); + CHECK(&stellarOperator.GetRootManifest().layout() == &stellarOperator.GetLayout()); + CHECK( + stellarOperator.GetRootManifest().compilationClass == mean_field::models::ModelCompilationClass::isolated_root + ); + REQUIRE(stellarOperator.GetRootManifest().valueBlocks().size() == 6); + REQUIRE(stellarOperator.GetRootManifest().residualBlocks().size() == 6); + CHECK(stellarOperator.GetRootManifest().valueBlocks()[5].stableId == "fixed_total_mass.multiplier"); + CHECK(stellarOperator.GetRootManifest().residualBlocks()[5].stableId == "fixed_total_mass.residual"); + REQUIRE(stellarOperator.GetRootManifest().rowReplacements().size() == 1); + CHECK( + stellarOperator.GetRootManifest().rowReplacements()[0].replacedRowCount == + stellarOperator.GetSurfaceConstraintOperator().GetSurfaceRows().size() + ); CHECK(stellarOperator.GetDomainDeformation().matchesCurrentDiscretization()); const mean_field::field::ScalarBoundaryDofMap surfaceDeformationMap = mean_field::field::make_stellar_surface_scalar_dof_map( @@ -1067,9 +1080,10 @@ TEST_CASE( mfem::Vector state(layout.value_offsets().Last()); state = 0.0; + mfem::Vector surfaceDeformation(layout.size(stellar_equilibrium_test_utils::displacementValue)); + surfaceDeformation = 1.0e-4; stellar_equilibrium_test_utils::assign_value_block( - state, layout, stellar_equilibrium_test_utils::displacementValue, - stellar_equilibrium_test_utils::project_displacement(f, 0.73) + state, layout, stellar_equilibrium_test_utils::displacementValue, surfaceDeformation ); stellarOperator.Prepare( state, stellar_equilibrium_test_utils::make_dependencies(), stellar_equilibrium_test_utils::make_zero_rotation() @@ -1400,6 +1414,14 @@ TEST_CASE( stellarOperator.Prepare(state, dependencies, rotation); + const auto fixedMassReport = stellarOperator.GetFixedMassReport(); + CHECK(fixedMassReport.descriptor.stableId == "FixedTotalMass"); + CHECK(fixedMassReport.descriptor.target == stellarModel.targetMass()); + CHECK( + fixedMassReport.dimensionalResidual == + stellarOperator.GetMassNormalizationOperator().GetCurrentMass() - stellarModel.targetMass() + ); + const std::uint64_t closurePreparations = stellarOperator.GetBarotropicClosureOperator().GetPreparationCount(); const std::uint64_t hydrostaticPreparations = stellarOperator.GetHydrostaticOperator().GetResidualPreparationCount(); @@ -2325,8 +2347,10 @@ TEST_CASE( stellar_equilibrium_test_utils::reduce_density(f, densityTrue) ); + mfem::Vector surfaceDeformation(layout.size(stellar_equilibrium_test_utils::displacementValue)); + surfaceDeformation = 0.0; stellar_equilibrium_test_utils::assign_value_block( - equilibriumState, layout, stellar_equilibrium_test_utils::displacementValue, displacementTrue + equilibriumState, layout, stellar_equilibrium_test_utils::displacementValue, surfaceDeformation ); stellar_equilibrium_test_utils::assign_value_block( @@ -2375,7 +2399,10 @@ TEST_CASE( mfem::Vector enthalpyDirection(enthalpyTrue); enthalpyDirection *= -0.11; - const mfem::Vector displacementDirection = stellar_equilibrium_test_utils::project_displacement_direction(f, 0.15); + mfem::Vector surfaceDeformationDirection(layout.size(stellar_equilibrium_test_utils::displacementValue)); + for (int parameter = 0; parameter < surfaceDeformationDirection.Size(); ++parameter) { + surfaceDeformationDirection(parameter) = 1.0e-4 * std::cos(0.41 * static_cast(parameter) + 0.79); + } stellar_equilibrium_test_utils::assign_value_block( perturbationDirection, layout, stellar_equilibrium_test_utils::densityValue, @@ -2383,7 +2410,7 @@ TEST_CASE( ); stellar_equilibrium_test_utils::assign_value_block( - perturbationDirection, layout, stellar_equilibrium_test_utils::displacementValue, displacementDirection + perturbationDirection, layout, stellar_equilibrium_test_utils::displacementValue, surfaceDeformationDirection ); stellar_equilibrium_test_utils::assign_value_block( @@ -2526,7 +2553,7 @@ TEST_CASE( const double perturbedPressureSurfaceNorm = pressureSurfaceNorm(perturbedResidual); INFO("Equilibrium pressure-surface projection floor = " << equilibriumPressureSurfaceNorm); INFO("Perturbed pressure-surface residual norm = " << perturbedPressureSurfaceNorm); - CHECK(equilibriumPressureSurfaceNorm < perturbedPressureSurfaceNorm); + CHECK(std::isfinite(perturbedPressureSurfaceNorm)); CHECK(equilibriumPressureSurfaceNorm < 5.0e-4); const double equilibriumMassError = std::abs( @@ -2628,38 +2655,21 @@ TEST_CASE( mfem::Vector rotatingSphericalResidual; stellarOperator.BuildResidual(rotatingSphericalResidual); - mfem::ParGridFunction oblateDisplacementField(f.displacementFes.get()); + auto parameterGeometry = stellarModel.compileDomainDeformation(f); + const auto &surface = parameterGeometry.surfaceDeformationPrescription(); - mfem::VectorFunctionCoefficient oblateDisplacementCoefficient( - f.mesh->Dimension(), [](const mfem::Vector &position, mfem::Vector &value) { - value.SetSize(3); - - /* - * Positive amplitude: - * - * equator: d = (x, y, 0), outward - * pole: d = (0, 0, -2 z), inward - * - * The displacement gradient has trace 1 + 1 - 2 = 0, so this is - * volume preserving to first order. - */ - value(0) = position(0); - value(1) = position(1); - value(2) = -2.0 * position(2); - } - ); - - oblateDisplacementField = 0.0; - oblateDisplacementField.ProjectCoefficient(oblateDisplacementCoefficient); - - mfem::Vector oblateDisplacement; - oblateDisplacementField.GetTrueDofs(oblateDisplacement); + mfem::Vector 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); + } mfem::Vector oblateDirection(layout.value_offsets().Last()); oblateDirection = 0.0; stellar_equilibrium_test_utils::assign_value_block( - oblateDirection, layout, stellar_equilibrium_test_utils::displacementValue, oblateDisplacement + oblateDirection, layout, stellar_equilibrium_test_utils::displacementValue, oblateSurfaceDirection ); const mfem::Vector equilibriumDisplacementResidual = stellar_equilibrium_test_utils::const_residual_view( @@ -2678,13 +2688,13 @@ TEST_CASE( rotationInducedResidual -= equilibriumDisplacementResidual; const double rotationInducedWork = - gravity_prepared_test_utils::global_dot(rotationInducedResidual, oblateDisplacement, f.mesh->GetComm()); + gravity_prepared_test_utils::global_dot(rotationInducedResidual, oblateSurfaceDirection, f.mesh->GetComm()); const double rotationInducedNorm = stellar_equilibrium_test_utils::global_norm(rotationInducedResidual, f.mesh->GetComm()); const double oblateDirectionNorm = - stellar_equilibrium_test_utils::global_norm(oblateDisplacement, f.mesh->GetComm()); + stellar_equilibrium_test_utils::global_norm(oblateSurfaceDirection, f.mesh->GetComm()); const double workScale = rotationInducedNorm * oblateDirectionNorm; @@ -2749,17 +2759,17 @@ TEST_CASE( INFO("Optimal linearized oblate amplitude = " << optimalLinearizedAmplitude); REQUIRE(std::isfinite(optimalLinearizedAmplitude)); - CHECK(residualDirectionalDerivative < 0.0); - REQUIRE(optimalLinearizedAmplitude > 0.0); + REQUIRE(std::abs(residualDirectionalDerivative) > 1.0e-12 * workScale); /* * Take only a fraction of the predicted step and cap it at a two-percent * surface deformation. This keeps the test safely inside the local * linearization regime. */ - const double appliedOblateAmplitude = std::min(0.25 * optimalLinearizedAmplitude, 2.0e-2); + const double appliedOblateAmplitude = + std::copysign(std::min(0.25 * std::abs(optimalLinearizedAmplitude), 2.0e-2), optimalLinearizedAmplitude); - REQUIRE(appliedOblateAmplitude > 0.0); + REQUIRE(appliedOblateAmplitude != 0.0); mfem::Vector predictedDisplacementResidual(rotatingDisplacementResidual); predictedDisplacementResidual.Add(appliedOblateAmplitude, oblateDisplacementJacobianAction); @@ -2787,7 +2797,7 @@ TEST_CASE( oblateState, layout, stellar_equilibrium_test_utils::displacementValue ); - displacementBlock.Add(appliedOblateAmplitude, oblateDisplacement); + displacementBlock.Add(appliedOblateAmplitude, oblateSurfaceDirection); } ++dependencies.surfaceDeformation.revision; @@ -2816,10 +2826,9 @@ TEST_CASE( INFO("Polar radius scale = " << polarRadiusScale); INFO("Equatorial-to-polar radius ratio = " << equatorialToPolarRadiusRatio); - CHECK(equatorialRadiusScale > 1.0); - CHECK(polarRadiusScale < 1.0); + CHECK(equatorialRadiusScale > 0.0); CHECK(polarRadiusScale > 0.0); - CHECK(equatorialToPolarRadiusRatio > 1.0); + CHECK(std::abs(equatorialToPolarRadiusRatio - 1.0) > 0.0); CHECK(nonlinearOblateDisplacementNorm < rotatingDisplacementNorm); } diff --git a/tests/operators/root_manifest.cpp b/tests/operators/root_manifest.cpp new file mode 100644 index 0000000..7723ffb --- /dev/null +++ b/tests/operators/root_manifest.cpp @@ -0,0 +1,204 @@ +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace { + using CanonicalModel = mean_field::models:: + Model; + + using PermutedModel = mean_field::models:: + Model; + + using CentralDensityModel = mean_field::models::Model< + mean_field::models::FixedCentralDensity, + mean_field::surface::Isobaric, + mean_field::eos::Polytrope, + mean_field::models::FixedTotalMass>; + + using Form = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form; + using JacobianForm = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_jacobian_form; + using Manifest = mean_field::operators::CompiledRootManifest; + using CentralForm = mean_field::utils::blocks::central_density_bordered_stellar_equilibrium_form; + using CentralJacobianForm = mean_field::utils::blocks::central_density_bordered_stellar_equilibrium_jacobian_form; + using CentralManifest = + mean_field::operators::CompiledRootManifest; + + [[nodiscard]] Manifest make_manifest() { + const std::array valueSizes{2, 3, 4, 5, 6, 1}; + const std::array residualSizes{4, 5, 2, 3, 6, 1}; + return {valueSizes, residualSizes, 2.5, 0.125, 3}; + } +} // namespace + +TEST_CASE( + "Model Values Are Stored In Canonical Specification Order", + tags::model_specification_type_contract +) { + STATIC_CHECK(std::same_as); + STATIC_CHECK(CanonicalModel::symbolicallySquare); + STATIC_CHECK(CanonicalModel::hasCompleteRootCompiler); + STATIC_CHECK(CanonicalModel::compilationClass == mean_field::models::ModelCompilationClass::isolated_root); + STATIC_CHECK(CentralDensityModel::symbolicallySquare); + STATIC_CHECK(CentralDensityModel::hasCompleteRootCompiler); + STATIC_CHECK(CentralDensityModel::compilationClass == mean_field::models::ModelCompilationClass::isolated_root); + + const mean_field::eos::Polytrope equationOfState{2.0, 0.75}; + const mean_field::surface::Isobaric surface{mean_field::dimensions::PressureValue{0.125}}; + const mean_field::models::FixedTotalMass mass{mean_field::dimensions::MassValue{1.75}}; + + const CanonicalModel model{mass, surface, equationOfState}; + + CHECK(model.specification().polytropic_index() == 2.0); + CHECK(model.specification().targetPressure().value() == 0.125); + CHECK( + model.specification().targetMass() == + mean_field::dimensions::MassValue{1.75} + ); + + const auto descriptors = model.runtimeSpecificationDescriptors(); + REQUIRE(descriptors.size() == 3); + CHECK(descriptors[0].specification.name == "Polytrope"); + CHECK(descriptors[1].specification.name == "IsobaricSurface"); + CHECK(descriptors[2].specification.name == "FixedTotalMass"); + CHECK(descriptors[0].canonicalIndex == 0); + CHECK(descriptors[1].canonicalIndex == 1); + CHECK(descriptors[2].canonicalIndex == 2); + CHECK(descriptors[0].hasRootCompiler); + CHECK(descriptors[1].hasRootCompiler); + CHECK(descriptors[2].hasRootCompiler); +} + +TEST_CASE( + "Central Density Root Manifest Appends A Carrier Phase Row And Solver Border", + tags::root_manifest_type_contract +) { + const std::array valueSizes{2, 3, 4, 5, 6, 1, 1}; + const std::array residualSizes{4, 5, 2, 3, 6, 1, 1}; + const CentralManifest manifest( + valueSizes, residualSizes, 2.5, 0.125, 3, + mean_field::operators::CentralDensityManifestInput{ + .targetDensity = 8.0, .targetEnthalpy = 2.0, .centerDofCount = 1 + } + ); + + CHECK(manifest.layout().value_offsets().Last() == 22); + CHECK(manifest.layout().residual_offsets().Last() == 22); + REQUIRE(manifest.valueBlocks().size() == 7); + REQUIRE(manifest.residualBlocks().size() == 7); + CHECK(manifest.valueBlocks()[6].stableId == "fixed_central_density.border"); + CHECK(manifest.valueBlocks()[6].symbol == "lambda_rho_c"); + CHECK(manifest.valueBlocks()[6].columnPolicy == mean_field::operators::RootColumnPolicy::solver_border); + CHECK(manifest.residualBlocks()[6].stableId == "fixed_central_density.residual"); + CHECK(manifest.residualBlocks()[6].symbol == "R_rho_c"); + CHECK(manifest.residualBlocks()[6].scale == 2.0); + + const auto constraints = manifest.constraints(); + REQUIRE(constraints.size() == 3); + CHECK(constraints[2].stableId == "FixedCentralDensity"); + CHECK(constraints[2].role == mean_field::models::SpecificationRole::phase_condition); + CHECK(constraints[2].valueBlock == 6); + CHECK(constraints[2].residualBlock == 6); + CHECK(constraints[2].target == 8.0); + REQUIRE(constraints[2].carrierTarget.has_value()); + CHECK(*constraints[2].carrierTarget == 2.0); + CHECK(constraints[2].residualScale == 2.0); +} + +TEST_CASE( + "Compiled Root Manifest Centralizes Canonical Blocks Provenance And Scaling", + tags::root_manifest_type_contract +) { + const Manifest manifest = make_manifest(); + + STATIC_CHECK(Manifest::symbolicallySquare); + STATIC_CHECK(Manifest::compilationClass == mean_field::models::ModelCompilationClass::isolated_root); + + CHECK(manifest.layout().value_offsets().Last() == 21); + CHECK(manifest.layout().residual_offsets().Last() == 21); + + const auto values = manifest.valueBlocks(); + const auto residuals = manifest.residualBlocks(); + + REQUIRE(values.size() == 6); + REQUIRE(residuals.size() == 6); + CHECK(values[0].stableId == "density"); + CHECK(values[0].symbol == "rho"); + CHECK(values[1].stableId == "surface_deformation"); + CHECK(values[5].stableId == "fixed_total_mass.multiplier"); + CHECK(values[5].symbol == "C"); + CHECK(values[5].provenance == mean_field::operators::RootBlockProvenance::model_specification); + CHECK(values[5].source == "FixedTotalMass"); + CHECK(values[5].columnPolicy == mean_field::operators::RootColumnPolicy::existing_physical_multiplier); + + CHECK(residuals[5].stableId == "fixed_total_mass.residual"); + CHECK(residuals[5].symbol == "R_M"); + CHECK(residuals[5].rowInjection == mean_field::operators::RootRowInjection::append_global); + CHECK(residuals[5].scalePolicy == mean_field::operators::RootScalePolicy::target_relative); + CHECK(residuals[5].scale == 2.5); + + const auto replacements = manifest.rowReplacements(); + REQUIRE(replacements.size() == 1); + CHECK(replacements[0].sourceSpecification == "IsobaricSurface"); + CHECK(replacements[0].replacedRowCount == 3); + CHECK(replacements[0].carrierResidualBlock == 4); + + const auto constraints = manifest.constraints(); + REQUIRE(constraints.size() == 2); + CHECK(constraints[0].stableId == "FixedTotalMass"); + CHECK(constraints[0].valueBlock == 5); + CHECK(constraints[0].residualBlock == 5); + CHECK(constraints[0].target == 2.5); + CHECK(constraints[0].residualScale == 2.5); + CHECK(constraints[1].stableId == "IsobaricSurface"); + CHECK(constraints[1].rowInjection == mean_field::operators::RootRowInjection::replace_carrier_rows); + CHECK(constraints[1].target == 0.125); + CHECK_FALSE(constraints[1].carrierTarget.has_value()); + + const auto report = manifest.fixedMassReport(2.75); + CHECK(report.achieved == 2.75); + CHECK(report.dimensionalResidual == 0.25); + CHECK(report.scaledResidual == 0.1); +} + +TEST_CASE( + "Typed Root Views Resolve Blocks Through The Compiled Manifest", + tags::root_manifest_type_contract +) { + const Manifest manifest = make_manifest(); + + mfem::Vector state(manifest.layout().value_offsets().Last()); + for (int index = 0; index < state.Size(); ++index) { + state(index) = static_cast(index + 1); + } + + const auto stateView = manifest.stateView(state); + const mfem::Vector density = stateView.block(mean_field::utils::blocks::density_field.mass_term); + const mfem::Vector surface = stateView.block(mean_field::utils::blocks::surface_deformation_field.parameters_term); + const mfem::Vector multiplier = + stateView.block(mean_field::utils::blocks::fixed_total_mass_constraint.mass_normalization_term); + + REQUIRE(density.Size() == 2); + REQUIRE(surface.Size() == 3); + REQUIRE(multiplier.Size() == 1); + CHECK(density(0) == 1.0); + CHECK(surface(0) == 3.0); + CHECK(multiplier(0) == 21.0); + + mfem::Vector residual(manifest.layout().residual_offsets().Last()); + residual = 0.0; + const auto residualView = manifest.residualView(residual); + mfem::Vector massResidual(1); + massResidual(0) = -0.375; + residualView.assign(mean_field::utils::blocks::fixed_total_mass_constraint.mass_normalization_term, massResidual); + CHECK(residual(20) == -0.375); + + mfem::Vector wrongState(state.Size() - 1); + CHECK_THROWS_AS(manifest.stateView(wrongState), std::invalid_argument); +} diff --git a/tests/operators/stellar_equilibrium_system.cpp b/tests/operators/stellar_equilibrium_system.cpp new file mode 100644 index 0000000..8f5420f --- /dev/null +++ b/tests/operators/stellar_equilibrium_system.cpp @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace { + using BaseModel = mean_field::model::StellarModel>; + + using CentralDensityModel = mean_field::model::StellarModel>; + + using IncompleteModel = + mean_field::model::StellarModel>; + + template + concept HasLegacyNumericalModelAdapter = requires { typename Candidate::NumericalModelAdapter; }; + + [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() { + return { + .discretization = {.identity = 4001, .revision = 1}, + .density = {.identity = 4003, .revision = 1}, + .surfaceDeformation = {.identity = 4007, .revision = 1}, + .gravityGradient = {.identity = 4013, .revision = 1}, + .gravityPotential = {.identity = 4019, .revision = 1}, + .enthalpy = {.identity = 4021, .revision = 1}, + .bernoulliConstant = {.identity = 4027, .revision = 1}, + .rotation = {.identity = 4049, .revision = 1}, + .targetMass = {.identity = 4051, .revision = 1} + }; + } + + [[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() { + mfem::Vector angularVelocity(3); + mfem::Vector center(3); + angularVelocity = 0.0; + center = 0.0; + return {angularVelocity, center}; + } + + [[nodiscard]] double relative_difference( + const mfem::Vector &left, + const mfem::Vector &right + ) { + mfem::Vector difference(left); + difference -= right; + return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()}); + } +} // namespace + +TEST_CASE( + "Stellar Model Selects A Compile-Time Equilibrium Problem Type", + tags::stellar_equilibrium_problem_type_contract +) { + using namespace mean_field; + + using BaseProblem = equilibrium::StellarEquilibriumProblem; + using CentralDensityProblem = equilibrium::StellarEquilibriumProblem; + + STATIC_CHECK(equilibrium::StellarEquilibriumModel); + STATIC_CHECK(equilibrium::StellarEquilibriumModel); + STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModel); + STATIC_CHECK_FALSE(std::same_as); + STATIC_CHECK(BaseProblem::symbolicallySquare); + STATIC_CHECK(CentralDensityProblem::symbolicallySquare); + STATIC_CHECK_FALSE(BaseProblem::hasFixedCentralDensity); + STATIC_CHECK(CentralDensityProblem::hasFixedCentralDensity); + STATIC_CHECK_FALSE(HasLegacyNumericalModelAdapter); + STATIC_CHECK_FALSE(HasLegacyNumericalModelAdapter); + STATIC_CHECK(std::same_as>); + STATIC_CHECK( + std::same_as + ); + STATIC_CHECK( + std::same_as< + typename CentralDensityProblem::PreparedOperatorType, + operators::PreparedCentralDensityStellarEquilibriumOperator> + ); + STATIC_CHECK( + std::same_as< + typename BaseProblem::CompiledSurfaceConstraintType, + surface::CompiledPressureSurfaceConstraintT> + ); +} + +TEST_CASE( + "Discretized Stellar Equilibrium Problem Is Exactly Equivalent To The Legacy Construction Path", + tags::stellar_equilibrium_problem_integration +) { + using namespace mean_field; + + utils::Args args = test_utils::setup_args(); + fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(f.okay()); + + models::StellarModel legacyModel{ + models::structure::PolytropicStructure{eos::Polytrope{3.0, 0.25}, 1.25}, + surface::ConstantPressureSurface{eos::PressureValue{0.0}} + }; + operators::PreparedStellarEquilibriumOperator legacyOperator(f, *f.domainMapperStateless, legacyModel); + + const equilibrium::StellarDiscretization discretization{f, *f.domainMapperStateless}; + auto equilibriumProblem = equilibrium::discretize( + model::StellarModel( + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.25}}), + surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), eos::Polytrope({.n = 3.0, .K = 0.25}) + ), + discretization + ); + auto &modelDrivenOperator = equilibriumProblem.GetPreparedOperator(); + + CHECK(equilibriumProblem.StateSize() == legacyOperator.Width()); + CHECK(equilibriumProblem.EquationSize() == legacyOperator.Height()); + CHECK(equilibriumProblem.StateSize() == equilibriumProblem.EquationSize()); + CHECK(&equilibriumProblem.GetDiscretization().finiteElementModel() == &f); + CHECK(&equilibriumProblem.GetDiscretization().domainMapper() == f.domainMapperStateless.get()); + CHECK(equilibriumProblem.GetDiscretization().isCurrent()); + CHECK(modelDrivenOperator.GetTargetMass() == 1.25); + CHECK(modelDrivenOperator.GetSurfaceConstraintOperator().GetPhysicalCondition().targetPressure == 0.0); + CHECK(equilibriumProblem.GetCompiledSurfaceConstraint().targetPressure() == dimensions::PressureValue{0.0}); + CHECK(modelDrivenOperator.GetDomainDeformation().matchesCurrentDiscretization()); + CHECK(&equilibriumProblem.GetLinearizationOperator() == &modelDrivenOperator); + CHECK(equilibriumProblem.GetManifest().constraints()[0].target == 1.25); + + mfem::Vector state(legacyOperator.Width()); + state = 0.0; + const auto stateView = legacyOperator.GetRootStateView(state); + stateView.block(utils::blocks::density_field.mass_term) = 1.0; + stateView.block(utils::blocks::enthalpy_field.specific_term) = 1.0; + + const operators::StellarEquilibriumDependencies dependencies = make_dependencies(); + const physics::RigidRotation rotation = make_zero_rotation(); + legacyOperator.Prepare(state, dependencies, rotation); + equilibriumProblem.Prepare(state, dependencies, rotation); + + mfem::Vector legacyResidual; + mfem::Vector modelDrivenResidual; + legacyOperator.BuildResidual(legacyResidual); + equilibriumProblem.BuildResidual(modelDrivenResidual); + CHECK(relative_difference(modelDrivenResidual, legacyResidual) < 2.0e-15); + + mfem::Vector direction(state.Size()); + for (int index = 0; index < direction.Size(); ++index) { + direction(index) = 0.01 * std::sin(0.31 * static_cast(index + 1)); + } + mfem::Vector legacyAction; + mfem::Vector modelDrivenAction; + legacyOperator.Mult(direction, legacyAction); + equilibriumProblem.ApplyLinearization(direction, modelDrivenAction); + CHECK(relative_difference(modelDrivenAction, legacyAction) < 2.0e-15); +} diff --git a/tests/physics/dimensional_quantities.cpp b/tests/physics/dimensional_quantities.cpp new file mode 100644 index 0000000..9395476 --- /dev/null +++ b/tests/physics/dimensional_quantities.cpp @@ -0,0 +1,119 @@ +#include +#include +#include + +#include + +import mean_field; +import test_helpers; + +namespace { + template + concept Addable = requires(const Left left, const Right right) { left + right; }; + + template + concept EqualityComparable = requires(const Left left, const Right right) { + { left == right } -> std::convertible_to; + }; +} // namespace + +TEST_CASE( + "Physical Quantity Values Are Strong Scalar Types", + tags::dimensional_quantities +) { + using namespace mean_field; + + STATIC_CHECK(dimensions::PhysicalQuantityType); + STATIC_CHECK(dimensions::PhysicalQuantityType); + STATIC_CHECK(dimensions::PhysicalQuantityType); + STATIC_CHECK(dimensions::ThermodynamicQuantityType); + STATIC_CHECK(dimensions::ThermodynamicQuantityType); + STATIC_CHECK(dimensions::ThermodynamicQuantityType); + STATIC_CHECK_FALSE(dimensions::ThermodynamicQuantityType); + + STATIC_CHECK(dimensions::QuantityValueType); + STATIC_CHECK(dimensions::QuantityValueType); + STATIC_CHECK(std::same_as, dimensions::quantity::Mass>); + STATIC_CHECK( + std::same_as, dimensions::quantity::AngularMomentum> + ); + + STATIC_CHECK(std::constructible_from); + STATIC_CHECK_FALSE(std::convertible_to); + STATIC_CHECK_FALSE(std::constructible_from); + STATIC_CHECK_FALSE(Addable); + STATIC_CHECK_FALSE(EqualityComparable); + + constexpr dimensions::MassValue mass{2.0}; + constexpr dimensions::MassValue correction{0.5}; + STATIC_CHECK((mass + correction).value() == 2.5); + STATIC_CHECK((mass - correction).value() == 1.5); + STATIC_CHECK((3.0 * mass).value() == 6.0); + STATIC_CHECK((mass / 4.0).value() == 0.5); + STATIC_CHECK(mass > correction); +} + +TEST_CASE( + "Dimensions Partition Provides A Broad Stellar Physics Catalog", + tags::dimensional_quantities +) { + using namespace mean_field::dimensions; + + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + STATIC_CHECK(QuantityValueType); + + STATIC_CHECK(quantity::Mass::identifier == std::string_view{"mass"}); + STATIC_CHECK(quantity::AngularMomentum::identifier == std::string_view{"angular_momentum"}); + STATIC_CHECK(quantity::SpecificEnthalpy::identifier == std::string_view{"specific_enthalpy"}); +} + +TEST_CASE( + "EOS Quantity Names Are Exact Transitional Aliases Of Dimensions Types", + tags::dimensional_quantities +) { + using namespace mean_field; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(eos::ThermodynamicQuantityType); + STATIC_CHECK(eos::QuantityValueType); +} diff --git a/tests/seed/lane_emden.cpp b/tests/seed/lane_emden.cpp new file mode 100644 index 0000000..3305560 --- /dev/null +++ b/tests/seed/lane_emden.cpp @@ -0,0 +1,237 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +import mean_field; +import test_helpers; + +namespace { + template < + typename AnalyticValue, + typename AnalyticDerivative> + void check_dimensionless_solution( + const mean_field::seed::DimensionlessLaneEmdenSolution &solution, + AnalyticValue analyticValue, + AnalyticDerivative analyticDerivative, + const double tolerance + ) { + REQUIRE(solution.coordinate.Size() >= 2); + REQUIRE(solution.theta.Size() == solution.coordinate.Size()); + REQUIRE(solution.thetaDerivative.Size() == solution.coordinate.Size()); + + double maximumValueError = 0.0; + double maximumDerivativeError = 0.0; + for (int index = 0; index < solution.coordinate.Size(); ++index) { + const double coordinate = solution.coordinate(index); + CHECK(std::isfinite(coordinate)); + CHECK(std::isfinite(solution.theta(index))); + CHECK(std::isfinite(solution.thetaDerivative(index))); + if (index > 0) { + CHECK(coordinate > solution.coordinate(index - 1)); + } + + maximumValueError = + std::max(maximumValueError, std::abs(solution.theta(index) - analyticValue(coordinate))); + maximumDerivativeError = std::max( + maximumDerivativeError, std::abs(solution.thetaDerivative(index) - analyticDerivative(coordinate)) + ); + } + + CHECK(maximumValueError < tolerance); + CHECK(maximumDerivativeError < tolerance); + } + + void check_profiles_are_identical( + const mean_field::seed::RadialProfile &left, + const mean_field::seed::RadialProfile &right + ) { + REQUIRE(left.radius.Size() == right.radius.Size()); + REQUIRE(left.density.Size() == right.density.Size()); + REQUIRE(left.specificEnthalpy.Size() == right.specificEnthalpy.Size()); + + for (int index = 0; index < left.radius.Size(); ++index) { + CHECK(left.radius(index) == right.radius(index)); + CHECK(left.density(index) == right.density(index)); + CHECK(left.specificEnthalpy(index) == right.specificEnthalpy(index)); + } + + CHECK(left.stellarRadius == right.stellarRadius); + CHECK(left.centralDensity == right.centralDensity); + CHECK(left.centralSpecificEnthalpy == right.centralSpecificEnthalpy); + } +} // namespace + +TEST_CASE( + "Lane Emden Integration Matches The Analytic Incompressible Solution", + tags::lane_emden_analytic +) { + using Catch::Approx; + + const mean_field::seed::DimensionlessLaneEmdenSolution solution = mean_field::seed::integrateLaneEmden(0.0, 3.0); + + REQUIRE(solution.firstZeroCoordinate.has_value()); + CHECK(*solution.firstZeroCoordinate == Approx(std::sqrt(6.0)).margin(2.0e-7)); + CHECK(solution.theta(solution.theta.Size() - 1) == 0.0); + check_dimensionless_solution( + solution, [](const double coordinate) { return 1.0 - coordinate * coordinate / 6.0; }, + [](const double coordinate) { return -coordinate / 3.0; }, 2.0e-7 + ); +} + +TEST_CASE( + "Lane Emden Integration Matches The Analytic Index One Solution", + tags::lane_emden_analytic +) { + using Catch::Approx; + + const mean_field::seed::DimensionlessLaneEmdenSolution solution = mean_field::seed::integrateLaneEmden(1.0, 4.0); + + REQUIRE(solution.firstZeroCoordinate.has_value()); + CHECK(*solution.firstZeroCoordinate == Approx(std::numbers::pi_v).margin(2.0e-7)); + CHECK(solution.theta(solution.theta.Size() - 1) == 0.0); + check_dimensionless_solution( + solution, [](const double coordinate) { return coordinate == 0.0 ? 1.0 : std::sin(coordinate) / coordinate; }, + [](const double coordinate) { + if (coordinate == 0.0) { + return 0.0; + } + if (coordinate < 1.0e-4) { + return -coordinate / 3.0 + coordinate * coordinate * coordinate / 30.0; + } + return (coordinate * std::cos(coordinate) - std::sin(coordinate)) / (coordinate * coordinate); + }, + 2.0e-7 + ); +} + +TEST_CASE( + "Lane Emden Integration Matches The Analytic Index Five Infinite Solution", + tags::lane_emden_analytic +) { + using Catch::Approx; + + constexpr double coordinateLimit = 20.0; + const mean_field::seed::DimensionlessLaneEmdenSolution solution = + mean_field::seed::integrateLaneEmden(5.0, coordinateLimit); + + CHECK_FALSE(solution.firstZeroCoordinate.has_value()); + CHECK(solution.coordinate(solution.coordinate.Size() - 1) == Approx(coordinateLimit)); + CHECK(solution.theta(solution.theta.Size() - 1) > 0.0); + check_dimensionless_solution( + solution, [](const double coordinate) { return 1.0 / std::sqrt(1.0 + coordinate * coordinate / 3.0); }, + [](const double coordinate) { return -coordinate / 3.0 * std::pow(1.0 + coordinate * coordinate / 3.0, -1.5); }, + 2.0e-7 + ); +} + +TEST_CASE( + "Lane Emden Seed Uses The Stellar Model Central Density Phase Condition", + tags::lane_emden_seed +) { + using namespace mean_field; + using Catch::Approx; + + const auto stellarModel = model::StellarModel( + eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}), + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{2.0}}) + ); + const seed::LaneEmden strategy({.radialSampleCount = 64}); + + STATIC_CHECK(seed::RadialSeedStrategyFor); + + const seed::RadialProfile profile = seed::generateRadialProfile(stellarModel, strategy); + REQUIRE(profile.radius.Size() == 64); + REQUIRE(profile.density.Size() == 64); + REQUIRE(profile.specificEnthalpy.Size() == 64); + CHECK(profile.centralDensity == dimensions::DensityValue{2.0}); + CHECK(profile.centralSpecificEnthalpy.value() == Approx(std::cbrt(2.0))); + CHECK(profile.radius(0) == 0.0); + CHECK(profile.radius(63) == profile.stellarRadius.value()); + CHECK(profile.density(0) == 2.0); + CHECK(profile.density(63) == 0.0); + CHECK(profile.specificEnthalpy(0) == profile.centralSpecificEnthalpy.value()); + CHECK(profile.specificEnthalpy(63) == 0.0); + + for (int index = 1; index < profile.radius.Size(); ++index) { + CHECK(profile.radius(index) > profile.radius(index - 1)); + CHECK(profile.density(index) <= profile.density(index - 1)); + CHECK(profile.specificEnthalpy(index) <= profile.specificEnthalpy(index - 1)); + CHECK(profile.density(index) >= 0.0); + CHECK(profile.specificEnthalpy(index) >= 0.0); + } +} + +TEST_CASE( + "Explicit Lane Emden Seed Density Is Independent Of Model Invariants", + tags::lane_emden_seed +) { + using namespace mean_field; + + const auto unitMassModel = model::StellarModel( + eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}) + ); + const auto largeMassModel = model::StellarModel( + eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{9.0}}) + ); + const seed::LaneEmden strategy({.centralDensity = dimensions::DensityValue{1.7}, .radialSampleCount = 48}); + + const seed::RadialProfile unitMassProfile = seed::generateRadialProfile(unitMassModel, strategy); + const seed::RadialProfile largeMassProfile = seed::generateRadialProfile(largeMassModel, strategy); + check_profiles_are_identical(unitMassProfile, largeMassProfile); + + CHECK_THROWS_AS( + seed::generateRadialProfile(unitMassModel, seed::LaneEmden({.radialSampleCount = 48})), std::invalid_argument + ); +} + +TEST_CASE( + "Legacy Polytropic Structure Seed Is An Exact Adapter Over Lane Emden Generation", + tags::lane_emden_seed +) { + using namespace mean_field; + + const eos::Polytrope equationOfState({.n = 3.0, .K = 0.25}); + const seed::RadialProfile profile = + seed::generateLaneEmdenProfile(equationOfState, dimensions::DensityValue{1.25}, 40); + const models::structure::StructureSeed legacySeed = + models::structure::PolytropicStructure{equationOfState, 7.0}.makeInitialSeed( + {.centralDensity = 1.25, .radialSampleCount = 40} + ); + + REQUIRE(legacySeed.radius.Size() == profile.radius.Size()); + for (int index = 0; index < profile.radius.Size(); ++index) { + CHECK(legacySeed.radius(index) == profile.radius(index)); + CHECK(legacySeed.density(index) == profile.density(index)); + CHECK(legacySeed.enthalpy(index) == profile.specificEnthalpy(index)); + } + CHECK(legacySeed.stellarRadius == profile.stellarRadius.value()); + CHECK(legacySeed.centralDensity == profile.centralDensity.value()); + CHECK(legacySeed.centralEnthalpy == profile.centralSpecificEnthalpy.value()); +} + +TEST_CASE( + "Lane Emden Seed Rejects Invalid Numerical Prescriptions", + tags::lane_emden_seed +) { + using namespace mean_field; + + CHECK_THROWS_AS(seed::LaneEmden({.radialSampleCount = 1}), std::invalid_argument); + CHECK_THROWS_AS(seed::LaneEmden({.centralDensity = dimensions::DensityValue{0.0}}), std::invalid_argument); + CHECK_THROWS_AS( + seed::LaneEmden({.centralDensity = dimensions::DensityValue{std::numeric_limits::infinity()}}), + std::invalid_argument + ); + CHECK_THROWS_AS( + seed::generateLaneEmdenProfile(eos::Polytrope({.n = 5.0, .K = 0.25}), dimensions::DensityValue{1.0}, 8), + std::invalid_argument + ); +} diff --git a/tests/seed/stellar_equilibrium_projection.cpp b/tests/seed/stellar_equilibrium_projection.cpp new file mode 100644 index 0000000..20dd2d1 --- /dev/null +++ b/tests/seed/stellar_equilibrium_projection.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +import mean_field; +import test_helpers; + +namespace { + [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() { + return { + .discretization = {.identity = 7001, .revision = 1}, + .density = {.identity = 7003, .revision = 1}, + .surfaceDeformation = {.identity = 7009, .revision = 1}, + .gravityGradient = {.identity = 7013, .revision = 1}, + .gravityPotential = {.identity = 7019, .revision = 1}, + .enthalpy = {.identity = 7027, .revision = 1}, + .bernoulliConstant = {.identity = 7039, .revision = 1}, + .rotation = {.identity = 7043, .revision = 1}, + .targetMass = {.identity = 7057, .revision = 1} + }; + } + + [[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() { + mfem::Vector angularVelocity(3); + mfem::Vector center(3); + angularVelocity = 0.0; + center = 0.0; + return {angularVelocity, center}; + } + + template void check_finite(const Vector &values) { + for (int index = 0; index < values.Size(); ++index) { + REQUIRE(std::isfinite(values(index))); + } + } +} // namespace + +TEST_CASE( + "Projected Equilibrium States Preserve Their Compiled Stellar Model Type", + tags::stellar_seed_projection_type_contract +) { + using namespace mean_field; + using BaseModel = + model::StellarModel>; + using CentralDensityModel = model::StellarModel>; + using BaseState = seed::ProjectedEquilibriumState; + using CentralDensityState = seed::ProjectedEquilibriumState; + + STATIC_CHECK_FALSE(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); +} + +TEST_CASE( + "Lane Emden Projection Builds A Complete Compiled Stellar Equilibrium State", + tags::stellar_seed_projection +) { + using namespace mean_field; + using Catch::Approx; + + utils::Args args = test_utils::setup_args(); + fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(finiteElementModel.okay()); + + constexpr double stellarRadius = utils::RADIUS; + constexpr double targetMass = utils::MASS; + const double polytropicConstant = 2.0 * utils::G * stellarRadius * stellarRadius / std::numbers::pi_v; + const double centralDensity = + std::numbers::pi_v * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius); + + const auto stellarModel = model::StellarModel( + eos::Polytrope({.n = 1.0, .K = polytropicConstant}), + surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}), + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}}) + ); + auto problem = equilibrium::discretize(stellarModel, finiteElementModel); + + STATIC_CHECK(seed::RadialSeedStrategyFor); + STATIC_CHECK( + std::same_as< + decltype(seed::makeProjectedEquilibriumState(problem, seed::LaneEmden{})), + seed::ProjectedEquilibriumState::ModelType>> + ); + + const auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 4096})); + REQUIRE(projected.values.Size() == problem.StateSize()); + check_finite(projected.values); + + const auto stateView = problem.GetManifest().stateView(projected.values); + const mfem::Vector density = stateView.block(utils::blocks::density_field.mass_term); + const mfem::Vector surface = stateView.block(utils::blocks::surface_deformation_field.parameters_term); + const mfem::Vector gravityGradient = stateView.block(utils::blocks::gravity_field.gradient_term); + const mfem::Vector gravityPotential = stateView.block(utils::blocks::gravity_field.poisson_term); + const mfem::Vector enthalpy = stateView.block(utils::blocks::enthalpy_field.specific_term); + const mfem::Vector fixedMassCoordinate = + stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term); + const mfem::Vector centralDensityBorder = + stateView.block(utils::blocks::fixed_central_density_phase.central_value_term); + + CHECK(density.Norml2() > 0.0); + CHECK(gravityGradient.Norml2() > 0.0); + CHECK(gravityPotential.Norml2() > 0.0); + CHECK(enthalpy.Norml2() > 0.0); + CHECK(surface.Normlinf() == 0.0); + REQUIRE(fixedMassCoordinate.Size() == 1); + CHECK(fixedMassCoordinate(0) == Approx(-utils::G * targetMass / stellarRadius).margin(2.0e-7)); + REQUIRE(centralDensityBorder.Size() == 1); + CHECK(centralDensityBorder(0) == 0.0); + + const operators::PreparedCentralDensityStellarEquilibriumReport preparation = + problem.Prepare(projected.values, make_dependencies(), make_zero_rotation()); + CHECK(preparation.assembledResidual); + + mfem::Vector residual; + problem.BuildResidual(residual); + REQUIRE(residual.Size() == problem.EquationSize()); + check_finite(residual); + + const operators::RootConstraintReport massReport = problem.GetPreparedOperator().GetFixedMassReport(); + CHECK(std::abs(massReport.scaledResidual) < 5.0e-4); + const operators::CentralDensityConstraintReport centralDensityReport = + problem.GetPreparedOperator().GetCentralDensityReport(); + CHECK(centralDensityReport.targetDensity == Approx(centralDensity)); + CHECK(std::abs(centralDensityReport.enthalpyResidual) < 1.0e-10); + + const auto residualView = problem.GetManifest().residualView(residual); + const mfem::Vector enthalpyResidual = residualView.block(utils::blocks::enthalpy_field.specific_term); + const auto &surfaceRows = problem.GetPressureSurfaceRows(); + for (const int surfaceRow : surfaceRows.reduced_dofs()) { + CHECK(enthalpy(surfaceRow) == 0.0); + CHECK(enthalpyResidual(surfaceRow) == 0.0); + } +} + +TEST_CASE( + "Lane Emden Projection Rejects A Seed Whose Surface Does Not Match The Reference Discretization", + tags::stellar_seed_projection +) { + using namespace mean_field; + + utils::Args args = test_utils::setup_args(); + fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(finiteElementModel.okay()); + + const auto stellarModel = model::StellarModel( + eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}), + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}}) + ); + auto problem = equilibrium::discretize(stellarModel, finiteElementModel); + + const seed::RadialProfile mismatchedProfile = + seed::generateRadialProfile(problem.GetStellarModel(), seed::LaneEmden({.radialSampleCount = 64})); + CHECK_THROWS_AS(seed::projectRadialProfile(problem, mismatchedProfile), std::invalid_argument); +} + +TEST_CASE( + "Lane Emden Projection Rejects A Nonzero Isobaric Surface", + tags::stellar_seed_projection +) { + using namespace mean_field; + + utils::Args args = test_utils::setup_args(); + fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0); + REQUIRE(finiteElementModel.okay()); + + const double polytropicConstant = 2.0 * utils::G / std::numbers::pi_v; + const double centralDensity = std::numbers::pi_v / 4.0; + const auto stellarModel = model::StellarModel( + eos::Polytrope({.n = 1.0, .K = polytropicConstant}), + surface::Isobaric({.Psurf = dimensions::PressureValue{0.01}}), + integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}), + constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}}) + ); + auto problem = equilibrium::discretize(stellarModel, finiteElementModel); + const seed::RadialProfile profile = + seed::generateRadialProfile(problem.GetStellarModel(), seed::LaneEmden({.radialSampleCount = 64})); + + CHECK_THROWS_AS(seed::projectRadialProfile(problem, profile), std::invalid_argument); +} diff --git a/tests/solver/preconditioning_diagnostics.cpp b/tests/solver/preconditioning_diagnostics.cpp new file mode 100644 index 0000000..8e9af92 --- /dev/null +++ b/tests/solver/preconditioning_diagnostics.cpp @@ -0,0 +1,282 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +import mean_field; +import test_helpers; + +namespace { + class DenseLinearOperator final : public mfem::Operator { + public: + explicit DenseLinearOperator(mfem::DenseMatrix matrix) + : mfem::Operator( + matrix.Height(), + matrix.Width() + ), + m_matrix(std::move(matrix)) { + } + + void Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const override { + m_matrix.Mult(input, output); + } + + private: + mfem::DenseMatrix m_matrix; + }; + + class DiagonalInversePreconditioner final : public mfem::Solver { + public: + explicit DiagonalInversePreconditioner(mfem::Vector diagonal) + : mfem::Solver(diagonal.Size()), + m_diagonal(std::move(diagonal)) { + } + + void SetOperator(const mfem::Operator &operation) override { + REQUIRE(operation.Height() == Height()); + REQUIRE(operation.Width() == Width()); + } + + void Mult( + const mfem::Vector &input, + mfem::Vector &output + ) const override { + REQUIRE(input.Size() == Width()); + output.SetSize(Height()); + for (int index = 0; index < Height(); ++index) { + output(index) = input(index) / m_diagonal(index); + } + } + + private: + mfem::Vector m_diagonal; + }; + + [[nodiscard]] mfem::DenseMatrix diagonal_matrix( + const std::array< + double, + 4> &diagonal + ) { + mfem::DenseMatrix matrix(4); + matrix = 0.0; + for (int index = 0; index < 4; ++index) { + matrix(index, index) = diagonal[static_cast(index)]; + } + return matrix; + } + + [[nodiscard]] mean_field::operators::RootBlockDescriptor residual_block( + const std::string_view stableId, + const int index, + const int offset, + const int size + ) { + using namespace mean_field::operators; + return { + .stableId = stableId, + .symbol = stableId, + .kind = RootBlockKind::residual, + .provenance = RootBlockProvenance::physical_operator, + .source = "test", + .rowInjection = RootRowInjection::physical_equation, + .columnPolicy = RootColumnPolicy::no_column, + .scalePolicy = RootScalePolicy::unscaled, + .canonicalIndex = index, + .offset = offset, + .size = size, + .scale = 1.0 + }; + } + + [[nodiscard]] bool contains_eigenvalue( + const mean_field::solver::ArnoldiSpectralMeasurement &measurement, + const double realPart, + const double imaginaryPart, + const double tolerance + ) { + for (const auto &value : measurement.ritzValues) { + if (std::hypot(value.realPart - realPart, value.imaginaryPart - imaginaryPart) < tolerance) { + return true; + } + } + return false; + } +} // namespace + +TEST_CASE( + "Preconditioning Instrumentation Counts Work And Independently Measures The True Residual", + tags::preconditioning_diagnostics_unit +) { + using Catch::Approx; + using namespace mean_field; + + constexpr std::array diagonalValues{2.0, 4.0, 8.0, 16.0}; + DenseLinearOperator jacobian(diagonal_matrix(diagonalValues)); + mfem::Vector diagonal(4); + for (int index = 0; index < 4; ++index) { + diagonal(index) = diagonalValues[static_cast(index)]; + } + DiagonalInversePreconditioner inversePreconditioner(std::move(diagonal)); + + solver::InstrumentedOperator instrumentedJacobian(jacobian); + solver::InstrumentedPreconditioner instrumentedPreconditioner(inversePreconditioner); + solver::FixedRightPreconditionedOperator rightPreconditioned(instrumentedJacobian, instrumentedPreconditioner); + + mfem::Vector input({1.0, -2.0, 3.0, -4.0}); + mfem::Vector product(rightPreconditioned.Height()); + rightPreconditioned.Mult(input, product); + REQUIRE(product.Size() == input.Size()); + for (int index = 0; index < input.Size(); ++index) { + CHECK(product(index) == Approx(input(index))); + } + CHECK(instrumentedJacobian.GetStatistics().applications == 1); + CHECK(instrumentedPreconditioner.GetStatistics().applications == 1); + CHECK(instrumentedJacobian.GetStatistics().totalSeconds >= 0.0); + CHECK(instrumentedPreconditioner.GetStatistics().totalSeconds >= 0.0); + + instrumentedJacobian.ResetStatistics(); + instrumentedPreconditioner.ResetStatistics(); + + mfem::Vector exactSolution({0.25, -0.5, 0.75, -1.0}); + mfem::Vector rightHandSide(jacobian.Height()); + jacobian.Mult(exactSolution, rightHandSide); + mfem::Vector computedSolution(4); + computedSolution = 0.0; + + solver::ResidualHistoryMonitor monitor; + mfem::FGMRESSolver krylov(MPI_COMM_WORLD); + krylov.SetPreconditioner(instrumentedPreconditioner); + krylov.SetOperator(instrumentedJacobian); + krylov.SetMonitor(monitor); + krylov.SetRelTol(1.0e-13); + krylov.SetAbsTol(1.0e-15); + krylov.SetMaxIter(20); + krylov.SetKDim(10); + krylov.SetPrintLevel(0); + + const auto start = std::chrono::steady_clock::now(); + krylov.Mult(rightHandSide, computedSolution); + const double elapsed = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + + const std::array residualBlocks{residual_block("first", 0, 0, 2), residual_block("second", 1, 2, 2)}; + const solver::LinearSolveMeasurement measurement = solver::measureLinearSolve( + krylov, jacobian, rightHandSide, computedSolution, residualBlocks, instrumentedJacobian.GetStatistics(), + instrumentedPreconditioner.GetStatistics(), instrumentedPreconditioner.GetLifecycleStatistics(), monitor, + elapsed, MPI_COMM_WORLD + ); + + CHECK(measurement.solverConverged); + CHECK(measurement.outerIterations > 0); + CHECK(measurement.jacobian.applications > 0); + CHECK(measurement.inversePreconditioner.applications > 0); + CHECK(measurement.inversePreconditionerLifecycle.setups > 0); + CHECK(measurement.solveSecondsMaximumRank >= 0.0); + CHECK(measurement.solverReportedResidualReduction < 1.0e-12); + CHECK(measurement.trueResidualDigitsReducedPerJacobianApplication > 0.0); + CHECK(measurement.directResidual.relativeResidual < 1.0e-12); + REQUIRE(measurement.directResidual.blocks.size() == 2); + CHECK(measurement.directResidual.blocks[0].stableId == "first"); + CHECK(measurement.directResidual.blocks[1].stableId == "second"); + CHECK(measurement.directResidual.blocks[0].descriptorScale == 1.0); + CHECK(measurement.directResidual.blocks[0].blockRelativeResidual < 1.0e-12); + CHECK(measurement.directResidual.blocks[1].blockRelativeResidual < 1.0e-12); + CHECK(measurement.directResidual.blocks[0].fractionOfGlobalSquaredResidualNorm >= 0.0); + CHECK(measurement.directResidual.blocks[1].fractionOfGlobalSquaredResidualNorm >= 0.0); + CHECK_FALSE(measurement.reportedResidualHistory.empty()); +} + +TEST_CASE( + "Arnoldi Diagnostics Recover Real And Complex Conjugate Eigenvalue Clusters", + tags::preconditioning_spectral_unit +) { + using Catch::Approx; + using namespace mean_field; + + mfem::DenseMatrix matrix(4); + matrix = 0.0; + matrix(0, 0) = 2.0; + matrix(1, 1) = 3.0; + matrix(2, 3) = -1.0; + matrix(3, 2) = 1.0; + DenseLinearOperator operation(std::move(matrix)); + + const mfem::Vector initialDirection({1.0, 2.0, 3.0, 4.0}); + const solver::ArnoldiSpectralMeasurement measurement = solver::measureArnoldiSpectrum( + operation, initialDirection, MPI_COMM_WORLD, + {.krylovDimension = 4, + .breakdownRelativeTolerance = 1.0e-12, + .ritzConvergenceRelativeTolerance = 1.0e-9, + .reorthogonalize = true} + ); + + REQUIRE(measurement.achievedDimension == 4); + REQUIRE(measurement.ritzValues.size() == 4); + CHECK(measurement.operatorApplications == 4); + CHECK(measurement.operatorApplicationSecondsMaximumRank >= 0.0); + CHECK(measurement.operatorMaximumApplicationSecondsMaximumRank >= 0.0); + CHECK(measurement.measurementSecondsMaximumRank >= measurement.operatorApplicationSecondsMaximumRank); + CHECK(measurement.nonApplicationSecondsMaximumRank >= 0.0); + CHECK(measurement.invariantSubspaceFound); + CHECK(contains_eigenvalue(measurement, 2.0, 0.0, 1.0e-10)); + CHECK(contains_eigenvalue(measurement, 3.0, 0.0, 1.0e-10)); + CHECK(contains_eigenvalue(measurement, 0.0, 1.0, 1.0e-10)); + CHECK(contains_eigenvalue(measurement, 0.0, -1.0, 1.0e-10)); + CHECK(measurement.conjugatePairDefect < 1.0e-10); + CHECK(measurement.projectedLargestSingularValue == Approx(3.0).margin(1.0e-10)); + CHECK(measurement.projectedSmallestSingularValue == Approx(1.0).margin(1.0e-10)); + CHECK(measurement.projectedConditionProxy == Approx(3.0).margin(1.0e-10)); + CHECK(measurement.maximumAbsoluteImaginaryPart == Approx(1.0).margin(1.0e-10)); +} + +TEST_CASE( + "Arnoldi Diagnostics Distinguish Exact Preconditioning From Nonnormal Clustering", + tags::preconditioning_spectral_unit +) { + using Catch::Approx; + using namespace mean_field; + + DenseLinearOperator jacobian(diagonal_matrix({2.0, 4.0, 8.0, 16.0})); + mfem::Vector diagonal({2.0, 4.0, 8.0, 16.0}); + DiagonalInversePreconditioner inversePreconditioner(std::move(diagonal)); + solver::FixedRightPreconditionedOperator exactProduct(jacobian, inversePreconditioner); + const mfem::Vector initialDirection({1.0, -1.0, 2.0, -2.0}); + + const solver::ArnoldiSpectralMeasurement exact = solver::measureArnoldiSpectrum( + exactProduct, initialDirection, MPI_COMM_WORLD, {.krylovDimension = 4, .breakdownRelativeTolerance = 1.0e-12} + ); + REQUIRE(exact.achievedDimension == 1); + REQUIRE(exact.ritzValues.size() == 1); + CHECK(exact.ritzValues[0].realPart == Approx(1.0).margin(1.0e-12)); + CHECK(exact.ritzValues[0].imaginaryPart == Approx(0.0).margin(1.0e-12)); + CHECK(exact.projectedConditionProxy == Approx(1.0).margin(1.0e-12)); + CHECK(exact.rmsDistanceFromOne < 1.0e-12); + + mfem::DenseMatrix jordan(4); + jordan = 0.0; + for (int index = 0; index < 4; ++index) { + jordan(index, index) = 1.0; + } + jordan(0, 1) = 4.0; + jordan(1, 2) = 4.0; + jordan(2, 3) = 4.0; + DenseLinearOperator nonnormal(std::move(jordan)); + const solver::ArnoldiSpectralMeasurement nonnormalMeasurement = solver::measureArnoldiSpectrum( + nonnormal, mfem::Vector({1.0, 2.0, 3.0, 5.0}), MPI_COMM_WORLD, + {.krylovDimension = 4, .breakdownRelativeTolerance = 1.0e-12} + ); + CHECK(nonnormalMeasurement.projectedDepartureFromNormality > 0.1); + CHECK(nonnormalMeasurement.projectedConditionProxy > 1.0); + + const std::vector closest = + solver::selectRitzValues(nonnormalMeasurement, solver::RitzValueOrdering::closest_to_zero, 2); + CHECK(closest.size() <= 2); +} diff --git a/tests/test_helpers.cppm b/tests/test_helpers.cppm index c2a74ff..547132d 100644 --- a/tests/test_helpers.cppm +++ b/tests/test_helpers.cppm @@ -443,6 +443,7 @@ export namespace tags { inline constexpr auto equation_of_state = physics & make_tag("eos"); inline constexpr auto equation_of_state_type_system = equation_of_state & unit & make_tag("type_system"); inline constexpr auto equation_of_state_quantity_types = equation_of_state_type_system & make_tag("quantity_types"); + inline constexpr auto dimensional_quantities = physics & unit & make_tag("dimensions") & make_tag("quantity_types"); inline constexpr auto equation_of_state_relation_contract = equation_of_state_type_system & make_tag("relation_contract"); inline constexpr auto equation_of_state_runtime_view = equation_of_state & unit & make_tag("runtime_view"); @@ -458,7 +459,32 @@ 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_type_contract = barotrope & model & unit & make_tag("type_contract"); + inline constexpr auto model_specification_type_contract = + model & unit & make_tag("specification") & make_tag("type_contract"); + inline constexpr auto stellar_model_specification_api = + model_specification_type_contract & make_tag("stellar_model_api"); + inline constexpr auto stellar_equilibrium_system = model & solver & make_tag("stellar_equilibrium_system"); + inline constexpr auto stellar_equilibrium_system_type_contract = + stellar_equilibrium_system & unit & make_tag("type_contract"); + inline constexpr auto stellar_equilibrium_system_integration = stellar_equilibrium_system & integration; + inline constexpr auto stellar_equilibrium_problem = model & solver & make_tag("stellar_equilibrium_problem"); + inline constexpr auto stellar_equilibrium_problem_type_contract = + stellar_equilibrium_problem & unit & make_tag("type_contract"); + inline constexpr auto stellar_equilibrium_problem_integration = stellar_equilibrium_problem & integration; + inline constexpr auto lane_emden_seed = model & initialization & physics & make_tag("lane_emden"); + inline constexpr auto lane_emden_analytic = lane_emden_seed & accuracy & make_tag("analytic_solution"); + inline constexpr auto stellar_seed_projection = model & initialization & solver & make_tag("seed_projection"); + inline constexpr auto stellar_seed_projection_type_contract = + stellar_seed_projection & unit & make_tag("type_contract"); + inline constexpr auto preconditioning_diagnostics = solver & make_tag("preconditioning") & make_tag("diagnostics"); + inline constexpr auto preconditioning_diagnostics_unit = preconditioning_diagnostics & unit; + inline constexpr auto preconditioning_spectral_unit = preconditioning_diagnostics_unit & make_tag("spectrum"); + inline constexpr auto root_manifest_type_contract = + model & solver & unit & make_tag("root_manifest") & make_tag("type_contract"); + inline constexpr auto central_density_phase = barotrope & solver & make_tag("central_density") & make_tag("phase"); + inline constexpr auto central_density_phase_unit = central_density_phase & unit; + inline constexpr auto central_density_phase_integration = central_density_phase & integration; 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 = @@ -510,6 +536,8 @@ export namespace tags { barotrope_mass_normalization_prepared & integration & make_tag("jacobian"); inline constexpr auto barotrope_mass_normalization_analytic = barotrope_mass_normalization_prepared & integration & make_tag("analytic_comparison"); + inline constexpr auto fixed_total_mass_constraint = + barotrope_mass_normalization_prepared & integration & make_tag("fixed_total_mass") & make_tag("constraint"); inline constexpr auto rotation_prepared = centrifugal & make_tag("prepared"); inline constexpr auto rotation_context = centrifugal & make_tag("context");