Compare commits

...

10 Commits

Author SHA1 Message Date
75cc638739 perf(allocations): reduced overall allocations by 95%, increaseed jacobian applicatin by 2x
This commit uses global pre allocated work space to dramatically reduce memory usage and allocation time
2026-09-10 06:50:56 -04:00
b3c04d507a feat(newton): first newton solver implementation 2026-09-08 06:36:39 -04:00
76818f2f82 feat(libmeanfield): variadic refactor
also added normaliztion operator
2026-09-06 10:15:00 -04:00
71423d543f feat(preconditioner): major work on preconditioner system
first preconditioner MVP
2026-09-04 07:54:10 -04:00
25510008dd perf(jacobian-action): major updates to jacobian action application by removing redudant quadrature work. ~5x increase in speed 2026-09-02 17:01:50 -04:00
85500fef3b feat(surface): surface deformation prescriptions
restricted the unknown state vector to surface deformation and implemented one prescription, NodalRadialSurface, while the full volumetric displacment field is reconstructed analytically from that. This reduced the number of degrees of freedom in the system by a factor of 80 while also removing many null vectors from the system.
2026-09-01 11:50:13 -04:00
0a7f18c5c7 feat(surface): major work on implementing surface constraints in a presciption agnostic manner 2026-08-30 16:41:14 -04:00
36adfa1174 feat(FieldDofMap): Completed FieldDofMap migration
also removed legacy BarotropicPolytrope implementation
2026-08-29 08:56:36 -04:00
177ae8b38a feat(support): continued migration to new field dof support system
mass normaliztion, gravity, displacement, and hydrostatic equilibrium are now migrated
2026-08-24 15:44:24 -04:00
0f3ca8050b feat(field-support): added field support system, mid migration
currently the barotope and the pressure force operator are migrated to the new support system
2026-08-23 10:13:53 -04:00
310 changed files with 315191 additions and 120299 deletions

View File

@@ -1,10 +1,19 @@
cmake_minimum_required(VERSION 3.28)
project(MeanField CXX)
project(MeanField C CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
option(MEAN_FIELD_ENABLE_PROFILING "Enable low-overhead scoped profiling instrumentation" OFF)
option(MEAN_FIELD_ENABLE_IPO "Enable interprocedural optimization in release builds" ON)
set(MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT 0 CACHE STRING
"Uniform increment applied to every registered finite-element family order")
if (NOT MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT MATCHES "^[0-9]+$")
message(FATAL_ERROR "MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT must be a non-negative integer")
endif ()
add_compile_options(
-gdwarf-4
-Wno-unused-parameter
@@ -31,9 +40,16 @@ find_package(PkgConfig REQUIRED)
pkg_check_modules(stroid REQUIRED IMPORTED_TARGET stroid)
pkg_check_modules(eigen3 REQUIRED IMPORTED_TARGET eigen3)
add_library(mean_field)
target_compile_definitions(mean_field
PUBLIC
MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT=${MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT}
MEAN_FIELD_ENABLE_PROFILING=$<BOOL:${MEAN_FIELD_ENABLE_PROFILING}>
)
target_include_directories(mean_field
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/libmeanfield/include>
@@ -41,10 +57,12 @@ target_include_directories(mean_field
target_sources(mean_field
PRIVATE
libmeanfield/impl/profile.cpp
libmeanfield/impl/analysis/integral.cpp
libmeanfield/impl/fem.cpp
libmeanfield/impl/fem/reference_tables.cpp
libmeanfield/impl/mapping/prepared_cache.cpp
libmeanfield/impl/mapping/coefficients.cpp
libmeanfield/impl/mapping/domain_mapper.cpp
libmeanfield/impl/mapping/compactification/kelvin.cpp
libmeanfield/impl/physics/gravity.cpp
libmeanfield/impl/physics/solid.cpp
@@ -56,8 +74,11 @@ target_sources(mean_field
libmeanfield/impl/integrators/gravity.cpp
libmeanfield/impl/integrators/mass_continuity.cpp
libmeanfield/impl/integrators/viscosity.cpp
libmeanfield/impl/mapping/domain_mapper_new.cpp
libmeanfield/impl/mapping/domain_mapper.cpp
libmeanfield/impl/mapping/transformations.cpp
libmeanfield/impl/deformation/nodal_radial_surface.cpp
libmeanfield/impl/deformation/radial_extensions.cpp
libmeanfield/impl/deformation/safe_newton_step.cpp
libmeanfield/impl/operators/gravity_field.cpp
libmeanfield/impl/operators/gravity_field_jacobian.cpp
libmeanfield/impl/operators/kernels/gravity_kernels.cpp
@@ -71,13 +92,40 @@ target_sources(mean_field
libmeanfield/impl/operators/contexts/hydrostatic_equilibrium_context.cpp
libmeanfield/impl/operators/prepared_hydrostatic_equilibrium.cpp
libmeanfield/impl/operators/kernels/pressure_force_kernels.cpp
libmeanfield/impl/operators/contexts/pressure_force_context.cpp
libmeanfield/impl/operators/prepared_pressure_force.cpp
libmeanfield/impl/operators/kernels/gravity_displacement_force_kernels.cpp
libmeanfield/impl/operators/prepared_gravity_displacement_force.cpp
libmeanfield/impl/operators/contexts/rotation_displacement_force_context.cpp
libmeanfield/impl/operators/kernels/rotation_displacement_force_kernels.cpp
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/preconditioning/gravity_field.cpp
libmeanfield/impl/operators/prepared_mass_normalization.cpp
libmeanfield/impl/operators/prepared_angular_momentum.cpp
libmeanfield/impl/operators/prepared_stellar_equilibrium.cpp
)
if (MEAN_FIELD_ENABLE_IPO)
include(CheckIPOSupported)
check_ipo_supported(RESULT mean_field_ipo_supported OUTPUT mean_field_ipo_error LANGUAGES CXX)
if (NOT mean_field_ipo_supported)
message(FATAL_ERROR "MEAN_FIELD_ENABLE_IPO was requested, but the compiler does not support it: ${mean_field_ipo_error}")
endif ()
set_property(TARGET mean_field PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
endif ()
target_sources(mean_field
PUBLIC
FILE_SET CXX_MODULES FILES
libmeanfield/interface/mean_field.cppm
libmeanfield/interface/fem.cppm
libmeanfield/interface/fem/reference_tables.cppm
libmeanfield/interface/mapping/prepared_cache.cppm
libmeanfield/interface/analysis/integral.cppm
libmeanfield/interface/boundary/context.cppm
libmeanfield/interface/mapping/coefficients.cppm
@@ -87,7 +135,6 @@ target_sources(mean_field
libmeanfield/interface/mapping/compactification/compactification.cppm
libmeanfield/interface/mapping/compactification/kelvin.cppm
libmeanfield/interface/mapping/compactification/options.cppm
libmeanfield/interface/physics/context.cppm
libmeanfield/interface/physics/gravity.cppm
libmeanfield/interface/physics/solid.cppm
libmeanfield/interface/utils/domain.cppm
@@ -103,6 +150,29 @@ target_sources(mean_field
libmeanfield/interface/quadrature/policy.cppm
libmeanfield/interface/quadrature/mfem.cppm
libmeanfield/interface/solver/fields.cppm
libmeanfield/interface/solver/linear_backend.cppm
libmeanfield/interface/solver/newton.cppm
libmeanfield/interface/solver/preconditioning_diagnostics.cppm
libmeanfield/interface/solver/stellar_equilibrium_types.cppm
libmeanfield/interface/solver/stellar_structure.cppm
libmeanfield/interface/solver/stellar_context.cppm
libmeanfield/interface/solver/stellar_equilibrium.cppm
libmeanfield/interface/preconditioning/backend.cppm
libmeanfield/interface/preconditioning/backend_implementations.cppm
libmeanfield/interface/preconditioning/gravity_field.cppm
libmeanfield/interface/preconditioning/material_surface.cppm
libmeanfield/interface/preconditioning/plan.cppm
libmeanfield/interface/preconditioning/stellar_equilibrium.cppm
libmeanfield/interface/preconditioning/stellar_structure.cppm
libmeanfield/interface/preconditioning/specification_border.cppm
libmeanfield/interface/preconditioning/equilibrium_coordinates.cppm
libmeanfield/interface/preconditioning/stellar_recipe.cppm
libmeanfield/interface/preconditioning/preconditioning.cppm
libmeanfield/interface/normalization/plan.cppm
libmeanfield/interface/normalization/physical_riesz.cppm
libmeanfield/interface/normalization/operators.cppm
libmeanfield/interface/normalization/stellar_equilibrium.cppm
libmeanfield/interface/normalization/normalization.cppm
libmeanfield/interface/operators/gravity_field.cppm
libmeanfield/interface/operators/gravity_field_jacobian.cppm
libmeanfield/interface/operators/kernels/gravity_kernels.cppm
@@ -115,13 +185,65 @@ target_sources(mean_field
libmeanfield/interface/field/field_base.cppm
libmeanfield/interface/field/field_registry.cppm
libmeanfield/interface/field/field_mfem.cppm
libmeanfield/interface/physics/barotrope.cppm
libmeanfield/interface/operators/prepared_barotropic_closure_operator.cppm
libmeanfield/interface/operators/contexts/barotropic_closure_linearization_context.cppm
libmeanfield/interface/physics/rigid_rotation.cppm
libmeanfield/interface/operators/kernels/hydrostatic_equilibrium_kernels.cppm
libmeanfield/interface/operators/contexts/hydrostatic_equilibrium_context.cppm
libmeanfield/interface/operators/kernels/pressure_force_kernels.cppm
libmeanfield/interface/operators/contexts/pressure_force_context.cppm
libmeanfield/interface/operators/prepared_pressure_force.cppm
libmeanfield/interface/operators/kernels/gravity_displacement_force_kernels.cppm
libmeanfield/interface/operators/prepared_gravity_displacement_force.cppm
libmeanfield/interface/operators/contexts/rotation_displacement_force_context.cppm
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
libmeanfield/interface/eos/evaluation.cppm
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_angular_momentum.cppm
libmeanfield/interface/models/compiled_fixed_central_density.cppm
libmeanfield/interface/surface/constant.cppm
libmeanfield/interface/surface/dependencies.cppm
libmeanfield/interface/surface/compiled.cppm
libmeanfield/interface/surface/compiler.cppm
libmeanfield/interface/material/thermodynamic_equations.cppm
libmeanfield/interface/deformation/descriptors.cppm
libmeanfield/interface/deformation/surface_prescription.cppm
libmeanfield/interface/deformation/nodal_radial_surface.cppm
libmeanfield/interface/deformation/interior_extension.cppm
libmeanfield/interface/deformation/vacuum_extension.cppm
libmeanfield/interface/deformation/radial_extensions.cppm
libmeanfield/interface/deformation/domain_deformation.cppm
libmeanfield/interface/deformation/safe_newton_step.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_angular_momentum.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/stellar_equilibrium_compiler.cppm
libmeanfield/interface/operators/prepared_variadic_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
)
@@ -132,6 +254,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
@@ -154,13 +277,19 @@ 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
tests/integrators/gravity.cpp
tests/mapping/domain_mapper.cpp
tests/fem/reference_tables.cpp
tests/mapping/prepared_cache.cpp
tests/mapping/compactification/kelvin.cpp
tests/utils/blocks.cpp
tests/utils/profiling.cpp
tests/operators/gravity_field.cpp
tests/mapping/hdiv_mass_tensor.cpp
tests/operators/prepared_hdiv_mass.cpp
@@ -168,6 +297,13 @@ add_executable(tests
tests/operators/contexts/gravity_field_context.cpp
tests/physics/gravity_monopole_accuracy.cpp
tests/physics/barotrope.cpp
tests/physics/polytropic_eos_characterization.cpp
tests/physics/equation_of_state_type_system.cpp
tests/physics/equation_of_state_consumer_contracts.cpp
tests/physics/polytropic_eos_relations.cpp
tests/physics/equation_of_state_runtime_view.cpp
tests/material/thermodynamic_equation_compilation.cpp
tests/surface/constant_surface_compilation.cpp
tests/operators/kernels/barotropic_closure_kernels.cpp
tests/operators/prepared_barotropic_closure.cpp
tests/operators/contexts/barotropic_closure_linearization_context.cpp
@@ -180,16 +316,91 @@ add_executable(tests
tests/operators/prepared_hydrostatic_equilibrium_analytic_accuracy.cpp
tests/physics/barotrope_pressure.cpp
tests/operators/kernels/pressure_force_kernels.cpp
tests/operators/contexts/pressure_force_context.cpp
tests/operators/prepared_pressure_force.cpp
tests/operators/gravity_displacement_force.cpp
tests/operators/gravity_displacement_force_analytic_comparisons.cpp
tests/operators/contexts/rotation_displacement_force_context.cpp
tests/operators/prepared_rotation_displacement_force.cpp
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/stellar_equilibrium_compiler.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/physics_specification_frontend.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
tests/deformation/radial_extensions.cpp
tests/deformation/domain_deformation.cpp
tests/deformation/safe_newton_step.cpp
tests/operators/prepared_mass_normalization.cpp
tests/operators/prepared_angular_momentum.cpp
tests/operators/prepared_stellar_equilibrium.cpp
tests/utils/domain.cpp
tests/field/field_base.cpp
tests/field/field_registry.cpp
tests/field/field_mfem.cpp
tests/field/field_dof_map.cpp
tests/preconditioning/plan.cpp
tests/preconditioning/backends.cpp
tests/preconditioning/gravity_field.cpp
tests/preconditioning/material_surface.cpp
tests/preconditioning/stellar_structure.cpp
tests/preconditioning/specification_border.cpp
tests/extensions/fixed_magnetic_specific_energy.cpp
tests/preconditioning/equilibrium_coordinates.cpp
tests/preconditioning/stellar_equilibrium.cpp
tests/normalization/plan.cpp
tests/normalization/physical_riesz.cpp
tests/normalization/stellar_equilibrium.cpp
tests/user-api/stellar_equilibrium.cpp
tests/solver/preconditioning_diagnostics.cpp
tests/solver/stellar_equilibrium_architecture.cpp
tests/solver/stellar_equilibrium_architecture_internal.cpp
tests/solver/stellar_equilibrium_runtime.cpp
)
target_link_libraries(tests PRIVATE mean_field test_mod Catch2::Catch2 Boost::boost)
# A deliberately opt-in API workbench. It is compiled explicitly during API
# verification but temporary user experiments do not break the default build.
add_executable(sandbox EXCLUDE_FROM_ALL sandbox.cpp)
target_link_libraries(sandbox PRIVATE mean_field)
# Opt-in diagnostics using the same context and operators as the sandbox.
add_executable(geometry_quality_experiment EXCLUDE_FROM_ALL experiments/geometry_quality_experiment.cpp)
target_link_libraries(geometry_quality_experiment PRIVATE mean_field)
# Independent n=1 reference and physical checks; does not change sandbox defaults.
add_executable(polytrope_validation_experiment EXCLUDE_FROM_ALL experiments/polytrope_validation_experiment.cpp)
target_link_libraries(polytrope_validation_experiment PRIVATE mean_field)
add_executable(mpi_tests
tests/mpi/mpi_test_main.cpp
tests/mpi/distributed_execution.cpp
tests/mpi/profiling.cpp
tests/deformation/safe_newton_step.cpp
)
target_link_libraries(mpi_tests PRIVATE mean_field test_mod Catch2::Catch2 Boost::boost)
if (MEAN_FIELD_ENABLE_IPO)
set_property(TARGET tests PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
set_property(TARGET mpi_tests PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
endif ()
add_library(experiment_mod)
target_sources(experiment_mod
PUBLIC
FILE_SET CXX_MODULES FILES
experiments/experiment_results.cppm
experiments/stellar_null_space.cppm
)
target_link_libraries(experiment_mod
PUBLIC
@@ -199,11 +410,37 @@ target_link_libraries(experiment_mod
add_executable(experiments
experiments/experiment_main.cpp
experiments/full_stellar_preconditioning.cpp
experiments/gravity_accuracy_budget.cpp
experiments/gravity_preconditioning.cpp
experiments/material_surface_preconditioning.cpp
experiments/preconditioning_diagnostics.cpp
)
target_link_libraries(experiments PRIVATE mean_field test_mod experiment_mod Catch2::Catch2 Boost::boost)
add_executable(stellar_null_space_experiments
experiments/experiment_main.cpp
experiments/rigid_motion_null_space.cpp
experiments/gravity_completed_rigid_motion.cpp
experiments/coupled_gauge_modes.cpp
)
target_link_libraries(stellar_null_space_experiments
PRIVATE
mean_field
test_mod
experiment_mod
Catch2::Catch2
Boost::boost
)
if (MEAN_FIELD_ENABLE_IPO)
foreach (mean_field_ipo_target IN ITEMS test_mod experiment_mod experiments stellar_null_space_experiments)
set_property(TARGET ${mean_field_ipo_target} PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
endforeach ()
endif ()
include (CTest)
include (Catch)
catch_discover_tests(
@@ -211,3 +448,57 @@ catch_discover_tests(
experiments
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
add_test(
NAME mpi_single_rank_stellar_root
COMMAND
${MPIEXEC_EXECUTABLE}
${MPIEXEC_NUMPROC_FLAG} 1
${MPIEXEC_PREFLAGS}
$<TARGET_FILE:mpi_tests>
${MPIEXEC_POSTFLAGS}
"[single-rank]"
)
set_tests_properties(
mpi_single_rank_stellar_root
PROPERTIES
LABELS "mpi;single-rank"
PROCESSORS 1
RESOURCE_LOCK mean_field_mpi
TIMEOUT 600
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
foreach (mean_field_mpi_ranks IN ITEMS 2 4)
add_test(
NAME mpi_${mean_field_mpi_ranks}_ranks
COMMAND
${MPIEXEC_EXECUTABLE}
${MPIEXEC_NUMPROC_FLAG} ${mean_field_mpi_ranks}
${MPIEXEC_PREFLAGS}
$<TARGET_FILE:mpi_tests>
${MPIEXEC_POSTFLAGS}
"[mpi]~[single-rank]"
)
set_tests_properties(
mpi_${mean_field_mpi_ranks}_ranks
PROPERTIES
LABELS "mpi;distributed"
PROCESSORS ${mean_field_mpi_ranks}
RESOURCE_LOCK mean_field_mpi
TIMEOUT 180
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
endforeach ()
add_custom_target(
check_mpi
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure --label-regex "mpi"
DEPENDS mpi_tests
USES_TERMINAL
)
# A deliberately separate, physics-developer-facing example. Its targets
# depend on MeanField, but none of its sources are part of the mean_field
# library or the main regression-test executable.
add_subdirectory(extension_example)

View File

@@ -128,7 +128,7 @@ BreakFunctionDefinitionParameters: false
BreakInheritanceList: BeforeColon
BreakStringLiterals: true
BreakTemplateDeclarations: MultiLine
ColumnLimit: 80
ColumnLimit: 120
CommentPragmas: "^ IWYU pragma:"
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4

View File

@@ -0,0 +1,301 @@
# Geometry quality experiment
`geometry_quality_experiment` observes the production stellar-equilibrium context,
normalization, analytic Jacobian, preconditioner, volume extension, mapper, and
geometry-safe-step estimator. It does not implement a second Newton solver or
change the physical equations. This document describes methods and usage, not
conclusions from a particular run.
## Build and run
The executable is an opt-in CMake target (`EXCLUDE_FROM_ALL`). From the repository
root, using an existing configured release build:
```sh
cmake --build cmake-build-release-homebrew --target geometry_quality_experiment -j 2
./cmake-build-release-homebrew/geometry_quality_experiment --output geometry_quality_results_baseline
```
If the build directory predates the target, regenerate it using the same CMake
configuration first. Building this target does not build or run the full test
suite. The executable does not automatically launch tests.
Use exactly one MPI rank. The executable and geometry observer reject multi-rank
runs: the per-element diagnostic invokes a collective estimator separately for
each local element, which is not a valid distributed loop when rank-local element
counts differ.
The output directory must **not already exist**. Select a new directory for every
run so comparisons cannot accidentally overwrite earlier evidence. The default
mesh path is relative to the current working directory; run from the repository
root or supply `--mesh` explicitly.
Useful initial runs:
```sh
# Geometry and reference-mesh probes only; no diagnostic Newton correction.
./cmake-build-release-homebrew/geometry_quality_experiment --geometry-only --output geometry_quality_results_geometry
# One frozen-seed correction, without costly residual derivative/block-column checks.
./cmake-build-release-homebrew/geometry_quality_experiment --tolerances 0.03 --no-fd --no-block-actions --output geometry_quality_results_fast
# Compare linear accuracy at the identical seed, including default checks.
./cmake-build-release-homebrew/geometry_quality_experiment --tolerances 0.03,0.003 --output geometry_quality_results_tolerances
# Inspect a state reached by up to three production Newton iterations.
./cmake-build-release-homebrew/geometry_quality_experiment --advance 3 --tolerances 0.03 --no-fd --no-block-actions --output geometry_quality_results_advance3_cold
# Repeat that state with the production correction warm-start vector.
./cmake-build-release-homebrew/geometry_quality_experiment --advance 3 --warm --tolerances 0.03 --no-fd --no-block-actions --output geometry_quality_results_advance3_warm
# Replay a previously saved seed correction and inspect additional geometry controls.
./cmake-build-release-homebrew/geometry_quality_experiment --replay-vectors geometry_quality_results_saved/newton_0_vectors.csv --no-fd --no-block-actions --output geometry_quality_results_replay
# Focus a seed replay on diagonal and exact-vertex probes.
./cmake-build-release-homebrew/geometry_quality_experiment --replay-vectors geometry_quality_results_saved/newton_0_vectors.csv --diagonal-only --output geometry_quality_results_vertices
```
Even geometry-only mode constructs the complete production context. Context
construction includes physical preparation and preconditioner setup, so this is
not a lightweight mesh-only program.
## Options
| Option | Default | Meaning |
| --- | --- | --- |
| `--mesh FILE` | `sandbox.smesh` | STROID mesh used by production FEM setup. |
| `--output DIRECTORY` | `geometry_quality_results` | New artifact directory; an existing path is rejected. |
| `--tolerances LIST` | `0.03,0.003` | Comma-separated positive relative linear tolerances less than one. |
| `--max-linear-iterations N` | `200` | Iteration cap for diagnostic linear solves. |
| `--advance N` | `0` | Run production Newton for at most N iterations before freezing the state. |
| `--warm` | Off | Start every diagnostic solve from the correction left in the production context. |
| `--geometry-only` | Off | Run prescribed-direction/reference geometry diagnostics and return. |
| `--no-fd` | Off | Skip full normalized residual directional finite differences. |
| `--no-block-actions` | Off | Skip Jacobian actions with individual correction blocks. |
| `--save-vectors` | Off | Save complete physical/normalized state and correction coefficients. |
| `--replay-vectors FILE` | Off | Load a saved correction after checking its accepted state against this context; evaluate its true linear residual without another linear solve. |
| `--diagonal-only` | Off | Seed replay only: skip full per-element scans, projected-volume controls, and residual/block-column checks; retain diagonal and vertex probes. |
| `--help` | | Print the command-line summary. |
The advancement phase uses production Newton with relative nonlinear tolerance
`1e-8`, default backtracking, and linear tolerance `0.03` / cap `200`. Its linear
settings do not change with `--tolerances` or `--max-linear-iterations`. Check
`trajectory.csv` and the printed accepted-step count: advancement can terminate
before the requested number of iterations.
## Frozen-state protocol
The model matches the current sandbox: nonrotating n=1 polytrope, fixed mass and
angular momentum, fixed central density, and isobaric zero-pressure surface.
The context uses production physical Riesz normalization, the default physical
preconditioner, and FGMRES restart length 40.
All listed diagnostic linear tolerances are evaluated at the same accepted state.
Each starts cold unless `--warm` is present; warm mode reuses the same captured
production correction for each tolerance, not the result of the preceding
diagnostic solve. At the untouched seed that warm vector is zero.
Replay mode reads the exact `--save-vectors` CSV format and checks both saved
physical and normalized accepted-state coefficients against the reconstructed
context, using `1e-12 * (1 + abs(saved_value))` per coefficient. It loads the
normalized correction, denormalizes through the current context, and recomputes
`Jp+F`. It uses only the first requested tolerance, and that tolerance classifies
the verified residual; it does not trigger a new linear solve. Consequently,
`iterations=0` and the replay wall time are **not** fresh linear-solve performance
measurements. A saved direction that does not meet the requested tolerance gets
status `maximum_iterations` as the current diagnostic classification, even though
no Krylov iteration limit was exercised. Replay verifies a saved state, not a
complete source/build/mesh identity.
`--diagonal-only` requires replay, `--advance 0`, and no `--geometry-only` flag.
It disables finite-difference and block-column checks. It still constructs the
full production context, independently checks the replayed correction, and calls
the ordinary global production preflight once, so `solves.csv` retains the
production quadrature boundary as a reference. It avoids repeated full
per-element scans; it is not a no-preflight or mesh-only mode.
No diagnostic correction is accepted. Finite-difference candidates use production
trial preparation, then restore accepted-state preparation before a subsequent
linear solve. The internal diagnostic access scope requires no live solver and
restores accepted-state preparation on exit.
The first diagnostic correction is additionally split into an unweighted mean
radial surface component and the remainder. These are geometry-only directional
comparisons, not independent solutions of the Newton equation. Uniform contraction
is another deliberately prescribed surface direction: each surface parameter is
minus its reference radius, and its volume displacement is generated by the
production extension.
Two additional prescribed-direction controls bypass the surface extension: the
physical-coordinate fields `u(X) = -X` and `u(X) = -(|X|/R) X` are projected into
the existing displacement FE space. Their geometry checks use only core elements
(attribute 1). These are diagnostic volume directions, not alternative
production surface prescriptions and not Newton corrections. At the default
orders, P3 displacement interpolation does not exactly represent the P4 physical
mesh coordinate field; even the nominally affine control therefore tests an
interpolant rather than an exact continuum affine map. Exterior geometry is not
certified by these core-only controls.
When inspecting an unadvanced seed (`--advance 0`) **with replay**, two additional
controls compile the existing production radial-interior prescription at powers
3 and 4 instead of 2. They apply the identical saved surface correction, retaining
the production surface and exterior prescriptions, then inspect all production
geometry rules. These are geometry-only interventions: the correction has not
been recomputed for the changed parameterization, so an improved geometry boundary
does not establish nonlinear convergence or equilibrium accuracy. These controls
do not run in ordinary non-replay or advanced-state inspection.
## Artifacts and interpretation
| File | Contents |
| --- | --- |
| `metadata.txt` | Mesh path/size, compile/compiler identifiers, polynomial increment, model/scaling description, MPI count, requested options, state size, and accepted residual. |
| `blocks.csv` | Accepted state/residual, corrections, and true linear residual `Jp+F`, separated by manifest block. |
| `solves.csv` | Linear tolerance/status/iterations, verified true residual, elapsed whole-solve time, geometry boundary, and differences from the first correction. |
| `surface.csv` | Surface reference coordinates and accepted/correction displacement divided by reference radius. |
| `surface_summary.csv` | Unweighted mean/RMS surface correction fraction, RMS nonmean component, and extrema. |
| `block_actions.csv` | Each individual correction block's contribution to each equation block, including its dot product with the accepted residual. First correction only. |
| `finite_differences.csv` | Blockwise directional derivative errors for the first correction. Header-only when disabled. |
| `extension_checks.csv` | Volume-direction consistency against finite differences of the freshly prepared generated volume displacement, plus trial residual norms. Uses the same first-correction perturbations as the residual derivative checks; header-only with `--no-fd`. |
| `trajectory.csv` | Accepted production iterations before inspection; present with `--advance`. |
| `CASE_vectors.csv` | Optional raw coefficient snapshots from `--save-vectors`. |
| `CASE_geometry_elements.csv` | Sorted per-element geometry boundaries, limiting samples, positions, directional gradients, determinant polynomials, and singular values. |
| `CASE_geometry_mapping_checks.csv` | Directly rebuilt mapped geometry versus the affine prediction at selected limiting samples. |
| `CASE_geometry_limiting_matrices.csv` | Base/directional mapping and reference/physical element matrices for the most limiting elements. |
| `CASE_vertices_geometry_*.csv` | Same geometry diagnostics and unchanged estimator, but with vertex-only sampling on eight selected core elements. Present for seed replay. |
| `CASE_core_diagonal.csv` | Fresh production FE direction along the negative core-corner diagonal, compared with the continuous logical-radius-squared profile. Written for `uniform_contraction` and `newton_0` on recognized sandbox geometry. |
| `uniform_contraction_reference_corner_probes.csv` | Reference transformation probes at and just inside core-element vertices, without inverse-Jacobian evaluation. |
Case names `newton_0`, `newton_1`, etc. follow the requested tolerance order.
`newton_0_mean` and `newton_0_nonmean` refer to the surface split. Geometry probes
also run for `uniform_contraction`.
Core-only controls use `core_projected_affine_contraction` and
`core_projected_physical_radial_contraction`.
Replay power controls use `newton_0_radial_power_3` and
`newton_0_radial_power_4`. The `action_difference_over_F` column in `solves.csv`
is the norm of the change in `Jp` from the first correction, divided by the accepted
residual norm. Compare it with the correction difference when investigating weakly
determined directions.
`solves.csv` writes the production `LinearSolveStatus` enumeration numerically:
`0` means converged, `1` maximum iterations, `2` breakdown, `3` non-finite, and
`4` backend failure.
A diagnostic run can still inspect and save an unconverged correction; check
status and the verified true residual before interpreting it as a Newton solve.
### Norms
`physical_l2` and `physical_linf` are Euclidean/max norms of physical FE
**coefficients**, not spatial integrals or physical field extrema. Different
blocks have different units, so summing or directly comparing their unscaled
physical coefficient norms is generally not meaningful.
Normalized norms use the production frozen scaling; their Euclidean combination
is the single-rank norm used by this experiment's Newton/linear diagnostics.
`linear_residual_over_block_F` divides by that equation block's initial normalized
residual. Retain the absolute numerator when interpreting it: a nearly zero
denominator can make a harmless small absolute residual look relatively large.
Surface summary statistics are unweighted over surface parameters, not area-
weighted spherical averages or a spherical-harmonic decomposition.
### Geometry boundaries and ties
The per-element boundary uses the same union of production quadrature rules as
Newton preflight. It is the first sampled determinant boundary in **[0, 1]** along
the supplied direction, not a search over arbitrary positive step sizes.
`limited=0,boundary_step=1` means no boundary was found in that interval; it does
not locate a boundary at one. The safety step is 90% of a limiting boundary.
Each CSV row represents one distinct element. Counts within one part per million
and within one percent of the global smallest boundary measure near-ties between
elements, not the potentially much larger number of near-tied quadrature points.
Element numbers and rule indices are local; this executable uses rank zero only.
For a limited element the row's sample is its actual limiting quadrature point.
For an unlimited element it is the element center, and rule/point indices are -1.
Thus directional-gradient entries are point samples, **not maxima over an entire
element**. The reported minimum accepted/full-step determinants, by contrast,
come from all sampled rules in that element.
Reference-element singular values describe the map from the element integration
coordinates to the undeformed reference mesh. Mapped singular values describe
the DomainMapper map relative to that reference mesh. Total physical element
values combine both Jacobians. Distinguishing these avoids attributing a poor
reference element to the Newton displacement map alone.
The worst 12 distinct elements receive detailed fresh-mapping checks at fractions
`0, 0.25, 0.5, 0.9, 0.99, 0.999` of their own boundary. Mapping-matrix error is
relative to the predicted matrix norm; determinant error is normalized by the
accepted determinant, not by the small near-boundary determinant. Geometry
validity between quadrature samples is not certified by these checks.
For unadvanced seed replay, additional `CASE_vertices` diagnostics pass all eight
vertices of each selected core element (0, 9, 18, 27, 36, 45, 54, 63) to the
**unchanged production estimator**. These use a different sample set, not different
geometry physics or altered tests. Compare vertex and ordinary quadrature
boundaries explicitly: one does not subsume the other. Vertex-only minimum
determinants are minima over those vertices, not over the element interiors or
the full mesh. The cases include uniform contraction, the replayed correction,
its mean/nonmean split, and radial-power controls. Selected elements are filtered
for core attribute 1 and hexahedral geometry; their numbering remains specific
to the sandbox mesh.
Corner probes cover elements 0, 9, 18, 27, 36, 45, 54, and 63, all their vertices,
and inward fractions `0, 1e-5, 1e-4, 0.001, 0.01, 0.05, 0.1` toward each element
center. These element IDs are specifically useful for the sandbox mesh, not a
universal classification for arbitrary `--mesh` inputs. Exact-vertex probes
evaluate only the reference transformation and its Jacobian; they deliberately
avoid inverses at potentially singular vertices.
The diagonal probe samples element 0 at equal integration coordinates `s`, using
`s = 0, 0.005, 0.010885670926971493, 0.02, 0.05, 0.1, 0.2,
0.276393202250021, 0.5, 0.723606797749979, 0.9, 1`. This includes the production
limiting sample and the P3 GaussLobatto interpolation nodes along that diagonal.
It reads the generated direction's true DOFs into a fresh grid function and
directly evaluates FE values and derivatives. The continuous comparison is
`u_radial = corner_surface_amplitude * r_logical^2`; its derivative uses the
logical transformation and the physical reference-radius derivative. Actual and
desired radial derivatives are both divided by `dr_physical/ds` to obtain radial
gradients, exposing separately the nodal interpolant and coordinate amplification.
No Jacobian inverse is used. The comparison is skipped unless element 0 matches
the sandbox's negative diagonal from logical coordinate -1/4 to -1/8, and assumes
logical stellar-surface radius one. It is not a general core-mode decomposition.
For power-control cases the continuous comparison uses the corresponding radial
power instead of two. Appended determinant coefficients represent
`det(J_reference + alpha * dU/dxi) / det(J_reference)`, evaluated directly by
column multilinearity, including at exact vertices. They apply to the
undeformed seed; the caller restricts these probes to that state. These
coefficients allow independent checks outside the production quadrature sample
set and require no matrix inverse.
### Derivative checks
Residual checks use forward differences
`(F(x + epsilon*p) - F(x))/epsilon`, with epsilon equal to the production safe
step times `1e-2`, `1e-3`, and `1e-4`. They compare against the complete normalized
`Jp`, using frozen normalization and fresh production trial preparation.
These are one-sided first-order checks, not central differences. Expect truncation
error to decrease with epsilon until cancellation or preparation/solve error
dominates. A single small error or three nonmonotone errors do not by themselves
establish or refute a Jacobian defect. Forward steps stay in the predicted
positive-direction interval; negative perturbations are not preflighted.
The accompanying extension check compares
`(generated_volume(x + epsilon*p) - generated_volume(x))/epsilon`
against `BuildVolumeDisplacementDirection(physical_p)`. Its relative error is a
Euclidean true-DOF vector norm divided by the expected direction norm. This checks
normalization, surface perturbation, and production volume generation together;
it is distinct from checking the mapper's element-local analytic variation.
## Reproducibility limits
Preserve the console log with the artifact directory. Metadata records useful
configuration identifiers but does not contain a mesh checksum, a complete
compiler flag dump, or a source/worktree snapshot. For controlled comparisons,
also retain the exact mesh, configured build options, and source revision plus
local diff. Do not equate runs with different initial residuals merely because
their executable or mesh filename matches.
An investigator may additionally save `provenance.txt` beside these artifacts;
that file is not currently produced automatically by the executable.

View File

@@ -0,0 +1,237 @@
# Geometry failure diagnosis — 2026-09-08
## Conclusion
The first production Newton correction is small in surface amplitude but produces
large, oscillatory radial displacement gradients near the eight core corners.
The tensor-P3 representation of the prescribed extension distorts the intended
ray-wise profile; the poorly conditioned reference mesh amplifies its gradients.
There is also a verified geometry-safety defect: the production quadrature union
misses inversion between its samples. At the first accepted step length
`alpha = 0.086127771767489855`, the relative mapping determinant is **-0.670218 at
the core corner and -0.428915 at an interior point close to it**, while the
reported limiting quadrature point has determinant **+0.100006**.
Thus this is not merely an approach toward a future singularity: the first
accepted step already corresponds to a folded represented geometry. The
production operators can continue evaluating because their own sampled points
remain admissible.
No physical equations, mapping/extension implementations, Newton policies, or
production tolerances were changed. This investigation implemented an opt-in
experiment and an internal, guarded diagnostic-access hook only.
## Reproduction and artifacts
Build:
```sh
cmake --build cmake-build-release-homebrew --target geometry_quality_experiment -j 6
```
Runs performed, all single-rank Release:
```sh
./cmake-build-release-homebrew/geometry_quality_experiment --output geometry_quality_results_2026-09-08_baseline --tolerances 0.03,0.003 --max-linear-iterations 100 --save-vectors
./cmake-build-release-homebrew/geometry_quality_experiment --output geometry_quality_results_2026-09-08_profile --replay-vectors geometry_quality_results_2026-09-08_baseline/newton_0_vectors.csv --no-fd --no-block-actions
./cmake-build-release-homebrew/geometry_quality_experiment --output geometry_quality_results_2026-09-08_vertices --replay-vectors geometry_quality_results_2026-09-08_baseline/newton_0_vectors.csv --diagonal-only
python3 experiments/summarize_geometry_quality.py geometry_quality_results_2026-09-08_baseline
python3 experiments/summarize_geometry_quality.py geometry_quality_results_2026-09-08_vertices
```
The runs took 510.721, 152.037, and 92.3526 seconds, respectively. Replay checks
the saved accepted state against a fresh context and verifies `Jp+F`, rather than
re-solving for a new correction. Output directories are refused if they already
exist. Help, invalid-tolerance rejection, existing-directory rejection, and normal
context restoration were exercised. `git diff --check` passed. No test suite was
built or run.
The existing build cache contained a nonexistent Eigen 5.0.1 include path. CMake
dependency discovery was refreshed with `-U '*eigen3*'`, locating installed Eigen
3.4.0. See the run provenance and metadata files. The fresh baseline reproduces
the earlier sandbox's seed residual, 23 Krylov iterations, and first safe step.
## 1. Element 0 is the representative of a core-corner pattern
The eight limiting elements are **0, 9, 18, 27, 36, 45, 54, 63**. They are core
elements (attribute 1), not compactified exterior elements. For the Newton
direction their quadrature boundaries differ by only about 0.013%; element 0
has the smallest value. They are near-ties, not exact ties. For uniform
contraction they agree within one part per million.
The actual Newton limiting sample is on element 0's body diagonal:
- Integration coordinates: `(0.010885670927, 0.010885670927, 0.010885670927)`.
- Physical reference position: approximately `(-0.144337126, -0.144337126, -0.144337126)`.
- Physical radius: `0.249999235`, immediately inside the core boundary.
- Reference element Jacobian singular values: minimum `5.51738e-5`, maximum
`0.108818`, condition number approximately **1,972**.
- At the exact corner the corresponding condition number is approximately **3,151**.
The relative displacement mapping at the seed is the identity. Its determinant
of one therefore conceals the conditioning of the underlying reference element.
Evidence: baseline `newton_0_geometry_elements.csv`,
`uniform_contraction_reference_corner_probes.csv`.
## 2. The dangerous correction is small and nonuniform
Unweighted surface-parameter statistics for the first correction:
| Quantity | Fraction of reference radius |
| --- | ---: |
| Mean | -0.000201759 |
| RMS | 0.000360659 |
| Nonmean RMS | 0.000298945 |
| Most negative | -0.000751271 |
| Most positive | +0.000661834 |
The largest displacement is only **0.0751% of the radius**. The negative extrema
occur at the eight cube-corner surface directions; positive extrema occur near
the twelve edge-center directions. The pattern is nearly invariant under cube
symmetries: the largest spread among symmetry-equivalent nodal directions is
`1.3544e-6`, compared with the full amplitude range `0.0014131`.
The mean-only correction has no sampled boundary through alpha=1. Removing the
mean changes the quadrature boundary from `0.0956975242` to `0.0957208926`, only
0.0244%. The nonmean component therefore accounts for almost the entire local
compression. It is not an oversized uniform radius change or a warm-start-only
artifact; the first solve starts cold.
## 3. Direct diagonal inspection identifies the interpolation mechanism
On the negative core-corner ray, the target surface point is fixed. For the
default radial-power-2 prescription and logical stellar radius one, the intended
continuous radial displacement is exactly
`u_r(s) = a_corner * r_logical(s)^2`, with `a_corner = -0.0007504485104433`.
At the Newton limiting sample:
| Quantity | Intended continuous ray profile | Represented P3 FE field |
| --- | ---: | ---: |
| Radial displacement | -4.6393850e-5 | -3.5188044e-5 |
| Physical radial derivative | -0.4881315 | **-10.4495911** |
The derivative is amplified **21.4 times relative to the intended profile**.
The values agree at the P3 interpolation nodes, but disagree between them. At
`s=0.1`, the FE radial displacement even becomes positive although the intended
profile remains negative.
The code constructs the extension from logical-radius weights and surface-trace
interpolation at volume nodes, then represents it in the tensor-P3 volume space.
The corner element spans three logical max-coordinate sectors, projecting toward
three surface faces. The resulting angular/radial composition is not a single
low-degree tensor polynomial inside that element. Its nodal interpolant need not
preserve the prescribed ray-wise radial behavior.
The physical gradient further multiplies by the inverse reference Jacobian.
Near the core corner, the physical radial coordinate changes extremely slowly
along the logical ray. At the limiting sample, `dr_logical/ds=-0.125` but
`dr_physical/ds=-9.55639e-5`.
This explanation is supported by the measured saved mesh, not just by mesher
source. Sibling STROID source suggests why the core map flattens there, but its
working tree is dirty and was not treated as proof of mesh provenance.
Evidence: profile/vertices `newton_0_core_diagonal.csv`; production implementation
`libmeanfield/impl/deformation/radial_extensions.cpp` and field registry P3
displacement versus saved mesh P4 geometry.
## 4. Quadrature safety is not element safety
The unchanged production estimator was run with a second sampling plan containing
the eight vertices of each of the eight core-corner elements. Independently,
the diagnostic formed the polynomial
`det(J_reference + alpha * dU/dxi) / det(J_reference)`
directly from element matrices, without inverse Jacobians. The methods agree.
| Direction | Production quadrature boundary | Vertex boundary |
| --- | ---: | ---: |
| Original Newton correction, radial power 2 | 0.0956975 | **0.0515682** |
| Same surface correction, radial power 3 | 0.382625 | **0.206273** |
| Same surface correction, radial power 4 | No boundary through 1 | **0.825091** |
For the original correction at alpha=0.0861277718:
| Diagonal coordinate s | Relative determinant |
| --- | ---: |
| 0, exact corner | **-0.670218** |
| 0.005, interior point | **-0.428915** |
| 0.01088567, production limiting quadrature point | +0.100006 |
| 0.02 | +0.609367 |
The negative interior value establishes an actual fold, not merely a singular
boundary vertex. Adding vertices would catch this case, but vertex sampling alone
is not a general positivity certificate: the uniform-contraction control has a
much stricter interior quadrature boundary than vertex boundary.
## 5. Controls and ruled-down explanations
### Linear accuracy
| Requested linear tolerance | Krylov iterations | Verified relative residual | Geometry-safe step |
| --- | ---: | ---: | ---: |
| 0.03 | 23 | 0.0244757 | 0.0861278 |
| 0.003 | 37 | 0.00299622 | 0.0865524 |
Tenfold tightening changes the normalized surface correction by 1.30% and the
safe step by only 0.49%. Linear inaccuracy is not the main explanation for this
first-step failure, although this does not establish behavior at every later
state or arbitrary tolerance.
### Mapping and derivative consistency
- Fresh mapped geometry agrees with the preflight affine prediction to about
`6e-16` for the actual Newton correction.
- Finite differences of generated volume displacement agree with the production
extension direction to about `1.6e-16`.
- Material/shape/hydrostatic directional residual differences improve roughly
tenfold with the first tenfold perturbation reduction, reaching relative errors
around `1e-7`, then show roundoff/cancellation.
- Gravity checks improve initially but have smaller net Jacobian actions and
noisier relative errors. Mass finite differences are cancellation-sensitive;
the smallest perturbation is worse. These checks do not prove the entire
Jacobian correct, but reveal no gross inconsistency along this correction.
- The central-density relative error of one has absolute magnitude `1.22e-23`;
it is not evidence of an important phase-border defect.
### Extension and representation controls
Uniform contraction through the default extension has a quadrature boundary of
0.04819 (4.82% surface contraction), independent of Newton/EOS balances.
Core-only P3 projections of physical affine and physical-radius-squared
displacements have boundaries 0.2014 and 0.7693. These are diagnostic bypass
controls, not production solutions. Even physical affine scaling is not exactly
representable by P3 displacement on P4 geometry.
Higher radial powers reduce core sensitivity, but **power 4 is not a demonstrated
fix**: despite all production quadrature points permitting alpha=1, its corner
determinant at alpha=1 is **-0.211997**, and a nearby interior determinant is also
negative. Increasing order or power alone should not be assumed to solve the
problem. In particular, faithfully interpolating the original logical-radial
profile can make the uniform control more singular near a poorly conditioned
reference corner; interpolation sometimes suppresses as well as amplifies it.
## Recommended next work
1. Add this replay direction and the negative interior/corner determinant as a
targeted geometry regression. Extend admissibility sampling to vertices and
near-corner/edge regions, then consider adaptive or bounded determinant
certification for high-order elements. Do not simply reduce the minimum step.
2. Design an extension that controls gradients in physical reference coordinates
and preserves simple radial/affine modes in the represented FE space. Treat
reference-mesh quality and displacement/geometry order compatibility together.
3. Rebuild the coupled residual/Jacobian and solve anew under any changed
extension. Do not accept a post-processed old Newton direction as though it
solved the new Newton equation.
4. Recheck the mesh-symmetric nonuniform surface pattern and block residuals after
repairing the geometry representation. The precise origin of that discrete
pattern, and full nonlinear convergence after a repair, remain unestablished.
The immediate geometric failure mechanism and the sampling defect are now
reproduced. A production remedy and its nonlinear convergence are deliberately
not claimed by this diagnostic experiment.

View File

@@ -0,0 +1,90 @@
# One-level uniform h-refinement — 2026-09-09
## Scope and reproducibility
This study adds exactly one `stroid.refinement.UniformRefinement(mesh, 1)`
to the **loaded coarse solve's input**, without regenerating the initial mesh,
changing field orders, or changing the model or solver tolerances. The original
`sandbox.smesh`, sandbox executable, and all existing coarse data are preserved.
The current MeanField release links an older STROID archive in `/usr/local`
which lacks the multiblock configuration field. Its refinement operation would
therefore not preserve the intended mapping strategy. Instead,
`experiments/refine_polytrope_mesh.py` uses the installed STROID 0.5.0 Python
binding in the `stroidDev` environment to load, refine and save a complete mesh.
The validation executable loads that refined snapshot with `extraRefine=0`.
No STROID installation, production-library edit or rebuild is involved.
STROID refines the logical reference mesh and reprojects high-order geometry.
The study therefore measures joint geometry/field h-convergence, not subdivision
of an unchanged physical polynomial geometry. The separately refined analytic
control measures the geometry contribution.
| Property | Coarse | Refined |
| --- | ---: | ---: |
| Total hex elements | 1,216 | 9,728 |
| Stellar hex elements | 832 | 6,656 |
| Stored refinement level | 2 | 3 |
| Geometry order | 4 | 4 |
| Density/potential DG order | 2 | 2 |
| Enthalpy/displacement H1 order | 3 | 3 |
| Gravity RT index (reported element order) | 2 (3) | 2 (3) |
| Nonlinear absolute/relative tolerance | 1e-8 / 1e-8 | 1e-8 / 1e-8 |
| Linear relative tolerance / iteration cap | 0.03 / 80 | 0.03 / 80 |
| Volume quadrature orders | 14, 18 | 14, 18 |
The model remains nonrotating n=1, G=M=R=1, K=2/pi, central density pi/4,
zero surface pressure. The coarse accepted solution is reused, not re-solved.
Interior physical-volume errors, shape and virial balance are the acceptance
focus; exterior profiles are diagnostic only insofar as they influence the
interior. Two levels give an observed reduction, not proof of asymptotic order.
## Artifacts and provenance
- Coarse solve: `polytrope_solution_2026-09-09/`.
- Coarse analytic control: `polytrope_analytic_checked_2026-09-09/`.
- Refined mesh and binding provenance: `polytrope_h1_mesh_2026-09-09/`.
- Refined analytic control: `polytrope_h1_analytic_2026-09-09/`.
- Refined solve: `polytrope_h1_solution_2026-09-09/`.
SHA-256 hashes at study start:
~~~text
coarse input / sandbox.smesh
ce11ae99e21e6c3bbfbee402acd2e191c1da0d8261d2227b4203f73f2f337a74
refined.smesh
39cf76d21c74730ffaacf50ae66e150d2e88dcb66caf6a499339b7e90a1f8182
release polytrope_validation_experiment
90caa65be08c5d9dd17ddab267d02159ea440b94304e7d0d4aa7807e688009af
release sandbox (not run in this study)
a904b39935a5add938129e5c3edb72f94b416f0e09ba5b3b937a8004fa463057
~~~
The refinement JSON additionally records the actual STROID native-extension
path/hash, interpreter, regional element counts and configuration checks.
Current mesh-based outputs each retain the fully refined `input.smesh`, so
ordinary saved-field replay requires no new refinement or regeneration.
## Analytic geometry control
All 140 independent analytic self-checks and all 24 refined physical-control
screens passed. These are analytic fields evaluated on the represented mesh,
not an FE equilibrium solution: zero volume field errors are by construction.
| Control | Coarse | Refined |
| --- | ---: | ---: |
| Surface-radius relative RMS error | 8.91509e-6 | 4.29640e-7 |
| Volume-radius relative error | 9.062e-9 | 7.63599e-11 |
| Relative mass error | 3.922e-10 | 5.01155e-13 |
| Virial error | 2.615e-10 | 7.19869e-13 |
| Invalid stellar corner/inset samples | 0 | 0 |
| Profile points located | 4,020 / 4,020 | 4,020 / 4,020 |
The analytic surface error decreases about 20.75-fold. The refined virial is
already near its observed quadrature sensitivity (2.43e-13), so its enormous
two-level ratio should not be interpreted as a convergence order.
## Refined equilibrium
The refined solve is in progress. No equilibrium h-convergence conclusion is
available until its accepted fields have been measured.

View File

@@ -0,0 +1,377 @@
# Nonrotating n=1 polytrope verification
This experiment separates nonlinear stopping criteria from physical accuracy.
It uses a closed-form reference independent of the production LaneEmden seed,
then measures the actual mapped mesh and, optionally, the last accepted
production state. It does not modify the physical equations, nonlinear policy,
mesh, or existing sandbox.
The target is `polytrope_validation_experiment`, declared `EXCLUDE_FROM_ALL`.
Build only that target:
~~~sh
cmake --build cmake-build-release-homebrew --target polytrope_validation_experiment -j 6
~~~
The executable requires one MPI rank and uses the CPU device. The following
sequence is deliberately bounded: analytic self-checks, an analytic-field mesh
check, then **one** production solve. Choose fresh output directories; existing
directories are refused.
~~~sh
./cmake-build-release-homebrew/polytrope_validation_experiment --self-check --output polytrope_self_checks
./cmake-build-release-homebrew/polytrope_validation_experiment --analytic-mesh --mesh sandbox.smesh --output polytrope_analytic_mesh
./cmake-build-release-homebrew/polytrope_validation_experiment --solve --mesh sandbox.smesh --output polytrope_solution
~~~
Inspect each stage before proceeding. The commands are usage examples, not a
claim that the corresponding runs passed. Results belong in a separate findings
report. No full test suite or parameter/resolution sweep is part of this
workflow.
For an existing completed output with profiles, the standard-library-only
summarizer can generate a static SVG and Markdown report without rerunning the
solver or changing its CSVs:
~~~sh
python3 experiments/summarize_polytrope_validation.py polytrope_solution --control-directory polytrope_analytic_mesh
~~~
## One additional uniform h-refinement
Use the saved coarse input, not a newly generated mesh, and retain a separate
refined snapshot. The helper calls `UniformRefinement(loaded_mesh, 1)`, verifies
eight children per hexahedron in each material region, and checks saved-mesh
reload and input immutability. It records mesh and STROID extension hashes.
~~~sh
/opt/homebrew/anaconda3/envs/stroidDev/bin/python3.14 experiments/refine_polytrope_mesh.py polytrope_solution_2026-09-09/input.smesh polytrope_h1_mesh
env OMPI_MCA_btl=self ./cmake-build-release-homebrew/polytrope_validation_experiment --analytic-mesh --mesh polytrope_h1_mesh/refined.smesh --output polytrope_h1_analytic
env OMPI_MCA_btl=self ./cmake-build-release-homebrew/polytrope_validation_experiment --solve --mesh polytrope_h1_mesh/refined.smesh --output polytrope_h1_solution
python3 experiments/compare_polytrope_refinement.py polytrope_solution_2026-09-09 polytrope_h1_solution --coarse-control polytrope_analytic_checked_2026-09-09 --fine-control polytrope_h1_analytic --output polytrope_h_comparison
~~~
Inspect the analytic control before launching the solve. Keep field orders,
model, quadrature and solver tolerances fixed. The full refined solve may take
well over an hour; reuse the coarse solve rather than repeating it.
The Python environment above contains multiblock-capable STROID 0.5.0. The
current MeanField build links an older `/usr/local` STROID that can load the
saved geometry but cannot correctly regenerate multiblock geometry from its
configuration. Therefore refinement is performed by the current Python binding,
not by that old linked library. No installation or relinking is needed.
The helper fails if `core_mapping` support is absent.
STROID refines the logical mesh and **reprojects the high-order geometry**;
this is not subdivision of a fixed physical polynomial geometry. Consequently
the comparison includes geometry approximation as well as FE field refinement.
Run the analytic control on both levels to quantify the geometry contribution.
The refined file is complete: the experiment uses `extraRefine=0`, and its
ordinary `input.smesh`/GF snapshots can be replayed without additional refinement
or the newer STROID library.
Two-level ratios `E_coarse/E_fine` and `log2(E_coarse/E_fine)` are observed
reductions, not proof of asymptotic order. Prioritize stellar volume field
errors, shape and virial balance. Exterior profile accuracy is diagnostic only,
relevant insofar as it affects the interior solution. Near-zero integral errors
and errors near the nonlinear/quadrature floor do not give reliable h-rates.
## Modes and controls
| Option | Meaning/default |
| --- | --- |
| `--self-check` | Check the independent reference without loading a mesh or constructing a solver. |
| `--analytic-mesh` | Evaluate exact fields at physical points on the actual mesh; exercise volume/surface integration and radial location without a Newton context. |
| `--solve` | Default mode. Construct the production n=1 problem, solve within the stated budget, and measure the last accepted state even after nonlinear failure. |
| `--replay DIRECTORY` | Re-measure saved nonrotating solve-mode fields using that directory's `input.smesh`; no Newton context or solve. Requires a new `--output` directory. |
| `--mesh FILE` | Default `sandbox.smesh`; input is not overwritten. |
| `--output DIRECTORY` | Default `polytrope_validation_results`; must not already exist. Its parent directory must exist. |
| `--absolute-tolerance VALUE` | Nonlinear absolute tolerance, default `1e-8`. |
| `--relative-tolerance VALUE` | Nonlinear relative tolerance, default `1e-8`. |
| `--linear-tolerance VALUE` | Relative linear tolerance, default `0.03`. |
| `--max-newton N` | Default 8 nonlinear iterations. |
| `--max-linear-iterations N` | Default 80 linear iterations per solve; FGMRES restart length is 40. |
| `--quadrature-order N` | Base physical-volume quadrature order, default 14. |
| `--check-quadrature-order N` | Independent higher order, default 18; must exceed the base order. Also used for surface integration. |
| `--skip-profiles` | Omit physical-shell/ray location and its output; other measurements remain enabled. |
| `--mu-points N`, `--phi-points N` | Angular quadrature sizes, default 6 and 12. Increase during cheap replay to check spherical-mean sampling sensitivity. |
| `--exterior-shells N` | Number of finite-exterior radii between 1.001R and 2R, default 8. Increase during replay to resolve radial structure. |
Select one mode explicitly. The parser accepts the last mode flag if several
are provided. Every mode first runs the independent analytic self-checks.
Exit codes are `0` for all requested checks passing, `1` for nonlinear failure,
`2` for an execution/input error, and `3` for verification failure. In solve
mode, nonlinear failure takes precedence over a subsequent physical-screen
result; consult the saved metrics and metadata as well as the exit code.
Replay retains that precedence using the source's historical convergence status.
`--analytic-mesh` is **not** an FE projection test or a numerical equilibrium
solution. Exact fields are evaluated directly, so volume field-versus-reference
errors are zero by construction at the same successfully evaluated points.
Profile comparisons instead use the requested physical radius and therefore
also test the locator's accuracy. The mode checks integration, physical
location, reference-domain geometry, and integral identities on that domain.
### Diagnostic replay without another solve
After a solve-mode output has saved its grid functions and completed
`physical_metrics.csv`, physical postprocessing can be repeated independently:
~~~sh
./cmake-build-release-homebrew/polytrope_validation_experiment --replay polytrope_solution --output polytrope_solution_replay
~~~
Replay requires an original `mode=solve` output for this nonrotating n=1
benchmark, matching compiled \(G,M,R\), and saved angular velocity exactly zero.
It loads the saved mesh and the five GF files into compatible FE spaces. The
saved accepted coefficients need not have passed the physical screen, but all
required source artifacts must be present. Keep the original solve output:
a replay output is not itself an accepted replay source.
Volume/surface metrics and requested profiles are recomputed. Solver convergence,
bordered/unbordered residual norms, central-border action/value, Bernoulli
constant, and angular-velocity norm are historical source diagnostics, **not
recomputed**. Metadata records
`solver_diagnostics=copied_from_source_not_recomputed`. In particular, a replay
pass is not a new residual evaluation or convergence claim, and solver-tolerance
options do not trigger a fresh solve. The source's nonlinear failure still
produces exit code `1` after successful physical postprocessing.
## Fixed analytic reference and normalization
The benchmark uses the compiled `utils::G`, `utils::MASS`, and `utils::RADIUS`
as fixed positive \(G,M,R\); their values are saved in `metadata.txt`. It does
not fit mass, radius, central density, or a potential offset to the numerical
solution. For \(\xi=\pi r/R\), the stellar solution is
\[
\theta=\frac{\sin\xi}{\xi},\qquad
K=\frac{2GR^2}{\pi},\qquad
\rho_c=\frac{\pi M}{4R^3},\qquad h_c=\frac{GM}{R},
\]
\[
\rho=\rho_c\theta,\qquad h=h_c\theta,\qquad P=K\rho^2,\qquad
\Phi=-h_c(1+\theta),\qquad
m(r)=\frac{M}{\pi}(\sin\xi-\xi\cos\xi).
\]
The radial potential gradient is outward-positive \(g_r=d\Phi/dr=Gm(r)/r^2\);
the acceleration is its negative. Outside the star the analytic material fields
are zero, \(\Phi=-GM/r\), and \(g_r=GM/r^2\). The implementation uses origin
series and a small-distance-to-surface expression, including exact values at
the center and surface. Reference radii must be finite and nonnegative.
The profile normalizations are fixed:
\[
\theta_\rho=\rho/\rho_c,\qquad
\theta_h=h/h_c,\qquad
\theta_\Phi=-R\Phi/(GM)-1.
\]
All three agree with \(\theta\) inside the analytic star. The normalized vacuum
potential is negative outside \(R\), approaching \(-1\), and is not clipped.
Numerical density and enthalpy are likewise never clipped. Negative samples and
their minima are reported. Since \(P_\rho=K\rho^2\) and
\(P_h=h^2/(4K)\) are positive even for negative arguments, pressure checks alone
do not establish physical positivity.
The independent integral references are
\[
\Pi=\int P\,dV=\frac{GM^2}{4R},\qquad
W=-\frac{3GM^2}{4R},\qquad
I_z=\frac23\left(1-\frac6{\pi^2}\right)MR^2.
\]
The self-checks use independent radial Simpson integration, including nonunit
scales, origin regularity, surface/vacuum joins, EOS and hydrostatic identities,
and the gravitational-field energy with its exterior contribution.
## Physical balances and interpretation
The volume measurements integrate over the current **stellar material**
elements with the full physical Jacobian. They compare fields at their actual
physical positions, not at logical radii. Relative volume \(L^2\) errors use
the analytic field's \(L^2\) norm over that same domain. Surface and
volume-equivalent radius errors separately measure the domain discrepancy.
Let \(g\) denote the reconstructed physical mixed gravity field and
\(\Psi=|\Omega\times x|^2/2\). The reported energies are
\[
T=\int\rho\Psi\,dV,\quad
W_\Phi=\tfrac12\int\rho\Phi\,dV,\quad
W_g=-\int\rho\,x\cdot g\,dV,\quad \Pi=\int P_\rho\,dV.
\]
The scalar virial error is
\(|2T+W_\Phi+3\Pi|/|W_\Phi|\); `virial_signed` retains its sign and
`virial_ratio` is \((2T+3\Pi)/|W_\Phi|\), which should approach one.
The force virial replaces \(W_\Phi\) by \(W_g\), keeping the same denominator.
`gravity_energy_consistency` measures \(|W_\Phi-W_g|/|W_\Phi|\).
These are complementary checks: an inaccurate potential and an inaccurate
mixed gravity field need not fail identically.
The central-density constraint is imposed through central enthalpy and an
additional hydrostatic border. Its reported achieved density is inferred from
that enthalpy; it is not an independently sampled DG density value. The
`unbordered` residual removes only the artificial central-border action from
hydrostatic rows, then uses the same production normalization. It retains the
physical Bernoulli constant and all scalar constraint rows. A small bordered
solver residual does not by itself establish a small unbordered physical
residual.
The potential is discontinuous across elements. Its elementwise, or *broken*,
gradient omits interface jumps; it is not the same discrete object as the
mixed H(div) gravity field. Their reported gradient mismatch is a diagnostic,
not a requirement of pointwise equality at finite resolution. The strong
enthalpy-gradient balance is also distinct from the weak assembled residual.
Bernoulli statistics concern \(h+\Phi-\Psi\). Both its mean error against the
fixed analytic constant \(-h_c\) and its spatial variation are saved. Computing
a centered variance does not fit or subtract a potential gauge from the fields.
### Weak EOS closure and the pressure projection floor
The default density space is DG/L2 order 2, while enthalpy is continuous H1
order 3; see [the field registry](../libmeanfield/interface/field/field_registry.cppm).
The [prepared closure](../libmeanfield/impl/operators/prepared_barotropic_closure.cpp)
assembles \(F_i=\int q_i(\rho-h/(2K))\,dV\) at \(n=1\), using the full physical
volume weight and density-space test functions. Thus, at fixed geometry and
zero closure residual, density is the quadrature-weighted \(L^2\) projection of
\(h/(2K)\), not necessarily its pointwise value. The seed's independent
coefficient projections do not themselves enforce this orthogonality.
The [pressure-force kernel](../libmeanfield/impl/operators/prepared_pressure_force.cpp)
uses \(P(h)=h^2/(4K)\), rather than the diagnostic's primary
\(P(\rho)=K\rho^2\); these formulas follow from the
[polytropic EOS](../libmeanfield/interface/eos/polytropic.cppm) for admissible
nonnegative enthalpy. With \(\delta=h-2K\rho\), define
\[
E=\int\frac{\delta^2}{4K}\,dV,\qquad
D=\Pi_h-\Pi_\rho.
\]
The algebraic identity, using the same domain and quadrature, is
\[
D=E+\int\rho\delta\,dV,\qquad
D-E=-2K\,\boldsymbol{\rho}^{\,T}\mathbf F_{\rm closure}.
\]
Consequently, exact weak closure gives \(D=E\ge0\), even when the strong EOS
mismatch \(\delta\) is nonzero. A pointwise mismatch can therefore persist at
a well-converged discrete solution without indicating an EOS/Jacobian algebra
bug. Changing diagnostic quadrature introduces an additional discrepancy in
the residual-pairing identity and should be checked separately.
The observer records `closure_projection_pressure_gap` (\(E\)),
`closure_density_inner_product` (\(\int\rho\delta\,dV\)), and
`closure_projection_pressure_gap_relative_defect`
(\((D-E)/\Pi_{\rm reference}\)). Older metric files also allow reconstruction
of \(E=Vh_c^2\,\text{eos_enthalpy_scaled_rms}^2/(4K)\).
Inspect these alongside the complete `barotropic_closure` residual block:
a small single global pairing can conceal cancellation and does not prove
every closure equation is satisfied.
`enthalpy_virial_error` and `enthalpy_force_virial_error` substitute \(\Pi_h\)
into the two virial diagnostics. They complement, not replace, the original
density-pressure checks. The predetermined pointwise EOS screening budget is
not relaxed: it may identify a finite-resolution projection floor requiring
further discretization study. Diagnostic replay can produce the additional
metrics from saved fields without another Newton solve.
## Shell and ray sampling
Default profiles use the origin, 32 interior radii through \(0.99R\), one
additional radius at \(0.999R\), and eight exterior radii from \(1.001R\) to
\(2R\). Each noncentral sphere uses six Gauss points in \(\mu=\cos\vartheta\)
and twelve uniformly spaced azimuths. The weights sum to one; angular moments
and exact constant-field weighted mean/variance are self-checked before
measurement. Separate 26-ray profiles contain six axes,
twelve face diagonals, and eight body diagonals. Rays are not angular quadrature.
The locator inverts the full physical mapping. Sampled element bounds prioritize
searches but do not exclude elements; missing points may therefore be expensive.
Location error, attempts, and coverage are reported.
Shell means are conditional on successfully located finite values. Density and
enthalpy are additionally conditional on **stellar material**. In particular,
their means near a displaced surface are not whole-sphere density/enthalpy
averages: inspect material and valid-weight coverage. Missing/exterior material
values are not silently replaced by zero. Angular spread and total RMS error
against the fixed radial reference are both written. Axis/diagonal DG values
can be one-sided traces at element interfaces. The origin is explicitly a
single trace, not an angular average; a radial gravity component is undefined
there.
## Output files
| File | Contents |
| --- | --- |
| `metadata.txt` | Mode, input mesh/snapshot, compiled scales/settings, FE orders, and available solve/measurement status and timing. |
| `input.smesh` | Exact input-mesh copy used to construct the FE spaces in each mesh-loading mode, including replay. |
| `analytic_self_checks.csv` | Independent check, observed/expected values, fixed scale, errors, tolerance, pass flag. |
| `volume_metrics_base.csv` | Physical-volume metrics at the base quadrature order. |
| `quadrature_comparison.csv` | Base/check values and their absolute/relative changes. Relative changes of nearly zero metrics require caution. |
| `physical_metrics.csv` | Higher-order volume metrics, surface/corner metrics, optional profile summaries, and available solver/border diagnostics. |
| `seed_physical_metrics.csv` | Production seed's lower-order volume metrics and residuals, measured before Newton to expose any physical degradation during correction. |
| `verification_checks.csv` | Declared screening budgets, observations, and individual pass flags. |
| `radial_profiles.csv` | Physical-shell means, angular spreads, analytic values, scaled errors, coverage, and normalized profiles. |
| `directional_profiles.csv` | Axis/diagonal values, analytic values, element/material identity, and location errors. |
| `newton_history.csv` | Solve-mode iteration residuals, accepted steps, trial counts, and linear/nonlinear diagnostics. |
| `residual_blocks.csv` | Solve-mode physical and normalized block norms, with and without the central border. |
| `field_reconstruction.csv` | Reduced/full field sizes and coefficient round-trip errors. |
| `accepted_state.txt`, `state_layout.csv` | Solve-mode last accepted coefficient vector and block layout. |
| `density.gf`, `enthalpy.gf`, `potential.gf`, `gravity_gradient_reference.gf`, `displacement.gf` | Solve-mode reconstructed grid functions saved before expensive postprocessing. |
| `polytrope_profiles.svg`, `polytrope_summary.md` | Optional summarizer products, generated from existing CSVs; may be regenerated independently. |
The text/GF data are **experiment artifacts, not a production checkpoint or a
supported solver-restart format**. Their supported reuse is the constrained
diagnostic replay described above, not continuation of Newton iterations.
Preserve the original solve output, saved input mesh, and metadata. In
particular, `gravity_gradient_reference.gf` contains the reference-mesh Piola
representation, not the already transformed physical gravity field; its
interpretation requires the corresponding mapping and displacement. Unsupported
material coefficients in saved grid functions are not numerical vacuum data.
## Screening budgets and limits
Budgets are declared in the driver before solving. The default screen requires:
- Relative mass, radius, density/enthalpy/potential/gravity \(L^2\), binding
energy, pressure integral, and axial inertia errors at most `1e-4`.
- Bernoulli scaled RMS variation at most `1e-4`.
- Scalar/force virial errors, gravity-energy disagreement, and EOS enthalpy
scaled RMS at most `1e-6`.
- Virial quadrature change and binding-energy relative quadrature change at
most `1e-8`; normalized unbordered residual at most `1e-8` when available
(fresh in solve mode, historical in replay).
- Maximum negative density/enthalpy excursions divided by their fixed central
scales at most `1e-8`; the underlying negative values are not altered.
- Zero invalid stellar corner samples and, when enabled, zero missing profile
points; kinetic energy at most `1e-14` in the compiled benchmark units.
- In `--analytic-mesh` mode with profiles enabled, maximum scaled pointwise
profile error at most `1e-8` and maximum scaled angular RMS at most `1e-10`
as independent location and spherical-scatter control gates.
All raw metrics remain available, including quantities without pass/fail
budgets. The corner checks include vertices and nearby interior points but
cannot certify positivity everywhere in a high-order element. Two quadrature
orders test integration sensitivity, not spatial-discretization convergence.
A passing single-resolution screen is not a physical convergence certificate;
a small nonlinear residual is not one either. Future work should separate
quadrature, mesh/order, and nonlinear-tolerance errors using planned, bounded
resolution studies, rather than starting broad sweeps automatically.
The user provided an independent ESTER executable at
`/Users/tboudreaux/Programming/ESTER_polytrope.pub/ester` and the command
`ester < dati_ester_polytrope`. This is a future cross-validation path only:
ESTER is not integrated or run by this experiment. Resolve the relative input
in its intended project working directory, and reconcile units, boundary
conditions, rotation, potential gauge, and diagnostic definitions before a
future comparison.

View File

@@ -0,0 +1,252 @@
# Physical validation findings — 2026-09-09
## Outcome
The nonrotating n=1 model reaches a well-converged **discrete** equilibrium, and
its global virial balance passes the initial 1e-6 screening budget. It does **not**
yet pass the full analytic-accuracy screen. In particular, the exterior potential
has roughly 1.31.4% errors near the surface, and several interior field/shape
errors exceed the declared 1e-4 budgets. This is not yet a physical-accuracy
sign-off for performance work.
These results do not establish that the formulation is wrong: one discretization
cannot distinguish ordinary approximation error from a formulation bias or
implementation defect. They do establish that further Newton convergence alone
is not an adequate verification strategy.
The implemented experiment and commands are documented in
[POLYTROPE_VALIDATION.md](POLYTROPE_VALIDATION.md).
## Scope and reproducibility
- Added an opt-in `polytrope_validation_experiment` target and experiment-only
reference, reconstruction, integration, profile, replay, and reporting code.
- No production physics, solver defaults, sandbox source/executable, or input
mesh was changed by this implementation.
- Ran **one** production Newton solve, approximately 864 seconds including
context setup and diagnostics. Three subsequent saved-field replays required
no Newton context or linear solves. Timings are not an isolated performance
benchmark; a diagnostic build overlapped part of the solve.
- Ran 140 independent analytic checks, mesh-based analytic controls, and angular
sampling checks. Did not run the full test suite or ESTER.
- The input mesh and sandbox executable SHA-256 hashes were unchanged:
`ce11ae99e21e6c3bbfbee402acd2e191c1da0d8261d2227b4203f73f2f337a74`
and `a904b39935a5add938129e5c3edb72f94b416f0e09ba5b3b937a8004fa463057`,
respectively. Every mesh-based run retains its own `input.smesh` copy.
The model has G=M=R=1, K=2/pi, central density pi/4, zero angular momentum,
and zero surface pressure. The current mesh has 1,216 elements and the state has
178,074 values. Density and potential use DG order 2, enthalpy and displacement
use H1 order 3. Gravity uses MFEM RT index 2 (reported element order 3).
The geometry's order 4 does not make all solution fields fourth order.
## Data and plots
| Artifact | Location |
|---|---|
| Original solve, seed baseline, coefficient/GF snapshots, residual blocks | [polytrope_solution_2026-09-09](../polytrope_solution_2026-09-09/) |
| Corrected analytic-mesh control | [polytrope_analytic_checked_2026-09-09](../polytrope_analytic_checked_2026-09-09/) |
| Default 6x12 angular replay | [polytrope_replay_2026-09-09](../polytrope_replay_2026-09-09/) |
| 12x24 angular replay, 64 exterior shells | [polytrope_replay_dense_2026-09-09](../polytrope_replay_dense_2026-09-09/) |
| 24x48 angular replay, 64 exterior shells | [polytrope_replay_angular24_2026-09-09](../polytrope_replay_angular24_2026-09-09/) |
| Main generated numerical report | [polytrope_summary.md](../polytrope_replay_angular24_2026-09-09/polytrope_summary.md) |
![Fixed-reference profiles, mean errors, angular scatter, and coverage](../polytrope_replay_angular24_2026-09-09/polytrope_profiles.svg)
The original `polytrope_analytic_mesh_2026-09-09` control is retained for
provenance, but its angular RMS statistic contained the roundoff artifact
described below. Use the **checked** control above for current interpretation.
The default-grid saved GF replay reproduced every pre-existing aggregate physical metric exactly;
new pressure/projection metrics were then added without rerunning Newton.
## Independent controls
All 140 closed-form checks pass, including non-unit scales, central/surface
limits, exterior potential, derivatives, and independent radial mass/energy
integrals. No production LaneEmden integration or seed helper supplies the
reference values.
On the actual undeformed mesh, exact analytic fields give:
| Control | Result |
|---|---:|
| Relative mass error | 3.92e-10 |
| Relative binding-energy error | 2.62e-10 |
| Virial error | 2.61e-10 |
| Force-based virial error | 5.23e-10 |
| Change in virial between quadrature orders 14 and 18 | 1.38e-13 |
| Located profile points | 4,020 / 4,020 |
| Maximum scaled pointwise sampling error | 4.96e-11 |
| Corrected maximum scaled angular RMS | 2.12e-11 |
The first weighted variance update initially introduced a one-ulp contribution
to the second moment, creating spurious angular RMS values near 1e-9. Exact
first-sample initialization fixed this; a constant-field zero-variance check now
guards it. This did not change the numerical volume integrals or Newton solve.
Control limitations matter: volume field errors are zero by construction because
the same independent reference supplies the analytic fields and comparisons.
Those zeros are not tests of FE representability. The nonzero global integral
errors test integration/geometry, and requested-radius profile errors test the
inverse mapping. Quadrature agreement alone does not resolve the extremely thin
layer where the approximate surface crosses the exact analytic support.
The undeformed surface has RMS radius error **8.92e-6 R**, despite a much smaller
volume-equivalent radius bias of 9.06e-9 R. Signed surface errors cancel in the
volume; this is not 1e-8 local surface accuracy.
## Nonlinear and physical results
The experiment used nonlinear absolute tolerance 1e-8, relative tolerance 1e-8,
linear relative tolerance 0.03, and an 80-iteration linear limit. It stopped
before attempting another increasingly expensive near-floor correction.
| Accepted step | Nonlinear residual afterward | Linear iterations | Step length |
|---|---:|---:|---:|
| Initial seed | 1.81187e-4 | — | — |
| 1 | 7.41173e-6 | 24 | 1 |
| 2 | 2.20576e-7 | 25 | 1 |
| 3 | 6.60533e-9 | 28 | 1 |
All field L2 errors below use the full three-dimensional, physical-volume-weighted
stellar domain and fixed analytic scales/radii, not fitted spherical profiles.
| Diagnostic | Final value | Initial screening budget |
|---|---:|---:|
| Relative mass error | 2.36e-11 | 1e-4 |
| W = 0.5 integral rho Phi | -0.750000539807 | Exact -0.75 |
| Integral P(rho) | 0.250000102482 | Exact 0.25 |
| Virial ratio 3 integral P(rho) / abs(W) | 0.999999690184 | Exact 1 |
| Virial error using P(rho) | 3.10e-7 | 1e-6 |
| Force-based virial error using P(rho) | 3.73e-7 | 1e-6 |
| Virial error using production P(h) | 2.62e-7 | Same 1e-6 comparison |
| Force-based virial error using P(h) | 3.25e-7 | Same 1e-6 comparison |
| Gravity-energy consistency error | 6.30e-8 | 1e-6 |
| Density relative L2 error | 4.70e-4 | 1e-4 — fails |
| Enthalpy relative L2 error | 3.27e-4 | 1e-4 — fails |
| Potential relative L2 error, stellar interior only | 1.26e-4 | 1e-4 — fails |
| Gravity-gradient relative L2 error | 5.98e-4 | 1e-4 — fails |
| Surface radius RMS error / R | 2.75e-4 | 1e-4 — fails |
| Volume-equivalent radius error / R | 2.07e-4 | 1e-4 — fails |
The full screen also flags pointwise EOS mismatch and Bernoulli variation. All
original budgets remain visible and unchanged. No density/enthalpy negativity
was found at the volume quadrature samples. Higher-order integration changes
the numerical virial by only 3.14e-14: these discrepancies are not explained by
the diagnostic volume quadrature order.
Comparing seed and final state at the **same quadrature order**, density error
worsens 1.43x, enthalpy error 12.8x, and potential error 1.31x. Gravity error falls
to 0.694 of its initial value; mass, binding energy, moment of inertia, and virial
balance improve. Newton solves the discrete equations, not the continuum
reference-error minimization problem.
## What is and is not responsible
### Geometry remains healthy; spherical accuracy does not
All 19,968 stellar corner/inset samples are valid. The worst element condition
number is 3.466, versus 3.464 before the solve. The smallest sampled relative
mapping determinant is 0.99632 at the corner/inset samples. This is not the old
folding/step-collapse mechanism.
Nevertheless, surface RMS radius error grows approximately 31x. The final mean
radius is 0.999793481 R, and sampled radii range from 0.999381621 R to
1.000574426 R. There is both a mean contraction and an aspherical component
(approximately 1.82e-4 R RMS). Center-of-mass displacement is only 1.79e-6 R and
cannot explain that shape error.
### The central-density border is small
The central border coefficient is -4.64e-13. Its normalized residual action is
1.25e-9; removing it changes the full residual norm from 6.61e-9 to 6.72e-9.
The unbordered equations still satisfy the 1e-8 screening budget. This is not a
large artificial center force masking the observed 1e-4-level field errors.
The constrained central enthalpy is exactly 1; the independently sampled central
DG density is 0.7853956513 versus the prescribed pi/4 = 0.7853981634.
### The pointwise EOS mismatch has a verified projection component
The code enforces closure weakly in the density space, while the pressure force
uses P(h). Put delta = h - 2K rho. Then
integral P(h) - integral P(rho)
= integral delta^2/(4K) + integral rho delta.
For a converged density-space projection, the last term vanishes, but the
nonnegative squared-error term can remain because density and enthalpy use
different spaces. Numerically:
- Measured pressure-integral difference: 1.20602555e-8.
- Predicted squared projection contribution: 1.20605303e-8.
- Difference: -2.75e-13, or -1.10e-12 of the analytic pressure integral.
- Normalized closure-block residual: 6.71e-12.
- Pointwise EOS RMS scaled by central enthalpy: 8.57e-5.
Thus the remaining pointwise EOS RMS is not evidence that Newton failed to solve
the weak closure. This explanation does **not** remove the independent field,
shape, or exterior-potential discrepancies.
## Spatial discrepancies that global virial balance misses
### Exterior potential
At r=1.001 R, the 24x48 angular sample has mean potential error **+0.012866 GM/R**,
approximately 1.29% of the analytic potential magnitude there. The angular RMS
is only 8.76e-5 GM/R: the error is predominantly radial.
The worst saved directional sample has error 0.0139741 GM/R on the (+,-,+) body
diagonal, in exterior element 923; all eight body diagonals have almost the same
error. Its inverse-location error is only 1.73e-15 R. This is not a single-element
or locator accident.
Along that ray, samples from 1.001 R through 2 R all lie in the same exterior
element. The error changes sign with radius rather than behaving like a constant
potential offset. A coarse exterior potential representation is a plausible
cause, but an exact-space approximation comparison is needed to establish it.
The small stellar-interior potential L2 error in the earlier table excludes this
vacuum region.
### Cube-aligned gravity traces and angular sampling
At r=0.556875 R, fixed rays give gravity-gradient errors of approximately
+0.01167 GM/R^2 on body diagonals, -0.00355 on face diagonals, and +0.000674 on
axes. Each symmetry family is internally close. These are significant localized
trace errors, not deteriorated element conditioning. Because RT tangential
components can have one-sided traces on block seams, their solid-angle extent
has not yet been established.
Angular sampling sensitivity is measurable:
| Angular grid | Mean radial-gravity error at 0.556875 R | Mean potential error at 1.001 R |
|---|---:|---:|
| 6x12 | -6.0520e-4 | +1.27950e-2 |
| 12x24 | +5.3099e-4 | +1.29368e-2 |
| 24x48 | -1.8616e-5 | +1.28660e-2 |
Errors use fixed GM/R^2 and GM/R scales, respectively. The exact radial means
should not yet be treated as angularly converged. The roughly 1.3% exterior
potential discrepancy survives all three grids. The full 3D volume errors and
energy integrals are independent of this spherical sampling choice. The densest
replay located all 114,268 requested points and used 64 finite-exterior shells.
## Recommended next work
1. Use the saved fields for a cheap representation audit: compare the exterior
potential with the best approximation of exact -GM/r in the identical
potential space on the finite first exterior cells; densely sample both
one-sided interface traces. Do not form a global physical L2 potential norm
over the entire infinite exterior, where the exact 1/r potential is not
square-integrable.
2. Perturb the axis/face/body-diagonal rays slightly off the block seams to
determine whether the gravity extrema occupy finite angular regions or are
mainly trace effects.
3. Perform a controlled mesh/order convergence study, retaining separate field,
surface, virial, and border metrics. Do not replace this with a tighter Newton
tolerance on the same discretization.
4. Only after the analytic case is satisfactory, use the separately planned
ESTER comparison for rotating/nonanalytic models. ESTER was not run here.
The tools needed to separate nonlinear convergence from physical accuracy are
now in place. Global balance is encouraging; the field and exterior checks
show why it is too early to certify this model as physically verified.

View File

@@ -24,7 +24,165 @@ Run only the budget and choose its output path with:
./mean_field_experiments --experiment-output gravity_budget.csv --catch2 "[accuracy]"
```
## Reduced stellar-surface conditioning experiments
`stellar_null_space_experiments` is a dedicated diagnostic executable rather
than an ordinary verification or validation test. It constructs the analytic
`n = 3` Lane-Emden seed and probes only directions representable by the reduced
surface coordinates: uniform radial homology, three translation-like radial
dipoles, an axisymmetric oblate quadrupole, and a degree-12 zonal spherical
harmonic. Tangential, rotational, stellar-interior-only, and vacuum-only mesh
motions are deliberately absent because they are generated coordinates rather
than root unknowns.
The experiment prints rank-zero progress messages while it builds the seed,
solves its gravity field, and completes each surface-mode case. The reachability
probe records the surface-to-volume lift amplification, complete root Jacobian
response by block, and centered-difference agreement at zero and half the
Keplerian angular speed. Run it with:
```text
mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \
--experiment-output reduced_surface_reachability.csv \
--catch2 "[null_space][surface_modes][reachability]"
```
The gravity-completed probe solves the linearized mixed gravity subsystem for
the gravity-gradient and gravity-potential variations accompanying each
reduced surface mode. It then measures the complete reduced root response:
```text
mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \
--experiment-output gravity_completed_surface_modes.csv \
--catch2 "[null_space][surface_modes][gravity_completed]"
```
The gravity solver prints its convergence summary, while the experiment prints
the current mode and completed-case count. This probe prepares each rotation
state only once and does not repeat the expensive nonlinear finite-difference
calculations from the reachability diagnostic.
The surface-frequency probe injects normalized zonal spherical harmonics over
a range of angular degrees. For each degree it lifts the unit surface pattern
once, samples the coefficients of
`det(I + a grad(d))` at the production geometry-inspection points, and locates
the positive and negative critical fractional amplitudes without repeatedly
rebuilding trial geometries. It also records the minimum determinant at
fractional amplitudes `1e-4`, `1e-3`, and `1e-2`:
```text
mpirun -np 1 ./cmake-build-debug-homebrew/stellar_null_space_experiments \
--experiment-output surface_frequency_limits.csv \
--catch2 "[surface_modes][frequency_limit]"
```
The coupled conditioning probe evaluates an extension-aware `n = 3` homology
direction together with the nonuniform reduced surface modes. Its density and
enthalpy tangents include the coordinate-composition terms generated by the
non-affine interior extension, and its gravity variation is completed through
the discrete mixed subsystem so the fixed-infinity exterior response is
consistent. The probe prepares the equilibrium once, reuses one restricted
gravity operator and preconditioner, and uses analytic Jacobian actions:
```text
mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \
--experiment-output coupled_surface_conditioning.csv \
--catch2 "[null_space][surface_modes][conditioning]"
```
The CSV reports prescribed and gravity-completed responses by residual block,
the response normalized by the completed direction, lift conditioning where
applicable, and the convergence of each restricted gravity solve.
The homology mass-cancellation experiment evaluates the signed decomposition
```text
delta M = delta M_density + delta M_geometry
```
without solving gravity or preparing the complete coupled operator. The two
default-build cases provide the registered-order baseline and one uniform
spatial refinement:
```text
mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \
--experiment-output homology_mass_h0_p0.csv \
--catch2 "[null_space][homology][mass_normalization][p_refinement]"
mpirun -np 1 ./cmake-build-release-homebrew/stellar_null_space_experiments \
--experiment-output homology_mass_h1_p0.csv \
--catch2 "[null_space][homology][mass_normalization][h_refinement]"
```
A reproducible one-level uniform polynomial refinement uses a separate build so
all registered field families and their quadrature policies see the same
compile-time order increment:
```text
cmake -S . -B cmake-build-release-homebrew-p1 -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja \
-DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \
-DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ \
-DUMFPACK_DIR=/opt/homebrew/lib/cmake/UMFPACK \
-DXAD_DIR=/usr/local/lib/cmake/XAD \
-Dhypre_DIR=/usr/local/lib/cmake/HYPRE \
-Dmfem_DIR=/usr/local/lib/cmake/mfem \
-DBoost_DIR=/opt/homebrew/anaconda3/lib/cmake/Boost-1.82.0 \
-DMEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT=1
cmake --build cmake-build-release-homebrew-p1 \
--target stellar_null_space_experiments -j 8
mpirun -np 1 \
./cmake-build-release-homebrew-p1/stellar_null_space_experiments \
--experiment-output homology_mass_h0_p1.csv \
--catch2 "[null_space][homology][mass_normalization][p_refinement]"
```
A whole-Jacobian dense singular-value experiment is intentionally deferred.
The checked-in `sandbox.smesh` is too large for a useful dense SVD, and the
current matrix-free root operator does not provide a transpose action needed by
a scalable smallest-singular-value method.
The executable needs the same dependencies, generated module mapping, and
configuration registration as the existing Catch2 test executable. Add
`experiment_main.cpp` and `gravity_accuracy_budget.cpp` as a second executable
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.

View File

@@ -0,0 +1,482 @@
#!/usr/bin/env python3
"""Compare two completed n=1 solves separated by one uniform h-refinement.
Standard library only; never runs Newton or modifies input data. Exit 0 means
the comparison is usable, not that physical verification passed; 3 indicates
incompatible/incomplete comparison data, and 2 an execution/input error.
"""
import argparse
import csv
import hashlib
import math
from pathlib import Path
import sys
MODEL = "nonrotating_n1_fixed_mass_fixed_central_density_zero_surface_pressure"
TEXT_KEYS = ("model", "normalization")
CONSTANT_KEYS = ("G", "M", "R", "K", "rho_c")
ORDER_KEYS = ("polynomial_increment", "density_order", "enthalpy_order",
"potential_order", "gravity_flux_order", "displacement_order")
TOLERANCE_KEYS = ("absolute_tolerance", "relative_tolerance", "linear_tolerance")
ITERATION_KEYS = ("max_newton", "max_linear_iterations")
# Optional rows were added after the original coarse solve. Their absence is
# visible but does not erase the usable rows from that historical dataset.
METRICS = (
("density_relative_l2_error", "Interior density relative L2", True),
("enthalpy_relative_l2_error", "Interior enthalpy relative L2", True),
("potential_relative_l2_error", "Interior potential relative L2", True),
("gravity_gradient_relative_l2_error", "Interior gravity-gradient relative L2", True),
("pressure_relative_l2_error", "Interior pressure relative L2", True),
("surface_radius_relative_rms_error", "Surface radius RMS / R", True),
("volume_radius_relative_error", "Volume-equivalent radius relative error", True),
("virial_error", "Virial error, P(rho)", True),
("force_virial_error", "Force virial error, P(rho)", True),
("enthalpy_virial_error", "Virial error, P(h)", False),
("enthalpy_force_virial_error", "Force virial error, P(h)", False),
("gravity_energy_consistency", "Gravity-energy consistency error", True),
("mass_relative_error", "Mass relative error", True),
("binding_relative_error", "Binding-energy relative error", True),
("pressure_integral_relative_error", "Pressure-integral relative error", True),
("moment_of_inertia_relative_error", "Moment-of-inertia relative error", True),
("eos_enthalpy_scaled_rms", "Pointwise EOS RMS / central enthalpy", True),
("bernoulli_scaled_rms_variation", "Bernoulli RMS variation / GM/R", True),
("bernoulli_scaled_range", "Sampled Bernoulli range / GM/R", False),
("bernoulli_mean_scaled_error", "Bernoulli mean scaled error", False),
("normalized_bordered_residual", "Normalized bordered residual", True),
("normalized_unbordered_residual", "Normalized unbordered residual", True),
("normalized_central_border_action", "Normalized central-border action", False),
("quadrature_virial_absolute_change", "Virial quadrature-order change", True),
("quadrature_binding_relative_change", "Binding-energy quadrature-order change", True),
)
EXTERIOR = (
("potential_mean_error_scaled", "Finite-exterior maximum absolute shell-mean potential error"),
("potential_rms_error_scaled", "Finite-exterior maximum shell RMS potential error"),
("gravity_radial_mean_error_scaled", "Finite-exterior maximum absolute shell-mean radial-gravity error"),
("gravity_radial_rms_error_scaled", "Finite-exterior maximum shell RMS radial-gravity error"),
)
def number(value):
try:
return float(value)
except (ValueError, TypeError):
return math.nan
def read_metadata(path):
result = {}
for line in path.read_text().splitlines():
if "=" not in line:
continue
key, value = line.split("=", 1)
if key in result:
raise ValueError(f"Duplicate metadata key {key!r}: {path}")
result[key] = value
return result
def read_metrics(path):
result = {}
with path.open(newline="") as stream:
reader = csv.DictReader(stream)
if reader.fieldnames != ["metric", "value"]:
raise ValueError(f"Expected metric,value CSV schema: {path}")
for row in reader:
key = row["metric"]
if not key or key in result or None in row:
raise ValueError(f"Malformed/duplicate metric row: {path}")
result[key] = number(row["value"])
return result
def read_dataset(directory):
directory = directory.resolve()
return {"directory": directory,
"metadata": read_metadata(directory / "metadata.txt"),
"metrics": read_metrics(directory / "physical_metrics.csv")}
def integer(value):
try:
# Unlike float->int, this rejects a nonintegral or nonfinite count.
return int(value)
except (ValueError, TypeError):
return None
def compatibility(coarse, fine, coarse_control=None, fine_control=None):
checks = []
def check(name, okay, detail):
checks.append((name, None if okay is None else bool(okay), detail))
def match(left, right, keys, numeric=False, integral=False, prefix="solve"):
for key in keys:
a, b = left.get(key), right.get(key)
if integral:
okay = integer(a) is not None and integer(a) == integer(b)
elif numeric:
okay = math.isfinite(number(a)) and number(a) == number(b)
else:
okay = a is not None and a == b
check(f"{prefix}: matching {key}", okay, f"{a!r} / {b!r}")
for name, data in (("coarse", coarse), ("fine", fine)):
meta = data["metadata"]
check(f"{name}: completed converged solve",
meta.get("mode") == "solve" and meta.get("solver_converged") == "1"
and meta.get("physical_screen_passed") in ("0", "1"),
f"mode={meta.get('mode')}, converged={meta.get('solver_converged')}, "
f"physical screen={meta.get('physical_screen_passed')} (not required to pass)")
check(f"{name}: supported model and MPI ranks",
meta.get("model") == MODEL and meta.get("mpi_ranks") == "1",
"Requires this single-rank nonrotating n=1 benchmark.")
check(f"{name}: positive finite constants",
all(math.isfinite(number(meta.get(key))) and number(meta.get(key)) > 0
for key in CONSTANT_KEYS), "G, M, R, K, rho_c must be positive and finite.")
check(f"{name}: zero rotation", data["metrics"].get("angular_velocity_norm") == 0.0,
"Requires a saved angular_velocity_norm of exactly zero.")
for metric, _, required in METRICS:
if required:
value = data["metrics"].get(metric)
check(f"{name}: usable {metric}",
value is not None and math.isfinite(value) and value >= 0,
f"Saved value: {value!r}")
a, b = coarse["metadata"], fine["metadata"]
match(a, b, TEXT_KEYS)
match(a, b, CONSTANT_KEYS + TOLERANCE_KEYS, numeric=True)
match(a, b, ORDER_KEYS, integral=True)
match(a, b, ITERATION_KEYS, integral=True)
elements_a, elements_b = integer(a.get("elements")), integer(b.get("elements"))
check("one uniform hexahedral level: 8x elements",
elements_a is not None and elements_a > 0 and elements_b == 8 * elements_a,
f"{elements_a} -> {elements_b}; element counts alone do not establish mesh ancestry.")
quad_a, quad_b = coarse["metrics"].get("quadrature_order"), fine["metrics"].get("quadrature_order")
check("matching diagnostic quadrature order",
quad_a is not None and math.isfinite(quad_a) and quad_a == quad_b,
f"{quad_a!r} / {quad_b!r}")
for name, solve, control in (("coarse control", coarse, coarse_control),
("fine control", fine, fine_control)):
if control is None:
continue
meta = control["metadata"]
check(f"{name}: completed passing analytic control",
meta.get("mode") == "analytic-mesh" and meta.get("physical_screen_passed") == "1"
and meta.get("mpi_ranks") == "1", "Analytic-control status is read, not fabricated.")
match(solve["metadata"], meta, TEXT_KEYS, prefix=name)
match(solve["metadata"], meta, CONSTANT_KEYS, numeric=True, prefix=name)
match(solve["metadata"], meta, ORDER_KEYS + ("elements",), integral=True, prefix=name)
left = solve["metrics"].get("quadrature_order")
right = control["metrics"].get("quadrature_order")
check(f"{name}: matching diagnostic quadrature order",
left is not None and math.isfinite(left) and left == right, f"{left!r} / {right!r}")
solve_path = solve.get("directory")
control_path = control.get("directory")
solve_snapshot = solve_path / "input.smesh" if solve_path is not None else None
control_snapshot = control_path / "input.smesh" if control_path is not None else None
if solve_snapshot is not None and control_snapshot is not None and solve_snapshot.is_file() and control_snapshot.is_file():
solve_hash, control_hash = digest(solve_snapshot), digest(control_snapshot)
check(f"{name}: identical input snapshot SHA-256", solve_hash == control_hash,
f"{solve_hash} / {control_hash}")
else:
check(f"{name}: identical input snapshot SHA-256", None,
"Unavailable: one or both snapshots absent; equal geometry is not established by the element-count check.")
return checks
def checks_satisfied(checks):
# Optional unavailable provenance checks are not fabricated passes.
return all(okay is not False for _, okay, _ in checks)
def reduction(coarse, fine, eligible=True):
if coarse is None or fine is None:
return None, None, "missing"
if not math.isfinite(coarse) or not math.isfinite(fine):
return None, None, "nonfinite"
if coarse < 0 or fine < 0:
return None, None, "negative error magnitude"
if not eligible:
return None, None, "suppressed: compatibility checks failed"
if fine == 0:
return None, None, "both zero; no rate" if coarse == 0 else "fine zero; no finite rate"
if coarse == 0:
return 0.0, None, "coarse zero; no finite rate"
ratio = coarse / fine
rate = math.log2(coarse) - math.log2(fine)
if ratio == 0:
return ratio, rate, "ratio underflow; log-rate remains finite"
return ratio, rate, "two-level observation" if math.isfinite(ratio) else "ratio overflow; log-rate remains finite"
def exterior_diagnostics(data):
path = data["directory"] / "radial_profiles.csv"
if not path.exists():
return {}, "not available (radial_profiles.csv absent)"
with path.open(newline="") as stream:
rows = [row for row in csv.DictReader(stream) if number(row.get("r_over_R")) > 1.0]
if not rows:
return {}, "not available (no requested exterior shells)"
signature = sorted({(str(row.get("mu_points")), str(row.get("phi_points"))) for row in rows})
description = (f"{len(rows)} shells, r/R={rows[0].get('r_over_R')}..{rows[-1].get('r_over_R')}, "
f"angular grids={signature}; sampled-shell diagnostics only")
values = {}
for column, _ in EXTERIOR:
samples = [number(row.get(column)) for row in rows]
field = "potential" if column.startswith("potential_") else "gravity_radial"
complete = all(number(row.get("located_weight_fraction")) >= 1.0 - 1e-12
and number(row.get(field + "_valid_weight_fraction")) >= 1.0 - 1e-12
for row in rows)
values[column] = (max(abs(value) for value in samples)
if complete and all(math.isfinite(value) for value in samples) else math.nan)
return values, description
def digest(path):
if not path.is_file():
return "not available"
value = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
value.update(chunk)
return value.hexdigest()
def display(value):
if value is None:
return "not available"
if not math.isfinite(value):
return str(value)
return f"{value:.8g}"
def markdown(value):
return str(value).replace("|", "\\|").replace("\n", " ")
def write_comparison(coarse, fine, output, coarse_control=None, fine_control=None):
checks = compatibility(coarse, fine, coarse_control, fine_control)
eligible = checks_satisfied(checks)
rows = []
for metric, label, required in METRICS:
a, b = coarse["metrics"].get(metric), fine["metrics"].get(metric)
ratio, rate, status = reduction(a, b, eligible)
rows.append({"metric": metric, "description": label, "category": "volume_surface_or_solver",
"required_data": int(required), "coarse": a, "fine": b,
"coarse_over_fine": ratio, "observed_log2_rate": rate, "status": status,
"coarse_control": coarse_control["metrics"].get(metric) if coarse_control else None,
"fine_control": fine_control["metrics"].get(metric) if fine_control else None})
exterior_a, sampling_a = exterior_diagnostics(coarse)
exterior_b, sampling_b = exterior_diagnostics(fine)
for metric, label in EXTERIOR:
a, b = exterior_a.get(metric), exterior_b.get(metric)
status = "diagnostic only; no gate/rate (angular/radial samples need independent convergence checks)"
if a is None or b is None or not math.isfinite(a) or not math.isfinite(b):
status = "diagnostic unavailable or incomplete; no gate/rate"
rows.append({"metric": "sampled_exterior_max_" + metric, "description": label,
"category": "sampled_exterior_diagnostic_only", "required_data": 0,
"coarse": a, "fine": b, "coarse_over_fine": None, "observed_log2_rate": None,
"status": status, "coarse_control": None, "fine_control": None})
# No overwrite, including output paths that alias an input directory.
output.mkdir()
with (output / "comparison.csv").open("x", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
lines = ["# Two-level polytrope refinement comparison", "",
"Required comparison checks: " + ("satisfied." if eligible else "**FAILED; h-rates suppressed.**"), "",
"This is an observed two-level error comparison, not an established asymptotic order or a physical-accuracy certificate. "
"Exit 0 indicates usable comparison data, not a passing physical screen. "
"Reported rates are log2(E_coarse/E_fine), conditional on one uniform level halving the logical cell scale. "
"An 8x element ratio alone cannot prove mesh ancestry, stable source code, or identical geometry construction.", "",
"Optional unavailable snapshot checks are marked unavailable, not passed; they do not establish identical control/solve geometry.", "",
"Interior L2 errors use the saved full 3D stellar-volume diagnostics, not fitted spherical means. "
"A negative rate means this error increased. Missing/nonfinite/negative errors and zero denominators never receive a fabricated rate. "
"No existing physical screening budget is changed or reinterpreted.", "",
"## Provenance", ""]
for name, data in (("Coarse solve", coarse), ("Fine solve", fine),
("Coarse analytic control", coarse_control), ("Fine analytic control", fine_control)):
if data is None:
lines.append(f"- {name}: not supplied.")
continue
meta = data["metadata"]
lines += [f"- {name}: `{markdown(data['directory'])}`; elements={markdown(meta.get('elements'))}; "
f"saved physical_screen_passed={markdown(meta.get('physical_screen_passed'))}."]
for filename in ("metadata.txt", "physical_metrics.csv", "input.smesh"):
lines.append(f" - {filename} SHA-256: `{digest(data['directory'] / filename)}`")
lines.append(f" - compiled={markdown(meta.get('compiled', 'not available'))}; compiler={markdown(meta.get('compiler', 'not available'))}.")
lines += ["", "Input hashes identify these artifacts, not the production source/library version. "
"Confirm unchanged physics/seed/normalization/mapping code separately. Geometry-aware STROID refinement can regenerate "
"the curved mesh, rather than merely subdividing its old polynomial geometry.", "",
"## Error comparison", "",
"| Diagnostic | Coarse | Fine | E_coarse/E_fine | Observed log2 rate | Coarse control | Fine control | Status |",
"|---|---:|---:|---:|---:|---:|---:|---|"]
for row in rows:
lines.append("| " + " | ".join(markdown(value) for value in (
row["description"], display(row["coarse"]), display(row["fine"]),
display(row["coarse_over_fine"]), display(row["observed_log2_rate"]),
display(row["coarse_control"]), display(row["fine_control"]), row["status"])) + " |")
lines += ["", "Analytic-control field errors may be zero by construction; they are not FE best-approximation errors. "
"Controls are shown without subtraction from solve errors. EOS projection floors, cancellation in integral errors, "
"sampled extrema, and algebraic residual floors can produce rates unrelated to formal FE approximation order.", "",
"## Finite-exterior sampling (diagnostic only)", "",
f"- Coarse: {markdown(sampling_a)}.", f"- Fine: {markdown(sampling_b)}.", "",
"Exterior rows are maxima across the saved requested shells with r/R > 1, not pointwise global maxima or volume L2 norms. "
"Potential is scaled by GM/R and radial gravity by GM/R^2. No rate or pass gate is inferred from these samples; "
"missing or incomplete shell coverage remains unavailable.", "", "## Compatibility checks", "",
"| Check | Satisfied | Detail |", "|---|---|---|"]
lines.extend(f"| {markdown(name)} | {'unavailable' if okay is None else ('yes' if okay else 'NO')} | {markdown(detail)} |"
for name, okay, detail in checks)
(output / "comparison.md").write_text("\n".join(lines) + "\n")
return eligible
def self_check():
"""Synthetic-only checks; no project data, solver, or persistent outputs."""
import tempfile
import unittest
class ComparisonChecks(unittest.TestCase):
@staticmethod
def fixture(elements):
meta = {key: "1" for key in CONSTANT_KEYS + ORDER_KEYS}
meta.update({"model": MODEL, "normalization": "synthetic", "mode": "solve", "mpi_ranks": "1",
"solver_converged": "1", "physical_screen_passed": "0", "elements": str(elements),
"max_newton": "8", "max_linear_iterations": "80",
"absolute_tolerance": "1e-8", "relative_tolerance": "1e-8", "linear_tolerance": ".03"})
metrics = {key: 1e-4 for key, _, _ in METRICS}
metrics.update({"angular_velocity_norm": 0.0, "quadrature_order": 18.0})
return {"metadata": meta, "metrics": metrics}
def test_rates_and_edge_cases(self):
self.assertEqual(reduction(8.0, 1.0)[:2], (8.0, 3.0))
self.assertEqual(reduction(1.0, 4.0)[:2], (.25, -2.0))
for a, b in ((None, 1), (math.nan, 1), (1, math.inf), (-1, 1), (0, 0), (1, 0)):
self.assertIsNone(reduction(a, b)[1])
self.assertEqual(reduction(0, 1)[:2], (0.0, None))
self.assertEqual(reduction(8, 1, False)[:2], (None, None))
def test_compatibility(self):
a, b = self.fixture(19), self.fixture(152)
self.assertTrue(checks_satisfied(compatibility(a, b)))
for key, bad in (("mode", "replay"), ("solver_converged", "0"), ("elements", "151"),
("elements", "152.5"), ("G", "nan"), ("density_order", "2"),
("linear_tolerance", ".02"), ("max_newton", "9"), ("max_linear_iterations", "81")):
broken = {"metadata": dict(b["metadata"], **{key: bad}), "metrics": b["metrics"]}
self.assertFalse(checks_satisfied(compatibility(a, broken)), key)
del b["metrics"]["density_relative_l2_error"]
self.assertFalse(checks_satisfied(compatibility(a, b)))
def test_control(self):
a, b, control = self.fixture(19), self.fixture(152), self.fixture(19)
control["metadata"].update(mode="analytic-mesh", physical_screen_passed="1")
self.assertTrue(checks_satisfied(compatibility(a, b, control)))
self.assertTrue(any(okay is None for _, okay, _ in compatibility(a, b, control)))
control["metadata"]["elements"] = "152"
self.assertFalse(checks_satisfied(compatibility(a, b, control)))
def test_control_snapshot_hash(self):
a, b, control = self.fixture(19), self.fixture(152), self.fixture(19)
control["metadata"].update(mode="analytic-mesh", physical_screen_passed="1", max_newton="99")
with tempfile.TemporaryDirectory(prefix="polytrope-snapshot-check-") as temporary:
root = Path(temporary)
for name, data in (("solve", a), ("control", control)):
data["directory"] = root / name
data["directory"].mkdir()
(data["directory"] / "input.smesh").write_text("identical synthetic snapshot\n")
checks = compatibility(a, b, control)
self.assertTrue(checks_satisfied(checks))
self.assertTrue(any("snapshot SHA-256" in name and okay is True for name, okay, _ in checks))
(control["directory"] / "input.smesh").write_text("different geometry, same element count\n")
checks = compatibility(a, b, control)
self.assertFalse(checks_satisfied(checks))
self.assertTrue(any("snapshot SHA-256" in name and okay is False for name, okay, _ in checks))
def test_optional_metrics_and_exterior_coverage(self):
a, b = self.fixture(19), self.fixture(152)
del a["metrics"]["enthalpy_virial_error"]
self.assertTrue(checks_satisfied(compatibility(a, b)))
with tempfile.TemporaryDirectory(prefix="polytrope-exterior-check-") as temporary:
directory = Path(temporary)
a["directory"] = directory
self.assertEqual(exterior_diagnostics(a)[0], {})
exterior = {"r_over_R": 1.001, "located_weight_fraction": 1,
"potential_valid_weight_fraction": 1, "gravity_radial_valid_weight_fraction": 1,
"mu_points": 6, "phi_points": 12,
**{column: -.02 if "mean" in column else .03 for column, _ in EXTERIOR}}
for coverage in (1, .5):
exterior["potential_valid_weight_fraction"] = coverage
with (directory / "radial_profiles.csv").open("w", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=list(exterior))
writer.writeheader()
writer.writerow(exterior)
values, _ = exterior_diagnostics(a)
if coverage == 1:
self.assertEqual(values["potential_mean_error_scaled"], .02)
else:
self.assertTrue(math.isnan(values["potential_mean_error_scaled"]))
self.assertEqual(values["gravity_radial_rms_error_scaled"], .03)
def test_round_trip_and_no_overwrite(self):
with tempfile.TemporaryDirectory(prefix="polytrope-comparison-check-") as temporary:
root = Path(temporary)
data = []
for name, elements in (("coarse", 19), ("fine", 152)):
fixture = self.fixture(elements)
directory = root / name
directory.mkdir()
(directory / "metadata.txt").write_text("".join(f"{k}={v}\n" for k, v in fixture["metadata"].items()))
with (directory / "physical_metrics.csv").open("w", newline="") as stream:
writer = csv.writer(stream)
writer.writerow(("metric", "value"))
writer.writerows(fixture["metrics"].items())
data.append(read_dataset(directory))
output = root / "comparison"
self.assertTrue(write_comparison(*data, output))
self.assertTrue((output / "comparison.csv").is_file())
self.assertIn("not an established asymptotic order", (output / "comparison.md").read_text())
with self.assertRaises(FileExistsError):
write_comparison(*data, output)
data[1]["metadata"]["solver_converged"] = "0"
self.assertFalse(write_comparison(*data, root / "failed"))
with (root / "failed" / "comparison.csv").open(newline="") as stream:
self.assertTrue(all(not row["observed_log2_rate"] for row in csv.DictReader(stream)))
suite = unittest.defaultTestLoader.loadTestsFromTestCase(ComparisonChecks)
return unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("coarse", type=Path, nargs="?", help="Completed coarse solve directory (not a replay)")
parser.add_argument("fine", type=Path, nargs="?", help="Completed fine solve directory (not a replay)")
parser.add_argument("--coarse-control", type=Path)
parser.add_argument("--fine-control", type=Path)
parser.add_argument("--output", type=Path, help="Fresh output directory; never overwritten")
parser.add_argument("--self-check", action="store_true", help="Run synthetic-only checks and exit")
arguments = parser.parse_args()
if arguments.self_check:
if any((arguments.coarse, arguments.fine, arguments.output, arguments.coarse_control, arguments.fine_control)):
parser.error("--self-check cannot be combined with dataset/output arguments")
return 0 if self_check() else 3
if not all((arguments.coarse, arguments.fine, arguments.output)):
parser.error("coarse, fine, and --output are required")
try:
eligible = write_comparison(
read_dataset(arguments.coarse), read_dataset(arguments.fine), arguments.output,
read_dataset(arguments.coarse_control) if arguments.coarse_control else None,
read_dataset(arguments.fine_control) if arguments.fine_control else None)
print(f"Wrote {arguments.output / 'comparison.md'}; comparison prerequisites satisfied={eligible} "
"(not a physical verification pass)")
return 0 if eligible else 3
except (OSError, ValueError, csv.Error) as error:
print(f"Comparison error: {error}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,781 @@
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
import experiment;
import experiment.stellar_null_space;
import mean_field;
import test_helpers;
namespace {
namespace null_space = experiment::null_space;
struct GaugeMode final {
std::string name;
std::string family;
int axis{-1};
bool requiresGravityCompletion{true};
mfem::Vector direction;
};
class GravityUnknownJacobian final : public mfem::Operator {
public:
explicit GravityUnknownJacobian(
const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator
)
: mfem::Operator(
stellarOperator.GetLayout().size(null_space::gravityGradientValue) +
stellarOperator.GetLayout().size(null_space::gravityPotentialValue)
),
m_stellarOperator(stellarOperator),
m_gravityGradientSize(stellarOperator.GetLayout().size(null_space::gravityGradientValue)) {
MFEM_VERIFY(Width() == Height(), "The restricted gravity Jacobian must be square.");
}
void Mult(
const mfem::Vector &gravityDirection,
mfem::Vector &gravityAction
) const override {
MFEM_VERIFY(gravityDirection.Size() == Width(), "The restricted gravity direction has the wrong size.");
const mfem::Vector gravityGradientDirection(
const_cast<mfem::real_t *>(gravityDirection.GetData()), m_gravityGradientSize
);
const mfem::Vector gravityPotentialDirection(
const_cast<mfem::real_t *>(gravityDirection.GetData()) + m_gravityGradientSize,
Width() - m_gravityGradientSize
);
m_stellarOperator.GetGravityOperator().ApplyGravityUnknowns(
gravityGradientDirection, gravityPotentialDirection,
m_stellarOperator.GetGravityContext().GetGeometryContext(), gravityAction
);
}
[[nodiscard]] int gravity_gradient_size() const noexcept {
return m_gravityGradientSize;
}
private:
const mean_field::operators::PreparedStellarEquilibriumOperator &m_stellarOperator;
int m_gravityGradientSize;
};
struct GravityCompletionResult final {
mfem::Vector direction;
double rightHandSideNorm{0.0};
double residualNorm{0.0};
double relativeResidual{0.0};
double finalNorm{0.0};
int iterations{0};
bool solvePerformed{false};
};
void add_block_metrics(
std::map<
std::string,
double> &metrics,
const std::string &prefix,
const std::array<
double,
6> &norms
) {
for (std::size_t block = 0; block < norms.size(); ++block) {
metrics.emplace(prefix + null_space::residualBlockNames[block] + "_norm", norms[block]);
}
}
[[nodiscard]] mfem::Vector gravity_residual_blocks(
const mfem::Vector &completeAction,
const mean_field::operators::StellarEquilibriumLayout &layout
) {
const mfem::Vector gradient =
null_space::const_residual_view(completeAction, layout, null_space::gravityGradientResidual);
const mfem::Vector potential =
null_space::const_residual_view(completeAction, layout, null_space::gravityPotentialResidual);
mfem::Vector result(gradient.Size() + potential.Size());
mfem::Vector(result.GetData(), gradient.Size()) = gradient;
mfem::Vector(result.GetData() + gradient.Size(), potential.Size()) = potential;
return result;
}
void assign_gravity_completion(
mfem::Vector &completeDirection,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mfem::Vector &gravityCompletion,
const int gravityGradientSize
) {
const mfem::Vector gravityGradient(
const_cast<mfem::real_t *>(gravityCompletion.GetData()), gravityGradientSize
);
const mfem::Vector gravityPotential(
const_cast<mfem::real_t *>(gravityCompletion.GetData()) + gravityGradientSize,
gravityCompletion.Size() - gravityGradientSize
);
null_space::assign_value_block(completeDirection, layout, null_space::gravityGradientValue, gravityGradient);
null_space::assign_value_block(completeDirection, layout, null_space::gravityPotentialValue, gravityPotential);
}
[[nodiscard]] GravityCompletionResult solve_gravity_completion(
const mfem::Vector &prescribedAction,
const mean_field::operators::StellarEquilibriumLayout &layout,
const MPI_Comm communicator,
GravityUnknownJacobian &gravityJacobian,
mfem::MINRESSolver &gravitySolver
) {
mfem::Vector rightHandSide = gravity_residual_blocks(prescribedAction, layout);
rightHandSide *= -1.0;
GravityCompletionResult result;
result.direction.SetSize(gravityJacobian.Width());
result.direction = 0.0;
result.rightHandSideNorm = null_space::global_norm(rightHandSide, communicator);
const double skipThreshold = 100.0 * std::numeric_limits<double>::epsilon();
if (result.rightHandSideNorm <= skipThreshold) {
return result;
}
gravitySolver.Mult(rightHandSide, result.direction);
REQUIRE(gravitySolver.GetConverged());
mfem::Vector action;
gravityJacobian.Mult(result.direction, action);
action -= rightHandSide;
result.residualNorm = null_space::global_norm(action, communicator);
result.relativeResidual = result.residualNorm / result.rightHandSideNorm;
result.finalNorm = gravitySolver.GetFinalNorm();
result.iterations = gravitySolver.GetNumIterations();
result.solvePerformed = true;
REQUIRE(std::isfinite(result.relativeResidual));
return result;
}
class ExtensionAwareHomologyScalarCoefficient final : public mfem::Coefficient {
public:
ExtensionAwareHomologyScalarCoefficient(
const mfem::ParGridFunction &baseField,
const mfem::ParGridFunction &coordinateVelocity,
const mfem::Vector &referenceCenter,
const double physicalScalingExponent
)
: m_baseField(&baseField),
m_coordinateVelocity(&coordinateVelocity),
m_referenceCenter(&referenceCenter),
m_physicalScalingExponent(physicalScalingExponent) {
}
double Eval(
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integrationPoint
) override {
transformation.SetIntPoint(&integrationPoint);
mfem::Vector referencePosition;
mfem::Vector coordinateVelocity;
mfem::Vector baseGradient;
transformation.Transform(integrationPoint, referencePosition);
m_coordinateVelocity->GetVectorValue(transformation, integrationPoint, coordinateVelocity);
m_baseField->GetGradient(transformation, baseGradient);
coordinateVelocity -= referencePosition;
coordinateVelocity += *m_referenceCenter;
return -m_physicalScalingExponent * m_baseField->GetValue(transformation, integrationPoint) +
baseGradient * coordinateVelocity;
}
private:
const mfem::ParGridFunction *m_baseField;
const mfem::ParGridFunction *m_coordinateVelocity;
const mfem::Vector *m_referenceCenter;
double m_physicalScalingExponent;
};
[[nodiscard]] mfem::Vector project_extension_aware_homology_scalar(
mfem::ParFiniteElementSpace &finiteElementSpace,
const mean_field::field::FieldDofMap &fieldMap,
const mfem::Vector &baseReducedField,
const mfem::ParGridFunction &coordinateVelocity,
const mfem::Vector &referenceCenter,
const double physicalScalingExponent
) {
mfem::ParGridFunction baseField(&finiteElementSpace);
baseField.SetFromTrueDofs(fieldMap.scatter(baseReducedField));
ExtensionAwareHomologyScalarCoefficient coefficient(
baseField, coordinateVelocity, referenceCenter, physicalScalingExponent
);
mfem::ParGridFunction directionField(&finiteElementSpace);
directionField.ProjectCoefficient(coefficient);
mfem::Vector directionTrue;
directionField.GetTrueDofs(directionTrue);
return fieldMap.gather(directionTrue);
}
struct HomologyMassCancellation final {
double currentMass{0.0};
double targetMass{0.0};
double densityContribution{0.0};
double geometryContribution{0.0};
double completeDerivative{0.0};
};
[[nodiscard]] HomologyMassCancellation measure_homology_mass_cancellation(
const mean_field::operators::PreparedMassNormalizationOperator &massOperator,
const mfem::Vector &densityDirection,
const mfem::Vector &volumeDirection
) {
mfem::Vector densityAction;
mfem::Vector geometryAction;
mfem::Vector completeAction;
massOperator.ApplyDensityJacobianAction(densityDirection, densityAction);
massOperator.ApplyDisplacementJacobianAction(volumeDirection, geometryAction);
massOperator.ApplyCompleteJacobianAction(densityDirection, volumeDirection, completeAction);
REQUIRE(densityAction.Size() == 1);
REQUIRE(geometryAction.Size() == 1);
REQUIRE(completeAction.Size() == 1);
const double recomposedDerivative = densityAction(0) + geometryAction(0);
const double comparisonScale = std::max({1.0, std::abs(recomposedDerivative), std::abs(completeAction(0))});
CHECK(
std::abs(completeAction(0) - recomposedDerivative) <=
64.0 * std::numeric_limits<double>::epsilon() * comparisonScale
);
return {
.currentMass = massOperator.GetCurrentMass(),
.targetMass = massOperator.GetTargetMass(),
.densityContribution = densityAction(0),
.geometryContribution = geometryAction(0),
.completeDerivative = completeAction(0)
};
}
void add_homology_mass_metrics(
std::map<
std::string,
double> &metrics,
const HomologyMassCancellation &cancellation
) {
const double uncancelledMagnitude =
std::abs(cancellation.densityContribution) + std::abs(cancellation.geometryContribution);
const double targetScale = std::max(std::abs(cancellation.targetMass), std::numeric_limits<double>::epsilon());
metrics.emplace("current_mass", cancellation.currentMass);
metrics.emplace("target_mass", cancellation.targetMass);
metrics.emplace("base_mass_residual", cancellation.currentMass - cancellation.targetMass);
metrics.emplace(
"relative_base_mass_residual", (cancellation.currentMass - cancellation.targetMass) / targetScale
);
metrics.emplace("density_mass_derivative", cancellation.densityContribution);
metrics.emplace("geometry_mass_derivative", cancellation.geometryContribution);
metrics.emplace("complete_mass_derivative", cancellation.completeDerivative);
metrics.emplace("mass_derivative_uncancelled_magnitude", uncancelledMagnitude);
metrics.emplace(
"mass_derivative_relative_cancellation_error",
std::abs(cancellation.completeDerivative) /
std::max(uncancelledMagnitude, std::numeric_limits<double>::epsilon())
);
metrics.emplace("complete_mass_derivative_per_target_mass", cancellation.completeDerivative / targetScale);
}
[[nodiscard]] GaugeMode make_homology_mode(
null_space::N3Equilibrium &fixture,
const null_space::SurfaceMode &uniformRadialMode
) {
const auto &layout = fixture.stellar_operator().GetLayout();
const auto &state = fixture.state();
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
const mfem::Vector volumeDirection = fixture.lifted_surface_direction(uniformRadialMode.direction);
mfem::ParGridFunction coordinateVelocity(fixture.fem().displacementFes.get());
coordinateVelocity.SetFromTrueDofs(volumeDirection);
const mean_field::field::FieldDofMap densityMap =
mean_field::field::make_field_dof_map<mean_field::field::Density, null_space::DomainSchema>(
*fixture.fem().densityFes
);
const mean_field::field::FieldDofMap enthalpyMap =
mean_field::field::make_field_dof_map<mean_field::field::Enthalpy, null_space::DomainSchema>(
*fixture.fem().enthalpyFes
);
const mfem::Vector &referenceCenter = fixture.model().surfaceDeformationPrescription().referenceCenter();
const mfem::Vector densityDirection = project_extension_aware_homology_scalar(
*fixture.fem().densityFes, densityMap,
null_space::const_value_view(state, layout, null_space::densityValue), coordinateVelocity, referenceCenter,
3.0
);
null_space::assign_value_block(direction, layout, null_space::densityValue, densityDirection);
null_space::assign_value_block(
direction, layout, null_space::surfaceDeformationValue,
null_space::const_value_view(uniformRadialMode.direction, layout, null_space::surfaceDeformationValue)
);
/*
* A physical homology scales rho and h, while the power-law mesh
* extension moves interior coordinates non-affinely. The scalar
* tangents therefore contain the coordinate-composition term
* grad(f) dot (v - (X-Xc)) in addition to their physical scaling.
* Gravity is completed through the discrete mixed subsystem below,
* which also supplies the correct fixed-infinity exterior response.
*/
const mfem::Vector enthalpyDirection = project_extension_aware_homology_scalar(
*fixture.fem().enthalpyFes, enthalpyMap,
null_space::const_value_view(state, layout, null_space::enthalpyValue), coordinateVelocity, referenceCenter,
1.0
);
null_space::assign_value_block(direction, layout, null_space::enthalpyValue, enthalpyDirection);
null_space::value_view(direction, layout, null_space::bernoulliValue)(0) =
-null_space::const_value_view(state, layout, null_space::bernoulliValue)(0);
return {
.name = "n3_homology",
.family = "homology",
.axis = -1,
.requiresGravityCompletion = true,
.direction = std::move(direction)
};
}
[[nodiscard]] std::vector<GaugeMode> make_gauge_modes(null_space::N3Equilibrium &fixture) {
auto surfaceModes = null_space::make_surface_modes(fixture);
const auto homologyMode = std::ranges::find_if(surfaceModes, [](const null_space::SurfaceMode &mode) {
return mode.kind == null_space::SurfaceModeKind::uniform_radial;
});
MFEM_VERIFY(homologyMode != surfaceModes.end(), "The reduced surface modes do not contain homology.");
std::vector<GaugeMode> modes;
modes.reserve(6);
modes.push_back(make_homology_mode(fixture, *homologyMode));
for (auto &surfaceMode : surfaceModes) {
if (surfaceMode.kind == null_space::SurfaceModeKind::uniform_radial) {
continue;
}
modes.push_back(
{.name = surfaceMode.name,
.family = null_space::surface_mode_kind_name(surfaceMode.kind),
.axis = surfaceMode.axis,
.requiresGravityCompletion = true,
.direction = std::move(surfaceMode.direction)}
);
}
return modes;
}
[[nodiscard]] long long global_nonzero_count(
const mfem::Vector &vector,
const MPI_Comm communicator
) {
long long localCount = 0;
for (int index = 0; index < vector.Size(); ++index) {
if (vector(index) != 0.0) {
++localCount;
}
}
long long globalCount = 0;
MPI_Allreduce(&localCount, &globalCount, 1, MPI_LONG_LONG, MPI_SUM, communicator);
return globalCount;
}
[[nodiscard]] mean_field::models::structure::StructureSeed make_n3_seed(null_space::Model &model) {
constexpr double surfaceCoordinate = 6.8968486193769603755;
constexpr int radialSampleCount = 8192;
const double pi = std::acos(-1.0);
const double radius = mean_field::utils::RADIUS;
const double targetMass = mean_field::utils::MASS;
constexpr double dimensionlessMass = 2.0182359509662283534;
const double polytropicConstant =
pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
const double centralDensity =
std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / radius, 3.0);
return model.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = radialSampleCount});
}
[[nodiscard]] mfem::Vector project_n3_density(
const mean_field::fem::FEM &fem,
const mean_field::models::structure::StructureSeed &seed
) {
const auto interpolate = [](const mfem::Vector &radii, const mfem::Vector &values, const double radius) {
if (radius <= radii(0)) {
return values(0);
}
const int finalIndex = radii.Size() - 1;
if (radius >= radii(finalIndex)) {
return values(finalIndex);
}
int lower = 0;
int upper = finalIndex;
while (upper - lower > 1) {
const int middle = lower + (upper - lower) / 2;
if (radii(middle) <= radius) {
lower = middle;
} else {
upper = middle;
}
}
const double fraction = (radius - radii(lower)) / (radii(upper) - radii(lower));
return (1.0 - fraction) * values(lower) + fraction * values(upper);
};
mfem::FunctionCoefficient densityCoefficient([&seed, &interpolate](const mfem::Vector &position) {
const double radius = position.Norml2();
return radius >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.density, radius);
});
mfem::ParGridFunction densityField(fem.densityFes.get());
densityField.ProjectCoefficient(densityCoefficient);
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
return densityTrue;
}
void run_homology_mass_cancellation_experiment(const int hRefinementLevel) {
REQUIRE(hRefinementLevel >= 0);
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, hRefinementLevel);
REQUIRE(fem.okay());
null_space::Model model = null_space::make_model();
const mean_field::models::structure::StructureSeed seed = make_n3_seed(model);
const mfem::Vector densityTrue = project_n3_density(fem, seed);
auto deformation = model.compileDomainDeformation(fem);
const auto &surface = deformation.surfaceDeformationPrescription();
mfem::Vector zeroSurfaceParameters(surface.parameterCount());
mfem::Vector homologySurfaceDirection(surface.parameterCount());
zeroSurfaceParameters = 0.0;
for (int parameter = 0; parameter < homologySurfaceDirection.Size(); ++parameter) {
homologySurfaceDirection(parameter) = surface.referenceRadius(parameter);
}
mfem::Vector volumeDirection(deformation.volumeDisplacementSize());
deformation.applyJacobian(zeroSurfaceParameters, homologySurfaceDirection, volumeDirection);
mfem::ParGridFunction coordinateVelocity(fem.displacementFes.get());
coordinateVelocity.SetFromTrueDofs(volumeDirection);
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
fem, *fem.domainMapperStateless
);
const mean_field::field::FieldDofMap &densityMap = gravityContext.GetDensityMap();
const mfem::Vector reducedDensity = densityMap.gather(densityTrue);
const mfem::Vector densityDirection = project_extension_aware_homology_scalar(
*fem.densityFes, densityMap, reducedDensity, coordinateVelocity, surface.referenceCenter(), 3.0
);
mfem::Vector zeroDisplacement(fem.displacementFes->GetTrueVSize());
mfem::Vector zeroGravityGradient(fem.gravityFluxFes->GetTrueVSize());
mfem::Vector zeroGravityPotential(fem.gravityPotentialFes->GetTrueVSize());
zeroDisplacement = 0.0;
zeroGravityGradient = 0.0;
zeroGravityPotential = 0.0;
const mean_field::operators::MassNormalizationDependencies dependencies{
.discretization = {.identity = 9101, .revision = 1},
.density = {.identity = 9103, .revision = 1},
.displacement = {.identity = 9109, .revision = 1},
.targetMass = {.identity = 9127, .revision = 1}
};
gravityContext.Prepare(
{.density = reducedDensity,
.displacement = gravityContext.GetDisplacementMap().gather(zeroDisplacement),
.gravity_gradient = gravityContext.GetGravityGradientMap().gather(zeroGravityGradient),
.gravity_potential = gravityContext.GetGravityPotentialMap().gather(zeroGravityPotential)},
{.discretization = {.value = dependencies.discretization.revision},
.displacement = {.value = dependencies.displacement.revision},
.density = {.value = dependencies.density.revision},
.gravity_gradient = {.value = 1},
.gravity_potential = {.value = 1}}
);
mean_field::operators::PreparedMassNormalizationOperator massOperator(
fem, *fem.domainMapperStateless, gravityContext
);
massOperator.Prepare({.targetMass = mean_field::utils::MASS}, dependencies);
const mfem::Vector reducedVolumeDirection = gravityContext.GetDisplacementMap().gather(volumeDirection);
const HomologyMassCancellation cancellation =
measure_homology_mass_cancellation(massOperator, densityDirection, reducedVolumeDirection);
std::map<std::string, double> metrics{
{"density_direction_norm", null_space::global_norm(densityDirection, fem.mesh->GetComm())},
{"surface_direction_norm", null_space::global_norm(homologySurfaceDirection, fem.mesh->GetComm())},
{"volume_direction_norm", null_space::global_norm(volumeDirection, fem.mesh->GetComm())},
{"global_element_count", static_cast<double>(fem.mesh->GetGlobalNE())},
{"global_density_true_dof_count", static_cast<double>(fem.densityFes->GlobalTrueVSize())},
{"global_displacement_true_dof_count", static_cast<double>(fem.displacementFes->GlobalTrueVSize())},
{"global_surface_parameter_count", static_cast<double>(surface.globalParameterCount())}
};
add_homology_mass_metrics(metrics, cancellation);
int rank = 0;
MPI_Comm_rank(fem.mesh->GetComm(), &rank);
if (rank == 0) {
const int pRefinementLevel = mean_field::field::uniformPolynomialOrderIncrement;
experiment::record_experiment_result(
"n3_homology_mass_cancellation",
"h" + std::to_string(hRefinementLevel) + "_p" + std::to_string(pRefinementLevel),
{{"h_refinement_level", std::to_string(hRefinementLevel)},
{"p_refinement_level", std::to_string(pRefinementLevel)},
{"density_polynomial_order", std::to_string(mean_field::field::Density::Scalar::familyOrder)},
{"enthalpy_polynomial_order", std::to_string(mean_field::field::Enthalpy::Scalar::familyOrder)},
{"displacement_polynomial_order",
std::to_string(mean_field::field::Displacement::Vector::familyOrder)},
{"gravity_polynomial_order", std::to_string(mean_field::field::Gravity::Potential::familyOrder)},
{"mesh_file", test_utils::setup_args().mesh_file}},
std::move(metrics)
);
}
}
} // namespace
TEST_CASE(
"Coupled Stellar Equilibrium Homology And Reduced Surface Mode Responses",
"[null_space][surface_modes][conditioning][homology]"
) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = std::min(args.p.rtol, 1.0e-12);
args.p.atol = std::min(args.p.atol, 1.0e-13);
args.p.max_iters = std::max(args.p.max_iters, 1500);
null_space::N3Equilibrium fixture(std::move(args));
const MPI_Comm communicator = fixture.fem().mesh->GetComm();
int rank = 0;
MPI_Comm_rank(communicator, &rank);
const std::vector<GaugeMode> modes = make_gauge_modes(fixture);
const auto &stellarOperator = fixture.stellar_operator();
const auto &layout = stellarOperator.GetLayout();
GravityUnknownJacobian gravityJacobian(stellarOperator);
mean_field::operators::ReducedGravityFieldPreconditioner gravityPreconditioner(
fixture.fem(), stellarOperator.GetGravityContext().GetGeometryContext()
);
mfem::MINRESSolver gravitySolver(communicator);
gravitySolver.SetOperator(gravityJacobian);
gravitySolver.SetPreconditioner(gravityPreconditioner);
gravitySolver.SetRelTol(1.0e-10);
gravitySolver.SetAbsTol(1.0e-12);
gravitySolver.SetMaxIter(1500);
gravitySolver.SetPrintLevel(0);
for (std::size_t modeIndex = 0; modeIndex < modes.size(); ++modeIndex) {
const GaugeMode &mode = modes[modeIndex];
null_space::report_progress(
communicator,
"evaluating " + mode.name + " (" + std::to_string(modeIndex + 1) + "/" + std::to_string(modes.size()) + ")"
);
const double prescribedInputNorm = null_space::global_norm(mode.direction, communicator);
REQUIRE(std::isfinite(prescribedInputNorm));
REQUIRE(prescribedInputNorm > 0.0);
const mfem::Vector prescribedAction = fixture.jacobian_action(mode.direction);
const double prescribedActionNorm = null_space::global_norm(prescribedAction, communicator);
GravityCompletionResult completion;
completion.direction.SetSize(gravityJacobian.Width());
completion.direction = 0.0;
mfem::Vector completedDirection(mode.direction);
if (mode.requiresGravityCompletion) {
completion =
solve_gravity_completion(prescribedAction, layout, communicator, gravityJacobian, gravitySolver);
assign_gravity_completion(
completedDirection, layout, completion.direction, gravityJacobian.gravity_gradient_size()
);
}
const mfem::Vector completedAction = fixture.jacobian_action(completedDirection);
const double completedInputNorm = null_space::global_norm(completedDirection, communicator);
const double completedActionNorm = null_space::global_norm(completedAction, communicator);
const double completionNorm = null_space::global_norm(completion.direction, communicator);
REQUIRE(std::isfinite(prescribedActionNorm));
REQUIRE(std::isfinite(completedInputNorm));
REQUIRE(std::isfinite(completedActionNorm));
REQUIRE(completedInputNorm > 0.0);
std::map<std::string, double> metrics{
{"prescribed_input_norm", prescribedInputNorm},
{"prescribed_action_norm", prescribedActionNorm},
{"completed_input_norm", completedInputNorm},
{"completed_action_norm", completedActionNorm},
{"normalized_completed_response", completedActionNorm / completedInputNorm},
{"gravity_completion_norm", completionNorm},
{"gravity_solve_rhs_norm", completion.rightHandSideNorm},
{"gravity_solve_residual_norm", completion.residualNorm},
{"gravity_solve_relative_residual", completion.relativeResidual},
{"gravity_solve_final_norm", completion.finalNorm},
{"gravity_solve_iterations", static_cast<double>(completion.iterations)},
{"global_nonzero_input_dofs", static_cast<double>(global_nonzero_count(mode.direction, communicator))}
};
if (mode.family == "homology") {
const mfem::Vector densityDirection =
null_space::const_value_view(mode.direction, layout, null_space::densityValue);
const mfem::Vector volumeDirection = fixture.lifted_surface_direction(mode.direction);
const HomologyMassCancellation massCancellation = measure_homology_mass_cancellation(
stellarOperator.GetMassNormalizationOperator(), densityDirection, volumeDirection
);
add_homology_mass_metrics(metrics, massCancellation);
}
if (completedActionNorm > 0.0) {
metrics.emplace("gravity_completion_reduction", prescribedActionNorm / completedActionNorm);
}
add_block_metrics(
metrics, "prescribed_", null_space::residual_block_norms(prescribedAction, layout, communicator)
);
add_block_metrics(
metrics, "completed_", null_space::residual_block_norms(completedAction, layout, communicator)
);
if (rank == 0) {
experiment::record_experiment_result(
"coupled_reduced_surface_mode_conditioning", mode.name,
{{"mode_family", mode.family},
{"axis", std::to_string(mode.axis)},
{"gravity_completion_requested", mode.requiresGravityCompletion ? "true" : "false"},
{"gravity_solve_performed", completion.solvePerformed ? "true" : "false"},
{"rotation_fraction_of_keplerian", "0.0"},
{"mesh_file", test_utils::setup_args().mesh_file},
{"local_state_dofs", std::to_string(stellarOperator.Width())}},
std::move(metrics)
);
}
}
null_space::report_progress(communicator, "coupled reduced surface-mode probe complete; writing CSV output");
}
TEST_CASE(
"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<GaugeMode> 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<mean_field::field::Enthalpy, null_space::DomainSchema>(
*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<mean_field::field::Enthalpy>(
*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, &centralEnthalpy, 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<mean_field::eos::quantity::Density>(
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<double>::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]"
) {
run_homology_mass_cancellation_experiment(0);
}
TEST_CASE(
"N3 Homology Mass Cancellation Under Uniform Spatial Refinement",
"[null_space][homology][mass_normalization][convergence][h_refinement]"
) {
run_homology_mass_cancellation_experiment(1);
}

View File

@@ -6,6 +6,7 @@
#include <fourdst/config/config.h>
#include <mfem.hpp>
#include <catch2/catch_test_case_info.hpp>
#include <fstream>
#include <iomanip>
#include <iostream>
@@ -14,8 +15,6 @@
#include <string>
#include <string_view>
#include <vector>
#include <catch2/catch_test_case_info.hpp>
import mean_field;
import test_helpers;
@@ -107,10 +106,8 @@ public:
void testCaseEnded(const Catch::TestCaseStats &statistics) override {
StreamingReporterBase::testCaseEnded(statistics);
const bool passed = statistics.totals.assertions.allPassed();
std::cout << (passed ? "PASS " : "FAIL ")
<< statistics.testInfo->name
<< " (" << statistics.totals.assertions.passed
<< " assertions)\n";
std::cout << (passed ? "PASS " : "FAIL ") << statistics.testInfo->name << " ("
<< statistics.totals.assertions.passed << " assertions)\n";
}
void testRunEnded(const Catch::TestRunStats &statistics) override {
@@ -119,9 +116,15 @@ public:
}
};
CATCH_REGISTER_REPORTER("experiment", ExperimentReporter)
CATCH_REGISTER_REPORTER(
"experiment",
ExperimentReporter
)
int main(int argc, char* argv[]) {
int main(
int argc,
char *argv[]
) {
fourdst::config::Config<mean_field::utils::Args> config;
CLI::App app{"Mean Field accuracy experiments"};
@@ -171,8 +174,8 @@ int main(int argc, char* argv[]) {
bool has_reporter = false;
for (const std::string &argument : catch_arguments) {
has_reporter = has_reporter || argument == "-r" || argument == "--reporter" ||
argument.starts_with("-r=") || argument.starts_with("--reporter=");
has_reporter = has_reporter || argument == "-r" || argument == "--reporter" || argument.starts_with("-r=") ||
argument.starts_with("--reporter=");
}
if (!has_reporter) {
catch_arguments.emplace_back("--reporter");

View File

@@ -52,14 +52,18 @@ export namespace experiment {
inline void record_experiment_result(
const std::string &experiment_name,
const std::string &case_name,
std::map<std::string, std::string> parameters,
std::map<std::string, double> metrics
std::map<
std::string,
std::string> parameters,
std::map<
std::string,
double> metrics
) {
ExperimentRegistry::instance().add_result({
.experiment_name = experiment_name,
ExperimentRegistry::instance().add_result(
{.experiment_name = experiment_name,
.case_name = case_name,
.parameters = std::move(parameters),
.metrics = std::move(metrics)
});
}
.metrics = std::move(metrics)}
);
}
} // namespace experiment

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,521 @@
#pragma once
// Include after `import mean_field;`. This is deliberately an experiment-only
// observer: it uses the production estimator and mapper without changing either.
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <span>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
namespace experiment {
namespace geometry_detail {
inline double Frobenius(const mfem::DenseMatrix &matrix) {
double squared = 0.0;
for (int row = 0; row < matrix.Height(); ++row) {
for (int column = 0; column < matrix.Width(); ++column) {
squared += matrix(row, column) * matrix(row, column);
}
}
return std::sqrt(squared);
}
inline double Difference(const mfem::DenseMatrix &left, const mfem::DenseMatrix &right) {
double squared = 0.0;
for (int row = 0; row < left.Height(); ++row) {
for (int column = 0; column < left.Width(); ++column) {
const double difference = left(row, column) - right(row, column);
squared += difference * difference;
}
}
return std::sqrt(squared);
}
inline std::array<double, 4> DeterminantCoefficients(
const mfem::DenseMatrix &base, const mfem::DenseMatrix &direction
) {
std::array<double, 4> coefficients{};
const int dimension = base.Height();
mfem::DenseMatrix selected(dimension);
for (int mask = 0; mask < (1 << dimension); ++mask) {
int degree = 0;
for (int column = 0; column < dimension; ++column) {
const bool useDirection = (mask & (1 << column)) != 0;
degree += useDirection ? 1 : 0;
for (int row = 0; row < dimension; ++row) {
selected(row, column) = useDirection ? direction(row, column) : base(row, column);
}
}
coefficients[static_cast<std::size_t>(degree)] += selected.Det();
}
return coefficients;
}
inline double Polynomial(const std::array<double, 4> &coefficients, const double alpha) {
return ((coefficients[3] * alpha + coefficients[2]) * alpha + coefficients[1]) * alpha + coefficients[0];
}
inline std::ofstream OpenCsv(const std::filesystem::path &path) {
std::ofstream stream(path);
if (!stream) {
throw std::runtime_error("Cannot open geometry diagnostic output: " + path.string());
}
stream << std::setprecision(17);
return stream;
}
inline void Coordinates(std::ostream &stream, const mfem::Vector &position) {
for (int component = 0; component < 3; ++component) {
stream << ',' << (component < position.Size() ? position(component) : 0.0);
}
}
struct ElementReport final {
int element{-1};
int globalRule{-1};
mean_field::deformation::LargestSafeNewtonStepSizeEstimate estimate{};
};
// Reference-mesh probes deliberately avoid DomainMapper and inverse
// Jacobians: an exact vertex may be singular even though interior
// quadrature points remain valid.
inline void InspectReferenceCorners(
mfem::ParMesh &mesh, const std::filesystem::path &outputDirectory, const std::string &label
) {
auto csv = OpenCsv(outputDirectory / (label + "_reference_corner_probes.csv"));
csv << "element,attribute,vertex,inward_fraction,xi,eta,zeta,reference_x,reference_y,reference_z,"
"reference_radius,reference_det,reference_sigma_min,reference_sigma_mid,reference_sigma_max\n";
mfem::Vector position;
for (const int element : {0, 9, 18, 27, 36, 45, 54, 63}) {
if (element >= mesh.GetNE()) {
continue;
}
auto *transformation = mesh.GetElementTransformation(element);
const auto *vertices = mfem::Geometries.GetVertices(transformation->GetGeometryType());
const auto &center = mfem::Geometries.GetCenter(transformation->GetGeometryType());
for (int vertex = 0; vertex < vertices->GetNPoints(); ++vertex) {
const auto &corner = vertices->IntPoint(vertex);
for (const double epsilon : {0.0, 1.0e-5, 1.0e-4, 0.001, 0.01, 0.05, 0.1}) {
mfem::IntegrationPoint point;
point.Set3(
(1.0 - epsilon) * corner.x + epsilon * center.x,
(1.0 - epsilon) * corner.y + epsilon * center.y,
(1.0 - epsilon) * corner.z + epsilon * center.z
);
transformation->SetIntPoint(&point);
transformation->Transform(point, position);
const auto &jacobian = transformation->Jacobian();
csv << element << ',' << transformation->Attribute << ',' << vertex << ',' << epsilon << ','
<< point.x << ',' << point.y << ',' << point.z;
Coordinates(csv, position);
csv << ',' << position.Norml2() << ',' << jacobian.Det() << ',' << jacobian.CalcSingularvalue(2)
<< ',' << jacobian.CalcSingularvalue(1) << ',' << jacobian.CalcSingularvalue(0) << '\n';
}
}
}
}
} // namespace geometry_detail
struct GeometryReport final {
double boundaryStep{1.0};
double safeStep{1.0};
int limitingElement{-1};
int limitedElements{0};
int elementsWithinOnePartPerMillion{0};
int elementsWithinOnePercent{0};
double maximumRelativeMappingError{0.0};
double maximumRelativeDeterminantError{0.0};
double maximumReportedPointGradient{0.0};
int invalidDirectSamples{0};
std::uint64_t sampleCount{0};
};
// Sandbox-specific inspection of the negative-corner core element. The
// continuous comparison describes a logical r_L^power radial extension before
// nodal interpolation, with logical stellar-surface radius equal to one.
// The measured direction is always the supplied production FE field.
inline void InspectCoreDiagonal(
const mean_field::fem::FEM &finiteElements,
const mfem::Vector &volumeDirection,
const double cornerSurfaceAmplitude,
const std::filesystem::path &outputDirectory,
const std::string &label,
const double radialPower = 2.0
) {
using namespace geometry_detail;
auto &space = *finiteElements.displacementFes;
auto &mesh = *finiteElements.mesh;
auto &logicalMesh = *finiteElements.logicalReferenceMesh;
int ranks = 0;
MPI_Comm_size(space.GetComm(), &ranks);
if (ranks != 1 || mesh.GetNE() < 1 || logicalMesh.GetNE() != mesh.GetNE() || mesh.GetAttribute(0) != 1 ||
volumeDirection.Size() != space.GetTrueVSize() || !std::isfinite(cornerSurfaceAmplitude) ||
!std::isfinite(radialPower) || radialPower <= 0.0) {
throw std::invalid_argument("Core-diagonal probe requires a single-rank compatible core displacement.");
}
auto *physicalTransformation = mesh.GetElementTransformation(0);
auto *logicalTransformation = logicalMesh.GetElementTransformation(0);
if (physicalTransformation->GetGeometryType() != mfem::Geometry::CUBE) {
std::cout << "CoreDiagonal[" << label << "]: skipped unrecognized non-hex core element\n";
return;
}
mfem::Vector physicalPosition(3), logicalPosition(3);
for (const double parameter : {0.0, 0.5, 1.0}) {
mfem::IntegrationPoint point;
point.Set3(parameter, parameter, parameter);
logicalTransformation->Transform(point, logicalPosition);
physicalTransformation->Transform(point, physicalPosition);
bool recognized = logicalPosition(0) < 0.0 && physicalPosition(0) < 0.0;
for (int component = 1; component < 3; ++component) {
recognized = recognized && std::abs(logicalPosition(component) - logicalPosition(0)) < 1.0e-11 &&
std::abs(physicalPosition(component) - physicalPosition(0)) < 1.0e-11;
}
// Element 0 in sandbox.smesh covers logical diagonal [-1/4,-1/8].
const double expectedLogical = -0.25 + 0.125 * parameter;
recognized = recognized && std::abs(logicalPosition(0) - expectedLogical) < 1.0e-11;
if (!recognized) {
std::cout << "CoreDiagonal[" << label << "]: skipped unrecognized sandbox core diagonal\n";
return;
}
}
mfem::ParGridFunction direction(&space);
direction.SetFromTrueDofs(volumeDirection);
mfem::Array<int> dofs;
auto *dofTransformation = space.GetElementVDofs(0, dofs);
mfem::Vector elementDirection;
direction.GetSubVector(dofs, elementDirection);
if (dofTransformation != nullptr) {
dofTransformation->InvTransformPrimal(elementDirection);
}
const auto &finiteElement = *space.GetFE(0);
const mean_field::mapping::ElementDisplacementData directionData(finiteElement, elementDirection, space.GetOrdering());
mfem::Vector shape(finiteElement.GetDof()), value(3), directionAlongDiagonal(3), physicalAlongDiagonal(3);
mfem::Vector logicalAlongDiagonal(3), unitDiagonal(3), radial(3), radialDerivative(3);
unitDiagonal = 1.0;
mfem::DenseMatrix derivativeShape(finiteElement.GetDof(), 3), directionGradientHat(3);
auto csv = OpenCsv(outputDirectory / (label + "_core_diagonal.csv"));
csv << "element,s,logical_x,logical_y,logical_z,reference_x,reference_y,reference_z,logical_radius,physical_radius,"
"corner_surface_amplitude,drL_ds,dr_ds,actual_radial_displacement,desired_radial_displacement,"
"actual_du_radial_ds,desired_du_radial_ds,actual_radial_gradient,desired_radial_gradient,"
"actual_du_x_ds,actual_du_y_ds,actual_du_z_ds,reference_det,reference_sigma_min,reference_sigma_max,"
"radial_power,relative_det_coefficient_0,relative_det_coefficient_1,relative_det_coefficient_2,relative_det_coefficient_3\n";
for (const double parameter : {0.0, 0.005, 0.010885670926971493, 0.02, 0.05, 0.1, 0.2,
0.276393202250021, 0.5, 0.723606797749979, 0.9, 1.0}) {
mfem::IntegrationPoint point;
point.Set3(parameter, parameter, parameter);
logicalTransformation->SetIntPoint(&point);
physicalTransformation->SetIntPoint(&point);
logicalTransformation->Transform(point, logicalPosition);
physicalTransformation->Transform(point, physicalPosition);
const auto &referenceJacobian = physicalTransformation->Jacobian();
referenceJacobian.Mult(unitDiagonal, physicalAlongDiagonal);
logicalTransformation->Jacobian().Mult(unitDiagonal, logicalAlongDiagonal);
finiteElement.CalcShape(point, shape);
finiteElement.CalcDShape(point, derivativeShape);
directionData.GetDofMatrix().MultTranspose(shape, value);
mfem::MultAtB(directionData.GetDofMatrix(), derivativeShape, directionGradientHat);
directionGradientHat.Mult(unitDiagonal, directionAlongDiagonal);
const double physicalRadius = physicalPosition.Norml2();
const double logicalRadius = std::max({std::abs(logicalPosition(0)), std::abs(logicalPosition(1)), std::abs(logicalPosition(2))});
const double logicalRadiusDerivative = -(logicalAlongDiagonal(0) + logicalAlongDiagonal(1) + logicalAlongDiagonal(2)) / 3.0;
const double nan = std::numeric_limits<double>::quiet_NaN();
double physicalRadiusDerivative = nan, actualRadial = nan, actualRadialDerivative = nan;
if (physicalRadius > 0.0) {
radial = physicalPosition;
radial /= physicalRadius;
physicalRadiusDerivative = radial * physicalAlongDiagonal;
radialDerivative = physicalAlongDiagonal;
radialDerivative.Add(-physicalRadiusDerivative, radial);
radialDerivative /= physicalRadius;
actualRadial = radial * value;
actualRadialDerivative = radial * directionAlongDiagonal + radialDerivative * value;
}
const double desiredRadial = cornerSurfaceAmplitude * std::pow(logicalRadius, radialPower);
const double desiredRadialDerivative = radialPower * cornerSurfaceAmplitude *
std::pow(logicalRadius, radialPower - 1.0) * logicalRadiusDerivative;
const double actualGradient = std::isfinite(physicalRadiusDerivative) && physicalRadiusDerivative != 0.0
? actualRadialDerivative / physicalRadiusDerivative : nan;
const double desiredGradient = std::isfinite(physicalRadiusDerivative) && physicalRadiusDerivative != 0.0
? desiredRadialDerivative / physicalRadiusDerivative : nan;
csv << "0," << parameter;
Coordinates(csv, logicalPosition);
Coordinates(csv, physicalPosition);
csv << ',' << logicalRadius << ',' << physicalRadius << ',' << cornerSurfaceAmplitude << ','
<< logicalRadiusDerivative << ',' << physicalRadiusDerivative << ',' << actualRadial << ',' << desiredRadial
<< ',' << actualRadialDerivative << ',' << desiredRadialDerivative << ',' << actualGradient << ',' << desiredGradient;
Coordinates(csv, directionAlongDiagonal);
const double referenceDeterminant = referenceJacobian.Det();
csv << ',' << referenceDeterminant << ',' << referenceJacobian.CalcSingularvalue(2) << ','
<< referenceJacobian.CalcSingularvalue(0) << ',' << radialPower;
// Direct total-element determinant, normalized by undeformed
// reference volume. This remains evaluable at a vertex without
// constructing a potentially ill-conditioned inverse Jacobian.
const auto determinant = DeterminantCoefficients(referenceJacobian, directionGradientHat);
for (const double coefficient : determinant) {
csv << ',' << (referenceDeterminant != 0.0 ? coefficient / referenceDeterminant : nan);
}
csv << '\n';
}
}
// Per-element calls to the collective production estimator are intentional.
// Restrict this diagnostic to one rank: element counts differ on MPI ranks,
// so independently iterating them would mismatch estimator collectives.
inline GeometryReport InspectGeometry(
const mean_field::mapping::DomainMapper &mapper,
const mfem::ParFiniteElementSpace &displacementSpace,
const mfem::ParGridFunction &compactification,
const mfem::Vector &acceptedVolumeDisplacement,
const mfem::Vector &volumeDirection,
const std::span<const mean_field::deformation::NewtonStepGeometryRule> productionRules,
const std::filesystem::path &outputDirectory,
const std::string &label,
const std::size_t detailedElementCount = 12
) {
using namespace mean_field;
using namespace geometry_detail;
int ranks = 0;
MPI_Comm_size(displacementSpace.GetComm(), &ranks);
if (ranks != 1) {
throw std::invalid_argument("Per-element geometry experiment requires exactly one MPI rank.");
}
if (mapper.GetDimension() != 3) {
throw std::invalid_argument("Geometry experiment currently requires three spatial dimensions.");
}
std::filesystem::create_directories(outputDirectory);
auto *mesh = displacementSpace.GetParMesh();
if (label.find("uniform") != std::string::npos) {
InspectReferenceCorners(*mesh, outputDirectory, label);
}
std::vector<std::vector<deformation::NewtonStepGeometryRule>> elementRules(mesh->GetNE());
std::vector<std::vector<int>> ruleIndices(mesh->GetNE());
for (std::size_t index = 0; index < productionRules.size(); ++index) {
const auto &rule = productionRules[index];
elementRules.at(rule.element).push_back(rule);
ruleIndices.at(rule.element).push_back(static_cast<int>(index));
}
GeometryReport report;
std::vector<ElementReport> elements;
elements.reserve(mesh->GetNE());
for (int element = 0; element < mesh->GetNE(); ++element) {
if (elementRules[element].empty()) {
continue;
}
const auto estimate = deformation::estimate_largest_safe_newton_step_size(
mapper, displacementSpace, compactification, acceptedVolumeDisplacement, volumeDirection,
elementRules[element]
);
const int globalRule = estimate.limitingRule < 0 ? -1 : ruleIndices[element].at(estimate.limitingRule);
elements.push_back({element, globalRule, estimate});
report.sampleCount += estimate.sampledQuadraturePointCount;
if (estimate.limitedByGeometry) {
++report.limitedElements;
if (report.limitingElement < 0 || estimate.boundaryStepSize < report.boundaryStep) {
report.boundaryStep = estimate.boundaryStepSize;
report.safeStep = estimate.stepSize;
report.limitingElement = element;
}
}
}
std::stable_sort(elements.begin(), elements.end(), [](const auto &left, const auto &right) {
return left.estimate.boundaryStepSize < right.estimate.boundaryStepSize;
});
for (const auto &element : elements) {
if (element.estimate.limitedByGeometry) {
report.elementsWithinOnePartPerMillion +=
element.estimate.boundaryStepSize <= report.boundaryStep * (1.0 + 1.0e-6);
report.elementsWithinOnePercent +=
element.estimate.boundaryStepSize <= report.boundaryStep * 1.01;
}
}
auto elementCsv = OpenCsv(outputDirectory / (label + "_geometry_elements.csv"));
elementCsv << "rank,element,attribute,compactified,limited,boundary_step,safe_step,rule_index,point_index,"
"rule_points,xi,eta,zeta,reference_x,reference_y,reference_z,physical_x,physical_y,physical_z,"
"physical_radius,min_det_accepted_samples,min_det_full_step_samples,limiter_det_accepted,"
"limiter_sigma_min_accepted,limiter_sigma_max_accepted,limiter_direction_gradient_frobenius,"
"limiter_direction_radial,limiter_direction_tangential,"
"limiter_radial_gradient,limiter_tangential_gradient_trace,"
"reference_element_det,reference_element_sigma_min,reference_element_sigma_max,"
"total_physical_element_det,total_physical_element_sigma_min,total_physical_element_sigma_max,"
"det_coefficient_0,det_coefficient_1,det_coefficient_2,det_coefficient_3\n";
auto sampleCsv = OpenCsv(outputDirectory / (label + "_geometry_mapping_checks.csv"));
sampleCsv << "element,rule_index,point_index,alpha,boundary_fraction,status,polynomial_det,direct_det,"
"relative_det_error,relative_mapping_matrix_error,"
"mapped_sigma_min,mapped_sigma_max,displacement_det,displacement_sigma_min\n";
auto matrixCsv = OpenCsv(outputDirectory / (label + "_geometry_limiting_matrices.csv"));
matrixCsv << "element,matrix,row,column,value\n";
mfem::Vector acceptedLocal(displacementSpace.GetVSize());
mfem::Vector directionLocal(displacementSpace.GetVSize());
const auto *prolongation = displacementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(acceptedVolumeDisplacement, acceptedLocal);
prolongation->Mult(volumeDirection, directionLocal);
} else {
acceptedLocal = acceptedVolumeDisplacement;
directionLocal = volumeDirection;
}
const auto *compactificationSpace = compactification.ParFESpace();
mfem::Array<int> displacementDofs, compactificationDofs;
mfem::Vector acceptedElement, directionElement, compactificationElement, trialElement;
mapping::DomainMapper::Workspace workspace(mapper.GetDimension());
mapping::MappingPointContext base, direct;
mapping::MappingPointVariation variation;
for (std::size_t sortedIndex = 0; sortedIndex < elements.size(); ++sortedIndex) {
const auto &entry = elements[sortedIndex];
const int element = entry.element;
auto *transformation = mesh->GetElementTransformation(element);
const auto *displacementFe = displacementSpace.GetFE(element);
const auto *compactificationFe = compactificationSpace->GetFE(element);
auto *displacementTransform = displacementSpace.GetElementVDofs(element, displacementDofs);
auto *compactificationTransform = compactificationSpace->GetElementDofs(element, compactificationDofs);
acceptedLocal.GetSubVector(displacementDofs, acceptedElement);
directionLocal.GetSubVector(displacementDofs, directionElement);
compactification.GetSubVector(compactificationDofs, compactificationElement);
if (displacementTransform != nullptr) {
displacementTransform->InvTransformPrimal(acceptedElement);
displacementTransform->InvTransformPrimal(directionElement);
}
if (compactificationTransform != nullptr) {
compactificationTransform->InvTransformPrimal(compactificationElement);
}
const mapping::ElementDisplacementData baseData(
*displacementFe, acceptedElement, displacementSpace.GetOrdering()
);
const mapping::ElementDisplacementData directionData(
*displacementFe, directionElement, displacementSpace.GetOrdering()
);
const mapping::ElementCompactificationData compactificationData(*compactificationFe, compactificationElement);
const mapping::ElementMappingData mappingData{baseData, compactificationData};
const auto &point = entry.globalRule >= 0
? productionRules[entry.globalRule].integrationRule->IntPoint(entry.estimate.limitingQuadraturePoint)
: mfem::Geometries.GetCenter(transformation->GetGeometryType());
if (mapper.EvaluatePoint(mappingData, *transformation, point, workspace, base) != mapping::MappingStatus::valid ||
mapper.EvaluatePointVariation(mappingData, directionData, *transformation, point, base, workspace, variation) !=
mapping::MappingStatus::valid) {
throw std::runtime_error("Geometry diagnostic could not evaluate element " + std::to_string(element));
}
const auto coefficients = DeterminantCoefficients(base.mapping_jacobian, variation.mapping_jacobian_variation);
const mfem::DenseMatrix referenceJacobian(transformation->Jacobian());
mfem::DenseMatrix totalPhysicalJacobian(3);
mfem::Mult(base.mapping_jacobian, referenceJacobian, totalPhysicalJacobian);
const double radius = base.physical_position.Norml2();
double radialDirection = 0.0;
if (radius > 0.0) {
radialDirection = (base.physical_position * variation.physical_position_variation) / radius;
}
const double directionNorm = variation.physical_position_variation.Norml2();
const double tangentialDirection = std::sqrt(std::max(0.0, directionNorm * directionNorm - radialDirection * radialDirection));
mfem::DenseMatrix physicalGradient(3);
mfem::Mult(variation.mapping_jacobian_variation, base.inverse_mapping_jacobian, physicalGradient);
double radialGradient = 0.0;
if (radius > 0.0) {
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column) {
radialGradient += base.physical_position(row) * physicalGradient(row, column) *
base.physical_position(column) / (radius * radius);
}
}
}
const double tangentialTrace = physicalGradient(0, 0) + physicalGradient(1, 1) + physicalGradient(2, 2) - radialGradient;
const double gradientNorm = Frobenius(variation.displacement_jacobian_variation);
report.maximumReportedPointGradient = std::max(report.maximumReportedPointGradient, gradientNorm);
elementCsv << "0," << element << ',' << transformation->Attribute << ',' << base.compactified << ','
<< entry.estimate.limitedByGeometry << ',' << entry.estimate.boundaryStepSize << ','
<< entry.estimate.stepSize << ',' << entry.globalRule << ',' << entry.estimate.limitingQuadraturePoint << ','
<< (entry.globalRule >= 0 ? productionRules[entry.globalRule].integrationRule->GetNPoints() : 0) << ','
<< point.x << ',' << point.y << ',' << point.z;
Coordinates(elementCsv, base.reference_position);
Coordinates(elementCsv, base.physical_position);
elementCsv << ',' << radius << ',' << entry.estimate.minimumDeterminantAtAcceptedState << ','
<< entry.estimate.minimumDeterminantAtMaximumStepSize << ',' << base.mapping_determinant << ','
<< base.mapping_jacobian.CalcSingularvalue(2) << ',' << base.mapping_jacobian.CalcSingularvalue(0) << ','
<< gradientNorm << ',' << radialDirection << ',' << tangentialDirection << ',' << radialGradient << ','
<< tangentialTrace << ',' << referenceJacobian.Det() << ',' << referenceJacobian.CalcSingularvalue(2)
<< ',' << referenceJacobian.CalcSingularvalue(0) << ',' << totalPhysicalJacobian.Det() << ','
<< totalPhysicalJacobian.CalcSingularvalue(2) << ',' << totalPhysicalJacobian.CalcSingularvalue(0);
for (const double coefficient : coefficients) {
elementCsv << ',' << coefficient;
}
elementCsv << '\n';
if (sortedIndex >= detailedElementCount) {
continue;
}
const std::array<std::pair<const char *, const mfem::DenseMatrix *>, 6> matrices{{
{"base_mapping", &base.mapping_jacobian},
{"direction_mapping", &variation.mapping_jacobian_variation},
{"direction_displacement", &variation.displacement_jacobian_variation},
{"physical_direction_gradient", &physicalGradient},
{"reference_element", &referenceJacobian},
{"total_physical_element", &totalPhysicalJacobian}
}};
for (const auto &[name, matrix] : matrices) {
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column) {
matrixCsv << element << ',' << name << ',' << row << ',' << column << ',' << (*matrix)(row, column) << '\n';
}
}
}
for (const double fraction : {0.0, 0.25, 0.5, 0.9, 0.99, 0.999}) {
const double alpha = fraction * entry.estimate.boundaryStepSize;
trialElement = acceptedElement;
trialElement.Add(alpha, directionElement);
const mapping::ElementDisplacementData trialData(*displacementFe, trialElement, displacementSpace.GetOrdering());
const mapping::ElementMappingData trialMappingData{trialData, compactificationData};
const auto status = mapper.EvaluatePoint(trialMappingData, *transformation, point, workspace, direct);
mfem::DenseMatrix affine(base.mapping_jacobian);
affine.Add(alpha, variation.mapping_jacobian_variation);
const double predictedDeterminant = Polynomial(coefficients, alpha);
const double nan = std::numeric_limits<double>::quiet_NaN();
double relativeMatrixError = nan, relativeDeterminantError = nan;
double minimumSingular = nan, maximumSingular = nan, displacementDeterminant = nan, displacementMinimumSingular = nan;
if (status == mapping::MappingStatus::valid) {
relativeMatrixError = Difference(direct.mapping_jacobian, affine) / std::max(Frobenius(affine), 1.0e-300);
relativeDeterminantError = std::abs(direct.mapping_determinant - predictedDeterminant) /
std::max(std::abs(base.mapping_determinant), 1.0e-300);
minimumSingular = direct.mapping_jacobian.CalcSingularvalue(2);
maximumSingular = direct.mapping_jacobian.CalcSingularvalue(0);
// DomainMapper's displacement_jacobian already includes I.
const mfem::DenseMatrix &displacementMapping = direct.displacement_jacobian;
displacementDeterminant = displacementMapping.Det();
displacementMinimumSingular = displacementMapping.CalcSingularvalue(2);
report.maximumRelativeMappingError = std::max(report.maximumRelativeMappingError, relativeMatrixError);
report.maximumRelativeDeterminantError = std::max(report.maximumRelativeDeterminantError, relativeDeterminantError);
} else {
++report.invalidDirectSamples;
}
sampleCsv << element << ',' << entry.globalRule << ',' << entry.estimate.limitingQuadraturePoint << ','
<< alpha << ',' << fraction << ',' << static_cast<int>(status) << ',' << predictedDeterminant << ','
<< (status == mapping::MappingStatus::valid ? direct.mapping_determinant : nan) << ','
<< relativeDeterminantError << ',' << relativeMatrixError << ',' << minimumSingular << ','
<< maximumSingular << ',' << displacementDeterminant << ',' << displacementMinimumSingular << '\n';
}
}
std::cout << "Geometry[" << label << "]: boundary=" << report.boundaryStep << " safe=" << report.safeStep
<< " limiter=" << report.limitingElement << " limited_elements=" << report.limitedElements
<< " ties_1ppm=" << report.elementsWithinOnePartPerMillion << " ties_1pct=" << report.elementsWithinOnePercent
<< " samples=" << report.sampleCount << " max_mapping_error=" << report.maximumRelativeMappingError
<< " max_det_error=" << report.maximumRelativeDeterminantError
<< " invalid_direct_samples=" << report.invalidDirectSamples << std::endl;
return report;
}
} // namespace experiment

View File

@@ -0,0 +1,472 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <numbers>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <mfem.hpp>
import mean_field;
#include "geometry_quality_diagnostics.hpp"
namespace {
using namespace mean_field;
using Clock = std::chrono::steady_clock;
struct Options {
std::string mesh = "sandbox.smesh";
std::filesystem::path output = "geometry_quality_results";
std::vector<double> tolerances{0.03, 0.003};
int maximumLinearIterations = 200;
int advance = 0;
bool finiteDifferences = true;
bool blockActions = true;
bool geometryOnly = false;
bool saveVectors = false;
bool warm = false;
std::filesystem::path replayVectors;
bool diagonalOnly=false;
};
Options Parse(int argc, char **argv) {
Options o;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
auto value = [&]() -> std::string {
if (++i >= argc) throw std::invalid_argument("Missing value after " + arg);
return argv[i];
};
if (arg == "--mesh") o.mesh = value();
else if (arg == "--output") o.output = value();
else if (arg == "--tolerances") {
o.tolerances.clear();
std::istringstream stream(value());
for (std::string token; std::getline(stream, token, ',');) {
const double tolerance = std::stod(token);
if (!(tolerance > 0.0 && tolerance < 1.0)) throw std::invalid_argument("Invalid tolerance");
o.tolerances.push_back(tolerance);
}
if (o.tolerances.empty()) throw std::invalid_argument("Empty tolerance list");
} else if (arg == "--max-linear-iterations") o.maximumLinearIterations = std::stoi(value());
else if (arg == "--advance") o.advance = std::stoi(value());
else if (arg == "--no-fd") o.finiteDifferences = false;
else if (arg == "--no-block-actions") o.blockActions = false;
else if (arg == "--geometry-only") o.geometryOnly = true;
else if (arg == "--save-vectors") o.saveVectors = true;
else if (arg == "--warm") o.warm = true;
else if (arg == "--replay-vectors") o.replayVectors=value();
else if (arg == "--diagonal-only") o.diagonalOnly=true;
else if (arg == "--help") {
std::cout << "geometry_quality_experiment [--mesh FILE] [--output NEW_DIRECTORY]\n"
" [--tolerances 0.03,0.003] [--max-linear-iterations 200] [--advance N]\n"
" [--warm] [--geometry-only] [--no-fd] [--no-block-actions] [--save-vectors]\n"
" [--replay-vectors CASE_vectors.csv] (verifies saved accepted state, no linear solve)\n"
" [--diagonal-only] (seed replay only; skips per-element scans and residual checks)\n"
"Single MPI rank only. Defaults inspect a frozen seed; --advance uses production Newton.\n";
std::exit(0);
} else throw std::invalid_argument("Unknown option: " + arg);
}
if (o.advance < 0 || o.maximumLinearIterations < 1) throw std::invalid_argument("Invalid iteration count");
if (!o.replayVectors.empty()) o.tolerances.resize(1);
if (o.diagonalOnly) {
if (o.replayVectors.empty() || o.advance!=0 || o.geometryOnly) throw std::invalid_argument("--diagonal-only requires seed correction replay");
o.finiteDifferences=false; o.blockActions=false;
}
return o;
}
std::ofstream File(const std::filesystem::path &path) {
std::ofstream stream(path);
stream.exceptions(std::ios::failbit | std::ios::badbit);
stream << std::setprecision(17);
return stream;
}
double Norm(const mfem::Vector &v, int offset, int size) {
long double sum = 0;
for (int i = offset; i < offset + size; ++i) sum += static_cast<long double>(v(i)) * v(i);
return std::sqrt(sum);
}
double MaxAbs(const mfem::Vector &v, int offset, int size) {
double result = 0;
for (int i = offset; i < offset + size; ++i) result = std::max(result, std::abs(v(i)));
return result;
}
template <typename State>
void Inspect(State &s, const fem::FEM &fem, const Options &options) {
auto &problem = s.Problem();
const auto &deformation = problem.GetPhysicalOperator().GetDomainDeformation();
// Recompile only the public surface-coordinate descriptor to inspect its
// radii/directions; the actual extension always remains the context's.
mfem::Vector center(3); center=0.0;
const deformation::SurfaceDeformationCompilationContext surfaceContext{
*fem.surfaceDeformationFes,
field::make_stellar_surface_scalar_dof_map<utils::domain::CoreEnvelopeVacuumDomainSchema>(*fem.surfaceDeformationFes)};
const auto surface=deformation::compileSurfaceDeformationPrescription(deformation::NodalRadialSurface{center},surfaceContext);
const auto values = problem.GetManifest().valueBlocks();
const auto rows = problem.GetManifest().residualBlocks();
const auto surfaceIterator = std::find_if(values.begin(), values.end(), [](const auto &b) {
return b.stableId == "surface_deformation";
});
if (surfaceIterator == values.end()) throw std::logic_error("Experiment requires a surface-deformation block");
const auto surfaceBlock = *surfaceIterator;
const mfem::Vector acceptedVolume(problem.GetPhysicalOperator().GetGeneratedVolumeDisplacement());
const mfem::Vector physicalResidual(s.normalizedOperator->GetPhysicalResidual());
const mfem::Vector warmCorrection(s.normalizedCorrection);
auto residuals = File(options.output / "blocks.csv");
residuals << "case,kind,block,size,physical_l2,normalized_l2,physical_linf,normalized_linf,linear_residual_over_block_F\n";
auto record = [&](const std::string &label, const std::string &kind, auto blocks,
const mfem::Vector &physical, const mfem::Vector &normalized, bool relative) {
for (const auto &b : blocks) {
const double denominator = relative ? Norm(s.acceptedNormalizedResidual, b.offset, b.size) : 0.0;
residuals << label << ',' << kind << ',' << b.stableId << ',' << b.size << ','
<< Norm(physical, b.offset, b.size) << ',' << Norm(normalized, b.offset, b.size) << ','
<< MaxAbs(physical, b.offset, b.size) << ',' << MaxAbs(normalized, b.offset, b.size) << ','
<< (relative && denominator > 0 ? Norm(normalized, b.offset, b.size) / denominator
: std::numeric_limits<double>::quiet_NaN()) << '\n';
}
residuals.flush();
};
record("accepted", "residual", rows, physicalResidual, s.acceptedNormalizedResidual, false);
record("accepted", "state", values, s.AcceptedPhysicalState(), s.acceptedNormalizedState, false);
auto metadata = File(options.output / "metadata.txt");
metadata << "mesh=" << options.mesh << "\nmesh_bytes=" << std::filesystem::file_size(options.mesh)
<< "\ncompiled=" << __DATE__ << ' ' << __TIME__ << "\ncompiler=" << __VERSION__
<< "\npolynomial_increment=" << MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT
<< "\nmpi_ranks=1\nmodel=n1_K2G_R2_over_pi_M1_R1_J0_fixed_central_density"
<< "\nnormalization=production_frozen_physical_Riesz_diagonal"
<< "\npreconditioner=production_default\nFGMRES_restart=40\nadvanced_steps=" << options.advance
<< "\nmax_linear_iterations=" << options.maximumLinearIterations
<< "\nwarm=" << options.warm << "\nstate_size=" << problem.StateSize()
<< "\nelement_count=" << fem.mesh->GetNE() << "\naccepted_residual=" << s.acceptedNormalizedResidual.Norml2()
<< "\ncoefficient_norms_are_unweighted_single_rank=true"
<< "\nsurface_mean_and_rms_are_nodal_not_area_weighted=true\ntolerances=";
for (double t : options.tolerances) metadata << t << ',';
metadata << '\n';
metadata << "replay_vectors=" << options.replayVectors.string() << '\n';
metadata << "diagonal_only=" << options.diagonalOnly << '\n';
metadata.flush();
auto vertexGeometry = [&](const std::string &label,const mfem::Vector &direction) {
// Deliberately independent sampling of the known sandbox core corners.
// Uses the unmodified production estimator with a vertex-only rule.
std::vector<deformation::NewtonStepGeometryRule> rules;
for (int element : {0,9,18,27,36,45,54,63}) {
if (element<fem.mesh->GetNE() && fem.mesh->GetAttribute(element)==1 &&
fem.mesh->GetElementBaseGeometry(element)==mfem::Geometry::CUBE)
rules.push_back({element,mfem::Geometries.GetVertices(mfem::Geometry::CUBE)});
}
if (!rules.empty()) experiment::InspectGeometry(problem.GetDiscretization().domainMapper(),*fem.displacementFes,
*fem.compactificationCoordinate,acceptedVolume,direction,rules,options.output,label+"_vertices");
};
auto geometry = [&](const std::string &label, const mfem::Vector &physicalDirection) {
mfem::Vector volumeDirection(acceptedVolume.Size());
problem.BuildVolumeDisplacementDirection(physicalDirection, volumeDirection);
std::cout << "Geometry: " << label << std::endl;
if (options.advance==0 && (label=="newton_0" || label=="uniform_contraction")) {
int corner=0;
double minimumSum=std::numeric_limits<double>::infinity();
for (int i=0;i<surfaceBlock.size;++i) {
double sum=0;
for (int d=0;d<3;++d) sum+=surface.radialDirection(i,d);
if (sum<minimumSum) { minimumSum=sum; corner=i; }
}
experiment::InspectCoreDiagonal(fem,volumeDirection,physicalDirection(surfaceBlock.offset+corner),options.output,label);
}
if (options.advance==0 && !options.replayVectors.empty()) vertexGeometry(label,volumeDirection);
if (options.diagonalOnly) return experiment::GeometryReport{};
return experiment::InspectGeometry(problem.GetDiscretization().domainMapper(), *fem.displacementFes,
*fem.compactificationCoordinate, acceptedVolume, volumeDirection, s.geometryPreflightRules,
options.output, label);
};
mfem::Vector contraction(problem.StateSize());
contraction = 0.0;
for (int i = 0; i < surfaceBlock.size; ++i) contraction(surfaceBlock.offset + i) = -surface.referenceRadius(i);
geometry("uniform_contraction", contraction);
// Controls, not candidate production prescriptions: bypass the surface
// extension and inspect only core (attribute 1) samples. P3 interpolation
// cannot represent the P4 physical mesh coordinates exactly.
std::vector<deformation::NewtonStepGeometryRule> coreRules;
for (const auto &rule : s.geometryPreflightRules) {
if (fem.mesh->GetAttribute(rule.element)==1) coreRules.push_back(rule);
}
if (!options.diagonalOnly) for (bool radial : {false,true}) {
mfem::VectorFunctionCoefficient coefficient(3,[radial](const mfem::Vector &x,mfem::Vector &u) {
u=x;
u *= radial ? -x.Norml2()/utils::RADIUS : -1.0;
});
mfem::ParGridFunction projected(fem.displacementFes.get());
projected.ProjectCoefficient(coefficient);
mfem::Vector direction; projected.GetTrueDofs(direction);
experiment::InspectGeometry(problem.GetDiscretization().domainMapper(), *fem.displacementFes,
*fem.compactificationCoordinate, acceptedVolume, direction, coreRules, options.output,
radial ? "core_projected_physical_radial_contraction" : "core_projected_affine_contraction");
}
if (options.geometryOnly) return;
auto solves = File(options.output / "solves.csv");
solves << "case,tolerance,status,iterations,relative_true_residual,wall_seconds,safe_step,boundary_step,limiting_element,correction_difference_from_baseline,surface_difference_from_baseline,action_difference_over_F\n";
auto surfaceFile = File(options.output / "surface.csv");
surfaceFile << "case,parameter,x,y,z,radius,accepted_fraction,correction_fraction\n";
auto surfaceSummary = File(options.output / "surface_summary.csv");
surfaceSummary << "case,mean_fraction,rms_fraction,rms_nonmean_fraction,min_fraction,max_fraction\n";
auto columns = File(options.output / "block_actions.csv");
columns << "case,column,row,normalized_action_l2,normalized_dot_with_F\n";
auto fd = File(options.output / "finite_differences.csv");
fd << "case,epsilon,row,relative_error,absolute_error,action_norm\n";
auto extensionChecks = File(options.output / "extension_checks.csv");
extensionChecks << "case,epsilon,relative_volume_direction_error,trial_residual_norm\n";
mfem::Vector baseline, baselineAction;
for (std::size_t caseIndex = 0; caseIndex < options.tolerances.size(); ++caseIndex) {
const std::string label = "newton_" + std::to_string(caseIndex);
const double tolerance = options.tolerances[caseIndex];
s.normalizedCorrection = options.warm ? warmCorrection : mfem::Vector(problem.StateSize());
if (!options.warm) s.normalizedCorrection = 0.0;
s.linearRightHandSide = s.acceptedNormalizedResidual;
s.linearRightHandSide *= -1;
std::cout << "Solve: " << label << " tolerance=" << tolerance << std::endl;
const auto start = Clock::now();
solver::LinearSolveReport solve;
if (options.replayVectors.empty()) {
solve=s.linearBackend->Solve(s.linearRightHandSide, s.normalizedCorrection,
{.relativeTolerance=tolerance, .absoluteTolerance=0.0, .maximumIterations=options.maximumLinearIterations});
} else {
std::ifstream saved(options.replayVectors);
if (!saved) throw std::runtime_error("Cannot open replay vectors");
std::string line; std::getline(saved,line);
if (line!="index,physical_state,normalized_state,physical_correction,normalized_correction")
throw std::runtime_error("Unrecognized replay vector header");
int index=0;
while (std::getline(saved,line)) {
std::replace(line.begin(),line.end(),',',' ');
std::istringstream row(line);
int savedIndex; double physicalState, normalizedState, physicalCorrection, normalizedCorrection;
if (!(row>>savedIndex>>physicalState>>normalizedState>>physicalCorrection>>normalizedCorrection) ||
savedIndex!=index || index>=problem.StateSize()) throw std::runtime_error("Invalid replay vector row");
if (std::abs(physicalState-s.AcceptedPhysicalState()(index))>1e-12*(1+std::abs(physicalState)) ||
std::abs(normalizedState-s.acceptedNormalizedState(index))>1e-12*(1+std::abs(normalizedState)))
throw std::runtime_error("Replay state differs from this frozen context");
if (!std::isfinite(normalizedCorrection)) throw std::runtime_error("Nonfinite replay direction");
s.normalizedCorrection(index++)=normalizedCorrection;
}
if (index!=problem.StateSize()) throw std::runtime_error("Incomplete replay vector file");
mfem::Vector check(problem.EquationSize());
s.normalizedOperator->Mult(s.normalizedCorrection,check);
check+=s.acceptedNormalizedResidual;
solve.relativeTrueResidualNorm=check.Norml2()/s.acceptedNormalizedResidual.Norml2();
solve.status=solve.relativeTrueResidualNorm<=tolerance ? solver::LinearSolveStatus::converged : solver::LinearSolveStatus::maximum_iterations;
solve.iterations=0;
std::cout << "Replayed saved correction; true residual independently checked" << std::endl;
}
const double elapsed = std::chrono::duration<double>(Clock::now()-start).count();
std::cout << "Solved: iterations=" << solve.iterations << " true_relative=" << solve.relativeTrueResidualNorm
<< " wall=" << elapsed << std::endl;
s.normalizedOperator->DenormalizeState(s.normalizedCorrection, s.physicalCorrection);
const auto safe = s.EstimateLargestSafeStepSize(1.0, 0.9);
geometry(label, s.physicalCorrection);
if (caseIndex==0 && options.advance==0 && !options.replayVectors.empty()) {
mfem::Vector zeroSurface(surfaceBlock.size), surfaceDirection(surfaceBlock.size);
zeroSurface=0.0;
for (int i=0;i<surfaceBlock.size;++i) surfaceDirection(i)=s.physicalCorrection(surfaceBlock.offset+i);
const auto extensionContext=deformation::makeRadialDeformationExtensionCompilationContext(
*fem.surfaceDeformationFes,*fem.displacementFes,*fem.logicalReferenceMesh);
for (double power : {3.0,4.0}) {
// Existing production prescription, but a geometry-only
// intervention: this is NOT a Newton direction for the new
// parameterization until that system is rebuilt and solved.
auto alternativeSurface=deformation::compileSurfaceDeformationPrescription(
deformation::NodalRadialSurface{center},surfaceContext);
auto alternativeInterior=deformation::compileInteriorDeformationExtension(
deformation::PowerLawRadialInteriorExtension{power},extensionContext);
auto alternativeVacuum=deformation::compileVacuumDeformationExtension(
deformation::FixedInfinityRadialVacuumExtension{},extensionContext);
auto alternative=deformation::composePreparedDomainDeformation(
std::move(alternativeSurface),std::move(alternativeInterior),std::move(alternativeVacuum),
*fem.surfaceDeformationFes,*fem.displacementFes,*fem.logicalReferenceMesh);
mfem::Vector direction(acceptedVolume.Size());
alternative.applyJacobian(zeroSurface,surfaceDirection,direction);
const std::string control="newton_0_radial_power_"+std::to_string(static_cast<int>(power));
int corner=0; double smallest=std::numeric_limits<double>::infinity();
for (int i=0;i<surfaceBlock.size;++i) {
double sum=0; for (int d=0;d<3;++d) sum+=surface.radialDirection(i,d);
if (sum<smallest) { smallest=sum; corner=i; }
}
experiment::InspectCoreDiagonal(fem,direction,surfaceDirection(corner),options.output,control,power);
vertexGeometry(control,direction);
if (!options.diagonalOnly) experiment::InspectGeometry(problem.GetDiscretization().domainMapper(),*fem.displacementFes,
*fem.compactificationCoordinate,acceptedVolume,direction,s.geometryPreflightRules,options.output,control);
}
}
mfem::Vector action(problem.EquationSize()), linearResidual(problem.EquationSize()), physicalLinear(problem.EquationSize());
s.normalizedOperator->Mult(s.normalizedCorrection, action);
linearResidual = action;
linearResidual += s.acceptedNormalizedResidual;
s.normalizedOperator->DenormalizeResidual(linearResidual, physicalLinear);
record(label, "linear_residual", rows, physicalLinear, linearResidual, true);
record(label, "correction", values, s.physicalCorrection, s.normalizedCorrection, false);
if (caseIndex == 0) { baseline = s.normalizedCorrection; baselineAction=action; }
mfem::Vector difference(s.normalizedCorrection);
difference -= baseline;
mfem::Vector actionDifference(action); actionDifference-=baselineAction;
solves << label << ',' << tolerance << ',' << static_cast<int>(solve.status) << ',' << solve.iterations << ','
<< solve.relativeTrueResidualNorm << ',' << elapsed << ',' << safe.stepSize << ',' << safe.boundaryStepSize
<< ',' << safe.limitingElement << ',' << difference.Norml2()/std::max(baseline.Norml2(),1e-300) << ','
<< Norm(difference,surfaceBlock.offset,surfaceBlock.size)/std::max(Norm(baseline,surfaceBlock.offset,surfaceBlock.size),1e-300) << ','
<< actionDifference.Norml2()/std::max(s.acceptedNormalizedResidual.Norml2(),1e-300) << '\n';
solves.flush();
double sum=0, squared=0, minimum=std::numeric_limits<double>::infinity(), maximum=-minimum;
for (int i=0; i<surfaceBlock.size; ++i) {
const double radius=surface.referenceRadius(i);
const double fraction=s.physicalCorrection(surfaceBlock.offset+i)/radius;
sum += fraction; squared += fraction*fraction; minimum=std::min(minimum,fraction); maximum=std::max(maximum,fraction);
surfaceFile << label << ',' << i;
for (int d=0; d<3; ++d) surfaceFile << ',' << surface.referenceCenter()(d)+radius*surface.radialDirection(i,d);
surfaceFile << ',' << radius << ',' << s.AcceptedPhysicalState()(surfaceBlock.offset+i)/radius << ',' << fraction << '\n';
}
const double mean=sum/surfaceBlock.size, meanSquared=squared/surfaceBlock.size;
surfaceSummary << label << ',' << mean << ',' << std::sqrt(meanSquared) << ','
<< std::sqrt(std::max(0.0,meanSquared-mean*mean)) << ',' << minimum << ',' << maximum << '\n';
surfaceFile.flush(); surfaceSummary.flush();
if (caseIndex == 0) {
// Split only the surface component; other physical fields do not
// enter the production volume-extension Jacobian.
mfem::Vector meanDirection(problem.StateSize()), nonmeanDirection(s.physicalCorrection);
meanDirection = 0.0;
for (int i=0;i<surfaceBlock.size;++i) {
meanDirection(surfaceBlock.offset+i)=mean*surface.referenceRadius(i);
nonmeanDirection(surfaceBlock.offset+i)-=meanDirection(surfaceBlock.offset+i);
}
geometry(label+"_mean",meanDirection);
geometry(label+"_nonmean",nonmeanDirection);
}
if (options.saveVectors) {
auto vectors=File(options.output/(label+"_vectors.csv"));
vectors << "index,physical_state,normalized_state,physical_correction,normalized_correction\n";
for (int i=0;i<problem.StateSize();++i) vectors << i << ',' << s.AcceptedPhysicalState()(i) << ','
<< s.acceptedNormalizedState(i) << ',' << s.physicalCorrection(i) << ',' << s.normalizedCorrection(i) << '\n';
}
if (options.blockActions && caseIndex == 0) {
std::cout << "Block-column actions" << std::endl;
for (const auto &column : values) {
mfem::Vector direction(problem.StateSize()), blockAction(problem.EquationSize());
direction = 0.0;
for (int i=column.offset;i<column.offset+column.size;++i) direction(i)=s.normalizedCorrection(i);
s.normalizedOperator->Mult(direction,blockAction);
for (const auto &row : rows) {
double dot=0;
for (int i=row.offset;i<row.offset+row.size;++i) dot+=blockAction(i)*s.acceptedNormalizedResidual(i);
columns << label << ',' << column.stableId << ',' << row.stableId << ','
<< Norm(blockAction,row.offset,row.size) << ',' << dot << '\n';
}
columns.flush();
}
}
if (options.finiteDifferences && caseIndex == 0) {
// Forward differences remain inside the verified positive-alpha interval.
// Frozen normalization and fresh dependency revisions match production trial preparation.
mfem::Vector expectedVolumeDirection(acceptedVolume.Size());
problem.BuildVolumeDisplacementDirection(s.physicalCorrection,expectedVolumeDirection);
for (double multiplier : {1e-2,1e-3,1e-4}) {
const double epsilon = std::min(1.0,safe.stepSize)*multiplier;
if (!(epsilon>0)) throw std::runtime_error("No admissible finite-difference step");
std::cout << "Finite difference epsilon=" << epsilon << std::endl;
s.trialNormalizedState=s.acceptedNormalizedState;
s.trialNormalizedState.Add(epsilon,s.normalizedCorrection);
const auto preparation=s.PrepareTrial();
if (!preparation) throw std::runtime_error("Finite-difference trial preparation rejected");
mfem::Vector volumeError(problem.GetPhysicalOperator().GetGeneratedVolumeDisplacement());
volumeError-=acceptedVolume;
volumeError/=epsilon;
volumeError-=expectedVolumeDirection;
extensionChecks << label << ',' << epsilon << ','
<< volumeError.Norml2()/std::max(expectedVolumeDirection.Norml2(),1e-300) << ','
<< s.trialNormalizedResidual.Norml2() << '\n';
extensionChecks.flush();
mfem::Vector approximation(s.trialNormalizedResidual);
approximation-=s.acceptedNormalizedResidual;
approximation/=epsilon;
approximation-=action;
for (const auto &row:rows) {
const double error=Norm(approximation,row.offset,row.size), magnitude=Norm(action,row.offset,row.size);
fd << label << ',' << epsilon << ',' << row.stableId << ','
<< (magnitude>0 ? error/magnitude : std::numeric_limits<double>::quiet_NaN()) << ',' << error << ',' << magnitude << '\n';
}
fd.flush();
}
s.RestoreAccepted();
}
}
}
} // namespace
int main(int argc, char **argv) {
mfem::Mpi::Init(argc,argv);
try {
const auto options=Parse(argc,argv);
int ranks=0; MPI_Comm_size(MPI_COMM_WORLD,&ranks);
if (ranks!=1) throw std::invalid_argument("This diagnostic executable currently requires exactly one MPI rank");
if (std::filesystem::exists(options.output)) throw std::invalid_argument("Output directory already exists; use a new --output path");
std::filesystem::create_directories(options.output);
mfem::Device device("cpu");
utils::Args args; args.mesh_file=options.mesh; args.p.rtol=1e-12; args.p.atol=1e-12;
const auto start=Clock::now();
auto fem=fem::setup_fem(args.mesh_file,args,0);
if (!fem.okay()) throw std::runtime_error("Finite-element setup failed");
constexpr double radius=utils::RADIUS, mass=utils::MASS, G=utils::G;
auto model=model::StellarModel(eos::Polytrope({.n=1.0,.K=2*G*radius*radius/std::numbers::pi}),
surface::Isobaric({.Psurf=dimensions::PressureValue{0}}),
integral::FixedTotalMass({.Mtotal=dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({.Jtotal=dimensions::AngularMomentumValue{0},.axis={0,0,1},.center={0,0,0}}),
constraint::FixedCentralDensity({.RhoC=dimensions::DensityValue{std::numbers::pi*mass/(4*radius*radius*radius)}}));
auto discretization=equilibrium::makeStellarDiscretization(std::move(fem),
normalization::PhysicalRieszDiagonal{dimensions::LengthValue{radius},G});
std::cout << "Constructing production context" << std::endl;
auto context=solver::makeContext(std::move(model),std::move(discretization),
preconditioning::makePreconditioner(),solver::linear::FGMRES({.restartLength=40,.printLevel=-1}));
std::cout << "Setup seconds=" << std::chrono::duration<double>(Clock::now()-start).count() << std::endl;
if (options.advance>0) {
auto trajectory=File(options.output/"trajectory.csv");
trajectory << "iteration,residual,step,limiting_element,boundary\n";
auto observer=solver::nonlinear::makeObserver([](const solver::nonlinear::BeforeIteration &) {},
[&](const solver::nonlinear::AfterIteration &event) {
trajectory << event.iteration << ',' << event.residualNorm << ',' << event.acceptedStepLength << ','
<< (event.geometryPreflight ? event.geometryPreflight->limitingElement : -1) << ','
<< (event.geometryPreflight ? event.geometryPreflight->boundaryStepSize : 0) << '\n';
trajectory.flush();
std::cout << "Advance iteration=" << event.iteration << " residual=" << event.residualNorm << " step=" << event.acceptedStepLength << std::endl;
});
auto method=solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{
.relativeTolerance=1e-8,.absoluteTolerance=0,.maximumIterations=options.advance,
.linearSolve={.relativeTolerance=0.03,.absoluteTolerance=0,.maximumIterations=200},.backtracking={}});
auto solve=solver::make(context,method,observer);
auto report=solve.evaluate();
std::cout << "Production accepted steps=" << report.completedNonlinearIterations() << std::endl;
}
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context,[&](auto &state, auto &fem) { Inspect(state,fem,options); });
if (!context.isReady() || context.hasActiveSolver()) throw std::logic_error("Diagnostic access did not restore the context");
std::cout << "Experiment complete: " << options.output << " total_seconds="
<< std::chrono::duration<double>(Clock::now()-start).count() << std::endl;
return 0;
} catch (const std::exception &error) {
std::cerr << "geometry_quality_experiment: " << error.what() << '\n';
return 1;
}
}

View File

@@ -10,7 +10,6 @@
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
import experiment;
@@ -35,28 +34,46 @@ struct AccuracyBudgetMetrics {
double virial_consistency_error{0.0};
};
static double global_norm(const mfem::Vector& vector, MPI_Comm communicator) {
static double global_norm(
const mfem::Vector &vector,
MPI_Comm communicator
) {
const double local_norm_squared = vector * vector;
double global_norm_squared = 0.0;
MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(global_norm_squared);
}
static double global_dot(const mfem::Vector& left, const mfem::Vector& right, MPI_Comm communicator) {
static double global_dot(
const mfem::Vector &left,
const mfem::Vector &right,
MPI_Comm communicator
) {
const double local_dot = left * right;
double global_dot_product = 0.0;
MPI_Allreduce(&local_dot, &global_dot_product, 1, MPI_DOUBLE, MPI_SUM, communicator);
return global_dot_product;
}
static void zero_vacuum_density(const mean_field::fem::FEM& fem, mfem::GridFunction& density) {
for (int index = 0; index < fem.vacuum_tdof_rho.Size(); ++index) {
density(fem.vacuum_tdof_rho[index]) = 0.0;
}
static void zero_vacuum_density(
const mean_field::fem::FEM &fem,
mfem::GridFunction &density
) {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
const mean_field::field::FieldDofMap density_map =
mean_field::field::make_field_dof_map<mean_field::field::Density, DomainSchema>(*fem.densityFes);
mfem::Vector density_true;
density.GetTrueDofs(density_true);
const mfem::Vector supported_density = density_map.gather(density_true);
density_map.scatter(supported_density, density_true);
density.SetFromTrueDofs(density_true);
}
static int diagnostic_quadrature_order(const mean_field::fem::FEM &fem) {
return 2 * std::max(fem.L2_fes->GetMaxElementOrder(), fem.RT_fes->GetMaxElementOrder()) + 8;
return 2 * std::max(fem.gravityPotentialFes->GetMaxElementOrder(), fem.gravityFluxFes->GetMaxElementOrder()) + 8;
}
static mfem::Vector assemble_monopole_projection_rhs(
@@ -65,20 +82,23 @@ static mfem::Vector assemble_monopole_projection_rhs(
const double mass,
const double stellar_radius
) {
static_cast<void>(displacement);
*fem.displacement = displacement;
mfem::Vector local_rhs(fem.RT_fes->GetVSize());
mfem::Vector local_rhs(fem.gravityFluxFes->GetVSize());
local_rhs = 0.0;
const int vacuum_attribute = fem.domain_mapper_stateless->GetVacuumElementAttribute();
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = diagnostic_quadrature_order(fem);
mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
const mfem::FiniteElement& gravity_element = *fem.RT_fes->GetFE(element_id);
const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id);
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
mfem::Array<int> gravity_dofs;
mfem::DofTransformation* gravity_transform = fem.RT_fes->GetElementVDofs(element_id, gravity_dofs);
mfem::DofTransformation *gravity_transform = fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs);
const int dof_count = gravity_element.GetDof();
const int dimension = transformation->GetSpaceDim();
@@ -86,18 +106,20 @@ static mfem::Vector assemble_monopole_projection_rhs(
mfem::Vector physical_position(dimension);
mfem::Vector analytic_field(dimension);
mfem::Vector pulled_field(dimension);
mfem::DenseMatrix mapping_jacobian(dimension);
mfem::DenseMatrix vector_shape(dof_count, dimension);
element_rhs = 0.0;
const mfem::IntegrationRule& rule = mfem::IntRules.Get(
transformation->GetGeometryType(),
quadrature_order
);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
fem.mapping->GetPhysicalPoint(*transformation, point, physical_position);
mean_field::mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mean_field::mapping::MappingStatus::valid,
"Invalid mapping in monopole projection RHS."
);
physical_position = mapping_context.physical_position;
const double radius = physical_position.Norml2();
MFEM_VERIFY(std::isfinite(radius) && radius > 0.0, "Invalid radius in monopole projection RHS.");
@@ -106,12 +128,10 @@ static mfem::Vector assemble_monopole_projection_rhs(
if (transformation->Attribute == vacuum_attribute) {
analytic_field *= mean_field::utils::G * mass / (radius * radius * radius);
} else {
analytic_field *= mean_field::utils::G * mass /
(stellar_radius * stellar_radius * stellar_radius);
analytic_field *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius);
}
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
mapping_jacobian.MultTranspose(analytic_field, pulled_field);
mapping_context.mapping_jacobian.MultTranspose(analytic_field, pulled_field);
transformation->SetIntPoint(&point);
gravity_element.CalcVShape(*transformation, vector_shape);
@@ -130,9 +150,9 @@ static mfem::Vector assemble_monopole_projection_rhs(
local_rhs.AddElementVector(gravity_dofs, element_rhs);
}
mfem::Vector true_rhs(fem.RT_fes->GetTrueVSize());
mfem::Vector true_rhs(fem.gravityFluxFes->GetTrueVSize());
true_rhs = 0.0;
const mfem::Operator* prolongation = fem.RT_fes->GetProlongationMatrix();
const mfem::Operator *prolongation = fem.gravityFluxFes->GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(local_rhs, true_rhs);
} else {
@@ -151,40 +171,35 @@ static mfem::Vector project_monopole_gradient(
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
const mfem::Vector projection_rhs = assemble_monopole_projection_rhs(
fem,
displacement,
mass,
stellar_radius
);
const mfem::Vector projection_rhs_true = assemble_monopole_projection_rhs(fem, displacement, mass, stellar_radius);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
fem,
*fem.domain_mapper_stateless
);
mass_operator.Prepare(displacement_true);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(fem, *fem.domainMapperStateless);
mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true));
mfem::CGSolver solver(fem.RT_fes->GetComm());
const mfem::Vector projection_rhs = mass_operator.GetFluxMap().gather(projection_rhs_true);
mfem::CGSolver solver(fem.gravityFluxFes->GetComm());
solver.SetOperator(mass_operator);
solver.SetRelTol(1.0e-11);
solver.SetAbsTol(1.0e-13);
solver.SetMaxIter(4000);
solver.SetPrintLevel(0);
mfem::Vector projected_gradient(fem.RT_fes->GetTrueVSize());
projected_gradient = 0.0;
solver.Mult(projection_rhs, projected_gradient);
mfem::Vector projected_gradient_reduced(mass_operator.GetFluxMap().reduced_size());
projected_gradient_reduced = 0.0;
solver.Mult(projection_rhs, projected_gradient_reduced);
mfem::Vector residual;
mass_operator.Mult(projected_gradient, residual);
mass_operator.Mult(projected_gradient_reduced, residual);
residual -= projection_rhs;
const double relative_residual = global_norm(residual, fem.RT_fes->GetComm()) /
std::max(global_norm(projection_rhs, fem.RT_fes->GetComm()), std::numeric_limits<double>::epsilon());
const double relative_residual =
global_norm(residual, fem.gravityFluxFes->GetComm()) /
std::max(global_norm(projection_rhs, fem.gravityFluxFes->GetComm()), std::numeric_limits<double>::epsilon());
REQUIRE(std::isfinite(relative_residual));
REQUIRE(relative_residual < 1.0e-8);
return projected_gradient;
return mass_operator.GetFluxMap().scatter(projected_gradient_reduced);
}
static double mapped_hdiv_relative_gap(
@@ -196,21 +211,20 @@ static double mapped_hdiv_relative_gap(
mfem::Vector displacement_true;
displacement.GetTrueDofs(displacement_true);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(
fem,
*fem.domain_mapper_stateless
);
mass_operator.Prepare(displacement_true);
mean_field::operators::PreparedMappedHDivMassOperator mass_operator(fem, *fem.domainMapperStateless);
mass_operator.Prepare(mass_operator.GetDisplacementMap().gather(displacement_true));
mfem::Vector difference(calculated);
difference -= reference;
mfem::Vector difference_action;
mfem::Vector reference_action;
mass_operator.Mult(difference, difference_action);
mass_operator.Mult(reference, reference_action);
const mfem::Vector reduced_difference = mass_operator.GetFluxMap().gather(difference);
const mfem::Vector reduced_reference = mass_operator.GetFluxMap().gather(reference);
mass_operator.Mult(reduced_difference, difference_action);
mass_operator.Mult(reduced_reference, reference_action);
const double difference_energy = global_dot(difference, difference_action, fem.RT_fes->GetComm());
const double reference_energy = global_dot(reference, reference_action, fem.RT_fes->GetComm());
const double difference_energy = global_dot(reduced_difference, difference_action, fem.gravityFluxFes->GetComm());
const double reference_energy = global_dot(reduced_reference, reference_action, fem.gravityFluxFes->GetComm());
MFEM_VERIFY(reference_energy > 0.0, "Projected monopole field has zero mapped H(div) norm.");
return std::sqrt(std::max(0.0, difference_energy) / reference_energy);
@@ -221,15 +235,17 @@ static AccuracyBudgetEnergies measure_stellar_energies(
const mfem::GridFunction &density,
const mean_field::physics::GravitySolution &solution
) {
const int vacuum_attribute = fem.domain_mapper_stateless->GetVacuumElementAttribute();
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = diagnostic_quadrature_order(fem);
mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
double local_binding = 0.0;
double local_virial = 0.0;
mfem::Vector physical_position(3);
mfem::Vector reference_field(3);
mfem::Vector physical_field(3);
mfem::DenseMatrix mapping_jacobian(3);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
@@ -237,18 +253,21 @@ static AccuracyBudgetEnergies measure_stellar_energies(
continue;
}
const mfem::IntegrationRule& rule = mfem::IntRules.Get(
transformation->GetGeometryType(),
quadrature_order
);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
fem.mapping->GetPhysicalPoint(*transformation, point, physical_position);
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
const double mapping_determinant = mapping_jacobian.Det();
mean_field::mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mean_field::mapping::MappingStatus::valid,
"Invalid mapping in energy diagnostic."
);
physical_position = mapping_context.physical_position;
const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian;
const double mapping_determinant = mapping_context.mapping_determinant;
MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in energy diagnostic.");
solution.gradPhi.GetVectorValue(element_id, point, reference_field);
@@ -264,8 +283,8 @@ static AccuracyBudgetEnergies measure_stellar_energies(
}
AccuracyBudgetEnergies energies;
MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, fem.L2_fes->GetComm());
MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, fem.L2_fes->GetComm());
MPI_Allreduce(&local_binding, &energies.binding, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm());
MPI_Allreduce(&local_virial, &energies.virial, 1, MPI_DOUBLE, MPI_SUM, fem.densityFes->GetComm());
return energies;
}
@@ -284,12 +303,22 @@ static double reduced_gravity_relative_residual(
mean_field::utils::blocks::gravity_field.poisson_term
);
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
const mean_field::field::FieldDofMap density_map =
mean_field::field::make_field_dof_map<mean_field::field::Density, DomainSchema>(*fem.densityFes);
const mean_field::field::FieldDofMap displacement_map =
mean_field::field::make_field_dof_map<mean_field::field::Displacement, DomainSchema>(*fem.displacementFes);
const mean_field::field::FieldDofMap gravity_flux_map =
mean_field::field::make_field_dof_map<mean_field::field::Gravity, DomainSchema>(*fem.gravityFluxFes);
const mean_field::field::FieldDofMap gravity_potential_map =
mean_field::field::make_field_dof_map<mean_field::field::Gravity, DomainSchema>(*fem.gravityPotentialFes);
const std::array<int, GravityFieldForm::value_block_count> value_sizes{
fem.L2_fes->GetTrueVSize(), fem.Vec_H1_fes->GetTrueVSize(),
fem.RT_fes->GetTrueVSize(), fem.L2_fes->GetTrueVSize()
density_map.reduced_size(), displacement_map.reduced_size(), gravity_flux_map.reduced_size(),
gravity_potential_map.reduced_size()
};
const std::array<int, GravityFieldForm::residual_block_count> residual_sizes{
fem.RT_fes->GetTrueVSize(), fem.L2_fes->GetTrueVSize()
gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()
};
const mean_field::utils::blocks::form_layout<GravityFieldForm> layout(value_sizes, residual_sizes);
@@ -303,47 +332,35 @@ static double reduced_gravity_relative_residual(
solution.phi.GetTrueDofs(potential_true);
mean_field::operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(
fem,
*fem.domain_mapper_stateless
fem, *fem.domainMapperStateless
);
mean_field::operators::GravityFieldJacobianOperator jacobian(
fem,
*fem.domain_mapper_stateless,
linearization_context,
layout.value_offsets(),
layout.residual_offsets()
fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
);
mean_field::operators::GravityFieldOperator field_operator(
fem,
*fem.domain_mapper_stateless,
linearization_context,
layout.value_offsets(),
jacobian
fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), jacobian
);
mean_field::operators::context::gravity_field::GravityFieldGeometryContext geometry_context(
fem,
*fem.domain_mapper_stateless
fem, *fem.domainMapperStateless
);
mean_field::operators::ReducedGravityFieldOperator reduced_operator(
field_operator,
geometry_context,
displacement_true
field_operator, geometry_context, displacement_map.gather(displacement_true)
);
mfem::Vector right_hand_side;
reduced_operator.BuildRightHandSide(density_true, right_hand_side);
reduced_operator.BuildRightHandSide(density_map.gather(density_true), right_hand_side);
mfem::BlockVector state(layout.residual_offsets());
state = 0.0;
state.GetBlock(gradient_block) = gradient_true;
state.GetBlock(poisson_block) = potential_true;
state.GetBlock(gradient_block) = gravity_flux_map.gather(gradient_true);
state.GetBlock(poisson_block) = gravity_potential_map.gather(potential_true);
mfem::Vector residual;
reduced_operator.Mult(state, residual);
residual -= right_hand_side;
return global_norm(residual, fem.L2_fes->GetComm()) /
std::max(global_norm(right_hand_side, fem.L2_fes->GetComm()), std::numeric_limits<double>::epsilon());
return global_norm(residual, fem.mesh->GetComm()) /
std::max(global_norm(right_hand_side, fem.mesh->GetComm()), std::numeric_limits<double>::epsilon());
}
static AccuracyBudgetMetrics measure_monopole_accuracy(
@@ -364,7 +381,7 @@ static AccuracyBudgetMetrics measure_monopole_accuracy(
solution.phi.GetTrueDofs(solution_potential);
projected_potential.GetTrueDofs(projection_potential);
mfem::ParGridFunction projected_gradient_grid_function(fem.RT_fes.get());
mfem::ParGridFunction projected_gradient_grid_function(fem.gravityFluxFes.get());
projected_gradient_grid_function.SetFromTrueDofs(projected_gradient);
double local_solution_gradient_error = 0.0;
@@ -374,29 +391,34 @@ static AccuracyBudgetMetrics measure_monopole_accuracy(
double local_projection_potential_error = 0.0;
double local_potential_norm = 0.0;
const int vacuum_attribute = fem.domain_mapper_stateless->GetVacuumElementAttribute();
const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute;
const int quadrature_order = diagnostic_quadrature_order(fem);
mean_field::mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
mfem::Vector physical_position(3);
mfem::Vector analytic_gradient(3);
mfem::Vector solution_reference_gradient(3);
mfem::Vector projection_reference_gradient(3);
mfem::Vector solution_physical_gradient(3);
mfem::Vector projection_physical_gradient(3);
mfem::DenseMatrix mapping_jacobian(3);
for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) {
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element_id);
const mfem::IntegrationRule& rule = mfem::IntRules.Get(
transformation->GetGeometryType(),
quadrature_order
);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadrature_order);
for (int quadrature_point_id = 0; quadrature_point_id < rule.GetNPoints(); ++quadrature_point_id) {
const mfem::IntegrationPoint &point = rule.IntPoint(quadrature_point_id);
transformation->SetIntPoint(&point);
fem.mapping->GetPhysicalPoint(*transformation, point, physical_position);
fem.mapping->ComputeJacobian(*transformation, mapping_jacobian);
const double mapping_determinant = mapping_jacobian.Det();
mean_field::mapping::MappingPointContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*transformation, point, mapping_context) ==
mean_field::mapping::MappingStatus::valid,
"Invalid mapping in accuracy diagnostic."
);
physical_position = mapping_context.physical_position;
const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping_jacobian;
const double mapping_determinant = mapping_context.mapping_determinant;
MFEM_VERIFY(mapping_determinant > 0.0, "Non-positive mapping determinant in accuracy diagnostic.");
const double radius = physical_position.Norml2();
@@ -408,8 +430,7 @@ static AccuracyBudgetMetrics measure_monopole_accuracy(
analytic_gradient *= mean_field::utils::G * mass / (radius * radius * radius);
analytic_potential = -mean_field::utils::G * mass / radius;
} else {
analytic_gradient *= mean_field::utils::G * mass /
(stellar_radius * stellar_radius * stellar_radius);
analytic_gradient *= mean_field::utils::G * mass / (stellar_radius * stellar_radius * stellar_radius);
analytic_potential = -mean_field::utils::G * mass *
(3.0 * stellar_radius * stellar_radius - radius * radius) /
(2.0 * stellar_radius * stellar_radius * stellar_radius);
@@ -432,10 +453,10 @@ static AccuracyBudgetMetrics measure_monopole_accuracy(
local_solution_gradient_error += weight * (solution_physical_gradient * solution_physical_gradient);
local_projection_gradient_error += weight * (projection_physical_gradient * projection_physical_gradient);
local_gradient_norm += weight * (analytic_gradient * analytic_gradient);
local_solution_potential_error += weight *
(solution_potential_value - analytic_potential) * (solution_potential_value - analytic_potential);
local_projection_potential_error += weight *
(projection_potential_value - analytic_potential) * (projection_potential_value - analytic_potential);
local_solution_potential_error += weight * (solution_potential_value - analytic_potential) *
(solution_potential_value - analytic_potential);
local_projection_potential_error += weight * (projection_potential_value - analytic_potential) *
(projection_potential_value - analytic_potential);
local_potential_norm += weight * analytic_potential * analytic_potential;
}
}
@@ -446,8 +467,8 @@ static AccuracyBudgetMetrics measure_monopole_accuracy(
};
std::array<double, 6> global_values{};
MPI_Allreduce(
local_values.data(), global_values.data(), static_cast<int>(local_values.size()),
MPI_DOUBLE, MPI_SUM, fem.L2_fes->GetComm()
local_values.data(), global_values.data(), static_cast<int>(local_values.size()), MPI_DOUBLE, MPI_SUM,
fem.mesh->GetComm()
);
const AccuracyBudgetEnergies energies = measure_stellar_energies(fem, density, solution);
@@ -460,17 +481,16 @@ static AccuracyBudgetMetrics measure_monopole_accuracy(
metrics.direct_relative_residual = reduced_gravity_relative_residual(fem, density, displacement, solution);
metrics.gradient_relative_error = std::sqrt(global_values[0] / global_values[2]);
metrics.gradient_projection_relative_error = std::sqrt(global_values[1] / global_values[2]);
metrics.gradient_solution_projection_gap = mapped_hdiv_relative_gap(
fem, displacement, solution_gradient, projected_gradient
);
metrics.gradient_solution_projection_gap =
mapped_hdiv_relative_gap(fem, displacement, solution_gradient, projected_gradient);
metrics.potential_relative_error = std::sqrt(global_values[3] / global_values[5]);
metrics.potential_projection_relative_error = std::sqrt(global_values[4] / global_values[5]);
mfem::Vector potential_difference(solution_potential);
potential_difference -= projection_potential;
const double projection_potential_norm = global_norm(projection_potential, fem.L2_fes->GetComm());
const double projection_potential_norm = global_norm(projection_potential, fem.gravityPotentialFes->GetComm());
REQUIRE(projection_potential_norm > 0.0);
metrics.potential_solution_projection_gap = global_norm(potential_difference, fem.L2_fes->GetComm()) /
projection_potential_norm;
metrics.potential_solution_projection_gap =
global_norm(potential_difference, fem.gravityPotentialFes->GetComm()) / projection_potential_norm;
metrics.binding_relative_error = std::abs(energies.binding - analytic_energy) / std::abs(analytic_energy);
metrics.virial_relative_error = std::abs(energies.virial - analytic_energy) / std::abs(analytic_energy);
metrics.virial_consistency_error = std::abs(energies.binding - energies.virial) /
@@ -491,19 +511,16 @@ static void run_monopole_case(
args.quadrature.global_boost = quadrature_boost;
mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(fem.mapping != nullptr);
REQUIRE(fem.domain_mapper_stateless != nullptr);
REQUIRE(fem.domainMapperStateless != nullptr);
const double stellar_radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double density_value = mass / ((4.0 / 3.0) * M_PI * stellar_radius * stellar_radius * stellar_radius);
mfem::ParGridFunction displacement(fem.Vec_H1_fes.get());
mfem::ParGridFunction displacement(fem.displacementFes.get());
displacement = 0.0;
fem.mapping->ResetDisplacement();
mean_field::physics::update_stiffness_matrix(fem);
mfem::GridFunction density(fem.L2_fes.get());
*fem.displacement = 0.0;
mfem::GridFunction density(fem.densityFes.get());
density = density_value;
zero_vacuum_density(fem, density);
mean_field::analysis::conserve_mass(fem, density, mass);
@@ -511,40 +528,26 @@ static void run_monopole_case(
fem.Q = mean_field::physics::compute_quadrupole_moment_tensor(fem, density, fem.com);
const mean_field::physics::GravitySolution solution =
mean_field::physics::grav_potential_new(fem, args, density, displacement);
mean_field::physics::solve_gravity_field(fem, args, density, displacement);
auto analytic_potential = [mass, stellar_radius](const mfem::Vector &position) {
const double radius = position.Norml2();
if (radius >= stellar_radius) {
return -mean_field::utils::G * mass / radius;
}
return -mean_field::utils::G * mass *
(3.0 * stellar_radius * stellar_radius - radius * radius) /
return -mean_field::utils::G * mass * (3.0 * stellar_radius * stellar_radius - radius * radius) /
(2.0 * stellar_radius * stellar_radius * stellar_radius);
};
mean_field::mapping::PhysicalPositionFunctionCoefficient potential_coefficient(
*fem.mapping,
analytic_potential
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, analytic_potential
);
mfem::ParGridFunction projected_potential(fem.L2_fes.get());
mfem::ParGridFunction projected_potential(fem.gravityPotentialFes.get());
projected_potential.ProjectCoefficient(potential_coefficient);
const mfem::Vector projected_gradient = project_monopole_gradient(
fem,
displacement,
mass,
stellar_radius
);
const mfem::Vector projected_gradient = project_monopole_gradient(fem, displacement, mass, stellar_radius);
const AccuracyBudgetMetrics metrics = measure_monopole_accuracy(
fem,
density,
displacement,
solution,
projected_potential,
projected_gradient,
mass,
stellar_radius
fem, density, displacement, solution, projected_potential, projected_gradient, mass, stellar_radius
);
REQUIRE(std::isfinite(metrics.direct_relative_residual));
@@ -553,15 +556,11 @@ static void run_monopole_case(
REQUIRE(std::isfinite(metrics.virial_consistency_error));
record_experiment_result(
sweep_name,
case_name,
{
{"solver_rtol", std::to_string(solver_tolerance)},
sweep_name, case_name,
{{"solver_rtol", std::to_string(solver_tolerance)},
{"quadrature_global_boost", std::to_string(quadrature_boost)},
{"mesh_file", args.mesh_file}
},
{
{"direct_relative_residual", metrics.direct_relative_residual},
{"mesh_file", args.mesh_file}},
{{"direct_relative_residual", metrics.direct_relative_residual},
{"gradient_relative_error", metrics.gradient_relative_error},
{"gradient_projection_relative_error", metrics.gradient_projection_relative_error},
{"gradient_solution_projection_gap", metrics.gradient_solution_projection_gap},
@@ -570,47 +569,37 @@ static void run_monopole_case(
{"potential_solution_projection_gap", metrics.potential_solution_projection_gap},
{"binding_relative_error", metrics.binding_relative_error},
{"virial_relative_error", metrics.virial_relative_error},
{"virial_consistency_error", metrics.virial_consistency_error}
}
{"virial_consistency_error", metrics.virial_consistency_error}}
);
}
TEST_CASE("Uniform Monopole Accuracy Budget: Solver Tolerance", tags::gravity & tags::accuracy & tags::integration) {
TEST_CASE(
"Uniform Monopole Accuracy Budget: Solver Tolerance",
tags::gravity_analytic_accuracy
) {
const mean_field::utils::Args args = test_utils::setup_args();
constexpr std::array<double, 4> solver_tolerances{1.0e-8, 1.0e-10, 1.0e-12, 1.0e-14};
for (const double solver_tolerance : solver_tolerances) {
run_monopole_case(
"solver_tolerance",
"uniform_monopole",
args,
solver_tolerance,
0
);
run_monopole_case("solver_tolerance", "uniform_monopole", args, solver_tolerance, 0);
}
}
TEST_CASE("Uniform Monopole Accuracy Budget: Quadrature", tags::gravity & tags::accuracy & tags::integration) {
TEST_CASE(
"Uniform Monopole Accuracy Budget: Quadrature",
tags::gravity_analytic_accuracy
) {
const mean_field::utils::Args args = test_utils::setup_args();
constexpr std::array<int, 3> quadrature_boosts{0, 4, 8};
for (const int quadrature_boost : quadrature_boosts) {
run_monopole_case(
"quadrature",
"uniform_monopole",
args,
1.0e-13,
quadrature_boost
);
run_monopole_case("quadrature", "uniform_monopole", args, 1.0e-13, quadrature_boost);
}
}
TEST_CASE("Uniform Monopole Accuracy Budget: Projection Decomposition", tags::gravity & tags::accuracy & tags::integration) {
run_monopole_case(
"projection_decomposition",
"uniform_monopole",
test_utils::setup_args(),
1.0e-13,
0
);
TEST_CASE(
"Uniform Monopole Accuracy Budget: Projection Decomposition",
tags::gravity_analytic_accuracy
) {
run_monopole_case("projection_decomposition", "uniform_monopole", test_utils::setup_args(), 1.0e-13, 0);
}

View File

@@ -0,0 +1,241 @@
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <string>
#include <mfem.hpp>
#include <mpi.h>
import experiment;
import experiment.stellar_null_space;
import mean_field;
import test_helpers;
namespace {
class GravityUnknownJacobian final : public mfem::Operator {
public:
explicit GravityUnknownJacobian(
const mean_field::operators::PreparedStellarEquilibriumOperator &stellarOperator
)
: mfem::Operator(
stellarOperator.GetLayout().size(experiment::null_space::gravityGradientValue) +
stellarOperator.GetLayout().size(experiment::null_space::gravityPotentialValue)
),
m_stellarOperator(stellarOperator),
m_gravityGradientSize(stellarOperator.GetLayout().size(experiment::null_space::gravityGradientValue)) {
MFEM_VERIFY(Width() == Height(), "The reduced gravity Jacobian must be square.");
}
void Mult(
const mfem::Vector &gravityDirection,
mfem::Vector &gravityAction
) const override {
MFEM_VERIFY(gravityDirection.Size() == Width(), "The reduced gravity direction has the wrong size.");
const mfem::Vector gravityGradientDirection(
const_cast<mfem::real_t *>(gravityDirection.GetData()), m_gravityGradientSize
);
const mfem::Vector gravityPotentialDirection(
const_cast<mfem::real_t *>(gravityDirection.GetData()) + m_gravityGradientSize,
Width() - m_gravityGradientSize
);
m_stellarOperator.GetGravityOperator().ApplyGravityUnknowns(
gravityGradientDirection, gravityPotentialDirection,
m_stellarOperator.GetGravityContext().GetGeometryContext(), gravityAction
);
}
[[nodiscard]] int gravity_gradient_size() const noexcept {
return m_gravityGradientSize;
}
private:
const mean_field::operators::PreparedStellarEquilibriumOperator &m_stellarOperator;
int m_gravityGradientSize;
};
void add_block_metrics(
std::map<
std::string,
double> &metrics,
const std::string &prefix,
const std::array<
double,
6> &norms
) {
for (std::size_t block = 0; block < norms.size(); ++block) {
metrics.emplace(prefix + experiment::null_space::residualBlockNames[block] + "_norm", norms[block]);
}
}
[[nodiscard]] mfem::Vector gravity_residual_blocks(
const mfem::Vector &completeAction,
const mean_field::operators::StellarEquilibriumLayout &layout
) {
const mfem::Vector gradient = experiment::null_space::const_residual_view(
completeAction, layout, experiment::null_space::gravityGradientResidual
);
const mfem::Vector potential = experiment::null_space::const_residual_view(
completeAction, layout, experiment::null_space::gravityPotentialResidual
);
mfem::Vector result(gradient.Size() + potential.Size());
mfem::Vector(result.GetData(), gradient.Size()) = gradient;
mfem::Vector(result.GetData() + gradient.Size(), potential.Size()) = potential;
return result;
}
void assign_gravity_completion(
mfem::Vector &completeDirection,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mfem::Vector &gravityCompletion,
const int gravityGradientSize
) {
const mfem::Vector gravityGradient(
const_cast<mfem::real_t *>(gravityCompletion.GetData()), gravityGradientSize
);
const mfem::Vector gravityPotential(
const_cast<mfem::real_t *>(gravityCompletion.GetData()) + gravityGradientSize,
gravityCompletion.Size() - gravityGradientSize
);
experiment::null_space::assign_value_block(
completeDirection, layout, experiment::null_space::gravityGradientValue, gravityGradient
);
experiment::null_space::assign_value_block(
completeDirection, layout, experiment::null_space::gravityPotentialValue, gravityPotential
);
}
} // namespace
TEST_CASE(
"Gravity-Completed Reduced Surface Mode Responses Of The Stellar Equilibrium Jacobian",
"[null_space][surface_modes][gravity_completed]"
) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = 1.0e-11;
args.p.atol = std::min(args.p.atol, 1.0e-13);
args.p.max_iters = std::max(args.p.max_iters, 2000);
experiment::null_space::N3Equilibrium fixture(std::move(args));
const MPI_Comm communicator = fixture.fem().mesh->GetComm();
int rank = 0;
MPI_Comm_rank(communicator, &rank);
const auto modes = experiment::null_space::make_surface_modes(fixture);
constexpr std::array<double, 2> rotationFractions{0.0, 0.5};
const int totalCases = static_cast<int>(rotationFractions.size() * modes.size());
int completedCases = 0;
for (const double rotationFraction : rotationFractions) {
const mean_field::physics::RigidRotation rotation = fixture.rotation(rotationFraction);
fixture.prepare(fixture.state(), rotation);
GravityUnknownJacobian gravityUnknownJacobian(fixture.stellar_operator());
mean_field::operators::ReducedGravityFieldPreconditioner gravityPreconditioner(
fixture.fem(), fixture.stellar_operator().GetGravityContext().GetGeometryContext()
);
mfem::MINRESSolver gravitySolver(communicator);
gravitySolver.SetOperator(gravityUnknownJacobian);
gravitySolver.SetPreconditioner(gravityPreconditioner);
gravitySolver.SetRelTol(1.0e-11);
gravitySolver.SetAbsTol(1.0e-13);
gravitySolver.SetMaxIter(2000);
gravitySolver.SetPrintLevel(1);
for (const experiment::null_space::SurfaceMode &mode : modes) {
experiment::null_space::report_progress(
communicator, "solving the gravity completion for " + mode.name + " at rotation fraction " +
std::to_string(rotationFraction) + " (" + std::to_string(completedCases + 1) + "/" +
std::to_string(totalCases) + ")"
);
const mfem::Vector surfaceOnlyAction = fixture.jacobian_action(mode.direction);
mfem::Vector gravityRightHandSide =
gravity_residual_blocks(surfaceOnlyAction, fixture.stellar_operator().GetLayout());
gravityRightHandSide *= -1.0;
mfem::Vector gravityCompletion(gravityUnknownJacobian.Width());
gravityCompletion = 0.0;
gravitySolver.Mult(gravityRightHandSide, gravityCompletion);
REQUIRE(gravitySolver.GetConverged());
mfem::Vector gravitySolveAction;
gravityUnknownJacobian.Mult(gravityCompletion, gravitySolveAction);
mfem::Vector gravitySolveResidual(gravitySolveAction);
gravitySolveResidual -= gravityRightHandSide;
const double gravityRightHandSideNorm =
experiment::null_space::global_norm(gravityRightHandSide, communicator);
const double gravitySolveResidualNorm =
experiment::null_space::global_norm(gravitySolveResidual, communicator);
const double gravitySolveRelativeResidual =
gravitySolveResidualNorm / std::max(gravityRightHandSideNorm, std::numeric_limits<double>::epsilon());
REQUIRE(std::isfinite(gravitySolveRelativeResidual));
mfem::Vector completedDirection(mode.direction);
assign_gravity_completion(
completedDirection, fixture.stellar_operator().GetLayout(), gravityCompletion,
gravityUnknownJacobian.gravity_gradient_size()
);
const mfem::Vector completedAction = fixture.jacobian_action(completedDirection);
std::map<std::string, double> metrics{
{"surface_only_input_norm", experiment::null_space::global_norm(mode.direction, communicator)},
{"gravity_completion_norm", experiment::null_space::global_norm(gravityCompletion, communicator)},
{"completed_input_norm", experiment::null_space::global_norm(completedDirection, communicator)},
{"surface_only_action_norm", experiment::null_space::global_norm(surfaceOnlyAction, communicator)},
{"gravity_completed_action_norm", experiment::null_space::global_norm(completedAction, communicator)},
{"gravity_solve_rhs_norm", gravityRightHandSideNorm},
{"gravity_solve_residual_norm", gravitySolveResidualNorm},
{"gravity_solve_relative_residual", gravitySolveRelativeResidual},
{"gravity_solve_iterations", static_cast<double>(gravitySolver.GetNumIterations())},
{"gravity_solve_final_norm", gravitySolver.GetFinalNorm()}
};
add_block_metrics(
metrics, "surface_only_",
experiment::null_space::residual_block_norms(
surfaceOnlyAction, fixture.stellar_operator().GetLayout(), communicator
)
);
add_block_metrics(
metrics, "gravity_completed_",
experiment::null_space::residual_block_norms(
completedAction, fixture.stellar_operator().GetLayout(), communicator
)
);
if (rank == 0) {
experiment::record_experiment_result(
"gravity_completed_reduced_surface_modes", mode.name,
{{"mode_kind", experiment::null_space::surface_mode_kind_name(mode.kind)},
{"axis", std::to_string(mode.axis)},
{"rotation_fraction_of_keplerian", std::to_string(rotationFraction)},
{"mesh_file", test_utils::setup_args().mesh_file},
{"local_state_dofs", std::to_string(fixture.stellar_operator().Width())}},
std::move(metrics)
);
}
++completedCases;
experiment::null_space::report_progress(
communicator, "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) +
" gravity-completed reduced surface-mode cases"
);
}
}
experiment::null_space::report_progress(
communicator, "gravity-completed reduced surface-mode probe complete; writing CSV output"
);
}

View File

@@ -0,0 +1,591 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iostream>
#include <limits>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import experiment;
import mean_field;
import test_helpers;
namespace {
using Clock = std::chrono::steady_clock;
namespace backend = mean_field::preconditioning::backend;
namespace preconditioning = mean_field::preconditioning;
[[nodiscard]] const char *buildConfiguration() noexcept {
#ifdef NDEBUG
return "release";
#else
return "debug";
#endif
}
[[nodiscard]] double maximumRankSeconds(
const Clock::time_point start,
const MPI_Comm communicator
) {
const double localSeconds = std::chrono::duration<double>(Clock::now() - start).count();
double maximumSeconds = 0.0;
MPI_Allreduce(&localSeconds, &maximumSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator);
return maximumSeconds;
}
[[nodiscard]] double globalNorm(
const mfem::Vector &vector,
const MPI_Comm communicator
) {
const double localSquared = vector * vector;
double globalSquared = 0.0;
MPI_Allreduce(&localSquared, &globalSquared, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(std::max(globalSquared, 0.0));
}
[[nodiscard]] double globalDot(
const mfem::Vector &left,
const mfem::Vector &right,
const MPI_Comm communicator
) {
const double localDot = left * right;
double result = 0.0;
MPI_Allreduce(&localDot, &result, 1, MPI_DOUBLE, MPI_SUM, communicator);
return result;
}
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 ReducedGravityOperator final : public mfem::Operator {
public:
explicit ReducedGravityOperator(
const mean_field::operators::context::gravity_field::GravityFieldGeometryContext &context
)
: mfem::Operator(
context.GetMassOperator().GetFluxMap().reduced_size() +
context.GetSourceOperator().GetPotentialMap().reduced_size()
),
m_mass(&context.GetMassOperator()),
m_divergence(
context.GetDivergenceOperator(),
context.GetMassOperator().GetFluxMap(),
context.GetSourceOperator().GetPotentialMap()
),
m_offsets(3),
m_gradientWorkspace(context.GetMassOperator().GetFluxMap().reduced_size()) {
m_offsets[0] = 0;
m_offsets[1] = context.GetMassOperator().GetFluxMap().reduced_size();
m_offsets[2] = Height();
}
void Mult(
const mfem::Vector &state,
mfem::Vector &residual
) const override {
if (state.Size() != Width() || residual.Size() != Height()) {
throw std::invalid_argument("The reduced gravity experiment requires preallocated compatible vectors.");
}
const mfem::Vector gradient(
const_cast<mfem::real_t *>(state.GetData()) + m_offsets[0], m_offsets[1] - m_offsets[0]
);
const mfem::Vector potential(
const_cast<mfem::real_t *>(state.GetData()) + m_offsets[1], m_offsets[2] - m_offsets[1]
);
mfem::Vector gradientResidual(residual.GetData() + m_offsets[0], m_offsets[1] - m_offsets[0]);
mfem::Vector potentialResidual(residual.GetData() + m_offsets[1], m_offsets[2] - m_offsets[1]);
m_mass->Mult(gradient, gradientResidual);
m_divergence.MultTranspose(potential, m_gradientWorkspace);
gradientResidual += m_gradientWorkspace;
m_divergence.Mult(gradient, potentialResidual);
}
private:
const mfem::Operator *m_mass;
preconditioning::ReducedGravityDivergenceOperator m_divergence;
mfem::Array<int> m_offsets;
mutable mfem::Vector m_gradientWorkspace;
};
[[nodiscard]] std::map<
std::string,
std::string>
commonParameters(
const std::string &candidate,
const std::string &measurement,
const int dimension
) {
return {
{"build_configuration", buildConfiguration()},
{"candidate", candidate},
{"experiment_schema", "p4_reduced_gravity_v1"},
{"factorization", candidate},
{"measurement", measurement},
{"mesh_file", test_utils::setup_args().mesh_file},
{"operator", "reduced_gravity_saddle_point"},
{"preconditioned_product", "G M^-1"},
{"root_dimension", std::to_string(dimension)}
};
}
void recordSpectrum(
const std::string &candidate,
const mean_field::solver::ArnoldiSpectralMeasurement &spectrum,
const int dimension,
const double setupSeconds
) {
experiment::record_experiment_result(
"gravity_preconditioning_p4", candidate + "_spectrum",
commonParameters(candidate, "arnoldi_summary", dimension),
{{"setup_seconds_maximum_rank", setupSeconds},
{"requested_dimension", static_cast<double>(spectrum.requestedDimension)},
{"achieved_dimension", static_cast<double>(spectrum.achievedDimension)},
{"operator_applications", static_cast<double>(spectrum.operatorApplications)},
{"measurement_seconds_maximum_rank", spectrum.measurementSecondsMaximumRank},
{"operator_application_seconds_maximum_rank", spectrum.operatorApplicationSecondsMaximumRank},
{"projected_condition_proxy", spectrum.projectedConditionProxy},
{"projected_largest_singular_value", spectrum.projectedLargestSingularValue},
{"projected_smallest_singular_value", spectrum.projectedSmallestSingularValue},
{"centroid_real_part", spectrum.centroidRealPart},
{"rms_distance_from_one", spectrum.rmsDistanceFromOne},
{"rms_cluster_radius", spectrum.rmsClusterRadius},
{"minimum_magnitude", spectrum.minimumMagnitude},
{"maximum_magnitude", spectrum.maximumMagnitude},
{"minimum_real_part", spectrum.minimumRealPart},
{"maximum_real_part", spectrum.maximumRealPart},
{"maximum_absolute_imaginary_part", spectrum.maximumAbsoluteImaginaryPart},
{"negative_real_part_count", static_cast<double>(spectrum.negativeRealPartCount)},
{"converged_ritz_value_count", static_cast<double>(spectrum.convergedRitzValueCount)},
{"conjugate_pair_defect", spectrum.conjugatePairDefect},
{"projected_departure_from_normality", spectrum.projectedDepartureFromNormality},
{"field_of_values_minimum_real_part", spectrum.projectedFieldOfValuesMinimumRealPart},
{"field_of_values_maximum_real_part", spectrum.projectedFieldOfValuesMaximumRealPart}}
);
for (std::size_t index = 0; index < spectrum.ritzValues.size(); ++index) {
const auto &value = spectrum.ritzValues[index];
experiment::record_experiment_result(
"gravity_preconditioning_p4", candidate + "_ritz_" + std::to_string(index),
commonParameters(candidate, "ritz_value", dimension),
{{"ritz_index", static_cast<double>(index)},
{"real_part", value.realPart},
{"imaginary_part", value.imaginaryPart},
{"magnitude", value.magnitude},
{"distance_from_one", value.distanceFromOne},
{"residual_estimate", value.residualEstimate},
{"relative_residual_estimate", value.relativeResidualEstimate},
{"converged", value.converged ? 1.0 : 0.0}}
);
}
}
void measureCandidate(
const std::string &candidate,
mfem::Solver &inversePreconditioner,
const double setupSeconds,
const ReducedGravityOperator &gravityOperator,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator
) {
constexpr int arnoldiDimension = 32;
mean_field::solver::InstrumentedOperator instrumentedGravity(gravityOperator);
mean_field::solver::InstrumentedPreconditioner instrumentedPreconditioner(inversePreconditioner);
mean_field::solver::ResidualHistoryMonitor monitor;
mfem::FGMRESSolver krylov(communicator);
krylov.SetPreconditioner(instrumentedPreconditioner);
krylov.SetOperator(instrumentedGravity);
krylov.SetMonitor(monitor);
krylov.SetRelTol(1.0e-8);
krylov.SetAbsTol(1.0e-12);
krylov.SetMaxIter(100);
krylov.SetKDim(30);
krylov.SetPrintLevel(0);
mfem::Vector solution(gravityOperator.Width());
solution = 0.0;
announce(communicator, "P4 reduced gravity: solving with " + candidate);
const Clock::time_point solveStart = Clock::now();
krylov.Mult(rightHandSide, solution);
const double solveSeconds = maximumRankSeconds(solveStart, communicator);
mfem::Vector reconstructed(rightHandSide.Size());
gravityOperator.Mult(solution, reconstructed);
reconstructed -= rightHandSide;
const double relativeResidual =
globalNorm(reconstructed, communicator) /
std::max(globalNorm(rightHandSide, communicator), std::numeric_limits<double>::epsilon());
const auto jacobianStatistics = instrumentedGravity.GetStatistics();
const auto preconditionerStatistics = instrumentedPreconditioner.GetStatistics();
REQUIRE(std::isfinite(relativeResidual));
experiment::record_experiment_result(
"gravity_preconditioning_p4", candidate + "_linear_solve",
commonParameters(candidate, "linear_solve", gravityOperator.Width()),
{{"setup_seconds_maximum_rank", setupSeconds},
{"solver_converged", krylov.GetConverged() ? 1.0 : 0.0},
{"outer_iterations", static_cast<double>(krylov.GetNumIterations())},
{"true_relative_residual", relativeResidual},
{"solve_seconds_maximum_rank", solveSeconds},
{"gravity_applications", static_cast<double>(jacobianStatistics.applications)},
{"gravity_application_seconds", jacobianStatistics.totalSeconds},
{"preconditioner_applications", static_cast<double>(preconditionerStatistics.applications)},
{"preconditioner_application_seconds", preconditionerStatistics.totalSeconds},
{"preconditioner_maximum_application_seconds", preconditionerStatistics.maximumSeconds}}
);
instrumentedGravity.ResetStatistics();
instrumentedPreconditioner.ResetStatistics();
mean_field::solver::FixedRightPreconditionedOperator product(instrumentedGravity, instrumentedPreconditioner);
announce(communicator, "P4 reduced gravity: measuring " + candidate + " Arnoldi spectrum");
const auto spectrum = mean_field::solver::measureArnoldiSpectrum(
product, arnoldiDirection, communicator,
{.krylovDimension = arnoldiDimension,
.breakdownRelativeTolerance = 1.0e-13,
.ritzConvergenceRelativeTolerance = 1.0e-7,
.reorthogonalize = true}
);
recordSpectrum(candidate, spectrum, gravityOperator.Width(), setupSeconds);
}
template <
preconditioning::GravityFactorizationPolicy Policy,
backend::Registered MassBackend = backend::Diagonal>
requires backend::Compatible<
MassBackend,
preconditioning::GravityMassInverseCharacteristics>
void prepareAndMeasureTypedCandidate(
const std::string &candidate,
Policy policy,
const mean_field::fem::FEM &finiteElements,
const mean_field::operators::context::gravity_field::GravityFieldGeometryContext &geometryContext,
const ReducedGravityOperator &gravityOperator,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator,
const int amgCycles = 1,
MassBackend massBackend = {}
) {
const Clock::time_point setupStart = Clock::now();
const auto block = preconditioning::GravityFieldBlock(
std::move(massBackend), backend::HypreBoomerAMG{backend::FixedCycles{.cycles = amgCycles}}, policy
);
auto prepared = preconditioning::prepare(finiteElements, geometryContext, block);
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &massOperator = geometryContext.GetMassOperator();
const mfem::Vector firstMassRightHandSide =
gravity_prepared_test_utils::make_deterministic_vector(massOperator.Width(), 0.41);
const mfem::Vector secondMassRightHandSide =
gravity_prepared_test_utils::make_deterministic_vector(massOperator.Width(), 1.17);
mfem::Vector firstMassAction(massOperator.Width());
mfem::Vector secondMassAction(massOperator.Width());
prepared.GetMassInverse().Mult(firstMassRightHandSide, firstMassAction);
prepared.GetMassInverse().Mult(secondMassRightHandSide, secondMassAction);
mfem::Vector recoveredMassRightHandSide(massOperator.Height());
massOperator.Mult(firstMassAction, recoveredMassRightHandSide);
recoveredMassRightHandSide -= firstMassRightHandSide;
const double massRecoveryDefect =
globalNorm(recoveredMassRightHandSide, communicator) / globalNorm(firstMassRightHandSide, communicator);
const double firstSecond = globalDot(firstMassRightHandSide, secondMassAction, communicator);
const double secondFirst = globalDot(secondMassRightHandSide, firstMassAction, communicator);
const double massSymmetryDefect =
std::abs(firstSecond - secondFirst) / std::max({1.0, std::abs(firstSecond), std::abs(secondFirst)});
const double massPositiveRayleigh = globalDot(firstMassRightHandSide, firstMassAction, communicator) /
std::max(
globalDot(firstMassRightHandSide, firstMassRightHandSide, communicator),
std::numeric_limits<double>::min()
);
const auto &schurOperator = prepared.GetPotentialSchurSurrogate();
const mfem::Vector schurRightHandSide =
gravity_prepared_test_utils::make_deterministic_vector(schurOperator.Width(), 0.73);
mfem::Vector schurAction(schurOperator.Width());
prepared.GetPotentialSchurInverse().Mult(schurRightHandSide, schurAction);
mfem::Vector recoveredSchurRightHandSide(schurOperator.Height());
schurOperator.Mult(schurAction, recoveredSchurRightHandSide);
recoveredSchurRightHandSide -= schurRightHandSide;
const double schurRecoveryDefect =
globalNorm(recoveredSchurRightHandSide, communicator) / globalNorm(schurRightHandSide, communicator);
experiment::record_experiment_result(
"gravity_preconditioning_p4", candidate + "_block_quality",
commonParameters(candidate, "block_inverse_quality", gravityOperator.Width()),
{{"amg_cycles", static_cast<double>(amgCycles)},
{"mass_inverse_recovery_defect", massRecoveryDefect},
{"mass_inverse_symmetry_defect", massSymmetryDefect},
{"mass_inverse_positive_rayleigh", massPositiveRayleigh},
{"potential_schur_inverse_recovery_defect", schurRecoveryDefect}}
);
measureCandidate(
candidate, prepared, setupTime, gravityOperator, rightHandSide, arnoldiDirection, communicator
);
}
[[nodiscard]] int firstReportedThresholdIteration(
const std::vector<mean_field::solver::IterationResidualMeasurement> &history,
const double initialNorm,
const double relativeThreshold
) {
if (!std::isfinite(initialNorm) || initialNorm <= 0.0) {
return -1;
}
for (const auto &sample : history) {
if (std::abs(sample.reportedNorm) / initialNorm <= relativeThreshold) {
return sample.iteration;
}
}
return -1;
}
} // namespace
TEST_CASE(
"Reduced Gravity P4 Factorization Comparison",
"[preconditioning][gravity][diagnostics][experiment][spectrum]"
) {
const auto arguments = test_utils::setup_args();
mean_field::fem::FEM finiteElements = mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
using GeometryContext = mean_field::operators::context::gravity_field::GravityFieldGeometryContext;
GeometryContext geometryContext(finiteElements, *finiteElements.domainMapperStateless);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
const mfem::Vector displacement = geometryContext.GetDisplacementMap().gather(displacementTrue);
geometryContext.PreparePrimal(displacement, {.value = 1}, {.value = 1});
ReducedGravityOperator gravityOperator(geometryContext);
const mfem::Vector exact = gravity_prepared_test_utils::make_deterministic_vector(gravityOperator.Width(), 0.37);
mfem::Vector rightHandSide(gravityOperator.Height());
gravityOperator.Mult(exact, rightHandSide);
const mfem::Vector arnoldiDirection =
gravity_prepared_test_utils::make_deterministic_vector(gravityOperator.Width(), 0.83);
const Clock::time_point legacySetupStart = Clock::now();
mean_field::operators::ReducedGravityFieldPreconditioner legacy(finiteElements, geometryContext);
const double legacySetupTime = maximumRankSeconds(legacySetupStart, communicator);
measureCandidate(
"legacy_block_diagonal", legacy, legacySetupTime, gravityOperator, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureTypedCandidate(
"typed_block_diagonal", preconditioning::GravityBlockDiagonal{}, finiteElements, geometryContext,
gravityOperator, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureTypedCandidate(
"lower_triangular", preconditioning::GravityLowerTriangular{}, finiteElements, geometryContext, gravityOperator,
rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureTypedCandidate(
"upper_triangular", preconditioning::GravityUpperTriangular{}, finiteElements, geometryContext, gravityOperator,
rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureTypedCandidate(
"approximate_ldu", preconditioning::GravityApproximateLDU{}, finiteElements, geometryContext, gravityOperator,
rightHandSide, arnoldiDirection, communicator
);
}
TEST_CASE(
"Reduced Gravity P4 Fixed AMG Cycle Sweep",
"[preconditioning][gravity][diagnostics][experiment][amg_cycle_sweep]"
) {
const auto arguments = test_utils::setup_args();
mean_field::fem::FEM finiteElements = mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
using GeometryContext = mean_field::operators::context::gravity_field::GravityFieldGeometryContext;
GeometryContext geometryContext(finiteElements, *finiteElements.domainMapperStateless);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
const mfem::Vector displacement = geometryContext.GetDisplacementMap().gather(displacementTrue);
geometryContext.PreparePrimal(displacement, {.value = 1}, {.value = 1});
ReducedGravityOperator gravityOperator(geometryContext);
const mfem::Vector exact = gravity_prepared_test_utils::make_deterministic_vector(gravityOperator.Width(), 0.37);
mfem::Vector rightHandSide(gravityOperator.Height());
gravityOperator.Mult(exact, rightHandSide);
const mfem::Vector arnoldiDirection =
gravity_prepared_test_utils::make_deterministic_vector(gravityOperator.Width(), 0.83);
for (const int cycles : {1, 2, 3, 4, 6, 8}) {
prepareAndMeasureTypedCandidate(
"approximate_ldu_amg_cycles_" + std::to_string(cycles), preconditioning::GravityApproximateLDU{},
finiteElements, geometryContext, gravityOperator, rightHandSide, arnoldiDirection, communicator, cycles
);
}
for (const int order : {2, 3, 4, 5}) {
for (const int cycles : {1, 2, 3}) {
prepareAndMeasureTypedCandidate(
"approximate_ldu_chebyshev_" + std::to_string(order) + "_amg_cycles_" + std::to_string(cycles),
preconditioning::GravityApproximateLDU{}, finiteElements, geometryContext, gravityOperator,
rightHandSide, arnoldiDirection, communicator, cycles,
backend::MatrixFreeChebyshev{.order = order, .powerIterations = 20}
);
}
}
}
TEST_CASE(
"Reduced Gravity P4 LDU Extended FGMRES Convergence",
"[preconditioning][gravity][diagnostics][experiment][p4_followup][extended_solve]"
) {
constexpr int maximumIterations = 200;
constexpr int restartDimension = 30;
const auto arguments = test_utils::setup_args();
mean_field::fem::FEM finiteElements = mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
using GeometryContext = mean_field::operators::context::gravity_field::GravityFieldGeometryContext;
GeometryContext geometryContext(finiteElements, *finiteElements.domainMapperStateless);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
const mfem::Vector displacement = geometryContext.GetDisplacementMap().gather(displacementTrue);
geometryContext.PreparePrimal(displacement, {.value = 1}, {.value = 1});
ReducedGravityOperator gravityOperator(geometryContext);
const mfem::Vector exact = gravity_prepared_test_utils::make_deterministic_vector(gravityOperator.Width(), 0.37);
mfem::Vector rightHandSide(gravityOperator.Height());
gravityOperator.Mult(exact, rightHandSide);
const Clock::time_point setupStart = Clock::now();
const auto block = preconditioning::GravityFieldBlock(
backend::Diagonal{}, backend::HypreBoomerAMG{backend::FixedCycles{.cycles = 1}},
preconditioning::GravityApproximateLDU{}
);
auto prepared = preconditioning::prepare(finiteElements, geometryContext, block);
const double setupTime = maximumRankSeconds(setupStart, communicator);
mean_field::solver::InstrumentedOperator instrumentedGravity(gravityOperator);
mean_field::solver::InstrumentedPreconditioner instrumentedPreconditioner(prepared);
mean_field::solver::ResidualHistoryMonitor monitor;
mfem::FGMRESSolver krylov(communicator);
krylov.SetPreconditioner(instrumentedPreconditioner);
krylov.SetOperator(instrumentedGravity);
krylov.SetMonitor(monitor);
krylov.SetRelTol(1.0e-8);
krylov.SetAbsTol(1.0e-12);
krylov.SetMaxIter(maximumIterations);
krylov.SetKDim(restartDimension);
krylov.SetPrintLevel(0);
mfem::Vector solution(gravityOperator.Width());
solution = 0.0;
announce(communicator, "P4 follow-up: running 200-iteration approximate-LDU FGMRES");
const Clock::time_point solveStart = Clock::now();
krylov.Mult(rightHandSide, solution);
const double solveSeconds = maximumRankSeconds(solveStart, communicator);
mfem::Vector reconstructed(rightHandSide.Size());
gravityOperator.Mult(solution, reconstructed);
reconstructed -= rightHandSide;
const double trueRelativeResidual =
globalNorm(reconstructed, communicator) /
std::max(globalNorm(rightHandSide, communicator), std::numeric_limits<double>::epsilon());
const double initialNorm = std::abs(krylov.GetInitialNorm());
const auto &history = monitor.GetHistory();
const int iteration1e4 = firstReportedThresholdIteration(history, initialNorm, 1.0e-4);
const int iteration1e6 = firstReportedThresholdIteration(history, initialNorm, 1.0e-6);
const int iteration1e8 = firstReportedThresholdIteration(history, initialNorm, 1.0e-8);
REQUIRE(std::isfinite(trueRelativeResidual));
REQUIRE_FALSE(history.empty());
experiment::record_experiment_result(
"gravity_preconditioning_p4_followup", "approximate_ldu_extended_linear_solve",
commonParameters("approximate_ldu_extended", "linear_solve", gravityOperator.Width()),
{{"maximum_iterations", static_cast<double>(maximumIterations)},
{"restart_dimension", static_cast<double>(restartDimension)},
{"setup_seconds_maximum_rank", setupTime},
{"solver_converged", krylov.GetConverged() ? 1.0 : 0.0},
{"outer_iterations", static_cast<double>(krylov.GetNumIterations())},
{"reported_initial_residual_norm", initialNorm},
{"reported_final_residual_norm", std::abs(krylov.GetFinalNorm())},
{"reported_residual_reduction", initialNorm > 0.0 ? std::abs(krylov.GetFinalNorm()) / initialNorm : 0.0},
{"reported_iteration_to_1e-4", static_cast<double>(iteration1e4)},
{"reported_iteration_to_1e-6", static_cast<double>(iteration1e6)},
{"reported_iteration_to_1e-8", static_cast<double>(iteration1e8)},
{"true_relative_residual", trueRelativeResidual},
{"solve_seconds_maximum_rank", solveSeconds},
{"gravity_applications", static_cast<double>(instrumentedGravity.GetStatistics().applications)},
{"gravity_application_seconds", instrumentedGravity.GetStatistics().totalSeconds},
{"preconditioner_applications", static_cast<double>(instrumentedPreconditioner.GetStatistics().applications)},
{"preconditioner_application_seconds", instrumentedPreconditioner.GetStatistics().totalSeconds}}
);
for (std::size_t index = 0; index < history.size(); ++index) {
const auto &sample = history[index];
experiment::record_experiment_result(
"gravity_preconditioning_p4_followup", "approximate_ldu_history_" + std::to_string(index),
commonParameters("approximate_ldu_extended", "fgmres_residual_history", gravityOperator.Width()),
{{"history_sample", static_cast<double>(index)},
{"iteration", static_cast<double>(sample.iteration)},
{"reported_residual_norm", sample.reportedNorm},
{"reported_relative_residual", initialNorm > 0.0 ? std::abs(sample.reportedNorm) / initialNorm : 0.0},
{"final_measurement", sample.final ? 1.0 : 0.0}}
);
}
}
TEST_CASE(
"Reduced Gravity P4 LDU Extended Arnoldi Convergence",
"[preconditioning][gravity][diagnostics][experiment][spectrum][p4_followup][extended_arnoldi]"
) {
constexpr int arnoldiDimension = 96;
const auto arguments = test_utils::setup_args();
mean_field::fem::FEM finiteElements = mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
using GeometryContext = mean_field::operators::context::gravity_field::GravityFieldGeometryContext;
GeometryContext geometryContext(finiteElements, *finiteElements.domainMapperStateless);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
const mfem::Vector displacement = geometryContext.GetDisplacementMap().gather(displacementTrue);
geometryContext.PreparePrimal(displacement, {.value = 1}, {.value = 1});
ReducedGravityOperator gravityOperator(geometryContext);
const mfem::Vector arnoldiDirection =
gravity_prepared_test_utils::make_deterministic_vector(gravityOperator.Width(), 0.83);
const Clock::time_point setupStart = Clock::now();
const auto block = preconditioning::GravityFieldBlock(
backend::Diagonal{}, backend::HypreBoomerAMG{backend::FixedCycles{.cycles = 1}},
preconditioning::GravityApproximateLDU{}
);
auto prepared = preconditioning::prepare(finiteElements, geometryContext, block);
const double setupTime = maximumRankSeconds(setupStart, communicator);
mean_field::solver::InstrumentedOperator instrumentedGravity(gravityOperator);
mean_field::solver::InstrumentedPreconditioner instrumentedPreconditioner(prepared);
mean_field::solver::FixedRightPreconditionedOperator product(instrumentedGravity, instrumentedPreconditioner);
announce(communicator, "P4 follow-up: measuring the 96-vector approximate-LDU Arnoldi spectrum");
const auto spectrum = mean_field::solver::measureArnoldiSpectrum(
product, arnoldiDirection, communicator,
{.krylovDimension = arnoldiDimension,
.breakdownRelativeTolerance = 1.0e-13,
.ritzConvergenceRelativeTolerance = 1.0e-7,
.reorthogonalize = true}
);
REQUIRE(spectrum.achievedDimension > 32);
recordSpectrum("approximate_ldu_arnoldi_96", spectrum, gravityOperator.Width(), setupTime);
}

View File

@@ -0,0 +1,993 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <limits>
#include <map>
#include <numbers>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import experiment;
import mean_field;
import test_helpers;
namespace {
using Clock = std::chrono::steady_clock;
namespace backend = mean_field::preconditioning::backend;
namespace preconditioning = mean_field::preconditioning;
namespace solver = mean_field::solver;
struct MaterialBlockMeasurements final {
double density{0.0};
double surface{0.0};
double enthalpy{0.0};
};
[[nodiscard]] const char *buildConfiguration() noexcept {
#ifdef NDEBUG
return "release";
#else
return "debug";
#endif
}
[[nodiscard]] double maximumRankSeconds(
const Clock::time_point start,
const MPI_Comm communicator
) {
const double localSeconds = std::chrono::duration<double>(Clock::now() - start).count();
double maximumSeconds = 0.0;
MPI_Allreduce(&localSeconds, &maximumSeconds, 1, MPI_DOUBLE, MPI_MAX, communicator);
return maximumSeconds;
}
[[nodiscard]] double globalNorm(
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));
}
void announce(
const MPI_Comm communicator,
const std::string &message
) {
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "[P9 material-surface] " << message << std::endl;
}
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies() {
return {
.discretization = {.identity = 10103, .revision = 1},
.density = {.identity = 10111, .revision = 1},
.surfaceDeformation = {.identity = 10133, .revision = 1},
.gravityGradient = {.identity = 10139, .revision = 1},
.gravityPotential = {.identity = 10141, .revision = 1},
.enthalpy = {.identity = 10151, .revision = 1},
.bernoulliConstant = {.identity = 10159, .revision = 1},
.rotation = {.identity = 10163, .revision = 1},
.targetMass = {.identity = 10169, .revision = 1}
};
}
[[nodiscard]] mean_field::physics::RigidRotation zeroRotation() {
mfem::Vector angularVelocity(3);
mfem::Vector center(3);
angularVelocity = 0.0;
center = 0.0;
return {angularVelocity, center};
}
[[nodiscard]] mfem::Vector blockBalancedDirection(
const mfem::Array<int> &offsets,
const double phase,
const MPI_Comm communicator
) {
mfem::Vector direction(offsets.Last());
direction = 0.0;
for (int block = 0; block < offsets.Size() - 1; ++block) {
mfem::Vector values(direction, offsets[block], offsets[block + 1] - offsets[block]);
for (int index = 0; index < values.Size(); ++index) {
const double ordinal = static_cast<double>(index + 1);
values(index) = std::sin(0.371 * ordinal + phase + static_cast<double>(block)) +
0.29 * std::cos(0.173 * ordinal - 0.5 * phase);
}
const double norm = globalNorm(values, communicator);
REQUIRE(norm > 0.0);
values /= norm;
values.SyncAliasMemory(direction);
}
return direction;
}
[[nodiscard]] MaterialBlockMeasurements blockNorms(
const mfem::Vector &vector,
const mfem::Array<int> &offsets,
const MPI_Comm communicator
) {
REQUIRE(offsets.Size() == 4);
const mfem::Vector density(const_cast<mfem::real_t *>(vector.GetData()) + offsets[0], offsets[1] - offsets[0]);
const mfem::Vector surface(const_cast<mfem::real_t *>(vector.GetData()) + offsets[1], offsets[2] - offsets[1]);
const mfem::Vector enthalpy(const_cast<mfem::real_t *>(vector.GetData()) + offsets[2], offsets[3] - offsets[2]);
return {
.density = globalNorm(density, communicator),
.surface = globalNorm(surface, communicator),
.enthalpy = globalNorm(enthalpy, communicator)
};
}
[[nodiscard]] MaterialBlockMeasurements relativeBlockNorms(
const mfem::Vector &numerator,
const mfem::Vector &denominator,
const mfem::Array<int> &offsets,
const MPI_Comm communicator
) {
const MaterialBlockMeasurements numeratorNorms = blockNorms(numerator, offsets, communicator);
const MaterialBlockMeasurements denominatorNorms = blockNorms(denominator, offsets, communicator);
constexpr double floor = 1.0e-300;
return {
.density = numeratorNorms.density / std::max(denominatorNorms.density, floor),
.surface = numeratorNorms.surface / std::max(denominatorNorms.surface, floor),
.enthalpy = numeratorNorms.enthalpy / std::max(denominatorNorms.enthalpy, floor)
};
}
[[nodiscard]] std::map<
std::string,
std::string>
commonParameters(
const std::string &candidate,
const std::string &measurement,
const int dimension
) {
return {
{"build_configuration", buildConfiguration()},
{"candidate", candidate},
{"equation_of_state", "Polytrope(n=1)"},
{"experiment_schema", "p9_material_surface_v1"},
{"factorization", candidate},
{"linearization_state", "projected_lane_emden"},
{"measurement", measurement},
{"mesh_file", test_utils::setup_args().mesh_file},
{"operator", "restricted_material_surface_jacobian"},
{"preconditioned_product", "A_material_surface M^-1"},
{"root_dimension", std::to_string(dimension)},
{"rotation", "zero"}
};
}
void recordSpectrum(
const std::string &candidate,
const solver::ArnoldiSpectralMeasurement &spectrum,
const int dimension,
const double setupSeconds
) {
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_arnoldi_summary",
commonParameters(candidate, "arnoldi_summary", dimension),
{{"setup_seconds_maximum_rank", setupSeconds},
{"requested_dimension", static_cast<double>(spectrum.requestedDimension)},
{"achieved_dimension", static_cast<double>(spectrum.achievedDimension)},
{"invariant_subspace_found", spectrum.invariantSubspaceFound ? 1.0 : 0.0},
{"operator_applications", static_cast<double>(spectrum.operatorApplications)},
{"measurement_seconds_maximum_rank", spectrum.measurementSecondsMaximumRank},
{"operator_application_seconds_maximum_rank", spectrum.operatorApplicationSecondsMaximumRank},
{"projected_condition_proxy", spectrum.projectedConditionProxy},
{"projected_largest_singular_value", spectrum.projectedLargestSingularValue},
{"projected_smallest_singular_value", spectrum.projectedSmallestSingularValue},
{"centroid_real_part", spectrum.centroidRealPart},
{"centroid_imaginary_part", spectrum.centroidImaginaryPart},
{"rms_distance_from_one", spectrum.rmsDistanceFromOne},
{"rms_cluster_radius", spectrum.rmsClusterRadius},
{"minimum_magnitude", spectrum.minimumMagnitude},
{"maximum_magnitude", spectrum.maximumMagnitude},
{"minimum_real_part", spectrum.minimumRealPart},
{"maximum_real_part", spectrum.maximumRealPart},
{"maximum_absolute_imaginary_part", spectrum.maximumAbsoluteImaginaryPart},
{"negative_real_part_count", static_cast<double>(spectrum.negativeRealPartCount)},
{"converged_ritz_value_count", static_cast<double>(spectrum.convergedRitzValueCount)},
{"conjugate_pair_defect", spectrum.conjugatePairDefect},
{"projected_departure_from_normality", spectrum.projectedDepartureFromNormality},
{"field_of_values_minimum_real_part", spectrum.projectedFieldOfValuesMinimumRealPart},
{"field_of_values_maximum_real_part", spectrum.projectedFieldOfValuesMaximumRealPart}}
);
std::vector<solver::RitzValueMeasurement> ordered = spectrum.ritzValues;
std::ranges::sort(ordered, [](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 < ordered.size(); ++index) {
const auto &value = ordered[index];
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_ritz_" + std::to_string(index),
commonParameters(candidate, "ritz_value", dimension),
{{"ritz_index", static_cast<double>(index)},
{"real_part", value.realPart},
{"imaginary_part", value.imaginaryPart},
{"magnitude", value.magnitude},
{"distance_from_one", value.distanceFromOne},
{"residual_estimate", value.residualEstimate},
{"relative_residual_estimate", value.relativeResidualEstimate},
{"converged", value.converged ? 1.0 : 0.0}}
);
}
}
template <typename Preconditioner>
void measureCandidate(
const std::string &candidate,
Preconditioner &inversePreconditioner,
const double setupSeconds,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const mfem::Vector &exactCorrection,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator,
std::map<
std::string,
double> preparationMetrics = {}
) {
constexpr int maximumIterations = 40;
constexpr int restartDimension = 20;
constexpr int arnoldiDimension = 16;
solver::InstrumentedOperator instrumentedOperation(operation);
solver::InstrumentedPreconditioner instrumentedPreconditioner(inversePreconditioner);
solver::ResidualHistoryMonitor monitor;
mfem::FGMRESSolver krylov(communicator);
krylov.SetPreconditioner(instrumentedPreconditioner);
krylov.SetOperator(instrumentedOperation);
krylov.SetMonitor(monitor);
krylov.SetRelTol(1.0e-8);
krylov.SetAbsTol(1.0e-12);
krylov.SetMaxIter(maximumIterations);
krylov.SetKDim(restartDimension);
krylov.SetPrintLevel(0);
mfem::Vector solution(operation.Width());
solution = 0.0;
announce(communicator, "solving manufactured system with " + candidate);
const Clock::time_point solveStart = Clock::now();
krylov.Mult(rightHandSide, solution);
const double solveSeconds = maximumRankSeconds(solveStart, communicator);
mfem::Vector trueResidual(operation.Height());
operation.Mult(solution, trueResidual);
trueResidual -= rightHandSide;
mfem::Vector solutionError(solution);
solutionError -= exactCorrection;
const double trueRelativeResidual =
globalNorm(trueResidual, communicator) /
std::max(globalNorm(rightHandSide, communicator), std::numeric_limits<double>::min());
const double relativeSolutionError =
globalNorm(solutionError, communicator) /
std::max(globalNorm(exactCorrection, communicator), std::numeric_limits<double>::min());
const MaterialBlockMeasurements relativeResidualBlocks =
relativeBlockNorms(trueResidual, rightHandSide, operation.GetOffsets(), communicator);
mfem::Vector preconditionedDirection(operation.Height());
inversePreconditioner.Mult(arnoldiDirection, preconditionedDirection);
mfem::Vector defect(operation.Height());
operation.Mult(preconditionedDirection, defect);
defect -= arnoldiDirection;
const MaterialBlockMeasurements defectBlocks = blockNorms(defect, operation.GetOffsets(), communicator);
const double defectNorm =
globalNorm(defect, communicator) /
std::max(globalNorm(arnoldiDirection, communicator), std::numeric_limits<double>::min());
std::map<std::string, double> solveMetrics{
{"setup_seconds_maximum_rank", setupSeconds},
{"maximum_iterations", static_cast<double>(maximumIterations)},
{"restart_dimension", static_cast<double>(restartDimension)},
{"solver_converged", krylov.GetConverged() ? 1.0 : 0.0},
{"outer_iterations", static_cast<double>(krylov.GetNumIterations())},
{"reported_initial_residual_norm", std::abs(krylov.GetInitialNorm())},
{"reported_final_residual_norm", std::abs(krylov.GetFinalNorm())},
{"true_relative_residual", trueRelativeResidual},
{"relative_solution_error", relativeSolutionError},
{"density_relative_residual", relativeResidualBlocks.density},
{"surface_relative_residual", relativeResidualBlocks.surface},
{"enthalpy_relative_residual", relativeResidualBlocks.enthalpy},
{"right_preconditioned_defect", defectNorm},
{"density_defect_norm", defectBlocks.density},
{"surface_defect_norm", defectBlocks.surface},
{"enthalpy_defect_norm", defectBlocks.enthalpy},
{"solve_seconds_maximum_rank", solveSeconds},
{"jacobian_applications", static_cast<double>(instrumentedOperation.GetStatistics().applications)},
{"jacobian_application_seconds", instrumentedOperation.GetStatistics().totalSeconds},
{"preconditioner_applications",
static_cast<double>(instrumentedPreconditioner.GetStatistics().applications)},
{"preconditioner_application_seconds", instrumentedPreconditioner.GetStatistics().totalSeconds},
{"preconditioner_maximum_application_seconds", instrumentedPreconditioner.GetStatistics().maximumSeconds}
};
solveMetrics.insert(preparationMetrics.begin(), preparationMetrics.end());
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_linear_solve",
commonParameters(candidate, "manufactured_linear_solve", operation.Width()), std::move(solveMetrics)
);
const double initialNorm = std::max(std::abs(krylov.GetInitialNorm()), 1.0e-300);
const auto &history = monitor.GetHistory();
for (std::size_t index = 0; index < history.size(); ++index) {
const auto &sample = history[index];
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_history_" + std::to_string(index),
commonParameters(candidate, "fgmres_residual_history", operation.Width()),
{{"history_sample", static_cast<double>(index)},
{"iteration", static_cast<double>(sample.iteration)},
{"reported_residual_norm", sample.reportedNorm},
{"reported_relative_residual", std::abs(sample.reportedNorm) / initialNorm},
{"final_measurement", sample.final ? 1.0 : 0.0}}
);
}
instrumentedOperation.ResetStatistics();
instrumentedPreconditioner.ResetStatistics();
solver::FixedRightPreconditionedOperator product(instrumentedOperation, instrumentedPreconditioner);
announce(communicator, "measuring " + candidate + " with 16-vector Arnoldi");
const solver::ArnoldiSpectralMeasurement spectrum = solver::measureArnoldiSpectrum(
product, arnoldiDirection, communicator,
{.krylovDimension = arnoldiDimension,
.breakdownRelativeTolerance = 1.0e-13,
.ritzConvergenceRelativeTolerance = 1.0e-7,
.reorthogonalize = true}
);
REQUIRE(std::isfinite(trueRelativeResidual));
REQUIRE(std::isfinite(relativeSolutionError));
REQUIRE(std::isfinite(defectNorm));
REQUIRE(std::isfinite(spectrum.projectedConditionProxy));
recordSpectrum(candidate, spectrum, operation.Width(), setupSeconds);
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "[P9 material-surface] " << candidate << ": iterations=" << krylov.GetNumIterations()
<< ", converged=" << (krylov.GetConverged() ? "yes" : "no")
<< ", true residual=" << trueRelativeResidual << ", defect=" << defectNorm
<< ", projected condition=" << spectrum.projectedConditionProxy << '\n';
}
}
template <preconditioning::MaterialSurfaceFactorizationPolicy Policy>
void prepareAndMeasure(
const std::string &candidate,
const Policy policy,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const mfem::Vector &exactCorrection,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator,
const preconditioning::MaterialSurfaceDiagonalOptions diagonalOptions = {}
) {
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::Diagonal{}, policy, diagonalOptions
);
auto prepared = preconditioning::prepare(problem, block);
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &density = prepared.GetDensityDiagonalQuality();
const auto &surface = prepared.GetSurfaceDiagonalQuality();
const auto &enthalpy = prepared.GetEnthalpyDiagonalQuality();
const auto &calibration = prepared.GetSurfaceCalibration();
measureCandidate(
candidate, prepared, setupTime, operation, exactCorrection, rightHandSide, arnoldiDirection, communicator,
{{"density_diagonal_minimum", density.minimumAbsoluteEntryBeforeRegularization},
{"density_diagonal_maximum", density.maximumAbsoluteEntryBeforeRegularization},
{"density_diagonal_floor", density.appliedFloor},
{"density_regularized_entries", static_cast<double>(density.regularizedEntries)},
{"surface_diagonal_minimum", surface.minimumAbsoluteEntryBeforeRegularization},
{"surface_diagonal_maximum", surface.maximumAbsoluteEntryBeforeRegularization},
{"surface_diagonal_floor", surface.appliedFloor},
{"surface_regularized_entries", static_cast<double>(surface.regularizedEntries)},
{"surface_calibration_target", static_cast<double>(calibration.target)},
{"surface_calibration_probes", static_cast<double>(calibration.probeCount)},
{"surface_calibration_objective", static_cast<double>(calibration.objective)},
{"surface_calibration_scale", calibration.scale},
{"surface_calibration_inverse_multiplier", calibration.inverseMultiplier},
{"surface_calibration_numerator", calibration.leastSquaresNumerator},
{"surface_calibration_denominator", calibration.leastSquaresDenominator},
{"enthalpy_diagonal_minimum", enthalpy.minimumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_maximum", enthalpy.maximumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_floor", enthalpy.appliedFloor},
{"enthalpy_regularized_entries", static_cast<double>(enthalpy.regularizedEntries)}}
);
}
template <preconditioning::MaterialSurfaceFactorizationPolicy Policy>
void prepareAndMeasureH1(
const std::string &candidate,
const Policy policy,
const int fixedAMGCycles,
const int calibrationProbeCount,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const mfem::Vector &exactCorrection,
const mfem::Vector &rightHandSide,
const mfem::Vector &arnoldiDirection,
const MPI_Comm communicator
) {
REQUIRE(fixedAMGCycles > 0);
REQUIRE(calibrationProbeCount >= 3);
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::HypreBoomerAMG{backend::FixedCycles{.cycles = fixedAMGCycles}},
policy,
preconditioning::SurfaceH1MassStiffness{
.calibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian,
.probeCount = calibrationProbeCount
}
}
);
auto prepared = preconditioning::prepare(problem, std::move(block));
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &density = prepared.GetDensityDiagonalQuality();
const auto &enthalpy = prepared.GetEnthalpyDiagonalQuality();
const auto &fit = prepared.GetSurfaceFit();
measureCandidate(
candidate, prepared, setupTime, operation, exactCorrection, rightHandSide, arnoldiDirection, communicator,
{{"density_diagonal_minimum", density.minimumAbsoluteEntryBeforeRegularization},
{"density_diagonal_maximum", density.maximumAbsoluteEntryBeforeRegularization},
{"density_diagonal_floor", density.appliedFloor},
{"density_regularized_entries", static_cast<double>(density.regularizedEntries)},
{"surface_h1_calibration_target", static_cast<double>(fit.target)},
{"surface_h1_calibration_probes", static_cast<double>(fit.probeCount)},
{"surface_h1_fit_sign", fit.sign},
{"surface_h1_mass_coefficient", fit.massCoefficient},
{"surface_h1_stiffness_coefficient", fit.stiffnessCoefficient},
{"surface_h1_fit_relative_residual", fit.relativeResidual},
{"surface_h1_fit_relative_gram_determinant", fit.relativeGramDeterminant},
{"surface_amg_fixed_cycles", static_cast<double>(fixedAMGCycles)},
{"enthalpy_diagonal_minimum", enthalpy.minimumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_maximum", enthalpy.maximumAbsoluteEntryBeforeRegularization},
{"enthalpy_diagonal_floor", enthalpy.appliedFloor},
{"enthalpy_regularized_entries", static_cast<double>(enthalpy.regularizedEntries)}}
);
const auto &surfaceBackendStatistics = prepared.GetSurfaceBackend().GetStatistics();
const auto &factorizationStatistics = prepared.GetFactorization().GetStatistics();
const auto &preparationStatistics = prepared.GetStatistics();
auto parameters = commonParameters(candidate, "surface_h1_backend_statistics", operation.Width());
parameters["experiment_schema"] = "p9_material_surface_h1_v1";
parameters["surface_surrogate"] = "h1_mass_plus_tangential_stiffness";
parameters["surface_calibration_target"] = "surface_jacobian";
parameters["surface_calibration_probes"] = std::to_string(calibrationProbeCount);
parameters["surface_amg_fixed_cycles"] = std::to_string(fixedAMGCycles);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_surface_h1_backend_statistics", std::move(parameters),
{{"setup_seconds_maximum_rank", setupTime},
{"surface_h1_fit_sign", fit.sign},
{"surface_h1_mass_coefficient", fit.massCoefficient},
{"surface_h1_stiffness_coefficient", fit.stiffnessCoefficient},
{"surface_h1_fit_relative_residual", fit.relativeResidual},
{"surface_h1_fit_relative_gram_determinant", fit.relativeGramDeterminant},
{"surface_backend_setups", static_cast<double>(surfaceBackendStatistics.setups)},
{"surface_backend_applications", static_cast<double>(surfaceBackendStatistics.applications)},
{"surface_backend_inner_iterations", static_cast<double>(surfaceBackendStatistics.innerIterations)},
{"surface_backend_last_inner_iterations",
static_cast<double>(surfaceBackendStatistics.lastInnerIterations)},
{"factorization_applications", static_cast<double>(factorizationStatistics.applications)},
{"surface_inverse_applications", static_cast<double>(factorizationStatistics.surfaceInverseApplications)},
{"block_setups", static_cast<double>(preparationStatistics.setups)},
{"surface_jacobian_probes", static_cast<double>(preparationStatistics.surfaceJacobianProbes)},
{"surface_h1_assemblies", static_cast<double>(preparationStatistics.surfaceH1Assemblies)}}
);
}
enum class SurfaceProbeMode { constant, ordered_low, alternating_high, deterministic_mixed };
[[nodiscard]] const char *surfaceProbeModeName(const SurfaceProbeMode mode) noexcept {
switch (mode) {
case SurfaceProbeMode::constant:
return "constant";
case SurfaceProbeMode::ordered_low:
return "ordered_low";
case SurfaceProbeMode::alternating_high:
return "alternating_high";
case SurfaceProbeMode::deterministic_mixed:
return "deterministic_mixed";
}
return "unknown";
}
[[nodiscard]] mfem::Vector normalizedSurfaceProbe(
const int localSize,
const SurfaceProbeMode mode,
const MPI_Comm communicator
) {
int globalSize = 0;
int offset = 0;
MPI_Allreduce(&localSize, &globalSize, 1, MPI_INT, MPI_SUM, communicator);
MPI_Exscan(&localSize, &offset, 1, MPI_INT, MPI_SUM, communicator);
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
offset = 0;
}
REQUIRE(globalSize > 0);
mfem::Vector probe(localSize);
for (int index = 0; index < localSize; ++index) {
const int globalIndex = offset + index;
const double position = (static_cast<double>(globalIndex) + 0.5) / static_cast<double>(globalSize);
switch (mode) {
case SurfaceProbeMode::constant:
probe(index) = 1.0;
break;
case SurfaceProbeMode::ordered_low:
probe(index) = std::cos(std::numbers::pi_v<double> * position);
break;
case SurfaceProbeMode::alternating_high:
probe(index) = globalIndex % 2 == 0 ? 1.0 : -1.0;
break;
case SurfaceProbeMode::deterministic_mixed:
probe(index) = 0.41 * std::cos(std::numbers::pi_v<double> * position) +
std::sin(5.0 * std::numbers::pi_v<double> * position) +
0.23 * (globalIndex % 2 == 0 ? 1.0 : -1.0);
break;
}
}
const double norm = globalNorm(probe, communicator);
REQUIRE(norm > 0.0);
probe /= norm;
return probe;
}
void applyParameterOverrides(
std::map<
std::string,
std::string> &parameters,
const std::map<
std::string,
std::string> &overrides
) {
for (const auto &[key, value] : overrides) {
parameters.insert_or_assign(key, value);
}
}
template <typename SurfaceInverse>
void recordSurfaceInverseRecovery(
const std::string &candidate,
SurfaceInverse &surfaceInverse,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator,
const std::map<
std::string,
std::string> &parameterOverrides
) {
constexpr std::array modes{
SurfaceProbeMode::constant, SurfaceProbeMode::ordered_low, SurfaceProbeMode::alternating_high,
SurfaceProbeMode::deterministic_mixed
};
const int surfaceSize = operation.GetOffsets()[2] - operation.GetOffsets()[1];
REQUIRE(surfaceInverse.Width() == surfaceSize);
REQUIRE(surfaceInverse.Height() == surfaceSize);
for (const SurfaceProbeMode mode : modes) {
const mfem::Vector probe = normalizedSurfaceProbe(surfaceSize, mode, communicator);
mfem::Vector surfaceAction(surfaceSize);
mfem::Vector recovered(surfaceSize);
operation.ApplySurfaceToSurface(probe, surfaceAction);
const Clock::time_point inverseStart = Clock::now();
surfaceInverse.Mult(surfaceAction, recovered);
const double inverseSeconds = maximumRankSeconds(inverseStart, communicator);
mfem::Vector recoveryError(recovered);
recoveryError -= probe;
const double probeNorm = globalNorm(probe, communicator);
const double actionNorm = globalNorm(surfaceAction, communicator);
const double recoveredNorm = globalNorm(recovered, communicator);
const double relativeError = globalNorm(recoveryError, communicator) / probeNorm;
constexpr double nonzeroFloor = 1.0e-300;
auto parameters = commonParameters(candidate, "surface_inverse_recovery", operation.Width());
applyParameterOverrides(parameters, parameterOverrides);
parameters["surface_probe_mode"] = surfaceProbeModeName(mode);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_" + surfaceProbeModeName(mode),
std::move(parameters),
{{"surface_probe_norm", probeNorm},
{"surface_action_norm", actionNorm},
{"surface_recovered_norm", recoveredNorm},
{"surface_recovery_relative_error", relativeError},
{"surface_operator_gain", actionNorm / probeNorm},
{"surface_inverse_gain", recoveredNorm / std::max(actionNorm, nonzeroFloor)},
{"surface_recovered_gain", recoveredNorm / probeNorm},
{"surface_inverse_seconds_maximum_rank", inverseSeconds}}
);
}
}
template <typename PreparedPreconditioner>
void recordBalancedPreconditionedDefect(
const std::string &candidate,
PreparedPreconditioner &prepared,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator,
const std::map<
std::string,
std::string> &parameterOverrides
) {
const mfem::Vector direction = blockBalancedDirection(operation.GetOffsets(), 1.37, communicator);
mfem::Vector correction(operation.Width());
mfem::Vector defect(operation.Height());
const Clock::time_point applicationStart = Clock::now();
prepared.Mult(direction, correction);
const double applicationSeconds = maximumRankSeconds(applicationStart, communicator);
operation.Mult(correction, defect);
defect -= direction;
const MaterialBlockMeasurements blockDefects = blockNorms(defect, operation.GetOffsets(), communicator);
const double relativeDefect = globalNorm(defect, communicator) /
std::max(globalNorm(direction, communicator), std::numeric_limits<double>::min());
auto parameters = commonParameters(candidate, "balanced_right_preconditioned_defect", operation.Width());
applyParameterOverrides(parameters, parameterOverrides);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_balanced_defect", std::move(parameters),
{{"right_preconditioned_defect", relativeDefect},
{"density_defect_norm", blockDefects.density},
{"surface_defect_norm", blockDefects.surface},
{"enthalpy_defect_norm", blockDefects.enthalpy},
{"preconditioner_application_seconds_maximum_rank", applicationSeconds}}
);
}
void measureH1CalibrationFloor(
const std::string &candidate,
const std::string &relativeMassCoefficientFloorLabel,
const double relativeMassCoefficientFloor,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator
) {
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::HypreBoomerAMG{backend::FixedCycles{.cycles = 1}},
preconditioning::ApproximateMaterialSurfaceLDU{},
preconditioning::SurfaceH1MassStiffness{
.calibration =
{.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian, .probeCount = 4},
.relativeMassCoefficientFloor = relativeMassCoefficientFloor
}
);
auto prepared = preconditioning::prepare(problem, std::move(block));
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &fit = prepared.GetSurfaceFit();
const std::map<std::string, std::string> parameters{
{"experiment_schema", "p9_material_surface_h1_tuning_v1"},
{"surface_surrogate", "h1_mass_plus_tangential_stiffness"},
{"surface_calibration_target", "surface_jacobian"},
{"surface_calibration_probes", "4"},
{"surface_amg_fixed_cycles", "1"},
{"relative_mass_coefficient_floor", relativeMassCoefficientFloorLabel}
};
recordSurfaceInverseRecovery(candidate, prepared.GetSurfaceInverse(), operation, communicator, parameters);
recordBalancedPreconditionedDefect(candidate, prepared, operation, communicator, parameters);
const auto &backendStatistics = prepared.GetSurfaceBackend().GetStatistics();
const auto &factorizationStatistics = prepared.GetFactorization().GetStatistics();
const auto &preparationStatistics = prepared.GetStatistics();
auto summaryParameters = commonParameters(candidate, "surface_h1_floor_summary", operation.Width());
applyParameterOverrides(summaryParameters, parameters);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_summary", std::move(summaryParameters),
{{"setup_seconds_maximum_rank", setupTime},
{"relative_mass_coefficient_floor", relativeMassCoefficientFloor},
{"surface_h1_fit_sign", fit.sign},
{"surface_h1_mass_coefficient", fit.massCoefficient},
{"surface_h1_stiffness_coefficient", fit.stiffnessCoefficient},
{"surface_h1_fit_relative_residual", fit.relativeResidual},
{"surface_h1_fit_relative_gram_determinant", fit.relativeGramDeterminant},
{"surface_backend_setups", static_cast<double>(backendStatistics.setups)},
{"surface_backend_applications", static_cast<double>(backendStatistics.applications)},
{"surface_backend_inner_iterations", static_cast<double>(backendStatistics.innerIterations)},
{"surface_backend_last_inner_iterations", static_cast<double>(backendStatistics.lastInnerIterations)},
{"factorization_applications", static_cast<double>(factorizationStatistics.applications)},
{"surface_inverse_applications", static_cast<double>(factorizationStatistics.surfaceInverseApplications)},
{"surface_jacobian_probes", static_cast<double>(preparationStatistics.surfaceJacobianProbes)},
{"surface_h1_assemblies", static_cast<double>(preparationStatistics.surfaceH1Assemblies)}}
);
}
void measureScalarSurfaceControl(
const std::string &candidate,
const preconditioning::SurfaceRieszCalibrationObjective objective,
const int calibrationProbeCount,
const auto &problem,
const mean_field::preconditioning::MaterialSurfaceJacobianOperator &operation,
const MPI_Comm communicator
) {
REQUIRE(calibrationProbeCount > 0);
const preconditioning::MaterialSurfaceDiagonalOptions calibration{
.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian,
.probeCount = calibrationProbeCount,
.objective = objective
}
};
const Clock::time_point setupStart = Clock::now();
auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::ApproximateMaterialSurfaceLDU{},
calibration
);
auto prepared = preconditioning::prepare(problem, std::move(block));
auto directSurfaceInverse = backend::prepare(backend::Diagonal{}, prepared.GetSurfaceDiagonal());
const double setupTime = maximumRankSeconds(setupStart, communicator);
const auto &calibrationData = prepared.GetSurfaceCalibration();
const std::map<std::string, std::string> parameters{
{"experiment_schema", "p9_material_surface_h1_tuning_v1"},
{"surface_surrogate", "scalar_mass_diagonal"},
{"surface_calibration_target", "surface_jacobian"},
{"surface_calibration_probes", std::to_string(calibrationProbeCount)},
{"surface_calibration_objective",
objective == preconditioning::SurfaceRieszCalibrationObjective::operator_action
? "operator_action"
: "right_preconditioned_action"}
};
recordSurfaceInverseRecovery(candidate, directSurfaceInverse, operation, communicator, parameters);
recordBalancedPreconditionedDefect(candidate, prepared, operation, communicator, parameters);
const auto &directStatistics = directSurfaceInverse.GetStatistics();
const auto &factorizationStatistics = prepared.GetFactorization().GetStatistics();
auto summaryParameters = commonParameters(candidate, "scalar_surface_control_summary", operation.Width());
applyParameterOverrides(summaryParameters, parameters);
experiment::record_experiment_result(
"material_surface_preconditioning_p9", candidate + "_summary", std::move(summaryParameters),
{{"setup_seconds_maximum_rank", setupTime},
{"surface_calibration_scale", calibrationData.scale},
{"surface_calibration_inverse_multiplier", calibrationData.inverseMultiplier},
{"surface_calibration_numerator", calibrationData.leastSquaresNumerator},
{"surface_calibration_denominator", calibrationData.leastSquaresDenominator},
{"surface_backend_setups", static_cast<double>(directStatistics.setups)},
{"surface_backend_applications", static_cast<double>(directStatistics.applications)},
{"factorization_applications", static_cast<double>(factorizationStatistics.applications)},
{"surface_inverse_applications", static_cast<double>(factorizationStatistics.surfaceInverseApplications)}}
);
}
} // namespace
TEST_CASE(
"Material Surface P9 Numerical Factorization Comparison",
"[preconditioning][material_surface][diagnostics][experiment][spectrum][p9][p9_baseline]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
const auto exactCorrection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.23, communicator);
mfem::Vector rightHandSide(materialSurfaceOperator.Height());
materialSurfaceOperator.Mult(exactCorrection, rightHandSide);
const auto arnoldiDirection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.79, communicator);
solver::IdentityPreconditioner identity(materialSurfaceOperator.Width());
measureCandidate(
"identity", identity, 0.0, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection,
communicator
);
prepareAndMeasure(
"block_diagonal", preconditioning::MaterialSurfaceBlockDiagonal{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"material_independent_surface", preconditioning::CoupledMaterialIndependentSurface{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"material_then_surface", preconditioning::MaterialThenSurfaceTriangular{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"surface_then_material", preconditioning::SurfaceThenMaterialTriangular{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"approximate_ldu", preconditioning::ApproximateMaterialSurfaceLDU{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
}
TEST_CASE(
"Material Surface P9 Calibrated LDU Comparison",
"[preconditioning][material_surface][diagnostics][experiment][spectrum][p9][p9_refinement]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
const auto exactCorrection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.23, communicator);
mfem::Vector rightHandSide(materialSurfaceOperator.Height());
materialSurfaceOperator.Mult(exactCorrection, rightHandSide);
const auto arnoldiDirection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.79, communicator);
constexpr preconditioning::MaterialSurfaceDiagonalOptions surfaceJacobianCalibration{
.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian, .probeCount = 4
}
};
constexpr preconditioning::MaterialSurfaceDiagonalOptions surfaceSchurCalibration{
.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::approximate_material_schur, .probeCount = 4
}
};
prepareAndMeasure(
"surface_then_material_calibrated_aqq", preconditioning::SurfaceThenMaterialTriangular{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator,
surfaceJacobianCalibration
);
prepareAndMeasure(
"approximate_ldu", preconditioning::ApproximateMaterialSurfaceLDU{}, problem, materialSurfaceOperator,
exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasure(
"approximate_ldu_calibrated_aqq", preconditioning::ApproximateMaterialSurfaceLDU{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator,
surfaceJacobianCalibration
);
prepareAndMeasure(
"approximate_ldu_calibrated_schur", preconditioning::ApproximateMaterialSurfaceLDU{}, problem,
materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator, surfaceSchurCalibration
);
}
TEST_CASE(
"Material Surface P9 Frequency-Aware Surface Refinement",
"[preconditioning][material_surface][diagnostics][experiment][spectrum][p9][p9_h1_refinement]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
const auto exactCorrection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.23, communicator);
mfem::Vector rightHandSide(materialSurfaceOperator.Height());
materialSurfaceOperator.Mult(exactCorrection, rightHandSide);
const auto arnoldiDirection = blockBalancedDirection(materialSurfaceOperator.GetOffsets(), 0.79, communicator);
constexpr int calibrationProbeCount = 4;
prepareAndMeasureH1(
"h1_aqq_surface_then_material_amg1", preconditioning::SurfaceThenMaterialTriangular{}, 1, calibrationProbeCount,
problem, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureH1(
"h1_aqq_approximate_ldu_amg1", preconditioning::ApproximateMaterialSurfaceLDU{}, 1, calibrationProbeCount,
problem, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
prepareAndMeasureH1(
"h1_aqq_approximate_ldu_amg2", preconditioning::ApproximateMaterialSurfaceLDU{}, 2, calibrationProbeCount,
problem, materialSurfaceOperator, exactCorrection, rightHandSide, arnoldiDirection, communicator
);
}
TEST_CASE(
"Material Surface P9 H1 Calibration Floor Tuning",
"[preconditioning][material_surface][diagnostics][experiment][p9][p9_h1_tuning]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::MaterialSurfaceJacobianOperator materialSurfaceOperator(physical);
constexpr std::array floorCases{
std::pair{"1e-10", 1.0e-10}, std::pair{"1e-4", 1.0e-4}, std::pair{"1e-2", 1.0e-2}, std::pair{"1e-1", 1.0e-1},
std::pair{"1", 1.0}
};
for (const auto &[label, floor] : floorCases) {
measureH1CalibrationFloor(
std::string("h1_aqq_floor_") + label, label, floor, problem, materialSurfaceOperator, communicator
);
}
measureScalarSurfaceControl(
"scalar_aqq_operator_calibrated_diagonal_control",
preconditioning::SurfaceRieszCalibrationObjective::operator_action, 4, problem, materialSurfaceOperator,
communicator
);
constexpr std::array inverseProbeCounts{1, 2, 4, 8, 16};
for (const int probeCount : inverseProbeCounts) {
measureScalarSurfaceControl(
"scalar_aqq_right_calibrated_diagonal_" + std::to_string(probeCount) + "_probes",
preconditioning::SurfaceRieszCalibrationObjective::right_preconditioned_action, probeCount, problem,
materialSurfaceOperator, communicator
);
}
}

View File

@@ -0,0 +1,155 @@
#pragma once
// Experiment-only closed-form benchmark. This intentionally does not use the
// production Lane-Emden integration, radial interpolation, EOS, or seed helpers.
#include <cmath>
#include <initializer_list>
#include <numbers>
#include <stdexcept>
namespace experiment::polytrope_validation {
struct AnalyticValues final {
double density{};
double enthalpy{};
double pressure{};
double potential{};
double enclosedMass{};
// Outward-positive dPhi/dr, not the inward gravitational acceleration.
double radialPotentialGradient{};
};
struct N1Reference final {
double gravitationalConstant{1.0};
double mass{1.0};
double radius{1.0};
// Validate once before using this reference in a quadrature loop.
void Validate() const {
if (!std::isfinite(gravitationalConstant) || gravitationalConstant <= 0.0
|| !std::isfinite(mass) || mass <= 0.0
|| !std::isfinite(radius) || radius <= 0.0) {
throw std::invalid_argument("The n=1 reference requires finite, positive G, M, and R.");
}
for (const double scale : {PolytropicConstant(), CentralDensity(), CentralEnthalpy(),
PressureIntegral(), -BindingEnergy(), MomentOfInertia(),
CentralEnthalpy() / radius, 0.5 * CentralEnthalpy() * CentralDensity()}) {
if (!std::isfinite(scale) || scale <= 0.0) {
throw std::invalid_argument("The n=1 reference scales are not representable as positive finite doubles.");
}
}
}
[[nodiscard]] double PolytropicConstant() const {
return 2.0 * gravitationalConstant * radius * radius / std::numbers::pi;
}
[[nodiscard]] double CentralDensity() const {
return (std::numbers::pi / 4.0) * (mass / radius) / radius / radius;
}
[[nodiscard]] double CentralEnthalpy() const {
return gravitationalConstant * mass / radius;
}
[[nodiscard]] double PressureIntegral() const {
return 0.25 * CentralEnthalpy() * mass;
}
// W = (1/2) integral rho Phi dV, with Phi tending to zero at infinity.
[[nodiscard]] double BindingEnergy() const {
return -0.75 * CentralEnthalpy() * mass;
}
// Axial moment of inertia, not integral rho r^2 dV.
[[nodiscard]] double MomentOfInertia() const {
constexpr double coefficient = (2.0 / 3.0)
* (1.0 - 6.0 / (std::numbers::pi * std::numbers::pi));
return coefficient * mass * radius * radius;
}
[[nodiscard]] double DimensionlessTheta(const double physicalRadius) const {
CheckRadius(physicalRadius);
if (physicalRadius >= radius) return 0.0;
const double fraction = physicalRadius / radius;
const double argument = std::numbers::pi * fraction;
if (argument < 0.25) {
const double squared = argument * argument;
return 1.0 + squared * (-1.0 / 6.0 + squared * (1.0 / 120.0
+ squared * (-1.0 / 5040.0 + squared * (1.0 / 362880.0
+ squared * (-1.0 / 39916800.0 + squared / 6227020800.0)))));
}
if (fraction > 0.5) {
// sin(pi-delta) avoids the nonzero floating-point sin(pi)
// floor. Form the small surface distance before dividing.
const double surfaceDistance = (radius - physicalRadius) / radius;
return std::sin(std::numbers::pi * surfaceDistance) / argument;
}
return std::sin(argument) / argument;
}
// This normalization also accepts numerical potentials: do not clamp
// its result to the stellar theta range or fit an additive constant.
[[nodiscard]] double NormalizedPotential(const double potential) const {
return -potential / CentralEnthalpy() - 1.0;
}
[[nodiscard]] AnalyticValues AtRadius(const double physicalRadius) const {
CheckRadius(physicalRadius);
const double centralEnthalpy = CentralEnthalpy();
if (physicalRadius >= radius) {
const double surfaceFraction = radius / physicalRadius;
return {
.density = 0.0,
.enthalpy = 0.0,
.pressure = 0.0,
.potential = -centralEnthalpy * surfaceFraction,
.enclosedMass = mass,
.radialPotentialGradient = (centralEnthalpy / radius) * surfaceFraction * surfaceFraction
};
}
const double fraction = physicalRadius / radius;
const double argument = std::numbers::pi * fraction;
const double theta = DimensionlessTheta(physicalRadius);
double massFraction = 0.0;
double gradientFraction = 0.0;
if (argument < 0.25) {
// sin(x)-x*cos(x) = x^3 [1/3-x^2/30+x^4/840-...].
// Evaluate g separately from m/r^2 to remain regular even
// when the representable enclosed mass underflows at r~0.
const double squared = argument * argument;
const double factor = 1.0 / 3.0 + squared * (-1.0 / 30.0
+ squared * (1.0 / 840.0 + squared * (-1.0 / 45360.0
+ squared * (1.0 / 3991680.0 - squared / 518918400.0))));
massFraction = std::numbers::pi * std::numbers::pi
* fraction * fraction * fraction * factor;
gradientFraction = std::numbers::pi * std::numbers::pi * fraction * factor;
} else {
double numerator = 0.0;
if (fraction > 0.5) {
const double delta = std::numbers::pi * ((radius - physicalRadius) / radius);
numerator = std::sin(delta) + argument * std::cos(delta);
} else {
numerator = std::sin(argument) - argument * std::cos(argument);
}
massFraction = numerator / std::numbers::pi;
gradientFraction = std::numbers::pi * numerator / (argument * argument);
}
const double centralDensity = CentralDensity();
return {
.density = centralDensity * theta,
.enthalpy = centralEnthalpy * theta,
.pressure = 0.5 * centralEnthalpy * centralDensity * theta * theta,
.potential = -centralEnthalpy * (1.0 + theta),
.enclosedMass = mass * massFraction,
.radialPotentialGradient = (centralEnthalpy / radius) * gradientFraction
};
}
private:
static void CheckRadius(const double physicalRadius) {
if (!std::isfinite(physicalRadius) || physicalRadius < 0.0) {
throw std::invalid_argument("The analytic reference radius must be finite and nonnegative.");
}
}
};
} // namespace experiment::polytrope_validation

View File

@@ -0,0 +1,221 @@
#pragma once
#include "polytrope_analytic_reference.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <numbers>
#include <string>
#include <utility>
#include <vector>
namespace experiment::polytrope_validation {
struct AnalyticSelfCheck final {
std::string name;
double observed{};
double expected{};
double scale{1.0};
double tolerance{};
bool passed{};
[[nodiscard]] double AbsoluteError() const { return std::abs(observed - expected); }
[[nodiscard]] double ScaledError() const { return AbsoluteError() / scale; }
};
struct AnalyticSelfCheckReport final {
std::vector<AnalyticSelfCheck> checks;
[[nodiscard]] bool Passed() const {
return std::all_of(checks.begin(), checks.end(), [](const AnalyticSelfCheck &check) {
return check.passed;
});
}
};
namespace analytic_detail {
struct RadialIntegrals final {
long double mass{};
long double pressure{};
long double potentialEnergy{};
long double gradientEnergy{};
long double fieldEnergy{};
long double momentOfInertia{};
};
// Independent physical radial integration: no mesh, projection, seed,
// or production quadrature implementation is involved in these checks.
inline RadialIntegrals IntegrateReference(const N1Reference &reference) {
constexpr int intervals = 8192;
constexpr long double pi = std::numbers::pi_v<long double>;
const long double spacing = static_cast<long double>(reference.radius) / intervals;
RadialIntegrals result;
for (int index = 0; index <= intervals; ++index) {
const double radius = reference.radius * (static_cast<double>(index) / intervals);
const auto values = reference.AtRadius(radius);
const long double r = radius;
const long double density = values.density;
const long double gradient = values.radialPotentialGradient;
const long double weight = (index == 0 || index == intervals) ? 1.0L
: ((index % 2 == 0) ? 2.0L : 4.0L);
const long double volumeWeight = weight * 4.0L * pi * r * r;
result.mass += volumeWeight * density;
result.pressure += volumeWeight * values.pressure;
result.potentialEnergy += 0.5L * volumeWeight * density * values.potential;
result.gradientEnergy -= volumeWeight * density * r * gradient;
result.fieldEnergy -= volumeWeight * gradient * gradient
/ (8.0L * pi * reference.gravitationalConstant);
result.momentOfInertia += (2.0L / 3.0L) * volumeWeight * density * r * r;
}
const long double factor = spacing / 3.0L;
result.mass *= factor;
result.pressure *= factor;
result.potentialEnergy *= factor;
result.gradientEnergy *= factor;
result.fieldEnergy *= factor;
result.momentOfInertia *= factor;
// The gravitational field outside the star is not zero. Its
// analytic contribution is essential to the field-energy identity.
result.fieldEnergy -= static_cast<long double>(reference.gravitationalConstant)
* reference.mass * reference.mass / (2.0L * reference.radius);
return result;
}
} // namespace analytic_detail
inline AnalyticSelfCheckReport RunAnalyticSelfChecks() {
AnalyticSelfCheckReport report;
auto check = [&](std::string name, const double observed, const double expected,
const double scale, const double tolerance) {
const bool passed = std::isfinite(observed) && std::isfinite(expected)
&& std::isfinite(scale) && scale > 0.0
&& std::abs(observed - expected) <= tolerance * scale;
report.checks.push_back({std::move(name), observed, expected, scale, tolerance, passed});
};
const std::array<std::pair<std::string, N1Reference>, 2> references{{
{"unit", {}},
{"nonunit", {.gravitationalConstant = 2.3, .mass = 3.7, .radius = 1.9}}
}};
for (const auto &[label, reference] : references) {
reference.Validate();
const double densityScale = reference.CentralDensity();
const double enthalpyScale = reference.CentralEnthalpy();
const double gradientScale = enthalpyScale / reference.radius;
const double energyScale = enthalpyScale * reference.mass;
const double inertiaScale = reference.mass * reference.radius * reference.radius;
const auto origin = reference.AtRadius(0.0);
check(label + ".origin.density", origin.density, densityScale, densityScale, 0.0);
check(label + ".origin.enthalpy", origin.enthalpy, enthalpyScale, enthalpyScale, 0.0);
check(label + ".origin.potential", origin.potential, -2.0 * enthalpyScale, enthalpyScale, 0.0);
check(label + ".origin.enclosed_mass", origin.enclosedMass, 0.0, reference.mass, 0.0);
check(label + ".origin.gradient", origin.radialPotentialGradient, 0.0, gradientScale, 0.0);
constexpr double smallFraction = 1.0e-8;
const auto nearOrigin = reference.AtRadius(reference.radius * smallFraction);
constexpr double centralSlope = std::numbers::pi * std::numbers::pi / 3.0;
check(label + ".origin.mass_cubic_coefficient",
nearOrigin.enclosedMass / (reference.mass * smallFraction * smallFraction * smallFraction),
centralSlope, centralSlope, 5.0e-15);
check(label + ".origin.gradient_linear_coefficient",
nearOrigin.radialPotentialGradient / (gradientScale * smallFraction),
centralSlope, centralSlope, 5.0e-15);
constexpr double tinyFraction = 1.0e-200;
const auto tinyRadius = reference.AtRadius(reference.radius * tinyFraction);
check(label + ".origin.gradient_without_mass_underflow_division",
tinyRadius.radialPotentialGradient / (gradientScale * tinyFraction),
centralSlope, centralSlope, 5.0e-15);
const auto surface = reference.AtRadius(reference.radius);
check(label + ".surface.density", surface.density, 0.0, densityScale, 0.0);
check(label + ".surface.enthalpy", surface.enthalpy, 0.0, enthalpyScale, 0.0);
check(label + ".surface.pressure", surface.pressure, 0.0, enthalpyScale * densityScale, 0.0);
check(label + ".surface.potential", surface.potential, -enthalpyScale, enthalpyScale, 0.0);
check(label + ".surface.enclosed_mass", surface.enclosedMass, reference.mass, reference.mass, 0.0);
check(label + ".surface.gradient", surface.radialPotentialGradient, gradientScale, gradientScale, 0.0);
const double innerRadius = std::nextafter(reference.radius, 0.0);
const double outerRadius = std::nextafter(reference.radius, std::numeric_limits<double>::infinity());
const auto justInside = reference.AtRadius(innerRadius);
const auto justOutside = reference.AtRadius(outerRadius);
check(label + ".surface.potential_join", justInside.potential, justOutside.potential, enthalpyScale, 2.0e-15);
check(label + ".surface.gradient_join", justInside.radialPotentialGradient,
justOutside.radialPotentialGradient, gradientScale, 3.0e-15);
check(label + ".surface.theta_linear_coefficient",
reference.DimensionlessTheta(innerRadius) / ((reference.radius - innerRadius) / reference.radius),
1.0, 1.0, 2.0e-15);
check(label + ".surface.positive_density_inside", justInside.density > 0.0 ? 1.0 : 0.0, 1.0, 1.0, 0.0);
const auto exterior = reference.AtRadius(2.0 * reference.radius);
check(label + ".exterior.vacuum_density", exterior.density, 0.0, densityScale, 0.0);
check(label + ".exterior.point_mass_potential", exterior.potential, -0.5 * enthalpyScale, enthalpyScale, 0.0);
check(label + ".exterior.point_mass_gradient", exterior.radialPotentialGradient, 0.25 * gradientScale, gradientScale, 0.0);
check(label + ".normalization.does_not_clip_negative_theta",
reference.NormalizedPotential(exterior.potential), -0.5, 1.0, 0.0);
check(label + ".normalization.does_not_clip_positive_potential",
reference.NormalizedPotential(enthalpyScale), -2.0, 1.0, 0.0);
int radialIndex = 0;
for (const double fraction : {0.0, 0.1, 0.25, 0.5, 0.75, 0.99, 1.0}) {
const auto values = reference.AtRadius(reference.radius * fraction);
const std::string prefix = label + ".radial_" + std::to_string(radialIndex++);
check(prefix + ".enthalpy_eos", values.enthalpy,
2.0 * reference.PolytropicConstant() * values.density, enthalpyScale, 2.0e-15);
check(prefix + ".pressure_eos", values.pressure,
reference.PolytropicConstant() * values.density * values.density,
enthalpyScale * densityScale, 2.0e-15);
check(prefix + ".hydrostatic_constant", values.enthalpy + values.potential,
-enthalpyScale, enthalpyScale, 2.0e-15);
check(prefix + ".normalized_potential", reference.NormalizedPotential(values.potential),
reference.DimensionlessTheta(reference.radius * fraction), 1.0, 2.0e-15);
}
radialIndex = 0;
for (const double fraction : {0.1, 0.25, 0.5, 0.75, 0.9}) {
const double radius = reference.radius * fraction;
const double spacing = 1.0e-4 * reference.radius;
const auto minusTwo = reference.AtRadius(radius - 2.0 * spacing);
const auto minusOne = reference.AtRadius(radius - spacing);
const auto plusOne = reference.AtRadius(radius + spacing);
const auto plusTwo = reference.AtRadius(radius + 2.0 * spacing);
const double enthalpyDerivative = (minusTwo.enthalpy - 8.0 * minusOne.enthalpy
+ 8.0 * plusOne.enthalpy - plusTwo.enthalpy) / (12.0 * spacing);
const double massDerivative = (minusTwo.enclosedMass - 8.0 * minusOne.enclosedMass
+ 8.0 * plusOne.enclosedMass - plusTwo.enclosedMass) / (12.0 * spacing);
const auto values = reference.AtRadius(radius);
const std::string prefix = label + ".derivative_" + std::to_string(radialIndex++);
check(prefix + ".hydrostatic_balance", enthalpyDerivative + values.radialPotentialGradient,
0.0, gradientScale, 5.0e-11);
check(prefix + ".enclosed_mass", massDerivative,
4.0 * std::numbers::pi * radius * radius * values.density,
reference.mass / reference.radius, 5.0e-11);
}
const auto integrals = analytic_detail::IntegrateReference(reference);
check(label + ".integral.mass", static_cast<double>(integrals.mass), reference.mass, reference.mass, 2.0e-12);
check(label + ".integral.pressure", static_cast<double>(integrals.pressure),
reference.PressureIntegral(), energyScale, 2.0e-12);
check(label + ".integral.binding_from_potential", static_cast<double>(integrals.potentialEnergy),
reference.BindingEnergy(), energyScale, 2.0e-12);
check(label + ".integral.binding_from_gradient", static_cast<double>(integrals.gradientEnergy),
reference.BindingEnergy(), energyScale, 2.0e-12);
check(label + ".integral.binding_from_field_with_exterior", static_cast<double>(integrals.fieldEnergy),
reference.BindingEnergy(), energyScale, 2.0e-12);
check(label + ".integral.binding_potential_vs_gradient", static_cast<double>(integrals.potentialEnergy),
static_cast<double>(integrals.gradientEnergy), energyScale, 2.0e-12);
check(label + ".integral.scalar_virial_nonrotating",
static_cast<double>(integrals.gradientEnergy + 3.0L * integrals.pressure),
0.0, energyScale, 2.0e-12);
check(label + ".integral.axial_moment_of_inertia", static_cast<double>(integrals.momentOfInertia),
reference.MomentOfInertia(), inertiaScale, 2.0e-12);
}
bool rejectedNegativeRadius = false;
try { (void)N1Reference{}.AtRadius(-1.0); }
catch (const std::invalid_argument &) { rejectedNegativeRadius = true; }
check("contract.negative_radius_rejected", rejectedNegativeRadius ? 1.0 : 0.0, 1.0, 1.0, 0.0);
bool rejectedInvalidScale = false;
try { N1Reference{.gravitationalConstant = -1.0}.Validate(); }
catch (const std::invalid_argument &) { rejectedInvalidScale = true; }
check("contract.invalid_reference_rejected", rejectedInvalidScale ? 1.0 : 0.0, 1.0, 1.0, 0.0);
return report;
}
} // namespace experiment::polytrope_validation

View File

@@ -0,0 +1,395 @@
#pragma once
// Include after `import mean_field;`. This observer reconstructs the accepted
// coefficients without mutating the prepared production operator or mesh.
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <limits>
#include <map>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
namespace experiment::polytrope_validation {
struct PhysicalPoint final {
mean_field::mapping::MappingPointContext mapping;
int element{-1};
int attribute{-1};
bool stellarMaterial{false};
double rho{std::numeric_limits<double>::quiet_NaN()};
double h{std::numeric_limits<double>::quiet_NaN()};
double phi{std::numeric_limits<double>::quiet_NaN()};
// Positive centrifugal potential Psi = |Omega cross (x-center)|^2 / 2.
// The pointwise Bernoulli balance is h + phi - Psi - C = 0.
double rotationPotential{std::numeric_limits<double>::quiet_NaN()};
mfem::Vector gravityGradientPhysical;
mfem::Vector enthalpyGradientPhysical;
mfem::Vector potentialGradientPhysical;
PhysicalPoint()
: gravityGradientPhysical(3), enthalpyGradientPhysical(3), potentialGradientPhysical(3) {
const double nan = std::numeric_limits<double>::quiet_NaN();
gravityGradientPhysical = nan;
enthalpyGradientPhysical = nan;
potentialGradientPhysical = nan;
}
};
struct FieldReconstructionReport final {
std::string field;
int reducedSize{0};
int fullTrueSize{0};
double maximumRoundTripError{0.0};
};
// Nonmovable because the mapping evaluator references the owned displacement
// grid function. The FEM, spaces, compactification field, and mapper remain
// borrowed: keep this object inside the diagnostic context callback.
class PhysicalState final {
public:
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
template <typename State>
explicit PhysicalState(State& state, const mean_field::fem::FEM& fem)
: finiteElements(fem),
density(RequireSpace(fem.densityFes)),
enthalpy(RequireSpace(fem.enthalpyFes)),
potential(RequireSpace(fem.gravityPotentialFes)),
gravityGradientReference(RequireSpace(fem.gravityFluxFes)),
displacement(RequireSpace(fem.displacementFes)),
domainMapper(state.Problem().GetDiscretization().domainMapper()),
mapping(domainMapper, displacement, RequireCompactification(fem)),
rotation(state.Problem().GetPreparedOperator().GetRotation()) {
int ranks = 0;
MPI_Comm_size(fem.mesh->GetComm(), &ranks);
if (ranks != 1) {
throw std::invalid_argument("Physical verification currently requires exactly one MPI rank.");
}
const auto& problem = state.Problem();
const auto& accepted = state.AcceptedPhysicalState();
const auto& prepared = state.normalizedOperator->GetPhysicalState();
if (accepted.Size() != prepared.Size()) {
throw std::logic_error("Accepted and prepared physical state sizes differ.");
}
for (int index = 0; index < accepted.Size(); ++index) {
if (!std::isfinite(accepted(index)) || accepted(index) != prepared(index)) {
throw std::logic_error("Physical verification requires the accepted state to be prepared.");
}
}
const auto values = problem.GetManifest().valueBlocks();
ScatterField<mean_field::field::Density>("density", values, accepted, density);
ScatterField<mean_field::field::Enthalpy>("specific_enthalpy", values, accepted, enthalpy);
ScatterField<mean_field::field::Gravity>("gravity_potential", values, accepted, potential);
ScatterField<mean_field::field::Gravity>("gravity_gradient", values, accepted, gravityGradientReference);
displacement.SetFromTrueDofs(problem.GetPhysicalOperator().GetGeneratedVolumeDisplacement());
mapping.InvalidateCache();
bernoulliConstant = Scalar(values, accepted, "fixed_total_mass.multiplier");
physicalResidualBordered = state.normalizedOperator->GetPhysicalResidual();
physicalResidualUnbordered = physicalResidualBordered;
normalizedBorderedResidual = state.acceptedNormalizedResidual;
normalizedUnborderedResidual.SetSize(physicalResidualUnbordered.Size());
if constexpr (requires { problem.GetPreparedOperator().GetCentralDensityConstraint(); }) {
centralBorder = Scalar(values, accepted, "fixed_central_density.border");
const auto& central = problem.GetPreparedOperator().GetCentralDensityConstraint();
centralDensityReport = central.GetConstraintReport();
const auto hydrostatic = RequireBlock(problem.GetManifest().residualBlocks(), "hydrostatic_balance");
if (central.GetCenterDof().field_size() != hydrostatic.size) {
throw std::logic_error("Central-density border and hydrostatic row layouts disagree.");
}
for (const int reducedDof : central.GetCenterDof().reduced_dofs()) {
if (reducedDof < 0 || reducedDof >= hydrostatic.size) {
throw std::logic_error("Central-density border index is outside the hydrostatic block.");
}
const int row = hydrostatic.offset + reducedDof;
centralHydrostaticRows.push_back(row);
physicalResidualUnbordered(row) -= centralBorder;
}
}
state.normalizedOperator->NormalizeResidual(physicalResidualUnbordered, normalizedUnborderedResidual);
normalizedBorderedResidualNorm = normalizedBorderedResidual.Norml2();
normalizedUnborderedResidualNorm = normalizedUnborderedResidual.Norml2();
mfem::Vector normalizedBorder(normalizedBorderedResidual);
normalizedBorder -= normalizedUnborderedResidual;
normalizedCentralBorderActionNorm = normalizedBorder.Norml2();
}
// Experiment-only postprocessing replay, not a solver restart. The
// caller must construct fem from savedDirectory/input.smesh. Historical
// residuals are not recomputed: only the saved finite-element fields
// are loaded, using exactly the same Evaluate implementation below.
explicit PhysicalState(
const mean_field::fem::FEM& fem, const std::filesystem::path& savedDirectory
)
: finiteElements(fem),
density(RequireSpace(fem.densityFes)),
enthalpy(RequireSpace(fem.enthalpyFes)),
potential(RequireSpace(fem.gravityPotentialFes)),
gravityGradientReference(RequireSpace(fem.gravityFluxFes)),
displacement(RequireSpace(fem.displacementFes)),
domainMapper(RequireDomainMapper(fem)),
mapping(domainMapper, displacement, RequireCompactification(fem)),
rotation(RequireNonrotatingReplay(fem, savedDirectory)) {
LoadField(savedDirectory / "density.gf", density);
LoadField(savedDirectory / "enthalpy.gf", enthalpy);
LoadField(savedDirectory / "potential.gf", potential);
LoadField(savedDirectory / "gravity_gradient_reference.gf", gravityGradientReference);
LoadField(savedDirectory / "displacement.gf", displacement);
mapping.InvalidateCache();
centralBorder = std::numeric_limits<double>::quiet_NaN();
normalizedCentralBorderActionNorm = std::numeric_limits<double>::quiet_NaN();
}
PhysicalState(const PhysicalState&) = delete;
PhysicalState& operator=(const PhysicalState&) = delete;
PhysicalState(PhysicalState&&) = delete;
PhysicalState& operator=(PhysicalState&&) = delete;
[[nodiscard]] bool isStellar(int element) const {
CheckElement(element);
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Stellar>(
finiteElements.mesh->GetAttribute(element)
);
}
[[nodiscard]] bool isVacuum(int element) const {
CheckElement(element);
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(
finiteElements.mesh->GetAttribute(element)
);
}
// Grid-function vector values include the MFEM/reference-mesh Piola
// transform already. Applying J_map/det(J_map) here completes, rather
// than repeats, the transformation to the deformed physical geometry.
[[nodiscard]] mean_field::mapping::MappingStatus Evaluate(
int element, const mfem::IntegrationPoint& point, PhysicalPoint& output
) {
using mean_field::mapping::MappingStatus;
CheckElement(element);
output.element = element;
output.attribute = finiteElements.mesh->GetAttribute(element);
output.stellarMaterial = isStellar(element);
const double nan = std::numeric_limits<double>::quiet_NaN();
output.rho = output.h = output.phi = output.rotationPotential = nan;
output.gravityGradientPhysical = nan;
output.enthalpyGradientPhysical = nan;
output.potentialGradientPhysical = nan;
auto* transformation = finiteElements.mesh->GetElementTransformation(element);
const auto status = mapping.EvaluatePoint(*transformation, point, output.mapping);
if (status != MappingStatus::valid) return status;
// GetValue/GetGradient evaluate the FE functions on the reference
// physical mesh. Scalar values pull back unchanged under DomainMapper.
output.phi = potential.GetValue(element, point);
transformation->SetIntPoint(&point);
potential.GetGradient(*transformation, m_referenceGradient);
output.mapping.inverse_mapping_jacobian.MultTranspose(m_referenceGradient, output.potentialGradientPhysical);
gravityGradientReference.GetVectorValue(element, point, m_referenceGravity);
output.mapping.mapping_jacobian.Mult(m_referenceGravity, output.gravityGradientPhysical);
output.gravityGradientPhysical /= output.mapping.mapping_determinant;
output.rotationPotential = rotation.potential(output.mapping.physical_position);
if (output.stellarMaterial) {
output.rho = density.GetValue(element, point);
output.h = enthalpy.GetValue(element, point);
transformation->SetIntPoint(&point);
enthalpy.GetGradient(*transformation, m_referenceGradient);
output.mapping.inverse_mapping_jacobian.MultTranspose(m_referenceGradient, output.enthalpyGradientPhysical);
}
// rho/h outside their material support intentionally remain NaN;
// zeroed unsupported FE coefficients are not physical vacuum data.
if (!std::isfinite(output.phi) || !std::isfinite(output.rotationPotential) ||
!AllFinite(output.gravityGradientPhysical) || !AllFinite(output.potentialGradientPhysical) ||
(output.stellarMaterial && (!std::isfinite(output.rho) || !std::isfinite(output.h) ||
!AllFinite(output.enthalpyGradientPhysical)))) {
return MappingStatus::non_finite_result;
}
return MappingStatus::valid;
}
const mean_field::fem::FEM& finiteElements;
mfem::ParGridFunction density;
mfem::ParGridFunction enthalpy;
mfem::ParGridFunction potential;
mfem::ParGridFunction gravityGradientReference;
mfem::ParGridFunction displacement;
const mean_field::mapping::DomainMapper& domainMapper;
mean_field::mapping::GridFunctionMappingEvaluator mapping;
mean_field::physics::RigidRotation rotation;
double bernoulliConstant{std::numeric_limits<double>::quiet_NaN()};
double centralBorder{0.0};
std::optional<mean_field::operators::CentralDensityConstraintReport> centralDensityReport;
std::vector<int> centralHydrostaticRows;
std::vector<FieldReconstructionReport> reconstructionReports;
// "Unbordered" removes only the artificial lambda_c*e_c contribution
// from hydrostatic balance. All scalar constraint rows and the physical
// Bernoulli constant remain present in the original root layout.
mfem::Vector physicalResidualBordered;
mfem::Vector physicalResidualUnbordered;
mfem::Vector normalizedBorderedResidual;
mfem::Vector normalizedUnborderedResidual;
double normalizedBorderedResidualNorm{std::numeric_limits<double>::quiet_NaN()};
double normalizedUnborderedResidualNorm{std::numeric_limits<double>::quiet_NaN()};
double normalizedCentralBorderActionNorm{0.0};
private:
struct Block final { int offset; int size; };
mfem::Vector m_referenceGradient = mfem::Vector(3);
mfem::Vector m_referenceGravity = mfem::Vector(3);
static mfem::ParFiniteElementSpace* RequireSpace(
const std::unique_ptr<mfem::ParFiniteElementSpace>& space
) {
if (space == nullptr) throw std::invalid_argument("Physical verification received an incomplete FE space.");
return space.get();
}
static const mfem::ParGridFunction& RequireCompactification(const mean_field::fem::FEM& fem) {
if (fem.mesh == nullptr || fem.compactificationCoordinate == nullptr) {
throw std::invalid_argument("Physical verification received incomplete mapping data.");
}
return *fem.compactificationCoordinate;
}
static const mean_field::mapping::DomainMapper& RequireDomainMapper(const mean_field::fem::FEM& fem) {
if (fem.domainMapperStateless == nullptr) {
throw std::invalid_argument("Physical replay received an incomplete domain mapper.");
}
return *fem.domainMapperStateless;
}
static mean_field::physics::RigidRotation RequireNonrotatingReplay(
const mean_field::fem::FEM& fem, const std::filesystem::path& directory
) {
int ranks = 0;
MPI_Comm_size(fem.mesh->GetComm(), &ranks);
if (ranks != 1) throw std::invalid_argument("Physical replay requires exactly one MPI rank.");
std::ifstream metadata(directory / "metadata.txt");
if (!metadata) throw std::invalid_argument("Physical replay requires saved metadata.txt.");
std::map<std::string, std::string> values;
std::string line;
while (std::getline(metadata, line)) {
const auto separator = line.find('=');
if (separator != std::string::npos) values[line.substr(0, separator)] = line.substr(separator + 1);
}
constexpr std::string_view supportedModel =
"nonrotating_n1_fixed_mass_fixed_central_density_zero_surface_pressure";
if (metadata.bad() || values["mode"] != "solve" || values["mpi_ranks"] != "1" ||
values["model"] != supportedModel) {
throw std::invalid_argument("Physical replay only supports saved single-rank nonrotating n=1 solves.");
}
if (!values.contains("elements") || std::stoi(values.at("elements")) != fem.mesh->GetNE()) {
throw std::invalid_argument("Replay FEM element count disagrees with saved metadata.");
}
// The supported model has exactly J=0 and computes Omega=0. If a
// completed measurement file exists, reject contradictory data;
// an interrupted postprocess can still replay its already-saved GFs.
std::ifstream metrics(directory / "physical_metrics.csv");
if (metrics) {
while (std::getline(metrics, line)) {
constexpr std::string_view prefix = "angular_velocity_norm,";
if (!line.starts_with(prefix)) continue;
const double angularVelocity = std::stod(line.substr(prefix.size()));
if (!std::isfinite(angularVelocity) || angularVelocity != 0.0) {
throw std::invalid_argument("Physical replay cannot infer a nonzero saved rotation vector.");
}
}
if (metrics.bad()) throw std::runtime_error("Could not read saved rotation verification data.");
}
mfem::Vector zero(3);
zero = 0.0;
return mean_field::physics::RigidRotation{zero, zero};
}
static void LoadField(const std::filesystem::path& path, mfem::ParGridFunction& target) {
std::ifstream stream(path);
if (!stream) throw std::invalid_argument("Missing saved physical field: " + path.string());
// ParGridFunction's stream constructor reverses the local-DOF
// orientation handling in ParGridFunction::Save (important for RT).
mfem::ParGridFunction loaded(target.ParFESpace()->GetParMesh(), stream);
if (stream.fail() || !AllFinite(loaded)) {
throw std::invalid_argument("Incomplete or non-finite saved physical field: " + path.string());
}
const auto& savedSpace = *loaded.ParFESpace();
const auto& targetSpace = *target.ParFESpace();
if (std::string_view(savedSpace.FEColl()->Name()) != targetSpace.FEColl()->Name() ||
savedSpace.GetVDim() != targetSpace.GetVDim() || savedSpace.GetOrdering() != targetSpace.GetOrdering() ||
savedSpace.GetVSize() != targetSpace.GetVSize() || savedSpace.GetTrueVSize() != targetSpace.GetTrueVSize() ||
loaded.Size() != target.Size()) {
throw std::invalid_argument("Saved field FE collection/layout is incompatible with this build: " + path.string());
}
mfem::Array<int> savedDofs, targetDofs;
for (int element = 0; element < targetSpace.GetNE(); ++element) {
if (savedSpace.GetElementOrder(element) != targetSpace.GetElementOrder(element)) {
throw std::invalid_argument("Saved field element order is incompatible with this build: " + path.string());
}
savedSpace.GetElementVDofs(element, savedDofs);
targetSpace.GetElementVDofs(element, targetDofs);
if (savedDofs.Size() != targetDofs.Size()) {
throw std::invalid_argument("Saved field element DOF count is incompatible with this build: " + path.string());
}
for (int index = 0; index < targetDofs.Size(); ++index) {
if (savedDofs[index] != targetDofs[index]) {
throw std::invalid_argument("Saved field element DOF ordering is incompatible with this build: " + path.string());
}
}
}
target = loaded;
}
void CheckElement(int element) const {
if (element < 0 || element >= finiteElements.mesh->GetNE()) {
throw std::out_of_range("Physical verification element index is outside the local mesh.");
}
}
template <typename Blocks>
static Block RequireBlock(const Blocks& blocks, std::string_view name) {
for (const auto& block : blocks) {
if (block.stableId == name) return {block.offset, block.size};
}
throw std::invalid_argument("Physical verification requires root block " + std::string(name));
}
template <typename Blocks>
static double Scalar(const Blocks& blocks, const mfem::Vector& values, std::string_view name) {
const auto block = RequireBlock(blocks, name);
if (block.size != 1 || block.offset < 0 || block.offset >= values.Size()) {
throw std::logic_error("Physical verification scalar block has an incompatible layout.");
}
return values(block.offset);
}
template <typename Field, typename Blocks>
void ScatterField(std::string_view name, const Blocks& blocks, const mfem::Vector& values, mfem::ParGridFunction& field) {
const auto block = RequireBlock(blocks, name);
const auto adapter = mean_field::field::make_field_dof_grid_function_adapter<Field, DomainSchema>(*field.ParFESpace());
if (block.size != adapter.dof_map().reduced_size() || block.offset < 0 ||
block.offset + block.size > values.Size()) {
throw std::logic_error("Physical verification field map disagrees with root block " + std::string(name));
}
mfem::Vector reduced(block.size);
for (int index = 0; index < block.size; ++index) reduced(index) = values(block.offset + index);
adapter.scatter(reduced, field);
const auto roundTrip = adapter.gather(field);
double error = 0.0;
for (int index = 0; index < block.size; ++index) error = std::max(error, std::abs(roundTrip(index) - reduced(index)));
reconstructionReports.push_back({std::string(name), block.size, adapter.dof_map().full_size(), error});
}
static bool AllFinite(const mfem::Vector& vector) {
for (int index = 0; index < vector.Size(); ++index) if (!std::isfinite(vector(index))) return false;
return true;
}
};
} // namespace experiment::polytrope_validation

View File

@@ -0,0 +1,435 @@
#pragma once
// Experiment-only direct physical-space sampling. Include after mean_field.
#include "polytrope_analytic_reference.hpp"
#include "polytrope_physical_state.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <limits>
#include <numbers>
#include <numeric>
#include <stdexcept>
#include <string>
#include <vector>
namespace experiment::polytrope_validation {
struct RadialProfileOptions final {
int interiorShellCount{32};
int exteriorShellCount{8};
int muPointCount{6};
int azimuthPointCount{12};
double exteriorRadiusMultiple{2.0};
double relativeLocationTolerance{2.0e-11};
int maximumNewtonIterations{35};
};
struct RadialProfileReport final {
std::size_t requestedPoints{0};
std::size_t locatedPoints{0};
std::size_t materialPoints{0};
std::size_t locatorElementAttempts{0};
double maximumLocationError{0.0};
double maximumScaledFieldError{0.0};
double maximumAngularRmsScaled{0.0};
double angularMomentError{0.0};
};
namespace radial_detail {
constexpr double NaN = std::numeric_limits<double>::quiet_NaN();
struct Direction final {
std::array<double, 3> value{};
double weight{0.0};
std::string family;
};
inline std::vector<Direction> AngularDirections(const RadialProfileOptions &options) {
std::vector<Direction> directions;
for (int root = 0; root < options.muPointCount; ++root) {
double mu = std::cos(std::numbers::pi * (root + 0.75) / (options.muPointCount + 0.5));
double derivative = 0.0;
for (int iteration = 0; iteration < 32; ++iteration) {
double previous = 1.0;
double current = mu;
for (int degree = 2; degree <= options.muPointCount; ++degree) {
const double next = ((2 * degree - 1) * mu * current - (degree - 1) * previous) / degree;
previous = current;
current = next;
}
derivative = options.muPointCount * (mu * current - previous) / (mu * mu - 1.0);
const double correction = current / derivative;
mu -= correction;
if (std::abs(correction) < 4.0 * std::numeric_limits<double>::epsilon()) break;
}
// The Newton update changes mu after evaluating its derivative.
// Form the quadrature weight at the final root, not that iterate.
double previous = 1.0;
double current = mu;
for (int degree = 2; degree <= options.muPointCount; ++degree) {
const double next = ((2 * degree - 1) * mu * current - (degree - 1) * previous) / degree;
previous = current;
current = next;
}
derivative = options.muPointCount * (mu * current - previous) / (mu * mu - 1.0);
// Half of the [-1,1] Gauss weight: angular weights sum to one.
const double weight = 1.0 / ((1.0 - mu * mu) * derivative * derivative * options.azimuthPointCount);
const double cylindricalRadius = std::sqrt(std::max(0.0, 1.0 - mu * mu));
for (int azimuth = 0; azimuth < options.azimuthPointCount; ++azimuth) {
const double phi = 2.0 * std::numbers::pi * (azimuth + 0.5) / options.azimuthPointCount;
directions.push_back({{cylindricalRadius * std::cos(phi), cylindricalRadius * std::sin(phi), mu}, weight, "angular"});
}
}
return directions;
}
inline std::vector<Direction> Rays() {
std::vector<Direction> directions;
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
for (int z = -1; z <= 1; ++z) {
const int nonzero = (x != 0) + (y != 0) + (z != 0);
if (nonzero == 0) continue;
const double norm = std::sqrt(static_cast<double>(nonzero));
directions.push_back({{x / norm, y / norm, z / norm}, 0.0,
nonzero == 1 ? "axis" : nonzero == 2 ? "face_diagonal" : "body_diagonal"});
}
}
}
return directions;
}
struct LocatedPoint final {
bool found{false};
double error{NaN};
PhysicalPoint physical;
};
// Bounds prioritize searches; they NEVER exclude an element. Sampled
// bounds are not a certificate for curved/Kelvin element images.
template <typename SampleState> class PhysicalLocator final {
public:
PhysicalLocator(SampleState &state, const double radius, const RadialProfileOptions &options)
: m_state(state), m_radius(radius), m_options(options) {
auto &mesh = *state.finiteElements.mesh;
m_elements.resize(static_cast<std::size_t>(mesh.GetNE()));
constexpr std::array<double, 5> coordinates{0.0, 0.125, 0.5, 0.875, 1.0};
for (int element = 0; element < mesh.GetNE(); ++element) {
auto *transformation = mesh.GetElementTransformation(element);
if (transformation->GetGeometryType() != mfem::Geometry::CUBE) {
throw std::invalid_argument("Direct radial profiles currently require hexahedral elements.");
}
auto &data = m_elements[element];
for (const double x : coordinates) for (const double y : coordinates) for (const double z : coordinates) {
Seed seed;
seed.point.Set3(x, y, z);
mean_field::mapping::MappingPointContext mapped;
if (state.mapping.EvaluatePoint(*transformation, seed.point, mapped) != mean_field::mapping::MappingStatus::valid) continue;
for (int d = 0; d < 3; ++d) {
seed.position[d] = mapped.physical_position(d);
data.minimum[d] = std::min(data.minimum[d], seed.position[d]);
data.maximum[d] = std::max(data.maximum[d], seed.position[d]);
}
data.seeds.push_back(seed);
}
}
}
LocatedPoint Locate(const mfem::Vector &target, int &hint) {
LocatedPoint result;
if (hint >= 0 && TryElement(hint, target, result)) return result;
std::vector<std::pair<double, int>> candidates;
candidates.reserve(m_elements.size());
for (int element = 0; element < static_cast<int>(m_elements.size()); ++element) {
if (element == hint || m_elements[element].seeds.empty()) continue;
const auto &data = m_elements[element];
double distance = 0.0;
double centerDistance = 0.0;
for (int d = 0; d < 3; ++d) {
const double outside = std::max({data.minimum[d] - target(d), target(d) - data.maximum[d], 0.0});
distance += outside * outside;
const double centered = target(d) - 0.5 * (data.minimum[d] + data.maximum[d]);
centerDistance += centered * centered;
}
candidates.emplace_back(distance + 1.0e-8 * centerDistance, element);
}
std::sort(candidates.begin(), candidates.end());
for (const auto &[distance, element] : candidates) {
if (TryElement(element, target, result)) {
hint = element;
return result;
}
}
hint = -1;
return result;
}
std::size_t ElementAttempts() const { return m_elementAttempts; }
private:
struct Seed final {
mfem::IntegrationPoint point;
std::array<double, 3> position{};
};
struct Element final {
std::array<double, 3> minimum{INFINITY, INFINITY, INFINITY};
std::array<double, 3> maximum{-INFINITY, -INFINITY, -INFINITY};
std::vector<Seed> seeds;
};
bool TryElement(const int element, const mfem::Vector &target, LocatedPoint &result) {
++m_elementAttempts;
const auto &seeds = m_elements[element].seeds;
if (seeds.empty()) return false;
const Seed *closest = &seeds.front();
double bestDistance = std::numeric_limits<double>::infinity();
for (const auto &seed : seeds) {
double distance = 0.0;
for (int d = 0; d < 3; ++d) distance += std::pow(target(d) - seed.position[d], 2);
if (distance < bestDistance) { bestDistance = distance; closest = &seed; }
}
if (Newton(element, closest->point, target, result)) return true;
mfem::IntegrationPoint center;
center.Set3(0.5, 0.5, 0.5);
return Newton(element, center, target, result);
}
bool Newton(const int element, mfem::IntegrationPoint point, const mfem::Vector &target, LocatedPoint &result) {
auto *transformation = m_state.finiteElements.mesh->GetElementTransformation(element);
const double tolerance = m_options.relativeLocationTolerance * std::max(m_radius, target.Norml2());
mfem::Vector residual(3), correction(3), trialResidual(3);
mfem::DenseMatrix totalJacobian(3), inverse(3);
mean_field::mapping::MappingPointContext mapped, trialMapped;
for (int iteration = 0; iteration < m_options.maximumNewtonIterations; ++iteration) {
if (m_state.mapping.EvaluatePoint(*transformation, point, mapped) != mean_field::mapping::MappingStatus::valid) return false;
residual = mapped.physical_position;
residual -= target;
const double error = residual.Norml2();
if (error <= tolerance) {
if (m_state.Evaluate(element, point, result.physical) != mean_field::mapping::MappingStatus::valid) return false;
result.found = true;
result.error = error;
return true;
}
transformation->SetIntPoint(&point);
mfem::Mult(mapped.mapping_jacobian, transformation->Jacobian(), totalJacobian);
if (!std::isfinite(totalJacobian.Det()) || totalJacobian.Det() <= 0.0) return false;
mfem::CalcInverse(totalJacobian, inverse);
inverse.Mult(residual, correction);
bool improved = false;
double alpha = 1.0;
for (int trial = 0; trial < 18; ++trial, alpha *= 0.5) {
mfem::IntegrationPoint candidate;
candidate.Set3(std::clamp(point.x - alpha * correction(0), 0.0, 1.0),
std::clamp(point.y - alpha * correction(1), 0.0, 1.0),
std::clamp(point.z - alpha * correction(2), 0.0, 1.0));
if (m_state.mapping.EvaluatePoint(*transformation, candidate, trialMapped) != mean_field::mapping::MappingStatus::valid) continue;
trialResidual = trialMapped.physical_position;
trialResidual -= target;
const double trialError = trialResidual.Norml2();
if (trialError <= tolerance) {
if (m_state.Evaluate(element, candidate, result.physical) != mean_field::mapping::MappingStatus::valid) return false;
result.found = true;
result.error = trialError;
return true;
}
if (trialError < error * (1.0 - 1.0e-4 * alpha)) {
point = candidate;
improved = true;
break;
}
}
if (!improved) return false;
}
return false;
}
SampleState &m_state;
double m_radius;
const RadialProfileOptions &m_options;
std::vector<Element> m_elements;
std::size_t m_elementAttempts{0};
};
inline std::array<double, 5> Values(const LocatedPoint &point, const Direction &direction, const bool origin) {
if (!point.found) return {NaN, NaN, NaN, NaN, NaN};
const auto &physical = point.physical;
double radial = 0.0;
double transverseSquared = 0.0;
for (int d = 0; d < 3; ++d) radial += physical.gravityGradientPhysical(d) * direction.value[d];
for (int d = 0; d < 3; ++d) {
const double transverse = physical.gravityGradientPhysical(d) - radial * direction.value[d];
transverseSquared += transverse * transverse;
}
return {physical.stellarMaterial ? physical.rho : NaN, physical.stellarMaterial ? physical.h : NaN,
physical.phi, origin ? NaN : radial, std::sqrt(transverseSquared)};
}
struct Moments final {
long double weight{0.0L};
long double mean{0.0L};
long double centeredSquared{0.0L};
void Add(const double value, const double addedWeight) {
if (!std::isfinite(value)) return;
// Starting from mean=0 and multiplying/dividing by the first
// weight injects O(epsilon) variance into a constant field.
if (weight == 0.0L) {
weight = addedWeight;
mean = value;
centeredSquared = 0.0L;
return;
}
const long double difference = static_cast<long double>(value) - mean;
weight += addedWeight;
mean += static_cast<long double>(addedWeight) * difference / weight;
centeredSquared += static_cast<long double>(addedWeight) * difference * (static_cast<long double>(value) - mean);
}
double Mean() const { return weight > 0.0L ? static_cast<double>(mean) : NaN; }
double Variance() const { return weight > 0.0L ? static_cast<double>(std::max(0.0L, centeredSquared / weight)) : NaN; }
};
inline std::ofstream Csv(const std::filesystem::path &path) {
std::ofstream stream(path);
if (!stream) throw std::runtime_error("Cannot write radial diagnostic output: " + path.string());
stream << std::setprecision(17);
return stream;
}
} // namespace radial_detail
// Means are conditional on successfully located finite values. Density and
// enthalpy are additionally conditional on stellar material; their coverage
// columns must be inspected. No exterior/missing value is replaced by zero.
// Axis/diagonal DG samples may be one-sided element-interface traces. They
// diagnose directional structure, and are NOT used as angular quadrature.
template <typename SampleState> inline RadialProfileReport WriteRadialProfiles(
SampleState &state, const N1Reference &reference, const std::filesystem::path &outputDirectory,
const RadialProfileOptions &options = {}
) {
using namespace radial_detail;
reference.Validate();
if (options.interiorShellCount < 1 || options.exteriorShellCount < 1 || options.muPointCount < 2 ||
options.azimuthPointCount < 4 || options.maximumNewtonIterations < 1 ||
!std::isfinite(options.exteriorRadiusMultiple) || options.exteriorRadiusMultiple <= 1.001 ||
!std::isfinite(options.relativeLocationTolerance) || options.relativeLocationTolerance <= 0.0) {
throw std::invalid_argument("Invalid direct radial-profile sampling options.");
}
int ranks = 0;
MPI_Comm_size(state.finiteElements.mesh->GetComm(), &ranks);
if (ranks != 1) throw std::invalid_argument("Direct physical radial profiles require one MPI rank.");
std::filesystem::create_directories(outputDirectory);
auto radial = Csv(outputDirectory / "radial_profiles.csv");
auto directional = Csv(outputDirectory / "directional_profiles.csv");
const std::array<std::string, 5> names{"density_material", "enthalpy_material", "potential", "gravity_radial", "gravity_nonradial_magnitude"};
radial << "sample_kind,radius,xi,r_over_R,requested_points,located_points,located_weight_fraction,material_weight_fraction,max_location_error";
for (const auto &name : names) radial << ',' << name << "_mean," << name << "_angular_rms," << name << "_analytic," << name << "_mean_error_scaled," << name << "_rms_error_scaled," << name << "_valid_weight_fraction";
radial << ",theta_density,theta_enthalpy,theta_potential,mu_points,phi_points\n";
directional << "radius,xi,family,ray,ux,uy,uz,located,element,attribute,stellar_material,location_error";
for (const auto &name : names) directional << ',' << name << ',' << name << "_analytic";
directional << ",theta_density,theta_enthalpy,theta_potential\n";
const auto angularDirections = AngularDirections(options);
const auto rays = Rays();
RadialProfileReport report;
double weightSum = 0.0;
std::array<double, 3> first{}, second{};
for (const auto &direction : angularDirections) {
weightSum += direction.weight;
for (int d = 0; d < 3; ++d) {
first[d] += direction.weight * direction.value[d];
second[d] += direction.weight * direction.value[d] * direction.value[d];
}
}
report.angularMomentError = std::abs(weightSum - 1.0);
for (int d = 0; d < 3; ++d) report.angularMomentError = std::max({report.angularMomentError, std::abs(first[d]), std::abs(second[d] - 1.0 / 3.0)});
if (report.angularMomentError > 1.0e-12) throw std::runtime_error("Spherical angular quadrature moment self-check failed.");
Moments constantField;
constexpr double constantValue = -1.873;
for (const auto &direction : angularDirections) constantField.Add(constantValue, direction.weight);
if (constantField.Mean() != constantValue || constantField.Variance() != 0.0) {
throw std::runtime_error("Constant-field weighted angular variance self-check failed.");
}
std::vector<double> radii{0.0};
for (int i = 1; i <= options.interiorShellCount; ++i) radii.push_back(0.99 * reference.radius * i / options.interiorShellCount);
radii.push_back(0.999 * reference.radius);
for (int i = 0; i < options.exteriorShellCount; ++i) {
const double fraction = options.exteriorShellCount > 1 ? static_cast<double>(i) / (options.exteriorShellCount - 1) : 0.0;
radii.push_back(reference.radius * (1.001 + fraction * (options.exteriorRadiusMultiple - 1.001)));
}
const double gravityScale = reference.gravitationalConstant * reference.mass / (reference.radius * reference.radius);
const std::array<double, 5> scales{reference.CentralDensity(), reference.CentralEnthalpy(), reference.CentralEnthalpy(), gravityScale, gravityScale};
PhysicalLocator locator(state, reference.radius, options);
std::vector<int> angularHints(angularDirections.size(), -1), rayHints(rays.size(), -1);
int originHint = -1;
for (const double radius : radii) {
const bool origin = radius == 0.0;
const auto analytic = reference.AtRadius(radius);
const std::array<double, 5> exact{analytic.density, analytic.enthalpy, analytic.potential, analytic.radialPotentialGradient, 0.0};
std::array<Moments, 5> moments;
std::size_t locatedCount = 0;
double locatedWeight = 0.0, materialWeight = 0.0, maxError = 0.0;
const std::size_t angularCount = origin ? 1 : angularDirections.size();
for (std::size_t index = 0; index < angularCount; ++index) {
const Direction direction = origin ? Direction{{0.0, 0.0, 0.0}, 1.0, "origin"} : angularDirections[index];
mfem::Vector target(3);
for (int d = 0; d < 3; ++d) target(d) = radius * direction.value[d];
auto located = locator.Locate(target, origin ? originHint : angularHints[index]);
++report.requestedPoints;
if (located.found) {
++report.locatedPoints;
++locatedCount;
locatedWeight += direction.weight;
if (located.physical.stellarMaterial) { materialWeight += direction.weight; ++report.materialPoints; }
maxError = std::max(maxError, located.error);
report.maximumLocationError = std::max(report.maximumLocationError, located.error);
}
const auto values = Values(located, direction, origin);
for (std::size_t field = 0; field < values.size(); ++field) {
moments[field].Add(values[field], direction.weight);
if (std::isfinite(values[field])) report.maximumScaledFieldError = std::max(report.maximumScaledFieldError, std::abs(values[field] - exact[field]) / scales[field]);
}
}
radial << (origin ? "origin_single_trace" : "physical_sphere") << ',' << radius << ',' << std::numbers::pi * radius / reference.radius << ',' << radius / reference.radius
<< ',' << angularCount << ',' << locatedCount << ',' << locatedWeight << ',' << materialWeight << ',' << (locatedCount ? maxError : NaN);
for (std::size_t field = 0; field < moments.size(); ++field) {
const double difference = moments[field].Mean() - exact[field];
const double angularRmsScaled = std::sqrt(moments[field].Variance()) / scales[field];
if (std::isfinite(angularRmsScaled)) report.maximumAngularRmsScaled = std::max(report.maximumAngularRmsScaled, angularRmsScaled);
radial << ',' << moments[field].Mean() << ',' << std::sqrt(moments[field].Variance()) << ',' << exact[field]
<< ',' << difference / scales[field] << ',' << std::sqrt(moments[field].Variance() + difference * difference) / scales[field] << ',' << moments[field].weight;
}
radial << ',' << moments[0].Mean() / reference.CentralDensity() << ',' << moments[1].Mean() / reference.CentralEnthalpy()
<< ',' << reference.NormalizedPotential(moments[2].Mean()) << ',' << (origin ? 0 : options.muPointCount) << ',' << (origin ? 0 : options.azimuthPointCount) << '\n';
const std::size_t rayCount = origin ? 1 : rays.size();
for (std::size_t index = 0; index < rayCount; ++index) {
const Direction direction = origin ? Direction{{0.0, 0.0, 0.0}, 1.0, "origin"} : rays[index];
mfem::Vector target(3);
for (int d = 0; d < 3; ++d) target(d) = radius * direction.value[d];
auto located = locator.Locate(target, origin ? originHint : rayHints[index]);
++report.requestedPoints;
if (located.found) {
++report.locatedPoints;
if (located.physical.stellarMaterial) ++report.materialPoints;
report.maximumLocationError = std::max(report.maximumLocationError, located.error);
}
const auto values = Values(located, direction, origin);
for (std::size_t field = 0; field < values.size(); ++field) {
if (std::isfinite(values[field])) report.maximumScaledFieldError = std::max(report.maximumScaledFieldError, std::abs(values[field] - exact[field]) / scales[field]);
}
directional << radius << ',' << std::numbers::pi * radius / reference.radius << ',' << direction.family << ',' << index;
for (const double component : direction.value) directional << ',' << component;
directional << ',' << located.found << ',' << (located.found ? located.physical.element : -1) << ',' << (located.found ? located.physical.attribute : -1)
<< ',' << (located.found && located.physical.stellarMaterial) << ',' << located.error;
for (std::size_t field = 0; field < values.size(); ++field) directional << ',' << values[field] << ',' << exact[field];
directional << ',' << values[0] / reference.CentralDensity() << ',' << values[1] / reference.CentralEnthalpy() << ',' << reference.NormalizedPotential(values[2]) << '\n';
}
}
report.locatorElementAttempts = locator.ElementAttempts();
return report;
}
} // namespace experiment::polytrope_validation

View File

@@ -0,0 +1,445 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <numbers>
#include <stdexcept>
#include <string>
#include <vector>
#include <mfem.hpp>
import mean_field;
#include "polytrope_analytic_self_checks.hpp"
#include "polytrope_validation_measurements.hpp"
#include "polytrope_radial_profiles.hpp"
namespace {
using namespace mean_field;
using namespace experiment::polytrope_validation;
using Clock = std::chrono::steady_clock;
struct Options final {
std::string mesh{"sandbox.smesh"};
std::filesystem::path output{"polytrope_validation_results"};
std::filesystem::path replay;
std::string mode{"solve"};
double absoluteTolerance{1.0e-8};
double relativeTolerance{1.0e-8};
double linearTolerance{0.03};
int maximumNewtonIterations{8};
int maximumLinearIterations{80};
int quadratureOrder{14};
int checkQuadratureOrder{18};
bool profiles{true};
RadialProfileOptions radial;
};
Options Parse(const int argc, char **argv) {
Options options;
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
auto value = [&]() -> std::string {
if (++index >= argc) throw std::invalid_argument("Missing value after " + argument);
return argv[index];
};
if (argument == "--mesh") options.mesh = value();
else if (argument == "--output") options.output = value();
else if (argument == "--self-check") options.mode = "self-check";
else if (argument == "--analytic-mesh") options.mode = "analytic-mesh";
else if (argument == "--solve") options.mode = "solve";
else if (argument == "--replay") {
options.mode = "replay";
options.replay = value();
options.mesh = (options.replay / "input.smesh").string();
}
else if (argument == "--absolute-tolerance") options.absoluteTolerance = std::stod(value());
else if (argument == "--relative-tolerance") options.relativeTolerance = std::stod(value());
else if (argument == "--linear-tolerance") options.linearTolerance = std::stod(value());
else if (argument == "--max-newton") options.maximumNewtonIterations = std::stoi(value());
else if (argument == "--max-linear-iterations") options.maximumLinearIterations = std::stoi(value());
else if (argument == "--quadrature-order") options.quadratureOrder = std::stoi(value());
else if (argument == "--check-quadrature-order") options.checkQuadratureOrder = std::stoi(value());
else if (argument == "--skip-profiles") options.profiles = false;
else if (argument == "--mu-points") options.radial.muPointCount = std::stoi(value());
else if (argument == "--phi-points") options.radial.azimuthPointCount = std::stoi(value());
else if (argument == "--exterior-shells") options.radial.exteriorShellCount = std::stoi(value());
else if (argument == "--help") {
std::cout << "polytrope_validation_experiment [--self-check | --analytic-mesh | --solve]\n"
" [--mesh sandbox.smesh] [--output NEW_DIRECTORY] [--skip-profiles]\n"
" [--absolute-tolerance 1e-8] [--relative-tolerance 1e-8]\n"
" [--linear-tolerance 0.03] [--max-newton 8] [--max-linear-iterations 80]\n"
" [--quadrature-order 14] [--check-quadrature-order 18]\n"
" [--replay SOLVER_OUTPUT_DIRECTORY] (saved nonrotating fields; no Newton context)\n"
" [--mu-points 6] [--phi-points 12] [--exterior-shells 8]\n"
"Single MPI rank. Default: nonrotating n=1 production solve.\n"
"Exit codes: 0 all requested checks pass; 1 nonlinear failure; 2 execution error;\n"
"3 verification failure. Existing output directories are never overwritten.\n";
options.mode = "help";
return options;
} else throw std::invalid_argument("Unknown option: " + argument);
}
if (!std::isfinite(options.absoluteTolerance) || options.absoluteTolerance < 0.0 ||
!std::isfinite(options.relativeTolerance) || options.relativeTolerance < 0.0 ||
!(options.absoluteTolerance > 0.0 || options.relativeTolerance > 0.0) ||
!(options.linearTolerance > 0.0 && options.linearTolerance < 1.0) ||
options.maximumNewtonIterations < 1 || options.maximumLinearIterations < 1 ||
options.quadratureOrder < 2 || options.checkQuadratureOrder <= options.quadratureOrder ||
options.radial.muPointCount < 2 || options.radial.azimuthPointCount < 4 || options.radial.exteriorShellCount < 1) {
throw std::invalid_argument("Invalid tolerance, iteration limit, or quadrature orders.");
}
if (options.mode == "replay") options.mesh = (options.replay / "input.smesh").string();
return options;
}
std::ofstream File(const std::filesystem::path &path) {
std::ofstream stream(path);
stream.exceptions(std::ios::failbit | std::ios::badbit);
stream << std::setprecision(17);
return stream;
}
bool SelfChecks(const Options &options) {
const auto report = RunAnalyticSelfChecks();
auto stream = File(options.output / "analytic_self_checks.csv");
stream << "check,observed,expected,scale,absolute_error,scaled_error,tolerance,passed\n";
for (const auto &check : report.checks) {
stream << check.name << ',' << check.observed << ',' << check.expected << ',' << check.scale << ','
<< check.AbsoluteError() << ',' << check.ScaledError() << ',' << check.tolerance << ',' << check.passed << '\n';
if (!check.passed) std::cerr << "Analytic check failed: " << check.name << " scaled error=" << check.ScaledError() << '\n';
}
std::cout << "Independent analytic checks: " << report.checks.size() << ", passed=" << report.Passed() << std::endl;
return report.Passed();
}
void WriteMetrics(const std::filesystem::path &path, const Measurements &measurements) {
auto stream = File(path);
stream << "metric,value\n";
for (const auto &[name, value] : measurements) stream << name << ',' << value << '\n';
}
std::map<std::string, std::string> ReadMetadata(const std::filesystem::path &directory) {
std::ifstream stream(directory / "metadata.txt");
if (!stream) throw std::runtime_error("Cannot read replay metadata.");
std::map<std::string, std::string> result;
for (std::string line; std::getline(stream, line);) {
const auto separator = line.find('=');
if (separator != std::string::npos) result[line.substr(0, separator)] = line.substr(separator+1);
}
return result;
}
Measurements ReadSavedSolverMetrics(const std::filesystem::path &directory) {
std::ifstream stream(directory / "physical_metrics.csv");
if (!stream) throw std::runtime_error("Replay needs completed physical_metrics.csv to preserve solver/border diagnostics.");
Measurements result;
std::string line;
std::getline(stream, line);
while (std::getline(stream, line)) {
const auto separator = line.find(',');
if (separator == std::string::npos) throw std::runtime_error("Malformed saved physical metrics.");
const auto name = line.substr(0, separator);
if (name == "normalized_bordered_residual" || name == "normalized_unbordered_residual" ||
name == "normalized_central_border_action" || name == "central_border" ||
name == "bernoulli_constant" || name == "angular_velocity_norm") {
result[name] = std::stod(line.substr(separator+1));
}
}
if (result.size() != 6 || result.at("angular_velocity_norm") != 0.0) {
throw std::runtime_error("Replay currently requires complete saved diagnostics and exactly zero rotation.");
}
return result;
}
template <typename SampleState>
bool Measure(SampleState &state, const N1Reference &reference, const Options &options,
Measurements additional = {}) {
std::cout << "Physical volume integration, order=" << options.quadratureOrder << std::endl;
const auto base = MeasureVolumes(state, reference, options.quadratureOrder);
WriteMetrics(options.output / "volume_metrics_base.csv", base);
std::cout << "Independent higher-order integration, order=" << options.checkQuadratureOrder << std::endl;
auto metrics = MeasureVolumes(state, reference, options.checkQuadratureOrder);
auto quadrature = File(options.output / "quadrature_comparison.csv");
quadrature << "metric,base,check,absolute_difference,relative_difference\n";
for (const auto &[name, high] : metrics) {
const double low = base.at(name);
quadrature << name << ',' << low << ',' << high << ',' << std::abs(high-low) << ','
<< std::abs(high-low) / std::max(std::abs(high), 1.0e-300) << '\n';
}
metrics["quadrature_virial_absolute_change"] = std::abs(metrics.at("virial_signed") - base.at("virial_signed"));
metrics["quadrature_binding_relative_change"] = std::abs(metrics.at("binding_energy") - base.at("binding_energy")) / std::abs(reference.BindingEnergy());
std::cout << "Stellar surface and all-element corner sampling" << std::endl;
metrics.merge(MeasureSurfaceAndCorners(state, reference, options.checkQuadratureOrder));
metrics.merge(additional);
if (options.profiles) {
std::cout << "Physical radial projection (stellar interior and finite exterior)" << std::endl;
const auto profiles = WriteRadialProfiles(state, reference, options.output, options.radial);
metrics["profile_requested_points"] = profiles.requestedPoints;
metrics["profile_missing_points"] = profiles.requestedPoints - profiles.locatedPoints;
metrics["profile_material_points"] = profiles.materialPoints;
metrics["profile_locator_element_attempts"] = profiles.locatorElementAttempts;
metrics["profile_maximum_location_error"] = profiles.maximumLocationError;
metrics["profile_angular_moment_error"] = profiles.angularMomentError;
metrics["profile_maximum_scaled_error"] = profiles.maximumScaledFieldError;
metrics["profile_maximum_angular_rms_scaled"] = profiles.maximumAngularRmsScaled;
}
metrics["negative_density_maximum_scaled"] = std::max(0.0, -metrics.at("minimum_density")) / reference.CentralDensity();
metrics["negative_enthalpy_maximum_scaled"] = std::max(0.0, -metrics.at("minimum_enthalpy")) / reference.CentralEnthalpy();
WriteMetrics(options.output / "physical_metrics.csv", metrics);
// Initial screening budgets, declared before running the solver. A pass
// is not a mesh-convergence certificate. The complete errors are saved.
std::map<std::string, double> budgets{
{"mass_relative_error", 1.0e-4}, {"volume_radius_relative_error", 1.0e-4},
{"surface_radius_relative_rms_error", 1.0e-4},
{"density_relative_l2_error", 1.0e-4}, {"enthalpy_relative_l2_error", 1.0e-4},
{"potential_relative_l2_error", 1.0e-4}, {"gravity_gradient_relative_l2_error", 1.0e-4},
{"binding_relative_error", 1.0e-4}, {"pressure_integral_relative_error", 1.0e-4},
{"moment_of_inertia_relative_error", 1.0e-4}, {"virial_error", 1.0e-6},
{"force_virial_error", 1.0e-6}, {"gravity_energy_consistency", 1.0e-6},
{"eos_enthalpy_scaled_rms", 1.0e-6}, {"bernoulli_scaled_rms_variation", 1.0e-4},
{"quadrature_virial_absolute_change", 1.0e-8}, {"quadrature_binding_relative_change", 1.0e-8},
{"invalid_stellar_corner_samples", 0.0}, {"kinetic_energy", 1.0e-14},
{"negative_density_maximum_scaled", 1.0e-8}, {"negative_enthalpy_maximum_scaled", 1.0e-8}
};
if (options.profiles) budgets["profile_missing_points"] = 0.0;
if (options.profiles && options.mode == "analytic-mesh") budgets["profile_maximum_scaled_error"] = 1.0e-8;
if (options.profiles && options.mode == "analytic-mesh") budgets["profile_maximum_angular_rms_scaled"] = 1.0e-10;
if (metrics.contains("normalized_unbordered_residual")) budgets["normalized_unbordered_residual"] = 1.0e-8;
auto checks = File(options.output / "verification_checks.csv");
checks << "metric,observed,maximum_allowed,passed\n";
bool passed = true;
for (const auto &[name, budget] : budgets) {
const double value = metrics.at(name);
const bool okay = std::isfinite(value) && std::abs(value) <= budget;
checks << name << ',' << value << ',' << budget << ',' << okay << '\n';
passed = passed && okay;
if (!okay) std::cout << "Screen failed: " << name << '=' << value << " budget=" << budget << '\n';
}
for (const std::string name : {"mass", "binding_energy", "pressure_integral", "virial_ratio", "virial_error",
"force_virial_error", "density_relative_l2_error", "enthalpy_relative_l2_error",
"potential_relative_l2_error", "surface_radius_relative_rms_error"}) {
std::cout << name << '=' << metrics.at(name) << '\n';
}
std::cout << "Physical screening passed=" << passed << " (not a resolution-convergence claim)" << std::endl;
return passed;
}
double BlockNorm(const mfem::Vector &vector, const int offset, const int size) {
long double sum = 0.0L;
for (int index = offset; index < offset + size; ++index) sum += static_cast<long double>(vector(index)) * vector(index);
return std::sqrt(sum);
}
int Run(const Options &options) {
if (options.mode == "help") return 0;
if (!std::filesystem::create_directory(options.output)) {
throw std::invalid_argument("Output directory already exists; choose a new --output directory.");
}
auto metadata = File(options.output / "metadata.txt");
const N1Reference reference{utils::G, utils::MASS, utils::RADIUS};
reference.Validate();
metadata << "mode=" << options.mode << "\nmesh=" << std::filesystem::absolute(options.mesh).string()
<< "\ncompiled=" << __DATE__ << ' ' << __TIME__ << "\ncompiler=" << __VERSION__
<< "\nmpi_ranks=1\nmodel=nonrotating_n1_fixed_mass_fixed_central_density_zero_surface_pressure"
<< "\nG=" << reference.gravitationalConstant << "\nM=" << reference.mass << "\nR=" << reference.radius
<< "\nK=" << reference.PolytropicConstant() << "\nrho_c=" << reference.CentralDensity()
<< "\nabsolute_tolerance=" << options.absoluteTolerance << "\nrelative_tolerance=" << options.relativeTolerance
<< "\nlinear_tolerance=" << options.linearTolerance << "\nmax_newton=" << options.maximumNewtonIterations
<< "\nmax_linear_iterations=" << options.maximumLinearIterations
<< "\npolynomial_increment=" << MEAN_FIELD_UNIFORM_POLYNOMIAL_ORDER_INCREMENT
<< "\nnormalization=production_frozen_physical_Riesz_diagonal\nprofiles=" << options.profiles << '\n';
metadata << "mu_points=" << options.radial.muPointCount << "\nphi_points=" << options.radial.azimuthPointCount
<< "\nexterior_shells=" << options.radial.exteriorShellCount << '\n';
metadata.flush();
if (!SelfChecks(options)) return 3;
if (options.mode == "self-check") return 0;
const auto meshSnapshot = options.output / "input.smesh";
std::filesystem::copy_file(options.mesh, meshSnapshot);
metadata << "mesh_snapshot=" << std::filesystem::absolute(meshSnapshot).string() << '\n';
utils::Args arguments;
arguments.mesh_file = meshSnapshot.string();
arguments.p.rtol = arguments.p.atol = 1.0e-12;
auto finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
if (!finiteElements.okay()) throw std::runtime_error("Could not construct finite elements.");
metadata << "mesh_bytes=" << std::filesystem::file_size(meshSnapshot)
<< "\nelements=" << finiteElements.mesh->GetNE()
<< "\ndensity_order=" << finiteElements.densityFes->GetMaxElementOrder()
<< "\nenthalpy_order=" << finiteElements.enthalpyFes->GetMaxElementOrder()
<< "\npotential_order=" << finiteElements.gravityPotentialFes->GetMaxElementOrder()
<< "\ngravity_flux_order=" << finiteElements.gravityFluxFes->GetMaxElementOrder()
<< "\ndisplacement_order=" << finiteElements.displacementFes->GetMaxElementOrder() << '\n';
metadata.flush();
if (options.mode == "analytic-mesh") {
AnalyticMeshState analytic(finiteElements, reference);
const bool passed = Measure(analytic, reference, options);
metadata << "physical_screen_passed=" << passed << '\n';
return passed ? 0 : 3;
}
if (options.mode == "replay") {
const auto source = ReadMetadata(options.replay);
if (source.at("mode") != "solve" || source.at("model") != "nonrotating_n1_fixed_mass_fixed_central_density_zero_surface_pressure" ||
std::stod(source.at("G")) != reference.gravitationalConstant || std::stod(source.at("M")) != reference.mass ||
std::stod(source.at("R")) != reference.radius) {
throw std::runtime_error("Replay source does not match this nonrotating n=1 experiment.");
}
auto savedMetrics = ReadSavedSolverMetrics(options.replay);
PhysicalState physical(finiteElements, options.replay);
const bool converged = source.at("solver_converged") == "1";
metadata << "replay_source=" << std::filesystem::absolute(options.replay).string()
<< "\nsolver_converged=" << converged
<< "\nsolver_diagnostics=copied_from_source_not_recomputed\n";
if (!converged) metadata << "solver_failure=" << source.at("solver_failure") << '\n';
metadata.flush();
const bool passed = Measure(physical, reference, options, std::move(savedMetrics));
metadata << "physical_screen_passed=" << passed << '\n';
return !converged ? 1 : (passed ? 0 : 3);
}
auto stellarModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = reference.PolytropicConstant()}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{reference.mass}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.0},
.axis = {0.0, 0.0, 1.0}, .center = {0.0, 0.0, 0.0}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{reference.CentralDensity()}})
);
auto discretization = equilibrium::makeStellarDiscretization(std::move(finiteElements),
normalization::PhysicalRieszDiagonal{dimensions::LengthValue{reference.radius}, reference.gravitationalConstant});
std::cout << "Constructing production context; nonrotating n=1, nonlinear atol=" << options.absoluteTolerance << std::endl;
const auto start = Clock::now();
auto context = solver::makeContext(std::move(stellarModel), std::move(discretization),
preconditioning::makePreconditioner(), solver::linear::FGMRES({.restartLength = 40, .printLevel = -1}));
metadata << "context_seconds=" << std::chrono::duration<double>(Clock::now()-start).count() << '\n';
metadata.flush();
// Exercise accepted-field reconstruction before the expensive solve,
// and retain a seed baseline to detect physical degradation by Newton.
std::cout << "Measuring production seed before Newton" << std::endl;
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &state, const fem::FEM &fem) {
PhysicalState physical(state, fem);
auto seedMetrics = MeasureVolumes(physical, reference, options.quadratureOrder);
seedMetrics["normalized_bordered_residual"] = physical.normalizedBorderedResidualNorm;
seedMetrics["normalized_unbordered_residual"] = physical.normalizedUnborderedResidualNorm;
seedMetrics["central_border"] = physical.centralBorder;
WriteMetrics(options.output / "seed_physical_metrics.csv", seedMetrics);
std::cout << "Seed |F|=" << physical.normalizedBorderedResidualNorm
<< ", density relative L2=" << seedMetrics.at("density_relative_l2_error")
<< ", virial error=" << seedMetrics.at("virial_error") << std::endl;
});
auto trajectory = File(options.output / "newton_history.csv");
trajectory << "iteration,residual,step,trials,iteration_seconds,linear_iterations,linear_relative_residual,linear_seconds\n";
auto observer = solver::nonlinear::makeObserver(
[](const solver::nonlinear::BeforeIteration &event) {
std::cout << "Newton " << event.iteration << ": |F|=" << event.residualNorm << std::endl;
},
[&](const solver::nonlinear::AfterIteration &event) {
trajectory << event.iteration << ',' << event.residualNorm << ',' << event.acceptedStepLength << ','
<< event.lineSearchTrials << ',' << event.iterationSeconds << ','
<< (event.linearSolve ? event.linearSolve->iterations : 0) << ','
<< (event.linearSolve ? event.linearSolve->relativeTrueResidualNorm : 0.0) << ','
<< (event.linearSolve ? event.linearSolve->solveSeconds : 0.0) << '\n';
trajectory.flush();
std::cout << " step=" << event.acceptedStepLength << ", |F|=" << event.residualNorm
<< ", seconds=" << event.iterationSeconds;
if (event.linearSolve) std::cout << ", linear_iterations=" << event.linearSolve->iterations
<< ", true_relative=" << event.linearSolve->relativeTrueResidualNorm;
std::cout << std::endl;
}
);
auto nonlinear = solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{
.relativeTolerance = options.relativeTolerance, .absoluteTolerance = options.absoluteTolerance,
.maximumIterations = options.maximumNewtonIterations,
.linearSolve = {.relativeTolerance = options.linearTolerance, .absoluteTolerance = 0.0,
.maximumIterations = options.maximumLinearIterations}, .backtracking = {}
});
const auto report = [&]() {
auto equilibriumSolver = solver::make(context, nonlinear, observer);
return equilibriumSolver.evaluate();
}(); // Release the solver before the guarded diagnostic callback.
metadata << "solver_converged=" << report.converged()
<< "\naccepted_steps=" << report.completedNonlinearIterations() << '\n';
if (!report.converged()) metadata << "solver_failure=" << report.failure().message << '\n';
metadata.flush();
std::cout << "Solver converged=" << report.converged() << "; measuring last accepted state" << std::endl;
bool passed = false;
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &state, const fem::FEM &fem) {
// Preserve expensive accepted coefficients before postprocessing.
// This is experiment data, not a versioned production checkpoint.
auto accepted = File(options.output / "accepted_state.txt");
accepted << state.AcceptedPhysicalState().Size() << '\n';
state.AcceptedPhysicalState().Print(accepted, 1);
accepted.close();
auto layout = File(options.output / "state_layout.csv");
layout << "block,offset,size\n";
for (const auto &block : state.Problem().GetManifest().valueBlocks()) {
layout << block.stableId << ',' << block.offset << ',' << block.size << '\n';
}
PhysicalState physical(state, fem);
auto saveField = [&](const char *name, const mfem::ParGridFunction &field) {
auto stream = File(options.output / (std::string(name) + ".gf"));
field.Save(stream);
};
saveField("density", physical.density);
saveField("enthalpy", physical.enthalpy);
saveField("potential", physical.potential);
saveField("gravity_gradient_reference", physical.gravityGradientReference);
saveField("displacement", physical.displacement);
auto residuals = File(options.output / "residual_blocks.csv");
residuals << "block,size,physical_bordered_l2,physical_unbordered_l2,normalized_bordered_l2,normalized_unbordered_l2\n";
for (const auto &block : state.Problem().GetManifest().residualBlocks()) {
residuals << block.stableId << ',' << block.size << ','
<< BlockNorm(physical.physicalResidualBordered, block.offset, block.size) << ','
<< BlockNorm(physical.physicalResidualUnbordered, block.offset, block.size) << ','
<< BlockNorm(physical.normalizedBorderedResidual, block.offset, block.size) << ','
<< BlockNorm(physical.normalizedUnborderedResidual, block.offset, block.size) << '\n';
}
auto reconstruction = File(options.output / "field_reconstruction.csv");
reconstruction << "field,reduced_size,full_true_size,maximum_round_trip_error\n";
for (const auto &field : physical.reconstructionReports) {
reconstruction << field.field << ',' << field.reducedSize << ',' << field.fullTrueSize << ',' << field.maximumRoundTripError << '\n';
}
metadata << "state_size=" << state.AcceptedPhysicalState().Size()
<< "\naccepted_normalized_residual=" << physical.normalizedBorderedResidualNorm << '\n';
if (physical.centralDensityReport) {
metadata << "central_constraint_density_inferred_from_h=" << physical.centralDensityReport->achievedDensity
<< "\ncentral_constraint_h=" << physical.centralDensityReport->achievedEnthalpy << '\n';
}
metadata.flush();
passed = Measure(physical, reference, options, {
{"normalized_bordered_residual", physical.normalizedBorderedResidualNorm},
{"normalized_unbordered_residual", physical.normalizedUnborderedResidualNorm},
{"normalized_central_border_action", physical.normalizedCentralBorderActionNorm},
{"central_border", physical.centralBorder}, {"bernoulli_constant", physical.bernoulliConstant},
{"angular_velocity_norm", physical.rotation.angular_velocity().Norml2()}
});
});
metadata << "physical_screen_passed=" << passed << "\ntotal_seconds=" << std::chrono::duration<double>(Clock::now()-start).count() << '\n';
return !report.converged() ? 1 : (passed ? 0 : 3);
}
} // namespace
int main(int argc, char **argv) {
mfem::Mpi::Init(argc, argv);
int result = 0;
try {
int ranks = 0;
MPI_Comm_size(MPI_COMM_WORLD, &ranks);
if (ranks != 1) throw std::invalid_argument("Run this verification on exactly one MPI rank.");
mfem::Device device("cpu");
std::cout << std::setprecision(12);
result = Run(Parse(argc, argv));
} catch (const std::exception &error) {
std::cerr << "polytrope verification failure: " << error.what() << std::endl;
result = 2;
}
mfem::Mpi::Finalize();
return result;
}

View File

@@ -0,0 +1,339 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <map>
#include <numbers>
#include <stdexcept>
#include <string>
#include "polytrope_analytic_reference.hpp"
#include "polytrope_physical_state.hpp"
namespace experiment::polytrope_validation {
using Measurements = std::map<std::string, double>;
// Exact fields evaluated on the actual mapped mesh. This checks the same
// integration and physical-point locator used for a numerical solution,
// without constructing a Newton context or using its Lane-Emden seed.
class AnalyticMeshState final {
public:
const mean_field::fem::FEM &finiteElements;
mean_field::mapping::GridFunctionMappingEvaluator mapping;
N1Reference reference;
AnalyticMeshState(const mean_field::fem::FEM &fem, const N1Reference &analytic)
: finiteElements(fem),
mapping(*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate),
reference(analytic) {
}
[[nodiscard]] bool isStellar(const int element) const {
using Schema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
return Schema::template attribute_belongs_to<mean_field::utils::domain::Stellar>(
finiteElements.mesh->GetAttribute(element)
);
}
mean_field::mapping::MappingStatus Evaluate(
const int element, const mfem::IntegrationPoint &point, PhysicalPoint &result
) {
auto *transformation = finiteElements.mesh->GetElementTransformation(element);
transformation->SetIntPoint(&point);
const auto status = mapping.EvaluatePoint(*transformation, point, result.mapping);
if (status != mean_field::mapping::MappingStatus::valid) return status;
const double radius = result.mapping.physical_position.Norml2();
const auto analytic = reference.AtRadius(radius);
result.element = element;
result.attribute = transformation->Attribute;
result.stellarMaterial = isStellar(element);
result.rho = analytic.density;
result.h = analytic.enthalpy;
result.phi = analytic.potential;
result.rotationPotential = 0.0;
result.gravityGradientPhysical.SetSize(3);
result.enthalpyGradientPhysical.SetSize(3);
result.potentialGradientPhysical.SetSize(3);
for (int component = 0; component < 3; ++component) {
const double gradient = radius > 0.0
? analytic.radialPotentialGradient * result.mapping.physical_position(component) / radius : 0.0;
result.gravityGradientPhysical(component) = gradient;
result.potentialGradientPhysical(component) = gradient;
result.enthalpyGradientPhysical(component) = radius <= reference.radius ? -gradient : 0.0;
}
return status;
}
};
struct ErrorIntegral final {
long double errorSquared{0.0L};
long double referenceSquared{0.0L};
double maximumScaledError{0.0};
void Add(const double value, const double exact, const double weight, const double scale) {
const long double difference = static_cast<long double>(value) - exact;
errorSquared += weight * difference * difference;
referenceSquared += static_cast<long double>(weight) * exact * exact;
maximumScaledError = std::max(maximumScaledError, std::abs(value - exact) / scale);
}
[[nodiscard]] double RelativeL2() const {
return referenceSquared > 0.0L ? static_cast<double>(std::sqrt(errorSquared / referenceSquared))
: std::numeric_limits<double>::quiet_NaN();
}
};
template <typename SampleState>
Measurements MeasureVolumes(SampleState &state, const N1Reference &reference, const int quadratureOrder) {
long double volume = 0.0L, mass = 0.0L, binding = 0.0L, forceBinding = 0.0L;
long double pressureIntegral = 0.0L, enthalpyPressureIntegral = 0.0L, kinetic = 0.0L;
long double momentOfInertia = 0.0L, bernoulliOffsetMean = 0.0L, bernoulliCenteredSquared = 0.0L;
long double closureSquared = 0.0L, closureDensityInnerProduct = 0.0L;
long double gradientMismatch = 0.0L, hydrostaticGradient = 0.0L;
std::array<long double, 3> firstMoment{};
ErrorIntegral densityError, enthalpyError, potentialError, pressureError, gravityError;
double minimumDensity = std::numeric_limits<double>::infinity();
double minimumEnthalpy = std::numeric_limits<double>::infinity();
double minimumDeterminant = std::numeric_limits<double>::infinity();
double bernoulliMinimum = std::numeric_limits<double>::infinity();
double bernoulliMaximum = -std::numeric_limits<double>::infinity();
std::uint64_t samples = 0, negativeDensitySamples = 0, negativeEnthalpySamples = 0;
PhysicalPoint point;
const double densityScale = reference.CentralDensity();
const double enthalpyScale = reference.CentralEnthalpy();
const double pressureScale = reference.PolytropicConstant() * densityScale * densityScale;
const double gravityScale = reference.gravitationalConstant * reference.mass /
(reference.radius * reference.radius);
for (int element = 0; element < state.finiteElements.mesh->GetNE(); ++element) {
if (!state.isStellar(element)) continue;
auto *transformation = state.finiteElements.mesh->GetElementTransformation(element);
const auto &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadratureOrder);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto &ip = rule.IntPoint(q);
if (state.Evaluate(element, ip, point) != mean_field::mapping::MappingStatus::valid) {
throw std::runtime_error("Invalid physical mapping in polytrope volume verification.");
}
transformation->SetIntPoint(&ip);
// Explicitly check the orientation of both the reference map
// and the deformation before integrating physical volume.
const double referenceDeterminant = transformation->Jacobian().Det();
const double weight = ip.weight * referenceDeterminant * point.mapping.mapping_determinant;
if (!(referenceDeterminant > 0.0) || !std::isfinite(referenceDeterminant)) {
throw std::runtime_error("Non-positive or non-finite reference Jacobian in polytrope volume verification.");
}
if (!(weight > 0.0) || !std::isfinite(weight) || !std::isfinite(point.rho) ||
!std::isfinite(point.h) || !std::isfinite(point.phi) || !std::isfinite(point.rotationPotential)) {
throw std::runtime_error("Non-finite field or non-positive physical integration weight.");
}
++samples;
const auto &position = point.mapping.physical_position;
const double radius = position.Norml2();
const auto exact = reference.AtRadius(radius);
// Do not clamp numerical density/enthalpy. Negative values are
// reported, and pressure consistency is checked independently.
const double pressure = reference.PolytropicConstant() * point.rho * point.rho;
const double pressureFromEnthalpy = point.h * point.h / (4.0 * reference.PolytropicConstant());
const long double specificBernoulli = static_cast<long double>(point.h) + point.phi - point.rotationPotential;
volume += weight;
mass += weight * point.rho;
binding += 0.5L * weight * point.rho * point.phi;
forceBinding -= weight * point.rho * (position * point.gravityGradientPhysical);
pressureIntegral += weight * pressure;
enthalpyPressureIntegral += weight * pressureFromEnthalpy;
// RigidRotation::potential is positive +|Omega x r|^2/2.
kinetic += weight * point.rho * point.rotationPotential;
momentOfInertia += weight * point.rho * (position(0) * position(0) + position(1) * position(1));
// Weighted Welford accumulation about the fixed analytic
// Bernoulli constant C=-h_c resolves small spatial variations
// without subtracting two O(h_c^2) second moments. No fitted
// potential offset is applied to any physical field/error.
const long double bernoulliOffset = specificBernoulli + enthalpyScale;
const long double bernoulliDelta = bernoulliOffset - bernoulliOffsetMean;
bernoulliOffsetMean += (static_cast<long double>(weight) / volume) * bernoulliDelta;
bernoulliCenteredSquared += weight * bernoulliDelta * (bernoulliOffset - bernoulliOffsetMean);
const double closure = point.h - 2.0 * reference.PolytropicConstant() * point.rho;
closureSquared += weight * closure * closure;
closureDensityInnerProduct += weight * point.rho * closure;
bernoulliMinimum = std::min(bernoulliMinimum, static_cast<double>(specificBernoulli));
bernoulliMaximum = std::max(bernoulliMaximum, static_cast<double>(specificBernoulli));
minimumDensity = std::min(minimumDensity, point.rho);
minimumEnthalpy = std::min(minimumEnthalpy, point.h);
minimumDeterminant = std::min(minimumDeterminant, point.mapping.mapping_determinant);
negativeDensitySamples += point.rho < 0.0;
negativeEnthalpySamples += point.h < 0.0;
densityError.Add(point.rho, exact.density, weight, densityScale);
enthalpyError.Add(point.h, exact.enthalpy, weight, enthalpyScale);
potentialError.Add(point.phi, exact.potential, weight, enthalpyScale);
pressureError.Add(pressure, exact.pressure, weight, pressureScale);
for (int component = 0; component < 3; ++component) {
if (!std::isfinite(point.gravityGradientPhysical(component)) ||
!std::isfinite(point.potentialGradientPhysical(component)) ||
!std::isfinite(point.enthalpyGradientPhysical(component))) {
throw std::runtime_error("Non-finite physical field gradient in polytrope volume verification.");
}
firstMoment[component] += weight * point.rho * position(component);
const double exactGradient = radius > 0.0
? exact.radialPotentialGradient * position(component) / radius : 0.0;
gravityError.Add(point.gravityGradientPhysical(component), exactGradient, weight, gravityScale);
const double mismatch = point.gravityGradientPhysical(component) - point.potentialGradientPhysical(component);
gradientMismatch += weight * mismatch * mismatch;
const double hydrostatic = point.enthalpyGradientPhysical(component) + point.gravityGradientPhysical(component);
hydrostaticGradient += weight * hydrostatic * hydrostatic;
}
}
}
if (!(volume > 0.0L) || !(mass > 0.0L) || !(binding < 0.0L)) {
throw std::runtime_error("Polytrope verification requires positive stellar volume/mass and negative binding energy.");
}
Measurements result{
{"quadrature_order", static_cast<double>(quadratureOrder)}, {"stellar_samples", static_cast<double>(samples)},
{"volume", static_cast<double>(volume)}, {"mass", static_cast<double>(mass)},
{"mass_relative_error", static_cast<double>(std::abs(mass / reference.mass - 1.0L))},
{"volume_radius", static_cast<double>(std::cbrt(3.0L * volume / (4.0L * std::numbers::pi_v<long double>)))},
{"binding_energy", static_cast<double>(binding)}, {"force_binding_energy", static_cast<double>(forceBinding)},
{"pressure_integral", static_cast<double>(pressureIntegral)},
{"enthalpy_pressure_integral", static_cast<double>(enthalpyPressureIntegral)},
{"kinetic_energy", static_cast<double>(kinetic)},
{"virial_signed", static_cast<double>((2.0L * kinetic + binding + 3.0L * pressureIntegral) / std::abs(binding))},
{"virial_error", static_cast<double>(std::abs(2.0L * kinetic + binding + 3.0L * pressureIntegral) / std::abs(binding))},
{"virial_ratio", static_cast<double>((2.0L * kinetic + 3.0L * pressureIntegral) / std::abs(binding))},
{"force_virial_error", static_cast<double>(std::abs(2.0L * kinetic + forceBinding + 3.0L * pressureIntegral) / std::abs(binding))},
{"enthalpy_virial_error", static_cast<double>(std::abs(2.0L * kinetic + binding + 3.0L * enthalpyPressureIntegral) / std::abs(binding))},
{"enthalpy_force_virial_error", static_cast<double>(std::abs(2.0L * kinetic + forceBinding + 3.0L * enthalpyPressureIntegral) / std::abs(binding))},
{"gravity_energy_consistency", static_cast<double>(std::abs(binding - forceBinding) / std::abs(binding))},
{"binding_relative_error", static_cast<double>(std::abs(binding / reference.BindingEnergy() - 1.0L))},
{"pressure_integral_relative_error", static_cast<double>(std::abs(pressureIntegral / reference.PressureIntegral() - 1.0L))},
{"pressure_integral_eos_disagreement", static_cast<double>(std::abs(pressureIntegral - enthalpyPressureIntegral) / reference.PressureIntegral())},
{"closure_projection_pressure_gap", static_cast<double>(closureSquared / (4.0L * reference.PolytropicConstant()))},
{"closure_density_inner_product", static_cast<double>(closureDensityInnerProduct)},
{"closure_projection_pressure_gap_relative_defect", static_cast<double>((enthalpyPressureIntegral - pressureIntegral -
closureSquared / (4.0L * reference.PolytropicConstant())) / reference.PressureIntegral())},
{"moment_of_inertia", static_cast<double>(momentOfInertia)},
{"moment_of_inertia_relative_error", static_cast<double>(std::abs(momentOfInertia / reference.MomentOfInertia() - 1.0L))},
{"bernoulli_mean", static_cast<double>(bernoulliOffsetMean - enthalpyScale)},
{"bernoulli_mean_scaled_error", static_cast<double>(std::abs(bernoulliOffsetMean) / enthalpyScale)},
{"bernoulli_scaled_range", (bernoulliMaximum - bernoulliMinimum) / enthalpyScale},
{"bernoulli_scaled_rms_variation", static_cast<double>(std::sqrt(std::max(0.0L, bernoulliCenteredSquared / volume)) / enthalpyScale)},
{"eos_enthalpy_scaled_rms", static_cast<double>(std::sqrt(closureSquared / volume) / enthalpyScale)},
{"gravity_gradient_vs_broken_potential_gradient_scaled_rms", static_cast<double>(std::sqrt(gradientMismatch / volume) / gravityScale)},
{"nonrotating_hydrostatic_gradient_scaled_rms", static_cast<double>(std::sqrt(hydrostaticGradient / volume) / gravityScale)},
{"minimum_density", minimumDensity}, {"minimum_enthalpy", minimumEnthalpy},
{"negative_density_samples", static_cast<double>(negativeDensitySamples)},
{"negative_enthalpy_samples", static_cast<double>(negativeEnthalpySamples)},
{"minimum_mapping_determinant", minimumDeterminant}
};
result["volume_radius_relative_error"] = std::abs(result.at("volume_radius") / reference.radius - 1.0);
for (const auto &[name, error] : std::array<std::pair<const char *, const ErrorIntegral *>, 5>{{
{"density", &densityError}, {"enthalpy", &enthalpyError}, {"potential", &potentialError},
{"pressure", &pressureError}, {"gravity_gradient", &gravityError}}}) {
result[std::string(name) + "_relative_l2_error"] = error->RelativeL2();
result[std::string(name) + "_maximum_scaled_error"] = error->maximumScaledError;
}
for (int component = 0; component < 3; ++component) {
result["center_of_mass_" + std::to_string(component)] = static_cast<double>(firstMoment[component] / mass);
}
return result;
}
template <typename SampleState>
Measurements MeasureSurfaceAndCorners(SampleState &state, const N1Reference &reference, const int order) {
auto &mesh = *state.finiteElements.mesh;
PhysicalPoint mapped;
double minimumRadius = std::numeric_limits<double>::infinity(), maximumRadius = 0.0;
double maximumSurfaceEnthalpy = 0.0, maximumSurfacePotentialError = 0.0;
long double area = 0.0L, radiusIntegral = 0.0L, radiusError = 0.0L;
std::uint64_t boundarySamples = 0, invalidCornerSamples = 0, cornerSamples = 0;
double minimumCornerDeterminant = std::numeric_limits<double>::infinity();
double maximumCornerCondition = 0.0;
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
if (mesh.GetBdrAttribute(boundary) != 1) continue; // Canonical sandbox stellar surface.
// The tagged stellar surface is an interior material interface
// when a vacuum region is present. Resolve the actual mesh face
// to retain both adjacent traces in that case.
const int faceIndex = mesh.GetBdrElementFaceIndex(boundary);
if (faceIndex < 0) throw std::runtime_error("Missing stellar surface mesh face.");
auto *face = mesh.GetFaceElementTransformations(faceIndex);
if (face == nullptr || face->Elem1 == nullptr) throw std::runtime_error("Missing stellar surface transformation.");
const bool first = state.isStellar(face->Elem1No);
if (!first && (face->Elem2 == nullptr || !state.isStellar(face->Elem2No))) {
throw std::runtime_error("Stellar surface has no stellar-material trace.");
}
const int element = first ? face->Elem1No : face->Elem2No;
const auto &rule = mfem::IntRules.Get(face->GetGeometryType(), order);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto &ip = rule.IntPoint(q);
// Field evaluation may reuse MFEM's cached element transforms;
// restore both adjacent face traces before each quadrature point.
face = mesh.GetFaceElementTransformations(faceIndex);
face->SetAllIntPoints(&ip);
const auto volumePoint = first ? face->Elem1->GetIntPoint() : face->Elem2->GetIntPoint();
mfem::DenseMatrix referenceFaceJacobian(face->Jacobian());
if (state.Evaluate(element, volumePoint, mapped) != mean_field::mapping::MappingStatus::valid) {
throw std::runtime_error("Invalid mapping on stellar surface.");
}
mfem::DenseMatrix physicalFaceJacobian(3, 2);
mfem::Mult(mapped.mapping.mapping_jacobian, referenceFaceJacobian, physicalFaceJacobian);
const double weight = ip.weight * physicalFaceJacobian.Weight();
const double radius = mapped.mapping.physical_position.Norml2();
if (!(weight > 0.0) || !std::isfinite(weight) || !std::isfinite(radius) ||
!std::isfinite(mapped.h) || !std::isfinite(mapped.phi)) {
throw std::runtime_error("Non-finite field or non-positive physical surface integration weight.");
}
area += weight;
radiusIntegral += weight * radius;
radiusError += weight * (radius - reference.radius) * (radius - reference.radius);
minimumRadius = std::min(minimumRadius, radius);
maximumRadius = std::max(maximumRadius, radius);
maximumSurfaceEnthalpy = std::max(maximumSurfaceEnthalpy, std::abs(mapped.h) / reference.CentralEnthalpy());
maximumSurfacePotentialError = std::max(maximumSurfacePotentialError,
std::abs(mapped.phi + reference.CentralEnthalpy()) / reference.CentralEnthalpy());
++boundarySamples;
}
}
for (int element = 0; element < mesh.GetNE(); ++element) {
if (!state.isStellar(element)) continue;
const auto geometry = mesh.GetElementBaseGeometry(element);
const auto &vertices = *mfem::Geometries.GetVertices(geometry);
const auto &center = mfem::Geometries.GetCenter(geometry);
for (int vertex = 0; vertex < vertices.GetNPoints(); ++vertex) {
for (const double inset : {0.0, 0.005, 0.02}) {
const auto &v = vertices.IntPoint(vertex);
mfem::IntegrationPoint ip;
ip.Set3((1.0-inset)*v.x+inset*center.x, (1.0-inset)*v.y+inset*center.y, (1.0-inset)*v.z+inset*center.z);
++cornerSamples;
if (state.Evaluate(element, ip, mapped) != mean_field::mapping::MappingStatus::valid) {
++invalidCornerSamples;
continue;
}
auto *transformation = mesh.GetElementTransformation(element);
transformation->SetIntPoint(&ip);
mfem::DenseMatrix totalJacobian(3);
mfem::Mult(mapped.mapping.mapping_jacobian, transformation->Jacobian(), totalJacobian);
minimumCornerDeterminant = std::min(minimumCornerDeterminant, mapped.mapping.mapping_determinant);
const double determinant = totalJacobian.Det();
const double smallest = totalJacobian.CalcSingularvalue(2);
const double largest = totalJacobian.CalcSingularvalue(0);
if (!(determinant > 0.0) || !std::isfinite(determinant) || !(smallest > 0.0) ||
!std::isfinite(smallest) || !std::isfinite(largest)) ++invalidCornerSamples;
else maximumCornerCondition = std::max(maximumCornerCondition, largest / smallest);
}
}
}
if (!(area > 0.0L) || !std::isfinite(area)) throw std::runtime_error("No finite positive stellar surface area.");
return {{"surface_samples", static_cast<double>(boundarySamples)}, {"surface_area", static_cast<double>(area)},
{"surface_radius_minimum", minimumRadius}, {"surface_radius_maximum", maximumRadius},
{"surface_radius_area_mean", static_cast<double>(radiusIntegral / area)},
{"surface_radius_relative_rms_error", static_cast<double>(std::sqrt(radiusError / area) / reference.radius)},
{"surface_radius_relative_range", (maximumRadius - minimumRadius) / reference.radius},
{"surface_enthalpy_maximum_scaled", maximumSurfaceEnthalpy},
{"surface_potential_maximum_scaled_error", maximumSurfacePotentialError},
{"stellar_corner_samples", static_cast<double>(cornerSamples)},
{"invalid_stellar_corner_samples", static_cast<double>(invalidCornerSamples)},
{"minimum_stellar_corner_mapping_determinant", minimumCornerDeterminant},
{"maximum_stellar_corner_element_condition", maximumCornerCondition}};
}
} // namespace experiment::polytrope_validation

View File

@@ -0,0 +1,454 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <map>
#include <numbers>
#include <ranges>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
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<double>(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<const mean_field::operators::RootBlockDescriptor> 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<double>(block.canonicalIndex + 1);
values(index) = std::sin(0.6180339887498948 * static_cast<double>(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<double> * dimensionlessMass);
const double polytropicConstant = std::numbers::pi_v<double> * utils::G * std::pow(massScale, 2.0 / 3.0);
const double radialScale = stellarRadius / surfaceCoordinate;
const double centralDensity =
std::pow(polytropicConstant / (std::numbers::pi_v<double> * 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, std::move(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 auto 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<double>(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<std::string, double> solveMetrics{
{"solver_converged", solveMeasurement.solverConverged ? 1.0 : 0.0},
{"outer_iterations", static_cast<double>(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<double>(solveMeasurement.jacobian.applications)},
{"jacobian_application_seconds", solveMeasurement.jacobian.totalSeconds},
{"jacobian_maximum_application_seconds", solveMeasurement.jacobian.maximumSeconds},
{"inverse_preconditioner_applications",
static_cast<double>(solveMeasurement.inversePreconditioner.applications)},
{"inverse_preconditioner_application_seconds", solveMeasurement.inversePreconditioner.totalSeconds},
{"inverse_preconditioner_maximum_application_seconds", solveMeasurement.inversePreconditioner.maximumSeconds},
{"inverse_preconditioner_setups", static_cast<double>(solveMeasurement.inversePreconditionerLifecycle.setups)},
{"inverse_preconditioner_refreshes",
static_cast<double>(solveMeasurement.inversePreconditionerLifecycle.refreshes)},
{"inverse_preconditioner_setup_seconds", solveMeasurement.inversePreconditionerLifecycle.setupSeconds},
{"inverse_preconditioner_refresh_seconds", solveMeasurement.inversePreconditionerLifecycle.refreshSeconds},
{"prepared_residual_assemblies_during_solve",
static_cast<double>(statisticsAfterSolve.residualAssemblies - statisticsBeforeSolve.residualAssemblies)},
{"prepared_geometry_builds_during_solve",
static_cast<double>(
statisticsAfterSolve.generatedGeometryBuilds - statisticsBeforeSolve.generatedGeometryBuilds
)},
{"prepared_jacobian_applications_during_solve",
static_cast<double>(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<double>(sample)},
{"iteration", static_cast<double>(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<double>(spectrum.requestedDimension)},
{"achieved_krylov_dimension", static_cast<double>(spectrum.achievedDimension)},
{"invariant_subspace_found", spectrum.invariantSubspaceFound ? 1.0 : 0.0},
{"operator_applications", static_cast<double>(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<double>(spectrum.convergedRitzValueCount)},
{"negative_real_part_ritz_values", static_cast<double>(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<double>(instrumentedJacobian.GetStatistics().applications)},
{"measured_jacobian_application_seconds", instrumentedJacobian.GetStatistics().totalSeconds},
{"measured_jacobian_maximum_application_seconds", instrumentedJacobian.GetStatistics().maximumSeconds},
{"measured_inverse_preconditioner_applications",
static_cast<double>(instrumentedPreconditioner.GetStatistics().applications)},
{"measured_inverse_preconditioner_application_seconds",
instrumentedPreconditioner.GetStatistics().totalSeconds}}
);
std::vector<solver::RitzValueMeasurement> 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<double>(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';
}
}

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Refine a saved STROID mesh once, retaining the original and provenance.
Run with a Python environment containing the multiblock-capable STROID build.
The output is a complete, already-refined mesh: pass it to the validation
experiment without any further refinement, including during saved-field replay.
"""
import argparse
import hashlib
import json
from pathlib import Path
import sys
import stroid
from stroid import _stroid
from stroid.IO import LoadStroidMesh, SaveStroidMesh
from stroid.refinement import UniformRefinement
def digest(path):
with Path(path).open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def counts(mesh):
result = stroid.stats.ComputeMeshStats(
mesh, stroid.stats.MeshStatFeatures.ELEMENT_COUNT
).element_counts
return {name: getattr(result, name) for name in
("total", "core", "envelope", "vacuum", "other")}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("output_directory", type=Path,
help="New directory; existing directories are refused")
args = parser.parse_args()
source = args.input.resolve(strict=True)
original_digest = digest(source)
mesh = LoadStroidMesh(str(source))
if not hasattr(mesh.config, "core_mapping"):
raise RuntimeError("STROID is too old: no core_mapping support")
if not mesh.has_mesh() or not mesh.has_rmesh():
raise RuntimeError("Both physical and logical reference meshes are required")
before = counts(mesh)
initial_level = mesh.refinement_levels
mapping = mesh.config.core_mapping
order = mesh.config.order
args.output_directory.mkdir(exist_ok=False)
print(f"Loaded {mesh}; mapping={mapping}, geometry order={order}, "
f"stored refinement level={initial_level}", flush=True)
# This is an ADDITIONAL level on the loaded mesh, not regeneration from
# MeshConfig.refinement_levels. STROID rebuilds the high-order geometry.
UniformRefinement(mesh, 1)
after = counts(mesh)
if mesh.refinement_levels != initial_level + 1:
raise RuntimeError("STROID did not advance exactly one refinement level")
if any(after[name] != 8 * count for name, count in before.items()):
raise RuntimeError(f"Expected eight children per hex: {before} -> {after}")
if mesh.config.core_mapping != mapping or mesh.config.order != order:
raise RuntimeError("Refinement changed mapping strategy or geometry order")
target = args.output_directory / "refined.smesh"
SaveStroidMesh(mesh, str(target), "One additional UniformRefinement of " + str(source))
reloaded = LoadStroidMesh(str(target))
if counts(reloaded) != after or reloaded.refinement_levels != initial_level + 1:
raise RuntimeError("Saved refinement failed its load/metadata round trip")
if reloaded.config.core_mapping != mapping or reloaded.config.order != order:
raise RuntimeError("Saved refinement lost its mapping/order configuration")
if digest(source) != original_digest:
raise RuntimeError("Input mesh changed during the operation")
provenance = {
"source": str(source), "source_sha256": original_digest,
"refined_mesh": str(target.resolve()), "refined_sha256": digest(target),
"operation": "stroid.refinement.UniformRefinement(loaded_mesh, 1)",
"additional_levels": 1, "initial_level": initial_level,
"final_level": mesh.refinement_levels,
"geometry_order": order, "core_mapping": mapping,
"counts_before": before, "counts_after": after,
"python": sys.executable, "stroid_version": stroid.__version__,
"stroid_extension": _stroid.__file__,
"stroid_extension_sha256": digest(_stroid.__file__),
"geometry_policy": "STROID reprojects refined logical geometry; not fixed physical-polynomial subdivision",
"downstream_extra_refinements": 0,
}
with (args.output_directory / "refinement.json").open("x") as stream:
json.dump(provenance, stream, indent=2)
stream.write("\n")
print(f"Saved {mesh} to {target}; load with extra refinement=0", flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,529 @@
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <stdexcept>
#include <string>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
import experiment;
import experiment.stellar_null_space;
import mean_field;
import test_helpers;
namespace {
struct DeterminantPolynomial final {
double linear{0.0};
double quadratic{0.0};
double cubic{0.0};
};
struct CriticalAmplitude final {
double magnitude{0.0};
double determinant{1.0};
bool searchLimitReached{false};
};
struct SymmetricFiniteDifferenceStep final {
double step{0.0};
double positiveMinimumDeterminant{0.0};
double negativeMinimumDeterminant{0.0};
};
struct PolynomialRoots final {
std::array<double, 3> values{
std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::quiet_NaN()
};
int count{0};
};
[[nodiscard]] double relative_difference(
const mfem::Vector &computed,
const mfem::Vector &reference,
const MPI_Comm communicator
) {
mfem::Vector difference(computed);
difference -= reference;
const double scale = std::max(
{experiment::null_space::global_norm(computed, communicator),
experiment::null_space::global_norm(reference, communicator), std::numeric_limits<double>::epsilon()}
);
return experiment::null_space::global_norm(difference, communicator) / scale;
}
void add_block_metrics(
std::map<
std::string,
double> &metrics,
const std::string &prefix,
const std::array<
double,
6> &norms
) {
for (std::size_t block = 0; block < norms.size(); ++block) {
metrics.emplace(prefix + experiment::null_space::residualBlockNames[block] + "_norm", norms[block]);
}
}
[[nodiscard]] std::vector<DeterminantPolynomial> collect_determinant_polynomials(
const mean_field::fem::FEM &fem,
const mfem::Vector &unitVolumeDirection
) {
MFEM_VERIFY(fem.mesh->SpaceDimension() == 3, "The spherical-harmonic frequency probe requires 3D geometry.");
mfem::ParGridFunction displacement(fem.displacementFes.get());
displacement.SetFromTrueDofs(unitVolumeDirection);
std::vector<DeterminantPolynomial> polynomials;
polynomials.reserve(static_cast<std::size_t>(fem.mesh->GetNE()) * 64);
for (int element = 0; element < fem.mesh->GetNE(); ++element) {
mfem::ElementTransformation *transformation = fem.mesh->GetElementTransformation(element);
const mfem::FiniteElement *finiteElement = fem.displacementFes->GetFE(element);
const int integrationOrder =
std::max(finiteElement->GetOrder() + 2, 2 * fem.mesh->SpaceDimension() * finiteElement->GetOrder());
const mfem::IntegrationRule &rule = mfem::IntRules.Get(transformation->GetGeometryType(), integrationOrder);
for (int point = 0; point < rule.GetNPoints(); ++point) {
transformation->SetIntPoint(&rule.IntPoint(point));
mfem::DenseMatrix gradient;
displacement.GetVectorGradient(*transformation, gradient);
double trace = 0.0;
double traceSquared = 0.0;
for (int row = 0; row < 3; ++row) {
trace += gradient(row, row);
for (int column = 0; column < 3; ++column) {
traceSquared += gradient(row, column) * gradient(column, row);
}
}
polynomials.push_back(
{.linear = trace, .quadratic = 0.5 * (trace * trace - traceSquared), .cubic = gradient.Det()}
);
}
}
return polynomials;
}
[[nodiscard]] double global_minimum_determinant(
const std::vector<DeterminantPolynomial> &polynomials,
const double amplitude,
const MPI_Comm communicator
) {
double localMinimum = std::numeric_limits<double>::infinity();
for (const DeterminantPolynomial &polynomial : polynomials) {
const double determinant =
1.0 +
amplitude * (polynomial.linear + amplitude * (polynomial.quadratic + amplitude * polynomial.cubic));
localMinimum = std::min(localMinimum, determinant);
}
double globalMinimum = std::numeric_limits<double>::infinity();
MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator);
return globalMinimum;
}
[[nodiscard]] double evaluate(
const DeterminantPolynomial &polynomial,
const double amplitude
) {
return 1.0 +
amplitude * (polynomial.linear + amplitude * (polynomial.quadratic + amplitude * polynomial.cubic));
}
void append_root(
PolynomialRoots &roots,
const double root
) {
if (roots.count < static_cast<int>(roots.values.size()) && std::isfinite(root)) {
roots.values[static_cast<std::size_t>(roots.count++)] = root;
}
}
[[nodiscard]] PolynomialRoots real_roots(const DeterminantPolynomial &polynomial) {
PolynomialRoots roots;
const double coefficientScale =
std::max({1.0, std::abs(polynomial.linear), std::abs(polynomial.quadratic), std::abs(polynomial.cubic)});
const double tolerance = 64.0 * std::numeric_limits<double>::epsilon() * coefficientScale;
if (std::abs(polynomial.cubic) <= tolerance) {
if (std::abs(polynomial.quadratic) <= tolerance) {
if (std::abs(polynomial.linear) > tolerance) {
append_root(roots, -1.0 / polynomial.linear);
}
return roots;
}
const double discriminant = polynomial.linear * polynomial.linear - 4.0 * polynomial.quadratic;
const double discriminantTolerance =
64.0 * std::numeric_limits<double>::epsilon() * std::max(1.0, polynomial.linear * polynomial.linear);
if (discriminant < -discriminantTolerance) {
return roots;
}
const double squareRoot = std::sqrt(std::max(0.0, discriminant));
const double stableNumerator = -0.5 * (polynomial.linear + std::copysign(squareRoot, polynomial.linear));
if (stableNumerator == 0.0) {
append_root(roots, -polynomial.linear / (2.0 * polynomial.quadratic));
} else {
append_root(roots, stableNumerator / polynomial.quadratic);
if (squareRoot > std::sqrt(discriminantTolerance)) {
append_root(roots, 1.0 / stableNumerator);
}
}
return roots;
}
const double quadratic = polynomial.quadratic / polynomial.cubic;
const double linear = polynomial.linear / polynomial.cubic;
const double constant = 1.0 / polynomial.cubic;
const double depressedLinear = linear - quadratic * quadratic / 3.0;
const double depressedConstant =
2.0 * quadratic * quadratic * quadratic / 27.0 - quadratic * linear / 3.0 + constant;
const double halfConstant = 0.5 * depressedConstant;
const double thirdLinear = depressedLinear / 3.0;
const double discriminant = halfConstant * halfConstant + thirdLinear * thirdLinear * thirdLinear;
const double discriminantTolerance =
128.0 * std::numeric_limits<double>::epsilon() *
std::max({1.0, std::abs(halfConstant * halfConstant), std::abs(thirdLinear * thirdLinear * thirdLinear)});
const double shift = quadratic / 3.0;
if (discriminant > discriminantTolerance) {
const double squareRoot = std::sqrt(discriminant);
append_root(roots, std::cbrt(-halfConstant + squareRoot) + std::cbrt(-halfConstant - squareRoot) - shift);
} else if (std::abs(depressedLinear) <= tolerance || thirdLinear >= 0.0) {
append_root(roots, std::cbrt(-depressedConstant) - shift);
} else {
const double radius = 2.0 * std::sqrt(std::max(0.0, -thirdLinear));
const double cosineArgument = std::clamp(
-halfConstant / std::sqrt(std::max(0.0, -thirdLinear * thirdLinear * thirdLinear)), -1.0, 1.0
);
const double phase = std::acos(cosineArgument) / 3.0;
constexpr double twoPiOverThree = 2.0943951023931954923;
for (int root = 0; root < 3; ++root) {
append_root(roots, radius * std::cos(phase - twoPiOverThree * static_cast<double>(root)) - shift);
}
}
for (int root = 0; root < roots.count; ++root) {
double &value = roots.values[static_cast<std::size_t>(root)];
for (int iteration = 0; iteration < 3; ++iteration) {
const double derivative =
polynomial.linear + value * (2.0 * polynomial.quadratic + 3.0 * value * polynomial.cubic);
if (std::abs(derivative) <= tolerance) {
break;
}
value -= evaluate(polynomial, value) / derivative;
}
}
return roots;
}
[[nodiscard]] CriticalAmplitude find_critical_amplitude(
const std::vector<DeterminantPolynomial> &polynomials,
const double sign,
const MPI_Comm communicator
) {
constexpr double maximumSearchMagnitude = 0.5;
MFEM_VERIFY(sign == 1.0 || sign == -1.0, "The critical-amplitude direction must be positive or negative.");
double localCriticalMagnitude = std::numeric_limits<double>::infinity();
for (const DeterminantPolynomial &polynomial : polynomials) {
const PolynomialRoots roots = real_roots(polynomial);
for (int root = 0; root < roots.count; ++root) {
const double signedMagnitude = sign * roots.values[static_cast<std::size_t>(root)];
if (signedMagnitude > 0.0) {
localCriticalMagnitude = std::min(localCriticalMagnitude, signedMagnitude);
}
}
}
double globalCriticalMagnitude = std::numeric_limits<double>::infinity();
MPI_Allreduce(&localCriticalMagnitude, &globalCriticalMagnitude, 1, MPI_DOUBLE, MPI_MIN, communicator);
if (!std::isfinite(globalCriticalMagnitude) || globalCriticalMagnitude > maximumSearchMagnitude) {
return {
.magnitude = maximumSearchMagnitude,
.determinant = global_minimum_determinant(polynomials, sign * maximumSearchMagnitude, communicator),
.searchLimitReached = true
};
}
return {
.magnitude = globalCriticalMagnitude,
.determinant = global_minimum_determinant(polynomials, sign * globalCriticalMagnitude, communicator),
.searchLimitReached = false
};
}
[[nodiscard]] SymmetricFiniteDifferenceStep find_symmetric_finite_difference_step(
const mean_field::deformation::PreparedDomainDeformationRuntime &deformation,
const mfem::Vector &unitVolumeDirection
) {
constexpr double requestedStep = 1.0e-4;
constexpr double minimumStep = 1.0e-10;
mfem::Vector trialVolumeDirection(unitVolumeDirection.Size());
for (double step = requestedStep; step >= minimumStep; step *= 0.25) {
trialVolumeDirection = unitVolumeDirection;
trialVolumeDirection *= step;
const mean_field::deformation::DomainDeformationGeometryReport positive =
deformation.inspectMappedGeometry(trialVolumeDirection);
trialVolumeDirection *= -1.0;
const mean_field::deformation::DomainDeformationGeometryReport negative =
deformation.inspectMappedGeometry(trialVolumeDirection);
if (positive.isOrientationPreserving() && negative.isOrientationPreserving()) {
return {
.step = step,
.positiveMinimumDeterminant = positive.minimumJacobianDeterminant,
.negativeMinimumDeterminant = negative.minimumJacobianDeterminant
};
}
}
throw std::domain_error(
"No symmetric orientation-preserving finite-difference step was found for the surface mode."
);
}
} // namespace
TEST_CASE(
"Reduced Surface Mode Reachability And Stellar Equilibrium Linearization",
"[null_space][surface_modes][reachability][linearization]"
) {
mean_field::utils::Args args = test_utils::setup_args();
args.p.rtol = 1.0e-12;
args.p.atol = std::min(args.p.atol, 1.0e-14);
args.p.max_iters = std::max(args.p.max_iters, 2000);
experiment::null_space::N3Equilibrium fixture(std::move(args));
const MPI_Comm communicator = fixture.fem().mesh->GetComm();
int rank = 0;
MPI_Comm_rank(communicator, &rank);
const auto modes = experiment::null_space::make_surface_modes(fixture);
constexpr std::array<double, 2> rotationFractions{0.0, 0.5};
const int totalCases = static_cast<int>(rotationFractions.size() * modes.size());
int completedCases = 0;
for (const double rotationFraction : rotationFractions) {
const mean_field::physics::RigidRotation rotation = fixture.rotation(rotationFraction);
fixture.prepare(fixture.state(), rotation);
const mfem::Vector baseResidual = fixture.residual();
REQUIRE(std::isfinite(experiment::null_space::global_norm(baseResidual, communicator)));
for (const experiment::null_space::SurfaceMode &mode : modes) {
experiment::null_space::report_progress(
communicator, "probing " + mode.name + " at rotation fraction " + std::to_string(rotationFraction) +
" (" + std::to_string(completedCases + 1) + "/" + std::to_string(totalCases) + ")"
);
fixture.prepare(fixture.state(), rotation);
const mfem::Vector action = fixture.jacobian_action(mode.direction);
const mfem::Vector liftedDirection = fixture.lifted_surface_direction(mode.direction);
const double inputNorm = experiment::null_space::global_norm(mode.direction, communicator);
const double actionNorm = experiment::null_space::global_norm(action, communicator);
const double liftNorm = experiment::null_space::global_norm(liftedDirection, communicator);
const SymmetricFiniteDifferenceStep coarseStep = find_symmetric_finite_difference_step(
fixture.stellar_operator().GetDomainDeformation(), liftedDirection
);
const std::array<double, 2> finiteDifferenceSteps{coarseStep.step, 1.0e-2 * coarseStep.step};
REQUIRE(inputNorm > 0.0);
REQUIRE(liftNorm > 0.0);
REQUIRE(std::isfinite(actionNorm));
std::map<std::string, double> metrics{
{"surface_parameter_input_norm", inputNorm},
{"lifted_volume_displacement_norm", liftNorm},
{"lift_amplification", liftNorm / inputNorm},
{"root_jacobian_action_norm", actionNorm},
{"root_action_per_surface_parameter_norm", actionNorm / inputNorm},
{"root_action_per_lifted_volume_norm", actionNorm / liftNorm},
{"base_residual_norm", experiment::null_space::global_norm(baseResidual, communicator)},
{"surface_parameter_count",
static_cast<double>(fixture.stellar_operator().GetDomainDeformation().parameterCount())},
{"volume_displacement_count",
static_cast<double>(fixture.stellar_operator().GetDomainDeformation().volumeDisplacementSize())},
{"finite_difference_coarse_step", finiteDifferenceSteps[0]},
{"finite_difference_fine_step", finiteDifferenceSteps[1]},
{"coarse_step_positive_minimum_determinant", coarseStep.positiveMinimumDeterminant},
{"coarse_step_negative_minimum_determinant", coarseStep.negativeMinimumDeterminant}
};
add_block_metrics(
metrics, "root_",
experiment::null_space::residual_block_norms(
action, fixture.stellar_operator().GetLayout(), communicator
)
);
for (const double step : finiteDifferenceSteps) {
mfem::Vector plusState(fixture.state());
plusState.Add(step, mode.direction);
fixture.prepare(plusState, rotation);
const mfem::Vector plusResidual = fixture.residual();
mfem::Vector minusState(fixture.state());
minusState.Add(-step, mode.direction);
fixture.prepare(minusState, rotation);
const mfem::Vector minusResidual = fixture.residual();
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference /= 2.0 * step;
const std::string stepName = step == finiteDifferenceSteps.front() ? "coarse" : "fine";
metrics.emplace(
"finite_difference_relative_error_" + stepName,
relative_difference(action, finiteDifference, communicator)
);
}
fixture.prepare(fixture.state(), rotation);
if (rank == 0) {
experiment::record_experiment_result(
"reduced_surface_mode_reachability", mode.name,
{{"mode_kind", experiment::null_space::surface_mode_kind_name(mode.kind)},
{"axis", std::to_string(mode.axis)},
{"rotation_fraction_of_keplerian", std::to_string(rotationFraction)},
{"mesh_file", test_utils::setup_args().mesh_file},
{"local_state_dofs", std::to_string(fixture.stellar_operator().Width())}},
std::move(metrics)
);
}
++completedCases;
experiment::null_space::report_progress(
communicator, "completed " + std::to_string(completedCases) + "/" + std::to_string(totalCases) +
" reduced surface-mode cases"
);
}
}
experiment::null_space::report_progress(communicator, "reduced surface-mode probe complete; writing CSV output");
}
TEST_CASE(
"Spherical Harmonic Surface Frequencies Preserve Orientation Up To Measured Critical Amplitudes",
"[surface_modes][frequency_limit][geometry][spherical_harmonic]"
) {
mean_field::utils::Args args = test_utils::setup_args();
mean_field::fem::FEM fem = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(fem.okay());
experiment::null_space::Model model = experiment::null_space::make_model();
auto deformation = model.compileDomainDeformation(fem);
const auto &surface = deformation.surfaceDeformationPrescription();
const MPI_Comm communicator = fem.mesh->GetComm();
int rank = 0;
MPI_Comm_rank(communicator, &rank);
constexpr std::array<int, 13> angularDegrees{0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20};
mfem::Vector zeroParameters(surface.parameterCount());
zeroParameters = 0.0;
for (std::size_t degreeIndex = 0; degreeIndex < angularDegrees.size(); ++degreeIndex) {
const int angularDegree = angularDegrees[degreeIndex];
experiment::null_space::report_progress(
communicator, "measuring zonal spherical-harmonic degree " + std::to_string(angularDegree) + " (" +
std::to_string(degreeIndex + 1) + "/" + std::to_string(angularDegrees.size()) + ")"
);
mfem::Vector parameters(surface.parameterCount());
double localMaximumAngularMagnitude = 0.0;
for (int parameter = 0; parameter < parameters.Size(); ++parameter) {
const double angularValue =
experiment::null_space::zonal_legendre(angularDegree, surface.radialDirection(parameter, 2));
parameters(parameter) = surface.referenceRadius(parameter) * angularValue;
localMaximumAngularMagnitude = std::max(localMaximumAngularMagnitude, std::abs(angularValue));
}
double globalMaximumAngularMagnitude = 0.0;
MPI_Allreduce(
&localMaximumAngularMagnitude, &globalMaximumAngularMagnitude, 1, MPI_DOUBLE, MPI_MAX, communicator
);
REQUIRE(globalMaximumAngularMagnitude > 0.0);
parameters /= globalMaximumAngularMagnitude;
mfem::Vector unitVolumeDirection(deformation.volumeDisplacementSize());
deformation.applyJacobian(zeroParameters, parameters, unitVolumeDirection);
const std::vector<DeterminantPolynomial> determinantPolynomials =
collect_determinant_polynomials(fem, unitVolumeDirection);
long long localSampleCount = static_cast<long long>(determinantPolynomials.size());
long long globalSampleCount = 0;
MPI_Allreduce(&localSampleCount, &globalSampleCount, 1, MPI_LONG_LONG, MPI_SUM, communicator);
REQUIRE(globalSampleCount > 0);
const CriticalAmplitude positiveCritical = find_critical_amplitude(determinantPolynomials, 1.0, communicator);
const CriticalAmplitude negativeCritical = find_critical_amplitude(determinantPolynomials, -1.0, communicator);
const double determinantPositive1e4 = global_minimum_determinant(determinantPolynomials, 1.0e-4, communicator);
const double determinantNegative1e4 = global_minimum_determinant(determinantPolynomials, -1.0e-4, communicator);
const double determinantPositive1e3 = global_minimum_determinant(determinantPolynomials, 1.0e-3, communicator);
const double determinantNegative1e3 = global_minimum_determinant(determinantPolynomials, -1.0e-3, communicator);
const double determinantPositive1e2 = global_minimum_determinant(determinantPolynomials, 1.0e-2, communicator);
const double determinantNegative1e2 = global_minimum_determinant(determinantPolynomials, -1.0e-2, communicator);
if (angularDegree == 12) {
mfem::Vector directInspectionDirection(unitVolumeDirection);
directInspectionDirection *= 1.0e-3;
const mean_field::deformation::DomainDeformationGeometryReport directInspection =
deformation.inspectMappedGeometry(directInspectionDirection);
const double comparisonScale = std::max(
{1.0, std::abs(directInspection.minimumJacobianDeterminant), std::abs(determinantPositive1e3)}
);
CHECK(
std::abs(directInspection.minimumJacobianDeterminant - determinantPositive1e3) <=
1.0e-11 * comparisonScale
);
}
REQUIRE(std::isfinite(positiveCritical.magnitude));
REQUIRE(std::isfinite(negativeCritical.magnitude));
REQUIRE(positiveCritical.magnitude > 0.0);
REQUIRE(negativeCritical.magnitude > 0.0);
if (rank == 0) {
experiment::record_experiment_result(
"spherical_harmonic_surface_frequency_limit", "zonal_l" + std::to_string(angularDegree),
{{"angular_degree", std::to_string(angularDegree)},
{"azimuthal_order", "0"},
{"positive_limit_censored", positiveCritical.searchLimitReached ? "true" : "false"},
{"negative_limit_censored", negativeCritical.searchLimitReached ? "true" : "false"},
{"mesh_file", test_utils::setup_args().mesh_file}},
{{"positive_critical_fractional_amplitude", positiveCritical.magnitude},
{"negative_critical_fractional_amplitude", negativeCritical.magnitude},
{"positive_critical_determinant", positiveCritical.determinant},
{"negative_critical_determinant", negativeCritical.determinant},
{"minimum_determinant_positive_1e-4", determinantPositive1e4},
{"minimum_determinant_negative_1e-4", determinantNegative1e4},
{"minimum_determinant_positive_1e-3", determinantPositive1e3},
{"minimum_determinant_negative_1e-3", determinantNegative1e3},
{"minimum_determinant_positive_1e-2", determinantPositive1e2},
{"minimum_determinant_negative_1e-2", determinantNegative1e2},
{"surface_parameter_norm", experiment::null_space::global_norm(parameters, communicator)},
{"lifted_volume_displacement_norm",
experiment::null_space::global_norm(unitVolumeDirection, communicator)},
{"global_geometry_sample_count", static_cast<double>(globalSampleCount)}}
);
}
}
experiment::null_space::report_progress(
communicator, "spherical-harmonic frequency-limit probe complete; writing CSV output"
);
}

View File

@@ -0,0 +1,531 @@
module;
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
export module experiment.stellar_null_space;
import mean_field;
import test_helpers;
export namespace experiment::null_space {
using Form = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using Model = mean_field::models::StellarModel<mean_field::models::structure::PolytropicStructure>;
constexpr auto densityValue =
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::density_field.mass_term);
constexpr auto surfaceDeformationValue = mean_field::utils::blocks::get_value_block<Form>(
mean_field::utils::blocks::surface_deformation_field.parameters_term
);
constexpr auto gravityGradientValue =
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialValue =
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto enthalpyValue =
mean_field::utils::blocks::get_value_block<Form>(mean_field::utils::blocks::enthalpy_field.specific_term);
constexpr auto bernoulliValue = mean_field::utils::blocks::get_value_block<Form>(
mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term
);
constexpr auto gravityGradientResidual =
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialResidual =
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto densityResidual =
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::density_field.mass_term);
constexpr auto surfaceShapeResidual = mean_field::utils::blocks::get_residual_block<Form>(
mean_field::utils::blocks::surface_deformation_field.shape_equilibrium_term
);
constexpr auto enthalpyResidual =
mean_field::utils::blocks::get_residual_block<Form>(mean_field::utils::blocks::enthalpy_field.specific_term);
constexpr auto massResidual = mean_field::utils::blocks::get_residual_block<Form>(
mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term
);
inline constexpr std::array<const char *, 6> residualBlockNames{"gravity_gradient", "gravity_potential", "closure",
"surface_shape", "hydrostatic", "mass"};
template <int index>
[[nodiscard]] mfem::Vector value_view(
mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::value_block<index> block
) {
return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block));
}
template <int index>
[[nodiscard]] mfem::Vector const_value_view(
const mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::value_block<index> block
) {
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + layout.offset(block), layout.size(block));
}
template <int index>
[[nodiscard]] mfem::Vector residual_view(
mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::residual_block<index> block
) {
return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block));
}
template <int index>
[[nodiscard]] mfem::Vector const_residual_view(
const mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::residual_block<index> block
) {
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + layout.offset(block), layout.size(block));
}
template <int index>
void assign_value_block(
mfem::Vector &vector,
const mean_field::operators::StellarEquilibriumLayout &layout,
const mean_field::utils::blocks::value_block<index> block,
const mfem::Vector &source
) {
MFEM_VERIFY(
source.Size() == layout.size(block), "Surface-mode experiment received a block with the wrong size."
);
value_view(vector, layout, block) = source;
}
[[nodiscard]] inline double global_norm(
const mfem::Vector &vector,
const MPI_Comm communicator
) {
const double localNormSquared = vector * vector;
double globalNormSquared = 0.0;
MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(globalNormSquared);
}
inline void report_progress(
const MPI_Comm communicator,
const std::string &message
) {
int rank = 0;
MPI_Comm_rank(communicator, &rank);
if (rank == 0) {
std::cout << "[reduced-surface experiment] " << message << std::endl;
}
}
[[nodiscard]] inline mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
return {
.discretization = {.identity = 2003, .revision = 1},
.density = {.identity = 2011, .revision = 1},
.surfaceDeformation = {.identity = 2017, .revision = 1},
.gravityGradient = {.identity = 2027, .revision = 1},
.gravityPotential = {.identity = 2029, .revision = 1},
.enthalpy = {.identity = 2039, .revision = 1},
.bernoulliConstant = {.identity = 2053, .revision = 1},
.rotation = {.identity = 2063, .revision = 1},
.targetMass = {.identity = 2069, .revision = 1}
};
}
inline void increment_state_revisions(mean_field::operators::StellarEquilibriumDependencies &dependencies) {
++dependencies.density.revision;
++dependencies.surfaceDeformation.revision;
++dependencies.gravityGradient.revision;
++dependencies.gravityPotential.revision;
++dependencies.enthalpy.revision;
++dependencies.bernoulliConstant.revision;
}
[[nodiscard]] inline mfem::Vector pack_gravity_state(
const mfem::Vector &density,
const mfem::Vector &displacement,
const mfem::Vector &gravityGradient,
const mfem::Vector &gravityPotential
) {
const std::array<int, 5> offsets{
0, density.Size(), density.Size() + displacement.Size(),
density.Size() + displacement.Size() + gravityGradient.Size(),
density.Size() + displacement.Size() + gravityGradient.Size() + gravityPotential.Size()
};
mfem::Vector packed(offsets.back());
mfem::Vector(packed.GetData() + offsets[0], density.Size()) = density;
mfem::Vector(packed.GetData() + offsets[1], displacement.Size()) = displacement;
mfem::Vector(packed.GetData() + offsets[2], gravityGradient.Size()) = gravityGradient;
mfem::Vector(packed.GetData() + offsets[3], gravityPotential.Size()) = gravityPotential;
return packed;
}
[[nodiscard]] inline Model make_model() {
const double pi = std::acos(-1.0);
const double targetMass = mean_field::utils::MASS;
constexpr double dimensionlessMass = 2.0182359509662283534;
const double polytropicConstant =
pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
return Model{
mean_field::models::structure::PolytropicStructure{
mean_field::eos::Polytrope{3.0, polytropicConstant}, targetMass
},
mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}}
};
}
class N3Equilibrium final {
public:
explicit N3Equilibrium(mean_field::utils::Args args)
: m_args(std::move(args)),
m_fem(
mean_field::fem::setup_fem(
m_args.mesh_file,
m_args,
0
)
),
m_model(make_model()),
m_operator(
m_fem,
*m_fem.domainMapperStateless,
m_model
),
m_state(m_operator.GetLayout().value_offsets().Last()),
m_dependencies(make_dependencies()) {
MFEM_VERIFY(m_fem.okay(), "The null-space experiment could not construct the finite-element problem.");
m_state = 0.0;
initialize_state();
}
[[nodiscard]] mean_field::fem::FEM &fem() noexcept {
return m_fem;
}
[[nodiscard]] const mean_field::fem::FEM &fem() const noexcept {
return m_fem;
}
[[nodiscard]] Model &model() noexcept {
return m_model;
}
[[nodiscard]] mean_field::operators::PreparedStellarEquilibriumOperator &stellar_operator() noexcept {
return m_operator;
}
[[nodiscard]] const mean_field::operators::PreparedStellarEquilibriumOperator &
stellar_operator() const noexcept {
return m_operator;
}
[[nodiscard]] const mfem::Vector &state() const noexcept {
return m_state;
}
[[nodiscard]] mean_field::physics::RigidRotation rotation(const double fractionOfKeplerian) const {
const double radius = mean_field::utils::RADIUS;
const double mass = mean_field::utils::MASS;
const double keplerianSpeed = std::sqrt(mean_field::utils::G * mass / (radius * radius * radius));
mfem::Vector angularVelocity(3);
angularVelocity = 0.0;
angularVelocity(2) = fractionOfKeplerian * keplerianSpeed;
mfem::Vector center(3);
center = 0.0;
return mean_field::physics::RigidRotation(angularVelocity, center);
}
void prepare(
const mfem::Vector &state,
const mean_field::physics::RigidRotation &rotation
) {
m_currentState = state;
increment_state_revisions(m_dependencies);
++m_dependencies.rotation.revision;
m_operator.Prepare(state, m_dependencies, rotation);
}
[[nodiscard]] mfem::Vector residual() const {
mfem::Vector result;
m_operator.BuildResidual(result);
return result;
}
[[nodiscard]] mfem::Vector jacobian_action(const mfem::Vector &direction) const {
mfem::Vector result;
m_operator.Mult(direction, result);
return result;
}
[[nodiscard]] mfem::Vector lifted_surface_direction(const mfem::Vector &rootDirection) const {
const auto &layout = m_operator.GetLayout();
const mfem::Vector surfaceDirection = const_value_view(rootDirection, layout, surfaceDeformationValue);
mfem::Vector volumeDirection(m_operator.GetDomainDeformation().volumeDisplacementSize());
m_operator.GetDomainDeformation().applyJacobian(
m_operator.GetSurfaceDeformationParameters(), surfaceDirection, volumeDirection
);
return volumeDirection;
}
private:
void initialize_state() {
report_progress(m_fem.mesh->GetComm(), "constructing the analytic n=3 Lane-Emden state");
constexpr double surfaceCoordinate = 6.8968486193769603755;
constexpr int radialSampleCount = 8192;
const double pi = std::acos(-1.0);
const double radius = mean_field::utils::RADIUS;
const double targetMass = mean_field::utils::MASS;
constexpr double dimensionlessMass = 2.0182359509662283534;
const double polytropicConstant =
pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0);
const double centralDensity =
std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / radius, 3.0);
const mean_field::models::structure::StructureSeed seed =
m_model.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = radialSampleCount});
const auto interpolate = [](const mfem::Vector &radii, const mfem::Vector &values, const double r) {
if (r <= radii(0)) {
return values(0);
}
const int finalIndex = radii.Size() - 1;
if (r >= radii(finalIndex)) {
return values(finalIndex);
}
int lower = 0;
int upper = finalIndex;
while (upper - lower > 1) {
const int middle = lower + (upper - lower) / 2;
if (radii(middle) <= r) {
lower = middle;
} else {
upper = middle;
}
}
const double fraction = (r - radii(lower)) / (radii(upper) - radii(lower));
return (1.0 - fraction) * values(lower) + fraction * values(upper);
};
mfem::FunctionCoefficient densityCoefficient([&seed, &interpolate](const mfem::Vector &position) {
const double r = position.Norml2();
return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.density, r);
});
mfem::FunctionCoefficient enthalpyCoefficient([&seed, &interpolate](const mfem::Vector &position) {
const double r = position.Norml2();
return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.enthalpy, r);
});
mfem::ParGridFunction densityField(m_fem.densityFes.get());
mfem::ParGridFunction enthalpyField(m_fem.enthalpyFes.get());
mfem::ParGridFunction displacementField(m_fem.displacementFes.get());
densityField = 0.0;
enthalpyField = 0.0;
displacementField = 0.0;
densityField.ProjectCoefficient(densityCoefficient);
enthalpyField.ProjectCoefficient(enthalpyCoefficient);
*m_fem.displacement = displacementField;
report_progress(m_fem.mesh->GetComm(), "solving the gravity field for the seed state");
const mean_field::physics::GravitySolution gravity =
mean_field::physics::solve_gravity_field(m_fem, m_args, densityField, displacementField);
mfem::Vector densityTrue;
mfem::Vector enthalpyTrue;
mfem::Vector gravityGradientTrue;
mfem::Vector gravityPotentialTrue;
densityField.GetTrueDofs(densityTrue);
enthalpyField.GetTrueDofs(enthalpyTrue);
gravity.gradPhi.GetTrueDofs(gravityGradientTrue);
gravity.phi.GetTrueDofs(gravityPotentialTrue);
const auto &layout = m_operator.GetLayout();
const mean_field::field::FieldDofMap densityMap =
mean_field::field::make_field_dof_map<mean_field::field::Density, DomainSchema>(*m_fem.densityFes);
const mean_field::field::FieldDofMap enthalpyMap =
mean_field::field::make_field_dof_map<mean_field::field::Enthalpy, DomainSchema>(*m_fem.enthalpyFes);
mfem::Vector surfaceParameters(layout.size(surfaceDeformationValue));
surfaceParameters = 0.0;
assign_value_block(m_state, layout, densityValue, densityMap.gather(densityTrue));
assign_value_block(m_state, layout, surfaceDeformationValue, surfaceParameters);
assign_value_block(m_state, layout, gravityGradientValue, gravityGradientTrue);
assign_value_block(m_state, layout, gravityPotentialValue, gravityPotentialTrue);
assign_value_block(m_state, layout, enthalpyValue, enthalpyMap.gather(enthalpyTrue));
value_view(m_state, layout, bernoulliValue)(0) = -mean_field::utils::G * targetMass / radius;
m_currentState = m_state;
prepare(m_state, rotation(0.0));
report_progress(m_fem.mesh->GetComm(), "analytic state is prepared");
}
mean_field::utils::Args m_args;
mean_field::fem::FEM m_fem;
Model m_model;
mean_field::operators::PreparedStellarEquilibriumOperator m_operator;
mfem::Vector m_state;
mfem::Vector m_currentState;
mean_field::operators::StellarEquilibriumDependencies m_dependencies;
};
enum class SurfaceModeKind : std::uint8_t {
uniform_radial,
translation_like_dipole,
oblate_quadrupole,
spherical_harmonic
};
struct SurfaceMode final {
std::string name;
SurfaceModeKind kind;
int axis;
mfem::Vector direction;
};
[[nodiscard]] inline const char *surface_mode_kind_name(const SurfaceModeKind kind) noexcept {
switch (kind) {
case SurfaceModeKind::uniform_radial:
return "uniform_radial";
case SurfaceModeKind::translation_like_dipole:
return "translation_like_dipole";
case SurfaceModeKind::oblate_quadrupole:
return "oblate_quadrupole";
case SurfaceModeKind::spherical_harmonic:
return "spherical_harmonic";
}
return "unknown";
}
[[nodiscard]] inline double zonal_legendre(
const int degree,
const double cosineOfPolarAngle
) {
MFEM_VERIFY(degree >= 0, "A zonal spherical-harmonic degree must be non-negative.");
const double coordinate = std::clamp(cosineOfPolarAngle, -1.0, 1.0);
if (degree == 0) {
return 1.0;
}
if (degree == 1) {
return coordinate;
}
double previousPrevious = 1.0;
double previous = coordinate;
for (int order = 2; order <= degree; ++order) {
const double current = ((2.0 * static_cast<double>(order) - 1.0) * coordinate * previous -
(static_cast<double>(order) - 1.0) * previousPrevious) /
static_cast<double>(order);
previousPrevious = previous;
previous = current;
}
return previous;
}
[[nodiscard]] inline std::vector<SurfaceMode> make_surface_modes(N3Equilibrium &fixture) {
const auto &layout = fixture.stellar_operator().GetLayout();
auto deformation = fixture.model().compileDomainDeformation(fixture.fem());
const auto &surface = deformation.surfaceDeformationPrescription();
MFEM_VERIFY(
surface.parameterCount() == layout.size(surfaceDeformationValue),
"The diagnostic surface prescription does not match the root surface block."
);
const auto make_root_direction = [&layout](const mfem::Vector &surfaceDirection) {
mfem::Vector direction(layout.value_offsets().Last());
direction = 0.0;
assign_value_block(direction, layout, surfaceDeformationValue, surfaceDirection);
return direction;
};
std::vector<SurfaceMode> modes;
modes.reserve(6);
mfem::Vector uniform(surface.parameterCount());
for (int parameter = 0; parameter < uniform.Size(); ++parameter) {
uniform(parameter) = surface.referenceRadius(parameter);
}
modes.push_back(
{.name = "uniform_radial_homology",
.kind = SurfaceModeKind::uniform_radial,
.axis = -1,
.direction = make_root_direction(uniform)}
);
for (int axis = 0; axis < surface.spatialDimension(); ++axis) {
mfem::Vector dipole(surface.parameterCount());
for (int parameter = 0; parameter < dipole.Size(); ++parameter) {
dipole(parameter) = surface.radialDirection(parameter, axis);
}
modes.push_back(
{.name = std::string("translation_like_dipole_") + static_cast<char>('x' + axis),
.kind = SurfaceModeKind::translation_like_dipole,
.axis = axis,
.direction = make_root_direction(dipole)}
);
}
mfem::Vector quadrupole(surface.parameterCount());
for (int parameter = 0; parameter < quadrupole.Size(); ++parameter) {
const double polarDirection = surface.radialDirection(parameter, 2);
quadrupole(parameter) = surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection);
}
modes.push_back(
{.name = "axisymmetric_oblate_quadrupole_z",
.kind = SurfaceModeKind::oblate_quadrupole,
.axis = 2,
.direction = make_root_direction(quadrupole)}
);
constexpr int diagnosticAngularDegree = 12;
mfem::Vector sphericalHarmonic(surface.parameterCount());
double localMaximumMagnitude = 0.0;
for (int parameter = 0; parameter < sphericalHarmonic.Size(); ++parameter) {
const double angularValue = zonal_legendre(diagnosticAngularDegree, surface.radialDirection(parameter, 2));
sphericalHarmonic(parameter) = surface.referenceRadius(parameter) * angularValue;
localMaximumMagnitude = std::max(localMaximumMagnitude, std::abs(angularValue));
}
double globalMaximumMagnitude = 0.0;
MPI_Allreduce(
&localMaximumMagnitude, &globalMaximumMagnitude, 1, MPI_DOUBLE, MPI_MAX, fixture.fem().mesh->GetComm()
);
MFEM_VERIFY(globalMaximumMagnitude > 0.0, "The spherical-harmonic surface mode has zero amplitude.");
sphericalHarmonic /= globalMaximumMagnitude;
modes.push_back(
{.name = "zonal_spherical_harmonic_l12",
.kind = SurfaceModeKind::spherical_harmonic,
.axis = -1,
.direction = make_root_direction(sphericalHarmonic)}
);
return modes;
}
[[nodiscard]] inline std::array<
double,
6>
residual_block_norms(
const mfem::Vector &action,
const mean_field::operators::StellarEquilibriumLayout &layout,
const MPI_Comm communicator
) {
return {
global_norm(const_residual_view(action, layout, gravityGradientResidual), communicator),
global_norm(const_residual_view(action, layout, gravityPotentialResidual), communicator),
global_norm(const_residual_view(action, layout, densityResidual), communicator),
global_norm(const_residual_view(action, layout, surfaceShapeResidual), communicator),
global_norm(const_residual_view(action, layout, enthalpyResidual), communicator),
global_norm(const_residual_view(action, layout, massResidual), communicator)
};
}
} // namespace experiment::null_space

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Read-only, standard-library summary of geometry_quality_experiment artifacts."""
import argparse
import csv
import math
from pathlib import Path
def rows(path):
if not path.exists():
return []
with path.open(newline="") as stream:
return list(csv.DictReader(stream))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("directory", type=Path)
args = parser.parse_args()
root = args.directory
print("GEOMETRY (boundaries are censored at alpha=1)")
for path in sorted(root.glob("*_geometry_elements.csv")):
data = rows(path)
limited = [r for r in data if r["limited"] == "1"]
if not limited:
print(path.stem, "no sampled boundary <=1")
continue
first = limited[0]
boundary = float(first["boundary_step"])
ties = [r["element"] for r in limited if float(r["boundary_step"]) <= boundary * (1 + 1e-6)]
print(path.stem, f"boundary={boundary:.10g}", f"limiter={first['element']}",
f"attr={first['attribute']}", f"ties_1ppm={','.join(ties)}",
f"radial_gradient={float(first['limiter_radial_gradient']):.7g}")
for key in first:
if "reference" in key and ("sigma" in key or "det" in key):
print(" ", key, first[key])
print("\nLINEAR SOLVES")
for row in rows(root / "solves.csv"):
print(row)
print("\nSURFACE (unweighted nodal fractional changes)")
for row in rows(root / "surface_summary.csv"):
print(row)
surface_rows = rows(root / "surface.csv")
for case in sorted({r["case"] for r in surface_rows}):
groups = {}
for row in surface_rows:
if row["case"] != case:
continue
radius = float(row["radius"])
key = tuple(sorted(round(abs(float(row[c]) / radius), 8) for c in ("x", "y", "z")))
groups.setdefault(key, []).append(float(row["correction_fraction"]))
print(case, "cubic_symmetry_groups=", len(groups), "max_within_group_spread=",
max((max(v) - min(v) for v in groups.values()), default=math.nan))
print("\nACCEPTED RESIDUAL BLOCKS")
for row in rows(root / "blocks.csv"):
if row["case"] == "accepted" and row["kind"] == "residual":
print(row["block"], "normalized_l2=" + row["normalized_l2"])
print("\nFINITE DIFFERENCES")
for row in rows(root / "finite_differences.csv"):
if float(row["action_norm"]) > 1e-12:
print(row["epsilon"], row["row"], "relative_error=" + row["relative_error"])
print("\nMAPPING CHECKS")
for path in sorted(root.glob("*_geometry_mapping_checks.csv")):
data = rows(path)
errors = [float(r["relative_mapping_matrix_error"]) for r in data]
errors = [v for v in errors if math.isfinite(v)]
print(path.stem, "max_matrix_error=", max(errors, default=math.nan),
"invalid_samples=", sum(not math.isfinite(float(r["direct_det"])) for r in data))
print("\nEXTENSION CHECKS")
for row in rows(root / "extension_checks.csv"):
print(row)
print("\nCORE DIAGONAL AT NEWTON LIMITER")
steps = {r["case"]: float(r["safe_step"]) for r in rows(root / "solves.csv")}
for path in sorted(root.glob("*_core_diagonal.csv")):
data = rows(path)
for row in data:
if abs(float(row["s"]) - 0.010885670926971493) < 1e-12:
print(path.stem, "actual_u=", row["actual_radial_displacement"],
"desired_u=", row["desired_radial_displacement"],
"actual_gradient=", row["actual_radial_gradient"],
"desired_gradient=", row["desired_radial_gradient"])
case = path.stem.removesuffix("_core_diagonal")
for alpha in sorted({1.0, steps.get(case, 1.0)}):
determinants = []
for row in data:
if "relative_det_coefficient_0" not in row:
continue
c = [float(row[f"relative_det_coefficient_{i}"]) for i in range(4)]
det = ((c[3] * alpha + c[2]) * alpha + c[1]) * alpha + c[0]
determinants.append((det, float(row["s"])))
if determinants:
print(" ", case, "alpha=", alpha, "min_diagonal_det_and_s=", min(determinants))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,287 @@
#!/usr/bin/env python3
"""Create a static SVG and Markdown summary of polytrope verification CSVs.
Uses only the Python standard library. Does not rerun the solver or alter input
CSVs; writes polytrope_profiles.svg and polytrope_summary.md in the input directory.
"""
import argparse
import csv
import html
import math
from pathlib import Path
FIELDS = (
("density_material", "theta_density", "density", "#2563eb"),
("enthalpy_material", "theta_enthalpy", "enthalpy", "#15803d"),
("potential", "theta_potential", "potential", "#c2410c"),
("gravity_radial", None, "radial gravity gradient", "#7e22ce"),
)
def read_rows(path):
with path.open(newline="") as stream:
return list(csv.DictReader(stream))
def number(value):
try:
return float(value)
except (TypeError, ValueError):
return math.nan
def read_metrics(directory):
return {row["metric"]: number(row["value"])
for row in read_rows(directory / "physical_metrics.csv")}
def metadata(directory):
path = directory / "metadata.txt"
if not path.exists():
return {}
return dict(line.split("=", 1) for line in path.read_text().splitlines() if "=" in line)
def finite_max(values):
return max((value for value in values if math.isfinite(value)), default=math.nan)
def format_number(value):
return f"{value:.5g}" if math.isfinite(value) else "not available"
def reference_scales(rows):
origin = next((row for row in rows if number(row.get("xi")) == 0.0), None)
if origin is None:
raise ValueError("radial_profiles.csv must contain its analytic origin reference")
density = number(origin["density_material_analytic"])
enthalpy = number(origin["enthalpy_material_analytic"])
nonzero = next(row for row in rows if number(row.get("xi")) > 0.0)
radius = math.pi * number(nonzero["radius"]) / number(nonzero["xi"])
if not all(math.isfinite(value) and value > 0.0 for value in (density, enthalpy, radius)):
raise ValueError("Non-finite/non-positive fixed analytic reference scales")
return {"density_material": density, "enthalpy_material": enthalpy,
"potential": enthalpy, "gravity_radial": enthalpy / radius,
"radius": radius}
def points(rows, column, scale=1.0, interior=False):
result = []
for row in rows:
xi = number(row.get("xi"))
if interior and xi > math.pi:
continue
result.append((xi, number(row.get(column)) / scale))
return result
def path_segments(data, xmap, ymap, logarithmic=False):
segments, current = [], []
for x, y in data:
if not math.isfinite(x) or not math.isfinite(y) or (logarithmic and y <= 0.0):
if current:
segments.append(current)
current = []
continue
current.append((xmap(x), ymap(y)))
if current:
segments.append(current)
return segments
class Figure:
def __init__(self, title):
self.parts = [
'<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="910" viewBox="0 0 1200 910" role="img">',
f"<title>{html.escape(title)}</title>",
'<desc>Fixed-reference n=1 physical-radius profiles, absolute mean errors, angular scatter, and sampling coverage.</desc>',
'<rect width="1200" height="910" fill="#ffffff"/>',
'<style>text{font-family:Arial,Helvetica,sans-serif;fill:#1f2937;font-size:12px}.title{font-size:22px;font-weight:bold}.panel{font-size:15px;font-weight:bold}.small{font-size:11px}</style>',
f'<text class="title" x="65" y="35">{html.escape(title)}</text>',
'<text x="65" y="58">Physical spheres; ξ = πr/R. Reference R and central scales are prescribed, never fitted.</text>',
]
def panel(self, ident, box, title, xlabel, ylabel, xlim, ylim, series, log=False):
left, top, width, height = box
xmap = lambda value: left + width * (value - xlim[0]) / (xlim[1] - xlim[0])
if log:
lower, upper = math.log10(ylim[0]), math.log10(ylim[1])
ymap = lambda value: top + height * (upper - math.log10(value)) / (upper - lower)
ticks = [(10.0 ** exponent, f"1e{exponent}")
for exponent in range(math.ceil(lower), math.floor(upper) + 1)]
if len(ticks) > 7:
ticks = ticks[::math.ceil(len(ticks) / 7)]
else:
ymap = lambda value: top + height * (ylim[1] - value) / (ylim[1] - ylim[0])
ticks = [(ylim[0] + i * (ylim[1] - ylim[0]) / 4.0,
f"{ylim[0] + i * (ylim[1] - ylim[0]) / 4.0:.2g}") for i in range(5)]
self.parts.append(f'<text class="panel" x="{left}" y="{top - 58}">{html.escape(title)}</text>')
self.parts.append(f'<defs><clipPath id="{ident}"><rect x="{left}" y="{top}" width="{width}" height="{height}"/></clipPath></defs>')
for value, label in ticks:
y = ymap(value)
self.parts.append(f'<line x1="{left}" x2="{left + width}" y1="{y:.3f}" y2="{y:.3f}" stroke="#e5e7eb"/>')
self.parts.append(f'<text x="{left - 9}" y="{y + 4:.3f}" text-anchor="end">{label}</text>')
for i in range(5):
value = xlim[0] + i * (xlim[1] - xlim[0]) / 4.0
x = xmap(value)
self.parts.append(f'<line x1="{x:.3f}" x2="{x:.3f}" y1="{top}" y2="{top + height}" stroke="#f1f5f9"/>')
self.parts.append(f'<text x="{x:.3f}" y="{top + height + 20}" text-anchor="middle">{value:.3g}</text>')
self.parts.append(f'<rect x="{left}" y="{top}" width="{width}" height="{height}" fill="none" stroke="#64748b"/>')
self.parts.append(f'<text x="{left + width / 2}" y="{top + height + 42}" text-anchor="middle">{html.escape(xlabel)}</text>')
self.parts.append(f'<text transform="translate({left - 57},{top + height / 2}) rotate(-90)" text-anchor="middle">{html.escape(ylabel)}</text>')
legend_index = 0
for item in series:
dash = ' stroke-dasharray="6 4"' if item.get("dash") else ""
for segment in path_segments(item["data"], xmap, ymap, log):
if len(segment) == 1:
x, y = segment[0]
self.parts.append(f'<circle clip-path="url(#{ident})" cx="{x:.3f}" cy="{y:.3f}" r="2" fill="{item["color"]}"/>')
else:
coordinates = " ".join(f"{x:.3f},{y:.3f}" for x, y in segment)
self.parts.append(f'<polyline clip-path="url(#{ident})" points="{coordinates}" fill="none" stroke="{item["color"]}" stroke-width="1.8"{dash}/>')
if item.get("label"):
x = left + (legend_index % 2) * width / 2
y = top - 35 + (legend_index // 2) * 17
self.parts.append(f'<line x1="{x}" x2="{x + 20}" y1="{y}" y2="{y}" stroke="{item["color"]}" stroke-width="2"{dash}/>')
self.parts.append(f'<text class="small" x="{x + 26}" y="{y + 4}">{html.escape(item["label"])}</text>')
legend_index += 1
def write(self, path, control):
note = "Solid: measured state. Dashed same-color: analytic-mesh control." if control else "Mean errors and scatter are separately scaled by fixed central/reference values."
self.parts.append(f'<text class="small" x="65" y="864">{html.escape(note)}</text>')
self.parts.append('<text class="small" x="65" y="884">Missing/nonfinite samples are not connected; zero errors are omitted on logarithmic axes. Material means are conditional.</text>')
self.parts.append("</svg>")
path.write_text("\n".join(self.parts) + "\n")
def logarithmic_limits(series):
values = [y for item in series for _, y in item["data"] if math.isfinite(y) and y > 0.0]
if not values:
return 1e-16, 1.0
lower = max(-300, math.floor(math.log10(min(values))))
upper = max(lower + 2, math.ceil(math.log10(max(values))))
return 10.0 ** lower, 10.0 ** upper
def make_figure(directory, rows, control_rows):
scales = reference_scales(rows)
control_scales = reference_scales(control_rows) if control_rows else {}
if control_rows:
for key in scales:
if not math.isclose(scales[key], control_scales[key], rel_tol=1e-12):
raise ValueError(f"Control and measured fixed-reference scales differ: {key}")
figure = Figure("n = 1 polytrope: physical profile verification")
analytic = [(math.pi * i / 300, math.sin(math.pi * i / 300) / (math.pi * i / 300) if i else 1.0)
for i in range(301)]
profile_series = [{"data": analytic, "color": "#111827", "label": "analytic sin(ξ)/ξ", "dash": True}]
for _, column, label, color in FIELDS[:3]:
profile_series.append({"data": points(rows, column, interior=True), "color": color, "label": label})
profile_values = [y for item in profile_series for _, y in item["data"] if math.isfinite(y)]
lo, hi = min(profile_values), max(profile_values)
padding = max(0.05, 0.05 * (hi - lo))
figure.panel("profiles", (85, 150, 470, 255), "Interior dimensionless profiles", "ξ = πr/R", "θ from each field",
(0.0, math.pi), (lo - padding, hi + padding), profile_series)
maximum_xi = finite_max(number(row["xi"]) for row in rows)
mean_series, scatter_series = [], []
for field, _, label, color in FIELDS:
mean_series.append({"data": [(x, abs(y)) for x, y in points(rows, field + "_mean_error_scaled")], "color": color, "label": label})
scatter_series.append({"data": points(rows, field + "_angular_rms", scales[field]), "color": color, "label": label})
if control_rows:
mean_series.append({"data": [(x, abs(y)) for x, y in points(control_rows, field + "_mean_error_scaled")], "color": color, "dash": True})
scatter_series.append({"data": points(control_rows, field + "_angular_rms", control_scales[field]), "color": color, "dash": True})
figure.panel("mean_errors", (690, 150, 440, 255), "Absolute spherical-mean error", "ξ = πr/R", "absolute error / fixed scale",
(0.0, maximum_xi), logarithmic_limits(mean_series), mean_series, log=True)
figure.panel("scatter", (85, 565, 470, 230), "Angular RMS about the spherical mean", "ξ = πr/R", "angular RMS / fixed scale",
(0.0, maximum_xi), logarithmic_limits(scatter_series), scatter_series, log=True)
coverage = [
{"data": points(rows, "located_weight_fraction"), "color": "#111827", "label": "point location"},
{"data": points(rows, "material_weight_fraction"), "color": "#64748b", "label": "stellar material"},
{"data": points(rows, "density_material_valid_weight_fraction"), "color": "#2563eb", "label": "finite density", "dash": True},
{"data": points(rows, "enthalpy_material_valid_weight_fraction"), "color": "#15803d", "label": "finite enthalpy", "dash": True},
]
figure.panel("coverage", (690, 565, 440, 230), "Sampling coverage (inspect before means)", "ξ = πr/R", "fraction of requested angular weight",
(0.0, maximum_xi), (-0.03, 1.03), coverage)
figure.write(directory / "polytrope_profiles.svg", bool(control_rows))
def make_report(directory, rows, metrics, checks, control_directory, control_metrics):
info = metadata(directory)
failed = [row for row in checks if row.get("passed", "").lower() not in ("1", "true")]
lines = ["# Polytrope physical verification", "", f"Source: `{directory.resolve()}`.", "",
f"Declared screening checks: **{len(checks) - len(failed)}/{len(checks)} passed**. "
"These budgets are not a mesh-convergence certificate.", ""]
if info:
lines.append(f"Mode: `{info.get('mode', 'unknown')}`. Solver convergence: `{info.get('solver_converged', 'not applicable/reported')}`.")
if "solver_failure" in info:
lines.extend(["", "Solver failure: " + info["solver_failure"]])
lines.append("")
lines.extend(["![Fixed-reference physical profiles, errors, angular scatter, and coverage](polytrope_profiles.svg)", "",
"## Main diagnostics", ""])
if control_directory:
lines.extend([f"Analytic-mesh control: `{control_directory.resolve()}`. Control errors are shown directly, not subtracted from numerical errors.", ""])
lines.append("| Metric | Measured |" + (" Analytic-mesh control |" if control_directory else ""))
lines.append("|---|---:|" + ("---:|" if control_directory else ""))
selected = ("mass", "mass_relative_error", "volume_radius_relative_error", "surface_radius_relative_rms_error",
"density_relative_l2_error", "enthalpy_relative_l2_error", "potential_relative_l2_error",
"gravity_gradient_relative_l2_error", "binding_energy", "pressure_integral", "virial_error",
"force_virial_error", "enthalpy_virial_error", "enthalpy_force_virial_error",
"gravity_energy_consistency", "eos_enthalpy_scaled_rms", "closure_projection_pressure_gap",
"closure_projection_pressure_gap_relative_defect",
"normalized_bordered_residual", "normalized_unbordered_residual", "normalized_central_border_action",
"invalid_stellar_corner_samples", "maximum_stellar_corner_element_condition",
"profile_missing_points", "profile_maximum_location_error", "profile_maximum_scaled_error",
"profile_maximum_angular_rms_scaled")
for key in selected:
if key not in metrics:
continue
row = f"| `{key}` | {format_number(metrics[key])} |"
if control_directory:
row += f" {format_number(control_metrics.get(key, math.nan))} |"
lines.append(row)
lines.extend(["", "## Radial-profile diagnostics", "",
"| Field | Max absolute mean error / fixed scale | Max angular RMS / fixed scale |", "|---|---:|---:|"])
scales = reference_scales(rows)
for field, _, label, _ in FIELDS:
mean_error = finite_max(abs(number(row.get(field + "_mean_error_scaled"))) for row in rows)
scatter = finite_max(number(row.get(field + "_angular_rms")) / scales[field] for row in rows)
lines.append(f"| {label} | {format_number(mean_error)} | {format_number(scatter)} |")
incomplete = [row for row in rows if number(row.get("located_weight_fraction")) < 1.0 - 1e-10]
partial_material = [row for row in rows if 1e-10 < number(row.get("material_weight_fraction")) < 1.0 - 1e-10]
lines.extend(["", f"Shells with incomplete point-location coverage: **{len(incomplete)}**. "
f"Shells crossing the numerical material boundary: **{len(partial_material)}**.", "",
"Density/enthalpy means are conditional on located stellar material and finite values. "
"Missing samples and undefined exterior material fields are never zero-filled. "
"Angular RMS exposes nonspherical variation that a spherical mean can hide. "
"Origin data are single traces; radial gravity is undefined there. "
"Gravity is outward-positive ∇Φ, not inward acceleration.", ""])
if failed:
lines.extend(["## Failed declared screens", "", "| Metric | Observed | Maximum allowed |", "|---|---:|---:|"])
for row in failed:
lines.append(f"| `{row['metric']}` | {format_number(number(row['observed']))} | {format_number(number(row['maximum_allowed']))} |")
lines.append("")
lines.append("All plotted normalizations use the prescribed analytic reference. No radius, central density, or potential offset is fitted.")
(directory / "polytrope_summary.md").write_text("\n".join(lines) + "\n")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("directory", type=Path)
parser.add_argument("--control-directory", type=Path)
args = parser.parse_args()
rows = read_rows(args.directory / "radial_profiles.csv")
if not rows:
parser.error("radial_profiles.csv contains no rows")
metrics = read_metrics(args.directory)
checks = read_rows(args.directory / "verification_checks.csv")
control_rows = read_rows(args.control_directory / "radial_profiles.csv") if args.control_directory else []
control_metrics = read_metrics(args.control_directory) if args.control_directory else {}
make_figure(args.directory, rows, control_rows)
make_report(args.directory, rows, metrics, checks, args.control_directory, control_metrics)
print(args.directory / "polytrope_profiles.svg")
print(args.directory / "polytrope_summary.md")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,50 @@
add_library(mean_field_extension_example)
target_sources(
mean_field_extension_example
PUBLIC
FILE_SET CXX_MODULES FILES
ideal_gas_radiation.cppm
rotating_stellar_model.cppm
)
target_link_libraries(mean_field_extension_example PUBLIC mean_field)
add_executable(extension_example_demo demo.cpp)
target_link_libraries(extension_example_demo PRIVATE mean_field_extension_example)
add_executable(
extension_example_tests
tests/ideal_gas_radiation.cpp
tests/rotating_stellar_model.cpp
)
target_link_libraries(
extension_example_tests
PRIVATE
mean_field_extension_example
Catch2::Catch2WithMain
)
catch_discover_tests(
extension_example_tests
TEST_PREFIX "extension_example::"
PROPERTIES LABELS "extension-example"
)
find_program(LATEXMK_EXECUTABLE latexmk)
if (LATEXMK_EXECUTABLE)
add_custom_target(
extension_example_manual
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/manual"
COMMAND
${LATEXMK_EXECUTABLE}
-pdf
-interaction=nonstopmode
-halt-on-error
-outdir=${CMAKE_CURRENT_BINARY_DIR}/manual
"${CMAKE_CURRENT_SOURCE_DIR}/manual/physics_developer_manual.tex"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/manual"
COMMENT "Compiling the MeanField physics developer manual"
VERBATIM
)
endif ()

View File

@@ -0,0 +1,55 @@
# MeanField physics extension example
This directory is a small, isolated example for physicists who want to extend
MeanField without first learning its internal block-matrix machinery.
Start in this order:
1. Read `ideal_gas_radiation.cppm`. It implements a monatomic ideal gas plus
equilibrium radiation using the public EOS relation protocol.
2. Read `rotating_stellar_model.cppm`. It composes that EOS with the existing
isobaric surface, fixed-total-mass invariant, and fixed-angular-momentum
invariant.
3. Read and run `demo.cpp`.
4. Read the tests. They show which claims should be compile-time contracts and
which claims require physical or numerical checks.
5. Use `manual/physics_developer_manual.pdf` as the detailed guide. Its LaTeX
source is beside it.
## The important boundary
`makeRotatingStellarModel(...)` produces a valid, strongly typed stellar-model
specification. The current equilibrium numerical core is still barotropic: it
expects density to be closed by specific enthalpy alone. An ideal-gas plus
radiation EOS depends independently on density and temperature, so a complete
thermal equilibrium solve also needs a temperature or entropy field and its
governing equation.
The example therefore proves at compile time that model composition succeeds
and that the present discretizer rejects this model. It does not disguise the
thermal EOS as a polytrope or claim that a missing energy equation exists.
## Build only this example
From the repository root, configure as usual, then build only these targets:
```sh
cmake --build cmake-build-profile-homebrew-llvm \
--target extension_example_demo extension_example_tests
```
Run only the extension tests:
```sh
./cmake-build-profile-homebrew-llvm/extension_example/extension_example_tests
```
Compile a fresh manual into the build directory:
```sh
cmake --build cmake-build-profile-homebrew-llvm \
--target extension_example_manual
```
No source under `libmeanfield/` belongs to this example, and the extension test
executable is separate from the main MeanField regression suite.

View File

@@ -0,0 +1,43 @@
#include <iomanip>
#include <iostream>
import mean_field;
import mean_field_extension_example.rotating_stellar_model;
int main() {
using namespace mean_field;
using namespace mean_field::extension_example;
const IdealGasRadiation equationOfState({
.meanMolecularWeight = 0.61,
.boltzmannConstant = 1.380649e-16,
.atomicMassUnit = 1.66053906660e-24,
.radiationConstant = 7.5657e-15
});
const dimensions::DensityValue density{10.0}; // g cm^-3
const dimensions::TemperatureValue temperature{1.5e7}; // K
const auto pressure = eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
density,
temperature
);
const auto model = makeRotatingStellarModel({
.equationOfState = equationOfState.parameters(),
.surfacePressure = dimensions::PressureValue{0.0},
.totalMass = dimensions::MassValue{1.0},
.totalAngularMomentum = dimensions::AngularMomentumValue{0.2}
});
std::cout << std::scientific
<< "P(rho = 10 g cm^-3, T = 1.5e7 K) = "
<< pressure.value() << " dyn cm^-2\n"
<< "Compiled specification count = "
<< model.specificationCount << '\n'
<< "Current barotropic backend accepts this thermal model = "
<< std::boolalpha
<< currentEquilibriumBackendSupportsIdealGasRadiation << '\n';
return 0;
}

View File

@@ -0,0 +1,403 @@
module;
#include <cmath>
#include <stdexcept>
#include <string>
export module mean_field_extension_example.ideal_gas_radiation;
import mean_field;
/*
* This file is intended to be read from top to bottom by a physicist who is
* adding an equation of state (EOS). The comments explain the small amount
* of type-system vocabulary required by MeanField; the thermodynamics remain
* visible as ordinary equations.
*/
export namespace mean_field::extension_example {
namespace eos_quantity = mean_field::dimensions::quantity;
/*
* A relation is only a compile-time sentence:
*
* output = f(input 1, input 2, ...).
*
* Input order is significant. These declarations say that density is
* the first argument and temperature is the second argument. They do not
* allocate data and have no runtime cost.
*/
using PressureFromDensityAndTemperature = mean_field::eos::Relation<
eos_quantity::Pressure,
eos_quantity::Density,
eos_quantity::Temperature>;
using SpecificInternalEnergyFromDensityAndTemperature = mean_field::eos::Relation<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Density,
eos_quantity::Temperature>;
using SpecificEnthalpyFromDensityAndTemperature = mean_field::eos::Relation<
eos_quantity::SpecificEnthalpy,
eos_quantity::Density,
eos_quantity::Temperature>;
/*
* A monatomic ideal gas plus equilibrium radiation:
*
* R = k_B / (mu m_u)
* P_gas = rho R T
* P_rad = a T^4 / 3
* u = (3/2) R T + a T^4 / rho
* h = u + P/rho
* = (5/2) R T + 4 a T^4 / (3 rho)
*
* The scalar QuantityValue wrappers identify what a number means. They
* intentionally do not perform unit conversion. Every number supplied
* here must therefore use one coherent unit system.
*/
class IdealGasRadiation final {
public:
struct Parameters final {
/* Mean particle mass in atomic-mass units. */
double meanMolecularWeight{0.61};
/* CGS defaults: erg K^-1, g, and erg cm^-3 K^-4. */
double boltzmannConstant{1.380649e-16};
double atomicMassUnit{1.66053906660e-24};
double radiationConstant{7.5657e-15};
};
/*
* This one alias makes the EOS a constitutive-law specification that
* can be placed directly in model::StellarModel(...). There is no
* registry edit and no central list of EOS combinations to maintain.
*/
using ModelDefinition = mean_field::eos::ConstitutiveLaw<IdealGasRadiation,"IdealGasRadiation">;
/*
* The catalog is the complete public claim made by this EOS. If an
* evaluate overload below is missing or has the wrong argument order,
* eos::EquationOfStateModel<IdealGasRadiation> becomes false at
* compile time.
*/
using Relations = mean_field::eos::RelationCatalog<
PressureFromDensityAndTemperature,
SpecificInternalEnergyFromDensityAndTemperature,
SpecificEnthalpyFromDensityAndTemperature
>;
struct PressureContributions final {
mean_field::dimensions::PressureValue gas;
mean_field::dimensions::PressureValue radiation;
[[nodiscard]] mean_field::dimensions::PressureValue total() const noexcept {
return gas + radiation;
}
};
explicit IdealGasRadiation(const Parameters parameters)
: m_parameters(validatedParameters(parameters)),
m_specificGasConstant(
m_parameters.boltzmannConstant /(m_parameters.meanMolecularWeight * m_parameters.atomicMassUnit)
) {}
[[nodiscard]] const Parameters &parameters() const noexcept {
return m_parameters;
}
[[nodiscard]] double specificGasConstant() const noexcept {
return m_specificGasConstant;
}
/*
* Named component functions are not required by the EOS protocol.
* They are provided because they make diagnostics and physics tests
* easier to read than repeated algebra in client code.
*/
[[nodiscard]] PressureContributions pressureContributions(
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
const double rho = density.value();
const double T = temperature.value();
return PressureContributions{
.gas = mean_field::dimensions::PressureValue{rho * m_specificGasConstant * T},
.radiation = mean_field::dimensions::PressureValue{
m_parameters.radiationConstant * fourthPower(T) / 3.0
}
};
}
[[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue gasSpecificInternalEnergy(
const mean_field::dimensions::TemperatureValue temperature
) const {
validateTemperature(temperature);
return mean_field::dimensions::SpecificInternalEnergyValue{
1.5 * m_specificGasConstant * temperature.value()
};
}
[[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue radiationSpecificInternalEnergy(
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::dimensions::SpecificInternalEnergyValue{
m_parameters.radiationConstant * fourthPower(temperature.value()) / density.value()
};
}
/* The evaluate overloads implement the three declared relations. */
[[nodiscard]] mean_field::dimensions::PressureValue evaluate(
PressureFromDensityAndTemperature,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
return pressureContributions(density, temperature).total();
}
[[nodiscard]] mean_field::dimensions::SpecificInternalEnergyValue evaluate(
SpecificInternalEnergyFromDensityAndTemperature,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
const auto gas = gasSpecificInternalEnergy(temperature);
const auto radiation = radiationSpecificInternalEnergy(density, temperature);
return gas + radiation;
}
[[nodiscard]] mean_field::dimensions::SpecificEnthalpyValue evaluate(
SpecificEnthalpyFromDensityAndTemperature,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
const double rho = density.value();
const double T = temperature.value();
return mean_field::dimensions::SpecificEnthalpyValue{
2.5 * m_specificGasConstant * T +
4.0 * m_parameters.radiationConstant * fourthPower(T) / (3.0 * rho)
};
}
/*
* Jacobian entries are ordinary analytic partial derivatives. The
* WithRespectTo tag prevents accidentally returning dP/dT from the
* overload that promised dP/drho.
*/
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Density>
partialDerivative(
PressureFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Density>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Density>{m_specificGasConstant * temperature.value()};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Temperature>
partialDerivative(
PressureFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Temperature>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
const double T = temperature.value();
return mean_field::eos::PartialDerivative<
eos_quantity::Pressure,
eos_quantity::Temperature>{
density.value() * m_specificGasConstant +
4.0 * m_parameters.radiationConstant * cube(T) / 3.0
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Density>
partialDerivative(
SpecificInternalEnergyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Density>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Density>{
-m_parameters.radiationConstant * fourthPower(temperature.value()) /
square(density.value())
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Temperature>
partialDerivative(
SpecificInternalEnergyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Temperature>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificInternalEnergy,
eos_quantity::Temperature>{
1.5 * m_specificGasConstant +
4.0 * m_parameters.radiationConstant * cube(temperature.value()) / density.value()
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Density>
partialDerivative(
SpecificEnthalpyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Density>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Density>{
-4.0 * m_parameters.radiationConstant * fourthPower(temperature.value()) /
(3.0 * square(density.value()))
};
}
[[nodiscard]] mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Temperature>
partialDerivative(
SpecificEnthalpyFromDensityAndTemperature,
mean_field::eos::WithRespectTo<eos_quantity::Temperature>,
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) const {
validateMaterialState(density, temperature);
return mean_field::eos::PartialDerivative<
eos_quantity::SpecificEnthalpy,
eos_quantity::Temperature>{
2.5 * m_specificGasConstant +
16.0 * m_parameters.radiationConstant * cube(temperature.value()) /
(3.0 * density.value())
};
}
private:
[[nodiscard]] static Parameters validatedParameters(const Parameters parameters) {
requirePositiveFinite(parameters.meanMolecularWeight, "mean molecular weight");
requirePositiveFinite(parameters.boltzmannConstant, "Boltzmann constant");
requirePositiveFinite(parameters.atomicMassUnit, "atomic mass unit");
requireNonnegativeFinite(parameters.radiationConstant, "radiation constant");
return parameters;
}
static void requirePositiveFinite(const double value, const char *name) {
if (!std::isfinite(value) || value <= 0.0) {
throw std::invalid_argument(
std::string{"IdealGasRadiation requires a finite, positive "} + name + "."
);
}
}
static void requireNonnegativeFinite(const double value, const char *name) {
if (!std::isfinite(value) || value < 0.0) {
throw std::invalid_argument(
std::string{"IdealGasRadiation requires a finite, nonnegative "} + name + "."
);
}
}
static void validateMaterialState(
const mean_field::dimensions::DensityValue density,
const mean_field::dimensions::TemperatureValue temperature
) {
if (!std::isfinite(density.value()) || !std::isfinite(temperature.value())) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::nonfinite_input,
"IdealGasRadiation requires finite density and temperature."
};
}
if (density.value() <= 0.0 || temperature.value() < 0.0) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::outside_domain,
"IdealGasRadiation requires rho > 0 and T >= 0."
};
}
}
static void validateTemperature(const mean_field::dimensions::TemperatureValue temperature) {
if (!std::isfinite(temperature.value())) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::nonfinite_input,
"IdealGasRadiation requires finite temperature."
};
}
if (temperature.value() < 0.0) {
throw mean_field::eos::EvaluationError{
mean_field::eos::EvaluationErrorCode::outside_domain,
"IdealGasRadiation requires T >= 0."
};
}
}
[[nodiscard]] static double square(const double value) noexcept {
return value * value;
}
[[nodiscard]] static double cube(const double value) noexcept {
return value * value * value;
}
[[nodiscard]] static double fourthPower(const double value) noexcept {
const double squared = square(value);
return squared * squared;
}
Parameters m_parameters;
double m_specificGasConstant;
};
/*
* These assertions are executable documentation. They prove that the
* class and every derivative satisfy the public extension protocol.
*/
static_assert(mean_field::models::SelfDescribingModelSpecification<IdealGasRadiation>);
static_assert(mean_field::eos::EquationOfStateModel<IdealGasRadiation>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
PressureFromDensityAndTemperature,
eos_quantity::Density>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
PressureFromDensityAndTemperature,
eos_quantity::Temperature>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificInternalEnergyFromDensityAndTemperature,
eos_quantity::Density>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificInternalEnergyFromDensityAndTemperature,
eos_quantity::Temperature>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificEnthalpyFromDensityAndTemperature,
eos_quantity::Density>);
static_assert(mean_field::eos::SupportsPartialDerivative<
IdealGasRadiation,
SpecificEnthalpyFromDensityAndTemperature,
eos_quantity::Temperature>);
} // namespace mean_field::extension_example

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
module;
#include <array>
#include <utility>
export module mean_field_extension_example.rotating_stellar_model;
export import mean_field_extension_example.ideal_gas_radiation;
import mean_field;
/*
* This file is the physics-facing composition layer. It contains no block
* matrices, generated residual types, Jacobian indices, or preconditioner
* plumbing. StellarModel infers those structural types from the four
* physical specifications passed to it.
*/
export namespace mean_field::extension_example {
struct RotatingStellarModelParameters final {
IdealGasRadiation::Parameters equationOfState;
mean_field::dimensions::PressureValue surfacePressure;
mean_field::dimensions::MassValue totalMass;
mean_field::dimensions::AngularMomentumValue totalAngularMomentum;
std::array<double, 3> rotationAxis{0.0, 0.0, 1.0};
std::array<double, 3> rotationCenter{0.0, 0.0, 0.0};
};
[[nodiscard]] auto makeRotatingStellarModel(const RotatingStellarModelParameters &parameters) {
return mean_field::model::StellarModel(
IdealGasRadiation(parameters.equationOfState),
mean_field::surface::Isobaric({.Psurf = parameters.surfacePressure}),
mean_field::integral::FixedTotalMass({.Mtotal = parameters.totalMass}),
mean_field::integral::FixedAngularMomentum({
.Jtotal = parameters.totalAngularMomentum,
.axis = parameters.rotationAxis,
.center = parameters.rotationCenter
})
);
}
using RotatingStellarModel = decltype(
makeRotatingStellarModel(std::declval<const RotatingStellarModelParameters &>())
);
static_assert(mean_field::model::StellarModelType<RotatingStellarModel>);
static_assert(RotatingStellarModel::symbolicallySquare);
/*
* Deliberate capability boundary:
*
* The specification above is a valid, strongly typed stellar model. The
* current numerical equilibrium core, however, closes density through a
* barotropic relation rho(h). This EOS instead needs an independent
* temperature or entropy field and its governing equation. Keeping this
* assertion false prevents an example from suggesting that discretize()
* already implements thermal equilibrium when it does not.
*/
inline constexpr bool currentEquilibriumBackendSupportsIdealGasRadiation =
mean_field::equilibrium::StellarEquilibriumModel<RotatingStellarModel>;
static_assert(!currentEquilibriumBackendSupportsIdealGasRadiation);
} // namespace mean_field::extension_example

View File

@@ -0,0 +1,292 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <concepts>
#include <limits>
#include <type_traits>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import mean_field_extension_example.ideal_gas_radiation;
namespace {
namespace dimensions = mean_field::dimensions;
namespace eos = mean_field::eos;
namespace example = mean_field::extension_example;
[[nodiscard]] example::IdealGasRadiation makeSimpleEquationOfState() {
/* R = k_B / (mu m_u) = 12 / (2 * 3) = 2. */
return example::IdealGasRadiation({
.meanMolecularWeight = 2.0,
.boltzmannConstant = 12.0,
.atomicMassUnit = 3.0,
.radiationConstant = 9.0
});
}
template <typename Function>
[[nodiscard]] double centeredDifference(
Function function,
const double point
) {
const double step = std::cbrt(std::numeric_limits<double>::epsilon()) *
std::max(1.0, std::abs(point));
return (function(point + step) - function(point - step)) / (2.0 * step);
}
template <typename EquationOfState>
concept CanEvaluatePressureWithReversedInputs = requires(
const EquationOfState &equationOfState,
const dimensions::TemperatureValue temperature,
const dimensions::DensityValue density
) {
eos::evaluate<dimensions::quantity::Pressure>(equationOfState, temperature, density);
};
} // namespace
TEST_CASE("The extension satisfies the EOS protocol at compile time", "[extension-example][eos][type]") {
using EquationOfState = example::IdealGasRadiation;
STATIC_CHECK(mean_field::models::SelfDescribingModelSpecification<EquationOfState>);
STATIC_CHECK(eos::EquationOfStateModel<EquationOfState>);
STATIC_CHECK(eos::SupportsRelation<EquationOfState, example::PressureFromDensityAndTemperature>);
STATIC_CHECK(eos::SupportsRelation<EquationOfState, example::SpecificInternalEnergyFromDensityAndTemperature>);
STATIC_CHECK(eos::SupportsRelation<EquationOfState, example::SpecificEnthalpyFromDensityAndTemperature>);
STATIC_CHECK_FALSE(eos::BarotropicClosureEquationOfState<EquationOfState>);
STATIC_CHECK_FALSE(CanEvaluatePressureWithReversedInputs<EquationOfState>);
using PressureResult = decltype(eos::evaluate<dimensions::quantity::Pressure>(
std::declval<const EquationOfState &>(),
dimensions::DensityValue{1.0},
dimensions::TemperatureValue{1.0}
));
STATIC_CHECK(std::same_as<PressureResult, dimensions::PressureValue>);
}
TEST_CASE("Gas and radiation terms reproduce the defining thermodynamics", "[extension-example][eos][physics]") {
const auto equationOfState = makeSimpleEquationOfState();
const dimensions::DensityValue density{4.0};
const dimensions::TemperatureValue temperature{2.0};
const auto pressureContributions = equationOfState.pressureContributions(density, temperature);
const auto pressure = eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
density,
temperature
);
const auto internalEnergy = eos::evaluate<dimensions::quantity::SpecificInternalEnergy>(
equationOfState,
density,
temperature
);
const auto enthalpy = eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
equationOfState,
density,
temperature
);
CHECK(equationOfState.specificGasConstant() == Catch::Approx(2.0));
CHECK(pressureContributions.gas.value() == Catch::Approx(16.0));
CHECK(pressureContributions.radiation.value() == Catch::Approx(48.0));
CHECK(pressure.value() == Catch::Approx(64.0));
CHECK(internalEnergy.value() == Catch::Approx(42.0));
CHECK(enthalpy.value() == Catch::Approx(58.0));
/* This is the thermodynamic identity h = u + P/rho. */
CHECK(enthalpy.value() == Catch::Approx(internalEnergy.value() + pressure.value() / density.value()));
}
TEST_CASE("The gas and photon terms have their expected scaling laws", "[extension-example][eos][physics]") {
const auto equationOfState = makeSimpleEquationOfState();
const dimensions::DensityValue density{3.5};
const dimensions::TemperatureValue temperature{1.25};
const auto baseline = equationOfState.pressureContributions(density, temperature);
const auto doubledDensity = equationOfState.pressureContributions(
dimensions::DensityValue{2.0 * density.value()},
temperature
);
const auto doubledTemperature = equationOfState.pressureContributions(
density,
dimensions::TemperatureValue{2.0 * temperature.value()}
);
CHECK(doubledDensity.gas.value() == Catch::Approx(2.0 * baseline.gas.value()));
CHECK(doubledDensity.radiation.value() == Catch::Approx(baseline.radiation.value()));
CHECK(doubledTemperature.gas.value() == Catch::Approx(2.0 * baseline.gas.value()));
CHECK(doubledTemperature.radiation.value() == Catch::Approx(16.0 * baseline.radiation.value()));
const double crossoverTemperature = std::cbrt(
3.0 * density.value() * equationOfState.specificGasConstant() /
equationOfState.parameters().radiationConstant
);
const auto crossover = equationOfState.pressureContributions(
density,
dimensions::TemperatureValue{crossoverTemperature}
);
CHECK(crossover.gas.value() == Catch::Approx(crossover.radiation.value()).epsilon(2.0e-14));
}
TEST_CASE("All declared Jacobian entries match centered numerical derivatives",
"[extension-example][eos][derivative][numerical]") {
const auto equationOfState = example::IdealGasRadiation({
.meanMolecularWeight = 1.25,
.boltzmannConstant = 2.75,
.atomicMassUnit = 0.8,
.radiationConstant = 0.35
});
struct State final {
double density;
double temperature;
};
const std::array states{
State{.density = 0.4, .temperature = 0.7},
State{.density = 2.0, .temperature = 1.5},
State{.density = 11.0, .temperature = 3.0}
};
for (const State state : states) {
const dimensions::DensityValue density{state.density};
const dimensions::TemperatureValue temperature{state.temperature};
const auto pressureDensity = eos::partialDerivative<
dimensions::quantity::Pressure,
dimensions::quantity::Density>(equationOfState, density, temperature);
const auto pressureTemperature = eos::partialDerivative<
dimensions::quantity::Pressure,
dimensions::quantity::Temperature>(equationOfState, density, temperature);
const auto energyDensity = eos::partialDerivative<
dimensions::quantity::SpecificInternalEnergy,
dimensions::quantity::Density>(equationOfState, density, temperature);
const auto energyTemperature = eos::partialDerivative<
dimensions::quantity::SpecificInternalEnergy,
dimensions::quantity::Temperature>(equationOfState, density, temperature);
const auto enthalpyDensity = eos::partialDerivative<
dimensions::quantity::SpecificEnthalpy,
dimensions::quantity::Density>(equationOfState, density, temperature);
const auto enthalpyTemperature = eos::partialDerivative<
dimensions::quantity::SpecificEnthalpy,
dimensions::quantity::Temperature>(equationOfState, density, temperature);
const double numericalPressureDensity = centeredDifference(
[&](const double rho) {
return eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{rho},
temperature
).value();
},
state.density
);
const double numericalPressureTemperature = centeredDifference(
[&](const double T) {
return eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
density,
dimensions::TemperatureValue{T}
).value();
},
state.temperature
);
const double numericalEnergyDensity = centeredDifference(
[&](const double rho) {
return eos::evaluate<dimensions::quantity::SpecificInternalEnergy>(
equationOfState,
dimensions::DensityValue{rho},
temperature
).value();
},
state.density
);
const double numericalEnergyTemperature = centeredDifference(
[&](const double T) {
return eos::evaluate<dimensions::quantity::SpecificInternalEnergy>(
equationOfState,
density,
dimensions::TemperatureValue{T}
).value();
},
state.temperature
);
const double numericalEnthalpyDensity = centeredDifference(
[&](const double rho) {
return eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
equationOfState,
dimensions::DensityValue{rho},
temperature
).value();
},
state.density
);
const double numericalEnthalpyTemperature = centeredDifference(
[&](const double T) {
return eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
equationOfState,
density,
dimensions::TemperatureValue{T}
).value();
},
state.temperature
);
constexpr double tolerance = 3.0e-9;
CHECK(pressureDensity.value() == Catch::Approx(numericalPressureDensity).epsilon(tolerance));
CHECK(pressureTemperature.value() == Catch::Approx(numericalPressureTemperature).epsilon(tolerance));
CHECK(energyDensity.value() == Catch::Approx(numericalEnergyDensity).epsilon(tolerance));
CHECK(energyTemperature.value() == Catch::Approx(numericalEnergyTemperature).epsilon(tolerance));
CHECK(enthalpyDensity.value() == Catch::Approx(numericalEnthalpyDensity).epsilon(tolerance));
CHECK(enthalpyTemperature.value() == Catch::Approx(numericalEnthalpyTemperature).epsilon(tolerance));
}
}
TEST_CASE("The physical domain is checked at the EOS boundary", "[extension-example][eos][domain]") {
const auto equationOfState = makeSimpleEquationOfState();
const double nan = std::numeric_limits<double>::quiet_NaN();
CHECK_THROWS_AS(
example::IdealGasRadiation({
.meanMolecularWeight = 0.0,
.boltzmannConstant = 1.0,
.atomicMassUnit = 1.0,
.radiationConstant = 1.0
}),
std::invalid_argument
);
CHECK_THROWS_AS(
example::IdealGasRadiation({
.meanMolecularWeight = 1.0,
.boltzmannConstant = 1.0,
.atomicMassUnit = 1.0,
.radiationConstant = -1.0
}),
std::invalid_argument
);
CHECK_THROWS_AS(
eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{0.0},
dimensions::TemperatureValue{1.0}
),
eos::EvaluationError
);
CHECK_THROWS_AS(
eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{1.0},
dimensions::TemperatureValue{-1.0}
),
eos::EvaluationError
);
CHECK_THROWS_AS(
eos::evaluate<dimensions::quantity::Pressure>(
equationOfState,
dimensions::DensityValue{nan},
dimensions::TemperatureValue{1.0}
),
eos::EvaluationError
);
}

View File

@@ -0,0 +1,67 @@
#include <concepts>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import mean_field_extension_example.rotating_stellar_model;
TEST_CASE("The example EOS composes with existing stellar specifications",
"[extension-example][model][type]") {
using namespace mean_field;
namespace example = mean_field::extension_example;
const auto stellarModel = example::makeRotatingStellarModel({
.equationOfState = {
.meanMolecularWeight = 0.62,
.boltzmannConstant = 1.380649e-16,
.atomicMassUnit = 1.66053906660e-24,
.radiationConstant = 7.5657e-15
},
.surfacePressure = dimensions::PressureValue{0.0},
.totalMass = dimensions::MassValue{1.75},
.totalAngularMomentum = dimensions::AngularMomentumValue{0.3},
.rotationAxis = {0.0, 0.0, 4.0},
.rotationCenter = {0.1, -0.2, 0.3}
});
using Model = std::remove_cvref_t<decltype(stellarModel)>;
STATIC_CHECK(std::same_as<Model, example::RotatingStellarModel>);
STATIC_CHECK(model::StellarModelType<Model>);
STATIC_CHECK(Model::symbolicallySquare);
STATIC_CHECK(Model::specificationCount == 4);
STATIC_CHECK(std::same_as<model::EquationOfStateType<Model>, example::IdealGasRadiation>);
STATIC_CHECK(Model::template containsSpecification<integral::FixedTotalMass>);
STATIC_CHECK(Model::template containsSpecification<integral::FixedAngularMomentum>);
STATIC_CHECK(Model::template specificationRoleCount<models::SpecificationRole::constitutive_law> == 1);
STATIC_CHECK(Model::template specificationRoleCount<models::SpecificationRole::boundary_condition> == 1);
STATIC_CHECK(Model::template specificationRoleCount<models::SpecificationRole::invariant> == 2);
CHECK(stellarModel.equationOfState().parameters().meanMolecularWeight == 0.62);
CHECK(stellarModel.surfaceCondition().targetPressure() == dimensions::PressureValue{0.0});
CHECK(stellarModel.specification<integral::FixedTotalMass>().targetMass() == dimensions::MassValue{1.75});
const auto &angularMomentum = stellarModel.specification<integral::FixedAngularMomentum>();
CHECK(angularMomentum.targetAngularMomentum() == dimensions::AngularMomentumValue{0.3});
CHECK(angularMomentum.axis()[0] == 0.0);
CHECK(angularMomentum.axis()[1] == 0.0);
CHECK(angularMomentum.axis()[2] == 1.0);
CHECK(angularMomentum.center()[0] == 0.1);
CHECK(angularMomentum.center()[1] == -0.2);
CHECK(angularMomentum.center()[2] == 0.3);
CHECK(stellarModel.runtimeSpecificationDescriptors().size() == 4);
}
TEST_CASE("The example states the current thermal-runtime boundary explicitly",
"[extension-example][model][capability]") {
using Model = mean_field::extension_example::RotatingStellarModel;
/*
* This is not a failure of model composition. It is the intended
* compile-time rejection of a thermal EOS by a currently barotropic
* numerical core. See the manual section 'What compiles today'.
*/
STATIC_CHECK(mean_field::model::StellarModelType<Model>);
STATIC_CHECK_FALSE(mean_field::extension_example::currentEquilibriumBackendSupportsIdealGasRadiation);
STATIC_CHECK_FALSE(mean_field::equilibrium::StellarEquilibriumModel<Model>);
}

4
format
View File

@@ -1,3 +1,3 @@
#!/bin/bash
find libmeanfield tests -type f \( -name '*.cpp' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}
find libmeanfield tests -type f \( -name '*.cppm' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}
find libmeanfield tests experiments -type f \( -name '*.cpp' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}
find libmeanfield tests experiments -type f \( -name '*.cppm' \) | xargs -I{} clang-format -style=file:clang-format-styles/style -i {}

View File

@@ -1,11 +1,27 @@
from stroid.config import MeshConfig
from stroid.IO import SaveStroidMesh
from stroid import GenerateMesh
import stroid
cfg = MeshConfig()
cfg.order = 4
cfg.refinement_levels = 2
print(cfg)
cfg = MeshConfig(
core_mapping="multi_block",
order=4,
refinement_levels=2,
include_external_domain=True,
r_core=0.25,
r_star=1.0,
r_infinity=5.0,
flattening=0.0,
core_id=1,
envelope_id=2,
vacuum_id=3,
surface_bdr_id=1,
inf_bdr_id=2,
optimization_methods=stroid.config.OptimizationMethods(
tmop=False,
smoothstep=True,
),
)
mesh = GenerateMesh(cfg)
SaveStroidMesh(mesh, "sandbox.smesh")

View File

@@ -1,4 +1,5 @@
module;
#include "profile.h"
#include <array>
#include <mfem.hpp>
@@ -6,6 +7,35 @@ module mean_field;
import :mapping.coefficients;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
mfem::Array<int> make_domain_marker(
const mfem::Mesh &mesh,
const mean_field::utils::DOMAINS domain
) {
switch (domain) {
case mean_field::utils::DOMAINS::CORE:
return mean_field::utils::domain::make_attribute_marker<mean_field::utils::domain::Core, DomainSchema>(
mesh
);
case mean_field::utils::DOMAINS::ENVELOPE:
return mean_field::utils::domain::make_attribute_marker<mean_field::utils::domain::Envelope, DomainSchema>(
mesh
);
case mean_field::utils::DOMAINS::ALL:
return mean_field::utils::domain::make_attribute_marker<mean_field::utils::domain::All, DomainSchema>(mesh);
case mean_field::utils::DOMAINS::STELLAR:
return mean_field::utils::domain::make_attribute_marker<mean_field::utils::domain::Stellar, DomainSchema>(
mesh
);
case mean_field::utils::DOMAINS::VACUUM:
return mean_field::utils::domain::make_attribute_marker<mean_field::utils::domain::Vacuum, DomainSchema>(
mesh
);
}
MFEM_ABORT("Unsupported integration domain.");
}
template <typename FormT>
const mfem::IntegrationRule &get_density_rule(
const mean_field::fem::FEM &fem,
@@ -13,23 +43,16 @@ namespace {
const std::array<
int,
FormT::dynamicOrderCount> &dynamic_orders = {},
const mean_field::utils::DOMAINS domain =
mean_field::utils::DOMAINS::ALL
const mean_field::utils::DOMAINS domain = mean_field::utils::DOMAINS::ALL
) {
using DensityField =
mean_field::field::Field<mean_field::field::Density>;
using DensityField = mean_field::field::Field<mean_field::field::Density>;
const mean_field::quadrature::Query query =
DensityField::make_query<FormT>(
mean_field::quadrature::QuadratureRole::diagnostic,
transformation.OrderW(), dynamic_orders, domain,
fem.has_mapping() ? mean_field::quadrature::MappingKind::general
: mean_field::quadrature::MappingKind::none
const mean_field::quadrature::Query query = DensityField::make_query<FormT>(
mean_field::quadrature::QuadratureRole::diagnostic, transformation.OrderW(), dynamic_orders, domain,
fem.has_mapping() ? mean_field::quadrature::MappingKind::general : mean_field::quadrature::MappingKind::none
);
return *fem.quadratureFactory
->get(query, transformation.GetGeometryType())
.integration_rule;
return *fem.quadratureFactory->get(query, transformation.GetGeometryType()).integration_rule;
}
} // namespace
@@ -40,21 +63,20 @@ namespace mean_field::analysis {
utils::DOMAINS domain,
mapping::COORDINATE_SPACE coord_space
) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("analysis::domain_integrate_grid_function", 0);
mfem::LinearForm lf(fem.densityFes.get());
mfem::GridFunctionCoefficient gf_c(&gf);
double local_integral;
mfem::Array<int> elem_markers;
populate_element_mask(fem.mesh.get(), domain, elem_markers);
const mfem::ElementTransformation &representative_transformation =
*fem.mesh->GetElementTransformation(0);
mfem::Array<int> elem_markers = make_domain_marker(*fem.mesh, domain);
const mfem::ElementTransformation &representative_transformation = *fem.mesh->GetElementTransformation(0);
const mfem::IntegrationRule &integration_rule =
get_density_rule<field::Density::Form::MassConservation>(
fem, representative_transformation, {}, domain
);
get_density_rule<field::Density::Form::MassConservation>(fem, representative_transformation, {}, domain);
if (fem.has_mapping() &&
coord_space == mapping::COORDINATE_SPACE::PHYSICAL) {
mapping::MappedScalarCoefficient mapped_gf_c(*fem.mapping, gf_c);
if (fem.has_mapping() && coord_space == mapping::COORDINATE_SPACE::PHYSICAL) {
mapping::MappedScalarCoefficient mapped_gf_c(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, gf_c
);
// ReSharper disable once CppDFAMemoryLeak // Disabled because MFEM
// takes ownership so memory is not leaked
@@ -80,10 +102,7 @@ namespace mean_field::analysis {
}
double global_integral = 0.0;
MPI_Allreduce(
&local_integral, &global_integral, 1, MPI_DOUBLE, MPI_SUM,
fem.mesh->GetComm()
);
MPI_Allreduce(&local_integral, &global_integral, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm());
return global_integral;
}
@@ -91,18 +110,23 @@ namespace mean_field::analysis {
const fem::FEM &fem,
const mfem::GridFunction &rho
) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("analysis::get_com", 0);
std::uint64_t mapping_evaluations = 0;
const int dim = fem.mesh->Dimension();
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
mfem::Vector local_com(dim);
mapping::VolumeMappingContext mapping_context;
local_com = 0.0;
double local_mass = 0.0;
for (int i = 0; i < fem.mesh->GetNE(); ++i) {
if (fem.mesh->GetAttribute(i) == 3)
if (!DomainSchema::template attribute_belongs_to<utils::domain::Stellar>(fem.mesh->GetAttribute(i)))
continue;
mfem::ElementTransformation *trans =
fem.mesh->GetElementTransformation(i);
const mfem::IntegrationRule &ir =
get_density_rule<field::Density::Form::CenterOfMass>(
mfem::ElementTransformation *trans = fem.mesh->GetElementTransformation(i);
const mfem::IntegrationRule &ir = get_density_rule<field::Density::Form::CenterOfMass>(
fem, *trans, std::array<int, 1>{1}, utils::DOMAINS::STELLAR
);
@@ -110,18 +134,15 @@ namespace mean_field::analysis {
const mfem::IntegrationPoint &ip = ir.IntPoint(j);
trans->SetIntPoint(&ip);
double weight = trans->Weight() * ip.weight;
if (fem.has_mapping()) {
weight *= fem.mapping->ComputeDetJ(*trans, ip);
}
MFEM_VERIFY(
mapping_evaluator.EvaluateVolume(*trans, ip, mapping_context) == mapping::MappingStatus::valid,
"Center-of-mass integration encountered an invalid mapping."
);
++mapping_evaluations;
const double weight = mapping_context.quadrature.weight;
double rho_val = rho.GetValue(i, ip);
mfem::Vector phys_point(dim);
if (fem.has_mapping()) {
fem.mapping->GetPhysicalPoint(*trans, ip, phys_point);
} else {
trans->Transform(ip, phys_point);
}
const mfem::Vector &phys_point = mapping_context.mapping.physical_position;
const double mass_term = rho_val * weight;
local_mass += mass_term;
@@ -132,17 +153,24 @@ namespace mean_field::analysis {
}
}
double global_mass = 0.0;
mfem::Vector global_com(dim);
MPI_Comm comm = fem.mesh->GetComm();
MPI_Allreduce(&local_mass, &global_mass, 1, MPI_DOUBLE, MPI_SUM, comm);
MEAN_FIELD_PROFILE_COUNT("analysis::get_com mapping evaluations", mapping_evaluations);
mfem::Vector local_integrals(dim + 1);
mfem::Vector global_integrals(dim + 1);
local_integrals(0) = local_mass;
for (int d = 0; d < dim; ++d) {
local_integrals(d + 1) = local_com(d);
}
MPI_Allreduce(
local_com.GetData(), global_com.GetData(), dim, MPI_DOUBLE, MPI_SUM,
comm
local_integrals.GetData(), global_integrals.GetData(), dim + 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm()
);
const double global_mass = global_integrals(0);
mfem::Vector global_com(dim);
for (int d = 0; d < dim; ++d) {
global_com(d) = global_integrals(d + 1);
}
if (global_mass > 1e-18) {
global_com /= global_mass;
} else {
@@ -157,9 +185,9 @@ namespace mean_field::analysis {
mfem::GridFunction &rho,
const double target_mass
) {
if (const double current_mass = domain_integrate_grid_function(
fem, rho, utils::DOMAINS::STELLAR
);
MEAN_FIELD_PROFILE_SCOPE_WARMUP("analysis::conserve_mass", 0);
if (const double current_mass = domain_integrate_grid_function(fem, rho, utils::DOMAINS::STELLAR);
current_mass > 1e-15)
rho *= (target_mass / current_mass);
}
@@ -168,15 +196,14 @@ namespace mean_field::analysis {
const fem::FEM &fem,
const mfem::GridFunction &rho
) {
auto s2_func = [](const mfem::Vector &x) {
return std::pow(x(0), 2) + std::pow(x(1), 2);
};
MEAN_FIELD_PROFILE_SCOPE_WARMUP("analysis::get_moment_of_inertia", 0);
auto s2_func = [](const mfem::Vector &x) { return std::pow(x(0), 2) + std::pow(x(1), 2); };
std::unique_ptr<mfem::Coefficient> s2_coeff;
if (fem.has_mapping()) {
s2_coeff =
std::make_unique<mapping::PhysicalPositionFunctionCoefficient>(
*fem.mapping, s2_func
s2_coeff = std::make_unique<mapping::PhysicalPositionFunctionCoefficient>(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, s2_func
);
} else {
s2_coeff = std::make_unique<mfem::FunctionCoefficient>(s2_func);
@@ -186,22 +213,17 @@ namespace mean_field::analysis {
mfem::ProductCoefficient I_integrand(rho_coeff, *s2_coeff);
mfem::LinearForm I_lf(fem.densityFes.get());
const mfem::ElementTransformation &representative_transformation =
*fem.mesh->GetElementTransformation(0);
const mfem::IntegrationRule &integration_rule =
get_density_rule<field::Density::Form::Quadrupole>(
fem, representative_transformation, std::array<int, 1>{2},
utils::DOMAINS::STELLAR
);
mfem::Array<int> stellar_markers;
populate_element_mask(
fem.mesh.get(), utils::DOMAINS::STELLAR, stellar_markers
const mfem::ElementTransformation &representative_transformation = *fem.mesh->GetElementTransformation(0);
const mfem::IntegrationRule &integration_rule = get_density_rule<field::Density::Form::Quadrupole>(
fem, representative_transformation, std::array<int, 1>{2}, utils::DOMAINS::STELLAR
);
mfem::Array<int> stellar_markers =
utils::domain::make_attribute_marker<utils::domain::Stellar, DomainSchema>(*fem.mesh);
double local_I = 0.0;
if (fem.has_mapping()) {
mapping::MappedScalarCoefficient mapped_integrand(
*fem.mapping, I_integrand
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate, I_integrand
);
auto *integrator = new mfem::DomainLFIntegrator(mapped_integrand);
integrator->SetIntRule(&integration_rule);
@@ -217,9 +239,7 @@ namespace mean_field::analysis {
}
double global_I = 0.0;
MPI_Allreduce(
&local_I, &global_I, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm()
);
MPI_Allreduce(&local_I, &global_I, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm());
return global_I;
}
@@ -228,39 +248,33 @@ namespace mean_field::analysis {
const mapping::COORDINATE_SPACE coordinate_space,
const utils::DOMAINS domain
) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("analysis::get_mesh_volume", 0);
mfem::ParMesh &mesh = *fem.mesh;
const bool physical =
(coordinate_space == mapping::COORDINATE_SPACE::PHYSICAL);
const bool physical = (coordinate_space == mapping::COORDINATE_SPACE::PHYSICAL);
if (physical && !fem.has_mapping()) {
MFEM_ABORT(
"Physical volume requested but no domain mapping is available."
);
MFEM_ABORT("Physical volume requested but no domain mapping is available.");
}
double local_volume = 0.0;
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
mapping::VolumeMappingContext mapping_context;
for (int e = 0; e < mesh.GetNE(); ++e) {
const int attr = mesh.GetAttribute(e);
switch (domain) {
case utils::DOMAINS::ALL:
break;
case utils::DOMAINS::STELLAR:
if (attr == 3)
const bool selected = domain == utils::DOMAINS::ALL ||
(domain == utils::DOMAINS::STELLAR &&
DomainSchema::template attribute_belongs_to<utils::domain::Stellar>(attr)) ||
(domain == utils::DOMAINS::VACUUM &&
DomainSchema::template attribute_belongs_to<utils::domain::Vacuum>(attr));
if (!selected)
continue;
break;
case utils::DOMAINS::VACUUM:
if (attr != 3)
continue;
break;
default:
MFEM_ABORT("Unsupported domain type for volume computation.");
}
mfem::ElementTransformation *T = mesh.GetElementTransformation(e);
const mfem::IntegrationRule &ir =
get_density_rule<field::Density::Form::MassConservation>(
fem, *T, {}, domain
);
get_density_rule<field::Density::Form::MassConservation>(fem, *T, {}, domain);
for (int q = 0; q < ir.GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir.IntPoint(q);
@@ -269,7 +283,11 @@ namespace mean_field::analysis {
double dV = ip.weight * T->Weight();
if (physical) {
dV *= std::fabs(fem.mapping->ComputeDetJ(*T, ip));
MFEM_VERIFY(
mapping_evaluator.EvaluateVolume(*T, ip, mapping_context) == mapping::MappingStatus::valid,
"Mesh-volume integration encountered an invalid mapping."
);
dV = mapping_context.quadrature.weight;
}
local_volume += dV;
@@ -277,10 +295,7 @@ namespace mean_field::analysis {
}
double global_volume = 0.0;
MPI_Allreduce(
&local_volume, &global_volume, 1, MPI_DOUBLE, MPI_SUM,
mesh.GetComm()
);
MPI_Allreduce(&local_volume, &global_volume, 1, MPI_DOUBLE, MPI_SUM, mesh.GetComm());
return global_volume;
}
} // namespace mean_field::analysis

View File

@@ -0,0 +1,345 @@
module;
#include <cmath>
#include <format>
#include <stdexcept>
#include <utility>
#include <mfem.hpp>
module mean_field;
import :deformation.nodal_radial_surface;
namespace mean_field::deformation {
namespace {
[[nodiscard]] SurfaceDeformationDescriptor nodalRadialDescriptor(const int spatialDimension) noexcept {
return {
.name = "NodalRadialSurface",
.spatialDimension = spatialDimension,
.motionKind = SurfaceMotionKind::Radial,
.linearOnReferenceGeometry = true,
.requiresStarShapedReferenceSurface = true,
.hasExactDerivativeTranspose = true,
.hasExactPullbackDerivative = true,
.translationTreatment = GeometricGaugeTreatment::Retained,
.orientationTreatment = GeometricGaugeTreatment::Retained
};
}
void requireFiniteVector(
const mfem::Vector &vector,
const char *message
) {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
throw std::invalid_argument(message);
}
}
}
} // namespace
SurfaceDeformationCompilationContext::SurfaceDeformationCompilationContext(
mfem::ParFiniteElementSpace &scalarFiniteElementSpace,
field::ScalarBoundaryDofMap surfaceDofMap
)
: m_scalarFiniteElementSpace(&scalarFiniteElementSpace),
m_surfaceDofMap(std::move(surfaceDofMap)) {
if (scalarFiniteElementSpace.Nonconforming()) {
throw std::invalid_argument(
"Surface deformation compilation currently requires a conforming scalar finite-element space."
);
}
if (scalarFiniteElementSpace.GetVDim() != 1) {
throw std::invalid_argument("Surface deformation compilation requires a scalar finite-element space.");
}
if (scalarFiniteElementSpace.GetMesh() == nullptr) {
throw std::invalid_argument("Surface deformation compilation requires a finite-element mesh.");
}
if (m_surfaceDofMap.volume_true_dof_size() != scalarFiniteElementSpace.GetTrueVSize()) {
throw std::invalid_argument(
"The surface DOF map and scalar finite-element space have incompatible true-DOF sizes."
);
}
if (m_surfaceDofMap.global_size() <= 0) {
throw std::invalid_argument("Surface deformation compilation requires at least one surface coordinate.");
}
}
mfem::ParFiniteElementSpace &SurfaceDeformationCompilationContext::scalarFiniteElementSpace() const noexcept {
return *m_scalarFiniteElementSpace;
}
const field::ScalarBoundaryDofMap &SurfaceDeformationCompilationContext::surfaceDofMap() const noexcept {
return m_surfaceDofMap;
}
NodalRadialSurface::NodalRadialSurface(mfem::Vector referenceCenter)
: m_referenceCenter(std::move(referenceCenter)) {
validate();
}
const mfem::Vector &NodalRadialSurface::referenceCenter() const noexcept {
return m_referenceCenter;
}
SurfaceDeformationDescriptor NodalRadialSurface::descriptor() const noexcept {
return nodalRadialDescriptor(m_referenceCenter.Size());
}
void NodalRadialSurface::validate() const {
if (m_referenceCenter.Size() <= 0) {
throw std::invalid_argument("NodalRadialSurface requires a non-empty reference center.");
}
requireFiniteVector(m_referenceCenter, "NodalRadialSurface reference-center coordinates must be finite.");
}
PreparedNodalRadialSurface::PreparedNodalRadialSurface(
const SurfaceDeformationDescriptor descriptor,
mfem::Vector referenceCenter,
field::ScalarBoundaryDofMap surfaceDofMap,
mfem::Vector radialDirections,
mfem::Vector referenceRadii
)
: m_descriptor(descriptor),
m_referenceCenter(std::move(referenceCenter)),
m_surfaceDofMap(std::move(surfaceDofMap)),
m_radialDirections(std::move(radialDirections)),
m_referenceRadii(std::move(referenceRadii)) {
}
SurfaceDeformationDescriptor PreparedNodalRadialSurface::descriptor() const noexcept {
return m_descriptor;
}
int PreparedNodalRadialSurface::parameterCount() const noexcept {
return m_surfaceDofMap.local_size();
}
long long PreparedNodalRadialSurface::globalParameterCount() const noexcept {
return m_surfaceDofMap.global_size();
}
long long PreparedNodalRadialSurface::globalParameterOffset() const noexcept {
return m_surfaceDofMap.global_offset();
}
int PreparedNodalRadialSurface::spatialDimension() const noexcept {
return m_descriptor.spatialDimension;
}
int PreparedNodalRadialSurface::surfaceDisplacementSize() const noexcept {
return spatialDimension() * parameterCount();
}
long long PreparedNodalRadialSurface::globalSurfaceDisplacementSize() const noexcept {
return static_cast<long long>(spatialDimension()) * globalParameterCount();
}
long long PreparedNodalRadialSurface::globalSurfaceDisplacementOffset() const noexcept {
return static_cast<long long>(spatialDimension()) * globalParameterOffset();
}
int PreparedNodalRadialSurface::surfaceDisplacementDof(
const int parameterDof,
const int component
) const {
if (parameterDof < 0 || parameterDof >= parameterCount()) {
throw std::out_of_range("Parameter DOF is outside PreparedNodalRadialSurface.");
}
if (component < 0 || component >= spatialDimension()) {
throw std::out_of_range("Surface-displacement component is outside PreparedNodalRadialSurface.");
}
return spatialDimension() * parameterDof + component;
}
double PreparedNodalRadialSurface::radialDirection(
const int parameterDof,
const int component
) const {
return m_radialDirections(surfaceDisplacementDof(parameterDof, component));
}
double PreparedNodalRadialSurface::referenceRadius(const int parameterDof) const {
if (parameterDof < 0 || parameterDof >= parameterCount()) {
throw std::out_of_range("Parameter DOF is outside PreparedNodalRadialSurface.");
}
return m_referenceRadii(parameterDof);
}
const mfem::Vector &PreparedNodalRadialSurface::referenceCenter() const noexcept {
return m_referenceCenter;
}
const field::ScalarBoundaryDofMap &PreparedNodalRadialSurface::surfaceDofMap() const noexcept {
return m_surfaceDofMap;
}
void PreparedNodalRadialSurface::buildSurfaceDisplacement(
const mfem::Vector &parameters,
mfem::Vector &surfaceDisplacement
) const {
requireParameterSize(parameters);
requireSurfaceDisplacementSize(surfaceDisplacement);
for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) {
for (int component = 0; component < spatialDimension(); ++component) {
const int surfaceDof = spatialDimension() * parameterDof + component;
surfaceDisplacement(surfaceDof) = parameters(parameterDof) * m_radialDirections(surfaceDof);
}
}
}
void PreparedNodalRadialSurface::applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &surfaceDisplacementDirection
) const {
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireSurfaceDisplacementSize(surfaceDisplacementDirection);
for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) {
for (int component = 0; component < spatialDimension(); ++component) {
const int surfaceDof = spatialDimension() * parameterDof + component;
surfaceDisplacementDirection(surfaceDof) =
parameterDirection(parameterDof) * m_radialDirections(surfaceDof);
}
}
}
void PreparedNodalRadialSurface::applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDual
) const {
requireParameterSize(parameters);
requireSurfaceDisplacementSize(surfaceDisplacementDual);
requireParameterSize(parameterDual);
for (int parameterDof = 0; parameterDof < parameterCount(); ++parameterDof) {
double radialWork = 0.0;
for (int component = 0; component < spatialDimension(); ++component) {
const int surfaceDof = spatialDimension() * parameterDof + component;
radialWork += m_radialDirections(surfaceDof) * surfaceDisplacementDual(surfaceDof);
}
parameterDual(parameterDof) = radialWork;
}
}
void PreparedNodalRadialSurface::applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &surfaceDisplacementDual,
mfem::Vector &parameterDualAction
) const {
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireSurfaceDisplacementSize(surfaceDisplacementDual);
requireParameterSize(parameterDualAction);
parameterDualAction = 0.0;
}
void PreparedNodalRadialSurface::requireParameterSize(const mfem::Vector &parameters) const {
if (parameters.Size() != parameterCount()) {
throw std::invalid_argument(
std::format(
"Nodal radial parameter vector has size {}, but the prepared surface requires {}.",
parameters.Size(), parameterCount()
)
);
}
}
void PreparedNodalRadialSurface::requireSurfaceDisplacementSize(const mfem::Vector &surfaceDisplacement) const {
if (surfaceDisplacement.Size() != surfaceDisplacementSize()) {
throw std::invalid_argument(
std::format(
"Surface displacement vector has size {}, but the prepared nodal radial surface requires {}.",
surfaceDisplacement.Size(), surfaceDisplacementSize()
)
);
}
}
PreparedNodalRadialSurface compileSurfaceDeformationPrescription(
const NodalRadialSurface &prescription,
const SurfaceDeformationCompilationContext &context
) {
prescription.validate();
mfem::ParFiniteElementSpace &scalarSpace = context.scalarFiniteElementSpace();
const mfem::Mesh *mesh = scalarSpace.GetMesh();
if (mesh == nullptr) {
throw std::invalid_argument("Nodal radial surface compilation requires a reference mesh.");
}
if (prescription.referenceCenter().Size() != mesh->SpaceDimension()) {
throw std::invalid_argument(
std::format(
"NodalRadialSurface reference center has dimension {}, but the reference mesh has spatial "
"dimension {}.",
prescription.referenceCenter().Size(), mesh->SpaceDimension()
)
);
}
const field::ScalarBoundaryDofMap &surfaceDofMap = context.surfaceDofMap();
const int parameterCount = surfaceDofMap.local_size();
const int spatialDimension = mesh->SpaceDimension();
mfem::Vector referencePositions(spatialDimension * parameterCount);
mfem::ParGridFunction coordinateField(&scalarSpace);
for (int component = 0; component < spatialDimension; ++component) {
mfem::FunctionCoefficient coordinateCoefficient([component](const mfem::Vector &position) {
return position(component);
});
coordinateField.ProjectCoefficient(coordinateCoefficient);
mfem::Vector coordinateTrueDofs;
coordinateField.GetTrueDofs(coordinateTrueDofs);
const mfem::Vector surfaceCoordinates = surfaceDofMap.gather(coordinateTrueDofs);
for (int parameterDof = 0; parameterDof < parameterCount; ++parameterDof) {
referencePositions(spatialDimension * parameterDof + component) = surfaceCoordinates(parameterDof);
}
}
mfem::Vector radialDirections(referencePositions.Size());
mfem::Vector referenceRadii(parameterCount);
for (int parameterDof = 0; parameterDof < parameterCount; ++parameterDof) {
double radiusSquared = 0.0;
for (int component = 0; component < spatialDimension; ++component) {
const int surfaceDof = spatialDimension * parameterDof + component;
const double radialCoordinate =
referencePositions(surfaceDof) - prescription.referenceCenter()(component);
radialDirections(surfaceDof) = radialCoordinate;
radiusSquared += radialCoordinate * radialCoordinate;
}
const double radius = std::sqrt(radiusSquared);
if (!std::isfinite(radius) || radius <= 0.0) {
throw std::invalid_argument(
"Every nodal radial surface coordinate must have a finite positive distance from the reference "
"center."
);
}
referenceRadii(parameterDof) = radius;
for (int component = 0; component < spatialDimension; ++component) {
radialDirections(spatialDimension * parameterDof + component) /= radius;
}
}
return PreparedNodalRadialSurface(
nodalRadialDescriptor(spatialDimension), prescription.referenceCenter(), surfaceDofMap,
std::move(radialDirections), std::move(referenceRadii)
);
}
} // namespace mean_field::deformation

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,650 @@
module;
#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <mfem.hpp>
#include <mpi.h>
#include <numeric>
#include <stdexcept>
module mean_field;
namespace {
using Coefficients = std::array<double, 4>;
enum class InputFailure : int {
none,
invalid_options,
incompatible_space,
invalid_vector,
invalid_rule,
non_affine_exterior_map
};
enum class EvaluationFailure : int {
none,
invalid_accepted_mapping,
accepted_determinant_below_floor,
invalid_mapping_variation,
non_finite_polynomial
};
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) noexcept {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
[[nodiscard]] bool matrix_is_finite(const mfem::DenseMatrix &matrix) noexcept {
for (int row = 0; row < matrix.Height(); ++row) {
for (int column = 0; column < matrix.Width(); ++column) {
if (!std::isfinite(matrix(row, column))) {
return false;
}
}
}
return true;
}
[[nodiscard]] bool
options_are_valid(const mean_field::deformation::LargestSafeNewtonStepSizeOptions &options) noexcept {
return std::isfinite(options.maximumStepSize) && options.maximumStepSize > 0.0 &&
std::isfinite(options.determinantFloor) && options.determinantFloor >= 0.0 &&
std::isfinite(options.fractionToBoundarySafety) && options.fractionToBoundarySafety > 0.0 &&
options.fractionToBoundarySafety < 1.0;
}
void require_mpi_success(
const int status,
const char *operation
) {
if (status != MPI_SUCCESS) {
throw std::runtime_error(operation);
}
}
[[nodiscard]] int collective_maximum(
const int localValue,
const MPI_Comm communicator,
const char *operation
) {
int globalValue = 0;
require_mpi_success(MPI_Allreduce(&localValue, &globalValue, 1, MPI_INT, MPI_MAX, communicator), operation);
return globalValue;
}
[[nodiscard]] double selected_entry(
const mfem::DenseMatrix &base,
const mfem::DenseMatrix &direction,
const unsigned int directionColumnMask,
const int row,
const int column,
const double maximumStepSize
) noexcept {
if ((directionColumnMask & (1U << static_cast<unsigned int>(column))) != 0U) {
return maximumStepSize * direction(row, column);
}
return base(row, column);
}
[[nodiscard]] double selected_column_determinant(
const mfem::DenseMatrix &base,
const mfem::DenseMatrix &direction,
const unsigned int directionColumnMask,
const int dimension,
const double maximumStepSize
) noexcept {
const auto entry = [&](const int row, const int column) {
return selected_entry(base, direction, directionColumnMask, row, column, maximumStepSize);
};
if (dimension == 1) {
return entry(0, 0);
}
if (dimension == 2) {
return entry(0, 0) * entry(1, 1) - entry(0, 1) * entry(1, 0);
}
return entry(0, 0) * (entry(1, 1) * entry(2, 2) - entry(1, 2) * entry(2, 1)) -
entry(0, 1) * (entry(1, 0) * entry(2, 2) - entry(1, 2) * entry(2, 0)) +
entry(0, 2) * (entry(1, 0) * entry(2, 1) - entry(1, 1) * entry(2, 0));
}
[[nodiscard]] Coefficients determinant_polynomial(
const mfem::DenseMatrix &base,
const mfem::DenseMatrix &direction,
const int dimension,
const double maximumStepSize,
const double determinantFloor
) noexcept {
Coefficients coefficients{};
const unsigned int termCount = 1U << static_cast<unsigned int>(dimension);
for (unsigned int mask = 0; mask < termCount; ++mask) {
const int degree = std::popcount(mask);
coefficients[static_cast<std::size_t>(degree)] +=
selected_column_determinant(base, direction, mask, dimension, maximumStepSize);
}
coefficients[0] -= determinantFloor;
return coefficients;
}
[[nodiscard]] double evaluate_polynomial(
const Coefficients &coefficients,
const double parameter
) noexcept {
return std::fma(
parameter, std::fma(parameter, std::fma(parameter, coefficients[3], coefficients[2]), coefficients[1]),
coefficients[0]
);
}
[[nodiscard]] int polynomial_degree(const Coefficients &coefficients) noexcept {
double scale = 0.0;
for (const double coefficient : coefficients) {
scale = std::max(scale, std::abs(coefficient));
}
const double tolerance = 64.0 * std::numeric_limits<double>::epsilon() * scale;
for (int degree = 3; degree > 0; --degree) {
if (std::abs(coefficients[static_cast<std::size_t>(degree)]) > tolerance) {
return degree;
}
}
return 0;
}
void append_unit_interval_root(
std::array<
double,
2> &roots,
int &rootCount,
const double root
) noexcept {
if (!std::isfinite(root) || root <= 0.0 || root >= 1.0) {
return;
}
if (rootCount > 0 && std::abs(root - roots[0]) <= 64.0 * std::numeric_limits<double>::epsilon()) {
return;
}
roots[static_cast<std::size_t>(rootCount)] = root;
++rootCount;
}
[[nodiscard]] int derivative_critical_points(
const Coefficients &coefficients,
const int degree,
std::array<
double,
2> &criticalPoints
) noexcept {
int count = 0;
if (degree == 2) {
append_unit_interval_root(criticalPoints, count, -coefficients[1] / (2.0 * coefficients[2]));
} else if (degree == 3) {
const double quadratic = 3.0 * coefficients[3];
const double linear = 2.0 * coefficients[2];
const double constant = coefficients[1];
const double discriminant = std::fma(linear, linear, -4.0 * quadratic * constant);
const double discriminantScale = linear * linear + std::abs(4.0 * quadratic * constant);
const double discriminantTolerance = 64.0 * std::numeric_limits<double>::epsilon() * discriminantScale;
if (discriminant >= -discriminantTolerance) {
const double squareRoot = std::sqrt(std::max(0.0, discriminant));
if (squareRoot == 0.0) {
append_unit_interval_root(criticalPoints, count, -linear / (2.0 * quadratic));
} else {
const double q = -0.5 * (linear + std::copysign(squareRoot, linear));
append_unit_interval_root(criticalPoints, count, q / quadratic);
append_unit_interval_root(criticalPoints, count, constant / q);
}
}
}
std::sort(criticalPoints.begin(), criticalPoints.begin() + count);
return count;
}
[[nodiscard]] double bisect_first_nonpositive_value(
const Coefficients &coefficients,
double lower,
double upper
) noexcept {
for (int iteration = 0; iteration < 80; ++iteration) {
const double middle = std::midpoint(lower, upper);
if (evaluate_polynomial(coefficients, middle) > 0.0) {
lower = middle;
} else {
upper = middle;
}
}
return upper;
}
[[nodiscard]] double first_boundary_parameter(const Coefficients &coefficients) noexcept {
const int degree = polynomial_degree(coefficients);
if (degree == 0) {
return std::numeric_limits<double>::infinity();
}
double coefficientScale = 0.0;
for (const double coefficient : coefficients) {
coefficientScale += std::abs(coefficient);
}
const double valueTolerance = 128.0 * std::numeric_limits<double>::epsilon() * coefficientScale;
std::array<double, 2> criticalPoints{};
const int criticalPointCount = derivative_critical_points(coefficients, degree, criticalPoints);
std::array<double, 4> intervalEnds{};
intervalEnds[0] = 0.0;
for (int index = 0; index < criticalPointCount; ++index) {
intervalEnds[static_cast<std::size_t>(index + 1)] = criticalPoints[static_cast<std::size_t>(index)];
}
intervalEnds[static_cast<std::size_t>(criticalPointCount + 1)] = 1.0;
for (int interval = 0; interval <= criticalPointCount; ++interval) {
const double lower = intervalEnds[static_cast<std::size_t>(interval)];
const double upper = intervalEnds[static_cast<std::size_t>(interval + 1)];
const double upperValue = evaluate_polynomial(coefficients, upper);
if (upperValue <= 0.0) {
return bisect_first_nonpositive_value(coefficients, lower, upper);
}
if (upperValue <= valueTolerance) {
// A repeated root only touches zero. Floating-point evaluation
// at the derivative root may land a few ulps above it.
return upper;
}
}
return std::numeric_limits<double>::infinity();
}
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;
}
}
} // namespace
namespace mean_field::deformation {
void LargestSafeNewtonStepSizeOptions::Validate() const {
if (!std::isfinite(maximumStepSize) || maximumStepSize <= 0.0) {
throw std::invalid_argument("A geometry preflight requires a finite, positive maximum step size.");
}
if (!std::isfinite(determinantFloor) || determinantFloor < 0.0) {
throw std::invalid_argument("A geometry preflight requires a finite, non-negative determinant floor.");
}
if (!std::isfinite(fractionToBoundarySafety) || fractionToBoundarySafety <= 0.0 ||
fractionToBoundarySafety >= 1.0) {
throw std::invalid_argument(
"A geometry preflight requires a finite fraction-to-boundary safety factor strictly between zero "
"and one."
);
}
}
LargestSafeNewtonStepSizeEstimate estimate_largest_safe_newton_step_size(
const mapping::DomainMapper &domainMapper,
const mfem::ParFiniteElementSpace &displacementSpace,
const mfem::ParGridFunction &compactificationCoordinate,
const mfem::Vector &acceptedVolumeDisplacement,
const mfem::Vector &volumeNewtonDirection,
const std::span<const NewtonStepGeometryRule> geometryRules,
const LargestSafeNewtonStepSizeOptions &options
) {
const MPI_Comm communicator = displacementSpace.GetComm();
if (communicator == MPI_COMM_NULL) {
throw std::invalid_argument("A geometry preflight requires a valid displacement communicator.");
}
const mfem::FiniteElementSpace *compactificationSpace = compactificationCoordinate.FESpace();
mfem::Mesh *mesh = displacementSpace.GetMesh();
InputFailure localInputFailure = InputFailure::none;
const auto recordInputFailure = [&](const InputFailure failure) {
localInputFailure =
static_cast<InputFailure>(std::max(static_cast<int>(localInputFailure), static_cast<int>(failure)));
};
if (!options_are_valid(options)) {
recordInputFailure(InputFailure::invalid_options);
}
const int dimension = domainMapper.GetDimension();
const mfem::Ordering::Type ordering = displacementSpace.GetOrdering();
if (mesh == nullptr || compactificationSpace == nullptr || compactificationSpace->GetMesh() != mesh ||
dimension < 1 || dimension > 3 || (mesh != nullptr && mesh->SpaceDimension() != dimension) ||
displacementSpace.GetVDim() != dimension ||
(compactificationSpace != nullptr && compactificationSpace->GetVDim() != 1) ||
(compactificationSpace != nullptr &&
compactificationCoordinate.Size() != compactificationSpace->GetVSize()) ||
(ordering != mfem::Ordering::byNODES && ordering != mfem::Ordering::byVDIM)) {
recordInputFailure(InputFailure::incompatible_space);
}
if (acceptedVolumeDisplacement.Size() != displacementSpace.GetTrueVSize() ||
volumeNewtonDirection.Size() != displacementSpace.GetTrueVSize() ||
!vector_is_finite(acceptedVolumeDisplacement) || !vector_is_finite(volumeNewtonDirection)) {
recordInputFailure(InputFailure::invalid_vector);
}
if (geometryRules.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
recordInputFailure(InputFailure::invalid_rule);
}
std::uint64_t localPointCount = 0;
if (mesh != nullptr) {
for (const NewtonStepGeometryRule &entry : geometryRules) {
if (entry.element < 0 || entry.element >= mesh->GetNE() || entry.integrationRule == nullptr ||
entry.integrationRule->GetNPoints() <= 0) {
recordInputFailure(InputFailure::invalid_rule);
continue;
}
localPointCount += static_cast<std::uint64_t>(entry.integrationRule->GetNPoints());
mfem::ElementTransformation *transformation = mesh->GetElementTransformation(entry.element);
const mfem::FiniteElement *displacementElement = displacementSpace.GetFE(entry.element);
const mfem::FiniteElement *compactificationElement =
compactificationSpace != nullptr ? compactificationSpace->GetFE(entry.element) : nullptr;
if (transformation == nullptr || displacementElement == nullptr || compactificationElement == nullptr ||
transformation->GetSpaceDim() != dimension || displacementElement->GetDim() != dimension ||
compactificationElement->GetDim() != dimension ||
displacementElement->GetGeomType() != compactificationElement->GetGeomType() ||
displacementElement->GetRangeType() != mfem::FiniteElement::SCALAR ||
displacementElement->GetMapType() != mfem::FiniteElement::VALUE ||
displacementElement->GetDerivType() != mfem::FiniteElement::GRAD ||
compactificationElement->GetRangeType() != mfem::FiniteElement::SCALAR ||
compactificationElement->GetMapType() != mfem::FiniteElement::VALUE ||
compactificationElement->GetDerivType() != mfem::FiniteElement::GRAD) {
recordInputFailure(InputFailure::invalid_rule);
} else if (
domainMapper.IsCompactifiedElement(*transformation) &&
!domainMapper.GetExteriorMap().IsAffineInDisplacement()
) {
recordInputFailure(InputFailure::non_affine_exterior_map);
}
}
}
const int globalInputFailure = collective_maximum(
static_cast<int>(localInputFailure), communicator,
"The geometry preflight could not validate its distributed inputs."
);
if (globalInputFailure != static_cast<int>(InputFailure::none)) {
switch (static_cast<InputFailure>(globalInputFailure)) {
case InputFailure::invalid_options:
throw std::invalid_argument("The geometry preflight options are invalid on at least one rank.");
case InputFailure::incompatible_space:
throw std::invalid_argument(
"The geometry preflight requires compatible displacement and compactification spaces in one to "
"three dimensions."
);
case InputFailure::invalid_vector:
throw std::invalid_argument(
"The geometry preflight received an incompatible or non-finite true-DOF displacement vector."
);
case InputFailure::invalid_rule:
throw std::invalid_argument("The geometry preflight received an invalid local quadrature rule.");
case InputFailure::non_affine_exterior_map:
throw std::invalid_argument(
"The geometry preflight requires compactified mappings that are affine in displacement."
);
case InputFailure::none:
break;
}
}
const std::array<double, 3> localOptions{
options.maximumStepSize, options.determinantFloor, options.fractionToBoundarySafety
};
std::array<double, 3> minimumOptions{};
std::array<double, 3> maximumOptions{};
require_mpi_success(
MPI_Allreduce(
localOptions.data(), minimumOptions.data(), static_cast<int>(localOptions.size()), MPI_DOUBLE, MPI_MIN,
communicator
),
"The geometry preflight could not compare its distributed options."
);
require_mpi_success(
MPI_Allreduce(
localOptions.data(), maximumOptions.data(), static_cast<int>(localOptions.size()), MPI_DOUBLE, MPI_MAX,
communicator
),
"The geometry preflight could not compare its distributed options."
);
if (minimumOptions != maximumOptions) {
throw std::invalid_argument("The geometry preflight requires identical options on every rank.");
}
std::uint64_t globalPointCount = 0;
require_mpi_success(
MPI_Allreduce(&localPointCount, &globalPointCount, 1, MPI_UINT64_T, MPI_SUM, communicator),
"The geometry preflight could not count its distributed samples."
);
if (globalPointCount == 0) {
throw std::invalid_argument("The geometry preflight requires at least one quadrature point globally.");
}
mfem::Vector acceptedLocal;
mfem::Vector directionLocal;
true_to_local(displacementSpace, acceptedVolumeDisplacement, acceptedLocal);
true_to_local(displacementSpace, volumeNewtonDirection, directionLocal);
mapping::DomainMapper::Workspace workspace(dimension);
mapping::MappingPointContext mappingContext;
mapping::MappingPointVariation mappingVariation;
mfem::Array<int> displacementDofs;
mfem::Array<int> compactificationDofs;
mfem::Vector elementAcceptedDisplacement;
mfem::Vector elementDirection;
mfem::Vector elementCompactification;
double localBoundaryStep = std::numeric_limits<double>::infinity();
double localMinimumAtAccepted = std::numeric_limits<double>::infinity();
double localMinimumAtMaximum = std::numeric_limits<double>::infinity();
Coefficients localLimitingCoefficients{};
int localLimitingElement = -1;
int localLimitingRule = -1;
int localLimitingPoint = -1;
EvaluationFailure localEvaluationFailure = EvaluationFailure::none;
const auto recordEvaluationFailure = [&](const EvaluationFailure failure) {
localEvaluationFailure = static_cast<EvaluationFailure>(
std::max(static_cast<int>(localEvaluationFailure), static_cast<int>(failure))
);
};
for (std::size_t ruleIndex = 0; ruleIndex < geometryRules.size(); ++ruleIndex) {
const NewtonStepGeometryRule &entry = geometryRules[ruleIndex];
mfem::ElementTransformation *transformation = mesh->GetElementTransformation(entry.element);
mfem::DofTransformation *displacementDofTransformation =
displacementSpace.GetElementVDofs(entry.element, displacementDofs);
mfem::DofTransformation *compactificationDofTransformation =
compactificationSpace->GetElementDofs(entry.element, compactificationDofs);
acceptedLocal.GetSubVector(displacementDofs, elementAcceptedDisplacement);
directionLocal.GetSubVector(displacementDofs, elementDirection);
compactificationCoordinate.GetSubVector(compactificationDofs, elementCompactification);
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->InvTransformPrimal(elementAcceptedDisplacement);
displacementDofTransformation->InvTransformPrimal(elementDirection);
}
if (compactificationDofTransformation != nullptr) {
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
}
const mfem::FiniteElement &displacementElement = *displacementSpace.GetFE(entry.element);
const mfem::FiniteElement &compactificationElement = *compactificationSpace->GetFE(entry.element);
const mapping::ElementDisplacementData acceptedData(
displacementElement, elementAcceptedDisplacement, displacementSpace.GetOrdering()
);
const mapping::ElementDisplacementData directionData(
displacementElement, elementDirection, displacementSpace.GetOrdering()
);
const mapping::ElementCompactificationData compactificationData(
compactificationElement, elementCompactification
);
const mapping::ElementMappingData elementData{
.displacement = acceptedData, .compactification = compactificationData
};
for (int point = 0; point < entry.integrationRule->GetNPoints(); ++point) {
const mfem::IntegrationPoint &integrationPoint = entry.integrationRule->IntPoint(point);
const mapping::MappingStatus mappingStatus = domainMapper.EvaluatePoint(
elementData, *transformation, integrationPoint, workspace, mappingContext
);
if (mappingStatus != mapping::MappingStatus::valid) {
recordEvaluationFailure(EvaluationFailure::invalid_accepted_mapping);
continue;
}
if (mappingContext.mapping_determinant <= options.determinantFloor) {
recordEvaluationFailure(EvaluationFailure::accepted_determinant_below_floor);
continue;
}
const mapping::MappingStatus variationStatus = domainMapper.EvaluatePointVariation(
elementData, directionData, *transformation, integrationPoint, mappingContext, workspace,
mappingVariation
);
if (variationStatus != mapping::MappingStatus::valid) {
recordEvaluationFailure(EvaluationFailure::invalid_mapping_variation);
continue;
}
if (!matrix_is_finite(mappingContext.mapping_jacobian) ||
!matrix_is_finite(mappingVariation.mapping_jacobian_variation)) {
recordEvaluationFailure(EvaluationFailure::invalid_mapping_variation);
continue;
}
Coefficients coefficients = determinant_polynomial(
mappingContext.mapping_jacobian, mappingVariation.mapping_jacobian_variation, dimension,
options.maximumStepSize, options.determinantFloor
);
// Use the mapper's own determinant at the accepted state to
// avoid a second, slightly different round-off path.
coefficients[0] = mappingContext.mapping_determinant - options.determinantFloor;
const double determinantAtMaximum = evaluate_polynomial(coefficients, 1.0) + options.determinantFloor;
if (!std::isfinite(determinantAtMaximum)) {
recordEvaluationFailure(EvaluationFailure::non_finite_polynomial);
continue;
}
localMinimumAtAccepted = std::min(localMinimumAtAccepted, mappingContext.mapping_determinant);
localMinimumAtMaximum = std::min(localMinimumAtMaximum, determinantAtMaximum);
const double boundaryParameter = first_boundary_parameter(coefficients);
if (std::isfinite(boundaryParameter)) {
const double boundaryStep = options.maximumStepSize * boundaryParameter;
if (boundaryStep < localBoundaryStep) {
localBoundaryStep = boundaryStep;
localLimitingCoefficients = coefficients;
localLimitingElement = entry.element;
localLimitingRule = static_cast<int>(ruleIndex);
localLimitingPoint = point;
}
}
}
}
const int globalEvaluationFailure = collective_maximum(
static_cast<int>(localEvaluationFailure), communicator,
"The geometry preflight could not combine its distributed mapping status."
);
if (globalEvaluationFailure != static_cast<int>(EvaluationFailure::none)) {
switch (static_cast<EvaluationFailure>(globalEvaluationFailure)) {
case EvaluationFailure::invalid_accepted_mapping:
throw std::domain_error(
"The geometry preflight received an accepted displacement with an invalid mapped geometry."
);
case EvaluationFailure::accepted_determinant_below_floor:
throw std::domain_error(
"The accepted displacement does not lie strictly above the requested determinant floor."
);
case EvaluationFailure::invalid_mapping_variation:
throw std::domain_error("The geometry preflight could not evaluate the mapping direction.");
case EvaluationFailure::non_finite_polynomial:
throw std::domain_error("The geometry preflight produced a non-finite determinant polynomial.");
case EvaluationFailure::none:
break;
}
}
double globalMinimumAtAccepted = 0.0;
double globalMinimumAtMaximum = 0.0;
require_mpi_success(
MPI_Allreduce(&localMinimumAtAccepted, &globalMinimumAtAccepted, 1, MPI_DOUBLE, MPI_MIN, communicator),
"The geometry preflight could not reduce its accepted-state determinant."
);
require_mpi_success(
MPI_Allreduce(&localMinimumAtMaximum, &globalMinimumAtMaximum, 1, MPI_DOUBLE, MPI_MIN, communicator),
"The geometry preflight could not reduce its maximum-step determinant."
);
int rank = 0;
require_mpi_success(MPI_Comm_rank(communicator, &rank), "The geometry preflight could not identify its rank.");
struct BoundaryLocation {
double step;
int rank;
};
const BoundaryLocation localLocation{.step = localBoundaryStep, .rank = rank};
BoundaryLocation globalLocation{};
require_mpi_success(
MPI_Allreduce(&localLocation, &globalLocation, 1, MPI_DOUBLE_INT, MPI_MINLOC, communicator),
"The geometry preflight could not select its limiting point."
);
const bool limitedByGeometry = std::isfinite(globalLocation.step);
std::array<int, 3> limitingLocation{-1, -1, -1};
Coefficients limitingCoefficients{};
if (limitedByGeometry) {
if (rank == globalLocation.rank) {
limitingLocation = {localLimitingElement, localLimitingRule, localLimitingPoint};
limitingCoefficients = localLimitingCoefficients;
}
require_mpi_success(
MPI_Bcast(
limitingLocation.data(), static_cast<int>(limitingLocation.size()), MPI_INT, globalLocation.rank,
communicator
),
"The geometry preflight could not broadcast its limiting location."
);
require_mpi_success(
MPI_Bcast(
limitingCoefficients.data(), static_cast<int>(limitingCoefficients.size()), MPI_DOUBLE,
globalLocation.rank, communicator
),
"The geometry preflight could not broadcast its limiting polynomial."
);
}
const double boundaryStepSize = limitedByGeometry ? globalLocation.step : options.maximumStepSize;
const double stepSize =
limitedByGeometry ? options.fractionToBoundarySafety * boundaryStepSize : options.maximumStepSize;
const double limitingPointDeterminantAtStepSize =
limitedByGeometry ? evaluate_polynomial(limitingCoefficients, stepSize / options.maximumStepSize) +
options.determinantFloor
: globalMinimumAtMaximum;
return {
.stepSize = stepSize,
.boundaryStepSize = boundaryStepSize,
.minimumDeterminantAtAcceptedState = globalMinimumAtAccepted,
.minimumDeterminantAtMaximumStepSize = globalMinimumAtMaximum,
.limitingPointDeterminantAtStepSize = limitingPointDeterminantAtStepSize,
.sampledQuadraturePointCount = globalPointCount,
.limitedByGeometry = limitedByGeometry,
.limitingRank = limitedByGeometry ? globalLocation.rank : -1,
.limitingElement = limitingLocation[0],
.limitingRule = limitingLocation[1],
.limitingQuadraturePoint = limitingLocation[2]
};
}
} // namespace mean_field::deformation

View File

@@ -33,6 +33,7 @@ namespace mean_field::fem {
using DisplacementVector = field::Displacement::Vector;
using DensityScalar = field::Density::Scalar;
using EnthalpyScalar = field::Enthalpy::Scalar;
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
// =====================================================================
// Section 1: Mesh construction
@@ -44,19 +45,33 @@ namespace mean_field::fem {
stroid::refinement::UniformRefinement(fem.smesh, extraRefine);
}
if (fem.smesh.mesh == nullptr || fem.smesh.reference_mesh == nullptr) {
throw std::runtime_error("A STROID mesh requires paired physical and logical reference meshes.");
}
int mpiSize = 1;
MPI_Comm_size(MPI_COMM_WORLD, &mpiSize);
const std::unique_ptr<int[]> meshPartitioning(
fem.smesh.mesh->GeneratePartitioning(mpiSize, 1)
);
const std::unique_ptr<int[]> meshPartitioning(fem.smesh.mesh->GeneratePartitioning(mpiSize, 1));
fem.mesh = std::make_unique<mfem::ParMesh>(
MPI_COMM_WORLD, *fem.smesh.mesh, meshPartitioning.get(), 1
);
fem.mesh = std::make_unique<mfem::ParMesh>(MPI_COMM_WORLD, *fem.smesh.mesh, meshPartitioning.get(), 1);
fem.logicalReferenceMesh =
std::make_unique<mfem::ParMesh>(MPI_COMM_WORLD, *fem.smesh.reference_mesh, meshPartitioning.get(), 1);
fem.mesh->EnsureNodes();
if (fem.logicalReferenceMesh->GetNE() != fem.mesh->GetNE()) {
throw std::runtime_error("The physical and logical reference meshes have incompatible local elements.");
}
for (int element = 0; element < fem.mesh->GetNE(); ++element) {
if (fem.logicalReferenceMesh->GetElementGeometry(element) != fem.mesh->GetElementGeometry(element) ||
fem.logicalReferenceMesh->GetAttribute(element) != fem.mesh->GetAttribute(element)) {
throw std::runtime_error(
"The physical and logical reference meshes do not preserve element correspondence."
);
}
}
// =====================================================================
// Section 2: Exterior compactification coordinate
// =====================================================================
@@ -73,11 +88,9 @@ namespace mean_field::fem {
throw std::runtime_error("Values for exterior coordinate not set.");
}
const mfem::FiniteElementSpace &serialCoordinateSpace =
*fem.smesh.exterior_coordinate->space;
const mfem::FiniteElementSpace &serialCoordinateSpace = *fem.smesh.exterior_coordinate->space;
const mfem::GridFunction &serialCoordinate =
*fem.smesh.exterior_coordinate->values;
const mfem::GridFunction &serialCoordinate = *fem.smesh.exterior_coordinate->values;
if (serialCoordinate.FESpace() != &serialCoordinateSpace) {
throw std::runtime_error(
@@ -94,9 +107,7 @@ namespace mean_field::fem {
}
if (serialCoordinateSpace.GetVDim() != 1) {
throw std::runtime_error(
"Exterior coordinate must be a scalar field."
);
throw std::runtime_error("Exterior coordinate must be a scalar field.");
}
if (serialCoordinate.Size() != serialCoordinateSpace.GetVSize()) {
@@ -106,35 +117,25 @@ namespace mean_field::fem {
);
}
const int compactificationOrder =
serialCoordinateSpace.GetMaxElementOrder();
const int compactificationOrder = serialCoordinateSpace.GetMaxElementOrder();
const int dimension = fem.mesh->Dimension();
fem.compactificationFec = std::make_unique<mfem::H1_FECollection>(
compactificationOrder, dimension
);
fem.compactificationFec = std::make_unique<mfem::H1_FECollection>(compactificationOrder, dimension);
fem.compactificationFes = std::make_unique<mfem::ParFiniteElementSpace>(
fem.mesh.get(), fem.compactificationFec.get()
);
fem.compactificationFes =
std::make_unique<mfem::ParFiniteElementSpace>(fem.mesh.get(), fem.compactificationFec.get());
mfem::ParGridFunction distributedCoordinate(
fem.mesh.get(), &serialCoordinate, meshPartitioning.get()
);
mfem::ParGridFunction distributedCoordinate(fem.mesh.get(), &serialCoordinate, meshPartitioning.get());
if (distributedCoordinate.Size() !=
fem.compactificationFes->GetVSize()) {
if (distributedCoordinate.Size() != fem.compactificationFes->GetVSize()) {
throw std::runtime_error(
"Distributed exterior coordinate does not match the "
"constructed parallel finite-element space."
);
}
fem.compactificationCoordinate =
std::make_unique<mfem::ParGridFunction>(
fem.compactificationFes.get()
);
fem.compactificationCoordinate = std::make_unique<mfem::ParGridFunction>(fem.compactificationFes.get());
*fem.compactificationCoordinate = distributedCoordinate;
@@ -142,14 +143,11 @@ namespace mean_field::fem {
double localMaximum = -std::numeric_limits<double>::infinity();
for (int index = 0; index < fem.compactificationCoordinate->Size();
++index) {
for (int index = 0; index < fem.compactificationCoordinate->Size(); ++index) {
const double value = (*fem.compactificationCoordinate)(index);
if (!std::isfinite(value)) {
throw std::runtime_error(
"Exterior coordinate contains a non-finite value."
);
throw std::runtime_error("Exterior coordinate contains a non-finite value.");
}
localMinimum = std::min(localMinimum, value);
@@ -160,20 +158,13 @@ namespace mean_field::fem {
double globalMinimum = 0.0;
double globalMaximum = 0.0;
MPI_Allreduce(
&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN,
MPI_COMM_WORLD
);
MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, MPI_COMM_WORLD);
MPI_Allreduce(
&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX,
MPI_COMM_WORLD
);
MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD);
constexpr double coordinateTolerance = 1.0e-12;
if (globalMinimum < -coordinateTolerance ||
globalMaximum > 1.0 + coordinateTolerance) {
if (globalMinimum < -coordinateTolerance || globalMaximum > 1.0 + coordinateTolerance) {
throw std::runtime_error(
"Exterior coordinate lies outside the expected "
"interval [0, 1]."
@@ -188,12 +179,9 @@ namespace mean_field::fem {
// Gravity potential: scalar L2
// ---------------------------------------------------------------------
fem.gravityPotentialFec =
GravityField::make_fec<GravityPotential>(dimension);
fem.gravityPotentialFec = GravityField::make_fec<GravityPotential>(dimension);
fem.gravityPotentialFes = GravityField::make_fespace<GravityPotential>(
*fem.mesh, *fem.gravityPotentialFec
);
fem.gravityPotentialFes = GravityField::make_fespace<GravityPotential>(*fem.mesh, *fem.gravityPotentialFec);
// ---------------------------------------------------------------------
// Gravity flux: H(div)/RT. Basis choices are encoded by field.mfem.
@@ -201,36 +189,38 @@ namespace mean_field::fem {
fem.gravityFluxFec = GravityField::make_fec<GravityFlux>(dimension);
fem.gravityFluxFes = GravityField::make_fespace<GravityFlux>(
*fem.mesh, *fem.gravityFluxFec
);
fem.gravityFluxFes = GravityField::make_fespace<GravityFlux>(*fem.mesh, *fem.gravityFluxFec);
// ---------------------------------------------------------------------
// Displacement: vector H1. Ordering is encoded by field.mfem.
// ---------------------------------------------------------------------
fem.displacementFec =
DisplacementField::make_fec<DisplacementVector>(dimension);
fem.displacementFec = DisplacementField::make_fec<DisplacementVector>(dimension);
fem.displacementFes =
DisplacementField::make_fespace<DisplacementVector>(
*fem.mesh, *fem.displacementFec
);
fem.displacementFes = DisplacementField::make_fespace<DisplacementVector>(*fem.mesh, *fem.displacementFec);
fem.displacement =
std::make_unique<mfem::ParGridFunction>(fem.displacementFes.get());
fem.displacement = std::make_unique<mfem::ParGridFunction>(fem.displacementFes.get());
*fem.displacement = 0.0;
// ---------------------------------------------------------------------
// Surface deformation: scalar H1 coordinates on StellarSurface.
//
// This ambient scalar space exists only to define the surface basis
// and owned true-DOF topology. Interior scalar DOFs are not nonlinear
// unknowns.
// ---------------------------------------------------------------------
fem.surfaceDeformationFes =
std::make_unique<mfem::ParFiniteElementSpace>(fem.mesh.get(), fem.displacementFec.get());
// ---------------------------------------------------------------------
// Density: scalar discontinuous L2
// ---------------------------------------------------------------------
fem.densityFec = DensityField::make_fec<DensityScalar>(dimension);
fem.densityFes = DensityField::make_fespace<DensityScalar>(
*fem.mesh, *fem.densityFec
);
fem.densityFes = DensityField::make_fespace<DensityScalar>(*fem.mesh, *fem.densityFec);
// ---------------------------------------------------------------------
// Specific enthalpy: scalar continuous H1
@@ -238,60 +228,10 @@ namespace mean_field::fem {
fem.enthalpyFec = EnthalpyField::make_fec<EnthalpyScalar>(dimension);
fem.enthalpyFes = EnthalpyField::make_fespace<EnthalpyScalar>(
*fem.mesh, *fem.enthalpyFec
);
fem.enthalpyFes = EnthalpyField::make_fespace<EnthalpyScalar>(*fem.mesh, *fem.enthalpyFec);
// =====================================================================
// Section 4: Domain mapping
// =====================================================================
auto [stellarRadiusReference, infinityRadiusReference] =
utils::discover_bounds(fem.mesh.get(), 3)
.or_else(
[](const boundary::BoundsError &)
-> std::expected<
boundary::Bounds, boundary::BoundsError> {
throw std::runtime_error(
"Unable to determine vacuum-domain reference "
"boundaries."
);
}
)
.value();
fem.mapping = std::make_unique<mapping::DomainMapper>(
*fem.displacement, stellarRadiusReference, infinityRadiusReference
);
// =====================================================================
// Section 5: Block offsets
//
// Legacy layouts only. New coupled operators use :utils.blocks forms.
//
// Main system: [Displacement | Density]
// Gravity system: [Flux | Potential]
// =====================================================================
fem.blockTrueOffsets.SetSize(3);
fem.blockTrueOffsets[0] = 0;
fem.blockTrueOffsets[1] = fem.displacementFes->GetTrueVSize();
fem.blockTrueOffsets[2] =
fem.blockTrueOffsets[1] + fem.densityFes->GetTrueVSize();
fem.gravityBlockTrueOffsets.SetSize(3);
fem.gravityBlockTrueOffsets[0] = 0;
fem.gravityBlockTrueOffsets[1] = fem.gravityFluxFes->GetTrueVSize();
fem.gravityBlockTrueOffsets[2] =
fem.gravityBlockTrueOffsets[1] +
fem.gravityPotentialFes->GetTrueVSize();
// =====================================================================
// Section 6: Multipole data
// Section 4: Multipole data
// =====================================================================
fem.com.SetSize(dimension);
@@ -301,16 +241,9 @@ namespace mean_field::fem {
fem.Q = 0.0;
// =====================================================================
// Section 7: Essential boundaries and domain masks
// Section 5: Boundary markers
// =====================================================================
fem.essentialDisplacementTdofs.SetSize(0);
populate_element_mask(
fem.mesh.get(), utils::DOMAINS::STELLAR,
fem.gravityContext.stellar_mask
);
const int boundaryAttributeCount = fem.mesh->bdr_attributes.Max();
fem.boundaryContext.inf_bounds.SetSize(boundaryAttributeCount);
@@ -320,108 +253,41 @@ namespace mean_field::fem {
fem.boundaryContext.inf_bounds = 0;
fem.boundaryContext.stellar_bounds = 0;
fem.boundaryContext.inf_bounds
[static_cast<int>(boundary::Boundaries::INF_SURFACE) - 1] = 1;
fem.boundaryContext.inf_bounds[static_cast<int>(boundary::Boundaries::INF_SURFACE) - 1] = 1;
fem.boundaryContext.stellar_bounds
[static_cast<int>(boundary::Boundaries::STELLAR_SURFACE) - 1] = 1;
fem.boundaryContext.stellar_bounds[static_cast<int>(boundary::Boundaries::STELLAR_SURFACE) - 1] = 1;
// =====================================================================
// Section 8: Gravity solver context
// Section 7: Quadrature policy
// =====================================================================
fem.gravityContext.minres =
std::make_unique<mfem::MINRESSolver>(fem.mesh->GetComm());
const quadrature::QuadratureOptions &quadratureOptions = args.quadrature;
fem.gravityContext.minres->SetRelTol(1.0e-12);
fem.gravityContext.minres->SetAbsTol(1.0e-12);
fem.gravityContext.minres->SetMaxIter(1000);
fem.gravityContext.minres->SetPrintLevel(0);
fem.gravityContext.prec_Phi = std::make_unique<mfem::HypreBoomerAMG>();
fem.gravityContext.prec_Phi->SetPrintLevel(0);
fem.gravityContext.block_prec =
std::make_unique<mfem::BlockDiagonalPreconditioner>(
fem.gravityBlockTrueOffsets
);
fem.gravityContext.minres->SetPreconditioner(
*fem.gravityContext.block_prec
);
// =====================================================================
// Section 9: Vacuum true-DOF masks
// =====================================================================
{
mfem::Array<int> vacuumMask;
utils::populate_element_mask(
fem.mesh.get(), utils::DOMAINS::VACUUM, vacuumMask
);
utils::populate_domain_tdofs(
fem.displacementFes.get(), vacuumMask,
fem.vacuumDisplacementTdofs
);
utils::populate_domain_tdofs(
fem.densityFes.get(), vacuumMask, fem.vacuumDensityTdofs
);
utils::populate_domain_tdofs(
fem.enthalpyFes.get(), vacuumMask, fem.vacuumEnthalpyTdofs
);
if (quadratureOptions.validation.reject_negative_boosts && quadratureOptions.global_boost < 0) {
throw std::invalid_argument("Global quadrature boost cannot be negative.");
}
// =====================================================================
// Section 10: Quadrature policy
// =====================================================================
const quadrature::QuadratureOptions &quadratureOptions =
args.quadrature;
if (quadratureOptions.validation.reject_negative_boosts &&
quadratureOptions.global_boost < 0) {
throw std::invalid_argument(
"Global quadrature boost cannot be negative."
);
}
quadrature::RuleSet quadratureRuleSet = quadrature::make_rule_set(
quadratureOptions.mode, quadratureOptions.global_boost
);
quadrature::RuleSet quadratureRuleSet =
quadrature::make_rule_set(quadratureOptions.mode, quadratureOptions.global_boost);
if (quadratureOptions.fallback_fixed_order.has_value()) {
if (*quadratureOptions.fallback_fixed_order < 0) {
throw std::invalid_argument(
"Fallback quadrature order cannot be negative."
);
throw std::invalid_argument("Fallback quadrature order cannot be negative.");
}
quadratureRuleSet.fallback.fixed_order =
quadratureOptions.fallback_fixed_order;
quadratureRuleSet.fallback.fixed_order = quadratureOptions.fallback_fixed_order;
}
auto apply_quadrature_options =
[&quadratureOptions](
auto apply_quadrature_options = [&quadratureOptions](
quadrature::RuleControl &ruleControl,
const quadrature::QuadratureTermOptions &termOptions
) {
if (termOptions.fixed_order.has_value() &&
*termOptions.fixed_order < 0) {
throw std::invalid_argument(
"Fixed quadrature order cannot be negative."
);
if (termOptions.fixed_order.has_value() && *termOptions.fixed_order < 0) {
throw std::invalid_argument("Fixed quadrature order cannot be negative.");
}
if (quadratureOptions.validation.reject_negative_boosts &&
termOptions.additional_boost < 0) {
throw std::invalid_argument(
"Term quadrature boost cannot be negative."
);
if (quadratureOptions.validation.reject_negative_boosts && termOptions.additional_boost < 0) {
throw std::invalid_argument("Term quadrature boost cannot be negative.");
}
ruleControl.boost += termOptions.additional_boost;
@@ -431,129 +297,74 @@ namespace mean_field::fem {
}
};
apply_quadrature_options(
quadratureRuleSet.gravity_hdiv_mass,
quadratureOptions.gravity_hdiv_mass
);
apply_quadrature_options(quadratureRuleSet.gravity_hdiv_mass, quadratureOptions.gravity_hdiv_mass);
apply_quadrature_options(
quadratureRuleSet.gravity_divergence,
quadratureOptions.gravity_divergence
);
apply_quadrature_options(quadratureRuleSet.gravity_divergence, quadratureOptions.gravity_divergence);
apply_quadrature_options(
quadratureRuleSet.gravity_source, quadratureOptions.gravity_source
);
apply_quadrature_options(quadratureRuleSet.gravity_source, quadratureOptions.gravity_source);
apply_quadrature_options(
quadratureRuleSet.gravity_boundary,
quadratureOptions.gravity_boundary
);
apply_quadrature_options(quadratureRuleSet.gravity_force, quadratureOptions.gravity_force);
apply_quadrature_options(
quadratureRuleSet.centrifugal, quadratureOptions.centrifugal
);
apply_quadrature_options(quadratureRuleSet.gravity_boundary, quadratureOptions.gravity_boundary);
apply_quadrature_options(
quadratureRuleSet.density_projection,
quadratureOptions.density_projection
);
apply_quadrature_options(quadratureRuleSet.centrifugal, quadratureOptions.centrifugal);
apply_quadrature_options(
quadratureRuleSet.eos_closure, quadratureOptions.eos_closure
);
apply_quadrature_options(quadratureRuleSet.density_projection, quadratureOptions.density_projection);
apply_quadrature_options(
quadratureRuleSet.hydrostatic_equilibrium,
quadratureOptions.hydrostatic_equilibrium
);
apply_quadrature_options(quadratureRuleSet.eos_closure, quadratureOptions.eos_closure);
apply_quadrature_options(
quadratureRuleSet.isobaric_surface,
quadratureOptions.isobaric_surface
);
apply_quadrature_options(quadratureRuleSet.hydrostatic_equilibrium, quadratureOptions.hydrostatic_equilibrium);
apply_quadrature_options(
quadratureRuleSet.mesh_extension, quadratureOptions.mesh_extension
);
apply_quadrature_options(quadratureRuleSet.isobaric_surface, quadratureOptions.isobaric_surface);
apply_quadrature_options(
quadratureRuleSet.mass_conservation,
quadratureOptions.mass_conservation
);
apply_quadrature_options(quadratureRuleSet.mesh_extension, quadratureOptions.mesh_extension);
apply_quadrature_options(
quadratureRuleSet.mass_normalization,
quadratureOptions.mass_normalization
);
apply_quadrature_options(quadratureRuleSet.mass_conservation, quadratureOptions.mass_conservation);
apply_quadrature_options(
quadratureRuleSet.center_of_mass, quadratureOptions.center_of_mass
);
apply_quadrature_options(quadratureRuleSet.mass_normalization, quadratureOptions.mass_normalization);
apply_quadrature_options(
quadratureRuleSet.quadrupole, quadratureOptions.quadrupole
);
apply_quadrature_options(quadratureRuleSet.center_of_mass, quadratureOptions.center_of_mass);
apply_quadrature_options(
quadratureRuleSet.gravitational_energy,
quadratureOptions.gravitational_energy
);
apply_quadrature_options(quadratureRuleSet.quadrupole, quadratureOptions.quadrupole);
apply_quadrature_options(
quadratureRuleSet.pressure_integral,
quadratureOptions.pressure_integral
);
apply_quadrature_options(quadratureRuleSet.gravitational_energy, quadratureOptions.gravitational_energy);
apply_quadrature_options(
quadratureRuleSet.pressure_force, quadratureOptions.pressure_force
);
apply_quadrature_options(quadratureRuleSet.pressure_integral, quadratureOptions.pressure_integral);
apply_quadrature_options(
quadratureRuleSet.virial, quadratureOptions.virial
);
apply_quadrature_options(quadratureRuleSet.pressure_force, quadratureOptions.pressure_force);
apply_quadrature_options(
quadratureRuleSet.error_norm, quadratureOptions.error_norm
);
apply_quadrature_options(quadratureRuleSet.virial, quadratureOptions.virial);
apply_quadrature_options(
quadratureRuleSet.roles.discretization,
quadratureOptions.roles.discretization
);
apply_quadrature_options(quadratureRuleSet.error_norm, quadratureOptions.error_norm);
apply_quadrature_options(
quadratureRuleSet.roles.preconditioner,
quadratureOptions.roles.preconditioner
);
apply_quadrature_options(quadratureRuleSet.roles.discretization, quadratureOptions.roles.discretization);
apply_quadrature_options(
quadratureRuleSet.roles.diagnostic,
quadratureOptions.roles.diagnostic
);
apply_quadrature_options(quadratureRuleSet.roles.preconditioner, quadratureOptions.roles.preconditioner);
apply_quadrature_options(
quadratureRuleSet.roles.projection,
quadratureOptions.roles.projection
);
apply_quadrature_options(quadratureRuleSet.roles.diagnostic, quadratureOptions.roles.diagnostic);
fem.quadratureFactory = std::make_unique<quadrature::RuleFactory>(
quadrature::Policy(std::move(quadratureRuleSet))
);
apply_quadrature_options(quadratureRuleSet.roles.projection, quadratureOptions.roles.projection);
fem.quadratureFactory =
std::make_unique<quadrature::RuleFactory>(quadrature::Policy(std::move(quadratureRuleSet)));
// =====================================================================
// Section 11: Stateless domain mapper
// =====================================================================
auto exteriorDomain = std::make_unique<
const mapping::compactification::KelvinCompactification>(
args.kelvin_options
auto exteriorDomain =
std::make_unique<const mapping::compactification::KelvinCompactification>(args.kelvin_options);
MFEM_VERIFY(
args.domain_mapper_options.vacuum_element_attribute ==
DomainSchema::template material_attribute<utils::domain::Vacuum>(),
"The domain-mapper compactification attribute must match the vacuum "
"material registered by the "
"production domain schema."
);
fem.domainMapperStateless =
std::make_unique<mapping::DomainMapperStateless>(
args.domain_mapper_options, std::move(exteriorDomain)
);
std::make_unique<mapping::DomainMapper>(args.domain_mapper_options, std::move(exteriorDomain));
return fem;
}

View File

@@ -0,0 +1,159 @@
module;
#include <array>
#include <cmath>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <utility>
#include <vector>
#include <mfem.hpp>
module mean_field;
import :fem.reference_tables;
namespace mean_field::fem {
namespace {
struct ReferenceTableKey {
const mfem::FiniteElement *element;
std::vector<std::array<double, 4>> points;
bool operator<(const ReferenceTableKey &other) const {
if (element != other.element)
return std::less<const mfem::FiniteElement *>{}(element, other.element);
return points < other.points;
}
};
ReferenceTableKey make_key(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
) {
ReferenceTableKey key{.element = &element, .points = {}};
key.points.reserve(rule.GetNPoints());
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto &point = rule.IntPoint(q);
const std::array<double, 4> values{
point.x, element.GetDim() > 1 ? point.y : 0.0, element.GetDim() > 2 ? point.z : 0.0, point.weight
};
for (const double value : values) {
if (!std::isfinite(value))
throw std::invalid_argument("Reference table quadrature entries must be finite.");
}
key.points.push_back(values);
}
return key;
}
} // namespace
struct ReferenceTableCache::Storage {
std::mutex mutex;
std::map<ReferenceTableKey, std::shared_ptr<const ScalarReferenceTable>> scalar_tables;
std::map<ReferenceTableKey, std::shared_ptr<const VectorReferenceTable>> vector_tables;
};
ReferenceTableCache::ReferenceTableCache() : m_storage(std::make_unique<Storage>()) {
}
ReferenceTableCache::~ReferenceTableCache() = default;
std::shared_ptr<const ScalarReferenceTable> ReferenceTableCache::GetScalarTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
) const {
if (element.GetRangeType() != mfem::FiniteElement::SCALAR)
throw std::invalid_argument("A scalar reference table requires a scalar finite element.");
auto key = make_key(element, rule);
const std::lock_guard lock(m_storage->mutex);
if (const auto found = m_storage->scalar_tables.find(key); found != m_storage->scalar_tables.end())
return found->second;
auto table = std::shared_ptr<const ScalarReferenceTable>(new ScalarReferenceTable(element, rule));
m_storage->scalar_tables.emplace(std::move(key), table);
return table;
}
std::shared_ptr<const VectorReferenceTable> ReferenceTableCache::GetVectorTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
) const {
if (element.GetRangeType() != mfem::FiniteElement::VECTOR)
throw std::invalid_argument("A vector reference table requires a vector finite element.");
auto key = make_key(element, rule);
const std::lock_guard lock(m_storage->mutex);
if (const auto found = m_storage->vector_tables.find(key); found != m_storage->vector_tables.end())
return found->second;
auto table = std::shared_ptr<const VectorReferenceTable>(new VectorReferenceTable(element, rule));
m_storage->vector_tables.emplace(std::move(key), table);
return table;
}
ScalarReferenceTable::ScalarReferenceTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
)
: m_values(
rule.GetNPoints(),
element.GetDof()
),
m_dimension(element.GetDim()) {
mfem::Vector values(element.GetDof());
if (element.GetDerivType() == mfem::FiniteElement::GRAD)
m_gradients.resize(rule.GetNPoints());
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto &point = rule.IntPoint(q);
element.CalcShape(point, values);
for (int dof = 0; dof < element.GetDof(); ++dof)
m_values(q, dof) = values(dof);
if (!m_gradients.empty()) {
auto &gradient = m_gradients[q];
gradient.SetSize(element.GetDof(), m_dimension);
element.CalcDShape(point, gradient);
}
}
}
const mfem::DenseMatrix &ScalarReferenceTable::GetValues() const {
return m_values;
}
const mfem::DenseMatrix &ScalarReferenceTable::GetGradients(const int point) const {
return m_gradients.at(point);
}
int ScalarReferenceTable::GetPointCount() const {
return m_values.Height();
}
int ScalarReferenceTable::GetDofCount() const {
return m_values.Width();
}
int ScalarReferenceTable::GetDimension() const {
return m_dimension;
}
VectorReferenceTable::VectorReferenceTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
)
: m_dof_count(element.GetDof()),
m_dimension(element.GetRangeDim()) {
m_values.resize(rule.GetNPoints());
for (int q = 0; q < rule.GetNPoints(); ++q) {
auto &values = m_values[q];
values.SetSize(m_dof_count, m_dimension);
element.CalcVShape(rule.IntPoint(q), values);
}
}
const mfem::DenseMatrix &VectorReferenceTable::GetValues(const int point) const {
return m_values.at(point);
}
int VectorReferenceTable::GetPointCount() const {
return static_cast<int>(m_values.size());
}
int VectorReferenceTable::GetDofCount() const {
return m_dof_count;
}
int VectorReferenceTable::GetDimension() const {
return m_dimension;
}
} // namespace mean_field::fem

View File

@@ -4,8 +4,16 @@ module;
module mean_field;
namespace mean_field::integrators {
AdvectionIntegrator::AdvectionIntegrator(const mapping::DomainMapper &map)
: m_map(map) {
AdvectionIntegrator::AdvectionIntegrator(
const mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate
)
: m_mapping(
mapper,
displacement,
compactification_coordinate
) {
}
void AdvectionIntegrator::AssembleElementVector(
@@ -14,6 +22,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array<mfem::Vector *> &elvec
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elvec)) {
return;
}
@@ -39,14 +49,13 @@ namespace mean_field::integrators {
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
mfem::DenseMatrix dshape_v_ref(dof_v, dim), dshape_v_phys(dof_v, dim);
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder() + 1);
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder() + 1);
for (int q = 0; q < ir->GetNPoints(); q++) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcShape(ip, shape_v);
fe_v->CalcDShape(ip, dshape_v_ref);
@@ -83,8 +92,7 @@ namespace mean_field::integrators {
for (int i = 0; i < dof_v; ++i) {
for (int c = 0; c < dim; ++c) {
r_v(i + c * dof_v) +=
shape_v(i) * rho_val * adv_val(c) * weight;
r_v(i + c * dof_v) += shape_v(i) * rho_val * adv_val(c) * weight;
}
}
}
@@ -96,6 +104,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array2D<mfem::DenseMatrix *> &elmats
) {
m_mapping.InvalidateCache();
const mfem::FiniteElement *fe_v = el[0];
const mfem::FiniteElement *fe_rho = el[1];
@@ -117,14 +127,13 @@ namespace mean_field::integrators {
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
mfem::DenseMatrix dshape_v_ref(dof_v, dim), dshape_v_phys(dof_v, dim);
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder() + 1);
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder() + 1);
for (int q = 0; q < ir->GetNPoints(); q++) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcShape(ip, shape_v);
fe_v->CalcDShape(ip, dshape_v_ref);
@@ -171,8 +180,7 @@ namespace mean_field::integrators {
double v_dot_grad_phi_j = 0.0;
for (int k = 0; k < dim; ++k) {
v_dot_grad_phi_j +=
v_val(k) * dshape_v_phys(j, k);
v_dot_grad_phi_j += v_val(k) * dshape_v_phys(j, k);
}
for (int d = 0; d < dim; ++d) {
@@ -187,11 +195,9 @@ namespace mean_field::integrators {
// \rho(\vec{v} \cdot \nabla \delta \vec{v})
// Only non-zero when the advected component
// matches the test component
double termB =
(c == d) ? v_dot_grad_phi_j : 0.0;
double termB = (c == d) ? v_dot_grad_phi_j : 0.0;
(*dv_dv)(row, col) += shape_v(i) * rho_val *
(termA + termB) * weight;
(*dv_dv)(row, col) += shape_v(i) * rho_val * (termA + termB) * weight;
}
}
}

View File

@@ -4,10 +4,16 @@ module mean_field;
namespace mean_field::integrators {
CentrifugalForceIntegrator::CentrifugalForceIntegrator(
const mapping::DomainMapper &map,
const mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
const mfem::Vector &omega
)
: m_map(map),
: m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_omega(3) {
MFEM_ASSERT(omega.Size() == 3, "Omega vector must be 3D");
m_omega = omega;
@@ -18,9 +24,7 @@ namespace mean_field::integrators {
m_omega = omega;
}
void CentrifugalForceIntegrator::SetIntegrationRule(
const mfem::IntegrationRule &ir
) {
void CentrifugalForceIntegrator::SetIntegrationRule(const mfem::IntegrationRule &ir) {
m_ir = &ir;
}
@@ -30,6 +34,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array<mfem::Vector *> &elvec
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elvec)) {
return;
}
@@ -52,8 +58,8 @@ namespace mean_field::integrators {
}
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
mfem::Vector x_phys(dim);
mfem::Vector a(dim), b(dim);
mapping::VolumeMappingContext mapping_context;
MFEM_VERIFY(
m_ir, "CentrifugalForceIntegrator must be configured with an "
@@ -66,12 +72,17 @@ namespace mean_field::integrators {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
const mapping::MappingStatus mapping_status = m_mapping.EvaluateVolume(Tr, ip, mapping_context);
MFEM_VERIFY(
mapping_status == mapping::MappingStatus::valid,
"Centrifugal-force assembly encountered an invalid volume mapping."
);
const double weight = mapping_context.quadrature.weight;
fe_v->CalcShape(ip, shape_v);
fe_rho->CalcShape(ip, shape_rho);
m_map.GetPhysicalPoint(Tr, ip, x_phys);
const mfem::Vector &x_phys = mapping_context.mapping.physical_position;
// ω x r
a(0) = m_omega(1) * x_phys(2) - m_omega(2) * x_phys(1);
@@ -102,6 +113,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array2D<mfem::DenseMatrix *> &elmats
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elmats)) {
return;
}
@@ -127,22 +140,26 @@ namespace mean_field::integrators {
return;
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
mfem::Vector x_phys(dim);
mfem::Vector a(dim), b(dim);
mapping::VolumeMappingContext mapping_context;
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
const mapping::MappingStatus mapping_status = m_mapping.EvaluateVolume(Tr, ip, mapping_context);
MFEM_VERIFY(
mapping_status == mapping::MappingStatus::valid,
"Centrifugal-force Jacobian assembly encountered an invalid volume mapping."
);
const double weight = mapping_context.quadrature.weight;
fe_v->CalcShape(ip, shape_v);
fe_rho->CalcShape(ip, shape_rho);
m_map.GetPhysicalPoint(Tr, ip, x_phys);
const mfem::Vector &x_phys = mapping_context.mapping.physical_position;
// ω x r
a(0) = m_omega(1) * x_phys(2) - m_omega(2) * x_phys(1);
@@ -159,8 +176,7 @@ namespace mean_field::integrators {
for (int c = 0; c < dim; ++c) {
const int row = i + c * dof_v;
for (int j = 0; j < dof_rho; ++j) {
(*dv_drho)(row, j) +=
shape_v(i) * shape_rho(j) * b(c) * weight;
(*dv_drho)(row, j) += shape_v(i) * shape_rho(j) * b(c) * weight;
}
}
}

View File

@@ -5,10 +5,16 @@ module mean_field;
namespace mean_field::integrators {
CoriolisIntegrator::CoriolisIntegrator(
const mapping::DomainMapper &map,
const mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
const mfem::Vector &omega
)
: m_map(map),
: m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_omega(omega) {
m_omega_mat.SetSize(3, 3);
m_omega_mat = 0.0;
@@ -26,6 +32,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array<mfem::Vector *> &elvec
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elvec)) {
return;
}
@@ -49,14 +57,13 @@ namespace mean_field::integrators {
}
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcShape(ip, shape_v);
fe_rho->CalcShape(ip, shape_rho);
@@ -78,8 +85,7 @@ namespace mean_field::integrators {
for (int i = 0; i < dof_v; ++i) {
for (int c = 0; c < dim; ++c) {
r_v(i + c * dof_v) +=
shape_v(i) * rho_val * F_coriolis(c) * weight;
r_v(i + c * dof_v) += shape_v(i) * rho_val * F_coriolis(c) * weight;
}
}
}
@@ -91,6 +97,7 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array2D<mfem::DenseMatrix *> &elmats
) {
m_mapping.InvalidateCache();
const mfem::FiniteElement *fe_v = el[0];
const mfem::FiniteElement *fe_rho = el[1];
@@ -111,14 +118,13 @@ namespace mean_field::integrators {
*dv_drho = 0.0;
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcShape(ip, shape_v);
fe_rho->CalcShape(ip, shape_rho);
@@ -146,9 +152,7 @@ namespace mean_field::integrators {
for (int d = 0; d < dim; ++d) {
int col = j + d * dof_v;
double coupling = m_omega_mat(c, d);
(*dv_dv)(row, col) += shape_v(i) * shape_v(j) *
2.0 * rho_val * coupling *
weight;
(*dv_dv)(row, col) += shape_v(i) * shape_v(j) * 2.0 * rho_val * coupling * weight;
}
}
}
@@ -161,8 +165,7 @@ namespace mean_field::integrators {
int row = i + c * dof_v;
for (int j = 0; j < dof_rho; ++j) {
int col = j;
(*dv_drho)(row, col) += shape_v(i) * shape_rho(j) *
F_coriolis(c) * weight;
(*dv_drho)(row, col) += shape_v(i) * shape_rho(j) * F_coriolis(c) * weight;
}
}
}

View File

@@ -6,39 +6,36 @@ import :solver.fields;
namespace {
using namespace mean_field;
constexpr int velocity_block =
solver::block_index(solver::FieldBlock::velocity);
constexpr int density_block =
solver::block_index(solver::FieldBlock::density);
constexpr int gravity_gradient_block =
solver::block_index(solver::FieldBlock::gravity_gradient);
constexpr int displacement_block =
solver::block_index(solver::FieldBlock::displacement);
constexpr int velocity_block = solver::block_index(solver::FieldBlock::velocity);
constexpr int density_block = solver::block_index(solver::FieldBlock::density);
constexpr int gravity_gradient_block = solver::block_index(solver::FieldBlock::gravity_gradient);
constexpr int displacement_block = solver::block_index(solver::FieldBlock::displacement);
} // namespace
namespace mean_field::integrators {
GravityMomentumIntegrator::GravityMomentumIntegrator(
const mapping::DomainMapper &map,
const mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
const GravityForceJacobianMode jacobian_mode
)
: m_map(map),
: m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_jacobian_mode(jacobian_mode) {
}
void GravityMomentumIntegrator::SetJacobianMode(
const GravityForceJacobianMode jacobian_mode
) {
void GravityMomentumIntegrator::SetJacobianMode(const GravityForceJacobianMode jacobian_mode) {
m_jacobian_mode = jacobian_mode;
}
void GravityMomentumIntegrator::SetIntegrationRule(
const mfem::IntegrationRule &integration_rule
) {
void GravityMomentumIntegrator::SetIntegrationRule(const mfem::IntegrationRule &integration_rule) {
m_integration_rule = &integration_rule;
}
GravityForceJacobianMode
GravityMomentumIntegrator::GetJacobianMode() const {
GravityForceJacobianMode GravityMomentumIntegrator::GetJacobianMode() const {
return m_jacobian_mode;
}
@@ -48,23 +45,22 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array<mfem::Vector *> &elvec
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elvec)) {
return;
}
MFEM_VERIFY(
m_integration_rule,
"GravityForceIntegrator must be configured with an "
m_integration_rule, "GravityForceIntegrator must be configured with an "
"integration rule before assembly."
);
MFEM_VERIFY(
el.Size() > gravity_gradient_block,
"GravityForceIntegrator requires velocity, density, and "
el.Size() > gravity_gradient_block, "GravityForceIntegrator requires velocity, density, and "
"gravity-gradient finite elements."
);
MFEM_VERIFY(
elfun.Size() > gravity_gradient_block,
"GravityForceIntegrator requires velocity, density, and "
elfun.Size() > gravity_gradient_block, "GravityForceIntegrator requires velocity, density, and "
"gravity-gradient element states."
);
MFEM_VERIFY(
@@ -72,8 +68,7 @@ namespace mean_field::integrators {
"GravityForceIntegrator requires a velocity residual block."
);
MFEM_VERIFY(
el[velocity_block] && el[density_block] &&
el[gravity_gradient_block],
el[velocity_block] && el[density_block] && el[gravity_gradient_block],
"GravityForceIntegrator received a null finite element."
);
MFEM_VERIFY(
@@ -83,22 +78,18 @@ namespace mean_field::integrators {
const mfem::FiniteElement *velocity_element = el[velocity_block];
const mfem::FiniteElement *density_element = el[density_block];
const mfem::FiniteElement *gravity_gradient_element =
el[gravity_gradient_block];
const mfem::FiniteElement *gravity_gradient_element = el[gravity_gradient_block];
const int velocity_dofs_count = velocity_element->GetDof();
const int density_dofs_count = density_element->GetDof();
const int gravity_gradient_dofs_count =
gravity_gradient_element->GetDof();
const int gravity_gradient_dofs_count = gravity_gradient_element->GetDof();
const int dim = Tr.GetSpaceDim();
const mfem::Vector &density_dofs = *elfun[density_block];
const mfem::Vector &gravity_gradient_dofs =
*elfun[gravity_gradient_block];
const mfem::Vector &gravity_gradient_dofs = *elfun[gravity_gradient_block];
MFEM_VERIFY(
density_dofs.Size() == density_dofs_count,
"GravityForceIntegrator received an incorrectly sized density "
density_dofs.Size() == density_dofs_count, "GravityForceIntegrator received an incorrectly sized density "
"state."
);
MFEM_VERIFY(
@@ -123,40 +114,31 @@ namespace mean_field::integrators {
*elvec[density_block] = 0.0;
}
if (elvec.Size() > gravity_gradient_block &&
elvec[gravity_gradient_block]) {
if (elvec.Size() > gravity_gradient_block && elvec[gravity_gradient_block]) {
elvec[gravity_gradient_block]->SetSize(gravity_gradient_dofs_count);
*elvec[gravity_gradient_block] = 0.0;
}
mfem::Vector velocity_shape(velocity_dofs_count);
mfem::Vector density_shape(density_dofs_count);
mfem::DenseMatrix gravity_gradient_shape(
gravity_gradient_dofs_count, dim
);
mfem::DenseMatrix gravity_gradient_shape(gravity_gradient_dofs_count, dim);
mfem::Vector gravity_gradient_element_value(dim);
mfem::Vector gravity_gradient_physical_value(dim);
const mfem::IntegrationRule &integration_rule = *m_integration_rule;
for (int q = 0; q < integration_rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &integration_point =
integration_rule.IntPoint(q);
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q);
Tr.SetIntPoint(&integration_point);
const mapping::VolumeQuadratureContext context =
m_map.GetQuadratureContext(Tr, integration_point);
const mapping::VolumeQuadratureContext context = m_mapping.GetQuadratureContext(Tr, integration_point);
velocity_element->CalcShape(integration_point, velocity_shape);
density_element->CalcShape(integration_point, density_shape);
gravity_gradient_element->CalcVShape(Tr, gravity_gradient_shape);
gravity_gradient_shape.MultTranspose(
gravity_gradient_dofs, gravity_gradient_element_value
);
context.J_inv.MultTranspose(
gravity_gradient_element_value, gravity_gradient_physical_value
);
gravity_gradient_shape.MultTranspose(gravity_gradient_dofs, gravity_gradient_element_value);
context.J_inv.MultTranspose(gravity_gradient_element_value, gravity_gradient_physical_value);
double density_value = 0.0;
for (int i = 0; i < density_dofs_count; ++i) {
@@ -166,9 +148,7 @@ namespace mean_field::integrators {
for (int i = 0; i < velocity_dofs_count; ++i) {
for (int component = 0; component < dim; ++component) {
velocity_residual(i + component * velocity_dofs_count) +=
velocity_shape(i) * density_value *
gravity_gradient_physical_value(component) *
context.weight;
velocity_shape(i) * density_value * gravity_gradient_physical_value(component) * context.weight;
}
}
}
@@ -180,28 +160,26 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array2D<mfem::DenseMatrix *> &elmats
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elmats)) {
return;
}
MFEM_VERIFY(
m_integration_rule,
"GravityForceIntegrator must be configured with an "
m_integration_rule, "GravityForceIntegrator must be configured with an "
"integration rule before assembly."
);
MFEM_VERIFY(
el.Size() > gravity_gradient_block,
"GravityForceIntegrator requires velocity, density, and "
el.Size() > gravity_gradient_block, "GravityForceIntegrator requires velocity, density, and "
"gravity-gradient finite elements."
);
MFEM_VERIFY(
elfun.Size() > gravity_gradient_block,
"GravityForceIntegrator requires velocity, density, and "
elfun.Size() > gravity_gradient_block, "GravityForceIntegrator requires velocity, density, and "
"gravity-gradient element states."
);
MFEM_VERIFY(
el[velocity_block] && el[density_block] &&
el[gravity_gradient_block],
el[velocity_block] && el[density_block] && el[gravity_gradient_block],
"GravityForceIntegrator received a null finite element."
);
MFEM_VERIFY(
@@ -221,29 +199,25 @@ namespace mean_field::integrators {
MFEM_ABORT(
"Exact GravityForceIntegrator geometry Jacobian is unavailable "
"until "
"DomainMapper linearization is "
"implemented."
"the stateless mapping variation is wired into this legacy "
"integrator."
);
}
const mfem::FiniteElement *velocity_element = el[velocity_block];
const mfem::FiniteElement *density_element = el[density_block];
const mfem::FiniteElement *gravity_gradient_element =
el[gravity_gradient_block];
const mfem::FiniteElement *gravity_gradient_element = el[gravity_gradient_block];
const int velocity_dofs_count = velocity_element->GetDof();
const int density_dofs_count = density_element->GetDof();
const int gravity_gradient_dofs_count =
gravity_gradient_element->GetDof();
const int gravity_gradient_dofs_count = gravity_gradient_element->GetDof();
const int dim = Tr.GetSpaceDim();
const mfem::Vector &density_dofs = *elfun[density_block];
const mfem::Vector &gravity_gradient_dofs =
*elfun[gravity_gradient_block];
const mfem::Vector &gravity_gradient_dofs = *elfun[gravity_gradient_block];
MFEM_VERIFY(
density_dofs.Size() == density_dofs_count,
"GravityForceIntegrator received an incorrectly sized density "
density_dofs.Size() == density_dofs_count, "GravityForceIntegrator received an incorrectly sized density "
"state."
);
MFEM_VERIFY(
@@ -254,8 +228,7 @@ namespace mean_field::integrators {
);
mfem::DenseMatrix *dv_drho = elmats(velocity_block, density_block);
mfem::DenseMatrix *dv_dgrad_phi =
m_jacobian_mode == GravityForceJacobianMode::field_coupled
mfem::DenseMatrix *dv_dgrad_phi = m_jacobian_mode == GravityForceJacobianMode::field_coupled
? elmats(velocity_block, gravity_gradient_block)
: nullptr;
@@ -265,9 +238,7 @@ namespace mean_field::integrators {
mfem::Vector velocity_shape(velocity_dofs_count);
mfem::Vector density_shape(density_dofs_count);
mfem::DenseMatrix gravity_gradient_shape(
gravity_gradient_dofs_count, dim
);
mfem::DenseMatrix gravity_gradient_shape(gravity_gradient_dofs_count, dim);
mfem::Vector gravity_gradient_element_value(dim);
mfem::Vector gravity_gradient_physical_value(dim);
mfem::Vector gravity_basis_element(dim);
@@ -276,23 +247,17 @@ namespace mean_field::integrators {
const mfem::IntegrationRule &integration_rule = *m_integration_rule;
for (int q = 0; q < integration_rule.GetNPoints(); ++q) {
const mfem::IntegrationPoint &integration_point =
integration_rule.IntPoint(q);
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q);
Tr.SetIntPoint(&integration_point);
const mapping::VolumeQuadratureContext context =
m_map.GetQuadratureContext(Tr, integration_point);
const mapping::VolumeQuadratureContext context = m_mapping.GetQuadratureContext(Tr, integration_point);
velocity_element->CalcShape(integration_point, velocity_shape);
density_element->CalcShape(integration_point, density_shape);
gravity_gradient_element->CalcVShape(Tr, gravity_gradient_shape);
gravity_gradient_shape.MultTranspose(
gravity_gradient_dofs, gravity_gradient_element_value
);
context.J_inv.MultTranspose(
gravity_gradient_element_value, gravity_gradient_physical_value
);
gravity_gradient_shape.MultTranspose(gravity_gradient_dofs, gravity_gradient_element_value);
context.J_inv.MultTranspose(gravity_gradient_element_value, gravity_gradient_physical_value);
double density_value = 0.0;
for (int i = 0; i < density_dofs_count; ++i) {
@@ -305,10 +270,8 @@ namespace mean_field::integrators {
const int row = i + component * velocity_dofs_count;
for (int j = 0; j < density_dofs_count; ++j) {
(*dv_drho)(row, j) +=
velocity_shape(i) * density_shape(j) *
gravity_gradient_physical_value(component) *
context.weight;
(*dv_drho)(row, j) += velocity_shape(i) * density_shape(j) *
gravity_gradient_physical_value(component) * context.weight;
}
}
}
@@ -317,21 +280,16 @@ namespace mean_field::integrators {
if (dv_dgrad_phi) {
for (int j = 0; j < gravity_gradient_dofs_count; ++j) {
for (int component = 0; component < dim; ++component) {
gravity_basis_element(component) =
gravity_gradient_shape(j, component);
gravity_basis_element(component) = gravity_gradient_shape(j, component);
}
context.J_inv.MultTranspose(
gravity_basis_element, gravity_basis_physical
);
context.J_inv.MultTranspose(gravity_basis_element, gravity_basis_physical);
for (int i = 0; i < velocity_dofs_count; ++i) {
for (int component = 0; component < dim; ++component) {
const int row = i + component * velocity_dofs_count;
(*dv_dgrad_phi)(row, j) +=
velocity_shape(i) * density_value *
gravity_basis_physical(component) *
context.weight;
velocity_shape(i) * density_value * gravity_basis_physical(component) * context.weight;
}
}
}

View File

@@ -5,9 +5,15 @@ module mean_field;
namespace mean_field::integrators {
ContinuityVolumeIntegrator::ContinuityVolumeIntegrator(
const mapping::DomainMapper &map
const mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate
)
: m_map(map) { };
: m_mapping(
mapper,
displacement,
compactification_coordinate
) { };
void ContinuityVolumeIntegrator::AssembleElementVector(
const mfem::Array<const mfem::FiniteElement *> &el,
@@ -15,6 +21,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array<mfem::Vector *> &elvec
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elvec)) {
return;
}
@@ -29,8 +37,7 @@ namespace mean_field::integrators {
const mfem::Vector v_dofs = *elfun[0];
const mfem::Vector rho_dofs = *elfun[1];
void *data_rho_before =
elvec[1] ? (void *)elvec[1]->GetData() : nullptr;
void *data_rho_before = elvec[1] ? (void *)elvec[1]->GetData() : nullptr;
int size_rho_before = elvec[1] ? elvec[1]->Size() : -1;
if (elvec[0]) {
@@ -42,17 +49,15 @@ namespace mean_field::integrators {
r_rho = 0.0;
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
mfem::DenseMatrix dshape_rho_ref(dof_rho, dim),
dshape_rho_phys(dof_rho, dim);
mfem::DenseMatrix dshape_rho_ref(dof_rho, dim), dshape_rho_phys(dof_rho, dim);
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcShape(ip, shape_v);
fe_rho->CalcShape(ip, shape_rho);
@@ -88,6 +93,7 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array2D<mfem::DenseMatrix *> &elmats
) {
m_mapping.InvalidateCache();
const mfem::FiniteElement *fe_v = el[0];
const mfem::FiniteElement *fe_rho = el[1];
@@ -113,17 +119,15 @@ namespace mean_field::integrators {
*drho_drho = 0.0;
mfem::Vector shape_v(dof_v), shape_rho(dof_rho);
mfem::DenseMatrix dshape_rho_ref(dof_rho, dim),
dshape_rho_phys(dof_rho, dim);
mfem::DenseMatrix dshape_rho_ref(dof_rho, dim), dshape_rho_phys(dof_rho, dim);
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcShape(ip, shape_v);
fe_rho->CalcShape(ip, shape_rho);
@@ -149,8 +153,7 @@ namespace mean_field::integrators {
for (int j = 0; j < dof_v; ++j) {
for (int d = 0; d < dim; ++d) {
const int col = j + d * dof_v;
(*drho_dv)(i, col) -= dshape_rho_phys(i, d) *
rho_val * shape_v(j) * weight;
(*drho_dv)(i, col) -= dshape_rho_phys(i, d) * rho_val * shape_v(j) * weight;
}
}
}
@@ -163,8 +166,7 @@ namespace mean_field::integrators {
grad_psi_dot_v += dshape_rho_phys(i, c) * v_val(c);
}
for (int j = 0; j < dof_rho; ++j) {
(*drho_drho)(i, j) -=
grad_psi_dot_v * shape_rho(j) * weight;
(*drho_drho)(i, j) -= grad_psi_dot_v * shape_rho(j) * weight;
}
}
}
@@ -172,9 +174,15 @@ namespace mean_field::integrators {
}
ContinuityFaceIntegrator::ContinuityFaceIntegrator(
const mapping::DomainMapper &map
const mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate
)
: m_map(map) {
: m_mapping(
mapper,
displacement,
compactification_coordinate
) {
}
void ContinuityFaceIntegrator::AssembleFaceVector(
@@ -184,6 +192,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array<mfem::Vector *> &elvect
) {
m_mapping.InvalidateCache();
const mfem::FiniteElement *fe_v_minus = el1[0];
const mfem::FiniteElement *fe_v_plus = el2[0];
@@ -208,9 +218,9 @@ namespace mean_field::integrators {
const int attr_minus = Tr.Elem1->Attribute;
const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1;
constexpr int VACUUM_ATTR = 3;
if (attr_minus == VACUUM_ATTR || attr_plus == VACUUM_ATTR) {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
if (DomainSchema::template attribute_belongs_to<utils::domain::Vacuum>(attr_minus) ||
DomainSchema::template attribute_belongs_to<utils::domain::Vacuum>(attr_plus)) {
return; // No flux contribution for vacuum faces
}
@@ -218,29 +228,21 @@ namespace mean_field::integrators {
return; // Boundary face,
}
const mfem::Vector &v_dofs =
*elfun[0]; // Size: dim * dof_v_minus + dim*dof_v_plus
const mfem::Vector &rho_dofs =
*elfun[1]; // Size: dof_rho_minus + dof_rho_plus
const mfem::Vector &v_dofs = *elfun[0]; // Size: dim * dof_v_minus + dim*dof_v_plus
const mfem::Vector &rho_dofs = *elfun[1]; // Size: dof_rho_minus + dof_rho_plus
// Helpers to auto offset to the correct point in the dof array
auto rho_minus_dof = [&](const int i) { return rho_dofs(i); };
auto rho_plus_dof = [&](const int i) {
return rho_dofs(i + dof_rho_minus);
};
auto v_minus_dof = [&](const int k, const int c) {
return v_dofs(k + c * dof_v_minus);
};
auto rho_plus_dof = [&](const int i) { return rho_dofs(i + dof_rho_minus); };
auto v_minus_dof = [&](const int k, const int c) { return v_dofs(k + c * dof_v_minus); };
const int p_v = fe_v_minus->GetOrder();
const int p_rho = fe_rho_minus->GetOrder();
const int int_order = 2 * std::max(p_v, p_rho) + 1;
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(Tr.GetGeometryType(), int_order);
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(Tr.GetGeometryType(), int_order);
mfem::Vector shape_v_minus(dof_v_minus), shape_rho_minus(dof_rho_minus),
shape_rho_plus(dof_rho_plus);
mfem::Vector shape_v_minus(dof_v_minus), shape_rho_minus(dof_rho_minus), shape_rho_plus(dof_rho_plus);
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &face_ip = ir->IntPoint(q);
@@ -249,8 +251,7 @@ namespace mean_field::integrators {
const mfem::IntegrationPoint &ip_minus = Tr.GetElement1IntPoint();
const mfem::IntegrationPoint &ip_plus = Tr.GetElement2IntPoint();
auto [n_unit, ds, v_dot_n_scale] =
m_map.GetFaceQuadratureContext(Tr, face_ip);
auto [n_unit, ds, v_dot_n_scale] = m_mapping.GetFaceQuadratureContext(Tr, face_ip);
fe_v_minus->CalcShape(ip_minus, shape_v_minus);
fe_rho_minus->CalcShape(ip_minus, shape_rho_minus);
@@ -303,6 +304,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array2D<mfem::DenseMatrix *> &elmats
) {
m_mapping.InvalidateCache();
const mfem::FiniteElement *fe_v_minus = el1[0];
const mfem::FiniteElement *fe_v_plus = el2[0];
const mfem::FiniteElement *fe_rho_minus = el1[1];
@@ -317,8 +320,7 @@ namespace mean_field::integrators {
const int N_v_total = dim * (dof_v_minus + dof_v_plus);
const int N_rho_total = dof_rho_minus + dof_rho_plus;
auto size_and_zero_mat = [&](mfem::DenseMatrix *mat, const int r_size,
const int c_size) {
auto size_and_zero_mat = [&](mfem::DenseMatrix *mat, const int r_size, const int c_size) {
if (mat) {
mat->SetSize(r_size, c_size);
*mat = 0.0;
@@ -342,13 +344,10 @@ namespace mean_field::integrators {
const mfem::Vector &v_dofs = *elfun[0];
const mfem::Vector &rho_dofs = *elfun[1];
const int int_order =
2 * std::max(fe_v_minus->GetOrder(), fe_rho_minus->GetOrder()) + 1;
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(Tr.GetGeometryType(), int_order);
const int int_order = 2 * std::max(fe_v_minus->GetOrder(), fe_rho_minus->GetOrder()) + 1;
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(Tr.GetGeometryType(), int_order);
mfem::Vector shape_v_minus(dof_v_minus), shape_rho_minus(dof_rho_minus),
shape_rho_plus(dof_rho_plus);
mfem::Vector shape_v_minus(dof_v_minus), shape_rho_minus(dof_rho_minus), shape_rho_plus(dof_rho_plus);
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &face_ip = ir->IntPoint(q);
@@ -356,15 +355,13 @@ namespace mean_field::integrators {
const mfem::IntegrationPoint &ip_minus = Tr.GetElement1IntPoint();
const mfem::IntegrationPoint &ip_plus = Tr.GetElement2IntPoint();
auto [n_unit, ds, v_dot_n_scale] =
m_map.GetFaceQuadratureContext(Tr, face_ip);
auto [n_unit, ds, v_dot_n_scale] = m_mapping.GetFaceQuadratureContext(Tr, face_ip);
fe_v_minus->CalcShape(ip_minus, shape_v_minus);
fe_rho_minus->CalcShape(ip_minus, shape_rho_minus);
fe_rho_plus->CalcShape(ip_plus, shape_rho_plus);
const double u_n =
compute_u_n(v_dofs, shape_v_minus, n_unit, dof_v_minus, dim);
const double u_n = compute_u_n(v_dofs, shape_v_minus, n_unit, dof_v_minus, dim);
double rho_minus_val = 0.0;
for (int i = 0; i < dof_rho_minus; ++i) {
@@ -390,8 +387,7 @@ namespace mean_field::integrators {
(*drho_drho)(i, ip) += shape_rho_minus(i) * col_w;
}
for (int j = 0; j < dof_rho_plus; ++j) {
(*drho_drho)(dof_rho_minus + j, ip) -=
shape_rho_plus(j) * col_w;
(*drho_drho)(dof_rho_minus + j, ip) -= shape_rho_plus(j) * col_w;
}
}
} else {
@@ -399,12 +395,10 @@ namespace mean_field::integrators {
const double col_w = u_w * shape_rho_plus(jp);
const int col_idx = dof_rho_minus + jp;
for (int i = 0; i < dof_rho_minus; ++i) {
(*drho_drho)(i, col_idx) +=
shape_rho_minus(i) * col_w;
(*drho_drho)(i, col_idx) += shape_rho_minus(i) * col_w;
}
for (int j = 0; j < dof_rho_plus; ++j) {
(*drho_drho)(dof_rho_minus + j, col_idx) -=
shape_rho_plus(j) * col_w;
(*drho_drho)(dof_rho_minus + j, col_idx) -= shape_rho_plus(j) * col_w;
}
}
}
@@ -418,12 +412,10 @@ namespace mean_field::integrators {
const int col_idx = k + c * dof_v_minus;
const double col_w = n_c_rho_w * shape_v_minus(k);
for (int i = 0; i < dof_rho_minus; ++i) {
(*drho_dv)(i, col_idx) +=
shape_rho_minus(i) * col_w;
(*drho_dv)(i, col_idx) += shape_rho_minus(i) * col_w;
}
for (int j = 0; j < dof_rho_plus; ++j) {
(*drho_dv)(dof_rho_minus + j, col_idx) -=
shape_rho_plus(j) * col_w;
(*drho_dv)(dof_rho_minus + j, col_idx) -= shape_rho_plus(j) * col_w;
}
}
}
@@ -431,13 +423,12 @@ namespace mean_field::integrators {
}
}
bool ContinuityFaceIntegrator::skip_face(
const mfem::FaceElementTransformations &Tr
) {
constexpr int VACUUM_ATTR = 3;
bool ContinuityFaceIntegrator::skip_face(const mfem::FaceElementTransformations &Tr) {
const int attr_minus = Tr.Elem1->Attribute;
const int attr_plus = (Tr.Elem2 != nullptr) ? Tr.Elem2->Attribute : -1;
if (attr_minus == VACUUM_ATTR || attr_plus == VACUUM_ATTR) {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
if (DomainSchema::template attribute_belongs_to<utils::domain::Vacuum>(attr_minus) ||
DomainSchema::template attribute_belongs_to<utils::domain::Vacuum>(attr_plus)) {
return true; // No flux contribution for vacuum faces
}
if (Tr.Elem2 == nullptr) {

View File

@@ -4,11 +4,17 @@ module mean_field;
namespace mean_field::integrators {
ViscosityIntegrator::ViscosityIntegrator(
const mapping::DomainMapper &map,
const mapping::DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
const double mu,
const int quad_boost
)
: m_map(map),
: m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_mu(mu),
m_quad_boost(quad_boost) {
}
@@ -23,6 +29,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array<mfem::Vector *> &elvec
) {
m_mapping.InvalidateCache();
if (utils::is_vacuum(Tr, elvec)) {
return;
}
@@ -49,16 +57,14 @@ namespace mean_field::integrators {
mfem::DenseMatrix dshape_v_ref(dof_v, dim), dshape_v_phys(dof_v, dim);
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(
fe_v->GetGeomType(), 2 * fe_v->GetOrder() + m_quad_boost
);
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder() + m_quad_boost);
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcDShape(ip, dshape_v_ref);
mfem::Mult(dshape_v_ref, J_inv, dshape_v_phys);
@@ -104,6 +110,8 @@ namespace mean_field::integrators {
const mfem::Array<const mfem::Vector *> &elfun,
const mfem::Array2D<mfem::DenseMatrix *> &elmats
) {
m_mapping.InvalidateCache();
const mfem::FiniteElement *fe_v = el[0];
const mfem::FiniteElement *fe_rho = el[1];
@@ -127,13 +135,12 @@ namespace mean_field::integrators {
mfem::DenseMatrix dshape_v_ref(dof_v, dim), dshape_v_phys(dof_v, dim);
const mfem::IntegrationRule *ir =
&mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
const mfem::IntegrationRule *ir = &mfem::IntRules.Get(fe_v->GetGeomType(), 2 * fe_v->GetOrder());
for (int q = 0; q < ir->GetNPoints(); ++q) {
const mfem::IntegrationPoint &ip = ir->IntPoint(q);
Tr.SetIntPoint(&ip);
auto [J_inv, detJ, weight] = m_map.GetQuadratureContext(Tr, ip);
auto [J_inv, detJ, weight] = m_mapping.GetQuadratureContext(Tr, ip);
fe_v->CalcDShape(ip, dshape_v_ref);
mfem::Mult(dshape_v_ref, J_inv, dshape_v_phys);
@@ -158,8 +165,7 @@ namespace mean_field::integrators {
val += dshape_v_phys(i, d) * dshape_v_phys(n, c);
val -= (2.0 / 3.0) * dshape_v_phys(i, c) *
dshape_v_phys(n, d);
val -= (2.0 / 3.0) * dshape_v_phys(i, c) * dshape_v_phys(n, d);
(*dv_dv)(row, col) += mu_w * val;
}
}

View File

@@ -9,11 +9,17 @@ namespace mean_field::mapping {
/// MappedScalarCoefficient ///
//////////////////////////////
MappedScalarCoefficient::MappedScalarCoefficient(
const DomainMapper &map,
const DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
Coefficient &coeff,
const COORDINATE_SPACE coord_space
)
: m_map(map),
: m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_coeff(coeff),
m_coord_space(coord_space) { };
@@ -27,8 +33,12 @@ namespace mean_field::mapping {
switch (m_coord_space) {
case COORDINATE_SPACE::PHYSICAL: {
f_val = eval_at_point(m_coeff, T, ip);
const double detJ = m_map.ComputeDetJ(T, ip);
return f_val * fabs(detJ);
VolumeMappingContext context;
MFEM_VERIFY(
m_mapping.EvaluateVolume(T, ip, context) == MappingStatus::valid,
"Mapped scalar coefficient encountered an invalid mapping."
);
return f_val * std::abs(context.mapping.mapping_determinant);
}
case COORDINATE_SPACE::REFERENCE: {
f_val = m_coeff.Eval(T, ip);
@@ -50,21 +60,33 @@ namespace mean_field::mapping {
//////////////////////////////////
MappedDiffusionCoefficient::MappedDiffusionCoefficient(
const DomainMapper &map,
const DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
mfem::Coefficient &sigma,
const int dim
)
: MatrixCoefficient(dim),
m_map(map),
m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_scalar(&sigma),
m_tensor(nullptr) { };
MappedDiffusionCoefficient::MappedDiffusionCoefficient(
const DomainMapper &map,
const DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
MatrixCoefficient &sigma
)
: MatrixCoefficient(sigma.GetHeight()),
m_map(map),
m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_scalar(nullptr),
m_tensor(&sigma) { };
@@ -76,10 +98,13 @@ namespace mean_field::mapping {
const int dim = height;
T.SetIntPoint(&ip);
mfem::DenseMatrix J(dim, dim), JInv(dim, dim);
m_map.ComputeJacobian(T, J);
const double detJ = J.Det();
mfem::CalcInverse(J, JInv);
VolumeMappingContext context;
MFEM_VERIFY(
m_mapping.EvaluateVolume(T, ip, context) == MappingStatus::valid,
"Mapped diffusion coefficient encountered an invalid mapping."
);
const mfem::DenseMatrix &JInv = context.mapping.inverse_mapping_jacobian;
const double detJ = context.mapping.mapping_determinant;
if (m_scalar) {
const double sig_val = m_scalar->Eval(T, ip);
@@ -101,11 +126,17 @@ namespace mean_field::mapping {
/// MappedVectorCoefficient ///
///////////////////////////////
MappedVectorCoefficient::MappedVectorCoefficient(
const DomainMapper &map,
const DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
VectorCoefficient &coeff
)
: VectorCoefficient(coeff.GetVDim()),
m_map(map),
m_mapping(
mapper,
displacement,
compactification_coordinate
),
m_coeff(coeff) { };
void MappedVectorCoefficient::Eval(
@@ -116,9 +147,13 @@ namespace mean_field::mapping {
const int dim = vdim;
T.SetIntPoint(&ip);
mfem::DenseMatrix JInv(dim, dim);
m_map.ComputeInverseJacobian(T, JInv);
double detJ = m_map.ComputeDetJ(T, ip);
VolumeMappingContext context;
MFEM_VERIFY(
m_mapping.EvaluateVolume(T, ip, context) == MappingStatus::valid,
"Mapped vector coefficient encountered an invalid mapping."
);
const mfem::DenseMatrix &JInv = context.mapping.inverse_mapping_jacobian;
const double detJ = context.mapping.mapping_determinant;
mfem::Vector C_phys(dim);
m_coeff.Eval(C_phys, T, ip);
@@ -132,28 +167,43 @@ namespace mean_field::mapping {
/// PhysicalPositionFunctionCoefficient ///
///////////////////////////////////////////
PhysicalPositionFunctionCoefficient::PhysicalPositionFunctionCoefficient(
const DomainMapper &map,
const DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
Func f // std::function<double(const mfem::Vector&)>
)
: m_f(std::move(f)),
m_map(map) { };
m_mapping(
mapper,
displacement,
compactification_coordinate
) { };
double PhysicalPositionFunctionCoefficient::Eval(
mfem::ElementTransformation &T,
const mfem::IntegrationPoint &ip
) {
T.SetIntPoint(&ip);
mfem::Vector x;
m_map.GetPhysicalPoint(T, ip, x);
return m_f(x);
MappingPointContext context;
MFEM_VERIFY(
m_mapping.EvaluatePoint(T, ip, context) == MappingStatus::valid,
"Physical-position coefficient encountered an invalid mapping."
);
return m_f(context.physical_position);
}
MappedHDivMassCoefficient::MappedHDivMassCoefficient(
const DomainMapper &map,
const DomainMapper &mapper,
const mfem::GridFunction &displacement,
const mfem::GridFunction &compactification_coordinate,
const int dim
)
: MatrixCoefficient(dim),
m_map(map) {
m_mapping(
mapper,
displacement,
compactification_coordinate
) {
}
void MappedHDivMassCoefficient::Eval(
@@ -163,15 +213,15 @@ namespace mean_field::mapping {
) {
transformation.SetIntPoint(&integration_point);
mfem::DenseMatrix map_jacobian(height, height);
m_map.ComputeJacobian(transformation, map_jacobian);
const double map_determinant = map_jacobian.Det();
VolumeMappingContext context;
MFEM_VERIFY(
map_determinant > 0.0,
"Domain mapping has a non-positive Jacobian determinant."
m_mapping.EvaluateVolume(transformation, integration_point, context) == MappingStatus::valid,
"Mapped H(div) coefficient encountered an invalid mapping."
);
const mfem::DenseMatrix &map_jacobian = context.mapping.mapping_jacobian;
const double map_determinant = context.mapping.mapping_determinant;
MFEM_VERIFY(map_determinant > 0.0, "Domain mapping has a non-positive Jacobian determinant.");
mfem::MultAtB(map_jacobian, map_jacobian, matrix);
matrix *= 1.0 / std::abs(map_determinant);

View File

@@ -27,26 +27,17 @@ namespace {
} // namespace
namespace mean_field::mapping::compactification {
KelvinCompactification::KelvinCompactification(
options::KelvinCompactificationOptions options
)
KelvinCompactification::KelvinCompactification(options::KelvinCompactificationOptions options)
: m_options(options) {
if (!std::isfinite(m_options.r_star_ref) ||
!std::isfinite(m_options.r_inf_ref)) {
throw std::invalid_argument(
"Kelvin compactification radii must be finite."
);
if (!std::isfinite(m_options.r_star_ref) || !std::isfinite(m_options.r_inf_ref)) {
throw std::invalid_argument("Kelvin compactification radii must be finite.");
}
if (m_options.r_star_ref <= 0.0 ||
m_options.r_inf_ref <= m_options.r_star_ref) {
throw std::invalid_argument(
"Kelvin compactification requires 0 < r_star_ref < r_inf_ref."
);
if (m_options.r_star_ref <= 0.0 || m_options.r_inf_ref <= m_options.r_star_ref) {
throw std::invalid_argument("Kelvin compactification requires 0 < r_star_ref < r_inf_ref.");
}
if (!std::isfinite(m_options.coordinate_tolerance) ||
m_options.coordinate_tolerance < 0.0 ||
if (!std::isfinite(m_options.coordinate_tolerance) || m_options.coordinate_tolerance < 0.0 ||
m_options.coordinate_tolerance >= 1.0) {
throw std::invalid_argument(
"Kelvin compactification coordinate tolerance must be finite "
@@ -63,10 +54,10 @@ namespace mean_field::mapping::compactification {
if (!std::isfinite(compactification_coordinate))
return MappingStatus::non_finite_input;
// How close we will allow the code to get to compactified infinity
const double tolerance = m_options.coordinate_tolerance;
if (compactification_coordinate < -tolerance ||
compactification_coordinate > 1.0 + tolerance) {
if (compactification_coordinate < -tolerance || compactification_coordinate > 1.0 + tolerance) {
return MappingStatus::outside_reference_domain;
}
@@ -78,15 +69,21 @@ namespace mean_field::mapping::compactification {
return MappingStatus::at_compactified_infinity;
}
// Here we need to do some transformations from the options defined on the mesh to useful computational coordinates
// r_inf_ref is the computational / reference radius of the infinity surface (the edge of the entire domain) and r_star_ref is the radius of the spherical
// stellar model inscribed within. Therefore radial extent is the computational radial distance between the stellar surface and the infinity surface.
// Note that this is separate from the parameterize compactification coordinate.
const double radial_extent = m_options.r_inf_ref - m_options.r_star_ref;
const double computational_radius =
m_options.r_star_ref + coordinate * radial_extent;
if (!std::isfinite(computational_radius) ||
computational_radius <= 0.0) {
// This places us at the correct spot in computational space given the current compactification coordinate. Say you have compactification = 0.5,
// an r_star_ref of 2 and a radial extent of 3, this this will place you at 2 + 0.5 * 3 = 3.5 in computational space, which is half way between the stellar surface and the infinity surface.
const double computational_radius = m_options.r_star_ref + coordinate * radial_extent;
if (!std::isfinite(computational_radius) || computational_radius <= 0.0) {
return MappingStatus::invalid_reference_radius;
}
// Invert the exterior coordinate so it runs from 0 at the star to 1 at compactified infinity
const double one_minus_coordinate = 1.0 - coordinate;
const double denominator = computational_radius * one_minus_coordinate;
@@ -94,10 +91,13 @@ namespace mean_field::mapping::compactification {
return MappingStatus::non_finite_result;
}
// The scale here is the factor which stretches the finite computational domain into the infinite physical domain. Properties we need this to have
// include that it should go to 1 at the stellar surface and go to infinity at the compactified infinity.
// Mathematically this is scale = |r|/|x| where r is the physical radius and x is the computational radius.
// Put another way, scale is the ratio of the target physical radius for the current compactification coordinate
// to the current mesh radius.
const double scale = m_options.r_star_ref / denominator;
const double scale_derivative =
scale *
(1.0 / one_minus_coordinate - radial_extent / computational_radius);
const double scale_derivative = scale * (1.0 / one_minus_coordinate - radial_extent / computational_radius);
if (!std::isfinite(scale) || !std::isfinite(scale_derivative)) {
return MappingStatus::non_finite_result;
@@ -122,21 +122,20 @@ namespace mean_field::mapping::compactification {
return MappingStatus::invalid_dimension;
}
if (input.displacement_jacobian.Height() != dimension ||
input.displacement_jacobian.Width() != dimension) {
if (input.displacement_jacobian.Height() != dimension || input.displacement_jacobian.Width() != dimension) {
return MappingStatus::invalid_dimension;
}
if (!vector_is_finite(input.reference_position) ||
!vector_is_finite(input.displaced_position) ||
if (!vector_is_finite(input.reference_position) || !vector_is_finite(input.displaced_position) ||
!vector_is_finite(input.compactification_coordinate_gradient) ||
!matrix_is_finite(input.displacement_jacobian)) {
return MappingStatus::non_finite_input;
}
RadialFactors factors;
const MappingStatus factor_status =
ComputeRadialFactors(input.compactification_coordinate, factors);
// The key radial factors we use are the scale (which stretches the finite computational domain into the infinite physical domain) and the scale derivative (which is used to compute the mapping jacobian).
const MappingStatus factor_status = ComputeRadialFactors(input.compactification_coordinate, factors);
if (factor_status != MappingStatus::valid)
return factor_status;
@@ -144,21 +143,25 @@ namespace mean_field::mapping::compactification {
result.mapping_jacobian.SetSize(dimension, dimension);
for (int i = 0; i < dimension; ++i) {
result.physical_position(i) =
factors.scale * input.displaced_position(i);
// Note how the physical position is just the product of the displaced position and the scale factor.
result.physical_position(i) = factors.scale * input.displaced_position(i);
// The mapping jacobian comes from trivial application of the product rule
// recall: r_i = scale * x_i where r is the physical position and x is the displaced position.
// then we can differentiate wrt. X_j holding nothing fixed. Note the capital X here, this is the mesh coordinate not the displaced position.
// Lets call this jacobian F
// F = \frac{\partial r_i}{\partial X_{j}}
// F then tells us how the physical position changes as we move along mesh coordinates
// Lets then apply this to the function we have for the kelvin compactification
// F = scale * \frac{\partial x_i}{\partial X_j} + x_i * \frac{\partial scale}{\partial X_j}
// Below you can see the displacement jacobian (\frac{\partial x_i}{\partial X_j}) and the scale derivative (\frac{\partial scale}{\partial X_j}) being applied to compute the mapping jacobian.
for (int j = 0; j < dimension; ++j) {
const double scale_gradient =
factors.scale_derivative *
input.compactification_coordinate_gradient(j);
result.mapping_jacobian(i, j) =
factors.scale * input.displacement_jacobian(i, j) +
input.displaced_position(i) * scale_gradient;
const double scale_gradient = factors.scale_derivative * input.compactification_coordinate_gradient(j);
result.mapping_jacobian(i, j) = factors.scale * input.displacement_jacobian(i, j) + input.displaced_position(i) * scale_gradient;
}
}
if (!vector_is_finite(result.physical_position) ||
!matrix_is_finite(result.mapping_jacobian)) {
if (!vector_is_finite(result.physical_position) || !matrix_is_finite(result.mapping_jacobian)) {
return MappingStatus::non_finite_result;
}
@@ -185,13 +188,11 @@ namespace mean_field::mapping::compactification {
return MappingStatus::invalid_dimension;
}
if (input.displacement_jacobian.Height() != dimension ||
input.displacement_jacobian.Width() != dimension) {
if (input.displacement_jacobian.Height() != dimension || input.displacement_jacobian.Width() != dimension) {
return MappingStatus::invalid_dimension;
}
if (result.physical_position.Size() != dimension ||
result.mapping_jacobian.Height() != dimension ||
if (result.physical_position.Size() != dimension || result.mapping_jacobian.Height() != dimension ||
result.mapping_jacobian.Width() != dimension) {
return MappingStatus::invalid_dimension;
}
@@ -202,23 +203,20 @@ namespace mean_field::mapping::compactification {
return MappingStatus::invalid_dimension;
}
if (!vector_is_finite(input.reference_position) ||
!vector_is_finite(input.displaced_position) ||
if (!vector_is_finite(input.reference_position) || !vector_is_finite(input.displaced_position) ||
!vector_is_finite(input.compactification_coordinate_gradient) ||
!matrix_is_finite(input.displacement_jacobian)) {
return MappingStatus::non_finite_input;
}
if (!vector_is_finite(result.physical_position) ||
!matrix_is_finite(result.mapping_jacobian) ||
if (!vector_is_finite(result.physical_position) || !matrix_is_finite(result.mapping_jacobian) ||
!vector_is_finite(direction.displaced_position_variation) ||
!matrix_is_finite(direction.displacement_jacobian_variation)) {
return MappingStatus::non_finite_input;
}
RadialFactors factors;
const MappingStatus factor_status =
ComputeRadialFactors(input.compactification_coordinate, factors);
const MappingStatus factor_status = ComputeRadialFactors(input.compactification_coordinate, factors);
if (factor_status != MappingStatus::valid)
return factor_status;
@@ -226,16 +224,12 @@ namespace mean_field::mapping::compactification {
variation.mapping_jacobian_variation.SetSize(dimension, dimension);
for (int i = 0; i < dimension; ++i) {
variation.physical_position_variation(i) =
factors.scale * direction.displaced_position_variation(i);
variation.physical_position_variation(i) = factors.scale * direction.displaced_position_variation(i);
for (int j = 0; j < dimension; ++j) {
const double scale_gradient =
factors.scale_derivative *
input.compactification_coordinate_gradient(j);
const double scale_gradient = factors.scale_derivative * input.compactification_coordinate_gradient(j);
variation.mapping_jacobian_variation(i, j) =
factors.scale *
direction.displacement_jacobian_variation(i, j) +
factors.scale * direction.displacement_jacobian_variation(i, j) +
direction.displaced_position_variation(i) * scale_gradient;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,916 +0,0 @@
module;
#include <cmath>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
#include <utility>
module mean_field;
import :mapping.types;
import :mapping.compactification;
import :utils.user;
namespace {
bool vector_is_finite(const mfem::Vector &vector) {
for (int i = 0; i < vector.Size(); ++i) {
if (!std::isfinite(vector(i)))
return false;
}
return true;
}
bool matrix_is_finite(const mfem::DenseMatrix &matrix) {
for (int i = 0; i < matrix.Height(); ++i) {
for (int j = 0; j < matrix.Width(); ++j) {
if (!std::isfinite(matrix(i, j)))
return false;
}
}
return true;
}
} // namespace
namespace mean_field::mapping {
ElementCompactificationData::ElementCompactificationData(
const mfem::FiniteElement &element,
const mfem::Vector &dofs
)
: m_element(&element),
m_dofs(dofs) {
if (element.GetRangeType() != mfem::FiniteElement::SCALAR) {
throw std::invalid_argument(
"Compactification coordinate requires a scalar finite element."
);
}
if (element.GetMapType() != mfem::FiniteElement::VALUE) {
throw std::invalid_argument(
"Compactification coordinate requires a value-mapped scalar "
"finite "
"element."
);
}
if (element.GetDerivType() != mfem::FiniteElement::GRAD) {
throw std::invalid_argument(
"Compactification coordinate finite element must provide a "
"gradient."
);
}
if (element.GetDof() <= 0) {
throw std::invalid_argument(
"Compactification coordinate finite element has no degrees of "
"freedom."
);
}
if (dofs.Size() != element.GetDof()) {
throw std::invalid_argument(
"Compactification coordinate DOF count does not match its "
"finite "
"element."
);
}
}
const mfem::FiniteElement &
ElementCompactificationData::GetElement() const noexcept {
return *m_element;
}
const mfem::Vector &ElementCompactificationData::GetDofs() const noexcept {
return m_dofs;
}
int ElementCompactificationData::GetDofCount() const noexcept {
return m_dofs.Size();
}
ElementDisplacementData::ElementDisplacementData(
const mfem::FiniteElement &element,
const mfem::Vector &displacement_dofs,
const mfem::Ordering::Type ordering
)
: m_element(&element),
m_dimension(0),
m_ordering(ordering) {
const int dof_count = element.GetDof();
if (dof_count <= 0)
throw std::invalid_argument(
"The displacement element must have at least one degree of "
"freedom."
);
if (displacement_dofs.Size() <= 0 ||
displacement_dofs.Size() % dof_count != 0) {
throw std::invalid_argument(
"The displacement vector size must be a positive multiple of "
"the "
"element degree-of-freedom count."
);
}
m_dimension = displacement_dofs.Size() / dof_count;
m_dof_matrix.SetSize(dof_count, m_dimension);
if (ordering == mfem::Ordering::byNODES) {
for (int component = 0; component < m_dimension; ++component) {
for (int i = 0; i < dof_count; ++i) {
m_dof_matrix(i, component) =
displacement_dofs(i + component * dof_count);
}
}
} else if (ordering == mfem::Ordering::byVDIM) {
for (int i = 0; i < dof_count; ++i) {
for (int component = 0; component < m_dimension; ++component) {
m_dof_matrix(i, component) =
displacement_dofs(component + i * m_dimension);
}
}
} else {
throw std::invalid_argument(
"Unsupported MFEM displacement ordering."
);
}
}
const mfem::FiniteElement &
ElementDisplacementData::GetElement() const noexcept {
return *m_element;
}
const mfem::DenseMatrix &
ElementDisplacementData::GetDofMatrix() const noexcept {
return m_dof_matrix;
}
int ElementDisplacementData::GetDimension() const noexcept {
return m_dimension;
}
int ElementDisplacementData::GetDofCount() const noexcept {
return m_element->GetDof();
}
mfem::Ordering::Type ElementDisplacementData::GetOrdering() const noexcept {
return m_ordering;
}
ElementDisplacementData ElementDisplacementDataFromElementVDofs(
const mfem::FiniteElement &element,
const mfem::Vector &displacement_dofs
) {
return ElementDisplacementData(
element, displacement_dofs, mfem::Ordering::byNODES
);
}
DomainMapperStateless::Workspace::Workspace(const int dimension) {
SetDimension(dimension);
}
void DomainMapperStateless::Workspace::SetDimension(const int dimension) {
if (dimension <= 0) {
throw std::invalid_argument(
"Domain mapping workspace dimension must be positive."
);
}
m_dimension = dimension;
m_field_value.SetSize(dimension);
m_field_jacobian.SetSize(dimension, dimension);
m_compactification_point.coordinate = 0.0;
m_compactification_point.coordinate_gradient.SetSize(dimension);
m_reference_normal.SetSize(dimension);
m_mapped_normal.SetSize(dimension);
m_full_element_jacobian.SetSize(dimension, dimension);
m_vector_temp.SetSize(dimension);
m_matrix_temp_1.SetSize(dimension, dimension);
m_matrix_temp_2.SetSize(dimension, dimension);
m_exterior_result.physical_position.SetSize(dimension);
m_exterior_result.mapping_jacobian.SetSize(dimension, dimension);
m_exterior_variation.physical_position_variation.SetSize(dimension);
m_exterior_variation.mapping_jacobian_variation.SetSize(
dimension, dimension
);
}
int DomainMapperStateless::Workspace::GetDimension() const noexcept {
return m_dimension;
}
DomainMapperStateless::DomainMapperStateless(
const utils::DomainMapperStatelessOptions options,
std::unique_ptr<const compactification::ExteriorDomainMap> exterior_map
)
: m_options(options),
m_exterior_map(std::move(exterior_map)) {
if (m_options.dimension <= 0)
throw std::invalid_argument(
"The domain-mapping dimension must be positive."
);
if (m_options.vacuum_element_attribute <= 0)
throw std::invalid_argument(
"The vacuum element attribute must be positive."
);
if (!m_exterior_map)
throw std::invalid_argument(
"DomainMapperStateless requires an exterior-domain mapping."
);
}
bool DomainMapperStateless::IsCompactifiedElement(
const mfem::ElementTransformation &transformation
) const noexcept {
return transformation.Attribute == m_options.vacuum_element_attribute;
}
int DomainMapperStateless::GetDimension() const noexcept {
return m_options.dimension;
}
int DomainMapperStateless::GetVacuumElementAttribute() const noexcept {
return m_options.vacuum_element_attribute;
}
const compactification::ExteriorDomainMap &
DomainMapperStateless::GetExteriorMap() const noexcept {
return *m_exterior_map;
}
void DomainMapperStateless::ValidateElementData(
const ElementMappingData &element_data
) const {
const ElementDisplacementData &displacement = element_data.displacement;
const ElementCompactificationData &compactification =
element_data.compactification;
if (displacement.GetDimension() != m_options.dimension) {
throw std::invalid_argument(
"Displacement field dimension does not match the domain mapper "
"dimension."
);
}
if (displacement.GetElement().GetDim() != m_options.dimension) {
throw std::invalid_argument(
"Displacement finite element dimension does not match the "
"domain "
"mapper dimension."
);
}
if (compactification.GetElement().GetDim() != m_options.dimension) {
throw std::invalid_argument(
"Compactification finite element dimension does not match the "
"domain "
"mapper dimension."
);
}
if (displacement.GetElement().GetGeomType() !=
compactification.GetElement().GetGeomType()) {
throw std::invalid_argument(
"Displacement and compactification finite elements have "
"different "
"geometries."
);
}
if (compactification.GetElement().GetRangeType() !=
mfem::FiniteElement::SCALAR) {
throw std::invalid_argument(
"Compactification coordinate requires a scalar finite element."
);
}
if (compactification.GetElement().GetMapType() !=
mfem::FiniteElement::VALUE) {
throw std::invalid_argument(
"Compactification coordinate requires a value-mapped finite "
"element."
);
}
if (compactification.GetElement().GetDerivType() !=
mfem::FiniteElement::GRAD) {
throw std::invalid_argument(
"Compactification coordinate finite element does not provide a "
"gradient."
);
}
if (compactification.GetDofCount() !=
compactification.GetElement().GetDof()) {
throw std::invalid_argument(
"Compactification coordinate DOF count does not match its "
"finite "
"element."
);
}
}
MappingStatus DomainMapperStateless::EvaluateCompactificationCoordinate(
const ElementCompactificationData &compactification,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
CompactificationPointData &point_data
) const {
const mfem::FiniteElement &element = compactification.GetElement();
const mfem::Vector &dofs = compactification.GetDofs();
const int dof_count = element.GetDof();
if (workspace.GetDimension() != m_options.dimension ||
transformation.GetSpaceDim() != m_options.dimension ||
element.GetDim() != m_options.dimension) {
return MappingStatus::invalid_dimension;
}
if (dofs.Size() != dof_count) {
return MappingStatus::invalid_dimension;
}
for (int i = 0; i < dofs.Size(); ++i) {
if (!std::isfinite(dofs(i)))
return MappingStatus::non_finite_input;
}
transformation.SetIntPoint(&integration_point);
workspace.m_compactification_shape.SetSize(dof_count);
workspace.m_compactification_dshape.SetSize(
dof_count, m_options.dimension
);
element.CalcShape(
integration_point, workspace.m_compactification_shape
);
element.CalcPhysDShape(
transformation, workspace.m_compactification_dshape
);
point_data.coordinate = dofs * workspace.m_compactification_shape;
point_data.coordinate_gradient.SetSize(m_options.dimension);
workspace.m_compactification_dshape.MultTranspose(
dofs, point_data.coordinate_gradient
);
if (!std::isfinite(point_data.coordinate)) {
return MappingStatus::non_finite_result;
}
for (int d = 0; d < point_data.coordinate_gradient.Size(); ++d) {
if (!std::isfinite(point_data.coordinate_gradient(d)))
return MappingStatus::non_finite_result;
}
return MappingStatus::valid;
}
void DomainMapperStateless::EvaluateField(
const ElementDisplacementData &field,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
mfem::Vector &value,
mfem::DenseMatrix &jacobian
) const {
transformation.SetIntPoint(&integration_point);
const mfem::FiniteElement &element = field.GetElement();
const mfem::DenseMatrix &dof_matrix = field.GetDofMatrix();
workspace.m_shape.SetSize(element.GetDof());
workspace.m_mesh_dshape.SetSize(element.GetDof(), m_options.dimension);
element.CalcShape(integration_point, workspace.m_shape);
element.CalcPhysDShape(transformation, workspace.m_mesh_dshape);
value.SetSize(m_options.dimension);
dof_matrix.MultTranspose(workspace.m_shape, value);
jacobian.SetSize(m_options.dimension, m_options.dimension);
mfem::MultAtB(dof_matrix, workspace.m_mesh_dshape, jacobian);
}
MappingStatus DomainMapperStateless::EvaluatePoint(
const ElementMappingData &element_data,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
MappingPointContext &context
) const {
ValidateElementData(element_data);
if (workspace.GetDimension() != m_options.dimension)
throw std::invalid_argument(
"The mapping workspace has the wrong dimension."
);
if (transformation.GetSpaceDim() != m_options.dimension)
throw std::invalid_argument(
"The element transformation has the wrong spatial dimension."
);
if (transformation.GetGeometryType() !=
element_data.displacement.GetElement().GetGeomType())
throw std::invalid_argument(
"The element transformation geometry does not match the "
"supplied "
"element data."
);
transformation.SetIntPoint(&integration_point);
context.reference_position.SetSize(m_options.dimension);
transformation.Transform(integration_point, context.reference_position);
EvaluateField(
element_data.displacement, transformation, integration_point,
workspace, workspace.m_field_value, workspace.m_field_jacobian
);
if (!vector_is_finite(context.reference_position) ||
!vector_is_finite(workspace.m_field_value) ||
!matrix_is_finite(workspace.m_field_jacobian)) {
return MappingStatus::non_finite_input;
}
context.displaced_position.SetSize(m_options.dimension);
context.displaced_position = context.reference_position;
context.displaced_position += workspace.m_field_value;
context.displacement_jacobian.SetSize(
m_options.dimension, m_options.dimension
);
context.displacement_jacobian = workspace.m_field_jacobian;
for (int i = 0; i < m_options.dimension; ++i)
context.displacement_jacobian(i, i) += 1.0;
context.compactified = IsCompactifiedElement(transformation);
if (context.compactified) {
const MappingStatus coordinate_status =
EvaluateCompactificationCoordinate(
element_data.compactification, transformation,
integration_point, workspace,
workspace.m_compactification_point
);
if (coordinate_status != MappingStatus::valid)
return coordinate_status;
const compactification::ExteriorMapInput exterior_input{
.reference_position = context.reference_position,
.displaced_position = context.displaced_position,
.displacement_jacobian = context.displacement_jacobian,
.compactification_coordinate =
workspace.m_compactification_point.coordinate,
.compactification_coordinate_gradient =
workspace.m_compactification_point.coordinate_gradient
};
const MappingStatus exterior_status = m_exterior_map->Evaluate(
exterior_input, workspace.m_exterior_result
);
if (exterior_status != MappingStatus::valid)
return exterior_status;
context.physical_position =
workspace.m_exterior_result.physical_position;
context.mapping_jacobian =
workspace.m_exterior_result.mapping_jacobian;
} else {
context.physical_position = context.displaced_position;
context.mapping_jacobian = context.displacement_jacobian;
}
if (!vector_is_finite(context.physical_position) ||
!matrix_is_finite(context.mapping_jacobian))
return MappingStatus::non_finite_result;
context.mapping_determinant = context.mapping_jacobian.Det();
if (!std::isfinite(context.mapping_determinant))
return MappingStatus::non_finite_result;
if (context.mapping_determinant <= 0.0)
return MappingStatus::non_positive_determinant;
context.inverse_mapping_jacobian.SetSize(
m_options.dimension, m_options.dimension
);
mfem::CalcInverse(
context.mapping_jacobian, context.inverse_mapping_jacobian
);
if (!matrix_is_finite(context.inverse_mapping_jacobian))
return MappingStatus::non_finite_result;
return MappingStatus::valid;
}
MappingStatus DomainMapperStateless::EvaluateVolume(
const ElementMappingData &element_data,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
VolumeMappingContext &context
) const {
const MappingStatus point_status = EvaluatePoint(
element_data, transformation, integration_point, workspace,
context.mapping
);
if (point_status != MappingStatus::valid)
return point_status;
transformation.SetIntPoint(&integration_point);
mfem::Mult(
context.mapping.mapping_jacobian, transformation.Jacobian(),
workspace.m_full_element_jacobian
);
context.quadrature.J_inv.SetSize(
m_options.dimension, m_options.dimension
);
mfem::CalcInverse(
workspace.m_full_element_jacobian, context.quadrature.J_inv
);
context.quadrature.detJ = context.mapping.mapping_determinant;
context.quadrature.weight = integration_point.weight *
transformation.Weight() *
context.mapping.mapping_determinant;
if (!matrix_is_finite(context.quadrature.J_inv) ||
!std::isfinite(context.quadrature.weight))
return MappingStatus::non_finite_result;
if (context.quadrature.weight <= 0.0)
return MappingStatus::non_positive_determinant;
return MappingStatus::valid;
}
mfem::ElementTransformation &
DomainMapperStateless::SelectFaceElementTransformation(
mfem::FaceElementTransformations &transformation,
const FaceElementSide side
) {
if (side == FaceElementSide::element_1) {
MFEM_VERIFY(
transformation.Elem1 != nullptr,
"The face does not have an element-1 transformation."
);
return *transformation.Elem1;
}
MFEM_VERIFY(
transformation.Elem2 != nullptr,
"The face does not have an element-2 transformation."
);
return *transformation.Elem2;
}
const mfem::IntegrationPoint &
DomainMapperStateless::SelectFaceElementIntegrationPoint(
mfem::FaceElementTransformations &transformation,
const FaceElementSide side
) {
mfem::ElementTransformation &element_transformation =
SelectFaceElementTransformation(transformation, side);
return element_transformation.GetIntPoint();
}
MappingStatus DomainMapperStateless::EvaluateFace(
const ElementMappingData &element_data,
mfem::FaceElementTransformations &transformation,
const FaceElementSide side,
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
FaceMappingContext &context
) const {
transformation.SetAllIntPoints(&integration_point);
mfem::ElementTransformation &element_transformation =
SelectFaceElementTransformation(transformation, side);
const mfem::IntegrationPoint &element_integration_point =
SelectFaceElementIntegrationPoint(transformation, side);
const MappingStatus point_status = EvaluatePoint(
element_data, element_transformation, element_integration_point,
workspace, context.mapping
);
if (point_status != MappingStatus::valid)
return point_status;
workspace.m_reference_normal.SetSize(m_options.dimension);
mfem::CalcOrtho(
transformation.Jacobian(), workspace.m_reference_normal
);
if (side == FaceElementSide::element_2)
workspace.m_reference_normal *= -1.0;
const double reference_normal_magnitude =
workspace.m_reference_normal.Norml2();
if (!std::isfinite(reference_normal_magnitude) ||
reference_normal_magnitude <= 0.0)
return MappingStatus::non_finite_result;
context.reference_normal.SetSize(m_options.dimension);
context.reference_normal = workspace.m_reference_normal;
context.reference_normal /= reference_normal_magnitude;
context.mapping.inverse_mapping_jacobian.MultTranspose(
workspace.m_reference_normal, workspace.m_mapped_normal
);
workspace.m_mapped_normal *= context.mapping.mapping_determinant;
const double mapped_normal_magnitude =
workspace.m_mapped_normal.Norml2();
if (!std::isfinite(mapped_normal_magnitude) ||
mapped_normal_magnitude <= 0.0)
return MappingStatus::non_finite_result;
context.quadrature.normal.SetSize(m_options.dimension);
context.quadrature.normal = workspace.m_mapped_normal;
context.quadrature.normal /= mapped_normal_magnitude;
context.reference_surface_weight =
integration_point.weight * reference_normal_magnitude;
context.physical_surface_weight =
integration_point.weight * mapped_normal_magnitude;
context.quadrature.ds = context.reference_surface_weight;
context.quadrature.v_dot_n_scale =
mapped_normal_magnitude / reference_normal_magnitude;
if (!vector_is_finite(context.quadrature.normal) ||
!std::isfinite(context.reference_surface_weight) ||
!std::isfinite(context.physical_surface_weight) ||
!std::isfinite(context.quadrature.v_dot_n_scale)) {
return MappingStatus::non_finite_result;
}
return MappingStatus::valid;
}
MappingStatus DomainMapperStateless::EvaluatePointVariation(
const ElementMappingData &element_data,
const ElementDisplacementData &direction,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
const MappingPointContext &base_context,
Workspace &workspace,
MappingPointVariation &variation
) const {
ValidateElementData(element_data);
const ElementMappingData direction_data{
.displacement = direction,
.compactification = element_data.compactification
};
ValidateElementData(direction_data);
if (element_data.displacement.GetDofCount() != direction.GetDofCount())
throw std::invalid_argument(
"The displacement and direction elements have different "
"degree-of-freedom counts."
);
if (workspace.GetDimension() != m_options.dimension)
throw std::invalid_argument(
"The mapping workspace has the wrong dimension."
);
if (base_context.compactified != IsCompactifiedElement(transformation))
throw std::invalid_argument(
"The base mapping context does not match the current element "
"domain."
);
EvaluateField(
direction, transformation, integration_point, workspace,
workspace.m_field_value, workspace.m_field_jacobian
);
if (!vector_is_finite(workspace.m_field_value) ||
!matrix_is_finite(workspace.m_field_jacobian))
return MappingStatus::non_finite_input;
variation.displacement_variation = workspace.m_field_value;
variation.displacement_jacobian_variation = workspace.m_field_jacobian;
if (base_context.compactified) {
const MappingStatus coordinate_status =
EvaluateCompactificationCoordinate(
element_data.compactification, transformation,
integration_point, workspace,
workspace.m_compactification_point
);
if (coordinate_status != MappingStatus::valid)
return coordinate_status;
const compactification::ExteriorMapInput exterior_input{
.reference_position = base_context.reference_position,
.displaced_position = base_context.displaced_position,
.displacement_jacobian = base_context.displacement_jacobian,
.compactification_coordinate =
workspace.m_compactification_point.coordinate,
.compactification_coordinate_gradient =
workspace.m_compactification_point.coordinate_gradient
};
workspace.m_exterior_result.physical_position =
base_context.physical_position;
workspace.m_exterior_result.mapping_jacobian =
base_context.mapping_jacobian;
const compactification::ExteriorMapDirection exterior_direction{
.displaced_position_variation =
variation.displacement_variation,
.displacement_jacobian_variation =
variation.displacement_jacobian_variation
};
// ReSharper disable once CppTooWideScopeInitStatement
const MappingStatus exterior_status =
m_exterior_map->EvaluateVariation(
exterior_input, workspace.m_exterior_result,
exterior_direction, workspace.m_exterior_variation
);
if (exterior_status != MappingStatus::valid) {
return exterior_status;
}
variation.physical_position_variation =
workspace.m_exterior_variation.physical_position_variation;
variation.mapping_jacobian_variation =
workspace.m_exterior_variation.mapping_jacobian_variation;
} else {
variation.physical_position_variation =
variation.displacement_variation;
variation.mapping_jacobian_variation =
variation.displacement_jacobian_variation;
}
mfem::Mult(
base_context.inverse_mapping_jacobian,
variation.mapping_jacobian_variation, workspace.m_matrix_temp_1
);
double trace = 0.0;
for (int i = 0; i < m_options.dimension; ++i)
trace += workspace.m_matrix_temp_1(i, i);
variation.mapping_determinant_variation =
base_context.mapping_determinant * trace;
variation.inverse_mapping_jacobian_variation.SetSize(
m_options.dimension, m_options.dimension
);
mfem::Mult(
workspace.m_matrix_temp_1, base_context.inverse_mapping_jacobian,
variation.inverse_mapping_jacobian_variation
);
variation.inverse_mapping_jacobian_variation *= -1.0;
if (!vector_is_finite(variation.physical_position_variation) ||
!matrix_is_finite(variation.mapping_jacobian_variation) ||
!matrix_is_finite(variation.inverse_mapping_jacobian_variation) ||
!std::isfinite(variation.mapping_determinant_variation)) {
return MappingStatus::non_finite_result;
}
return MappingStatus::valid;
}
MappingStatus DomainMapperStateless::EvaluateVolumeVariation(
const ElementMappingData &element_data,
const ElementDisplacementData &direction,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
const VolumeMappingContext &base_context,
Workspace &workspace,
VolumeMappingVariation &variation
) const {
const MappingStatus point_status = EvaluatePointVariation(
element_data, direction, transformation, integration_point,
base_context.mapping, workspace, variation.mapping
);
if (point_status != MappingStatus::valid)
return point_status;
transformation.SetIntPoint(&integration_point);
mfem::Mult(
variation.mapping.mapping_jacobian_variation,
transformation.Jacobian(), workspace.m_full_element_jacobian
);
mfem::Mult(
base_context.quadrature.J_inv, workspace.m_full_element_jacobian,
workspace.m_matrix_temp_1
);
variation.inverse_element_jacobian_variation.SetSize(
m_options.dimension, m_options.dimension
);
mfem::Mult(
workspace.m_matrix_temp_1, base_context.quadrature.J_inv,
variation.inverse_element_jacobian_variation
);
variation.inverse_element_jacobian_variation *= -1.0;
variation.weight_variation =
integration_point.weight * transformation.Weight() *
variation.mapping.mapping_determinant_variation;
if (!matrix_is_finite(variation.inverse_element_jacobian_variation) ||
!std::isfinite(variation.weight_variation))
return MappingStatus::non_finite_result;
return MappingStatus::valid;
}
MappingStatus DomainMapperStateless::EvaluateFaceVariation(
const ElementMappingData &element_data,
const ElementDisplacementData &direction,
mfem::FaceElementTransformations &transformation,
const FaceElementSide side,
const mfem::IntegrationPoint &integration_point,
const FaceMappingContext &base_context,
Workspace &workspace,
FaceMappingVariation &variation
) const {
transformation.SetAllIntPoints(&integration_point);
mfem::ElementTransformation &element_transformation =
SelectFaceElementTransformation(transformation, side);
const mfem::IntegrationPoint &element_integration_point =
SelectFaceElementIntegrationPoint(transformation, side);
const MappingStatus point_status = EvaluatePointVariation(
element_data, direction, element_transformation,
element_integration_point, base_context.mapping, workspace,
variation.mapping
);
if (point_status != MappingStatus::valid)
return point_status;
workspace.m_reference_normal.SetSize(m_options.dimension);
mfem::CalcOrtho(
transformation.Jacobian(), workspace.m_reference_normal
);
if (side == FaceElementSide::element_2)
workspace.m_reference_normal *= -1.0;
const double reference_normal_magnitude =
workspace.m_reference_normal.Norml2();
if (!std::isfinite(reference_normal_magnitude) ||
reference_normal_magnitude <= 0.0)
return MappingStatus::non_finite_result;
base_context.mapping.inverse_mapping_jacobian.MultTranspose(
workspace.m_reference_normal, workspace.m_vector_temp
);
workspace.m_mapped_normal = workspace.m_vector_temp;
workspace.m_mapped_normal *= base_context.mapping.mapping_determinant;
variation.physical_normal_variation.SetSize(m_options.dimension);
variation.mapping.inverse_mapping_jacobian_variation.MultTranspose(
workspace.m_reference_normal, variation.physical_normal_variation
);
variation.physical_normal_variation *=
base_context.mapping.mapping_determinant;
variation.physical_normal_variation.Add(
variation.mapping.mapping_determinant_variation,
workspace.m_vector_temp
);
const double mapped_normal_magnitude =
workspace.m_mapped_normal.Norml2();
if (!std::isfinite(mapped_normal_magnitude) ||
mapped_normal_magnitude <= 0.0)
return MappingStatus::non_finite_result;
const double mapped_normal_magnitude_variation =
base_context.quadrature.normal *
variation.physical_normal_variation;
variation.physical_normal_variation.Add(
-mapped_normal_magnitude_variation, base_context.quadrature.normal
);
variation.physical_normal_variation /= mapped_normal_magnitude;
variation.physical_surface_weight_variation =
integration_point.weight * mapped_normal_magnitude_variation;
variation.normal_flux_scale_variation =
mapped_normal_magnitude_variation / reference_normal_magnitude;
if (!vector_is_finite(variation.physical_normal_variation) ||
!std::isfinite(variation.physical_surface_weight_variation) ||
!std::isfinite(variation.normal_flux_scale_variation)) {
return MappingStatus::non_finite_result;
}
return MappingStatus::valid;
}
} // namespace mean_field::mapping

View File

@@ -0,0 +1,128 @@
module;
#include <algorithm>
#include <cstddef>
#include <mfem.hpp>
#include <stdexcept>
module mean_field;
import :mapping.prepared_cache;
import :mapping.types;
namespace mean_field::mapping {
namespace {
void pack_vector(
double *&destination,
const mfem::Vector &vector,
const int dimension
) {
if (vector.Size() != dimension)
throw std::invalid_argument("Prepared mapping vector dimension mismatch.");
std::copy_n(vector.HostRead(), dimension, destination);
destination += dimension;
}
void pack_matrix(
double *&destination,
const mfem::DenseMatrix &matrix,
const int dimension
) {
if (matrix.Height() != dimension || matrix.Width() != dimension)
throw std::invalid_argument("Prepared mapping matrix dimension mismatch.");
std::copy_n(matrix.HostRead(), dimension * dimension, destination);
destination += dimension * dimension;
}
void unpack_vector(
const double *&source,
mfem::Vector &vector,
const int dimension
) {
vector.SetSize(dimension);
std::copy_n(source, dimension, vector.HostWrite());
source += dimension;
}
void unpack_matrix(
const double *&source,
mfem::DenseMatrix &matrix,
const int dimension
) {
matrix.SetSize(dimension);
std::copy_n(source, dimension * dimension, matrix.HostWrite());
source += dimension * dimension;
}
} // namespace
void VolumeMappingCache::SetSize(
const int point_count,
const int dimension
) {
if (point_count < 0 || dimension < 1 || dimension > 3)
throw std::invalid_argument("Prepared mapping storage requires nonnegative point count and dimension 1-3.");
const int stride = 3 * dimension + 4 * dimension * dimension + 4;
m_data.resize(static_cast<std::size_t>(point_count) * stride);
m_point_count = point_count;
m_dimension = dimension;
m_point_stride = stride;
}
const double *VolumeMappingCache::GetPointData(const int point) const {
if (point < 0 || point >= m_point_count)
throw std::out_of_range("Prepared mapping quadrature point is out of range.");
return m_data.data() + static_cast<std::size_t>(point) * m_point_stride;
}
void VolumeMappingCache::Store(
const int point,
const VolumeMappingContext &context
) {
// Validate the index through the same checked accessor used by readers.
(void)GetPointData(point);
double *data = m_data.data() + static_cast<std::size_t>(point) * m_point_stride;
pack_vector(data, context.mapping.reference_position, m_dimension);
pack_vector(data, context.mapping.displaced_position, m_dimension);
pack_vector(data, context.mapping.physical_position, m_dimension);
pack_matrix(data, context.mapping.displacement_jacobian, m_dimension);
pack_matrix(data, context.mapping.mapping_jacobian, m_dimension);
pack_matrix(data, context.mapping.inverse_mapping_jacobian, m_dimension);
pack_matrix(data, context.quadrature.J_inv, m_dimension);
*data++ = context.mapping.mapping_determinant;
*data++ = context.mapping.compactified ? 1.0 : 0.0;
*data++ = context.quadrature.detJ;
*data = context.quadrature.weight;
}
void VolumeMappingCache::Load(
const int point,
VolumeMappingContext &context
) const {
const double *data = GetPointData(point);
unpack_vector(data, context.mapping.reference_position, m_dimension);
unpack_vector(data, context.mapping.displaced_position, m_dimension);
unpack_vector(data, context.mapping.physical_position, m_dimension);
unpack_matrix(data, context.mapping.displacement_jacobian, m_dimension);
unpack_matrix(data, context.mapping.mapping_jacobian, m_dimension);
unpack_matrix(data, context.mapping.inverse_mapping_jacobian, m_dimension);
unpack_matrix(data, context.quadrature.J_inv, m_dimension);
context.mapping.mapping_determinant = *data++;
context.mapping.compactified = *data++ != 0.0;
context.quadrature.detJ = *data++;
context.quadrature.weight = *data;
}
void VolumeMappingCache::LoadInverseJacobian(
const int point,
mfem::DenseMatrix &inverse
) const {
const double *data = GetPointData(point) + 3 * m_dimension + 3 * m_dimension * m_dimension;
unpack_matrix(data, inverse, m_dimension);
}
int VolumeMappingCache::GetPointCount() const {
return m_point_count;
}
int VolumeMappingCache::GetDimension() const {
return m_dimension;
}
} // namespace mean_field::mapping

View File

@@ -41,15 +41,12 @@ namespace mean_field::mapping {
mfem::Vector &physical_gradient
) {
MFEM_VERIFY(
reference_gradient.Size() ==
context.inverse_mapping_jacobian.Height(),
reference_gradient.Size() == context.inverse_mapping_jacobian.Height(),
"The reference scalar gradient has the wrong dimension."
);
physical_gradient.SetSize(reference_gradient.Size());
context.inverse_mapping_jacobian.MultTranspose(
reference_gradient, physical_gradient
);
context.inverse_mapping_jacobian.MultTranspose(reference_gradient, physical_gradient);
}
void MapPhysicalGradientToReference(
@@ -63,9 +60,7 @@ namespace mean_field::mapping {
);
reference_gradient.SetSize(physical_gradient.Size());
context.mapping_jacobian.MultTranspose(
physical_gradient, reference_gradient
);
context.mapping_jacobian.MultTranspose(physical_gradient, reference_gradient);
}
void MapReferenceVectorGradientToPhysical(
@@ -74,19 +69,12 @@ namespace mean_field::mapping {
mfem::DenseMatrix &physical_gradient
) {
MFEM_VERIFY(
reference_gradient.Width() ==
context.inverse_mapping_jacobian.Height(),
reference_gradient.Width() == context.inverse_mapping_jacobian.Height(),
"The reference vector gradient has the wrong dimension."
);
physical_gradient.SetSize(
reference_gradient.Height(),
context.inverse_mapping_jacobian.Width()
);
mfem::Mult(
reference_gradient, context.inverse_mapping_jacobian,
physical_gradient
);
physical_gradient.SetSize(reference_gradient.Height(), context.inverse_mapping_jacobian.Width());
mfem::Mult(reference_gradient, context.inverse_mapping_jacobian, physical_gradient);
}
void MapPhysicalVectorGradientToReference(
@@ -99,12 +87,8 @@ namespace mean_field::mapping {
"The physical vector gradient has the wrong dimension."
);
reference_gradient.SetSize(
physical_gradient.Height(), context.mapping_jacobian.Width()
);
mfem::Mult(
physical_gradient, context.mapping_jacobian, reference_gradient
);
reference_gradient.SetSize(physical_gradient.Height(), context.mapping_jacobian.Width());
mfem::Mult(physical_gradient, context.mapping_jacobian, reference_gradient);
}
double MapHDivDivergenceToPhysical(
@@ -120,19 +104,11 @@ namespace mean_field::mapping {
) {
const int dimension = context.mapping_jacobian.Height();
MFEM_VERIFY(
context.mapping_jacobian.Width() == dimension,
"The mapping Jacobian must be square."
);
MFEM_VERIFY(
context.mapping_determinant > 0.0,
"The mapping determinant must be positive."
);
MFEM_VERIFY(context.mapping_jacobian.Width() == dimension, "The mapping Jacobian must be square.");
MFEM_VERIFY(context.mapping_determinant > 0.0, "The mapping determinant must be positive.");
mass_tensor.SetSize(dimension, dimension);
mfem::MultAtB(
context.mapping_jacobian, context.mapping_jacobian, mass_tensor
);
mfem::MultAtB(context.mapping_jacobian, context.mapping_jacobian, mass_tensor);
mass_tensor *= 1 / context.mapping_determinant;
}
@@ -143,19 +119,12 @@ namespace mean_field::mapping {
const int dimension = context.inverse_mapping_jacobian.Height();
MFEM_VERIFY(
context.inverse_mapping_jacobian.Width() == dimension,
"The inverse mapping Jacobian must be square."
);
MFEM_VERIFY(
context.mapping_determinant > 0.0,
"The mapping determinant must be positive."
context.inverse_mapping_jacobian.Width() == dimension, "The inverse mapping Jacobian must be square."
);
MFEM_VERIFY(context.mapping_determinant > 0.0, "The mapping determinant must be positive.");
diffusion_tensor.SetSize(dimension, dimension);
mfem::MultABt(
context.inverse_mapping_jacobian, context.inverse_mapping_jacobian,
diffusion_tensor
);
mfem::MultABt(context.inverse_mapping_jacobian, context.inverse_mapping_jacobian, diffusion_tensor);
diffusion_tensor *= context.mapping_determinant;
}
@@ -170,9 +139,7 @@ namespace mean_field::mapping {
);
physical_field.SetSize(reference_field.Size());
context.inverse_mapping_jacobian.MultTranspose(
reference_field, physical_field
);
context.inverse_mapping_jacobian.MultTranspose(reference_field, physical_field);
}
void MapPhysicalFieldToHCurlReference(
@@ -240,37 +207,32 @@ namespace mean_field::mapping {
mfem::DenseMatrix &mass_tensor_variation
) {
const double determinant = context.mapping_determinant;
const double determinant_variation =
variation.mapping_determinant_variation;
const double determinant_variation = variation.mapping_determinant_variation;
const int dimension = context.inverse_mapping_jacobian.Width();
mass_tensor_variation.SetSize(dimension, dimension);
MFEM_VERIFY(
std::isfinite(determinant) && determinant > 0.0,
"The mapping determinant must be positive and finite."
);
MFEM_VERIFY(
std::isfinite(determinant_variation),
"The mapping determinant variation must be finite."
std::isfinite(determinant) && determinant > 0.0, "The mapping determinant must be positive and finite."
);
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

View File

@@ -0,0 +1,67 @@
module;
#include <cmath>
#include <format>
#include <stdexcept>
#include <utility>
module mean_field;
import :model.structure.polytropic;
namespace mean_field::models::structure {
PolytropicStructure::PolytropicStructure(
eos::Polytrope equationOfState,
const double targetMass
)
: m_equationOfState(std::move(equationOfState)),
m_targetMass(targetMass) {
validate();
}
const eos::Polytrope &PolytropicStructure::equationOfState() const noexcept {
return m_equationOfState;
}
double PolytropicStructure::targetMass() const noexcept {
return m_targetMass;
}
StructureSeed PolytropicStructure::makeInitialSeed(const StructureSeedRequest &request) const {
const seed::RadialProfile profile = seed::generateLaneEmdenProfile(
m_equationOfState, dimensions::DensityValue{request.centralDensity}, request.radialSampleCount
);
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 {
const double polytropicIndex = m_equationOfState.polytropic_index();
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 "
"provided.",
polytropicIndex
)
);
}
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.",
m_targetMass
)
);
}
}
} // namespace mean_field::models::structure

View File

@@ -1,135 +1,190 @@
module;
#include <cstdint>
#include <cmath>
#include <mfem.hpp>
module mean_field;
import :operators.context.barotropic_closure_linearization;
namespace {
void validate_finite_vector(
const mfem::Vector &vector,
const char *message
) {
for (int i = 0; i < vector.Size(); ++i) {
MFEM_VERIFY(std::isfinite(vector(i)), message);
}
}
template <typename Stamp>
void validate_dependency_transition(
const Stamp &prepared,
const Stamp &requested,
const char *message
) {
MFEM_VERIFY(requested.CanFollow(prepared), message);
MFEM_VERIFY(
prepared.identity == requested.identity || prepared.revision != requested.revision,
"A new barotropic-closure dependency identity must also carry a visibly different revision."
);
}
} // namespace
namespace mean_field::operators::context::barotropic {
BarotropicClosureLinearizationContext::
BarotropicClosureLinearizationContext(
BarotropicClosureLinearizationContext::BarotropicClosureLinearizationContext(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const physics::PolytropicBarotrope &barotrope
const mapping::DomainMapper &domainMapper,
const field::FieldDofMap &densityMap,
const field::FieldDofMap &enthalpyMap,
const field::FieldDofMap &displacementMap
)
: m_f(f),
m_operator(
f,
domainMapper,
barotrope
) {
m_domainMapper(domainMapper),
m_densitySize(densityMap.reduced_size()),
m_enthalpySize(enthalpyMap.reduced_size()),
m_displacementSize(displacementMap.reduced_size()) {
MFEM_VERIFY(m_f.mesh != nullptr, "BarotropicClosureLinearizationContext requires a mesh.");
MFEM_VERIFY(m_f.densityFes != nullptr, "BarotropicClosureLinearizationContext requires the density FE space.");
MFEM_VERIFY(
m_f.densityFes != nullptr,
"The closure linearization context requires the "
"density finite-element space."
m_f.enthalpyFes != nullptr, "BarotropicClosureLinearizationContext requires the enthalpy FE space."
);
MFEM_VERIFY(
m_f.enthalpyFes != nullptr,
"The closure linearization context requires the "
"enthalpy finite-element space."
m_f.displacementFes != nullptr, "BarotropicClosureLinearizationContext requires the displacement FE space."
);
MFEM_VERIFY(
m_f.displacementFes != nullptr,
"The closure linearization context requires the "
"displacement finite-element space."
m_domainMapper.GetDimension() == m_f.mesh->Dimension(),
"The barotropic-closure context domain-mapper dimension does not match the mesh dimension."
);
MFEM_VERIFY(
densityMap.full_size() == m_f.densityFes->GetTrueVSize(),
"The density FieldDofMap does not match the density FE space."
);
MFEM_VERIFY(
enthalpyMap.full_size() == m_f.enthalpyFes->GetTrueVSize(),
"The enthalpy FieldDofMap does not match the enthalpy FE space."
);
MFEM_VERIFY(
displacementMap.full_size() == m_f.displacementFes->GetTrueVSize(),
"The displacement FieldDofMap does not match the displacement FE space."
);
}
void BarotropicClosureLinearizationContext::Prepare(
const mfem::Vector &baseDensityTrue,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector &displacementTrue,
const BarotropicClosureRevisions &revisions
BarotropicClosurePreparationReport BarotropicClosureLinearizationContext::Prepare(
const BarotropicClosureStateView &state,
const BarotropicClosureDependencies &dependencies
) {
MFEM_VERIFY(state.density.Size() == m_densitySize, "The supported closure density vector has the wrong size.");
MFEM_VERIFY(
baseDensityTrue.Size() == m_f.densityFes->GetTrueVSize(),
"The closure base-density vector has the wrong size."
state.enthalpy.Size() == m_enthalpySize, "The supported closure enthalpy vector has the wrong size."
);
MFEM_VERIFY(
state.displacement.Size() == m_displacementSize,
"The supported closure displacement vector has the wrong size."
);
MFEM_VERIFY(
baseEnthalpyTrue.Size() == m_f.enthalpyFes->GetTrueVSize(),
"The closure base-enthalpy vector has the wrong size."
);
validate_finite_vector(state.density, "The closure density state contains a non-finite value.");
validate_finite_vector(state.enthalpy, "The closure enthalpy state contains a non-finite value.");
validate_finite_vector(state.displacement, "The closure displacement state contains a non-finite value.");
MFEM_VERIFY(
displacementTrue.Size() == m_f.displacementFes->GetTrueVSize(),
"The closure displacement vector has the wrong size."
if (m_isPrepared) {
validate_dependency_transition(
m_dependencies.discretization, dependencies.discretization,
"BarotropicClosureLinearizationContext received an older discretization revision for the same identity."
);
validate_dependency_transition(
m_dependencies.density, dependencies.density,
"BarotropicClosureLinearizationContext received an older density revision for the same identity."
);
validate_dependency_transition(
m_dependencies.enthalpy, dependencies.enthalpy,
"BarotropicClosureLinearizationContext received an older enthalpy revision for the same identity."
);
validate_dependency_transition(
m_dependencies.displacement, dependencies.displacement,
"BarotropicClosureLinearizationContext received an older displacement revision for the same identity."
);
if (m_isPrepared && revisions == m_revisions) {
return;
}
m_operator.Prepare(baseDensityTrue, baseEnthalpyTrue, displacementTrue);
const bool staticChanged = !m_isPrepared || dependencies.discretization != m_dependencies.discretization;
const bool densityChanged = !m_isPrepared || dependencies.density != m_dependencies.density;
const bool enthalpyChanged = !m_isPrepared || dependencies.enthalpy != m_dependencies.enthalpy;
const bool displacementChanged = !m_isPrepared || dependencies.displacement != m_dependencies.displacement;
m_baseDensityTrue = baseDensityTrue;
m_baseEnthalpyTrue = baseEnthalpyTrue;
m_displacementTrue = displacementTrue;
const bool geometryPreparationRequired = staticChanged || displacementChanged;
const bool baseStatePreparationRequired =
staticChanged || geometryPreparationRequired || densityChanged || enthalpyChanged;
m_revisions = revisions;
BarotropicClosurePreparationReport report;
report.preparedStaticDependencies = staticChanged;
report.preparedGeometryState = geometryPreparationRequired;
report.preparedBaseState = baseStatePreparationRequired;
if (staticChanged || densityChanged) {
m_baseDensity = state.density;
report.updatedDensity = true;
}
if (staticChanged || enthalpyChanged) {
m_baseEnthalpy = state.enthalpy;
report.updatedEnthalpy = true;
}
if (geometryPreparationRequired) {
m_displacement = state.displacement;
report.updatedDisplacement = true;
}
if (report.preparedStaticDependencies) {
++m_statistics.staticPreparations;
}
if (report.preparedGeometryState) {
++m_statistics.geometryPreparations;
}
if (report.preparedBaseState) {
++m_statistics.baseStatePreparations;
}
m_dependencies = dependencies;
m_isPrepared = true;
++m_preparationCount;
return report;
}
bool BarotropicClosureLinearizationContext::IsPrepared() const noexcept {
return m_isPrepared;
}
bool BarotropicClosureLinearizationContext::MatchesRevisions(
const BarotropicClosureRevisions &revisions
bool BarotropicClosureLinearizationContext::MatchesDependencies(
const BarotropicClosureDependencies &dependencies
) const noexcept {
return m_isPrepared && revisions == m_revisions;
return m_isPrepared && dependencies == m_dependencies;
}
std::uint64_t BarotropicClosureLinearizationContext::
GetPreparationCount() const noexcept {
return m_preparationCount;
}
const BarotropicClosureRevisions &
BarotropicClosureLinearizationContext::GetRevisions() const {
const BarotropicClosureDependencies &BarotropicClosureLinearizationContext::GetDependencies() const {
VerifyPrepared();
return m_revisions;
return m_dependencies;
}
const mfem::Vector &
BarotropicClosureLinearizationContext::GetBaseDensityTrue() const {
const BarotropicClosurePreparationStatistics &
BarotropicClosureLinearizationContext::GetPreparationStatistics() const noexcept {
return m_statistics;
}
const mfem::Vector &BarotropicClosureLinearizationContext::GetBaseDensity() const {
VerifyPrepared();
return m_baseDensityTrue;
return m_baseDensity;
}
const mfem::Vector &
BarotropicClosureLinearizationContext::GetBaseEnthalpyTrue() const {
const mfem::Vector &BarotropicClosureLinearizationContext::GetBaseEnthalpy() const {
VerifyPrepared();
return m_baseEnthalpyTrue;
return m_baseEnthalpy;
}
const mfem::Vector &
BarotropicClosureLinearizationContext::GetDisplacementTrue() const {
const mfem::Vector &BarotropicClosureLinearizationContext::GetDisplacement() const {
VerifyPrepared();
return m_displacementTrue;
}
const PreparedBarotropicClosureOperator &
BarotropicClosureLinearizationContext::GetOperator() const noexcept {
return m_operator;
}
void BarotropicClosureLinearizationContext::BuildResidual(
mfem::Vector &residual
) const {
VerifyPrepared();
m_operator.BuildResidual(residual);
return m_displacement;
}
void BarotropicClosureLinearizationContext::VerifyPrepared() const {
MFEM_VERIFY(
m_isPrepared, "The barotropic-closure linearization context "
"has not been prepared."
);
MFEM_VERIFY(m_isPrepared, "BarotropicClosureLinearizationContext has not been prepared.");
}
} // namespace mean_field::operators::context::barotropic

View File

@@ -1,5 +1,6 @@
module;
#include <cmath>
#include <expected>
#include <memory>
#include <mfem.hpp>
@@ -7,26 +8,196 @@ module mean_field;
import :operators.context.gravity_field;
namespace {
void validate_displacement(
const mean_field::fem::FEM &f,
const mfem::Vector &displacement_true
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldPreparationRejection
make_gravity_field_rejection(const mean_field::operators::HDivMassPreparationRejection &rejection) {
using ChildReason = mean_field::operators::HDivMassPreparationRejectionReason;
using Failure = mean_field::operators::context::gravity_field::GravityFieldPreparationRejection;
using Reason = mean_field::operators::context::gravity_field::GravityFieldPreparationRejectionReason;
return Failure{
.reason = rejection.reason == ChildReason::invalid_mapping ? Reason::invalid_mapping
: Reason::non_finite_arithmetic,
.mappingStatus = rejection.mappingStatus
};
}
[[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldPreparationRejection
make_gravity_field_rejection(const mean_field::operators::GravitySourcePreparationRejection &rejection) {
using ChildReason = mean_field::operators::GravitySourcePreparationRejectionReason;
using Failure = mean_field::operators::context::gravity_field::GravityFieldPreparationRejection;
using Reason = mean_field::operators::context::gravity_field::GravityFieldPreparationRejectionReason;
return Failure{
.reason = rejection.reason == ChildReason::invalid_mapping ? Reason::invalid_mapping
: Reason::non_finite_arithmetic,
.mappingStatus = rejection.mappingStatus
};
}
void true_to_local(
const mfem::ParFiniteElementSpace &finite_element_space,
const mfem::Vector &true_vector,
mfem::Vector &local_vector
) {
MFEM_VERIFY(
f.displacementFes != nullptr,
"GravityFieldGeometryContext requires the "
"displacement finite-element space."
true_vector.Size() == finite_element_space.GetTrueVSize(),
"True-DOF operator received an input vector with the wrong size."
);
local_vector.SetSize(finite_element_space.GetVSize());
const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(true_vector, local_vector);
} else {
local_vector = true_vector;
}
}
void local_to_true(
const mfem::ParFiniteElementSpace &finite_element_space,
const mfem::Vector &local_vector,
mfem::Vector &true_vector
) {
MFEM_VERIFY(
local_vector.Size() == finite_element_space.GetVSize(),
"True-DOF operator produced a local vector with the wrong size."
);
true_vector.SetSize(finite_element_space.GetTrueVSize());
const mfem::Operator *prolongation = finite_element_space.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(local_vector, true_vector);
} else {
true_vector = local_vector;
}
}
bool communicator_has_single_rank(const MPI_Comm communicator) {
int size = 0;
MFEM_VERIFY(MPI_Comm_size(communicator, &size) == MPI_SUCCESS, "Failed to query the MPI communicator size.");
MFEM_VERIFY(size > 0, "The MPI communicator must contain at least one rank.");
return size == 1;
}
class TrueDofParMixedBilinearFormOperator final : public mfem::Operator {
public:
TrueDofParMixedBilinearFormOperator(
const mfem::ParFiniteElementSpace &trial_space,
const mfem::ParFiniteElementSpace &test_space,
std::unique_ptr<mfem::ParMixedBilinearForm> local_form
)
: Operator(
test_space.GetTrueVSize(),
trial_space.GetTrueVSize()
),
m_trial_space(trial_space),
m_test_space(test_space),
m_local_form(std::move(local_form)),
m_single_rank(communicator_has_single_rank(trial_space.GetComm())) {
int communicators_compare = MPI_UNEQUAL;
MFEM_VERIFY(
MPI_Comm_compare(trial_space.GetComm(), test_space.GetComm(), &communicators_compare) == MPI_SUCCESS,
"Failed to compare mixed-operator MPI communicators."
);
MFEM_VERIFY(
displacement_true.Size() == f.displacementFes->GetTrueVSize(),
communicators_compare == MPI_IDENT || communicators_compare == MPI_CONGRUENT,
"True-DOF mixed operator requires congruent trial and test communicators."
);
MFEM_VERIFY(m_local_form != nullptr, "True-DOF mixed operator requires a local bilinear form.");
MFEM_VERIFY(
m_local_form->Width() == m_trial_space.GetVSize(),
"True-DOF mixed operator received an incompatible trial space."
);
MFEM_VERIFY(
m_local_form->Height() == m_test_space.GetVSize(),
"True-DOF mixed operator received an incompatible test space."
);
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
MFEM_VERIFY(input.Size() == Width(), "True-DOF mixed operator received an input with the wrong size.");
if (m_single_rank) [[likely]] {
output.SetSize(Height());
m_local_form->Mult(input, output);
return;
}
true_to_local(m_trial_space, input, m_trial_local);
m_test_local.SetSize(m_test_space.GetVSize());
m_local_form->Mult(m_trial_local, m_test_local);
local_to_true(m_test_space, m_test_local, output);
}
void MultTranspose(
const mfem::Vector &input,
mfem::Vector &output
) const override {
MFEM_VERIFY(input.Size() == Height(), "True-DOF mixed transpose received an input with the wrong size.");
if (m_single_rank) [[likely]] {
output.SetSize(Width());
m_local_form->MultTranspose(input, output);
return;
}
true_to_local(m_test_space, input, m_test_local);
m_trial_local.SetSize(m_trial_space.GetVSize());
m_local_form->MultTranspose(m_test_local, m_trial_local);
local_to_true(m_trial_space, m_trial_local, output);
}
private:
const mfem::ParFiniteElementSpace &m_trial_space;
const mfem::ParFiniteElementSpace &m_test_space;
std::unique_ptr<mfem::ParMixedBilinearForm> m_local_form;
mutable mfem::Vector m_trial_local;
mutable mfem::Vector m_test_local;
bool m_single_rank;
};
[[nodiscard]] std::unique_ptr<mfem::Operator> make_divergence_operator(const mean_field::fem::FEM &f) {
auto divergence =
std::make_unique<mfem::ParMixedBilinearForm>(f.gravityFluxFes.get(), f.gravityPotentialFes.get());
divergence->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
auto integrator = std::make_unique<mfem::VectorFEDivergenceIntegrator>();
const mfem::FiniteElement &trialElement = *f.gravityFluxFes->GetTypicalFE();
const mfem::FiniteElement &testElement = *f.gravityPotentialFes->GetTypicalFE();
const mfem::ElementTransformation &transformation = *f.mesh->GetElementTransformation(0);
f.quadratureFactory->configure_gravity_divergence(
*integrator, mean_field::quadrature::QuadratureRole::discretization, trialElement, testElement,
transformation, mean_field::utils::DOMAINS::ALL, mean_field::quadrature::MappingKind::none
);
divergence->AddDomainIntegrator(integrator.release());
divergence->Assemble();
return std::make_unique<TrueDofParMixedBilinearFormOperator>(
*f.gravityFluxFes, *f.gravityPotentialFes, std::move(divergence)
);
}
void validate_displacement(
const mean_field::field::FieldDofMap &displacement_map,
const mfem::Vector &displacement
) {
MFEM_VERIFY(
displacement.Size() == displacement_map.reduced_size(),
"GravityFieldGeometryContext received a displacement vector with "
"the "
"wrong size."
);
for (int i = 0; i < displacement_true.Size(); ++i) {
for (int i = 0; i < displacement.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(displacement_true(i)),
"GravityFieldGeometryContext received a non-finite "
std::isfinite(displacement(i)), "GravityFieldGeometryContext received a non-finite "
"displacement "
"value."
);
@@ -34,52 +205,32 @@ namespace {
}
void validate_linearization_state(
const mean_field::fem::FEM &f,
const mean_field::operators::context::gravity_field::
GravityFieldStateView &state
const mean_field::field::FieldDofMap &density_map,
const mean_field::field::FieldDofMap &displacement_map,
const mean_field::field::FieldDofMap &gravity_gradient_map,
const mean_field::field::FieldDofMap &gravity_potential_map,
const mean_field::operators::context::gravity_field::GravityFieldStateView &state
) {
MFEM_VERIFY(
f.densityFes != nullptr, "GravityFieldLinearizationContext "
"requires the density finite-element "
"space."
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"GravityFieldLinearizationContext requires the gravity-potential "
"finite-element space."
);
MFEM_VERIFY(
f.gravityFluxFes != nullptr,
"GravityFieldLinearizationContext requires the "
"gravity-gradient finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"GravityFieldLinearizationContext requires "
"the displacement finite-element space."
);
MFEM_VERIFY(
state.density.Size() == f.densityFes->GetTrueVSize(),
state.density.Size() == density_map.reduced_size(),
"GravityFieldLinearizationContext received a density vector with "
"the "
"wrong size."
);
MFEM_VERIFY(
state.displacement.Size() == f.displacementFes->GetTrueVSize(),
state.displacement.Size() == displacement_map.reduced_size(),
"GravityFieldLinearizationContext received a displacement vector "
"with "
"the wrong size."
);
MFEM_VERIFY(
state.gravity_gradient.Size() == f.gravityFluxFes->GetTrueVSize(),
state.gravity_gradient.Size() == gravity_gradient_map.reduced_size(),
"GravityFieldLinearizationContext received a gravity-gradient "
"vector "
"with the wrong size."
);
MFEM_VERIFY(
state.gravity_potential.Size() ==
f.gravityPotentialFes->GetTrueVSize(),
state.gravity_potential.Size() == gravity_potential_map.reduced_size(),
"GravityFieldLinearizationContext received a gravity-potential "
"vector "
"with the wrong size."
@@ -87,8 +238,7 @@ namespace {
for (int i = 0; i < state.density.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(state.density(i)),
"GravityFieldLinearizationContext received a non-finite "
std::isfinite(state.density(i)), "GravityFieldLinearizationContext received a non-finite "
"density "
"value."
);
@@ -96,8 +246,7 @@ namespace {
for (int i = 0; i < state.displacement.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(state.displacement(i)),
"GravityFieldLinearizationContext received a non-finite "
std::isfinite(state.displacement(i)), "GravityFieldLinearizationContext received a non-finite "
"displacement "
"value."
);
@@ -105,16 +254,14 @@ namespace {
for (int i = 0; i < state.gravity_gradient.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(state.gravity_gradient(i)),
"GravityFieldLinearizationContext received a non-finite "
std::isfinite(state.gravity_gradient(i)), "GravityFieldLinearizationContext received a non-finite "
"gravity-gradient value."
);
}
for (int i = 0; i < state.gravity_potential.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(state.gravity_potential(i)),
"GravityFieldLinearizationContext received a non-finite "
std::isfinite(state.gravity_potential(i)), "GravityFieldLinearizationContext received a non-finite "
"gravity-potential value."
);
}
@@ -124,46 +271,42 @@ namespace {
namespace mean_field::operators::context::gravity_field {
GravityFieldGeometryContext::GravityFieldGeometryContext(
const fem::FEM &f,
const mapping::DomainMapperStateless &domain_mapper
const mapping::DomainMapper &domain_mapper
)
: m_fem(f),
m_domain_mapper(domain_mapper) {
m_domain_mapper(domain_mapper),
m_displacement_map(
field::make_field_dof_map<
field::Displacement,
DomainSchema>(*f.displacementFes)
) {
MFEM_VERIFY(f.mesh != nullptr, "GravityFieldGeometryContext requires a mesh.");
MFEM_VERIFY(
f.mesh != nullptr, "GravityFieldGeometryContext requires a mesh."
);
MFEM_VERIFY(
f.gravityFluxFes != nullptr,
"GravityFieldGeometryContext requires the "
f.gravityFluxFes != nullptr, "GravityFieldGeometryContext requires the "
"gravity-gradient finite-element space."
);
MFEM_VERIFY(
f.densityFes != nullptr,
"GravityFieldGeometryContext requires the density finite-element "
f.densityFes != nullptr, "GravityFieldGeometryContext requires the density finite-element "
"space."
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"GravityFieldGeometryContext requires the gravity-potential "
f.gravityPotentialFes != nullptr, "GravityFieldGeometryContext requires the gravity-potential "
"finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"GravityFieldGeometryContext requires the "
f.displacementFes != nullptr, "GravityFieldGeometryContext requires the "
"displacement finite-element space."
);
MFEM_VERIFY(
f.compactificationFes != nullptr,
"GravityFieldGeometryContext requires the compactification "
f.compactificationFes != nullptr, "GravityFieldGeometryContext requires the compactification "
"finite-element space."
);
MFEM_VERIFY(
f.compactificationCoordinate != nullptr,
"GravityFieldGeometryContext requires the compactification "
f.compactificationCoordinate != nullptr, "GravityFieldGeometryContext requires the compactification "
"coordinate."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr,
"GravityFieldGeometryContext requires the quadrature-rule factory."
f.quadratureFactory != nullptr, "GravityFieldGeometryContext requires the quadrature-rule factory."
);
MFEM_VERIFY(
domain_mapper.GetDimension() == f.mesh->Dimension(),
@@ -173,11 +316,57 @@ namespace mean_field::operators::context::gravity_field {
}
GravityFieldGeometryPreparation GravityFieldGeometryContext::Prepare(
const mfem::Vector &displacement_true,
const mfem::Vector &displacement,
const DiscretizationRevision discretization_revision,
const DisplacementRevision displacement_revision
) {
validate_displacement(m_fem, displacement_true);
auto result = TryPrepareImpl(
displacement, discretization_revision, displacement_revision, PreparationMode::linearization
);
if (!result.has_value()) {
throwGravityFieldPreparationRejection(result.error());
}
return std::move(result).value();
}
GravityFieldPreparationResult<GravityFieldGeometryPreparation> GravityFieldGeometryContext::TryPrepare(
const mfem::Vector &displacement,
const DiscretizationRevision discretization_revision,
const DisplacementRevision displacement_revision
) {
return TryPrepareImpl(
displacement, discretization_revision, displacement_revision, PreparationMode::linearization
);
}
GravityFieldGeometryPreparation GravityFieldGeometryContext::PreparePrimal(
const mfem::Vector &displacement,
const DiscretizationRevision discretization_revision,
const DisplacementRevision displacement_revision
) {
auto result =
TryPrepareImpl(displacement, discretization_revision, displacement_revision, PreparationMode::primal);
if (!result.has_value()) {
throwGravityFieldPreparationRejection(result.error());
}
return std::move(result).value();
}
GravityFieldPreparationResult<GravityFieldGeometryPreparation> GravityFieldGeometryContext::TryPreparePrimal(
const mfem::Vector &displacement,
const DiscretizationRevision discretization_revision,
const DisplacementRevision displacement_revision
) {
return TryPrepareImpl(displacement, discretization_revision, displacement_revision, PreparationMode::primal);
}
GravityFieldPreparationResult<GravityFieldGeometryPreparation> GravityFieldGeometryContext::TryPrepareImpl(
const mfem::Vector &displacement,
const DiscretizationRevision discretization_revision,
const DisplacementRevision displacement_revision,
const PreparationMode mode
) {
validate_displacement(m_displacement_map, displacement);
if (m_is_prepared) {
MFEM_VERIFY(
@@ -192,84 +381,108 @@ namespace mean_field::operators::context::gravity_field {
);
}
const bool discretization_changed =
!m_is_prepared ||
discretization_revision != m_discretization_revision;
const bool displacement_changed =
!m_is_prepared || displacement_revision != m_displacement_revision;
const bool discretization_changed = !m_is_prepared || discretization_revision != m_discretization_revision;
const bool displacement_changed = !m_is_prepared || displacement_revision != m_displacement_revision;
const bool requires_variation = mode == PreparationMode::linearization;
const bool variation_upgrade = requires_variation && !m_variation_state_prepared;
GravityFieldGeometryPreparation preparation;
if (!discretization_changed && !displacement_changed) {
if (!discretization_changed && !displacement_changed && !variation_upgrade) {
return preparation;
}
if (discretization_changed) {
auto mass_operator =
std::make_unique<PreparedMappedHDivMassOperator>(
m_fem, m_domain_mapper
);
auto source_operator =
std::make_unique<PreparedMappedGravitySourceOperator>(
m_fem, m_domain_mapper
);
// The existing child operators may be mutated by a fallible
// preparation below. Stop advertising the parent as prepared until
// every child has accepted the same candidate and the parent state is
// committed.
m_is_prepared = false;
m_variation_state_prepared = false;
mass_operator->Prepare(displacement_true);
source_operator->Prepare(displacement_true);
const auto prepare_mass = [&](PreparedMappedHDivMassOperator &mass_operator) {
if (requires_variation) {
return mass_operator.TryPrepare(displacement);
}
return mass_operator.TryPreparePrimal(displacement);
};
const auto prepare_source = [&](PreparedMappedGravitySourceOperator &source_operator) {
if (requires_variation) {
return source_operator.TryPrepare(displacement);
}
return source_operator.TryPreparePrimal(displacement);
};
if (discretization_changed) {
auto mass_operator = std::make_unique<PreparedMappedHDivMassOperator>(m_fem, m_domain_mapper);
auto source_operator = std::make_unique<PreparedMappedGravitySourceOperator>(m_fem, m_domain_mapper);
auto divergence_operator = make_divergence_operator(m_fem);
auto transpose_divergence_operator = std::make_unique<mfem::TransposeOperator>(divergence_operator.get());
auto massResult = prepare_mass(*mass_operator);
if (!massResult.has_value()) {
return std::unexpected(make_gravity_field_rejection(massResult.error()));
}
auto sourceResult = prepare_source(*source_operator);
if (!sourceResult.has_value()) {
return std::unexpected(make_gravity_field_rejection(sourceResult.error()));
}
m_mass_operator = std::move(mass_operator);
m_source_operator = std::move(source_operator);
m_divergence_operator = std::move(divergence_operator);
m_transpose_divergence_operator = std::move(transpose_divergence_operator);
preparation.reconstructed_operators = true;
preparation.rebuilt_mass_operator = true;
preparation.rebuilt_source_operator = true;
preparation.rebuilt_divergence_operator = true;
} else {
MFEM_VERIFY(
m_mass_operator != nullptr, "GravityFieldGeometryContext has "
"no prepared H(div) mass operator."
);
MFEM_VERIFY(
m_source_operator != nullptr,
"GravityFieldGeometryContext has no prepared gravity source "
m_source_operator != nullptr, "GravityFieldGeometryContext has no prepared gravity source "
"operator."
);
m_mass_operator->Prepare(displacement_true);
m_source_operator->Prepare(displacement_true);
auto massResult = prepare_mass(*m_mass_operator);
if (!massResult.has_value()) {
return std::unexpected(make_gravity_field_rejection(massResult.error()));
}
auto sourceResult = prepare_source(*m_source_operator);
if (!sourceResult.has_value()) {
return std::unexpected(make_gravity_field_rejection(sourceResult.error()));
}
preparation.rebuilt_mass_operator = true;
preparation.rebuilt_source_operator = true;
}
m_displacement_true = displacement_true;
m_displacement_true.SetSize(m_displacement_map.full_size());
m_displacement_map.scatter(displacement, m_displacement_true);
m_discretization_revision = discretization_revision;
m_displacement_revision = displacement_revision;
m_is_prepared = true;
m_variation_state_prepared = requires_variation;
preparation.refreshed_variation_state = true;
preparation.refreshed_variation_state = requires_variation;
return preparation;
}
const PreparedMappedHDivMassOperator &
GravityFieldGeometryContext::GetMassOperator() const {
const PreparedMappedHDivMassOperator &GravityFieldGeometryContext::GetMassOperator() const {
MFEM_VERIFY(
m_is_prepared,
"GravityFieldGeometryContext must be prepared before "
m_is_prepared, "GravityFieldGeometryContext must be prepared before "
"accessing its mass operator."
);
MFEM_VERIFY(
m_mass_operator != nullptr,
"GravityFieldGeometryContext has no prepared H(div) mass operator."
);
MFEM_VERIFY(m_mass_operator != nullptr, "GravityFieldGeometryContext has no prepared H(div) mass operator.");
return *m_mass_operator;
}
const PreparedMappedGravitySourceOperator &
GravityFieldGeometryContext::GetSourceOperator() const {
const PreparedMappedGravitySourceOperator &GravityFieldGeometryContext::GetSourceOperator() const {
MFEM_VERIFY(
m_is_prepared,
"GravityFieldGeometryContext must be prepared before "
m_is_prepared, "GravityFieldGeometryContext must be prepared before "
"accessing its source operator."
);
MFEM_VERIFY(
@@ -279,22 +492,40 @@ namespace mean_field::operators::context::gravity_field {
return *m_source_operator;
}
const mfem::Vector &GravityFieldGeometryContext::GetDisplacement() const {
const mfem::Operator &GravityFieldGeometryContext::GetDivergenceOperator() const {
MFEM_VERIFY(m_is_prepared, "GravityFieldGeometryContext must be prepared before accessing divergence.");
MFEM_VERIFY(m_divergence_operator != nullptr, "GravityFieldGeometryContext has no divergence operator.");
return *m_divergence_operator;
}
const mfem::Operator &GravityFieldGeometryContext::GetTransposeDivergenceOperator() const {
MFEM_VERIFY(
m_is_prepared,
"GravityFieldGeometryContext must be prepared before "
m_is_prepared, "GravityFieldGeometryContext must be prepared before accessing transpose divergence."
);
MFEM_VERIFY(
m_transpose_divergence_operator != nullptr,
"GravityFieldGeometryContext has no transpose-divergence operator."
);
return *m_transpose_divergence_operator;
}
const mfem::Vector &GravityFieldGeometryContext::GetDisplacementTrue() const {
MFEM_VERIFY(
m_is_prepared, "GravityFieldGeometryContext must be prepared before "
"accessing its displacement."
);
return m_displacement_true;
}
DiscretizationRevision
GravityFieldGeometryContext::GetDiscretizationRevision() const noexcept {
const field::FieldDofMap &GravityFieldGeometryContext::GetDisplacementMap() const noexcept {
return m_displacement_map;
}
DiscretizationRevision GravityFieldGeometryContext::GetDiscretizationRevision() const noexcept {
return m_discretization_revision;
}
DisplacementRevision
GravityFieldGeometryContext::GetDisplacementRevision() const noexcept {
DisplacementRevision GravityFieldGeometryContext::GetDisplacementRevision() const noexcept {
return m_displacement_revision;
}
@@ -304,12 +535,27 @@ namespace mean_field::operators::context::gravity_field {
GravityFieldLinearizationContext::GravityFieldLinearizationContext(
const fem::FEM &f,
const mapping::DomainMapperStateless &domain_mapper
const mapping::DomainMapper &domain_mapper
)
: m_fem(f),
m_geometry_context(
f,
domain_mapper
),
m_density_map(
field::make_field_dof_map<
field::Density,
DomainSchema>(*f.densityFes)
),
m_gravity_gradient_map(
field::make_field_dof_map<
field::Gravity,
DomainSchema>(*f.gravityFluxFes)
),
m_gravity_potential_map(
field::make_field_dof_map<
field::Gravity,
DomainSchema>(*f.gravityPotentialFes)
) {
MFEM_VERIFY(
f.densityFes != nullptr, "GravityFieldLinearizationContext "
@@ -317,18 +563,15 @@ namespace mean_field::operators::context::gravity_field {
"space."
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"GravityFieldLinearizationContext requires the gravity-potential "
f.gravityPotentialFes != nullptr, "GravityFieldLinearizationContext requires the gravity-potential "
"finite-element space."
);
MFEM_VERIFY(
f.gravityFluxFes != nullptr,
"GravityFieldLinearizationContext requires the "
f.gravityFluxFes != nullptr, "GravityFieldLinearizationContext requires the "
"gravity-gradient finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"GravityFieldLinearizationContext requires "
f.displacementFes != nullptr, "GravityFieldLinearizationContext requires "
"the displacement finite-element space."
);
}
@@ -337,7 +580,21 @@ namespace mean_field::operators::context::gravity_field {
const GravityFieldStateView &state,
const GravityFieldRevisions &revisions
) {
validate_linearization_state(m_fem, state);
auto result = TryPrepare(state, revisions);
if (!result.has_value()) {
throwGravityFieldPreparationRejection(result.error());
}
return std::move(result).value();
}
GravityFieldPreparationResult<GravityFieldPreparationReport> GravityFieldLinearizationContext::TryPrepare(
const GravityFieldStateView &state,
const GravityFieldRevisions &revisions
) {
validate_linearization_state(
m_density_map, m_geometry_context.GetDisplacementMap(), m_gravity_gradient_map, m_gravity_potential_map,
state
);
if (m_is_prepared) {
MFEM_VERIFY(
@@ -353,8 +610,7 @@ namespace mean_field::operators::context::gravity_field {
"revision."
);
MFEM_VERIFY(
revisions.density >= m_revisions.density,
"GravityFieldLinearizationContext received an older density "
revisions.density >= m_revisions.density, "GravityFieldLinearizationContext received an older density "
"revision."
);
MFEM_VERIFY(
@@ -370,28 +626,35 @@ namespace mean_field::operators::context::gravity_field {
);
}
const bool discretization_changed =
!m_is_prepared ||
revisions.discretization != m_revisions.discretization;
const bool density_changed = !m_is_prepared || discretization_changed ||
revisions.density != m_revisions.density;
const bool discretization_changed = !m_is_prepared || revisions.discretization != m_revisions.discretization;
const bool density_changed =
!m_is_prepared || discretization_changed || revisions.density != m_revisions.density;
const bool gravity_gradient_changed =
!m_is_prepared || discretization_changed ||
revisions.gravity_gradient != m_revisions.gravity_gradient;
!m_is_prepared || discretization_changed || revisions.gravity_gradient != m_revisions.gravity_gradient;
GravityFieldPreparationReport report;
report.geometry = m_geometry_context.Prepare(
state.displacement, revisions.discretization, revisions.displacement
);
// Geometry preparation is fallible and may invalidate one of its
// prepared children. The linearization context must therefore remain
// inaccessible until the complete shared state has been committed.
m_is_prepared = false;
auto geometryResult =
m_geometry_context.TryPrepare(state.displacement, revisions.discretization, revisions.displacement);
if (!geometryResult.has_value()) {
return std::unexpected(geometryResult.error());
}
report.geometry = std::move(geometryResult).value();
if (density_changed) {
m_density_true = state.density;
m_density_true.SetSize(m_density_map.full_size());
m_density_map.scatter(state.density, m_density_true);
report.updated_density = true;
}
if (gravity_gradient_changed) {
m_gravity_gradient_true = state.gravity_gradient;
m_gravity_gradient_true.SetSize(m_gravity_gradient_map.full_size());
m_gravity_gradient_map.scatter(state.gravity_gradient, m_gravity_gradient_true);
report.updated_gravity_gradient = true;
}
@@ -401,8 +664,7 @@ namespace mean_field::operators::context::gravity_field {
return report;
}
const GravityFieldGeometryContext &
GravityFieldLinearizationContext::GetGeometryContext() const {
const GravityFieldGeometryContext &GravityFieldLinearizationContext::GetGeometryContext() const {
MFEM_VERIFY(
m_is_prepared, "GravityFieldLinearizationContext must be prepared "
"before accessing its geometry context."
@@ -410,7 +672,7 @@ namespace mean_field::operators::context::gravity_field {
return m_geometry_context;
}
const mfem::Vector &GravityFieldLinearizationContext::GetDensity() const {
const mfem::Vector &GravityFieldLinearizationContext::GetDensityTrue() const {
MFEM_VERIFY(
m_is_prepared, "GravityFieldLinearizationContext must be prepared "
"before accessing its density."
@@ -418,8 +680,7 @@ namespace mean_field::operators::context::gravity_field {
return m_density_true;
}
const mfem::Vector &
GravityFieldLinearizationContext::GetGravityGradient() const {
const mfem::Vector &GravityFieldLinearizationContext::GetGravityGradientTrue() const {
MFEM_VERIFY(
m_is_prepared, "GravityFieldLinearizationContext must be prepared "
"before accessing its gravity gradient."
@@ -427,8 +688,23 @@ namespace mean_field::operators::context::gravity_field {
return m_gravity_gradient_true;
}
const GravityFieldRevisions &
GravityFieldLinearizationContext::GetRevisions() const {
const field::FieldDofMap &GravityFieldLinearizationContext::GetDensityMap() const noexcept {
return m_density_map;
}
const field::FieldDofMap &GravityFieldLinearizationContext::GetDisplacementMap() const noexcept {
return m_geometry_context.GetDisplacementMap();
}
const field::FieldDofMap &GravityFieldLinearizationContext::GetGravityGradientMap() const noexcept {
return m_gravity_gradient_map;
}
const field::FieldDofMap &GravityFieldLinearizationContext::GetGravityPotentialMap() const noexcept {
return m_gravity_potential_map;
}
const GravityFieldRevisions &GravityFieldLinearizationContext::GetRevisions() const {
MFEM_VERIFY(
m_is_prepared, "GravityFieldLinearizationContext must be prepared "
"before accessing its revisions."

View File

@@ -9,6 +9,8 @@ module mean_field;
import :operators.context.hydrostatic_equilibrium;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
void validate_finite_vector(
const mfem::Vector &vector,
const char *message
@@ -19,45 +21,24 @@ namespace {
}
void validate_state(
const mean_field::fem::FEM &f,
const mean_field::operators::context::hydrostatic::
HydrostaticEquilibriumStateView &state
const mean_field::field::FieldDofMap &enthalpyMap,
const mean_field::field::FieldDofMap &gravityPotentialMap,
const mean_field::field::FieldDofMap &displacementMap,
const mean_field::operators::context::hydrostatic::HydrostaticEquilibriumStateView &state
) {
MFEM_VERIFY(
f.enthalpyFes != nullptr,
"HydrostaticEquilibriumContext requires the "
"enthalpy finite-element space."
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"HydrostaticEquilibriumContext requires the "
"gravity-potential finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"HydrostaticEquilibriumContext requires the "
"displacement finite-element space."
);
MFEM_VERIFY(
state.enthalpy.Size() == f.enthalpyFes->GetTrueVSize(),
"HydrostaticEquilibriumContext received an "
state.enthalpy.Size() == enthalpyMap.reduced_size(), "HydrostaticEquilibriumContext received a supported "
"enthalpy vector with the wrong size."
);
MFEM_VERIFY(
state.gravityPotential.Size() ==
f.gravityPotentialFes->GetTrueVSize(),
"HydrostaticEquilibriumContext received a "
"gravity-potential vector with the wrong size."
state.gravityPotential.Size() == gravityPotentialMap.reduced_size(),
"HydrostaticEquilibriumContext received a supported gravity-potential vector with the wrong size."
);
MFEM_VERIFY(
state.displacement.Size() == f.displacementFes->GetTrueVSize(),
"HydrostaticEquilibriumContext received a "
"displacement vector with the wrong size."
state.displacement.Size() == displacementMap.reduced_size(),
"HydrostaticEquilibriumContext received a supported displacement vector with the wrong size."
);
validate_finite_vector(
@@ -76,8 +57,7 @@ namespace {
);
MFEM_VERIFY(
std::isfinite(state.bernoulliConstant),
"HydrostaticEquilibriumContext received a "
std::isfinite(state.bernoulliConstant), "HydrostaticEquilibriumContext received a "
"non-finite Bernoulli constant."
);
}
@@ -95,46 +75,58 @@ namespace {
namespace mean_field::operators::context::hydrostatic {
HydrostaticEquilibriumContext::HydrostaticEquilibriumContext(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper
const mapping::DomainMapper &domainMapper
)
: m_f(f),
m_domainMapper(domainMapper) {
MFEM_VERIFY(
m_f.mesh != nullptr,
"HydrostaticEquilibriumContext requires a mesh."
);
m_domainMapper(domainMapper),
m_enthalpyMap(
field::make_field_dof_map<
field::Enthalpy,
DomainSchema>(*f.enthalpyFes)
),
m_gravityPotentialMap(
field::make_field_dof_map<
field::Gravity,
DomainSchema>(*f.gravityPotentialFes)
),
m_displacementMap(
field::make_field_dof_map<
field::Displacement,
DomainSchema>(*f.displacementFes)
) {
MFEM_VERIFY(m_f.mesh != nullptr, "HydrostaticEquilibriumContext requires a mesh.");
MFEM_VERIFY(
m_f.enthalpyFes != nullptr,
"HydrostaticEquilibriumContext requires the "
m_f.enthalpyFes != nullptr, "HydrostaticEquilibriumContext requires the "
"enthalpy finite-element space."
);
MFEM_VERIFY(
m_f.gravityPotentialFes != nullptr,
"HydrostaticEquilibriumContext requires the "
m_f.gravityPotentialFes != nullptr, "HydrostaticEquilibriumContext requires the "
"gravity-potential finite-element space."
);
MFEM_VERIFY(
m_f.displacementFes != nullptr,
"HydrostaticEquilibriumContext requires the "
m_f.displacementFes != nullptr, "HydrostaticEquilibriumContext requires the "
"displacement finite-element space."
);
MFEM_VERIFY(
m_domainMapper.GetDimension() == m_f.mesh->Dimension(),
"The hydrostatic context's stateless "
m_domainMapper.GetDimension() == m_f.mesh->Dimension(), "The hydrostatic context's stateless "
"domain-mapper dimension does not match the mesh "
"dimension."
);
m_baseEnthalpyTrue.SetSize(m_enthalpyMap.full_size());
m_baseGravityPotentialTrue.SetSize(m_gravityPotentialMap.full_size());
m_displacementTrue.SetSize(m_displacementMap.full_size());
}
HydrostaticPreparationReport HydrostaticEquilibriumContext::Prepare(
const HydrostaticEquilibriumStateView &state,
const HydrostaticEquilibriumDependencies &dependencies
) {
validate_state(m_f, state);
validate_state(m_enthalpyMap, m_gravityPotentialMap, m_displacementMap, state);
if (m_isPrepared) {
validate_dependency_transition(
@@ -168,44 +160,32 @@ namespace mean_field::operators::context::hydrostatic {
);
validate_dependency_transition(
m_dependencies.bernoulliConstant,
dependencies.bernoulliConstant,
m_dependencies.bernoulliConstant, dependencies.bernoulliConstant,
"HydrostaticEquilibriumContext received an older "
"Bernoulli-constant revision for the same identity."
);
}
const bool staticChanged =
!m_isPrepared ||
dependencies.discretization != m_dependencies.discretization;
const bool staticChanged = !m_isPrepared || dependencies.discretization != m_dependencies.discretization;
const bool enthalpyChanged =
!m_isPrepared || dependencies.enthalpy != m_dependencies.enthalpy;
const bool enthalpyChanged = !m_isPrepared || dependencies.enthalpy != m_dependencies.enthalpy;
const bool gravityPotentialChanged =
!m_isPrepared ||
dependencies.gravityPotential != m_dependencies.gravityPotential;
!m_isPrepared || dependencies.gravityPotential != m_dependencies.gravityPotential;
const bool displacementChanged =
!m_isPrepared ||
dependencies.displacement != m_dependencies.displacement;
const bool displacementChanged = !m_isPrepared || dependencies.displacement != m_dependencies.displacement;
const bool rotationChanged =
!m_isPrepared || dependencies.rotation != m_dependencies.rotation;
const bool rotationChanged = !m_isPrepared || dependencies.rotation != m_dependencies.rotation;
const bool bernoulliConstantChanged =
!m_isPrepared ||
dependencies.bernoulliConstant != m_dependencies.bernoulliConstant;
!m_isPrepared || dependencies.bernoulliConstant != m_dependencies.bernoulliConstant;
const bool geometryPreparationRequired =
staticChanged || displacementChanged;
const bool geometryPreparationRequired = staticChanged || displacementChanged;
const bool rotationPreparationRequired =
geometryPreparationRequired || rotationChanged;
const bool rotationPreparationRequired = geometryPreparationRequired || rotationChanged;
const bool baseStatePreparationRequired =
rotationPreparationRequired || enthalpyChanged ||
gravityPotentialChanged || bernoulliConstantChanged;
rotationPreparationRequired || enthalpyChanged || gravityPotentialChanged || bernoulliConstantChanged;
HydrostaticPreparationReport report;
@@ -215,18 +195,18 @@ namespace mean_field::operators::context::hydrostatic {
report.preparedBaseState = baseStatePreparationRequired;
if (staticChanged || enthalpyChanged) {
m_baseEnthalpyTrue = state.enthalpy;
m_enthalpyMap.scatter(state.enthalpy, m_baseEnthalpyTrue);
report.updatedEnthalpy = true;
}
if (staticChanged || gravityPotentialChanged) {
m_baseGravityPotentialTrue = state.gravityPotential;
m_gravityPotentialMap.scatter(state.gravityPotential, m_baseGravityPotentialTrue);
report.updatedGravityPotential = true;
}
if (geometryPreparationRequired) {
m_displacementTrue = state.displacement;
m_displacementMap.scatter(state.displacement, m_displacementTrue);
report.updatedDisplacement = true;
}
@@ -267,31 +247,38 @@ namespace mean_field::operators::context::hydrostatic {
return m_isPrepared && dependencies == m_dependencies;
}
const HydrostaticEquilibriumDependencies &
HydrostaticEquilibriumContext::GetDependencies() const {
const HydrostaticEquilibriumDependencies &HydrostaticEquilibriumContext::GetDependencies() const {
VerifyPrepared();
return m_dependencies;
}
const HydrostaticPreparationStatistics &
HydrostaticEquilibriumContext::GetPreparationStatistics() const noexcept {
const HydrostaticPreparationStatistics &HydrostaticEquilibriumContext::GetPreparationStatistics() const noexcept {
return m_statistics;
}
const mfem::Vector &
HydrostaticEquilibriumContext::GetBaseEnthalpyTrue() const {
const field::FieldDofMap &HydrostaticEquilibriumContext::GetEnthalpyMap() const noexcept {
return m_enthalpyMap;
}
const field::FieldDofMap &HydrostaticEquilibriumContext::GetGravityPotentialMap() const noexcept {
return m_gravityPotentialMap;
}
const field::FieldDofMap &HydrostaticEquilibriumContext::GetDisplacementMap() const noexcept {
return m_displacementMap;
}
const mfem::Vector &HydrostaticEquilibriumContext::GetBaseEnthalpyTrue() const {
VerifyPrepared();
return m_baseEnthalpyTrue;
}
const mfem::Vector &
HydrostaticEquilibriumContext::GetBaseGravityPotentialTrue() const {
const mfem::Vector &HydrostaticEquilibriumContext::GetBaseGravityPotentialTrue() const {
VerifyPrepared();
return m_baseGravityPotentialTrue;
}
const mfem::Vector &
HydrostaticEquilibriumContext::GetDisplacementTrue() const {
const mfem::Vector &HydrostaticEquilibriumContext::GetDisplacementTrue() const {
VerifyPrepared();
return m_displacementTrue;
}
@@ -302,8 +289,6 @@ namespace mean_field::operators::context::hydrostatic {
}
void HydrostaticEquilibriumContext::VerifyPrepared() const {
MFEM_VERIFY(
m_isPrepared, "HydrostaticEquilibriumContext has not been prepared."
);
MFEM_VERIFY(m_isPrepared, "HydrostaticEquilibriumContext has not been prepared.");
}
} // namespace mean_field::operators::context::hydrostatic

View File

@@ -0,0 +1,219 @@
module;
#include <cmath>
#include <mfem.hpp>
module mean_field;
import :operators.context.pressure_force;
namespace {
void validate_finite_vector(
const mfem::Vector &vector,
const char *message
) {
for (int index = 0; index < vector.Size(); ++index) {
MFEM_VERIFY(std::isfinite(vector(index)), message);
}
}
template <typename Dependency>
void validate_dependency_transition(
const Dependency &prepared,
const Dependency &requested,
const char *message
) {
MFEM_VERIFY(requested.CanFollow(prepared), message);
MFEM_VERIFY(
prepared.identity == requested.identity || prepared.revision != requested.revision,
"A new pressure-force dependency identity must also carry "
"a visibly different revision."
);
}
} // namespace
namespace mean_field::operators::context::pressure_force {
PressureForceLinearizationContext::PressureForceLinearizationContext(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const field::FieldDofMap &enthalpyMap,
const field::FieldDofMap &displacementMap
)
: m_enthalpySize(enthalpyMap.reduced_size()),
m_displacementSize(displacementMap.reduced_size()) {
MFEM_VERIFY(f.mesh != nullptr, "PressureForceLinearizationContext requires a mesh.");
MFEM_VERIFY(
f.enthalpyFes != nullptr, "PressureForceLinearizationContext requires the enthalpy "
"finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr, "PressureForceLinearizationContext requires the displacement "
"finite-element space."
);
MFEM_VERIFY(
domainMapper.GetDimension() == f.mesh->Dimension(), "The pressure-force context's stateless domain-mapper "
"dimension does not match the mesh dimension."
);
MFEM_VERIFY(
enthalpyMap.full_size() == f.enthalpyFes->GetTrueVSize(),
"The pressure-force enthalpy FieldDofMap does not match the "
"enthalpy finite-element space."
);
MFEM_VERIFY(
displacementMap.full_size() == f.displacementFes->GetTrueVSize(),
"The pressure-force displacement FieldDofMap does not match "
"the displacement finite-element space."
);
}
PressureForcePreparationReport PressureForceLinearizationContext::Prepare(
const PressureForceStateView &state,
const PressureForceDependencies &dependencies
) {
MFEM_VERIFY(
state.enthalpy.Size() == m_enthalpySize, "PressureForceLinearizationContext received a supported "
"enthalpy vector with the wrong size."
);
MFEM_VERIFY(
state.displacement.Size() == m_displacementSize, "PressureForceLinearizationContext received a supported "
"displacement vector with the wrong size."
);
validate_finite_vector(
state.enthalpy, "PressureForceLinearizationContext received a non-finite "
"enthalpy value."
);
validate_finite_vector(
state.displacement, "PressureForceLinearizationContext received a non-finite "
"displacement value."
);
if (m_isPrepared) {
validate_dependency_transition(
m_dependencies.discretization, dependencies.discretization,
"PressureForceLinearizationContext received an older "
"discretization revision for the same identity."
);
validate_dependency_transition(
m_dependencies.enthalpy, dependencies.enthalpy,
"PressureForceLinearizationContext received an older "
"enthalpy revision for the same identity."
);
validate_dependency_transition(
m_dependencies.displacement, dependencies.displacement,
"PressureForceLinearizationContext received an older "
"displacement revision for the same identity."
);
}
const bool discretizationChanged =
!m_isPrepared || dependencies.discretization != m_dependencies.discretization;
const bool enthalpyChanged = !m_isPrepared || dependencies.enthalpy != m_dependencies.enthalpy;
const bool displacementChanged = !m_isPrepared || dependencies.displacement != m_dependencies.displacement;
/*
* Static data depend only on discretization.
*
* Geometry data depend on discretization and displacement.
*
* Material data depend on both geometry and enthalpy because
* pressure and its enthalpy derivative are evaluated on the frozen
* mapped state.
*/
const bool geometryPreparationRequired = discretizationChanged || displacementChanged;
const bool materialPreparationRequired = geometryPreparationRequired || enthalpyChanged;
PressureForcePreparationReport report;
report.preparedStaticDependencies = discretizationChanged;
report.preparedGeometryState = geometryPreparationRequired;
report.preparedMaterialState = materialPreparationRequired;
/*
* A discretization change invalidates every frozen field because
* their coordinate interpretation may have changed.
*/
if (discretizationChanged || enthalpyChanged) {
m_baseEnthalpy = state.enthalpy;
report.updatedEnthalpy = true;
}
if (geometryPreparationRequired) {
m_displacement = state.displacement;
report.updatedDisplacement = true;
}
if (report.preparedStaticDependencies) {
++m_statistics.staticPreparations;
}
if (report.preparedGeometryState) {
++m_statistics.geometryPreparations;
}
if (report.preparedMaterialState) {
++m_statistics.materialPreparations;
}
m_dependencies = dependencies;
m_isPrepared = true;
return report;
}
const PressureForcePreparationStatistics &
PressureForceLinearizationContext::GetPreparationStatistics() const noexcept {
return m_statistics;
}
bool PressureForceLinearizationContext::IsPrepared() const noexcept {
return m_isPrepared;
}
bool PressureForceLinearizationContext::MatchesDependencies(
const PressureForceDependencies &dependencies
) const noexcept {
return m_isPrepared && dependencies == m_dependencies;
}
const PressureForceDependencies &PressureForceLinearizationContext::GetDependencies() const {
VerifyPrepared();
return m_dependencies;
}
const mfem::Vector &PressureForceLinearizationContext::GetBaseEnthalpy() const {
VerifyPrepared();
return m_baseEnthalpy;
}
const mfem::Vector &PressureForceLinearizationContext::GetDisplacement() const {
VerifyPrepared();
return m_displacement;
}
void PressureForceLinearizationContext::VerifyPrepared() const {
MFEM_VERIFY(m_isPrepared, "PressureForceLinearizationContext has not been prepared.");
}
} // namespace mean_field::operators::context::pressure_force

View File

@@ -0,0 +1,225 @@
module;
#include <cmath>
#include <mfem.hpp>
module mean_field;
import :operators.context.rotational_displacement_force;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
void validate_finite_vector(
const mfem::Vector &vector,
const char *message
) {
for (int index = 0; index < vector.Size(); ++index) {
MFEM_VERIFY(std::isfinite(vector(index)), message);
}
}
template <typename Dependency>
void validate_dependency_transition(
const Dependency &prepared,
const Dependency &requested,
const char *message
) {
MFEM_VERIFY(requested.CanFollow(prepared), message);
}
} // namespace
namespace mean_field::operators::context::rotational_displacement_force {
RotationalDisplacementForceLinearizationContext::RotationalDisplacementForceLinearizationContext(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper
)
: m_f(f),
m_densityMap(
field::make_field_dof_map<
field::Density,
DomainSchema>(*f.densityFes)
),
m_displacementMap(
field::make_field_dof_map<
field::Displacement,
DomainSchema>(*f.displacementFes)
) {
MFEM_VERIFY(
m_f.mesh != nullptr, "RotationalDisplacementForceLinearizationContext requires a "
"mesh."
);
MFEM_VERIFY(
m_f.densityFes != nullptr, "RotationalDisplacementForceLinearizationContext requires the "
"density finite-element space."
);
MFEM_VERIFY(
m_f.displacementFes != nullptr, "RotationalDisplacementForceLinearizationContext requires the "
"displacement finite-element space."
);
MFEM_VERIFY(
domainMapper.GetDimension() == m_f.mesh->Dimension(),
"The rotational-displacement-force context's stateless "
"domain-mapper dimension does not match the mesh dimension."
);
}
RotationalDisplacementForcePreparationReport RotationalDisplacementForceLinearizationContext::Prepare(
const RotationalDisplacementForceStateView &state,
const RotationalDisplacementForceDependencies &dependencies
) {
MFEM_VERIFY(
state.density.Size() == m_densityMap.reduced_size(),
"RotationalDisplacementForceLinearizationContext received a "
"density vector with the wrong size."
);
MFEM_VERIFY(
state.displacement.Size() == m_displacementMap.reduced_size(),
"RotationalDisplacementForceLinearizationContext received a "
"displacement vector with the wrong size."
);
validate_finite_vector(
state.density, "RotationalDisplacementForceLinearizationContext received a "
"non-finite density value."
);
validate_finite_vector(
state.displacement, "RotationalDisplacementForceLinearizationContext received a "
"non-finite displacement value."
);
if (m_isPrepared) {
validate_dependency_transition(
m_dependencies.discretization, dependencies.discretization,
"RotationalDisplacementForceLinearizationContext received "
"an older discretization revision for the same identity."
);
validate_dependency_transition(
m_dependencies.density, dependencies.density,
"RotationalDisplacementForceLinearizationContext received "
"an older density revision for the same identity."
);
validate_dependency_transition(
m_dependencies.displacement, dependencies.displacement,
"RotationalDisplacementForceLinearizationContext received "
"an older displacement revision for the same identity."
);
validate_dependency_transition(
m_dependencies.rotation, dependencies.rotation,
"RotationalDisplacementForceLinearizationContext received "
"an older rotation revision for the same identity."
);
}
const bool discretizationChanged =
!m_isPrepared || dependencies.discretization != m_dependencies.discretization;
const bool densityChanged = !m_isPrepared || dependencies.density != m_dependencies.density;
const bool displacementChanged = !m_isPrepared || dependencies.displacement != m_dependencies.displacement;
const bool rotationChanged = !m_isPrepared || dependencies.rotation != m_dependencies.rotation;
const bool geometryPreparationRequired = discretizationChanged || displacementChanged;
const bool rotationPreparationRequired = discretizationChanged || rotationChanged;
const bool baseStatePreparationRequired =
geometryPreparationRequired || rotationPreparationRequired || densityChanged;
RotationalDisplacementForcePreparationReport report;
report.preparedStaticDependencies = discretizationChanged;
report.preparedGeometryState = geometryPreparationRequired;
report.preparedRotationDependencies = rotationPreparationRequired;
report.preparedBaseState = baseStatePreparationRequired;
if (discretizationChanged || densityChanged) {
m_baseDensityTrue.SetSize(m_densityMap.full_size());
m_densityMap.scatter(state.density, m_baseDensityTrue);
report.updatedDensity = true;
}
if (geometryPreparationRequired) {
m_displacementTrue.SetSize(m_displacementMap.full_size());
m_displacementMap.scatter(state.displacement, m_displacementTrue);
report.updatedDisplacement = true;
}
if (report.preparedStaticDependencies) {
++m_statistics.staticPreparations;
}
if (report.preparedGeometryState) {
++m_statistics.geometryPreparations;
}
if (report.preparedRotationDependencies) {
++m_statistics.rotationPreparations;
}
if (report.preparedBaseState) {
++m_statistics.baseStatePreparations;
}
m_dependencies = dependencies;
m_isPrepared = true;
return report;
}
bool RotationalDisplacementForceLinearizationContext::IsPrepared() const noexcept {
return m_isPrepared;
}
bool RotationalDisplacementForceLinearizationContext::MatchesDependencies(
const RotationalDisplacementForceDependencies &dependencies
) const noexcept {
return m_isPrepared && dependencies == m_dependencies;
}
const RotationalDisplacementForceDependencies &
RotationalDisplacementForceLinearizationContext::GetDependencies() const {
VerifyPrepared();
return m_dependencies;
}
const RotationalDisplacementForcePreparationStatistics &
RotationalDisplacementForceLinearizationContext::GetPreparationStatistics() const noexcept {
return m_statistics;
}
const mfem::Vector &RotationalDisplacementForceLinearizationContext::GetBaseDensityTrue() const {
VerifyPrepared();
return m_baseDensityTrue;
}
const mfem::Vector &RotationalDisplacementForceLinearizationContext::GetDisplacementTrue() const {
VerifyPrepared();
return m_displacementTrue;
}
const field::FieldDofMap &RotationalDisplacementForceLinearizationContext::GetDensityMap() const noexcept {
return m_densityMap;
}
const field::FieldDofMap &RotationalDisplacementForceLinearizationContext::GetDisplacementMap() const noexcept {
return m_displacementMap;
}
void RotationalDisplacementForceLinearizationContext::VerifyPrepared() const {
MFEM_VERIFY(
m_isPrepared, "RotationalDisplacementForceLinearizationContext has not been "
"prepared."
);
}
} // namespace mean_field::operators::context::rotational_displacement_force

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@ module;
module mean_field;
import :operators.gravity_field_jacobian;
import :operators.kernels.gravity_field;
import :utils.blocks;
namespace {
@@ -15,9 +14,7 @@ namespace {
) {
const int offset = offsets[index];
const int size = offsets[index + 1] - offset;
return mfem::Vector(
const_cast<mfem::real_t *>(vector.GetData()) + offset, size
);
return mfem::Vector(const_cast<mfem::real_t *>(vector.GetData()) + offset, size);
}
template <int index>
@@ -56,10 +53,7 @@ namespace {
MFEM_VERIFY(offsets[0] == 0, "Block offsets must begin at zero.");
for (int i = 0; i < block_count; ++i)
MFEM_VERIFY(
offsets[i + 1] >= offsets[i],
"Block offsets must be nondecreasing."
);
MFEM_VERIFY(offsets[i + 1] >= offsets[i], "Block offsets must be nondecreasing.");
}
void validate_layout(
@@ -70,29 +64,18 @@ namespace {
using form = mean_field::utils::blocks::gravity_field_form;
constexpr auto density_block =
mean_field::utils::blocks::get_value_block<form>(
mean_field::utils::blocks::density_field.mass_term
);
constexpr auto displacement_block =
mean_field::utils::blocks::get_value_block<form>(
mean_field::utils::blocks::get_value_block<form>(mean_field::utils::blocks::density_field.mass_term);
constexpr auto displacement_block = mean_field::utils::blocks::get_value_block<form>(
mean_field::utils::blocks::displacement_field.geometry_term
);
constexpr auto gravity_gradient_block =
mean_field::utils::blocks::get_value_block<form>(
mean_field::utils::blocks::gravity_field.gradient_term
);
mean_field::utils::blocks::get_value_block<form>(mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_block =
mean_field::utils::blocks::get_value_block<form>(
mean_field::utils::blocks::gravity_field.poisson_term
);
mean_field::utils::blocks::get_value_block<form>(mean_field::utils::blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual_block =
mean_field::utils::blocks::get_residual_block<form>(
mean_field::utils::blocks::gravity_field.gradient_term
);
mean_field::utils::blocks::get_residual_block<form>(mean_field::utils::blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual_block =
mean_field::utils::blocks::get_residual_block<form>(
mean_field::utils::blocks::gravity_field.poisson_term
);
mean_field::utils::blocks::get_residual_block<form>(mean_field::utils::blocks::gravity_field.poisson_term);
validate_offsets(
state_offsets, form::value_block_count,
@@ -105,34 +88,38 @@ namespace {
"form."
);
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
const auto density_map =
mean_field::field::make_field_dof_map<mean_field::field::Density, DomainSchema>(*f.densityFes);
const auto displacement_map =
mean_field::field::make_field_dof_map<mean_field::field::Displacement, DomainSchema>(*f.displacementFes);
const auto flux_map =
mean_field::field::make_field_dof_map<mean_field::field::Gravity, DomainSchema>(*f.gravityFluxFes);
const auto potential_map =
mean_field::field::make_field_dof_map<mean_field::field::Gravity, DomainSchema>(*f.gravityPotentialFes);
MFEM_VERIFY(
get_block_size(state_offsets, density_block) ==
f.densityFes->GetTrueVSize(),
get_block_size(state_offsets, density_block) == density_map.reduced_size(),
"The Jacobian density block has the wrong size."
);
MFEM_VERIFY(
get_block_size(state_offsets, displacement_block) ==
f.displacementFes->GetTrueVSize(),
get_block_size(state_offsets, displacement_block) == displacement_map.reduced_size(),
"The Jacobian displacement block has the wrong size."
);
MFEM_VERIFY(
get_block_size(state_offsets, gravity_gradient_block) ==
f.gravityFluxFes->GetTrueVSize(),
get_block_size(state_offsets, gravity_gradient_block) == flux_map.reduced_size(),
"The Jacobian gravity-gradient block has the wrong size."
);
MFEM_VERIFY(
get_block_size(state_offsets, gravity_potential_block) ==
f.gravityPotentialFes->GetTrueVSize(),
get_block_size(state_offsets, gravity_potential_block) == potential_map.reduced_size(),
"The Jacobian gravity-potential block has the wrong size."
);
MFEM_VERIFY(
get_block_size(residual_offsets, gravity_gradient_residual_block) ==
f.gravityFluxFes->GetTrueVSize(),
get_block_size(residual_offsets, gravity_gradient_residual_block) == flux_map.reduced_size(),
"The Jacobian gradient-residual block has the wrong size."
);
MFEM_VERIFY(
get_block_size(residual_offsets, gravity_poisson_residual_block) ==
f.gravityPotentialFes->GetTrueVSize(),
get_block_size(residual_offsets, gravity_poisson_residual_block) == potential_map.reduced_size(),
"The Jacobian Poisson-residual block has the wrong size."
);
}
@@ -141,53 +128,38 @@ namespace {
namespace mean_field::operators {
GravityFieldJacobianOperator::GravityFieldJacobianOperator(
fem::FEM &f,
const mapping::DomainMapperStateless &domain_mapper,
const context::gravity_field::GravityFieldLinearizationContext
&linearization_context,
const mfem::Array<int> &state_true_offsets,
const mfem::Array<int> &residual_true_offsets
const mapping::DomainMapper &domain_mapper,
const context::gravity_field::GravityFieldLinearizationContext &linearization_context,
const mfem::Array<int> &state_offsets,
const mfem::Array<int> &residual_offsets
)
: Operator(
residual_true_offsets.Last(),
state_true_offsets.Last()
residual_offsets.Last(),
state_offsets.Last()
),
m_fem(f),
m_domain_mapper(domain_mapper),
m_linearization_context(linearization_context),
m_state_true_offsets(state_true_offsets),
m_residual_true_offsets(residual_true_offsets) {
m_state_offsets(state_offsets),
m_residual_offsets(residual_offsets) {
MFEM_VERIFY(
f.densityFes != nullptr,
"GravityFieldJacobianOperator requires the density finite-element "
f.densityFes != nullptr, "GravityFieldJacobianOperator requires the density finite-element "
"space."
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"GravityFieldJacobianOperator requires the gravity-potential "
f.gravityPotentialFes != nullptr, "GravityFieldJacobianOperator requires the gravity-potential "
"finite-element space."
);
MFEM_VERIFY(
f.gravityFluxFes != nullptr,
"GravityFieldJacobianOperator requires the "
f.gravityFluxFes != nullptr, "GravityFieldJacobianOperator requires the "
"gravity-gradient finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"GravityFieldJacobianOperator requires the "
f.displacementFes != nullptr, "GravityFieldJacobianOperator requires the "
"displacement finite-element space."
);
MFEM_VERIFY(
f.gravityContext.b_form != nullptr,
"GravityFieldJacobianOperator requires the divergence operator."
);
MFEM_VERIFY(
f.gravityContext.BT != nullptr,
"GravityFieldJacobianOperator requires the transpose divergence "
"operator."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr,
"GravityFieldJacobianOperator requires the quadrature-rule factory."
f.quadratureFactory != nullptr, "GravityFieldJacobianOperator requires the quadrature-rule factory."
);
MFEM_VERIFY(
domain_mapper.GetDimension() == f.mesh->Dimension(),
@@ -196,7 +168,7 @@ namespace mean_field::operators {
"dimension."
);
validate_layout(f, m_state_true_offsets, m_residual_true_offsets);
validate_layout(f, m_state_offsets, m_residual_offsets);
}
void GravityFieldJacobianOperator::Mult(
@@ -204,110 +176,100 @@ namespace mean_field::operators {
mfem::Vector &action
) const {
MFEM_VERIFY(
m_linearization_context.IsPrepared(),
"GravityFieldJacobianOperator requires a prepared linearization "
m_linearization_context.IsPrepared(), "GravityFieldJacobianOperator requires a prepared linearization "
"context."
);
MFEM_VERIFY(
direction.Size() == Width(),
"GravityFieldJacobianOperator received a direction with the wrong "
direction.Size() == Width(), "GravityFieldJacobianOperator received a direction with the wrong "
"size."
);
using form = utils::blocks::gravity_field_form;
constexpr auto density_block = utils::blocks::get_value_block<form>(
utils::blocks::density_field.mass_term
);
constexpr auto density_block = utils::blocks::get_value_block<form>(utils::blocks::density_field.mass_term);
constexpr auto displacement_block =
utils::blocks::get_value_block<form>(
utils::blocks::displacement_field.geometry_term
);
utils::blocks::get_value_block<form>(utils::blocks::displacement_field.geometry_term);
constexpr auto gravity_gradient_block =
utils::blocks::get_value_block<form>(
utils::blocks::gravity_field.gradient_term
);
utils::blocks::get_value_block<form>(utils::blocks::gravity_field.gradient_term);
constexpr auto gravity_potential_block =
utils::blocks::get_value_block<form>(
utils::blocks::gravity_field.poisson_term
);
utils::blocks::get_value_block<form>(utils::blocks::gravity_field.poisson_term);
constexpr auto gravity_gradient_residual_block =
utils::blocks::get_residual_block<form>(
utils::blocks::gravity_field.gradient_term
);
utils::blocks::get_residual_block<form>(utils::blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual_block =
utils::blocks::get_residual_block<form>(
utils::blocks::gravity_field.poisson_term
);
utils::blocks::get_residual_block<form>(utils::blocks::gravity_field.poisson_term);
const context::gravity_field::GravityFieldGeometryContext
&geometry_context = m_linearization_context.GetGeometryContext();
const mfem::Vector &density = m_linearization_context.GetDensity();
const mfem::Vector &displacement = geometry_context.GetDisplacement();
const mfem::Vector &gravity_gradient =
m_linearization_context.GetGravityGradient();
const context::gravity_field::GravityFieldGeometryContext &geometry_context =
m_linearization_context.GetGeometryContext();
const mfem::Vector &density = m_linearization_context.GetDensityTrue();
const mfem::Vector &gravity_gradient = m_linearization_context.GetGravityGradientTrue();
const mfem::Vector density_direction = make_read_only_value_view(
direction, m_state_true_offsets, density_block
);
const mfem::Vector displacement_direction = make_read_only_value_view(
direction, m_state_true_offsets, displacement_block
);
const mfem::Vector density_direction = make_read_only_value_view(direction, m_state_offsets, density_block);
const mfem::Vector displacement_direction =
make_read_only_value_view(direction, m_state_offsets, displacement_block);
const mfem::Vector gravity_gradient_direction =
make_read_only_value_view(
direction, m_state_true_offsets, gravity_gradient_block
);
make_read_only_value_view(direction, m_state_offsets, gravity_gradient_block);
const mfem::Vector gravity_potential_direction =
make_read_only_value_view(
direction, m_state_true_offsets, gravity_potential_block
);
make_read_only_value_view(direction, m_state_offsets, gravity_potential_block);
const field::FieldDofMap &displacement_map = m_linearization_context.GetDisplacementMap();
const field::FieldDofMap &flux_map = m_linearization_context.GetGravityGradientMap();
const field::FieldDofMap &potential_map = m_linearization_context.GetGravityPotentialMap();
mfem::Vector displacement_direction_true(displacement_map.full_size());
mfem::Vector gravity_gradient_direction_true(flux_map.full_size());
mfem::Vector gravity_potential_direction_true(potential_map.full_size());
displacement_map.scatter(displacement_direction, displacement_direction_true);
flux_map.scatter(gravity_gradient_direction, gravity_gradient_direction_true);
potential_map.scatter(gravity_potential_direction, gravity_potential_direction_true);
action.SetSize(Height());
action = 0.0;
mfem::Vector gravity_gradient_action = make_residual_view(
action, m_residual_true_offsets, gravity_gradient_residual_block
);
mfem::Vector gravity_poisson_action = make_residual_view(
action, m_residual_true_offsets, gravity_poisson_residual_block
);
mfem::Vector gravity_gradient_action =
make_residual_view(action, m_residual_offsets, gravity_gradient_residual_block);
mfem::Vector gravity_poisson_action =
make_residual_view(action, m_residual_offsets, gravity_poisson_residual_block);
mfem::Vector transpose_divergence_action;
mfem::Vector transpose_divergence_action_true;
mfem::Vector transpose_divergence_action(flux_map.reduced_size());
mfem::Vector divergence_action_true;
mfem::Vector source_action;
mfem::Vector mass_variation_action;
mfem::Vector source_variation_action;
mfem::Vector mass_variation_action_true;
mfem::Vector mass_variation_action(flux_map.reduced_size());
mfem::Vector source_variation_action_true;
mfem::Vector source_variation_action(potential_map.reduced_size());
geometry_context.GetMassOperator().Mult(
gravity_gradient_direction, gravity_gradient_action
);
geometry_context.GetSourceOperator().Mult(
density_direction, source_action
);
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, mass_variation_action
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, source_variation_action
geometry_context.GetSourceOperator().MultDisplacementVariationTrue(
density, displacement_direction_true, source_variation_action_true
);
potential_map.gather(source_variation_action_true, source_variation_action);
transpose_divergence_action.SetSize(gravity_gradient_action.Size());
m_fem.gravityContext.BT->Mult(
gravity_potential_direction, transpose_divergence_action
transpose_divergence_action_true.SetSize(flux_map.full_size());
geometry_context.GetTransposeDivergenceOperator().Mult(
gravity_potential_direction_true, transpose_divergence_action_true
);
flux_map.gather(transpose_divergence_action_true, transpose_divergence_action);
gravity_gradient_action += transpose_divergence_action;
gravity_gradient_action += mass_variation_action;
m_fem.gravityContext.b_form->Mult(
gravity_gradient_direction, gravity_poisson_action
);
divergence_action_true.SetSize(potential_map.full_size());
geometry_context.GetDivergenceOperator().Mult(gravity_gradient_direction_true, divergence_action_true);
potential_map.gather(divergence_action_true, gravity_poisson_action);
gravity_poisson_action -= source_action;
gravity_poisson_action -= source_variation_action;
gravity_gradient_action.SyncAliasMemory(action);
gravity_poisson_action.SyncAliasMemory(action);
}
const context::gravity_field::GravityFieldLinearizationContext &

View File

@@ -8,24 +8,32 @@ module;
module mean_field;
import :operators.kernels.barotropic_closure;
import :field.registry;
import :utils.domain;
namespace {
namespace dimensions = mean_field::dimensions;
namespace eos = mean_field::eos;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using ClosureDomain = mean_field::field::FieldDomainT<mean_field::field::Density>;
enum class ClosureAction { residual, density, enthalpy };
[[nodiscard]] bool element_is_in_closure_support(const int attribute) {
return DomainSchema::template attribute_belongs_to<ClosureDomain>(attribute);
}
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
mfem::Vector &localVector
) {
MFEM_VERIFY(
trueVector.Size() == finiteElementSpace.GetTrueVSize(),
"True vector has the wrong size."
);
MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), "True vector has the wrong size.");
localVector.SetSize(finiteElementSpace.GetVSize());
const mfem::Operator *prolongation =
finiteElementSpace.GetProlongationMatrix();
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(trueVector, localVector);
@@ -39,16 +47,12 @@ namespace {
const mfem::Vector &localVector,
mfem::Vector &trueVector
) {
MFEM_VERIFY(
localVector.Size() == finiteElementSpace.GetVSize(),
"Local vector has the wrong size."
);
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();
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(localVector, trueVector);
@@ -57,19 +61,13 @@ namespace {
}
}
int get_eos_extra_order(
const mean_field::physics::PolytropicBarotrope &barotrope
) {
const double extraOrder =
(barotrope.polytropic_index() - 1.0) *
static_cast<double>(
mean_field::field::Enthalpy::Scalar::familyOrder
);
int get_eos_extra_order(const mean_field::eos::Polytrope &barotrope) {
const double extraOrder = (barotrope.polytropic_index() - 1.0) *
static_cast<double>(mean_field::field::Enthalpy::Scalar::familyOrder);
MFEM_VERIFY(
std::isfinite(extraOrder) && extraOrder >= 0.0 &&
extraOrder <=
static_cast<double>(std::numeric_limits<int>::max()),
extraOrder <= static_cast<double>(std::numeric_limits<int>::max()),
"The EOS effective polynomial order is invalid."
);
@@ -78,43 +76,36 @@ namespace {
const mfem::IntegrationRule &get_eos_rule(
const mean_field::fem::FEM &f,
const mean_field::physics::PolytropicBarotrope &barotrope,
const mean_field::eos::Polytrope &barotrope,
const mfem::FiniteElement &densityElement,
const mfem::FiniteElement &enthalpyElement,
const mfem::ElementTransformation &transformation
) {
using EnthalpyField =
mean_field::field::Field<mean_field::field::Enthalpy>;
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
MFEM_VERIFY(
densityElement.GetOrder() ==
mean_field::field::Density::Scalar::familyOrder,
densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder,
"The EOS test element does not match the "
"registered density field."
);
MFEM_VERIFY(
enthalpyElement.GetOrder() ==
mean_field::field::Enthalpy::Scalar::familyOrder,
enthalpyElement.GetOrder() == mean_field::field::Enthalpy::Scalar::familyOrder,
"The EOS trial element does not match the "
"registered enthalpy field."
);
const mean_field::quadrature::Query query = EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::EosClosureSource>(
mean_field::quadrature::QuadratureRole::discretization,
transformation.OrderW(),
std::array<int, 1>{get_eos_extra_order(barotrope)},
mean_field::utils::DOMAINS::STELLAR,
const mean_field::quadrature::Query query =
EnthalpyField::make_query<mean_field::field::Enthalpy::Form::EosClosureSource>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(),
std::array<int, 1>{get_eos_extra_order(barotrope)}, mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
);
const auto resolution =
f.quadratureFactory->get(query, transformation.GetGeometryType());
const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
resolution.integration_rule != nullptr,
"The quadrature policy did not return an "
resolution.integration_rule != nullptr, "The quadrature policy did not return an "
"EOS-closure integration rule."
);
@@ -123,57 +114,47 @@ namespace {
void validate_common_inputs(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapperStateless &domainMapper,
const mean_field::mapping::DomainMapper &domainMapper,
const mfem::Vector &displacementTrue
) {
MFEM_VERIFY(f.mesh != nullptr, "The EOS closure kernel requires a mesh.");
MFEM_VERIFY(
f.mesh != nullptr, "The EOS closure kernel requires a mesh."
);
MFEM_VERIFY(
f.densityFes != nullptr,
"The EOS closure kernel requires the density "
f.densityFes != nullptr, "The EOS closure kernel requires the density "
"finite-element space."
);
MFEM_VERIFY(
f.enthalpyFes != nullptr,
"The EOS closure kernel requires the enthalpy "
f.enthalpyFes != nullptr, "The EOS closure kernel requires the enthalpy "
"finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"The EOS closure kernel requires the displacement "
f.displacementFes != nullptr, "The EOS closure kernel requires the displacement "
"finite-element space."
);
MFEM_VERIFY(
f.compactificationFes != nullptr,
"The EOS closure kernel requires the "
f.compactificationFes != nullptr, "The EOS closure kernel requires the "
"compactification finite-element space."
);
MFEM_VERIFY(
f.compactificationCoordinate != nullptr,
"The EOS closure kernel requires the "
f.compactificationCoordinate != nullptr, "The EOS closure kernel requires the "
"compactification coordinate."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr,
"The EOS closure kernel requires the quadrature "
f.quadratureFactory != nullptr, "The EOS closure kernel requires the quadrature "
"rule factory."
);
MFEM_VERIFY(
displacementTrue.Size() == f.displacementFes->GetTrueVSize(),
"The displacement vector has the wrong size."
displacementTrue.Size() == f.displacementFes->GetTrueVSize(), "The displacement vector has the wrong size."
);
MFEM_VERIFY(
domainMapper.GetDimension() == f.mesh->Dimension(),
"The domain-mapper dimension does not match "
domainMapper.GetDimension() == f.mesh->Dimension(), "The domain-mapper dimension does not match "
"the mesh dimension."
);
}
void apply_closure_action(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapperStateless &domainMapper,
const mean_field::physics::PolytropicBarotrope &barotrope,
const mean_field::mapping::DomainMapper &domainMapper,
const mean_field::eos::Polytrope &barotrope,
const ClosureAction closureAction,
const mfem::Vector *densityInputTrue,
const mfem::Vector *baseEnthalpyTrue,
@@ -183,29 +164,23 @@ namespace {
) {
validate_common_inputs(f, domainMapper, displacementTrue);
if (closureAction == ClosureAction::residual ||
closureAction == ClosureAction::density) {
if (closureAction == ClosureAction::residual || closureAction == ClosureAction::density) {
MFEM_VERIFY(
densityInputTrue != nullptr &&
densityInputTrue->Size() == f.densityFes->GetTrueVSize(),
densityInputTrue != nullptr && densityInputTrue->Size() == f.densityFes->GetTrueVSize(),
"The density input has the wrong size."
);
}
if (closureAction == ClosureAction::residual ||
closureAction == ClosureAction::enthalpy) {
if (closureAction == ClosureAction::residual || closureAction == ClosureAction::enthalpy) {
MFEM_VERIFY(
baseEnthalpyTrue != nullptr &&
baseEnthalpyTrue->Size() == f.enthalpyFes->GetTrueVSize(),
baseEnthalpyTrue != nullptr && baseEnthalpyTrue->Size() == f.enthalpyFes->GetTrueVSize(),
"The base enthalpy has the wrong size."
);
}
if (closureAction == ClosureAction::enthalpy) {
MFEM_VERIFY(
enthalpyVariationTrue != nullptr &&
enthalpyVariationTrue->Size() ==
f.enthalpyFes->GetTrueVSize(),
enthalpyVariationTrue != nullptr && enthalpyVariationTrue->Size() == f.enthalpyFes->GetTrueVSize(),
"The enthalpy variation has the wrong size."
);
}
@@ -224,9 +199,7 @@ namespace {
}
if (enthalpyVariationTrue != nullptr) {
true_to_local(
*f.enthalpyFes, *enthalpyVariationTrue, enthalpyVariationLocal
);
true_to_local(*f.enthalpyFes, *enthalpyVariationTrue, enthalpyVariationLocal);
}
true_to_local(*f.displacementFes, displacementTrue, displacementLocal);
@@ -234,9 +207,7 @@ namespace {
mfem::Vector localAction(f.densityFes->GetVSize());
localAction = 0.0;
mean_field::mapping::DomainMapperStateless::Workspace workspace(
f.mesh->Dimension()
);
mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
mfem::Array<int> densityDofs;
mfem::Array<int> enthalpyDofs;
@@ -253,115 +224,78 @@ namespace {
mfem::Vector densityShape;
mfem::Vector enthalpyShape;
const int vacuumAttribute = domainMapper.GetVacuumElementAttribute();
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation =
f.mesh->GetElementTransformation(elementId);
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(
transformation != nullptr,
"The EOS closure kernel received a null "
transformation != nullptr, "The EOS closure kernel received a null "
"element transformation."
);
if (transformation->Attribute == vacuumAttribute) {
if (!element_is_in_closure_support(transformation->Attribute)) {
continue;
}
const mfem::FiniteElement &densityElement =
*f.densityFes->GetFE(elementId);
const mfem::FiniteElement &enthalpyElement =
*f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement =
*f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement =
*f.compactificationFes->GetFE(elementId);
const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId);
const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId);
mfem::DofTransformation *densityDofTransformation =
f.densityFes->GetElementDofs(elementId, densityDofs);
mfem::DofTransformation *densityDofTransformation = f.densityFes->GetElementDofs(elementId, densityDofs);
mfem::DofTransformation *enthalpyDofTransformation =
f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *displacementDofTransformation =
f.displacementFes->GetElementVDofs(elementId, displacementDofs);
mfem::DofTransformation *compactificationDofTransformation =
f.compactificationFes->GetElementDofs(
elementId, compactificationDofs
);
f.compactificationFes->GetElementDofs(elementId, compactificationDofs);
if (densityInputTrue != nullptr) {
densityInputLocal.GetSubVector(
densityDofs, elementDensityInput
);
densityInputLocal.GetSubVector(densityDofs, elementDensityInput);
if (densityDofTransformation != nullptr) {
densityDofTransformation->InvTransformPrimal(
elementDensityInput
);
densityDofTransformation->InvTransformPrimal(elementDensityInput);
}
}
if (baseEnthalpyTrue != nullptr) {
baseEnthalpyLocal.GetSubVector(
enthalpyDofs, elementBaseEnthalpy
);
baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy);
if (enthalpyDofTransformation != nullptr) {
enthalpyDofTransformation->InvTransformPrimal(
elementBaseEnthalpy
);
enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy);
}
}
if (enthalpyVariationTrue != nullptr) {
enthalpyVariationLocal.GetSubVector(
enthalpyDofs, elementEnthalpyVariation
);
enthalpyVariationLocal.GetSubVector(enthalpyDofs, elementEnthalpyVariation);
if (enthalpyDofTransformation != nullptr) {
enthalpyDofTransformation->InvTransformPrimal(
elementEnthalpyVariation
);
enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation);
}
}
displacementLocal.GetSubVector(
displacementDofs, elementDisplacement
);
displacementLocal.GetSubVector(displacementDofs, elementDisplacement);
f.compactificationCoordinate->GetSubVector(
compactificationDofs, elementCompactification
);
f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification);
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->InvTransformPrimal(
elementDisplacement
);
displacementDofTransformation->InvTransformPrimal(elementDisplacement);
}
if (compactificationDofTransformation != nullptr) {
compactificationDofTransformation->InvTransformPrimal(
elementCompactification
);
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
}
const mean_field::mapping::ElementDisplacementData
displacementData = mean_field::mapping::
ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacement
);
const mean_field::mapping::ElementDisplacementData displacementData =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement);
const mean_field::mapping::ElementCompactificationData
compactificationData(
const mean_field::mapping::ElementCompactificationData compactificationData(
compactificationElement, elementCompactification
);
const mean_field::mapping::ElementMappingData mappingData{
.displacement = displacementData,
.compactification = compactificationData
.displacement = displacementData, .compactification = compactificationData
};
densityShape.SetSize(densityElement.GetDof());
@@ -369,34 +303,26 @@ namespace {
elementAction.SetSize(densityElement.GetDof());
elementAction = 0.0;
const mfem::IntegrationRule &integrationRule = get_eos_rule(
f, barotrope, densityElement, enthalpyElement, *transformation
);
const mfem::IntegrationRule &integrationRule =
get_eos_rule(f, barotrope, densityElement, enthalpyElement, *transformation);
for (int quadratureIndex = 0;
quadratureIndex < integrationRule.GetNPoints();
++quadratureIndex) {
const mfem::IntegrationPoint &integrationPoint =
integrationRule.IntPoint(quadratureIndex);
for (int quadratureIndex = 0; quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) {
const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadratureIndex);
transformation->SetIntPoint(&integrationPoint);
mean_field::mapping::VolumeMappingContext mappingContext;
const mean_field::mapping::MappingStatus mappingStatus =
domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint,
workspace, mappingContext
const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mean_field::mapping::MappingStatus::valid,
"Stateless mapping failed in the EOS "
"closure kernel. Element: "
<< elementId
<< ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex
<< ", status: " << static_cast<int>(mappingStatus)
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex << ", status: " << static_cast<int>(mappingStatus)
);
densityElement.CalcShape(integrationPoint, densityShape);
@@ -408,34 +334,35 @@ namespace {
} else {
enthalpyElement.CalcShape(integrationPoint, enthalpyShape);
const double baseEnthalpy =
elementBaseEnthalpy * enthalpyShape;
const double baseEnthalpy = elementBaseEnthalpy * enthalpyShape;
if (closureAction == ClosureAction::residual) {
const double density =
elementDensityInput * densityShape;
const double density = elementDensityInput * densityShape;
integrand =
density -
barotrope.density_from_enthalpy(baseEnthalpy);
const double equationOfStateDensity =
eos::evaluate<dimensions::quantity::Density>(
barotrope, dimensions::SpecificEnthalpyValue{baseEnthalpy}
)
.value();
integrand = density - equationOfStateDensity;
} else {
const double enthalpyVariation =
elementEnthalpyVariation * enthalpyShape;
const double enthalpyVariation = elementEnthalpyVariation * enthalpyShape;
integrand = -barotrope.density_derivative_from_enthalpy(
baseEnthalpy
) *
enthalpyVariation;
const double densityDerivative =
eos::partialDerivative<eos::quantity::Density, eos::quantity::SpecificEnthalpy>(
barotrope, dimensions::SpecificEnthalpyValue{baseEnthalpy}
)
.value();
integrand = -densityDerivative * enthalpyVariation;
}
}
const double weightedIntegrand =
mappingContext.quadrature.weight * integrand;
const double weightedIntegrand = mappingContext.quadrature.weight * integrand;
for (int densityDof = 0; densityDof < densityElement.GetDof();
++densityDof) {
elementAction(densityDof) +=
weightedIntegrand * densityShape(densityDof);
for (int densityDof = 0; densityDof < densityElement.GetDof(); ++densityDof) {
elementAction(densityDof) += weightedIntegrand * densityShape(densityDof);
}
}
@@ -453,52 +380,52 @@ namespace {
namespace mean_field::operators::kernels {
void apply_barotropic_closure(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const physics::PolytropicBarotrope &barotrope,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const mfem::Vector &densityTrue,
const mfem::Vector &enthalpyTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &residual
) {
apply_closure_action(
f, domainMapper, barotrope, ClosureAction::residual, &densityTrue,
&enthalpyTrue, nullptr, displacementTrue, residual
f, domainMapper, barotrope, ClosureAction::residual, &densityTrue, &enthalpyTrue, nullptr, displacementTrue,
residual
);
}
void apply_barotropic_closure_density_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const physics::PolytropicBarotrope &barotrope,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &action
) {
apply_closure_action(
f, domainMapper, barotrope, ClosureAction::density,
&densityVariationTrue, nullptr, nullptr, displacementTrue, action
f, domainMapper, barotrope, ClosureAction::density, &densityVariationTrue, nullptr, nullptr,
displacementTrue, action
);
}
void apply_barotropic_closure_enthalpy_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const physics::PolytropicBarotrope &barotrope,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector &enthalpyVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &action
) {
apply_closure_action(
f, domainMapper, barotrope, ClosureAction::enthalpy, nullptr,
&baseEnthalpyTrue, &enthalpyVariationTrue, displacementTrue, action
f, domainMapper, barotrope, ClosureAction::enthalpy, nullptr, &baseEnthalpyTrue, &enthalpyVariationTrue,
displacementTrue, action
);
}
void apply_barotropic_closure_displacement_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const physics::PolytropicBarotrope &barotrope,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const mfem::Vector &baseDensityTrue,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector &displacementTrue,
@@ -511,65 +438,54 @@ namespace mean_field::operators::kernels {
);
MFEM_VERIFY(
f.densityFes != nullptr,
"The barotropic-closure displacement action "
f.densityFes != nullptr, "The barotropic-closure displacement action "
"requires the density finite-element space."
);
MFEM_VERIFY(
f.enthalpyFes != nullptr,
"The barotropic-closure displacement action "
f.enthalpyFes != nullptr, "The barotropic-closure displacement action "
"requires the enthalpy finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"The barotropic-closure displacement action "
f.displacementFes != nullptr, "The barotropic-closure displacement action "
"requires the displacement finite-element space."
);
MFEM_VERIFY(
f.compactificationFes != nullptr,
"The barotropic-closure displacement action "
f.compactificationFes != nullptr, "The barotropic-closure displacement action "
"requires the compactification finite-element space."
);
MFEM_VERIFY(
f.compactificationCoordinate != nullptr,
"The barotropic-closure displacement action "
f.compactificationCoordinate != nullptr, "The barotropic-closure displacement action "
"requires the compactification coordinate."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr,
"The barotropic-closure displacement action "
f.quadratureFactory != nullptr, "The barotropic-closure displacement action "
"requires the quadrature-rule factory."
);
MFEM_VERIFY(
baseDensityTrue.Size() == f.densityFes->GetTrueVSize(),
"The base-density vector has the wrong size."
baseDensityTrue.Size() == f.densityFes->GetTrueVSize(), "The base-density vector has the wrong size."
);
MFEM_VERIFY(
baseEnthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(),
"The base-enthalpy vector has the wrong size."
baseEnthalpyTrue.Size() == f.enthalpyFes->GetTrueVSize(), "The base-enthalpy vector has the wrong size."
);
MFEM_VERIFY(
displacementTrue.Size() == f.displacementFes->GetTrueVSize(),
"The displacement vector has the wrong size."
displacementTrue.Size() == f.displacementFes->GetTrueVSize(), "The displacement vector has the wrong size."
);
MFEM_VERIFY(
displacementVariationTrue.Size() ==
f.displacementFes->GetTrueVSize(),
displacementVariationTrue.Size() == f.displacementFes->GetTrueVSize(),
"The displacement-variation vector has the wrong size."
);
MFEM_VERIFY(
domainMapper.GetDimension() == f.mesh->Dimension(),
"The domain-mapper dimension does not match the "
domainMapper.GetDimension() == f.mesh->Dimension(), "The domain-mapper dimension does not match the "
"mesh dimension."
);
@@ -584,17 +500,12 @@ namespace mean_field::operators::kernels {
true_to_local(*f.displacementFes, displacementTrue, displacementLocal);
true_to_local(
*f.displacementFes, displacementVariationTrue,
displacementVariationLocal
);
true_to_local(*f.displacementFes, displacementVariationTrue, displacementVariationLocal);
mfem::Vector localAction(f.densityFes->GetVSize());
localAction = 0.0;
mapping::DomainMapperStateless::Workspace workspace(
f.mesh->Dimension()
);
mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
mfem::Array<int> densityDofs;
mfem::Array<int> enthalpyDofs;
@@ -614,109 +525,76 @@ namespace mean_field::operators::kernels {
mapping::VolumeMappingContext mappingContext;
mapping::VolumeMappingVariation mappingVariation;
const int vacuumAttribute = domainMapper.GetVacuumElementAttribute();
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation =
f.mesh->GetElementTransformation(elementId);
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(
transformation != nullptr,
"The barotropic-closure displacement action "
transformation != nullptr, "The barotropic-closure displacement action "
"received a null element transformation."
);
if (transformation->Attribute == vacuumAttribute) {
if (!element_is_in_closure_support(transformation->Attribute)) {
continue;
}
const mfem::FiniteElement &densityElement =
*f.densityFes->GetFE(elementId);
const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId);
const mfem::FiniteElement &enthalpyElement =
*f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement =
*f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement =
*f.compactificationFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId);
mfem::DofTransformation *densityDofTransformation =
f.densityFes->GetElementDofs(elementId, densityDofs);
mfem::DofTransformation *densityDofTransformation = f.densityFes->GetElementDofs(elementId, densityDofs);
mfem::DofTransformation *enthalpyDofTransformation =
f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *displacementDofTransformation =
f.displacementFes->GetElementVDofs(elementId, displacementDofs);
mfem::DofTransformation *compactificationDofTransformation =
f.compactificationFes->GetElementDofs(
elementId, compactificationDofs
);
f.compactificationFes->GetElementDofs(elementId, compactificationDofs);
baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity);
baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy);
displacementLocal.GetSubVector(
displacementDofs, elementDisplacement
);
displacementLocal.GetSubVector(displacementDofs, elementDisplacement);
displacementVariationLocal.GetSubVector(
displacementDofs, elementDisplacementVariation
);
displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation);
f.compactificationCoordinate->GetSubVector(
compactificationDofs, elementCompactification
);
f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification);
if (densityDofTransformation != nullptr) {
densityDofTransformation->InvTransformPrimal(
elementBaseDensity
);
densityDofTransformation->InvTransformPrimal(elementBaseDensity);
}
if (enthalpyDofTransformation != nullptr) {
enthalpyDofTransformation->InvTransformPrimal(
elementBaseEnthalpy
);
enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy);
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->InvTransformPrimal(
elementDisplacement
);
displacementDofTransformation->InvTransformPrimal(elementDisplacement);
displacementDofTransformation->InvTransformPrimal(
elementDisplacementVariation
);
displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation);
}
if (compactificationDofTransformation != nullptr) {
compactificationDofTransformation->InvTransformPrimal(
elementCompactification
);
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
}
const mapping::ElementDisplacementData displacementData =
mapping::ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacement
);
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement);
const mapping::ElementDisplacementData displacementVariationData =
mapping::ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacementVariation
);
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacementVariation);
const mapping::ElementCompactificationData compactificationData(
compactificationElement, elementCompactification
);
const mapping::ElementMappingData mappingData{
.displacement = displacementData,
.compactification = compactificationData
.displacement = displacementData, .compactification = compactificationData
};
densityShape.SetSize(densityElement.GetDof());
@@ -726,22 +604,16 @@ namespace mean_field::operators::kernels {
elementAction.SetSize(densityElement.GetDof());
elementAction = 0.0;
const mfem::IntegrationRule &integrationRule = get_eos_rule(
f, barotrope, densityElement, enthalpyElement, *transformation
);
const mfem::IntegrationRule &integrationRule =
get_eos_rule(f, barotrope, densityElement, enthalpyElement, *transformation);
for (int quadraturePoint = 0;
quadraturePoint < integrationRule.GetNPoints();
++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint =
integrationRule.IntPoint(quadraturePoint);
for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadraturePoint);
transformation->SetIntPoint(&integrationPoint);
const mapping::MappingStatus mappingStatus =
domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint,
workspace, mappingContext
const mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
@@ -749,17 +621,13 @@ namespace mean_field::operators::kernels {
"The base mapping is invalid while applying "
"the barotropic-closure displacement action. "
"Element: "
<< elementId
<< ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadraturePoint
<< ", status: " << static_cast<int>(mappingStatus)
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadraturePoint << ", status: " << static_cast<int>(mappingStatus)
);
const mapping::MappingStatus variationStatus =
domainMapper.EvaluateVolumeVariation(
mappingData, displacementVariationData, *transformation,
integrationPoint, mappingContext, workspace,
mappingVariation
const mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation(
mappingData, displacementVariationData, *transformation, integrationPoint, mappingContext,
workspace, mappingVariation
);
MFEM_VERIFY(
@@ -767,10 +635,8 @@ namespace mean_field::operators::kernels {
"The mapping variation is invalid while "
"applying the barotropic-closure "
"displacement action. Element: "
<< elementId
<< ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadraturePoint
<< ", status: " << static_cast<int>(variationStatus)
<< elementId << ", attribute: " << transformation->Attribute << ", quadrature point: "
<< quadraturePoint << ", status: " << static_cast<int>(variationStatus)
);
densityElement.CalcShape(integrationPoint, densityShape);
@@ -779,19 +645,19 @@ namespace mean_field::operators::kernels {
const double densityValue = elementBaseDensity * densityShape;
const double enthalpyValue =
elementBaseEnthalpy * enthalpyShape;
const double enthalpyValue = elementBaseEnthalpy * enthalpyShape;
const double closureValue =
densityValue -
barotrope.density_from_enthalpy(enthalpyValue);
const double equationOfStateDensity = eos::evaluate<dimensions::quantity::Density>(
barotrope, dimensions::SpecificEnthalpyValue{enthalpyValue}
)
.value();
const double geometryActionValue =
closureValue * mappingVariation.weight_variation;
const double closureValue = densityValue - equationOfStateDensity;
const double geometryActionValue = closureValue * mappingVariation.weight_variation;
MFEM_VERIFY(
std::isfinite(closureValue) &&
std::isfinite(geometryActionValue),
std::isfinite(closureValue) && std::isfinite(geometryActionValue),
"The barotropic-closure displacement action "
"encountered a non-finite quadrature value."
);

View File

@@ -0,0 +1,870 @@
module;
#include <array>
#include <cmath>
#include <expected>
#include <optional>
#include <stdexcept>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
import :operators.kernels.gravity_displacement_force;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] bool is_vacuum_attribute(const int attribute) {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
enum class GravityDisplacementForceAction { residual, density, gravityGradient, displacement, complete };
using Rejection = mean_field::operators::kernels::GravityDisplacementForceRejection;
using Reason = mean_field::operators::kernels::GravityDisplacementForceRejectionReason;
using Result = mean_field::operators::kernels::GravityDisplacementForceResult;
[[nodiscard]] Rejection mapping_rejection(const mean_field::mapping::MappingStatus status) {
MFEM_VERIFY(
status != mean_field::mapping::MappingStatus::invalid_dimension,
"The gravity-displacement-force mapping reported an invariant dimension mismatch."
);
return {.reason = Reason::invalid_mapping, .mappingStatus = status};
}
[[nodiscard]] Rejection non_finite_rejection() noexcept {
return {.reason = Reason::non_finite_arithmetic};
}
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) noexcept {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
[[nodiscard]] int encode_rejection(const std::optional<Rejection> &rejection) noexcept {
if (!rejection.has_value()) {
return 0;
}
if (rejection->reason == Reason::non_finite_arithmetic) {
return 256;
}
return static_cast<int>(rejection->mappingStatus) + 1;
}
[[nodiscard]] Rejection decode_rejection(const int encoded) noexcept {
if (encoded >= 256) {
return non_finite_rejection();
}
return mapping_rejection(static_cast<mean_field::mapping::MappingStatus>(encoded - 1));
}
[[nodiscard]] Result synchronize_rejection(
const std::optional<Rejection> &localRejection,
const MPI_Comm communicator
) {
const int localEncoded = encode_rejection(localRejection);
int globalEncoded = 0;
if (MPI_Allreduce(&localEncoded, &globalEncoded, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("Could not synchronize gravity-displacement-force candidate validity.");
}
if (globalEncoded != 0) {
return std::unexpected(decode_rejection(globalEncoded));
}
return {};
}
[[noreturn]] void throw_rejection(const Rejection &rejection) {
if (rejection.reason == Reason::non_finite_arithmetic) {
throw std::domain_error("The gravity-displacement force produced non-finite arithmetic.");
}
throw std::domain_error("The gravity-displacement force encountered an invalid mapped domain.");
}
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
mfem::Vector &localVector
) {
MFEM_VERIFY(
trueVector.Size() == finiteElementSpace.GetTrueVSize(),
"The gravity-displacement-force true vector has the wrong size."
);
localVector.SetSize(finiteElementSpace.GetVSize());
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(trueVector, localVector);
} else {
localVector = trueVector;
}
}
void local_to_true(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &localVector,
mfem::Vector &trueVector
) {
MFEM_VERIFY(
localVector.Size() == finiteElementSpace.GetVSize(),
"The gravity-displacement-force local vector has the wrong size."
);
trueVector.SetSize(finiteElementSpace.GetTrueVSize());
trueVector = 0.0;
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(localVector, trueVector);
} else {
trueVector = localVector;
}
}
[[nodiscard]] int vector_dof_index(
const mfem::Ordering::Type ordering,
const int scalarDof,
const int component,
const int scalarDofCount,
const int dimension
) {
if (ordering == mfem::Ordering::byNODES) {
return scalarDof + component * scalarDofCount;
}
if (ordering == mfem::Ordering::byVDIM) {
return scalarDof * dimension + component;
}
MFEM_ABORT(
"The gravity-displacement-force test space uses an unsupported "
"ordering."
);
return -1;
}
[[nodiscard]] const mfem::IntegrationRule &get_gravity_force_rule(
const mean_field::fem::FEM &f,
const mfem::FiniteElement &densityElement,
const mfem::FiniteElement &gravityGradientElement,
const mfem::FiniteElement &displacementElement,
const mfem::ElementTransformation &transformation
) {
using DisplacementField = mean_field::field::Field<mean_field::field::Displacement>;
MFEM_VERIFY(
densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder,
"The gravity-displacement-force density element does not match "
"the registered density field."
);
MFEM_VERIFY(
gravityGradientElement.GetOrder() == mean_field::field::Gravity::Flux::familyOrder + 1,
"The gravity-displacement-force RT element does not match the "
"registered gravity-gradient field."
);
MFEM_VERIFY(
displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder,
"The gravity-displacement-force test element does not match the "
"registered displacement field."
);
const mean_field::quadrature::Query query =
DisplacementField::make_query<mean_field::field::Displacement::Form::GravityForce>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
rule.integration_rule != nullptr, "The quadrature policy did not return a gravity-displacement-"
"force integration rule."
);
return *rule.integration_rule;
}
void validate_finite_vector(
const mfem::Vector &vector,
const char *message
) {
for (int index = 0; index < vector.Size(); ++index) {
MFEM_VERIFY(std::isfinite(vector(index)), message);
}
}
void validate_common_inputs(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &domainMapper,
const mfem::Vector &displacementTrue
) {
MFEM_VERIFY(f.mesh != nullptr, "The gravity-displacement-force kernel requires a mesh.");
MFEM_VERIFY(
f.densityFes != nullptr, "The gravity-displacement-force kernel requires the density "
"finite-element space."
);
MFEM_VERIFY(
f.gravityFluxFes != nullptr, "The gravity-displacement-force kernel requires the gravity-"
"gradient finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr, "The gravity-displacement-force kernel requires the displacement "
"finite-element space."
);
MFEM_VERIFY(
f.compactificationFes != nullptr && f.compactificationCoordinate != nullptr,
"The gravity-displacement-force kernel requires the "
"compactification coordinate."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr, "The gravity-displacement-force kernel requires the quadrature "
"rule factory."
);
MFEM_VERIFY(
displacementTrue.Size() == f.displacementFes->GetTrueVSize(),
"The gravity-displacement-force displacement vector has the "
"wrong size."
);
MFEM_VERIFY(
domainMapper.GetDimension() == f.mesh->Dimension(),
"The gravity-displacement-force mapper dimension does not match "
"the mesh dimension."
);
MFEM_VERIFY(
f.displacementFes->GetVDim() == f.mesh->Dimension(),
"The gravity-displacement-force displacement dimension does not "
"match the mesh dimension."
);
}
void validate_density(
const mean_field::fem::FEM &f,
const mfem::Vector &density,
const char *message
) {
MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message);
}
void validate_gravity_gradient(
const mean_field::fem::FEM &f,
const mfem::Vector &gravityGradient,
const char *message
) {
MFEM_VERIFY(gravityGradient.Size() == f.gravityFluxFes->GetTrueVSize(), message);
}
[[nodiscard]] Result apply_gravity_displacement_force_action(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &domainMapper,
const GravityDisplacementForceAction requestedAction,
const mfem::Vector *baseDensityTrue,
const mfem::Vector *densityVariationTrue,
const mfem::Vector *baseGravityGradientTrue,
const mfem::Vector *gravityGradientVariationTrue,
const mfem::Vector *displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue,
const bool reportCandidateRejection
) {
validate_common_inputs(f, domainMapper, displacementTrue);
const bool needsBaseDensity = requestedAction == GravityDisplacementForceAction::residual ||
requestedAction == GravityDisplacementForceAction::gravityGradient ||
requestedAction == GravityDisplacementForceAction::displacement ||
requestedAction == GravityDisplacementForceAction::complete;
const bool needsDensityVariation = requestedAction == GravityDisplacementForceAction::density ||
requestedAction == GravityDisplacementForceAction::complete;
const bool needsBaseGravityGradient = requestedAction == GravityDisplacementForceAction::residual ||
requestedAction == GravityDisplacementForceAction::density ||
requestedAction == GravityDisplacementForceAction::displacement ||
requestedAction == GravityDisplacementForceAction::complete;
const bool needsGravityGradientVariation = requestedAction == GravityDisplacementForceAction::gravityGradient ||
requestedAction == GravityDisplacementForceAction::complete;
const bool needsDisplacementVariation = requestedAction == GravityDisplacementForceAction::displacement ||
requestedAction == GravityDisplacementForceAction::complete;
if (needsBaseDensity) {
MFEM_VERIFY(
baseDensityTrue != nullptr, "The gravity-displacement-force action requires a base "
"density."
);
validate_density(f, *baseDensityTrue, "The gravity-displacement-force base density is invalid.");
}
if (needsDensityVariation) {
MFEM_VERIFY(
densityVariationTrue != nullptr, "The gravity-displacement-force action requires a density "
"variation."
);
validate_density(
f, *densityVariationTrue,
"The gravity-displacement-force density variation is "
"invalid."
);
}
if (needsBaseGravityGradient) {
MFEM_VERIFY(
baseGravityGradientTrue != nullptr, "The gravity-displacement-force action requires a base "
"gravity gradient."
);
validate_gravity_gradient(
f, *baseGravityGradientTrue,
"The gravity-displacement-force base gravity gradient is "
"invalid."
);
}
if (needsGravityGradientVariation) {
MFEM_VERIFY(
gravityGradientVariationTrue != nullptr, "The gravity-displacement-force action requires a gravity-"
"gradient variation."
);
validate_gravity_gradient(
f, *gravityGradientVariationTrue,
"The gravity-displacement-force gravity-gradient variation "
"is invalid."
);
}
if (needsDisplacementVariation) {
MFEM_VERIFY(
displacementVariationTrue != nullptr &&
displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(),
"The gravity-displacement-force displacement variation is "
"invalid."
);
validate_finite_vector(
*displacementVariationTrue, "The gravity-displacement-force displacement variation "
"contains a non-finite value."
);
}
bool inputsAreFinite = vector_is_finite(displacementTrue);
if (needsBaseDensity) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*baseDensityTrue);
}
if (needsDensityVariation) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*densityVariationTrue);
}
if (needsBaseGravityGradient) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*baseGravityGradientTrue);
}
if (needsGravityGradientVariation) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*gravityGradientVariationTrue);
}
if (needsDisplacementVariation) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*displacementVariationTrue);
}
if (!reportCandidateRejection) {
MFEM_VERIFY(inputsAreFinite, "The gravity-displacement-force action contains non-finite input data.");
} else {
const std::optional<Rejection> inputRejection =
inputsAreFinite ? std::optional<Rejection>{} : std::optional<Rejection>{non_finite_rejection()};
auto synchronized = synchronize_rejection(inputRejection, f.mesh->GetComm());
if (!synchronized.has_value()) {
return synchronized;
}
}
mfem::Vector baseDensityLocal;
mfem::Vector densityVariationLocal;
mfem::Vector baseGravityGradientLocal;
mfem::Vector gravityGradientVariationLocal;
mfem::Vector displacementLocal;
mfem::Vector displacementVariationLocal;
if (needsBaseDensity) {
true_to_local(*f.densityFes, *baseDensityTrue, baseDensityLocal);
}
if (needsDensityVariation) {
true_to_local(*f.densityFes, *densityVariationTrue, densityVariationLocal);
}
if (needsBaseGravityGradient) {
true_to_local(*f.gravityFluxFes, *baseGravityGradientTrue, baseGravityGradientLocal);
}
if (needsGravityGradientVariation) {
true_to_local(*f.gravityFluxFes, *gravityGradientVariationTrue, gravityGradientVariationLocal);
}
true_to_local(*f.displacementFes, displacementTrue, displacementLocal);
if (needsDisplacementVariation) {
true_to_local(*f.displacementFes, *displacementVariationTrue, displacementVariationLocal);
}
mfem::Vector localAction(f.displacementFes->GetVSize());
localAction = 0.0;
mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
mfem::Array<int> densityDofs;
mfem::Array<int> gravityGradientDofs;
mfem::Array<int> displacementDofs;
mfem::Array<int> compactificationDofs;
mfem::Vector elementBaseDensity;
mfem::Vector elementDensityVariation;
mfem::Vector elementBaseGravityGradient;
mfem::Vector elementGravityGradientVariation;
mfem::Vector elementDisplacement;
mfem::Vector elementDisplacementVariation;
mfem::Vector elementCompactification;
mfem::Vector elementAction;
mfem::Vector densityShape;
mfem::Vector displacementShape;
mfem::DenseMatrix gravityGradientShape;
mfem::Vector baseGravityReferenceValue;
mfem::Vector gravityVariationReferenceValue;
mfem::Vector mappedBaseGravity;
mfem::Vector mappedGravityVariation;
mfem::Vector mappedGeometryVariation;
mfem::Vector forceValue;
mean_field::mapping::VolumeMappingContext mappingContext;
mean_field::mapping::VolumeMappingVariation mappingVariation;
const int dimension = f.mesh->Dimension();
const mfem::Ordering::Type displacementOrdering = f.displacementFes->GetOrdering();
std::optional<Rejection> candidateRejection;
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(
transformation != nullptr, "The gravity-displacement-force kernel received a null "
"element transformation."
);
if (is_vacuum_attribute(transformation->Attribute)) {
continue;
}
const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId);
const mfem::FiniteElement &gravityGradientElement = *f.gravityFluxFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId);
mfem::DofTransformation *densityDofTransformation = f.densityFes->GetElementDofs(elementId, densityDofs);
mfem::DofTransformation *gravityGradientDofTransformation =
f.gravityFluxFes->GetElementVDofs(elementId, gravityGradientDofs);
mfem::DofTransformation *displacementDofTransformation =
f.displacementFes->GetElementVDofs(elementId, displacementDofs);
mfem::DofTransformation *compactificationDofTransformation =
f.compactificationFes->GetElementDofs(elementId, compactificationDofs);
if (needsBaseDensity) {
baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity);
}
if (needsDensityVariation) {
densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation);
}
if (needsBaseGravityGradient) {
baseGravityGradientLocal.GetSubVector(gravityGradientDofs, elementBaseGravityGradient);
}
if (needsGravityGradientVariation) {
gravityGradientVariationLocal.GetSubVector(gravityGradientDofs, elementGravityGradientVariation);
}
displacementLocal.GetSubVector(displacementDofs, elementDisplacement);
if (needsDisplacementVariation) {
displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation);
}
f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification);
if (densityDofTransformation != nullptr) {
if (needsBaseDensity) {
densityDofTransformation->InvTransformPrimal(elementBaseDensity);
}
if (needsDensityVariation) {
densityDofTransformation->InvTransformPrimal(elementDensityVariation);
}
}
if (gravityGradientDofTransformation != nullptr) {
if (needsBaseGravityGradient) {
gravityGradientDofTransformation->InvTransformPrimal(elementBaseGravityGradient);
}
if (needsGravityGradientVariation) {
gravityGradientDofTransformation->InvTransformPrimal(elementGravityGradientVariation);
}
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->InvTransformPrimal(elementDisplacement);
if (needsDisplacementVariation) {
displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation);
}
}
if (compactificationDofTransformation != nullptr) {
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
}
const mean_field::mapping::ElementDisplacementData displacementData =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement);
const mean_field::mapping::ElementCompactificationData compactificationData(
compactificationElement, elementCompactification
);
const mean_field::mapping::ElementMappingData mappingData{
.displacement = displacementData, .compactification = compactificationData
};
std::optional<mean_field::mapping::ElementDisplacementData> displacementVariationData;
if (needsDisplacementVariation) {
displacementVariationData.emplace(
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacementVariation
)
);
}
const int scalarDisplacementDofCount = displacementElement.GetDof();
MFEM_VERIFY(
displacementDofs.Size() == scalarDisplacementDofCount * dimension,
"The gravity-displacement-force element displacement vector "
"has the wrong size."
);
densityShape.SetSize(densityElement.GetDof());
displacementShape.SetSize(scalarDisplacementDofCount);
gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension);
baseGravityReferenceValue.SetSize(dimension);
gravityVariationReferenceValue.SetSize(dimension);
mappedBaseGravity.SetSize(dimension);
mappedGravityVariation.SetSize(dimension);
mappedGeometryVariation.SetSize(dimension);
forceValue.SetSize(dimension);
elementAction.SetSize(displacementDofs.Size());
elementAction = 0.0;
const mfem::IntegrationRule &integrationRule =
get_gravity_force_rule(f, densityElement, gravityGradientElement, displacementElement, *transformation);
for (int quadratureIndex = 0; quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) {
const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadratureIndex);
transformation->SetIntPoint(&integrationPoint);
const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
if (mappingStatus != mean_field::mapping::MappingStatus::valid) {
if (!reportCandidateRejection) {
MFEM_VERIFY(
false, "Stateless mapping failed in the gravity-displacement-"
"force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex
<< ", status: " << static_cast<int>(mappingStatus)
);
}
candidateRejection = mapping_rejection(mappingStatus);
continue;
}
if (needsDisplacementVariation) {
const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation(
mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext,
workspace, mappingVariation
);
if (variationStatus != mean_field::mapping::MappingStatus::valid) {
if (!reportCandidateRejection) {
MFEM_VERIFY(
false, "Stateless mapping variation failed in the gravity-"
"displacement-force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex
<< ", status: " << static_cast<int>(variationStatus)
);
}
candidateRejection = mapping_rejection(variationStatus);
continue;
}
}
densityElement.CalcShape(integrationPoint, densityShape);
displacementElement.CalcShape(integrationPoint, displacementShape);
gravityGradientElement.CalcVShape(*transformation, gravityGradientShape);
double baseDensityValue = 0.0;
double densityVariationValue = 0.0;
if (needsBaseDensity) {
baseDensityValue = elementBaseDensity * densityShape;
}
if (needsDensityVariation) {
densityVariationValue = elementDensityVariation * densityShape;
}
if (needsBaseGravityGradient) {
gravityGradientShape.MultTranspose(elementBaseGravityGradient, baseGravityReferenceValue);
mappingContext.mapping.mapping_jacobian.Mult(baseGravityReferenceValue, mappedBaseGravity);
} else {
mappedBaseGravity = 0.0;
}
if (needsGravityGradientVariation) {
gravityGradientShape.MultTranspose(elementGravityGradientVariation, gravityVariationReferenceValue);
mappingContext.mapping.mapping_jacobian.Mult(
gravityVariationReferenceValue, mappedGravityVariation
);
} else {
mappedGravityVariation = 0.0;
}
if (needsDisplacementVariation) {
mappingVariation.mapping.mapping_jacobian_variation.Mult(
baseGravityReferenceValue, mappedGeometryVariation
);
} else {
mappedGeometryVariation = 0.0;
}
forceValue = 0.0;
if (requestedAction == GravityDisplacementForceAction::residual) {
forceValue.Add(baseDensityValue, mappedBaseGravity);
} else {
if (needsDensityVariation) {
forceValue.Add(densityVariationValue, mappedBaseGravity);
}
if (needsGravityGradientVariation) {
forceValue.Add(baseDensityValue, mappedGravityVariation);
}
if (needsDisplacementVariation) {
forceValue.Add(baseDensityValue, mappedGeometryVariation);
}
}
/*
* If g_ref is the RT pullback, then
*
* g_phys = J_map g_ref / det(J_map),
* dV_phys = det(J_map) dV_ref.
*
* The determinant cancels exactly. Consequently the base
* integrand uses J_map g_ref and its geometry derivative uses
* delta(J_map) g_ref. This is algebraically identical to
* differentiating the Piola map and physical volume weight,
* but avoids a numerically pointless cancellation.
*/
const double referenceWeight = integrationPoint.weight * transformation->Weight();
forceValue *= referenceWeight;
for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) {
for (int component = 0; component < dimension; ++component) {
const int vectorDof = vector_dof_index(
displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension
);
const double contribution = displacementShape(scalarDof) * forceValue(component);
if (!std::isfinite(contribution)) {
if (!reportCandidateRejection) {
MFEM_VERIFY(
false, "The gravity-displacement-force kernel "
"encountered a non-finite contribution."
);
}
candidateRejection = non_finite_rejection();
continue;
}
elementAction(vectorDof) += contribution;
}
}
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->TransformDual(elementAction);
}
localAction.AddElementVector(displacementDofs, elementAction);
}
if (reportCandidateRejection && !vector_is_finite(localAction)) {
candidateRejection = non_finite_rejection();
}
if (reportCandidateRejection) {
auto synchronized = synchronize_rejection(candidateRejection, f.mesh->GetComm());
if (!synchronized.has_value()) {
return synchronized;
}
}
local_to_true(*f.displacementFes, localAction, actionTrue);
if (reportCandidateRejection) {
const std::optional<Rejection> outputRejection = vector_is_finite(actionTrue)
? std::optional<Rejection>{}
: std::optional<Rejection>{non_finite_rejection()};
auto synchronized = synchronize_rejection(outputRejection, f.mesh->GetComm());
if (!synchronized.has_value()) {
return synchronized;
}
}
return {};
}
} // namespace
namespace mean_field::operators::kernels {
GravityDisplacementForceResult try_apply_gravity_displacement_force_residual(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &densityTrue,
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
) {
return apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::residual, &densityTrue, nullptr, &gravityGradientTrue,
nullptr, nullptr, displacementTrue, residualTrue, true
);
}
void apply_gravity_displacement_force_residual(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &densityTrue,
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
) {
auto result = try_apply_gravity_displacement_force_residual(
f, domainMapper, densityTrue, gravityGradientTrue, displacementTrue, residualTrue
);
if (!result.has_value()) {
throw_rejection(result.error());
}
}
void apply_gravity_displacement_force_density_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &densityVariationTrue,
const mfem::Vector &baseGravityGradientTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::density, nullptr, &densityVariationTrue,
&baseGravityGradientTrue, nullptr, nullptr, displacementTrue, actionTrue, false
);
}
void apply_gravity_displacement_force_gradient_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &baseDensityTrue,
const mfem::Vector &gravityGradientVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::gravityGradient, &baseDensityTrue, nullptr, nullptr,
&gravityGradientVariationTrue, nullptr, displacementTrue, actionTrue, false
);
}
void apply_gravity_displacement_force_displacement_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &baseDensityTrue,
const mfem::Vector &baseGravityGradientTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::displacement, &baseDensityTrue, nullptr,
&baseGravityGradientTrue, nullptr, &displacementVariationTrue, displacementTrue, actionTrue, false
);
}
void apply_gravity_displacement_force_complete_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &baseDensityTrue,
const mfem::Vector &densityVariationTrue,
const mfem::Vector &baseGravityGradientTrue,
const mfem::Vector &gravityGradientVariationTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::complete, &baseDensityTrue, &densityVariationTrue,
&baseGravityGradientTrue, &gravityGradientVariationTrue, &displacementVariationTrue, displacementTrue,
actionTrue, false
);
}
} // namespace mean_field::operators::kernels

File diff suppressed because it is too large Load Diff

View File

@@ -11,20 +11,22 @@ module mean_field;
import :operators.kernels.hydrostatic_equilibrium;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] bool is_vacuum_attribute(const int attribute) {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
mfem::Vector &localVector
) {
MFEM_VERIFY(
trueVector.Size() == finiteElementSpace.GetTrueVSize(),
"True vector has the wrong size."
);
MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), "True vector has the wrong size.");
localVector.SetSize(finiteElementSpace.GetVSize());
const mfem::Operator *prolongation =
finiteElementSpace.GetProlongationMatrix();
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(trueVector, localVector);
@@ -38,17 +40,13 @@ namespace {
const mfem::Vector &localVector,
mfem::Vector &trueVector
) {
MFEM_VERIFY(
localVector.Size() == finiteElementSpace.GetVSize(),
"Local vector has the wrong size."
);
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();
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(localVector, trueVector);
@@ -59,11 +57,9 @@ namespace {
void validate_fem(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapperStateless &domainMapper
const mean_field::mapping::DomainMapper &domainMapper
) {
MFEM_VERIFY(
f.mesh != nullptr, "The hydrostatic kernel requires a mesh."
);
MFEM_VERIFY(f.mesh != nullptr, "The hydrostatic kernel requires a mesh.");
MFEM_VERIFY(
f.enthalpyFes != nullptr, "The hydrostatic kernel requires the "
@@ -71,8 +67,7 @@ namespace {
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"The hydrostatic kernel requires the "
f.gravityPotentialFes != nullptr, "The hydrostatic kernel requires the "
"gravity-potential finite-element space."
);
@@ -82,32 +77,27 @@ namespace {
);
MFEM_VERIFY(
f.compactificationFes != nullptr,
"The hydrostatic kernel requires the "
f.compactificationFes != nullptr, "The hydrostatic kernel requires the "
"compactification finite-element space."
);
MFEM_VERIFY(
f.compactificationCoordinate != nullptr,
"The hydrostatic kernel requires the "
f.compactificationCoordinate != nullptr, "The hydrostatic kernel requires the "
"compactification coordinate."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr,
"The hydrostatic kernel requires the "
f.quadratureFactory != nullptr, "The hydrostatic kernel requires the "
"quadrature-rule factory."
);
MFEM_VERIFY(
f.mesh->Dimension() == 3,
"The rigid-rotation hydrostatic kernel "
f.mesh->Dimension() == 3, "The rigid-rotation hydrostatic kernel "
"currently requires a three-dimensional mesh."
);
MFEM_VERIFY(
domainMapper.GetDimension() == f.mesh->Dimension(),
"The domain-mapper dimension does not match "
domainMapper.GetDimension() == f.mesh->Dimension(), "The domain-mapper dimension does not match "
"the mesh dimension."
);
}
@@ -118,69 +108,51 @@ namespace {
const mfem::FiniteElement &potentialElement,
const mfem::ElementTransformation &transformation
) {
using EnthalpyField =
mean_field::field::Field<mean_field::field::Enthalpy>;
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
MFEM_VERIFY(
enthalpyElement.GetOrder() ==
mean_field::field::Enthalpy::Scalar::familyOrder,
enthalpyElement.GetOrder() == mean_field::field::Enthalpy::Scalar::familyOrder,
"The hydrostatic test element does not match "
"the registered enthalpy field."
);
MFEM_VERIFY(
potentialElement.GetOrder() ==
mean_field::field::Gravity::Potential::familyOrder,
potentialElement.GetOrder() == mean_field::field::Gravity::Potential::familyOrder,
"The hydrostatic potential element does not "
"match the registered gravity-potential field."
);
const auto enthalpyQuery = EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::EquilibriumEnthalpy>(
mean_field::quadrature::QuadratureRole::discretization,
transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
const auto enthalpyQuery = EnthalpyField::make_query<mean_field::field::Enthalpy::Form::EquilibriumEnthalpy>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
const auto gravityQuery = EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::EquilibriumGravity>(
mean_field::quadrature::QuadratureRole::discretization,
transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
const auto gravityQuery = EnthalpyField::make_query<mean_field::field::Enthalpy::Form::EquilibriumGravity>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
const auto rotationQuery = EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::EquilibriumRotation>(
mean_field::quadrature::QuadratureRole::discretization,
transformation.OrderW(), std::array<int, 1>{2},
mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
const auto rotationQuery = EnthalpyField::make_query<mean_field::field::Enthalpy::Form::EquilibriumRotation>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array<int, 1>{2},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
const auto constantQuery = EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::EquilibriumConstant>(
mean_field::quadrature::QuadratureRole::discretization,
transformation.OrderW(), {}, mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
const auto constantQuery = EnthalpyField::make_query<mean_field::field::Enthalpy::Form::EquilibriumConstant>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
int integrationOrder = 0;
const auto update_order = [&f, &transformation, &integrationOrder](
const mean_field::quadrature::Query &query
) {
const auto rule = f.quadratureFactory->get(
query, transformation.GetGeometryType()
);
const auto update_order = [&f, &transformation, &integrationOrder](const mean_field::quadrature::Query &query) {
const auto rule = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
rule.integration_rule != nullptr,
"The quadrature policy did not return "
rule.integration_rule != nullptr, "The quadrature policy did not return "
"a hydrostatic-equilibrium rule."
);
integrationOrder =
std::max(integrationOrder, rule.resolution.order);
integrationOrder = std::max(integrationOrder, rule.resolution.order);
};
update_order(enthalpyQuery);
@@ -188,9 +160,7 @@ namespace {
update_order(rotationQuery);
update_order(constantQuery);
return mfem::IntRules.Get(
transformation.GetGeometryType(), integrationOrder
);
return mfem::IntRules.Get(transformation.GetGeometryType(), integrationOrder);
}
struct HydrostaticAssemblyRequest {
@@ -211,7 +181,7 @@ namespace {
void assemble_hydrostatic_form(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapperStateless &domainMapper,
const mean_field::mapping::DomainMapper &domainMapper,
const mfem::Vector &displacementTrue,
const HydrostaticAssemblyRequest &request,
mfem::Vector &result
@@ -219,81 +189,64 @@ namespace {
validate_fem(f, domainMapper);
MFEM_VERIFY(
displacementTrue.Size() == f.displacementFes->GetTrueVSize(),
"The hydrostatic displacement vector has "
displacementTrue.Size() == f.displacementFes->GetTrueVSize(), "The hydrostatic displacement vector has "
"the wrong size."
);
MFEM_VERIFY(
std::isfinite(request.bernoulliConstant),
"The Bernoulli constant is non-finite."
);
MFEM_VERIFY(std::isfinite(request.bernoulliConstant), "The Bernoulli constant is non-finite.");
MFEM_VERIFY(
std::isfinite(request.constantVariation),
"The Bernoulli-constant variation is non-finite."
);
MFEM_VERIFY(std::isfinite(request.constantVariation), "The Bernoulli-constant variation is non-finite.");
const bool requiresBaseState =
request.buildResidual ||
request.displacementVariationTrue != nullptr;
const bool requiresBaseState = request.buildResidual || request.displacementVariationTrue != nullptr;
if (requiresBaseState) {
MFEM_VERIFY(
request.rotation != nullptr,
"The hydrostatic residual or geometry "
request.rotation != nullptr, "The hydrostatic residual or geometry "
"action requires the rotation model."
);
MFEM_VERIFY(
request.baseEnthalpyTrue != nullptr,
"The hydrostatic residual or geometry "
request.baseEnthalpyTrue != nullptr, "The hydrostatic residual or geometry "
"action requires the base enthalpy."
);
MFEM_VERIFY(
request.basePotentialTrue != nullptr,
"The hydrostatic residual or geometry "
request.basePotentialTrue != nullptr, "The hydrostatic residual or geometry "
"action requires the base potential."
);
}
if (request.baseEnthalpyTrue != nullptr) {
MFEM_VERIFY(
request.baseEnthalpyTrue->Size() ==
f.enthalpyFes->GetTrueVSize(),
request.baseEnthalpyTrue->Size() == f.enthalpyFes->GetTrueVSize(),
"The base enthalpy vector has the wrong size."
);
}
if (request.basePotentialTrue != nullptr) {
MFEM_VERIFY(
request.basePotentialTrue->Size() ==
f.gravityPotentialFes->GetTrueVSize(),
request.basePotentialTrue->Size() == f.gravityPotentialFes->GetTrueVSize(),
"The base potential vector has the wrong size."
);
}
if (request.enthalpyVariationTrue != nullptr) {
MFEM_VERIFY(
request.enthalpyVariationTrue->Size() ==
f.enthalpyFes->GetTrueVSize(),
request.enthalpyVariationTrue->Size() == f.enthalpyFes->GetTrueVSize(),
"The enthalpy variation has the wrong size."
);
}
if (request.potentialVariationTrue != nullptr) {
MFEM_VERIFY(
request.potentialVariationTrue->Size() ==
f.gravityPotentialFes->GetTrueVSize(),
request.potentialVariationTrue->Size() == f.gravityPotentialFes->GetTrueVSize(),
"The potential variation has the wrong size."
);
}
if (request.displacementVariationTrue != nullptr) {
MFEM_VERIFY(
request.displacementVariationTrue->Size() ==
f.displacementFes->GetTrueVSize(),
request.displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(),
"The displacement variation has the wrong size."
);
}
@@ -308,46 +261,30 @@ namespace {
mfem::Vector displacementVariationLocal;
if (request.baseEnthalpyTrue != nullptr) {
true_to_local(
*f.enthalpyFes, *request.baseEnthalpyTrue, baseEnthalpyLocal
);
true_to_local(*f.enthalpyFes, *request.baseEnthalpyTrue, baseEnthalpyLocal);
}
if (request.basePotentialTrue != nullptr) {
true_to_local(
*f.gravityPotentialFes, *request.basePotentialTrue,
basePotentialLocal
);
true_to_local(*f.gravityPotentialFes, *request.basePotentialTrue, basePotentialLocal);
}
if (request.enthalpyVariationTrue != nullptr) {
true_to_local(
*f.enthalpyFes, *request.enthalpyVariationTrue,
enthalpyVariationLocal
);
true_to_local(*f.enthalpyFes, *request.enthalpyVariationTrue, enthalpyVariationLocal);
}
if (request.potentialVariationTrue != nullptr) {
true_to_local(
*f.gravityPotentialFes, *request.potentialVariationTrue,
potentialVariationLocal
);
true_to_local(*f.gravityPotentialFes, *request.potentialVariationTrue, potentialVariationLocal);
}
if (request.displacementVariationTrue != nullptr) {
true_to_local(
*f.displacementFes, *request.displacementVariationTrue,
displacementVariationLocal
);
true_to_local(*f.displacementFes, *request.displacementVariationTrue, displacementVariationLocal);
}
mfem::Vector localResult(f.enthalpyFes->GetVSize());
localResult = 0.0;
mean_field::mapping::DomainMapperStateless::Workspace workspace(
f.mesh->Dimension()
);
mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
mfem::Array<int> enthalpyDofs;
mfem::Array<int> potentialDofs;
@@ -366,36 +303,27 @@ namespace {
mfem::Vector enthalpyShape;
mfem::Vector potentialShape;
const int vacuumAttribute = domainMapper.GetVacuumElementAttribute();
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation =
f.mesh->GetElementTransformation(elementId);
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(
transformation != nullptr,
"The hydrostatic kernel received a null "
transformation != nullptr, "The hydrostatic kernel received a null "
"element transformation."
);
if (transformation->Attribute == vacuumAttribute) {
if (is_vacuum_attribute(transformation->Attribute)) {
continue;
}
const mfem::FiniteElement &enthalpyElement =
*f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &potentialElement =
*f.gravityPotentialFes->GetFE(elementId);
const mfem::FiniteElement &potentialElement = *f.gravityPotentialFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement =
*f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement =
*f.compactificationFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId);
mfem::DofTransformation *enthalpyDofTransformation =
f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *potentialDofTransformation =
f.gravityPotentialFes->GetElementDofs(elementId, potentialDofs);
@@ -404,117 +332,80 @@ namespace {
f.displacementFes->GetElementVDofs(elementId, displacementDofs);
mfem::DofTransformation *compactificationDofTransformation =
f.compactificationFes->GetElementDofs(
elementId, compactificationDofs
);
f.compactificationFes->GetElementDofs(elementId, compactificationDofs);
displacementLocal.GetSubVector(
displacementDofs, elementDisplacement
);
displacementLocal.GetSubVector(displacementDofs, elementDisplacement);
f.compactificationCoordinate->GetSubVector(
compactificationDofs, elementCompactification
);
f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification);
if (request.baseEnthalpyTrue != nullptr) {
baseEnthalpyLocal.GetSubVector(
enthalpyDofs, elementBaseEnthalpy
);
baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy);
}
if (request.basePotentialTrue != nullptr) {
basePotentialLocal.GetSubVector(
potentialDofs, elementBasePotential
);
basePotentialLocal.GetSubVector(potentialDofs, elementBasePotential);
}
if (request.enthalpyVariationTrue != nullptr) {
enthalpyVariationLocal.GetSubVector(
enthalpyDofs, elementEnthalpyVariation
);
enthalpyVariationLocal.GetSubVector(enthalpyDofs, elementEnthalpyVariation);
}
if (request.potentialVariationTrue != nullptr) {
potentialVariationLocal.GetSubVector(
potentialDofs, elementPotentialVariation
);
potentialVariationLocal.GetSubVector(potentialDofs, elementPotentialVariation);
}
if (request.displacementVariationTrue != nullptr) {
displacementVariationLocal.GetSubVector(
displacementDofs, elementDisplacementVariation
);
displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation);
}
if (enthalpyDofTransformation != nullptr) {
if (request.baseEnthalpyTrue != nullptr) {
enthalpyDofTransformation->InvTransformPrimal(
elementBaseEnthalpy
);
enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy);
}
if (request.enthalpyVariationTrue != nullptr) {
enthalpyDofTransformation->InvTransformPrimal(
elementEnthalpyVariation
);
enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation);
}
}
if (potentialDofTransformation != nullptr) {
if (request.basePotentialTrue != nullptr) {
potentialDofTransformation->InvTransformPrimal(
elementBasePotential
);
potentialDofTransformation->InvTransformPrimal(elementBasePotential);
}
if (request.potentialVariationTrue != nullptr) {
potentialDofTransformation->InvTransformPrimal(
elementPotentialVariation
);
potentialDofTransformation->InvTransformPrimal(elementPotentialVariation);
}
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->InvTransformPrimal(
elementDisplacement
);
displacementDofTransformation->InvTransformPrimal(elementDisplacement);
if (request.displacementVariationTrue != nullptr) {
displacementDofTransformation->InvTransformPrimal(
elementDisplacementVariation
);
displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation);
}
}
if (compactificationDofTransformation != nullptr) {
compactificationDofTransformation->InvTransformPrimal(
elementCompactification
);
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
}
const mean_field::mapping::ElementDisplacementData
displacementData = mean_field::mapping::
ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacement
);
const mean_field::mapping::ElementDisplacementData displacementData =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement);
const mean_field::mapping::ElementCompactificationData
compactificationData(
const mean_field::mapping::ElementCompactificationData compactificationData(
compactificationElement, elementCompactification
);
const mean_field::mapping::ElementMappingData mappingData{
.displacement = displacementData,
.compactification = compactificationData
.displacement = displacementData, .compactification = compactificationData
};
std::optional<mean_field::mapping::ElementDisplacementData>
displacementVariationData;
std::optional<mean_field::mapping::ElementDisplacementData> displacementVariationData;
if (request.displacementVariationTrue != nullptr) {
displacementVariationData.emplace(
mean_field::mapping::
ElementDisplacementDataFromElementVDofs(
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacementVariation
)
);
@@ -528,32 +419,25 @@ namespace {
potentialShape.SetSize(potentialElement.GetDof());
const mfem::IntegrationRule &integrationRule = get_hydrostatic_rule(
f, enthalpyElement, potentialElement, *transformation
);
const mfem::IntegrationRule &integrationRule =
get_hydrostatic_rule(f, enthalpyElement, potentialElement, *transformation);
for (int quadraturePoint = 0;
quadraturePoint < integrationRule.GetNPoints();
++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint =
integrationRule.IntPoint(quadraturePoint);
for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadraturePoint);
transformation->SetIntPoint(&integrationPoint);
mean_field::mapping::VolumeMappingContext mappingContext;
const mean_field::mapping::MappingStatus mappingStatus =
domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint,
workspace, mappingContext
const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mean_field::mapping::MappingStatus::valid,
"The base mapping is invalid in the "
"hydrostatic kernel. Element: "
<< elementId
<< ", quadrature point: " << quadraturePoint
<< elementId << ", quadrature point: " << quadraturePoint
<< ", status: " << static_cast<int>(mappingStatus)
);
@@ -564,27 +448,18 @@ namespace {
double baseIntegrand = 0.0;
if (requiresBaseState) {
const double enthalpyValue =
elementBaseEnthalpy * enthalpyShape;
const double enthalpyValue = elementBaseEnthalpy * enthalpyShape;
const double potentialValue =
elementBasePotential * potentialShape;
const double potentialValue = elementBasePotential * potentialShape;
const double rotationPotential =
request.rotation->potential(
mappingContext.mapping.physical_position
);
request.rotation->potential(mappingContext.mapping.physical_position);
baseIntegrand = enthalpyValue + potentialValue -
rotationPotential -
request.bernoulliConstant;
baseIntegrand = enthalpyValue + potentialValue - rotationPotential - request.bernoulliConstant;
}
if (request.buildResidual) {
elementResult.Add(
mappingContext.quadrature.weight * baseIntegrand,
enthalpyShape
);
elementResult.Add(mappingContext.quadrature.weight * baseIntegrand, enthalpyShape);
continue;
}
@@ -592,44 +467,34 @@ namespace {
double materialVariation = -request.constantVariation;
if (request.enthalpyVariationTrue != nullptr) {
materialVariation +=
elementEnthalpyVariation * enthalpyShape;
materialVariation += elementEnthalpyVariation * enthalpyShape;
}
if (request.potentialVariationTrue != nullptr) {
materialVariation +=
elementPotentialVariation * potentialShape;
materialVariation += elementPotentialVariation * potentialShape;
}
double weightedVariation =
mappingContext.quadrature.weight * materialVariation;
double weightedVariation = mappingContext.quadrature.weight * materialVariation;
if (request.displacementVariationTrue != nullptr) {
mean_field::mapping::VolumeMappingVariation
mappingVariation;
mean_field::mapping::VolumeMappingVariation mappingVariation;
const mean_field::mapping::MappingStatus variationStatus =
domainMapper.EvaluateVolumeVariation(
mappingData, *displacementVariationData,
*transformation, integrationPoint, mappingContext,
const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation(
mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext,
workspace, mappingVariation
);
MFEM_VERIFY(
variationStatus ==
mean_field::mapping::MappingStatus::valid,
variationStatus == mean_field::mapping::MappingStatus::valid,
"The mapping variation is invalid "
"in the hydrostatic kernel."
);
const double rotationVariation =
request.rotation->potential_directional_derivative(
mappingContext.mapping.physical_position,
mappingVariation.mapping.physical_position_variation
const double rotationVariation = request.rotation->potential_directional_derivative(
mappingContext.mapping.physical_position, mappingVariation.mapping.physical_position_variation
);
weightedVariation +=
baseIntegrand * mappingVariation.weight_variation -
weightedVariation += baseIntegrand * mappingVariation.weight_variation -
rotationVariation * mappingContext.quadrature.weight;
}
@@ -650,7 +515,7 @@ namespace {
namespace mean_field::operators::kernels {
void apply_hydrostatic_equilibrium(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &enthalpyTrue,
const mfem::Vector &potentialTrue,
@@ -666,14 +531,12 @@ namespace mean_field::operators::kernels {
request.bernoulliConstant = bernoulliConstant;
request.buildResidual = true;
assemble_hydrostatic_form(
f, domainMapper, displacementTrue, request, residual
);
assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, residual);
}
void apply_hydrostatic_equilibrium_enthalpy_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &enthalpyVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &action
@@ -682,14 +545,12 @@ namespace mean_field::operators::kernels {
request.enthalpyVariationTrue = &enthalpyVariationTrue;
assemble_hydrostatic_form(
f, domainMapper, displacementTrue, request, action
);
assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action);
}
void apply_hydrostatic_equilibrium_potential_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const mapping::DomainMapper &domainMapper,
const mfem::Vector &potentialVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &action
@@ -698,14 +559,12 @@ namespace mean_field::operators::kernels {
request.potentialVariationTrue = &potentialVariationTrue;
assemble_hydrostatic_form(
f, domainMapper, displacementTrue, request, action
);
assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action);
}
void apply_hydrostatic_equilibrium_constant_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const mapping::DomainMapper &domainMapper,
const double constantVariation,
const mfem::Vector &displacementTrue,
mfem::Vector &action
@@ -714,14 +573,12 @@ namespace mean_field::operators::kernels {
request.constantVariation = constantVariation;
assemble_hydrostatic_form(
f, domainMapper, displacementTrue, request, action
);
assemble_hydrostatic_form(f, domainMapper, displacementTrue, request, action);
}
void apply_hydrostatic_equilibrium_displacement_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector &basePotentialTrue,
@@ -738,14 +595,12 @@ namespace mean_field::operators::kernels {
request.displacementVariationTrue = &displacementVariationTrue;
request.bernoulliConstant = baseBernoulliConstant;
assemble_hydrostatic_form(
f, domainMapper, baseDisplacementTrue, request, action
);
assemble_hydrostatic_form(f, domainMapper, baseDisplacementTrue, request, action);
}
void apply_hydrostatic_equilibrium_action(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector &basePotentialTrue,
@@ -768,8 +623,6 @@ namespace mean_field::operators::kernels {
request.bernoulliConstant = baseBernoulliConstant;
request.constantVariation = constantVariation;
assemble_hydrostatic_form(
f, domainMapper, baseDisplacementTrue, request, action
);
assemble_hydrostatic_form(f, domainMapper, baseDisplacementTrue, request, action);
}
} // namespace mean_field::operators::kernels

View File

@@ -3,6 +3,7 @@ module;
#include <array>
#include <cmath>
#include <limits>
#include <optional>
#include <mfem.hpp>
@@ -11,20 +12,29 @@ module mean_field;
import :operators.kernels.pressure_force;
namespace {
namespace dimensions = mean_field::dimensions;
namespace eos = mean_field::eos;
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] bool is_vacuum_attribute(const int attribute) {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
enum class PressureForceAction { residual, enthalpy, displacement };
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
mfem::Vector &localVector
) {
MFEM_VERIFY(
trueVector.Size() == finiteElementSpace.GetTrueVSize(),
"The pressure-force true vector has the wrong size."
trueVector.Size() == finiteElementSpace.GetTrueVSize(), "The pressure-force true vector has the wrong size."
);
localVector.SetSize(finiteElementSpace.GetVSize());
const mfem::Operator *prolongation =
finiteElementSpace.GetProlongationMatrix();
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(trueVector, localVector);
@@ -39,15 +49,13 @@ namespace {
mfem::Vector &trueVector
) {
MFEM_VERIFY(
localVector.Size() == finiteElementSpace.GetVSize(),
"The pressure-force local vector has the wrong size."
localVector.Size() == finiteElementSpace.GetVSize(), "The pressure-force local vector has the wrong size."
);
trueVector.SetSize(finiteElementSpace.GetTrueVSize());
trueVector = 0.0;
const mfem::Operator *prolongation =
finiteElementSpace.GetProlongationMatrix();
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(localVector, trueVector);
@@ -68,15 +76,14 @@ namespace {
}
if (ordering == mfem::Ordering::byVDIM) {
return component + scalarDof * dimension;
return scalarDof * dimension + component;
}
MFEM_ABORT("The displacement space uses an unsupported ordering.");
return -1;
}
[[nodiscard]] int get_pressure_extra_order(
const mean_field::physics::PolytropicBarotrope &barotrope
) {
[[nodiscard]] int get_pressure_extra_order(const mean_field::eos::Polytrope &barotrope) {
/*
* Pressure has the enthalpy dependence
*
@@ -87,15 +94,11 @@ namespace {
* contribution is therefore n times that order.
*/
const double extraOrder =
barotrope.polytropic_index() *
static_cast<double>(
mean_field::field::Enthalpy::Scalar::familyOrder
);
barotrope.polytropic_index() * static_cast<double>(mean_field::field::Enthalpy::Scalar::familyOrder);
MFEM_VERIFY(
std::isfinite(extraOrder) && extraOrder >= 0.0 &&
extraOrder <=
static_cast<double>(std::numeric_limits<int>::max()),
extraOrder <= static_cast<double>(std::numeric_limits<int>::max()),
"The pressure EOS effective polynomial order is invalid."
);
@@ -104,43 +107,36 @@ namespace {
[[nodiscard]] const mfem::IntegrationRule &get_pressure_force_rule(
const mean_field::fem::FEM &f,
const mean_field::physics::PolytropicBarotrope &barotrope,
const mean_field::eos::Polytrope &barotrope,
const mfem::FiniteElement &enthalpyElement,
const mfem::FiniteElement &displacementElement,
const mfem::ElementTransformation &transformation
) {
using EnthalpyField =
mean_field::field::Field<mean_field::field::Enthalpy>;
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
MFEM_VERIFY(
enthalpyElement.GetOrder() ==
mean_field::field::Enthalpy::Scalar::familyOrder,
enthalpyElement.GetOrder() == mean_field::field::Enthalpy::Scalar::familyOrder,
"The pressure-force enthalpy element does not match the "
"registered enthalpy field."
);
MFEM_VERIFY(
displacementElement.GetOrder() ==
mean_field::field::Displacement::Vector::familyOrder,
displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder,
"The pressure-force test element does not match the "
"registered displacement field."
);
const mean_field::quadrature::Query query = EnthalpyField::make_query<
mean_field::field::Enthalpy::Form::PressureForce>(
mean_field::quadrature::QuadratureRole::discretization,
transformation.OrderW(),
std::array<int, 1>{get_pressure_extra_order(barotrope)},
mean_field::utils::DOMAINS::STELLAR,
const mean_field::quadrature::Query query =
EnthalpyField::make_query<mean_field::field::Enthalpy::Form::PressureForce>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(),
std::array<int, 1>{get_pressure_extra_order(barotrope)}, mean_field::utils::DOMAINS::STELLAR,
mean_field::quadrature::MappingKind::general
);
const mean_field::quadrature::MfemRule rule =
f.quadratureFactory->get(query, transformation.GetGeometryType());
const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
rule.integration_rule != nullptr,
"The quadrature policy did not return a pressure-force "
rule.integration_rule != nullptr, "The quadrature policy did not return a pressure-force "
"integration rule."
);
@@ -149,41 +145,34 @@ namespace {
void validate_inputs(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapperStateless &domainMapper,
const mean_field::mapping::DomainMapper &domainMapper,
const mfem::Vector &enthalpyTrue,
const mfem::Vector &displacementTrue
) {
MFEM_VERIFY(
f.mesh != nullptr, "The pressure-force kernel requires a mesh."
);
MFEM_VERIFY(f.mesh != nullptr, "The pressure-force kernel requires a mesh.");
MFEM_VERIFY(
f.enthalpyFes != nullptr,
"The pressure-force kernel requires the enthalpy "
f.enthalpyFes != nullptr, "The pressure-force kernel requires the enthalpy "
"finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr,
"The pressure-force kernel requires the displacement "
f.displacementFes != nullptr, "The pressure-force kernel requires the displacement "
"finite-element space."
);
MFEM_VERIFY(
f.compactificationFes != nullptr,
"The pressure-force kernel requires the compactification "
f.compactificationFes != nullptr, "The pressure-force kernel requires the compactification "
"finite-element space."
);
MFEM_VERIFY(
f.compactificationCoordinate != nullptr,
"The pressure-force kernel requires the compactification "
f.compactificationCoordinate != nullptr, "The pressure-force kernel requires the compactification "
"coordinate."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr,
"The pressure-force kernel requires the quadrature "
f.quadratureFactory != nullptr, "The pressure-force kernel requires the quadrature "
"rule factory."
);
@@ -204,72 +193,103 @@ namespace {
);
MFEM_VERIFY(
f.displacementFes->GetVDim() == f.mesh->Dimension(),
"The displacement vector dimension does not match the "
f.displacementFes->GetVDim() == f.mesh->Dimension(), "The displacement vector dimension does not match the "
"mesh dimension."
);
/*
* ElementDisplacementDataFromElementVDofs currently consumes the
* registered byNODES layout. Keep this explicit so a future
* registry change fails immediately rather than silently
* corrupting the geometry.
*/
MFEM_VERIFY(
f.displacementFes->GetOrdering() == mfem::Ordering::byNODES,
"The pressure-force kernel requires the registered byNODES "
"displacement ordering."
);
}
} // namespace
namespace mean_field::operators::kernels {
void apply_pressure_force_residual(
const fem::FEM &f,
const mapping::DomainMapperStateless &domainMapper,
const physics::PolytropicBarotrope &barotrope,
const mfem::Vector &enthalpyTrue,
void apply_pressure_force_action(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &domainMapper,
const mean_field::eos::Polytrope &barotrope,
const PressureForceAction pressureForceAction,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector *enthalpyVariationTrue,
const mfem::Vector *displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
mfem::Vector &actionTrue
) {
validate_inputs(f, domainMapper, enthalpyTrue, displacementTrue);
validate_inputs(f, domainMapper, baseEnthalpyTrue, displacementTrue);
mfem::Vector enthalpyLocal;
if (pressureForceAction == PressureForceAction::enthalpy) {
MFEM_VERIFY(
enthalpyVariationTrue != nullptr && enthalpyVariationTrue->Size() == f.enthalpyFes->GetTrueVSize(),
"The pressure-force enthalpy variation has the wrong size."
);
}
if (pressureForceAction == PressureForceAction::displacement) {
MFEM_VERIFY(
displacementVariationTrue != nullptr &&
displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(),
"The pressure-force displacement variation has the wrong "
"size."
);
}
mfem::Vector baseEnthalpyLocal;
mfem::Vector enthalpyVariationLocal;
mfem::Vector displacementLocal;
mfem::Vector displacementVariationLocal;
true_to_local(*f.enthalpyFes, enthalpyTrue, enthalpyLocal);
true_to_local(*f.enthalpyFes, baseEnthalpyTrue, baseEnthalpyLocal);
if (enthalpyVariationTrue != nullptr) {
true_to_local(*f.enthalpyFes, *enthalpyVariationTrue, enthalpyVariationLocal);
}
true_to_local(*f.displacementFes, displacementTrue, displacementLocal);
mfem::Vector localResidual(f.displacementFes->GetVSize());
localResidual = 0.0;
if (displacementVariationTrue != nullptr) {
true_to_local(*f.displacementFes, *displacementVariationTrue, displacementVariationLocal);
}
mapping::DomainMapperStateless::Workspace workspace(
f.mesh->Dimension()
);
mfem::Vector localAction(f.displacementFes->GetVSize());
localAction = 0.0;
mfem::Array<int> enthalpyDofs;
mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
mfem::Array<int> enthalpyDofsofs;
mfem::Array<int> displacementDofs;
mfem::Array<int> compactificationDofs;
mfem::Vector elementEnthalpy;
mfem::Vector elementBaseEnthalpy;
mfem::Vector elementEnthalpyVariation;
mfem::Vector elementDisplacement;
mfem::Vector elementDisplacementVariation;
mfem::Vector elementCompactification;
mfem::Vector elementResidual;
mfem::Vector elementAction;
mfem::Vector enthalpyShape;
mfem::Array<int> enthalpyDofs;
mfem::DenseMatrix displacementDShapeReference;
mfem::DenseMatrix displacementDShapePhysical;
mfem::DenseMatrix displacementDShapePhysicalVariation;
mapping::VolumeMappingContext mappingContext;
mean_field::mapping::VolumeMappingContext mappingContext;
const int dimension = f.mesh->Dimension();
const int vacuumAttribute = domainMapper.GetVacuumElementAttribute();
const mfem::Ordering::Type displacementOrdering =
f.displacementFes->GetOrdering();
const mfem::Ordering::Type displacementOrdering = f.displacementFes->GetOrdering();
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation =
f.mesh->GetElementTransformation(elementId);
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(
transformation != nullptr,
"The pressure-force kernel received a null element "
transformation != nullptr, "The pressure-force kernel received a null element "
"transformation."
);
@@ -277,132 +297,141 @@ namespace mean_field::operators::kernels {
* Skip vacuum before constructing or evaluating any mapping
* data for the element.
*/
if (transformation->Attribute == vacuumAttribute) {
if (is_vacuum_attribute(transformation->Attribute)) {
continue;
}
const mfem::FiniteElement &enthalpyElement =
*f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &enthalpyElement = *f.enthalpyFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement =
*f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement =
*f.compactificationFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId);
mfem::DofTransformation *enthalpyDofTransformation =
f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *enthalpyDofTransformation = f.enthalpyFes->GetElementDofs(elementId, enthalpyDofs);
mfem::DofTransformation *displacementDofTransformation =
f.displacementFes->GetElementVDofs(elementId, displacementDofs);
mfem::DofTransformation *compactificationDofTransformation =
f.compactificationFes->GetElementDofs(
elementId, compactificationDofs
);
f.compactificationFes->GetElementDofs(elementId, compactificationDofs);
enthalpyLocal.GetSubVector(enthalpyDofs, elementEnthalpy);
baseEnthalpyLocal.GetSubVector(enthalpyDofs, elementBaseEnthalpy);
displacementLocal.GetSubVector(
displacementDofs, elementDisplacement
);
if (enthalpyVariationTrue != nullptr) {
enthalpyVariationLocal.GetSubVector(enthalpyDofs, elementEnthalpyVariation);
}
f.compactificationCoordinate->GetSubVector(
compactificationDofs, elementCompactification
);
displacementLocal.GetSubVector(displacementDofs, elementDisplacement);
if (displacementVariationTrue != nullptr) {
displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation);
}
f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification);
if (enthalpyDofTransformation != nullptr) {
enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy);
enthalpyDofTransformation->InvTransformPrimal(elementBaseEnthalpy);
if (enthalpyVariationTrue != nullptr) {
enthalpyDofTransformation->InvTransformPrimal(elementEnthalpyVariation);
}
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->InvTransformPrimal(
elementDisplacement
);
displacementDofTransformation->InvTransformPrimal(elementDisplacement);
if (displacementVariationTrue != nullptr) {
displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation);
}
}
if (compactificationDofTransformation != nullptr) {
compactificationDofTransformation->InvTransformPrimal(
elementCompactification
);
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
}
const mapping::ElementDisplacementData displacementData =
mapping::ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacement
);
const mean_field::mapping::ElementDisplacementData displacementData =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement);
const mapping::ElementCompactificationData compactificationData(
const mean_field::mapping::ElementCompactificationData compactificationData(
compactificationElement, elementCompactification
);
const mapping::ElementMappingData mappingData{
.displacement = displacementData,
.compactification = compactificationData
const mean_field::mapping::ElementMappingData mappingData{
.displacement = displacementData, .compactification = compactificationData
};
std::optional<mean_field::mapping::ElementDisplacementData> displacementVariationData;
if (displacementVariationTrue != nullptr) {
displacementVariationData.emplace(
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacementVariation
)
);
}
const int scalarDisplacementDofCount = displacementElement.GetDof();
MFEM_VERIFY(
displacementDofs.Size() ==
scalarDisplacementDofCount * dimension,
displacementDofs.Size() == scalarDisplacementDofCount * dimension,
"The pressure-force element displacement vector has "
"the wrong size."
);
enthalpyShape.SetSize(enthalpyElement.GetDof());
displacementDShapeReference.SetSize(
scalarDisplacementDofCount, dimension
);
displacementDShapeReference.SetSize(scalarDisplacementDofCount, dimension);
displacementDShapePhysical.SetSize(
scalarDisplacementDofCount, dimension
);
displacementDShapePhysical.SetSize(scalarDisplacementDofCount, dimension);
elementResidual.SetSize(displacementDofs.Size());
elementResidual = 0.0;
displacementDShapePhysicalVariation.SetSize(scalarDisplacementDofCount, dimension);
elementAction.SetSize(displacementDofs.Size());
elementAction = 0.0;
const mfem::IntegrationRule &integrationRule =
get_pressure_force_rule(
f, barotrope, enthalpyElement, displacementElement,
*transformation
);
get_pressure_force_rule(f, barotrope, enthalpyElement, displacementElement, *transformation);
for (int quadratureIndex = 0;
quadratureIndex < integrationRule.GetNPoints();
++quadratureIndex) {
const mfem::IntegrationPoint &integrationPoint =
integrationRule.IntPoint(quadratureIndex);
for (int quadratureIndex = 0; quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) {
const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadratureIndex);
transformation->SetIntPoint(&integrationPoint);
const mapping::MappingStatus mappingStatus =
domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint,
workspace, mappingContext
const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mapping::MappingStatus::valid,
mappingStatus == mean_field::mapping::MappingStatus::valid,
"Stateless mapping failed in the pressure-force "
"kernel. Element: "
<< elementId
<< ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex
<< ", status: " << static_cast<int>(mappingStatus)
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex << ", status: " << static_cast<int>(mappingStatus)
);
enthalpyElement.CalcShape(integrationPoint, enthalpyShape);
const double enthalpyValue = elementEnthalpy * enthalpyShape;
const double enthalpyValue = elementBaseEnthalpy * enthalpyShape;
const double pressureValue =
barotrope.pressure_from_enthalpy(enthalpyValue);
double pressureFactor = 0.0;
displacementElement.CalcDShape(
integrationPoint, displacementDShapeReference
);
if (pressureForceAction == PressureForceAction::residual ||
pressureForceAction == PressureForceAction::displacement) {
pressureFactor = eos::evaluate<dimensions::quantity::Pressure>(
barotrope, dimensions::SpecificEnthalpyValue{enthalpyValue}
)
.value();
} else {
const double enthalpyVariationValue = elementEnthalpyVariation * enthalpyShape;
pressureFactor = eos::partialDerivative<eos::quantity::Pressure, eos::quantity::SpecificEnthalpy>(
barotrope, dimensions::SpecificEnthalpyValue{enthalpyValue}
)
.value() *
enthalpyVariationValue;
}
displacementElement.CalcDShape(integrationPoint, displacementDShapeReference);
/*
* Row i of DShape is grad_reference(N_i). Multiplication
@@ -411,17 +440,45 @@ namespace mean_field::operators::kernels {
* grad_physical(N_i)
* = grad_reference(N_i) J^{-1}.
*/
mfem::Mult(
displacementDShapeReference,
mappingContext.quadrature.J_inv, displacementDShapePhysical
mfem::Mult(displacementDShapeReference, mappingContext.quadrature.J_inv, displacementDShapePhysical);
std::optional<mean_field::mapping::VolumeMappingVariation> mappingVariation;
if (pressureForceAction == PressureForceAction::displacement) {
mappingVariation.emplace();
const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation(
mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext,
workspace, *mappingVariation
);
const double weightedPressure =
pressureValue * mappingContext.quadrature.weight;
MFEM_VERIFY(
variationStatus == mean_field::mapping::MappingStatus::valid,
"Stateless mapping variation failed in the "
"pressure-force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute << ", quadrature point: "
<< quadratureIndex << ", status: " << static_cast<int>(variationStatus)
);
/*
* Differentiating
*
* grad_x(N_i) = grad_reference(N_i) J^{-1}
*
* at the frozen base geometry gives the physical
* test-gradient variation used by the geometric
* pressure block.
*/
mfem::Mult(
displacementDShapeReference, mappingVariation->inverse_element_jacobian_variation,
displacementDShapePhysicalVariation
);
}
const double weightedPressureFactor = pressureFactor * mappingContext.quadrature.weight;
MFEM_VERIFY(
std::isfinite(pressureValue) &&
std::isfinite(weightedPressure),
std::isfinite(pressureFactor) && std::isfinite(weightedPressureFactor),
"The pressure-force kernel encountered a non-finite "
"quadrature value."
);
@@ -436,29 +493,96 @@ namespace mean_field::operators::kernels {
* R_(i,c)
* = -integral P partial_c N_i dV.
*/
for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount;
++scalarDof) {
for (int component = 0; component < dimension;
++component) {
for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) {
for (int component = 0; component < dimension; ++component) {
const int vectorDof = vector_dof_index(
displacementOrdering, scalarDof, component,
scalarDisplacementDofCount, dimension
displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension
);
elementResidual(vectorDof) -=
weightedPressure *
displacementDShapePhysical(scalarDof, component);
if (pressureForceAction == PressureForceAction::displacement) {
/*
* Differentiate the complete discrete factor
*
* grad_x(N_i) dV_x.
*
* The enthalpy DOFs, and therefore P(h), are
* frozen in this Jacobian column.
*/
const double gradientWeightVariation =
mappingContext.quadrature.weight *
displacementDShapePhysicalVariation(scalarDof, component) +
mappingVariation->weight_variation * displacementDShapePhysical(scalarDof, component);
const double contribution = pressureFactor * gradientWeightVariation;
MFEM_VERIFY(
std::isfinite(gradientWeightVariation) && std::isfinite(contribution),
"The pressure-force geometry action "
"encountered a non-finite contribution."
);
elementAction(vectorDof) -= contribution;
} else {
elementAction(vectorDof) -=
weightedPressureFactor * displacementDShapePhysical(scalarDof, component);
}
}
}
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->TransformDual(elementResidual);
displacementDofTransformation->TransformDual(elementAction);
}
localResidual.AddElementVector(displacementDofs, elementResidual);
localAction.AddElementVector(displacementDofs, elementAction);
}
local_to_true(*f.displacementFes, localResidual, residualTrue);
local_to_true(*f.displacementFes, localAction, actionTrue);
}
} // namespace
namespace mean_field::operators::kernels {
void apply_pressure_force_residual(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const mfem::Vector &enthalpyTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
) {
apply_pressure_force_action(
f, domainMapper, barotrope, PressureForceAction::residual, enthalpyTrue, nullptr, nullptr, displacementTrue,
residualTrue
);
}
void apply_pressure_force_enthalpy_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector &enthalpyVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_pressure_force_action(
f, domainMapper, barotrope, PressureForceAction::enthalpy, baseEnthalpyTrue, &enthalpyVariationTrue,
nullptr, displacementTrue, actionTrue
);
}
void apply_pressure_force_displacement_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const mfem::Vector &baseEnthalpyTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_pressure_force_action(
f, domainMapper, barotrope, PressureForceAction::displacement, baseEnthalpyTrue, nullptr,
&displacementVariationTrue, displacementTrue, actionTrue
);
}
} // namespace mean_field::operators::kernels

View File

@@ -0,0 +1,738 @@
module;
#include <array>
#include <cmath>
#include <expected>
#include <optional>
#include <stdexcept>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
import :operators.kernels.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<mean_field::utils::domain::Vacuum>(attribute);
}
enum class RotationalDisplacementForceAction { residual, density, displacement, complete };
using Rejection = mean_field::operators::kernels::RotationalDisplacementForceRejection;
using Reason = mean_field::operators::kernels::RotationalDisplacementForceRejectionReason;
using Result = mean_field::operators::kernels::RotationalDisplacementForceResult;
[[nodiscard]] Rejection mapping_rejection(const mean_field::mapping::MappingStatus status) {
MFEM_VERIFY(
status != mean_field::mapping::MappingStatus::invalid_dimension,
"The rotational-displacement-force mapping reported an invariant dimension mismatch."
);
return {.reason = Reason::invalid_mapping, .mappingStatus = status};
}
[[nodiscard]] Rejection non_finite_rejection() noexcept {
return {.reason = Reason::non_finite_arithmetic};
}
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) noexcept {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
[[nodiscard]] int encode_rejection(const std::optional<Rejection> &rejection) noexcept {
if (!rejection.has_value()) {
return 0;
}
if (rejection->reason == Reason::non_finite_arithmetic) {
return 256;
}
return static_cast<int>(rejection->mappingStatus) + 1;
}
[[nodiscard]] Rejection decode_rejection(const int encoded) noexcept {
if (encoded >= 256) {
return non_finite_rejection();
}
return mapping_rejection(static_cast<mean_field::mapping::MappingStatus>(encoded - 1));
}
[[nodiscard]] Result synchronize_rejection(
const std::optional<Rejection> &localRejection,
const MPI_Comm communicator
) {
const int localEncoded = encode_rejection(localRejection);
int globalEncoded = 0;
if (MPI_Allreduce(&localEncoded, &globalEncoded, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("Could not synchronize rotational-displacement-force candidate validity.");
}
if (globalEncoded != 0) {
return std::unexpected(decode_rejection(globalEncoded));
}
return {};
}
[[noreturn]] void throw_rejection(const Rejection &rejection) {
if (rejection.reason == Reason::non_finite_arithmetic) {
throw std::domain_error("The rotational-displacement force produced non-finite arithmetic.");
}
throw std::domain_error("The rotational-displacement force encountered an invalid mapped domain.");
}
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
mfem::Vector &localVector
) {
MFEM_VERIFY(
trueVector.Size() == finiteElementSpace.GetTrueVSize(),
"The rotational-displacement-force true vector has the wrong "
"size."
);
localVector.SetSize(finiteElementSpace.GetVSize());
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(trueVector, localVector);
} else {
localVector = trueVector;
}
}
void local_to_true(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &localVector,
mfem::Vector &trueVector
) {
MFEM_VERIFY(
localVector.Size() == finiteElementSpace.GetVSize(),
"The rotational-displacement-force local vector has the wrong "
"size."
);
trueVector.SetSize(finiteElementSpace.GetTrueVSize());
trueVector = 0.0;
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->MultTranspose(localVector, trueVector);
} else {
trueVector = localVector;
}
}
[[nodiscard]] int vector_dof_index(
const mfem::Ordering::Type ordering,
const int scalarDof,
const int component,
const int scalarDofCount,
const int dimension
) {
if (ordering == mfem::Ordering::byNODES) {
return scalarDof + component * scalarDofCount;
}
if (ordering == mfem::Ordering::byVDIM) {
return scalarDof * dimension + component;
}
MFEM_ABORT(
"The rotational-displacement-force test space uses an "
"unsupported ordering."
);
return -1;
}
[[nodiscard]] const mfem::IntegrationRule &get_rotation_force_rule(
const mean_field::fem::FEM &f,
const mfem::FiniteElement &densityElement,
const mfem::FiniteElement &displacementElement,
const mfem::ElementTransformation &transformation
) {
using DisplacementField = mean_field::field::Field<mean_field::field::Displacement>;
MFEM_VERIFY(
densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder,
"The rotational-displacement-force density element does not "
"match the registered density field."
);
MFEM_VERIFY(
displacementElement.GetOrder() == mean_field::field::Displacement::Vector::familyOrder,
"The rotational-displacement-force test element does not match "
"the registered displacement field."
);
/*
* grad(Psi_rotation) is linear in physical position, so it adds one
* dynamic polynomial-order contribution.
*/
const mean_field::quadrature::Query query =
DisplacementField::make_query<mean_field::field::Displacement::Form::CentrifugalForce>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array<int, 1>{1},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
rule.integration_rule != nullptr, "The quadrature policy did not return a rotational-"
"displacement-force integration rule."
);
return *rule.integration_rule;
}
void validate_finite_vector(
const mfem::Vector &vector,
const char *message
) {
for (int index = 0; index < vector.Size(); ++index) {
MFEM_VERIFY(std::isfinite(vector(index)), message);
}
}
void validate_common_inputs(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &domainMapper,
const mfem::Vector &displacementTrue
) {
MFEM_VERIFY(f.mesh != nullptr, "The rotational-displacement-force kernel requires a mesh.");
MFEM_VERIFY(
f.mesh->Dimension() == 3, "The rotational-displacement-force kernel requires a "
"three-dimensional mesh."
);
MFEM_VERIFY(
f.densityFes != nullptr, "The rotational-displacement-force kernel requires the density "
"finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr, "The rotational-displacement-force kernel requires the "
"displacement finite-element space."
);
MFEM_VERIFY(
f.compactificationFes != nullptr && f.compactificationCoordinate != nullptr,
"The rotational-displacement-force kernel requires the "
"compactification coordinate."
);
MFEM_VERIFY(
f.quadratureFactory != nullptr, "The rotational-displacement-force kernel requires the "
"quadrature-rule factory."
);
MFEM_VERIFY(
displacementTrue.Size() == f.displacementFes->GetTrueVSize(),
"The rotational-displacement-force displacement vector has the "
"wrong size."
);
MFEM_VERIFY(
domainMapper.GetDimension() == f.mesh->Dimension(),
"The rotational-displacement-force mapper dimension does not "
"match the mesh dimension."
);
MFEM_VERIFY(
f.displacementFes->GetVDim() == f.mesh->Dimension(),
"The rotational-displacement-force displacement dimension does "
"not match the mesh dimension."
);
}
void validate_density(
const mean_field::fem::FEM &f,
const mfem::Vector &density,
const char *message
) {
MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message);
}
[[nodiscard]] Result apply_rotational_displacement_force_action(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &domainMapper,
const mean_field::physics::RigidRotation &rotation,
const RotationalDisplacementForceAction requestedAction,
const mfem::Vector *baseDensityTrue,
const mfem::Vector *densityVariationTrue,
const mfem::Vector *displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue,
const bool reportCandidateRejection
) {
validate_common_inputs(f, domainMapper, displacementTrue);
const bool needsBaseDensity = requestedAction == RotationalDisplacementForceAction::residual ||
requestedAction == RotationalDisplacementForceAction::displacement ||
requestedAction == RotationalDisplacementForceAction::complete;
const bool needsDensityVariation = requestedAction == RotationalDisplacementForceAction::density ||
requestedAction == RotationalDisplacementForceAction::complete;
const bool needsDisplacementVariation = requestedAction == RotationalDisplacementForceAction::displacement ||
requestedAction == RotationalDisplacementForceAction::complete;
if (needsBaseDensity) {
MFEM_VERIFY(
baseDensityTrue != nullptr, "The rotational-displacement-force action requires a base "
"density."
);
validate_density(f, *baseDensityTrue, "The rotational-displacement-force base density is invalid.");
}
if (needsDensityVariation) {
MFEM_VERIFY(
densityVariationTrue != nullptr, "The rotational-displacement-force action requires a "
"density variation."
);
validate_density(
f, *densityVariationTrue,
"The rotational-displacement-force density variation is "
"invalid."
);
}
if (needsDisplacementVariation) {
MFEM_VERIFY(
displacementVariationTrue != nullptr &&
displacementVariationTrue->Size() == f.displacementFes->GetTrueVSize(),
"The rotational-displacement-force displacement variation "
"is invalid."
);
validate_finite_vector(
*displacementVariationTrue, "The rotational-displacement-force displacement variation "
"contains a non-finite value."
);
}
bool inputsAreFinite = vector_is_finite(displacementTrue);
if (needsBaseDensity) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*baseDensityTrue);
}
if (needsDensityVariation) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*densityVariationTrue);
}
if (needsDisplacementVariation) {
inputsAreFinite = inputsAreFinite && vector_is_finite(*displacementVariationTrue);
}
if (!reportCandidateRejection) {
MFEM_VERIFY(inputsAreFinite, "The rotational-displacement-force action contains non-finite input data.");
} else {
const std::optional<Rejection> inputRejection =
inputsAreFinite ? std::optional<Rejection>{} : std::optional<Rejection>{non_finite_rejection()};
auto synchronized = synchronize_rejection(inputRejection, f.mesh->GetComm());
if (!synchronized.has_value()) {
return synchronized;
}
}
mfem::Vector baseDensityLocal;
mfem::Vector densityVariationLocal;
mfem::Vector displacementLocal;
mfem::Vector displacementVariationLocal;
if (needsBaseDensity) {
true_to_local(*f.densityFes, *baseDensityTrue, baseDensityLocal);
}
if (needsDensityVariation) {
true_to_local(*f.densityFes, *densityVariationTrue, densityVariationLocal);
}
true_to_local(*f.displacementFes, displacementTrue, displacementLocal);
if (needsDisplacementVariation) {
true_to_local(*f.displacementFes, *displacementVariationTrue, displacementVariationLocal);
}
mfem::Vector localAction(f.displacementFes->GetVSize());
localAction = 0.0;
mean_field::mapping::DomainMapper::Workspace workspace(f.mesh->Dimension());
mfem::Array<int> densityDofs;
mfem::Array<int> displacementDofs;
mfem::Array<int> compactificationDofs;
mfem::Vector elementBaseDensity;
mfem::Vector elementDensityVariation;
mfem::Vector elementDisplacement;
mfem::Vector elementDisplacementVariation;
mfem::Vector elementCompactification;
mfem::Vector elementAction;
mfem::Vector densityShape;
mfem::Vector displacementShape;
mfem::Vector potentialGradient;
mfem::Vector potentialGradientVariation;
mfem::Vector centrifugalAcceleration;
mfem::Vector centrifugalAccelerationVariation;
mfem::Vector weightedForce;
mean_field::mapping::VolumeMappingContext mappingContext;
mean_field::mapping::VolumeMappingVariation mappingVariation;
const int dimension = f.mesh->Dimension();
const mfem::Ordering::Type displacementOrdering = f.displacementFes->GetOrdering();
std::optional<Rejection> candidateRejection;
for (int elementId = 0; elementId < f.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(
transformation != nullptr, "The rotational-displacement-force kernel received a null "
"element transformation."
);
if (is_vacuum_attribute(transformation->Attribute)) {
continue;
}
const mfem::FiniteElement &densityElement = *f.densityFes->GetFE(elementId);
const mfem::FiniteElement &displacementElement = *f.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement = *f.compactificationFes->GetFE(elementId);
mfem::DofTransformation *densityDofTransformation = f.densityFes->GetElementDofs(elementId, densityDofs);
mfem::DofTransformation *displacementDofTransformation =
f.displacementFes->GetElementVDofs(elementId, displacementDofs);
mfem::DofTransformation *compactificationDofTransformation =
f.compactificationFes->GetElementDofs(elementId, compactificationDofs);
if (needsBaseDensity) {
baseDensityLocal.GetSubVector(densityDofs, elementBaseDensity);
}
if (needsDensityVariation) {
densityVariationLocal.GetSubVector(densityDofs, elementDensityVariation);
}
displacementLocal.GetSubVector(displacementDofs, elementDisplacement);
if (needsDisplacementVariation) {
displacementVariationLocal.GetSubVector(displacementDofs, elementDisplacementVariation);
}
f.compactificationCoordinate->GetSubVector(compactificationDofs, elementCompactification);
if (densityDofTransformation != nullptr) {
if (needsBaseDensity) {
densityDofTransformation->InvTransformPrimal(elementBaseDensity);
}
if (needsDensityVariation) {
densityDofTransformation->InvTransformPrimal(elementDensityVariation);
}
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->InvTransformPrimal(elementDisplacement);
if (needsDisplacementVariation) {
displacementDofTransformation->InvTransformPrimal(elementDisplacementVariation);
}
}
if (compactificationDofTransformation != nullptr) {
compactificationDofTransformation->InvTransformPrimal(elementCompactification);
}
const mean_field::mapping::ElementDisplacementData displacementData =
mean_field::mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacement);
const mean_field::mapping::ElementCompactificationData compactificationData(
compactificationElement, elementCompactification
);
const mean_field::mapping::ElementMappingData mappingData{
.displacement = displacementData, .compactification = compactificationData
};
std::optional<mean_field::mapping::ElementDisplacementData> displacementVariationData;
if (needsDisplacementVariation) {
displacementVariationData.emplace(
mean_field::mapping::ElementDisplacementDataFromElementVDofs(
displacementElement, elementDisplacementVariation
)
);
}
const int scalarDisplacementDofCount = displacementElement.GetDof();
MFEM_VERIFY(
displacementDofs.Size() == scalarDisplacementDofCount * dimension,
"The rotational-displacement-force element displacement "
"vector has the wrong size."
);
densityShape.SetSize(densityElement.GetDof());
displacementShape.SetSize(scalarDisplacementDofCount);
potentialGradient.SetSize(dimension);
potentialGradientVariation.SetSize(dimension);
centrifugalAcceleration.SetSize(dimension);
centrifugalAccelerationVariation.SetSize(dimension);
weightedForce.SetSize(dimension);
elementAction.SetSize(displacementDofs.Size());
elementAction = 0.0;
const mfem::IntegrationRule &integrationRule =
get_rotation_force_rule(f, densityElement, displacementElement, *transformation);
for (int quadratureIndex = 0; quadratureIndex < integrationRule.GetNPoints(); ++quadratureIndex) {
const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadratureIndex);
transformation->SetIntPoint(&integrationPoint);
const mean_field::mapping::MappingStatus mappingStatus = domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
if (mappingStatus != mean_field::mapping::MappingStatus::valid) {
if (!reportCandidateRejection) {
MFEM_VERIFY(
false, "Stateless mapping failed in the rotational-"
"displacement-force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex
<< ", status: " << static_cast<int>(mappingStatus)
);
}
candidateRejection = mapping_rejection(mappingStatus);
continue;
}
if (needsDisplacementVariation) {
const mean_field::mapping::MappingStatus variationStatus = domainMapper.EvaluateVolumeVariation(
mappingData, *displacementVariationData, *transformation, integrationPoint, mappingContext,
workspace, mappingVariation
);
if (variationStatus != mean_field::mapping::MappingStatus::valid) {
if (!reportCandidateRejection) {
MFEM_VERIFY(
false, "Stateless mapping variation failed in the "
"rotational-displacement-force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex
<< ", status: " << static_cast<int>(variationStatus)
);
}
candidateRejection = mapping_rejection(variationStatus);
continue;
}
}
densityElement.CalcShape(integrationPoint, densityShape);
displacementElement.CalcShape(integrationPoint, displacementShape);
double baseDensityValue = 0.0;
double densityVariationValue = 0.0;
if (needsBaseDensity) {
baseDensityValue = elementBaseDensity * densityShape;
}
if (needsDensityVariation) {
densityVariationValue = elementDensityVariation * densityShape;
}
rotation.potential_gradient(mappingContext.mapping.physical_position, potentialGradient);
centrifugalAcceleration = potentialGradient;
centrifugalAcceleration *= -1.0;
if (needsDisplacementVariation) {
rotation.potential_gradient_directional_derivative(
mappingVariation.mapping.physical_position_variation, potentialGradientVariation
);
centrifugalAccelerationVariation = potentialGradientVariation;
centrifugalAccelerationVariation *= -1.0;
} else {
centrifugalAccelerationVariation = 0.0;
}
weightedForce = 0.0;
if (requestedAction == RotationalDisplacementForceAction::residual) {
weightedForce.Add(baseDensityValue * mappingContext.quadrature.weight, centrifugalAcceleration);
} else {
if (needsDensityVariation) {
weightedForce.Add(
densityVariationValue * mappingContext.quadrature.weight, centrifugalAcceleration
);
}
if (needsDisplacementVariation) {
weightedForce.Add(
baseDensityValue * mappingContext.quadrature.weight, centrifugalAccelerationVariation
);
weightedForce.Add(
baseDensityValue * mappingVariation.weight_variation, centrifugalAcceleration
);
}
}
for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) {
for (int component = 0; component < dimension; ++component) {
const int vectorDof = vector_dof_index(
displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension
);
const double contribution = displacementShape(scalarDof) * weightedForce(component);
if (!std::isfinite(contribution)) {
if (!reportCandidateRejection) {
MFEM_VERIFY(
false, "The rotational-displacement-force kernel "
"encountered a non-finite contribution."
);
}
candidateRejection = non_finite_rejection();
continue;
}
elementAction(vectorDof) += contribution;
}
}
}
if (displacementDofTransformation != nullptr) {
displacementDofTransformation->TransformDual(elementAction);
}
localAction.AddElementVector(displacementDofs, elementAction);
}
if (reportCandidateRejection && !vector_is_finite(localAction)) {
candidateRejection = non_finite_rejection();
}
if (reportCandidateRejection) {
auto synchronized = synchronize_rejection(candidateRejection, f.mesh->GetComm());
if (!synchronized.has_value()) {
return synchronized;
}
}
local_to_true(*f.displacementFes, localAction, actionTrue);
if (reportCandidateRejection) {
const std::optional<Rejection> outputRejection = vector_is_finite(actionTrue)
? std::optional<Rejection>{}
: std::optional<Rejection>{non_finite_rejection()};
auto synchronized = synchronize_rejection(outputRejection, f.mesh->GetComm());
if (!synchronized.has_value()) {
return synchronized;
}
}
return {};
}
} // namespace
namespace mean_field::operators::kernels {
RotationalDisplacementForceResult try_apply_rotational_displacement_force_residual(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &densityTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
) {
return apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::residual, &densityTrue, nullptr, nullptr,
displacementTrue, residualTrue, true
);
}
void apply_rotational_displacement_force_residual(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &densityTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
) {
auto result = try_apply_rotational_displacement_force_residual(
f, domainMapper, rotation, densityTrue, displacementTrue, residualTrue
);
if (!result.has_value()) {
throw_rejection(result.error());
}
}
void apply_rotational_displacement_force_density_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
(void)apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::density, nullptr, &densityVariationTrue,
nullptr, displacementTrue, actionTrue, false
);
}
void apply_rotational_displacement_force_displacement_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &baseDensityTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
(void)apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::displacement, &baseDensityTrue, nullptr,
&displacementVariationTrue, displacementTrue, actionTrue, false
);
}
void apply_rotational_displacement_force_complete_action(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const physics::RigidRotation &rotation,
const mfem::Vector &baseDensityTrue,
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
(void)apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::complete, &baseDensityTrue,
&densityVariationTrue, &displacementVariationTrue, displacementTrue, actionTrue, false
);
}
} // namespace mean_field::operators::kernels

View File

@@ -0,0 +1,724 @@
module;
#include <algorithm>
#include <array>
#include <cmath>
#include <expected>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
import :operators.prepared_angular_momentum;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] bool is_vacuum_attribute(const int attribute) {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
[[nodiscard]] bool is_finite_vector(const mfem::Vector &vector) {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
void verify_finite_vector(
const mfem::Vector &vector,
const char *message
) {
MFEM_VERIFY(is_finite_vector(vector), message);
}
[[nodiscard]] bool is_candidate_mapping_failure(const mean_field::mapping::MappingStatus status) {
using mean_field::mapping::MappingStatus;
return status == MappingStatus::non_finite_input || status == MappingStatus::non_finite_result ||
status == MappingStatus::non_positive_determinant;
}
[[nodiscard]] std::optional<mean_field::mapping::MappingStatus> synchronize_mapping_failure(
const std::optional<mean_field::mapping::MappingStatus> localFailure,
const MPI_Comm communicator
) {
int localFailures[2]{0, 0};
if (localFailure.has_value()) {
const int encodedStatus = static_cast<int>(*localFailure) + 1;
if (is_candidate_mapping_failure(*localFailure)) {
localFailures[0] = encodedStatus;
} else {
localFailures[1] = encodedStatus;
}
}
int globalFailures[2]{0, 0};
if (MPI_Allreduce(localFailures, globalFailures, 2, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("PreparedAngularMomentumOperator could not synchronize mapped-geometry validity.");
}
if (globalFailures[1] != 0) {
throw std::runtime_error(
"PreparedAngularMomentumOperator encountered a structural mapping failure with status " +
std::to_string(globalFailures[1] - 1) + "."
);
}
if (globalFailures[0] == 0) {
return std::nullopt;
}
return static_cast<mean_field::mapping::MappingStatus>(globalFailures[0] - 1);
}
[[nodiscard]] bool synchronize_non_finite_failure(
const bool localFailure,
const MPI_Comm communicator,
const char *operation
) {
const int localStatus = localFailure ? 1 : 0;
int globalStatus = 0;
if (MPI_Allreduce(&localStatus, &globalStatus, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error(
std::string("PreparedAngularMomentumOperator could not synchronize ") + operation + "."
);
}
return globalStatus != 0;
}
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
mfem::Vector &localVector
) {
MFEM_VERIFY(trueVector.Size() == finiteElementSpace.GetTrueVSize(), "True vector has the wrong size.");
localVector.SetSize(finiteElementSpace.GetVSize());
const mfem::Operator *prolongation = finiteElementSpace.GetProlongationMatrix();
if (prolongation != nullptr) {
prolongation->Mult(trueVector, localVector);
} else {
localVector = trueVector;
}
}
[[nodiscard]] const mfem::IntegrationRule &get_moment_of_inertia_rule(
const mean_field::fem::FEM &f,
const mfem::FiniteElement &densityElement,
const mfem::ElementTransformation &transformation
) {
using DensityField = mean_field::field::Field<mean_field::field::Density>;
MFEM_VERIFY(
densityElement.GetOrder() == mean_field::field::Density::Scalar::familyOrder,
"The angular-momentum element does not match the registered density field."
);
const mean_field::quadrature::Query query =
DensityField::make_query<mean_field::field::Density::Form::Quadrupole>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array<int, 1>{2},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
const auto resolution = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
resolution.integration_rule != nullptr,
"The quadrature policy did not return an angular-momentum integration rule."
);
return *resolution.integration_rule;
}
void validate_shared_gravity_revisions(
const mean_field::operators::context::gravity_field::GravityFieldLinearizationContext &gravityContext,
const mean_field::operators::AngularMomentumDependencies &dependencies
) {
MFEM_VERIFY(
gravityContext.IsPrepared(),
"PreparedAngularMomentumOperator requires the shared gravity context to be prepared first."
);
const auto &revisions = gravityContext.GetRevisions();
MFEM_VERIFY(
revisions.discretization.value == dependencies.discretization.revision &&
revisions.density.value == dependencies.density.revision &&
revisions.displacement.value == dependencies.displacement.revision,
"PreparedAngularMomentumOperator received revisions that do not match the shared gravity context."
);
}
void validate_identity_transition(
const mean_field::operators::AngularMomentumDependencyStamp &prepared,
const mean_field::operators::AngularMomentumDependencyStamp &requested,
const char *message
) {
MFEM_VERIFY(prepared.identity == requested.identity || prepared.revision != requested.revision, message);
}
} // namespace
namespace mean_field::operators {
PreparedAngularMomentumOperator::PreparedAngularMomentumOperator(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const context::gravity_field::GravityFieldLinearizationContext &gravityContext,
models::CompiledFixedAngularMomentum constraint
)
: m_fem(f),
m_domainMapper(domainMapper),
m_gravityContext(gravityContext),
m_constraint(std::move(constraint)) {
MFEM_VERIFY(m_fem.mesh != nullptr, "PreparedAngularMomentumOperator requires a mesh.");
MFEM_VERIFY(
m_fem.mesh->Dimension() == 3 && m_domainMapper.GetDimension() == 3,
"PreparedAngularMomentumOperator currently requires a three-dimensional mapped domain."
);
MFEM_VERIFY(
m_fem.densityFes != nullptr && m_fem.displacementFes != nullptr && m_fem.compactificationFes != nullptr &&
m_fem.compactificationCoordinate != nullptr && m_fem.quadratureFactory != nullptr,
"PreparedAngularMomentumOperator requires density, displacement, compactification, and quadrature data."
);
MFEM_VERIFY(
m_gravityContext.GetDensityMap().full_size() == m_fem.densityFes->GetTrueVSize() &&
m_gravityContext.GetDisplacementMap().full_size() == m_fem.displacementFes->GetTrueVSize(),
"PreparedAngularMomentumOperator received incompatible shared FieldDof maps."
);
m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size());
m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size());
}
PreparedAngularMomentumReport PreparedAngularMomentumOperator::Prepare(
const double angularVelocity,
const AngularMomentumDependencies &dependencies
) {
auto result = TryPrepare(angularVelocity, dependencies);
if (!result.has_value()) {
throwAngularMomentumPreparationRejection(result.error());
}
return std::move(result).value();
}
AngularMomentumPreparationResult PreparedAngularMomentumOperator::TryPrepare(
const double angularVelocity,
const AngularMomentumDependencies &dependencies
) {
validate_shared_gravity_revisions(m_gravityContext, dependencies);
if (m_isPrepared) {
validate_identity_transition(
m_preparedDependencies.discretization, dependencies.discretization,
"A new angular-momentum discretization identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.density, dependencies.density,
"A new angular-momentum density identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.displacement, dependencies.displacement,
"A new angular-momentum displacement identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.rotation, dependencies.rotation,
"A new angular-momentum rotation identity must change its revision."
);
}
const bool rebuildStaticPlan =
!m_isPrepared || dependencies.discretization != m_preparedDependencies.discretization;
const bool refreshGeometry =
rebuildStaticPlan || dependencies.displacement != m_preparedDependencies.displacement;
const bool refreshDensity = rebuildStaticPlan || dependencies.density != m_preparedDependencies.density;
const bool updateAngularVelocity = !m_isPrepared || dependencies.rotation != m_preparedDependencies.rotation ||
angularVelocity != m_angularVelocity;
m_isPrepared = false;
if (synchronize_non_finite_failure(
!std::isfinite(angularVelocity), m_fem.mesh->GetComm(), "angular-velocity validity"
)) {
return std::unexpected(
AngularMomentumPreparationRejection{
.reason = AngularMomentumPreparationRejectionReason::non_finite_angular_velocity
}
);
}
PreparedAngularMomentumReport report;
if (rebuildStaticPlan) {
BuildStaticPlan();
report.rebuiltStaticPlan = true;
}
if (refreshGeometry) {
const auto mappingFailure = synchronize_mapping_failure(
RefreshGeometry(m_gravityContext.GetGeometryContext().GetDisplacementTrue()), m_fem.mesh->GetComm()
);
if (mappingFailure.has_value()) {
const auto reason = *mappingFailure == mapping::MappingStatus::non_positive_determinant
? AngularMomentumPreparationRejectionReason::inverted_geometry
: AngularMomentumPreparationRejectionReason::non_finite_geometry;
return std::unexpected(
AngularMomentumPreparationRejection{.reason = reason, .mappingStatus = *mappingFailure}
);
}
report.refreshedGeometry = true;
}
if (refreshDensity) {
if (synchronize_non_finite_failure(
!RefreshDensity(m_gravityContext.GetDensityTrue()), m_fem.mesh->GetComm(),
"interpolated-density validity"
)) {
return std::unexpected(
AngularMomentumPreparationRejection{
.reason = AngularMomentumPreparationRejectionReason::non_finite_density
}
);
}
report.refreshedDensity = true;
}
if (updateAngularVelocity) {
m_angularVelocity = angularVelocity;
report.updatedAngularVelocity = true;
}
if (refreshGeometry || refreshDensity || updateAngularVelocity) {
auto rejection = TryAssembleResidual();
if (rejection.has_value()) {
return std::unexpected(*rejection);
}
report.assembledResidual = true;
}
m_preparedDependencies = dependencies;
m_isPrepared = true;
return report;
}
void PreparedAngularMomentumOperator::BuildStaticPlan() {
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
int localStellarElementCount = 0;
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId);
MFEM_VERIFY(transformation != nullptr, "Angular-momentum preparation received a null transformation.");
if (is_vacuum_attribute(transformation->Attribute)) {
continue;
}
++localStellarElementCount;
m_elements.emplace_back();
ElementPAData &data = m_elements.back();
data.elementId = elementId;
data.densityDofTransformation = m_fem.densityFes->GetElementDofs(elementId, data.densityDofs);
data.displacementDofTransformation =
m_fem.displacementFes->GetElementVDofs(elementId, data.displacementDofs);
data.compactificationDofTransformation =
m_fem.compactificationFes->GetElementDofs(elementId, data.compactificationDofs);
const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(elementId);
const mfem::IntegrationRule &integrationRule =
get_moment_of_inertia_rule(m_fem, densityElement, *transformation);
data.integrationRule = &integrationRule;
data.densityBasis = m_fem.GetReferenceTables().GetScalarTable(densityElement, integrationRule);
data.mappingContexts.SetSize(integrationRule.GetNPoints(), m_fem.mesh->Dimension());
data.density.SetSize(integrationRule.GetNPoints());
data.quadratureWeights.SetSize(integrationRule.GetNPoints());
data.cylindricalRadiusSquared.SetSize(integrationRule.GetNPoints());
}
int globalStellarElementCount = 0;
MFEM_VERIFY(
MPI_Allreduce(
&localStellarElementCount, &globalStellarElementCount, 1, MPI_INT, MPI_SUM, m_fem.mesh->GetComm()
) == MPI_SUCCESS,
"PreparedAngularMomentumOperator could not count stellar elements."
);
MFEM_VERIFY(globalStellarElementCount > 0, "PreparedAngularMomentumOperator found no stellar elements.");
}
std::optional<mapping::MappingStatus>
PreparedAngularMomentumOperator::RefreshGeometry(const mfem::Vector &displacement) {
MFEM_VERIFY(
displacement.Size() == m_fem.displacementFes->GetTrueVSize(),
"Angular-momentum geometry has the wrong displacement size."
);
if (!is_finite_vector(displacement)) {
return mapping::MappingStatus::non_finite_input;
}
mfem::Vector displacementLocal;
true_to_local(*m_fem.displacementFes, displacement, displacementLocal);
if (!is_finite_vector(displacementLocal)) {
return mapping::MappingStatus::non_finite_result;
}
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingContext mappingContext;
for (ElementPAData &data : m_elements) {
displacementLocal.GetSubVector(data.displacementDofs, data.baseDisplacement);
m_fem.compactificationCoordinate->GetSubVector(data.compactificationDofs, data.compactification);
if (data.displacementDofTransformation != nullptr) {
data.displacementDofTransformation->InvTransformPrimal(data.baseDisplacement);
}
if (data.compactificationDofTransformation != nullptr) {
data.compactificationDofTransformation->InvTransformPrimal(data.compactification);
}
if (!is_finite_vector(data.baseDisplacement)) {
return mapping::MappingStatus::non_finite_result;
}
MFEM_VERIFY(
is_finite_vector(data.compactification),
"Angular-momentum preparation encountered invalid static compactification data."
);
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId);
const mapping::ElementDisplacementData displacementData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement);
const mapping::ElementCompactificationData compactificationData(
compactificationElement, data.compactification
);
const mapping::ElementMappingData mappingData{
.displacement = displacementData, .compactification = compactificationData
};
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) {
const mapping::MappingStatus status = m_domainMapper.EvaluateVolume(
mappingData, *transformation, data.integrationRule->IntPoint(quadraturePoint), workspace,
mappingContext
);
if (status != mapping::MappingStatus::valid) {
return status;
}
if (mappingContext.mapping.compactified) {
return mapping::MappingStatus::at_compactified_infinity;
}
data.cylindricalRadiusSquared(quadraturePoint) =
CylindricalRadiusSquared(mappingContext.mapping.physical_position);
if (!std::isfinite(data.cylindricalRadiusSquared(quadraturePoint))) {
return mapping::MappingStatus::non_finite_result;
}
data.mappingContexts.Store(quadraturePoint, mappingContext);
data.quadratureWeights(quadraturePoint) = mappingContext.quadrature.weight;
}
}
return std::nullopt;
}
bool PreparedAngularMomentumOperator::RefreshDensity(const mfem::Vector &density) {
MFEM_VERIFY(density.Size() == m_fem.densityFes->GetTrueVSize(), "Angular-momentum density has the wrong size.");
if (!is_finite_vector(density)) {
return false;
}
mfem::Vector densityLocal;
true_to_local(*m_fem.densityFes, density, densityLocal);
if (!is_finite_vector(densityLocal)) {
return false;
}
mfem::Vector elementDensity;
for (ElementPAData &data : m_elements) {
densityLocal.GetSubVector(data.densityDofs, elementDensity);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->InvTransformPrimal(elementDensity);
}
if (!is_finite_vector(elementDensity)) {
return false;
}
data.densityBasis->GetValues().Mult(elementDensity, data.density);
if (!is_finite_vector(data.density)) {
return false;
}
}
return true;
}
std::optional<AngularMomentumPreparationRejection> PreparedAngularMomentumOperator::TryAssembleResidual() {
double localMomentOfInertia = 0.0;
for (const ElementPAData &data : m_elements) {
for (int quadraturePoint = 0; quadraturePoint < data.density.Size(); ++quadraturePoint) {
localMomentOfInertia += data.density(quadraturePoint) * data.cylindricalRadiusSquared(quadraturePoint) *
data.quadratureWeights(quadraturePoint);
}
}
m_momentOfInertia = GlobalSum(localMomentOfInertia);
if (!std::isfinite(m_momentOfInertia)) {
return AngularMomentumPreparationRejection{
.reason = AngularMomentumPreparationRejectionReason::non_finite_moment_of_inertia,
.momentOfInertia = m_momentOfInertia
};
}
if (m_momentOfInertia < 0.0) {
return AngularMomentumPreparationRejection{
.reason = AngularMomentumPreparationRejectionReason::negative_moment_of_inertia,
.momentOfInertia = m_momentOfInertia
};
}
MFEM_VERIFY(
std::isfinite(m_constraint.targetAngularMomentum().value()),
"PreparedAngularMomentumOperator has a non-finite configured target angular momentum."
);
m_currentAngularMomentum = m_angularVelocity * m_momentOfInertia;
m_cachedResidual.SetSize(1);
m_cachedResidual(0) = m_currentAngularMomentum - m_constraint.targetAngularMomentum().value();
if (!std::isfinite(m_currentAngularMomentum) || !std::isfinite(m_cachedResidual(0))) {
return AngularMomentumPreparationRejection{
.reason = AngularMomentumPreparationRejectionReason::non_finite_residual,
.momentOfInertia = m_momentOfInertia
};
}
++m_preparationCount;
return std::nullopt;
}
void PreparedAngularMomentumOperator::BuildResidual(mfem::Vector &residual) const {
VerifyPrepared();
residual = m_cachedResidual;
++m_residualApplicationCount;
}
double
PreparedAngularMomentumOperator::EvaluateDensityMomentActionLocal(const mfem::Vector &densityVariation) const {
MFEM_VERIFY(
densityVariation.Size() == m_fem.densityFes->GetTrueVSize(),
"Angular-momentum density action has the wrong true-vector size."
);
true_to_local(*m_fem.densityFes, densityVariation, m_densityVariationLocal);
mfem::Vector quadratureDensityVariation;
double localAction = 0.0;
for (const ElementPAData &data : m_elements) {
m_densityVariationLocal.GetSubVector(data.densityDofs, m_elementDensityVariation);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->InvTransformPrimal(m_elementDensityVariation);
}
quadratureDensityVariation.SetSize(data.integrationRule->GetNPoints());
data.densityBasis->GetValues().Mult(m_elementDensityVariation, quadratureDensityVariation);
for (int quadraturePoint = 0; quadraturePoint < quadratureDensityVariation.Size(); ++quadraturePoint) {
localAction += quadratureDensityVariation(quadraturePoint) *
data.cylindricalRadiusSquared(quadraturePoint) * data.quadratureWeights(quadraturePoint);
}
}
return localAction;
}
double PreparedAngularMomentumOperator::EvaluateDisplacementMomentActionLocal(
const mfem::Vector &displacementVariation
) const {
MFEM_VERIFY(
displacementVariation.Size() == m_fem.displacementFes->GetTrueVSize(),
"Angular-momentum displacement action has the wrong true-vector size."
);
true_to_local(*m_fem.displacementFes, displacementVariation, m_displacementVariationLocal);
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingVariation variation;
mapping::VolumeMappingContext mappingContext;
double localAction = 0.0;
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 mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId);
const mapping::ElementDisplacementData baseDisplacementData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement);
const mapping::ElementDisplacementData directionData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, m_elementDisplacementVariation);
const mapping::ElementCompactificationData compactificationData(
compactificationElement, data.compactification
);
const mapping::ElementMappingData mappingData{
.displacement = baseDisplacementData, .compactification = compactificationData
};
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) {
data.mappingContexts.Load(quadraturePoint, mappingContext);
const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation(
mappingData, directionData, *transformation, data.integrationRule->IntPoint(quadraturePoint),
mappingContext, workspace, variation
);
MFEM_VERIFY(
status == mapping::MappingStatus::valid,
"Mapped angular-momentum variation is invalid. Element: " << data.elementId
);
const double radiusSquaredVariation = CylindricalRadiusSquaredVariation(
mappingContext.mapping.physical_position, variation.mapping.physical_position_variation
);
localAction += data.density(quadraturePoint) *
(radiusSquaredVariation * data.quadratureWeights(quadraturePoint) +
data.cylindricalRadiusSquared(quadraturePoint) * variation.weight_variation);
}
}
return localAction;
}
void PreparedAngularMomentumOperator::ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
densityVariation.Size() == m_gravityContext.GetDensityMap().reduced_size(),
"Angular-momentum density action has the wrong reduced size."
);
verify_finite_vector(densityVariation, "Angular-momentum density direction is non-finite.");
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
action.SetSize(1);
action(0) = m_angularVelocity * GlobalSum(EvaluateDensityMomentActionLocal(m_densityVariationTrue));
++m_actionStatistics.densityApplications;
}
void PreparedAngularMomentumOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(),
"Angular-momentum displacement action has the wrong reduced size."
);
verify_finite_vector(displacementVariation, "Angular-momentum displacement direction is non-finite.");
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
action.SetSize(1);
action(0) = m_angularVelocity * GlobalSum(EvaluateDisplacementMomentActionLocal(m_displacementVariationTrue));
++m_actionStatistics.displacementApplications;
}
void PreparedAngularMomentumOperator::ApplyAngularVelocityJacobianAction(
const double angularVelocityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(std::isfinite(angularVelocityVariation), "Angular-velocity direction is non-finite.");
action.SetSize(1);
action(0) = m_momentOfInertia * angularVelocityVariation;
++m_actionStatistics.angularVelocityApplications;
}
void PreparedAngularMomentumOperator::ApplyCompleteJacobianAction(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
const double angularVelocityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
MFEM_VERIFY(
densityVariation.Size() == m_gravityContext.GetDensityMap().reduced_size() &&
displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(),
"Angular-momentum complete action has incompatible reduced coordinates."
);
verify_finite_vector(densityVariation, "Angular-momentum density direction is non-finite.");
verify_finite_vector(displacementVariation, "Angular-momentum displacement direction is non-finite.");
MFEM_VERIFY(std::isfinite(angularVelocityVariation), "Angular-velocity direction is non-finite.");
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
const double localMomentAction = EvaluateDensityMomentActionLocal(m_densityVariationTrue) +
EvaluateDisplacementMomentActionLocal(m_displacementVariationTrue);
action.SetSize(1);
action(0) = m_angularVelocity * GlobalSum(localMomentAction) + m_momentOfInertia * angularVelocityVariation;
++m_actionStatistics.completeApplications;
}
double
PreparedAngularMomentumOperator::CylindricalRadiusSquared(const mfem::Vector &physicalPosition) const noexcept {
const auto &axis = m_constraint.specification().axis();
const auto &center = m_constraint.specification().center();
double radiusSquared = 0.0;
double axialPosition = 0.0;
for (int component = 0; component < 3; ++component) {
const double relative = physicalPosition(component) - center[static_cast<std::size_t>(component)];
radiusSquared += relative * relative;
axialPosition += axis[static_cast<std::size_t>(component)] * relative;
}
const double perpendicularRadiusSquared = radiusSquared - axialPosition * axialPosition;
if (!std::isfinite(perpendicularRadiusSquared)) {
return perpendicularRadiusSquared;
}
return std::max(0.0, perpendicularRadiusSquared);
}
double PreparedAngularMomentumOperator::CylindricalRadiusSquaredVariation(
const mfem::Vector &physicalPosition,
const mfem::Vector &physicalPositionVariation
) const noexcept {
const auto &axis = m_constraint.specification().axis();
const auto &center = m_constraint.specification().center();
double relativeDotVariation = 0.0;
double axialPosition = 0.0;
double axialVariation = 0.0;
for (int component = 0; component < 3; ++component) {
const double relative = physicalPosition(component) - center[static_cast<std::size_t>(component)];
relativeDotVariation += relative * physicalPositionVariation(component);
axialPosition += axis[static_cast<std::size_t>(component)] * relative;
axialVariation += axis[static_cast<std::size_t>(component)] * physicalPositionVariation(component);
}
return 2.0 * (relativeDotVariation - axialPosition * axialVariation);
}
double PreparedAngularMomentumOperator::GlobalSum(const double localValue) const {
double globalValue = 0.0;
if (MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_fem.mesh->GetComm()) != MPI_SUCCESS) {
throw std::runtime_error("PreparedAngularMomentumOperator could not reduce the moment of inertia.");
}
return globalValue;
}
bool PreparedAngularMomentumOperator::IsPrepared() const noexcept {
if (!m_isPrepared || !m_gravityContext.IsPrepared()) {
return false;
}
const auto &revisions = m_gravityContext.GetRevisions();
return revisions.discretization.value == m_preparedDependencies.discretization.revision &&
revisions.density.value == m_preparedDependencies.density.revision &&
revisions.displacement.value == m_preparedDependencies.displacement.revision;
}
double PreparedAngularMomentumOperator::GetMomentOfInertia() const {
VerifyPrepared();
return m_momentOfInertia;
}
double PreparedAngularMomentumOperator::GetAngularVelocity() const {
VerifyPrepared();
return m_angularVelocity;
}
double PreparedAngularMomentumOperator::GetCurrentAngularMomentum() const {
VerifyPrepared();
return m_currentAngularMomentum;
}
double PreparedAngularMomentumOperator::GetTargetAngularMomentum() const noexcept {
return m_constraint.targetAngularMomentum().value();
}
physics::RigidRotation PreparedAngularMomentumOperator::GetRotation() const {
VerifyPrepared();
return m_constraint.makeRotation(m_angularVelocity);
}
AngularMomentumConstraintReport PreparedAngularMomentumOperator::GetConstraintReport() const {
VerifyPrepared();
const double target = GetTargetAngularMomentum();
const double residual = m_currentAngularMomentum - target;
return {
.targetAngularMomentum = target,
.achievedAngularMomentum = m_currentAngularMomentum,
.momentOfInertia = m_momentOfInertia,
.angularVelocity = m_angularVelocity,
.dimensionalResidual = residual,
.scaledResidual = residual / std::max(std::abs(target), 1.0e-300)
};
}
std::uint64_t PreparedAngularMomentumOperator::GetPreparationCount() const noexcept {
return m_preparationCount;
}
std::uint64_t PreparedAngularMomentumOperator::GetResidualApplicationCount() const noexcept {
return m_residualApplicationCount;
}
const PreparedAngularMomentumActionStatistics &
PreparedAngularMomentumOperator::GetActionStatistics() const noexcept {
return m_actionStatistics;
}
const models::CompiledFixedAngularMomentum &
PreparedAngularMomentumOperator::GetCompiledConstraint() const noexcept {
return m_constraint;
}
void PreparedAngularMomentumOperator::VerifyPrepared() const {
MFEM_VERIFY(IsPrepared(), "The angular-momentum invariant must be prepared before application.");
}
} // namespace mean_field::operators

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,685 @@
module;
#include <cmath>
#include <expected>
#include <mfem.hpp>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mpi.h>
module mean_field;
import :operators.prepared_displacement_residual;
namespace {
using Dependencies = mean_field::operators::DisplacementResidualDependencies;
using Rejection = mean_field::operators::DisplacementResidualPreparationRejection;
using Source = mean_field::operators::DisplacementResidualPreparationRejectionSource;
using Reason = mean_field::operators::DisplacementResidualPreparationRejectionReason;
[[nodiscard]] Rejection
pressure_rejection(const mean_field::operators::PressureForcePreparationRejection &rejection) noexcept {
using PressureReason = mean_field::operators::PressureForcePreparationRejectionReason;
switch (rejection.reason) {
case PressureReason::equation_of_state:
return {
.source = Source::pressure,
.reason = Reason::equation_of_state,
.equationOfStateCode = rejection.equationOfStateCode
};
case PressureReason::invalid_mapping:
return {
.source = Source::pressure, .reason = Reason::invalid_mapping, .mappingStatus = rejection.mappingStatus
};
case PressureReason::non_finite_arithmetic:
default:
return {.source = Source::pressure, .reason = Reason::non_finite_arithmetic};
}
}
[[nodiscard]] Rejection
gravity_rejection(const mean_field::operators::kernels::GravityDisplacementForceRejection &rejection) noexcept {
if (rejection.reason ==
mean_field::operators::kernels::GravityDisplacementForceRejectionReason::invalid_mapping) {
return {
.source = Source::gravity, .reason = Reason::invalid_mapping, .mappingStatus = rejection.mappingStatus
};
}
return {.source = Source::gravity, .reason = Reason::non_finite_arithmetic};
}
[[nodiscard]] Rejection
rotation_rejection(const mean_field::operators::kernels::RotationalDisplacementForceRejection &rejection) noexcept {
if (rejection.reason ==
mean_field::operators::kernels::RotationalDisplacementForceRejectionReason::invalid_mapping) {
return {
.source = Source::rotation, .reason = Reason::invalid_mapping, .mappingStatus = rejection.mappingStatus
};
}
return {.source = Source::rotation, .reason = Reason::non_finite_arithmetic};
}
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) noexcept {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
[[noreturn]] void throw_rejection(const Rejection &rejection) {
switch (rejection.reason) {
case Reason::equation_of_state:
throw mean_field::eos::EvaluationError(
rejection.equationOfStateCode,
"PreparedDisplacementResidualOperator encountered invalid thermodynamic data."
);
case Reason::invalid_mapping:
throw std::domain_error("PreparedDisplacementResidualOperator encountered an invalid mapped domain.");
case Reason::non_finite_arithmetic:
default:
throw std::domain_error("PreparedDisplacementResidualOperator produced non-finite arithmetic.");
}
}
[[nodiscard]] mean_field::operators::context::pressure_force::PressureForceDependencies
make_pressure_dependencies(const Dependencies &dependencies) {
return {
.discretization =
{.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision},
.enthalpy = {.identity = dependencies.enthalpy.identity, .revision = dependencies.enthalpy.revision},
.displacement = {
.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision
}
};
}
[[nodiscard]] mean_field::operators::context::rotational_displacement_force::RotationalDisplacementForceDependencies
make_rotational_dependencies(const Dependencies &dependencies) {
return {
.discretization =
{.identity = dependencies.discretization.identity, .revision = dependencies.discretization.revision},
.density = {.identity = dependencies.density.identity, .revision = dependencies.density.revision},
.displacement =
{.identity = dependencies.displacement.identity, .revision = dependencies.displacement.revision},
.rotation = {.identity = dependencies.rotation.identity, .revision = dependencies.rotation.revision}
};
}
void validate_shared_gravity_revisions(
const mean_field::operators::context::gravity_field::GravityFieldLinearizationContext &gravityContext,
const Dependencies &dependencies
) {
MFEM_VERIFY(
gravityContext.IsPrepared(), "PreparedDisplacementResidualOperator requires the shared "
"gravity linearization context to be prepared first."
);
const mean_field::operators::context::gravity_field::GravityFieldRevisions &gravityRevisions =
gravityContext.GetRevisions();
MFEM_VERIFY(
gravityRevisions.discretization.value == dependencies.discretization.revision &&
gravityRevisions.density.value == dependencies.density.revision &&
gravityRevisions.displacement.value == dependencies.displacement.revision &&
gravityRevisions.gravity_gradient.value == dependencies.gravityGradient.revision,
"PreparedDisplacementResidualOperator received dependency "
"revisions that do not match the shared gravity context."
);
}
void validate_shared_identity_transition(
const mean_field::operators::DisplacementResidualDependencyStamp &prepared,
const mean_field::operators::DisplacementResidualDependencyStamp &requested,
const char *message
) {
MFEM_VERIFY(prepared.identity == requested.identity || prepared.revision != requested.revision, message);
}
void add_compatible(
mfem::Vector &destination,
const mfem::Vector &source,
const char *message
) {
MFEM_VERIFY(destination.Size() == source.Size(), message);
destination += source;
}
} // namespace
namespace mean_field::operators {
PreparedDisplacementResidualOperator::PreparedDisplacementResidualOperator(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const eos::Polytrope &barotrope,
const context::gravity_field::GravityFieldLinearizationContext &gravityContext
)
: m_fem(f),
m_domainMapper(domainMapper),
m_gravityContext(gravityContext),
m_pressureOperator(
f,
domainMapper,
barotrope
),
m_gravityOperator(
f,
domainMapper,
gravityContext
),
m_rotationalOperator(
f,
domainMapper
) {
MFEM_VERIFY(m_fem.mesh != nullptr, "PreparedDisplacementResidualOperator requires a mesh.");
MFEM_VERIFY(
m_fem.densityFes != nullptr && m_fem.displacementFes != nullptr && m_fem.gravityFluxFes != nullptr &&
m_fem.enthalpyFes != nullptr,
"PreparedDisplacementResidualOperator requires density, "
"displacement, gravity-gradient, and enthalpy finite-element "
"spaces."
);
MFEM_VERIFY(
m_domainMapper.GetDimension() == m_fem.mesh->Dimension(),
"PreparedDisplacementResidualOperator received a mapper with "
"the wrong dimension."
);
}
PreparedDisplacementResidualReport PreparedDisplacementResidualOperator::Prepare(
const DisplacementResidualStateView &state,
const DisplacementResidualDependencies &dependencies,
const physics::RigidRotation &rotation
) {
auto result = TryPrepare(state, dependencies, rotation);
if (!result.has_value()) {
throw_rejection(result.error());
}
return std::move(result).value();
}
std::expected<
PreparedDisplacementResidualReport,
DisplacementResidualPreparationRejection>
PreparedDisplacementResidualOperator::TryPrepare(
const DisplacementResidualStateView &state,
const DisplacementResidualDependencies &dependencies,
const physics::RigidRotation &rotation
) {
validate_shared_gravity_revisions(m_gravityContext, dependencies);
if (m_isPrepared) {
/*
* GravityFieldLinearizationContext currently tracks revisions
* but not semantic identities. Require an identity replacement
* to be accompanied by a visible revision change so it cannot
* silently reuse the old shared density, geometry, or flux.
*/
validate_shared_identity_transition(
m_preparedDependencies.discretization, dependencies.discretization,
"A new displacement-residual discretization identity must "
"also change the shared gravity revision."
);
validate_shared_identity_transition(
m_preparedDependencies.density, dependencies.density,
"A new displacement-residual density identity must also "
"change the shared gravity revision."
);
validate_shared_identity_transition(
m_preparedDependencies.displacement, dependencies.displacement,
"A new displacement-residual displacement identity must "
"also change the shared gravity revision."
);
validate_shared_identity_transition(
m_preparedDependencies.gravityGradient, dependencies.gravityGradient,
"A new displacement-residual gravity-gradient identity "
"must also change the shared gravity revision."
);
}
const mfem::Vector density = m_gravityContext.GetDensityMap().gather(m_gravityContext.GetDensityTrue());
const mfem::Vector displacement =
m_gravityContext.GetDisplacementMap().gather(m_gravityContext.GetGeometryContext().GetDisplacementTrue());
m_isPrepared = false;
PreparedDisplacementResidualReport report;
auto pressureResult = m_pressureOperator.TryPrepare(
{.enthalpy = state.enthalpy, .displacement = displacement}, make_pressure_dependencies(dependencies)
);
if (!pressureResult.has_value()) {
return std::unexpected(pressure_rejection(pressureResult.error()));
}
report.pressure = std::move(pressureResult).value();
auto gravityResult = m_gravityOperator.TryPrepare();
if (!gravityResult.has_value()) {
return std::unexpected(gravity_rejection(gravityResult.error()));
}
report.gravity = std::move(gravityResult).value();
auto rotationResult = m_rotationalOperator.TryPrepare(
{.density = density, .displacement = displacement}, make_rotational_dependencies(dependencies), rotation
);
if (!rotationResult.has_value()) {
return std::unexpected(rotation_rejection(rotationResult.error()));
}
report.rotation = std::move(rotationResult).value();
if (report.DidAnyChildWork() ||
m_cachedResidual.Size() != m_gravityContext.GetDisplacementMap().reduced_size()) {
const auto localAssemblyRejection = AssembleResidual();
const int localRejected = localAssemblyRejection.has_value() ? 1 : 0;
int globallyRejected = 0;
if (MPI_Allreduce(
&localRejected, &globallyRejected, 1, MPI_INT, MPI_MAX, m_fem.displacementFes->GetComm()
) != MPI_SUCCESS) {
throw std::runtime_error(
"PreparedDisplacementResidualOperator could not synchronize residual validity."
);
}
if (globallyRejected != 0) {
return std::unexpected(
Rejection{.source = Source::composition, .reason = Reason::non_finite_arithmetic}
);
}
report.assembledResidual = true;
++m_residualPreparationCount;
}
MFEM_VERIFY(
m_cachedResidual.Size() == m_gravityContext.GetDisplacementMap().reduced_size(),
"PreparedDisplacementResidualOperator produced a cached "
"residual with the wrong size."
);
m_preparedDependencies = dependencies;
m_isPrepared = true;
return report;
}
std::optional<DisplacementResidualPreparationRejection> PreparedDisplacementResidualOperator::AssembleResidual() {
mfem::Vector pressureResidual;
mfem::Vector gravityResidual;
mfem::Vector rotationalResidual;
m_pressureOperator.BuildResidual(pressureResidual);
m_gravityOperator.BuildResidual(gravityResidual);
m_rotationalOperator.BuildResidual(rotationalResidual);
m_cachedResidual = pressureResidual;
add_compatible(
m_cachedResidual, gravityResidual,
"Cannot combine pressure and gravity displacement residuals "
"with different sizes."
);
add_compatible(
m_cachedResidual, rotationalResidual,
"Cannot combine mechanical displacement residuals with "
"different sizes."
);
if (!vector_is_finite(m_cachedResidual)) {
return Rejection{.source = Source::composition, .reason = Reason::non_finite_arithmetic};
}
return std::nullopt;
}
void PreparedDisplacementResidualOperator::BuildResidual(mfem::Vector &residual) const {
VerifyPrepared();
residual = m_cachedResidual;
++m_residualApplicationCount;
}
void PreparedDisplacementResidualOperator::ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
mfem::Vector rotationalAction;
m_gravityOperator.ApplyDensityJacobianAction(densityVariation, action);
m_rotationalOperator.ApplyDensityJacobianAction(densityVariation, rotationalAction);
add_compatible(
action, rotationalAction,
"Cannot combine gravity and rotation density-column actions "
"with different sizes."
);
++m_actionStatistics.densityApplications;
}
void PreparedDisplacementResidualOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
VerifyPrepared();
mfem::Vector gravityAction;
mfem::Vector rotationalAction;
m_pressureOperator.ApplyDisplacementJacobianAction(displacementVariation, action);
m_gravityOperator.ApplyDisplacementJacobianAction(displacementVariation, gravityAction);
m_rotationalOperator.ApplyDisplacementJacobianAction(displacementVariation, rotationalAction);
add_compatible(
action, gravityAction,
"Cannot combine pressure and gravity displacement-column "
"actions with different sizes."
);
add_compatible(
action, rotationalAction,
"Cannot combine mechanical displacement-column actions with "
"different sizes."
);
++m_actionStatistics.displacementApplications;
}
void PreparedDisplacementResidualOperator::ApplyGravityGradientJacobianAction(
const mfem::Vector &gravityGradientVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_gravityOperator.ApplyGravityGradientJacobianAction(gravityGradientVariation, action);
++m_actionStatistics.gravityGradientApplications;
}
void PreparedDisplacementResidualOperator::ApplyEnthalpyJacobianAction(
const mfem::Vector &enthalpyVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_pressureOperator.ApplyEnthalpyJacobianAction(enthalpyVariation, action);
++m_actionStatistics.enthalpyApplications;
}
void PreparedDisplacementResidualOperator::ApplyCompleteJacobianAction(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
const mfem::Vector &gravityGradientVariation,
const mfem::Vector &enthalpyVariation,
mfem::Vector &action
) const {
VerifyPrepared();
mfem::Vector gravityAction;
mfem::Vector rotationalAction;
m_pressureOperator.ApplyCompleteJacobianAction(enthalpyVariation, displacementVariation, action);
m_gravityOperator.ApplyCompleteJacobianAction(
densityVariation, displacementVariation, gravityGradientVariation, gravityAction
);
m_rotationalOperator.ApplyCompleteJacobianAction(densityVariation, displacementVariation, rotationalAction);
add_compatible(
action, gravityAction,
"Cannot combine pressure and gravity complete Jacobian "
"actions with different sizes."
);
add_compatible(
action, rotationalAction,
"Cannot combine mechanical complete Jacobian actions with "
"different sizes."
);
++m_actionStatistics.densityApplications;
++m_actionStatistics.displacementApplications;
++m_actionStatistics.gravityGradientApplications;
++m_actionStatistics.enthalpyApplications;
++m_actionStatistics.completeApplications;
}
bool PreparedDisplacementResidualOperator::IsPrepared() const noexcept {
if (!m_isPrepared || !m_pressureOperator.IsPrepared() || !m_gravityOperator.IsPrepared() ||
!m_rotationalOperator.IsPrepared() || !m_gravityContext.IsPrepared()) {
return false;
}
const context::gravity_field::GravityFieldRevisions &gravityRevisions = m_gravityContext.GetRevisions();
return gravityRevisions.discretization.value == m_preparedDependencies.discretization.revision &&
gravityRevisions.density.value == m_preparedDependencies.density.revision &&
gravityRevisions.displacement.value == m_preparedDependencies.displacement.revision &&
gravityRevisions.gravity_gradient.value == m_preparedDependencies.gravityGradient.revision;
}
std::uint64_t PreparedDisplacementResidualOperator::GetResidualPreparationCount() const noexcept {
return m_residualPreparationCount;
}
std::uint64_t PreparedDisplacementResidualOperator::GetResidualApplicationCount() const noexcept {
return m_residualApplicationCount;
}
const PreparedDisplacementResidualActionStatistics &
PreparedDisplacementResidualOperator::GetActionStatistics() const noexcept {
return m_actionStatistics;
}
const PreparedPressureForceOperator &PreparedDisplacementResidualOperator::GetPressureOperator() const noexcept {
return m_pressureOperator;
}
const PreparedGravityDisplacementForceOperator &
PreparedDisplacementResidualOperator::GetGravityOperator() const noexcept {
return m_gravityOperator;
}
const PreparedRotationalDisplacementForceOperator &
PreparedDisplacementResidualOperator::GetRotationalOperator() const noexcept {
return m_rotationalOperator;
}
const fem::FEM &PreparedDisplacementResidualOperator::GetFEM() const noexcept {
return m_fem;
}
const context::gravity_field::GravityFieldLinearizationContext &
PreparedDisplacementResidualOperator::GetGravityContext() const noexcept {
return m_gravityContext;
}
void PreparedDisplacementResidualOperator::VerifyPrepared() const {
MFEM_VERIFY(
IsPrepared(), "PreparedDisplacementResidualOperator must be prepared for "
"the current shared gravity-context revisions before residual "
"or Jacobian application."
);
}
PreparedDisplacementResidualJacobianOperator::PreparedDisplacementResidualJacobianOperator(
const DisplacementResidualLayout &layout,
const PreparedDisplacementResidualOperator &preparedOperator
)
: mfem::Operator(
layout.residual_offsets().Last(),
layout.value_offsets().Last()
),
m_layout(layout),
m_preparedOperator(preparedOperator) {
const fem::FEM &f = m_preparedOperator.GetFEM();
MFEM_VERIFY(
f.densityFes != nullptr && f.displacementFes != nullptr && f.gravityFluxFes != nullptr &&
f.gravityPotentialFes != nullptr && f.enthalpyFes != nullptr,
"Prepared displacement-residual MFEM adapter requires every "
"finite-element space in the barotropic equilibrium layout."
);
using Form = utils::blocks::barotropic_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto gravityGradientValue =
utils::blocks::get_value_block<Form>(utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialValue =
utils::blocks::get_value_block<Form>(utils::blocks::gravity_field.poisson_term);
constexpr auto enthalpyValue =
utils::blocks::get_value_block<Form>(utils::blocks::enthalpy_field.specific_term);
constexpr auto barotropicConstantValue =
utils::blocks::get_value_block<Form>(utils::blocks::barotropic_constant_field.mass_normalization_term);
constexpr auto gravityGradientResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::gravity_field.gradient_term);
constexpr auto gravityPotentialResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::gravity_field.poisson_term);
constexpr auto densityResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto enthalpyResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::enthalpy_field.specific_term);
constexpr auto massResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::barotropic_constant_field.mass_normalization_term);
MFEM_VERIFY(
m_layout.size(densityValue) == m_preparedOperator.GetGravityContext().GetDensityMap().reduced_size() &&
m_layout.size(displacementValue) ==
m_preparedOperator.GetGravityContext().GetDisplacementMap().reduced_size() &&
m_layout.size(gravityGradientValue) ==
m_preparedOperator.GetGravityContext().GetGravityGradientMap().reduced_size() &&
m_layout.size(gravityPotentialValue) ==
m_preparedOperator.GetGravityContext().GetGravityPotentialMap().reduced_size() &&
m_layout.size(barotropicConstantValue) == 1,
"Prepared displacement-residual MFEM adapter received "
"incompatible barotropic value-block sizes."
);
MFEM_VERIFY(
m_layout.size(enthalpyValue) == m_preparedOperator.GetPressureOperator().GetEnthalpySize(),
"Prepared displacement-residual MFEM adapter received an "
"incompatible enthalpy value block."
);
MFEM_VERIFY(
m_layout.size(gravityGradientResidual) ==
m_preparedOperator.GetGravityContext().GetGravityGradientMap().reduced_size() &&
m_layout.size(gravityPotentialResidual) ==
m_preparedOperator.GetGravityContext().GetGravityPotentialMap().reduced_size() &&
m_layout.size(densityResidual) ==
m_preparedOperator.GetGravityContext().GetDensityMap().reduced_size() &&
m_layout.size(displacementResidual) ==
m_preparedOperator.GetGravityContext().GetDisplacementMap().reduced_size() &&
m_layout.size(enthalpyResidual) == m_preparedOperator.GetPressureOperator().GetEnthalpySize() &&
m_layout.size(massResidual) == 1,
"Prepared displacement-residual MFEM adapter received "
"incompatible barotropic residual-block sizes."
);
MFEM_VERIFY(
Height() == m_layout.residual_offsets().Last() && Width() == m_layout.value_offsets().Last(),
"Prepared displacement-residual MFEM adapter has inconsistent "
"operator dimensions."
);
}
void PreparedDisplacementResidualJacobianOperator::Mult(
const mfem::Vector &direction,
mfem::Vector &action
) const {
MFEM_VERIFY(
m_preparedOperator.IsPrepared(), "Prepared displacement-residual MFEM adapter requires a "
"prepared row operator."
);
MFEM_VERIFY(
direction.Size() == Width(), "Prepared displacement-residual MFEM adapter received a "
"direction with the wrong size."
);
using Form = utils::blocks::barotropic_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto gravityGradientValue =
utils::blocks::get_value_block<Form>(utils::blocks::gravity_field.gradient_term);
constexpr auto enthalpyValue =
utils::blocks::get_value_block<Form>(utils::blocks::enthalpy_field.specific_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
const mfem::Vector densityVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(densityValue), m_layout.size(densityValue)
);
const mfem::Vector displacementVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(displacementValue),
m_layout.size(displacementValue)
);
const mfem::Vector gravityGradientVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(gravityGradientValue),
m_layout.size(gravityGradientValue)
);
const mfem::Vector enthalpyVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(enthalpyValue),
m_layout.size(enthalpyValue)
);
mfem::Vector displacementAction;
m_preparedOperator.ApplyCompleteJacobianAction(
densityVariation, displacementVariation, gravityGradientVariation, enthalpyVariation, displacementAction
);
MFEM_VERIFY(
displacementAction.Size() == m_layout.size(displacementResidual),
"Prepared displacement-residual MFEM adapter produced an "
"action with the wrong size."
);
action.SetSize(Height());
action = 0.0;
const int residualOffset = m_layout.offset(displacementResidual);
for (int entry = 0; entry < displacementAction.Size(); ++entry) {
action(residualOffset + entry) = displacementAction(entry);
}
}
const DisplacementResidualLayout &PreparedDisplacementResidualJacobianOperator::GetLayout() const noexcept {
return m_layout;
}
} // namespace mean_field::operators

View File

@@ -0,0 +1,786 @@
module;
#include <cmath>
#include <expected>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
import :operators.kernels.gravity_displacement_force;
import :operators.prepared_gravity_displacement_force;
import :fem.reference_tables;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using Rejection = mean_field::operators::kernels::GravityDisplacementForceRejection;
using Reason = mean_field::operators::kernels::GravityDisplacementForceRejectionReason;
[[nodiscard]] bool relevant_revisions_match(
const mean_field::operators::context::gravity_field::GravityFieldRevisions &left,
const mean_field::operators::context::gravity_field::GravityFieldRevisions &right
) noexcept {
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<mean_field::utils::domain::Vacuum>(attribute);
}
[[nodiscard]] Rejection mapping_rejection(const mean_field::mapping::MappingStatus status) {
MFEM_VERIFY(
status != mean_field::mapping::MappingStatus::invalid_dimension,
"Prepared gravity force mapping reported an invariant dimension mismatch."
);
return {.reason = Reason::invalid_mapping, .mappingStatus = status};
}
[[nodiscard]] Rejection non_finite_rejection() noexcept {
return {.reason = Reason::non_finite_arithmetic};
}
[[nodiscard]] int encode_rejection(const std::optional<Rejection> &rejection) noexcept {
if (!rejection.has_value()) {
return 0;
}
if (rejection->reason == Reason::non_finite_arithmetic) {
return 256;
}
return static_cast<int>(rejection->mappingStatus) + 1;
}
[[nodiscard]] Rejection decode_rejection(const int encoded) {
if (encoded >= 256) {
return non_finite_rejection();
}
return mapping_rejection(static_cast<mean_field::mapping::MappingStatus>(encoded - 1));
}
[[nodiscard]] std::expected<
void,
Rejection>
synchronize_rejection(
const std::optional<Rejection> &localRejection,
const MPI_Comm communicator
) {
const int localEncoded = encode_rejection(localRejection);
int globalEncoded = 0;
if (MPI_Allreduce(&localEncoded, &globalEncoded, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("Could not synchronize prepared gravity-force candidate validity.");
}
if (globalEncoded != 0) {
return std::unexpected(decode_rejection(globalEncoded));
}
return {};
}
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) noexcept {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
[[noreturn]] void throw_rejection(const Rejection &rejection) {
if (rejection.reason == Reason::non_finite_arithmetic) {
throw std::domain_error("Prepared gravity force produced non-finite arithmetic.");
}
throw std::domain_error("Prepared gravity force encountered an invalid mapped domain.");
}
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<mean_field::field::Displacement>;
const mean_field::quadrature::Query query =
DisplacementField::make_query<mean_field::field::Displacement::Form::GravityForce>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), {},
mean_field::utils::DOMAINS::STELLAR, mean_field::quadrature::MappingKind::general
);
const mean_field::quadrature::MfemRule rule = f.quadratureFactory->get(query, transformation.GetGeometryType());
MFEM_VERIFY(
rule.integration_rule != nullptr,
"The quadrature policy did not return a gravity-displacement-force integration rule."
);
return *rule.integration_rule;
}
} // namespace
namespace mean_field::operators {
PreparedGravityDisplacementForceOperator::PreparedGravityDisplacementForceOperator(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,
const context::gravity_field::GravityFieldLinearizationContext &gravityContext
)
: m_fem(f),
m_domainMapper(domainMapper),
m_gravityContext(gravityContext) {
MFEM_VERIFY(m_fem.mesh != nullptr, "PreparedGravityDisplacementForceOperator requires a mesh.");
MFEM_VERIFY(
m_fem.densityFes != nullptr && m_fem.gravityFluxFes != nullptr && m_fem.displacementFes != nullptr,
"PreparedGravityDisplacementForceOperator requires density, "
"gravity-gradient, and displacement finite-element spaces."
);
MFEM_VERIFY(
m_domainMapper.GetDimension() == m_fem.mesh->Dimension(),
"PreparedGravityDisplacementForceOperator received a mapper "
"with the wrong dimension."
);
}
std::expected<
void,
kernels::GravityDisplacementForceRejection>
PreparedGravityDisplacementForceOperator::TryPrepareElementData() {
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<int> 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);
data.densityReferenceTable =
m_fem.GetReferenceTables().GetScalarTable(densityElement, *data.integrationRule);
data.displacementReferenceTable =
m_fem.GetReferenceTables().GetScalarTable(displacementElement, *data.integrationRule);
if (gravityGradientElement.GetMapType() == mfem::FiniteElement::H_DIV &&
gravityGradientElement.GetDim() == dimension && gravityGradientElement.GetRangeDim() == dimension &&
transformation->GetSpaceDim() == dimension) {
data.gravityReferenceTable =
m_fem.GetReferenceTables().GetVectorTable(gravityGradientElement, *data.integrationRule);
data.meshPiolaJacobians.SetSize(data.integrationRule->GetNPoints(), dimension * dimension);
}
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
);
if (status != mapping::MappingStatus::valid) {
return std::unexpected(mapping_rejection(status));
}
MFEM_VERIFY(
!mappingContext.mapping.compactified,
"Prepared gravity force encountered compactification on a stellar element."
);
for (int dof = 0; dof < densityElement.GetDof(); ++dof) {
densityShape(dof) = data.densityReferenceTable->GetValues()(quadraturePoint, dof);
}
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();
const mfem::DenseMatrix &meshJacobian = transformation->Jacobian();
const double inverseMeshWeight = 1.0 / transformation->Weight();
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);
if (data.gravityReferenceTable != nullptr) {
data.meshPiolaJacobians(quadraturePoint, entry) =
inverseMeshWeight * meshJacobian(row, column);
}
}
}
if (!std::isfinite(data.baseDensityValues(quadraturePoint)) ||
!std::isfinite(data.referenceWeights(quadraturePoint)) ||
!vector_is_finite(baseGravityReferenceValue)) {
return std::unexpected(non_finite_rejection());
}
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
const int entry = row * dimension + column;
if (!std::isfinite(data.mappingJacobians(quadraturePoint, entry)) ||
!std::isfinite(data.inverseMeshJacobians(quadraturePoint, entry)) ||
(data.gravityReferenceTable != nullptr &&
!std::isfinite(data.meshPiolaJacobians(quadraturePoint, entry)))) {
return std::unexpected(non_finite_rejection());
}
}
}
}
}
return {};
}
PreparedGravityDisplacementForceReport PreparedGravityDisplacementForceOperator::Prepare() {
auto result = TryPrepare();
if (!result.has_value()) {
throw_rejection(result.error());
}
return std::move(result).value();
}
std::expected<
PreparedGravityDisplacementForceReport,
kernels::GravityDisplacementForceRejection>
PreparedGravityDisplacementForceOperator::TryPrepare() {
MFEM_VERIFY(
m_gravityContext.IsPrepared(), "PreparedGravityDisplacementForceOperator requires the shared "
"gravity linearization context to be prepared first."
);
const context::gravity_field::GravityFieldRevisions &requestedRevisions = m_gravityContext.GetRevisions();
if (m_isPrepared && relevant_revisions_match(requestedRevisions, m_preparedRevisions)) {
return {};
}
m_isPrepared = false;
/*
* Build the reusable element plan before assembling the residual.
* This pass stops at the first invalid mapped quadrature point, so a
* rejected line-search candidate need not traverse the full stateless
* residual kernel. Synchronize before proceeding so every rank takes
* the same branch.
*/
const auto elementResult = TryPrepareElementData();
const std::optional<Rejection> localElementRejection =
elementResult.has_value() ? std::optional<Rejection>{} : std::optional<Rejection>{elementResult.error()};
auto synchronizedElement = synchronize_rejection(localElementRejection, m_fem.mesh->GetComm());
if (!synchronizedElement.has_value()) {
return std::unexpected(synchronizedElement.error());
}
auto residualResult = kernels::try_apply_gravity_displacement_force_residual(
m_fem, m_domainMapper, m_gravityContext.GetDensityTrue(), m_gravityContext.GetGravityGradientTrue(),
m_gravityContext.GetGeometryContext().GetDisplacementTrue(), m_actionTrue
);
if (!residualResult.has_value()) {
return std::unexpected(residualResult.error());
}
m_cachedResidual.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, m_cachedResidual);
std::optional<Rejection> localRejection;
if (!vector_is_finite(m_cachedResidual)) {
localRejection = non_finite_rejection();
}
auto synchronized = synchronize_rejection(localRejection, m_fem.mesh->GetComm());
if (!synchronized.has_value()) {
return std::unexpected(synchronized.error());
}
m_preparedRevisions = requestedRevisions;
++m_residualPreparationCount;
m_isPrepared = true;
return PreparedGravityDisplacementForceReport{.preparedResidual = true};
}
void PreparedGravityDisplacementForceOperator::BuildResidual(mfem::Vector &residual) const {
VerifyPrepared();
residual = m_cachedResidual;
++m_residualApplicationCount;
}
void PreparedGravityDisplacementForceOperator::ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size());
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
kernels::apply_gravity_displacement_force_density_action(
m_fem, m_domainMapper, m_densityVariationTrue, m_gravityContext.GetGravityGradientTrue(),
m_gravityContext.GetGeometryContext().GetDisplacementTrue(), m_actionTrue
);
action.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, action);
++m_densityJacobianStatistics.applications;
}
void PreparedGravityDisplacementForceOperator::ApplyGravityGradientJacobianAction(
const mfem::Vector &gravityGradientVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_gravityGradientVariationTrue.SetSize(m_gravityContext.GetGravityGradientMap().full_size());
m_gravityContext.GetGravityGradientMap().scatter(gravityGradientVariation, m_gravityGradientVariationTrue);
kernels::apply_gravity_displacement_force_gradient_action(
m_fem, m_domainMapper, m_gravityContext.GetDensityTrue(), m_gravityGradientVariationTrue,
m_gravityContext.GetGeometryContext().GetDisplacementTrue(), m_actionTrue
);
action.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, action);
++m_gravityGradientJacobianStatistics.applications;
}
void PreparedGravityDisplacementForceOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size());
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
kernels::apply_gravity_displacement_force_displacement_action(
m_fem, m_domainMapper, m_gravityContext.GetDensityTrue(), m_gravityContext.GetGravityGradientTrue(),
m_displacementVariationTrue, m_gravityContext.GetGeometryContext().GetDisplacementTrue(), m_actionTrue
);
action.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, action);
++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.");
MFEM_VERIFY(
data.densityReferenceTable != nullptr && data.displacementReferenceTable != nullptr,
"Prepared gravity force has no reference basis tables."
);
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_referenceDisplacementJacobian.SetSize(dimension, dimension);
m_displacementJacobianVariation.SetSize(dimension, dimension);
m_mappingJacobian.SetSize(dimension, dimension);
m_inverseMeshJacobian.SetSize(dimension, dimension);
m_meshPiolaJacobian.SetSize(dimension, dimension);
m_baseGravityReferenceValue.SetSize(dimension);
m_gravityVariationReferenceValue.SetSize(dimension);
m_gravityVariationReferenceCellValue.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);
const mfem::DenseMatrix &referenceDisplacementDShape =
data.displacementReferenceTable->GetGradients(quadraturePoint);
mfem::MultAtB(directionDofs, referenceDisplacementDShape, m_referenceDisplacementJacobian);
const mfem::DenseMatrix &densityValues = data.densityReferenceTable->GetValues();
const mfem::DenseMatrix &displacementValues = data.displacementReferenceTable->GetValues();
for (int dof = 0; dof < densityElement.GetDof(); ++dof) {
m_densityShape(dof) = densityValues(quadraturePoint, dof);
}
for (int dof = 0; dof < scalarDisplacementDofCount; ++dof) {
m_displacementShape(dof) = displacementValues(quadraturePoint, dof);
}
if (data.gravityReferenceTable != nullptr) {
data.gravityReferenceTable->GetValues(quadraturePoint)
.MultTranspose(m_elementGravityGradientVariation, m_gravityVariationReferenceCellValue);
} else {
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);
if (data.gravityReferenceTable != nullptr) {
m_meshPiolaJacobian(row, column) = data.meshPiolaJacobians(quadraturePoint, entry);
}
}
}
if (data.gravityReferenceTable != nullptr) {
m_meshPiolaJacobian.Mult(m_gravityVariationReferenceCellValue, m_gravityVariationReferenceValue);
}
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,
const mfem::Vector &gravityGradientVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size());
m_gravityGradientVariationTrue.SetSize(m_gravityContext.GetGravityGradientMap().full_size());
m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size());
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_gravityContext.GetGravityGradientMap().scatter(gravityGradientVariation, m_gravityGradientVariationTrue);
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
ApplyPreparedCompleteJacobianActionTrue(
m_densityVariationTrue, m_displacementVariationTrue, m_gravityGradientVariationTrue, m_actionTrue
);
action.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, action);
++m_densityJacobianStatistics.applications;
++m_gravityGradientJacobianStatistics.applications;
++m_displacementJacobianStatistics.applications;
++m_completeJacobianStatistics.applications;
}
bool PreparedGravityDisplacementForceOperator::IsPrepared() const noexcept {
if (!m_isPrepared || !m_gravityContext.IsPrepared()) {
return false;
}
return relevant_revisions_match(m_gravityContext.GetRevisions(), m_preparedRevisions);
}
std::uint64_t PreparedGravityDisplacementForceOperator::GetResidualPreparationCount() const noexcept {
return m_residualPreparationCount;
}
std::uint64_t PreparedGravityDisplacementForceOperator::GetResidualApplicationCount() const noexcept {
return m_residualApplicationCount;
}
const PreparedGravityDisplacementForceColumnStatistics &
PreparedGravityDisplacementForceOperator::GetDensityJacobianStatistics() const noexcept {
return m_densityJacobianStatistics;
}
const PreparedGravityDisplacementForceColumnStatistics &
PreparedGravityDisplacementForceOperator::GetGravityGradientJacobianStatistics() const noexcept {
return m_gravityGradientJacobianStatistics;
}
const PreparedGravityDisplacementForceColumnStatistics &
PreparedGravityDisplacementForceOperator::GetDisplacementJacobianStatistics() const noexcept {
return m_displacementJacobianStatistics;
}
const PreparedGravityDisplacementForceCompleteStatistics &
PreparedGravityDisplacementForceOperator::GetCompleteJacobianStatistics() const noexcept {
return m_completeJacobianStatistics;
}
const fem::FEM &PreparedGravityDisplacementForceOperator::GetFEM() const noexcept {
return m_fem;
}
const context::gravity_field::GravityFieldLinearizationContext &
PreparedGravityDisplacementForceOperator::GetGravityContext() const noexcept {
return m_gravityContext;
}
void PreparedGravityDisplacementForceOperator::VerifyPrepared() const {
MFEM_VERIFY(
IsPrepared(), "PreparedGravityDisplacementForceOperator must be prepared for "
"the current shared gravity-context revisions before residual or "
"Jacobian application."
);
}
PreparedGravityDisplacementForceJacobianOperator::PreparedGravityDisplacementForceJacobianOperator(
const GravityDisplacementForceLayout &layout,
const PreparedGravityDisplacementForceOperator &preparedOperator
)
: mfem::Operator(
layout.residual_offsets().Last(),
layout.value_offsets().Last()
),
m_layout(layout),
m_preparedOperator(preparedOperator) {
using Form = utils::blocks::barotropic_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto gravityGradientValue =
utils::blocks::get_value_block<Form>(utils::blocks::gravity_field.gradient_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
MFEM_VERIFY(
m_layout.size(densityValue) == m_preparedOperator.GetGravityContext().GetDensityMap().reduced_size() &&
m_layout.size(displacementValue) ==
m_preparedOperator.GetGravityContext().GetDisplacementMap().reduced_size() &&
m_layout.size(gravityGradientValue) ==
m_preparedOperator.GetGravityContext().GetGravityGradientMap().reduced_size() &&
m_layout.size(displacementResidual) ==
m_preparedOperator.GetGravityContext().GetDisplacementMap().reduced_size(),
"Prepared gravity-displacement-force MFEM adapter received "
"incompatible coupled block sizes."
);
}
void PreparedGravityDisplacementForceJacobianOperator::Mult(
const mfem::Vector &direction,
mfem::Vector &action
) const {
MFEM_VERIFY(
m_preparedOperator.IsPrepared(), "Prepared gravity-displacement-force MFEM adapter requires a "
"prepared operator."
);
MFEM_VERIFY(
direction.Size() == Width(), "Prepared gravity-displacement-force MFEM adapter received a "
"direction with the wrong size."
);
using Form = utils::blocks::barotropic_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto gravityGradientValue =
utils::blocks::get_value_block<Form>(utils::blocks::gravity_field.gradient_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
const mfem::Vector densityVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(densityValue), m_layout.size(densityValue)
);
const mfem::Vector displacementVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(displacementValue),
m_layout.size(displacementValue)
);
const mfem::Vector gravityGradientVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(gravityGradientValue),
m_layout.size(gravityGradientValue)
);
mfem::Vector displacementAction;
m_preparedOperator.ApplyCompleteJacobianAction(
densityVariation, displacementVariation, gravityGradientVariation, displacementAction
);
MFEM_VERIFY(
displacementAction.Size() == m_layout.size(displacementResidual),
"Prepared gravity-displacement-force MFEM adapter produced a "
"displacement action with the wrong size."
);
action.SetSize(Height());
action = 0.0;
const int residualOffset = m_layout.offset(displacementResidual);
for (int entry = 0; entry < displacementAction.Size(); ++entry) {
action(residualOffset + entry) = displacementAction(entry);
}
}
const GravityDisplacementForceLayout &PreparedGravityDisplacementForceJacobianOperator::GetLayout() const noexcept {
return m_layout;
}
} // namespace mean_field::operators

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,686 @@
module;
#include <array>
#include <cmath>
#include <expected>
#include <mfem.hpp>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mpi.h>
module mean_field;
import :operators.kernels.rotational_displacement_force;
import :operators.prepared_rotational_displacement_force;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
using Rejection = mean_field::operators::kernels::RotationalDisplacementForceRejection;
using Reason = mean_field::operators::kernels::RotationalDisplacementForceRejectionReason;
[[nodiscard]] bool is_vacuum_attribute(const int attribute) {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
[[nodiscard]] Rejection mapping_rejection(const mean_field::mapping::MappingStatus status) {
MFEM_VERIFY(
status != mean_field::mapping::MappingStatus::invalid_dimension,
"Prepared rotational force mapping reported an invariant dimension mismatch."
);
return {.reason = Reason::invalid_mapping, .mappingStatus = status};
}
[[nodiscard]] Rejection non_finite_rejection() noexcept {
return {.reason = Reason::non_finite_arithmetic};
}
[[nodiscard]] int encode_rejection(const std::optional<Rejection> &rejection) noexcept {
if (!rejection.has_value()) {
return 0;
}
if (rejection->reason == Reason::non_finite_arithmetic) {
return 256;
}
return static_cast<int>(rejection->mappingStatus) + 1;
}
[[nodiscard]] Rejection decode_rejection(const int encoded) {
if (encoded >= 256) {
return non_finite_rejection();
}
return mapping_rejection(static_cast<mean_field::mapping::MappingStatus>(encoded - 1));
}
[[nodiscard]] std::expected<
void,
Rejection>
synchronize_rejection(
const std::optional<Rejection> &localRejection,
const MPI_Comm communicator
) {
const int localEncoded = encode_rejection(localRejection);
int globalEncoded = 0;
if (MPI_Allreduce(&localEncoded, &globalEncoded, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("Could not synchronize prepared rotational-force candidate validity.");
}
if (globalEncoded != 0) {
return std::unexpected(decode_rejection(globalEncoded));
}
return {};
}
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) noexcept {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
[[noreturn]] void throw_rejection(const Rejection &rejection) {
if (rejection.reason == Reason::non_finite_arithmetic) {
throw std::domain_error("Prepared rotational force produced non-finite arithmetic.");
}
throw std::domain_error("Prepared rotational force encountered an invalid mapped domain.");
}
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<mean_field::field::Displacement>;
const mean_field::quadrature::Query query =
DisplacementField::make_query<mean_field::field::Displacement::Form::CentrifugalForce>(
mean_field::quadrature::QuadratureRole::discretization, transformation.OrderW(), std::array<int, 1>{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,
const mapping::DomainMapper &domainMapper
)
: m_fem(f),
m_domainMapper(domainMapper),
m_context(
f,
domainMapper
) {
MFEM_VERIFY(m_fem.mesh != nullptr, "PreparedRotationalDisplacementForceOperator requires a mesh.");
MFEM_VERIFY(
m_fem.mesh->Dimension() == 3, "PreparedRotationalDisplacementForceOperator requires a "
"three-dimensional mesh."
);
MFEM_VERIFY(
m_fem.densityFes != nullptr && m_fem.displacementFes != nullptr,
"PreparedRotationalDisplacementForceOperator requires density "
"and displacement finite-element spaces."
);
MFEM_VERIFY(
m_fem.compactificationFes != nullptr && m_fem.compactificationCoordinate != nullptr,
"PreparedRotationalDisplacementForceOperator requires the "
"compactification coordinate."
);
MFEM_VERIFY(
m_fem.quadratureFactory != nullptr, "PreparedRotationalDisplacementForceOperator requires the "
"quadrature-rule factory."
);
MFEM_VERIFY(
m_domainMapper.GetDimension() == m_fem.mesh->Dimension(),
"PreparedRotationalDisplacementForceOperator received a mapper "
"with the wrong dimension."
);
}
std::expected<
void,
kernels::RotationalDisplacementForceRejection>
PreparedRotationalDisplacementForceOperator::TryPrepareElementData() {
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<int> 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
);
if (status != mapping::MappingStatus::valid) {
return std::unexpected(mapping_rejection(status));
}
MFEM_VERIFY(
!mappingContext.mapping.compactified,
"Prepared rotational force encountered compactification on a stellar element."
);
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);
}
}
if (!std::isfinite(data.baseDensityValues(quadraturePoint)) ||
!std::isfinite(data.quadratureWeights(quadraturePoint)) || !vector_is_finite(potentialGradient)) {
return std::unexpected(non_finite_rejection());
}
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
if (!std::isfinite(data.inverseElementJacobians(quadraturePoint, row * dimension + column))) {
return std::unexpected(non_finite_rejection());
}
}
}
}
}
return {};
}
PreparedRotationalDisplacementForceReport PreparedRotationalDisplacementForceOperator::Prepare(
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation
) {
auto result = TryPrepare(state, dependencies, rotation);
if (!result.has_value()) {
throw_rejection(result.error());
}
return std::move(result).value();
}
std::expected<
PreparedRotationalDisplacementForceReport,
kernels::RotationalDisplacementForceRejection>
PreparedRotationalDisplacementForceOperator::TryPrepare(
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation
) {
const bool wasPrepared = m_isPrepared;
const bool rotationChanged =
!m_context.IsPrepared() || dependencies.rotation != m_context.GetDependencies().rotation;
PreparedRotationalDisplacementForceReport report;
report.contextReport = m_context.Prepare(state, dependencies);
if (!report.contextReport.DidAnyWork() && wasPrepared) {
return report;
}
m_isPrepared = false;
if (rotationChanged) {
m_rotation = rotation;
report.updatedRotation = true;
}
MFEM_VERIFY(
m_rotation.has_value(), "PreparedRotationalDisplacementForceOperator has no frozen "
"rotation state."
);
if (report.contextReport.preparedBaseState || !wasPrepared) {
auto residualResult = kernels::try_apply_rotational_displacement_force_residual(
m_fem, m_domainMapper, *m_rotation, m_context.GetBaseDensityTrue(), m_context.GetDisplacementTrue(),
m_actionTrue
);
if (!residualResult.has_value()) {
return std::unexpected(residualResult.error());
}
m_cachedResidual.SetSize(m_context.GetDisplacementMap().reduced_size());
m_context.GetDisplacementMap().gather(m_actionTrue, m_cachedResidual);
const auto elementResult = TryPrepareElementData();
std::optional<Rejection> localRejection = elementResult.has_value()
? std::optional<Rejection>{}
: std::optional<Rejection>{elementResult.error()};
if (!vector_is_finite(m_cachedResidual)) {
localRejection = non_finite_rejection();
}
auto synchronized = synchronize_rejection(localRejection, m_fem.mesh->GetComm());
if (!synchronized.has_value()) {
return std::unexpected(synchronized.error());
}
++m_residualPreparationCount;
report.preparedResidual = true;
}
MFEM_VERIFY(
m_cachedResidual.Size() == m_context.GetDisplacementMap().reduced_size(),
"The prepared rotational-displacement-force residual has the "
"wrong size."
);
m_preparedDependencies = dependencies;
m_isPrepared = true;
return report;
}
void PreparedRotationalDisplacementForceOperator::BuildResidual(mfem::Vector &residual) const {
VerifyPrepared();
residual = m_cachedResidual;
++m_residualApplicationCount;
}
void PreparedRotationalDisplacementForceOperator::ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_context.GetDensityMap().full_size());
m_context.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
kernels::apply_rotational_displacement_force_density_action(
m_fem, m_domainMapper, *m_rotation, m_densityVariationTrue, m_context.GetDisplacementTrue(), m_actionTrue
);
action.SetSize(m_context.GetDisplacementMap().reduced_size());
m_context.GetDisplacementMap().gather(m_actionTrue, action);
++m_densityJacobianStatistics.applications;
}
void PreparedRotationalDisplacementForceOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_displacementVariationTrue.SetSize(m_context.GetDisplacementMap().full_size());
m_context.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
kernels::apply_rotational_displacement_force_displacement_action(
m_fem, m_domainMapper, *m_rotation, m_context.GetBaseDensityTrue(), m_displacementVariationTrue,
m_context.GetDisplacementTrue(), m_actionTrue
);
action.SetSize(m_context.GetDisplacementMap().reduced_size());
m_context.GetDisplacementMap().gather(m_actionTrue, action);
++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,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_context.GetDensityMap().full_size());
m_displacementVariationTrue.SetSize(m_context.GetDisplacementMap().full_size());
m_context.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_context.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
ApplyPreparedCompleteJacobianActionTrue(m_densityVariationTrue, m_displacementVariationTrue, m_actionTrue);
action.SetSize(m_context.GetDisplacementMap().reduced_size());
m_context.GetDisplacementMap().gather(m_actionTrue, action);
++m_densityJacobianStatistics.applications;
++m_displacementJacobianStatistics.applications;
++m_completeJacobianStatistics.applications;
}
bool PreparedRotationalDisplacementForceOperator::IsPrepared() const noexcept {
return m_isPrepared && m_rotation.has_value() && m_context.MatchesDependencies(m_preparedDependencies);
}
const context::rotational_displacement_force::RotationalDisplacementForcePreparationStatistics &
PreparedRotationalDisplacementForceOperator::GetContextPreparationStatistics() const noexcept {
return m_context.GetPreparationStatistics();
}
std::uint64_t PreparedRotationalDisplacementForceOperator::GetResidualPreparationCount() const noexcept {
return m_residualPreparationCount;
}
std::uint64_t PreparedRotationalDisplacementForceOperator::GetResidualApplicationCount() const noexcept {
return m_residualApplicationCount;
}
const PreparedRotationalDisplacementForceColumnStatistics &
PreparedRotationalDisplacementForceOperator::GetDensityJacobianStatistics() const noexcept {
return m_densityJacobianStatistics;
}
const PreparedRotationalDisplacementForceColumnStatistics &
PreparedRotationalDisplacementForceOperator::GetDisplacementJacobianStatistics() const noexcept {
return m_displacementJacobianStatistics;
}
const PreparedRotationalDisplacementForceCompleteStatistics &
PreparedRotationalDisplacementForceOperator::GetCompleteJacobianStatistics() const noexcept {
return m_completeJacobianStatistics;
}
const fem::FEM &PreparedRotationalDisplacementForceOperator::GetFEM() const noexcept {
return m_fem;
}
const context::rotational_displacement_force::RotationalDisplacementForceLinearizationContext &
PreparedRotationalDisplacementForceOperator::GetContext() const noexcept {
return m_context;
}
void PreparedRotationalDisplacementForceOperator::VerifyPrepared() const {
MFEM_VERIFY(
IsPrepared(), "PreparedRotationalDisplacementForceOperator must be prepared "
"for the current revisions before residual or Jacobian "
"application."
);
}
PreparedRotationalDisplacementForceJacobianOperator::PreparedRotationalDisplacementForceJacobianOperator(
const RotationalDisplacementForceLayout &layout,
const PreparedRotationalDisplacementForceOperator &preparedOperator
)
: mfem::Operator(
layout.residual_offsets().Last(),
layout.value_offsets().Last()
),
m_layout(layout),
m_preparedOperator(preparedOperator) {
using Form = utils::blocks::barotropic_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
MFEM_VERIFY(
m_layout.size(densityValue) == m_preparedOperator.GetContext().GetDensityMap().reduced_size() &&
m_layout.size(displacementValue) ==
m_preparedOperator.GetContext().GetDisplacementMap().reduced_size() &&
m_layout.size(displacementResidual) ==
m_preparedOperator.GetContext().GetDisplacementMap().reduced_size(),
"Prepared rotational-displacement-force MFEM adapter received "
"incompatible coupled block sizes."
);
}
void PreparedRotationalDisplacementForceJacobianOperator::Mult(
const mfem::Vector &direction,
mfem::Vector &action
) const {
MFEM_VERIFY(
m_preparedOperator.IsPrepared(), "Prepared rotational-displacement-force MFEM adapter requires "
"a prepared operator."
);
MFEM_VERIFY(
direction.Size() == Width(), "Prepared rotational-displacement-force MFEM adapter received "
"a direction with the wrong size."
);
using Form = utils::blocks::barotropic_equilibrium_form;
constexpr auto densityValue = utils::blocks::get_value_block<Form>(utils::blocks::density_field.mass_term);
constexpr auto displacementValue =
utils::blocks::get_value_block<Form>(utils::blocks::displacement_field.geometry_term);
constexpr auto displacementResidual =
utils::blocks::get_residual_block<Form>(utils::blocks::displacement_field.geometry_term);
const mfem::Vector densityVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(densityValue), m_layout.size(densityValue)
);
const mfem::Vector displacementVariation(
const_cast<mfem::real_t *>(direction.GetData()) + m_layout.offset(displacementValue),
m_layout.size(displacementValue)
);
mfem::Vector displacementAction;
m_preparedOperator.ApplyCompleteJacobianAction(densityVariation, displacementVariation, displacementAction);
MFEM_VERIFY(
displacementAction.Size() == m_layout.size(displacementResidual),
"Prepared rotational-displacement-force MFEM adapter produced "
"a displacement action with the wrong size."
);
action.SetSize(Height());
action = 0.0;
const int residualOffset = m_layout.offset(displacementResidual);
for (int entry = 0; entry < displacementAction.Size(); ++entry) {
action(residualOffset + entry) = displacementAction(entry);
}
}
const RotationalDisplacementForceLayout &
PreparedRotationalDisplacementForceJacobianOperator::GetLayout() const noexcept {
return m_layout;
}
} // namespace mean_field::operators

File diff suppressed because it is too large Load Diff

View File

@@ -1,246 +1,58 @@
module;
#include "mfem.hpp"
#include "profile.h"
#include <array>
#include <cmath>
#include <format>
#include <source_location>
#include <string_view>
#include <unordered_map>
module mean_field;
import :mapping.coefficients;
import :analysis.integral;
namespace {
double centrifugal_potential(
const mfem::Vector &phys_x,
const double omega
) {
const double s2 = std::pow(phys_x(0), 2) + std::pow(phys_x(1), 2);
return -0.5 * s2 * std::pow(omega, 2);
}
void grid_function_to_true_dofs(
const mfem::ParFiniteElementSpace &finite_element_space,
const mfem::GridFunction &grid_function,
mfem::Vector &true_dofs
) {
MFEM_VERIFY(
grid_function.Size() == finite_element_space.GetVSize(),
"The grid function does not match the requested finite-element "
"space."
);
true_dofs.SetSize(finite_element_space.GetTrueVSize());
const mfem::Operator *restriction =
finite_element_space.GetRestrictionMatrix();
if (restriction != nullptr) {
restriction->Mult(grid_function, true_dofs);
} else {
MFEM_VERIFY(
grid_function.Size() == true_dofs.Size(),
"A finite-element space without a restriction operator must "
"have "
"matching local and true sizes."
);
true_dofs = grid_function;
}
}
} // namespace
namespace mean_field::physics {
GravitySolution grav_potential(
fem::FEM &f,
const utils::Args &args,
const mfem::GridFunction &rho,
const bool phi_warm
) {
MFEM_VERIFY(
f.densityFes != nullptr && rho.FESpace() == f.densityFes.get(),
"Gravity solve requires rho to use the registered density space."
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"Gravity solve requires the registered gravity-potential space."
);
mfem::Array<int> outer_bdr_marker(f.mesh->bdr_attributes.Max());
outer_bdr_marker = 0;
outer_bdr_marker[1] = 1;
mfem::ParLinearForm g_rhs(f.gravityFluxFes.get());
// ReSharper disable once CppTooWideScope
std::unique_ptr<mfem::Coefficient> boundary_potential_coeff;
if (!f.has_mapping()) { // We only need to explicitly add a boundary
// integrator if a mapping is not being used. In
// the case where the outer domain has been
// compactified the φ=0 boundary condition is
// the natural condition and MFEM automatically
// handles this
auto boundary_potential = [&f](const mfem::Vector &x_physical) {
return l2_multipole_potential(f, utils::MASS, x_physical);
};
boundary_potential_coeff =
std::make_unique<mfem::FunctionCoefficient>(boundary_potential);
auto boundary_integrator =
std::make_unique<mfem::VectorFEBoundaryFluxLFIntegrator>(
*boundary_potential_coeff
);
const mfem::FiniteElement &boundary_element =
*f.gravityFluxFes->GetTypicalTraceElement();
f.quadratureFactory->configure_gravity_boundary(
*boundary_integrator,
quadrature::QuadratureRole::discretization, boundary_element,
utils::DOMAINS::VACUUM, quadrature::MappingKind::none
);
g_rhs.AddBoundaryIntegrator(
boundary_integrator.release(), outer_bdr_marker
);
}
g_rhs.Assemble();
mfem::GridFunctionCoefficient rho_coeff(&rho);
mfem::ConstantCoefficient G4pi(4.0 * M_PI * utils::G);
mfem::ProductCoefficient source_coeff(G4pi, rho_coeff);
mfem::ParLinearForm f_rhs(f.gravityPotentialFes.get());
std::unique_ptr<mfem::Coefficient> mapped_source_coeff;
mfem::Coefficient *active_source_coeff = &source_coeff;
quadrature::MappingKind source_mapping_kind =
quadrature::MappingKind::none;
if (f.has_mapping()) {
mapped_source_coeff =
std::make_unique<mapping::MappedScalarCoefficient>(
*f.mapping, source_coeff
);
active_source_coeff = mapped_source_coeff.get();
source_mapping_kind = quadrature::MappingKind::general;
}
auto source_integrator =
std::make_unique<mfem::DomainLFIntegrator>(*active_source_coeff);
const mfem::FiniteElement &source_test_element =
*f.gravityPotentialFes->GetTypicalFE();
const mfem::ElementTransformation &source_transformation =
*f.mesh->GetElementTransformation(0);
const int source_coefficient_order = f.densityFes->GetMaxElementOrder();
f.quadratureFactory->configure_gravity_source(
*source_integrator, quadrature::QuadratureRole::discretization,
source_test_element, source_transformation,
source_coefficient_order, utils::DOMAINS::STELLAR,
source_mapping_kind
);
f_rhs.AddDomainIntegrator(
source_integrator.release(), f.gravityContext.stellar_mask
);
f_rhs.Assemble();
mfem::BlockVector RHS(f.gravityBlockTrueOffsets);
RHS.GetBlock(0) = *g_rhs.ParallelAssemble();
RHS.GetBlock(1) = *f_rhs.ParallelAssemble();
mfem::BlockVector X(f.gravityBlockTrueOffsets);
X = 0.0;
f.gravityContext.minres->SetOperator(*f.gravityContext.block_A);
f.gravityContext.minres->Mult(RHS, X);
GravitySolution solution(f);
solution.gradPhi.SetFromTrueDofs(X.GetBlock(0));
solution.phi.SetFromTrueDofs(X.GetBlock(1));
return solution;
}
mfem::GridFunction get_potential(
fem::FEM &fem,
const utils::Args &args,
const mfem::GridFunction &rho,
const bool warm
) {
auto phi = grav_potential(fem, args, rho, warm);
if (args.r.enabled) {
auto rot = [&fem, &args](const mfem::Vector &x) {
mfem::Vector rel_x = x;
rel_x -= fem.com;
return centrifugal_potential(rel_x, args.r.omega);
};
std::unique_ptr<mfem::Coefficient> centrifugal_coeff;
if (fem.has_mapping()) {
centrifugal_coeff = std::make_unique<
mapping::PhysicalPositionFunctionCoefficient>(
*fem.mapping, rot
);
} else {
centrifugal_coeff =
std::make_unique<mfem::FunctionCoefficient>(rot);
}
mfem::GridFunction centrifugal_gf(fem.gravityPotentialFes.get());
centrifugal_gf.ProjectCoefficient(*centrifugal_coeff);
phi.phi += centrifugal_gf;
}
return phi.phi;
}
mfem::DenseMatrix compute_quadrupole_moment_tensor(
const fem::FEM &fem,
const mfem::GridFunction &rho,
const mfem::Vector &com
) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("analysis::quadrupole", 0);
const int dim = fem.mesh->Dimension();
mfem::DenseMatrix local_Q(dim, dim);
local_Q = 0.0;
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
std::uint64_t mapping_evaluations = 0;
mapping::VolumeMappingContext mapping_context;
mfem::Vector x_prime(dim);
for (int i = 0; i < fem.mesh->GetNE(); ++i) {
if (fem.mesh->GetAttribute(i) == 3)
if (!DomainSchema::template attribute_belongs_to<utils::domain::Stellar>(fem.mesh->GetAttribute(i)))
continue;
mfem::ElementTransformation *trans =
fem.mesh->GetElementTransformation(i);
mfem::ElementTransformation *trans = fem.mesh->GetElementTransformation(i);
using DensityField = field::Field<field::Density>;
const quadrature::Query query =
DensityField::make_query<field::Density::Form::Quadrupole>(
quadrature::QuadratureRole::diagnostic, trans->OrderW(),
std::array<int, 1>{2}, utils::DOMAINS::STELLAR,
fem.has_mapping() ? quadrature::MappingKind::general
: quadrature::MappingKind::none
const quadrature::Query query = DensityField::make_query<field::Density::Form::Quadrupole>(
quadrature::QuadratureRole::diagnostic, trans->OrderW(), std::array<int, 1>{2}, utils::DOMAINS::STELLAR,
fem.has_mapping() ? quadrature::MappingKind::general : quadrature::MappingKind::none
);
const mfem::IntegrationRule &ir =
*fem.quadratureFactory->get(query, trans->GetGeometryType())
.integration_rule;
*fem.quadratureFactory->get(query, trans->GetGeometryType()).integration_rule;
for (int j = 0; j < ir.GetNPoints(); ++j) {
const mfem::IntegrationPoint &ip = ir.IntPoint(j);
trans->SetIntPoint(&ip);
double weight = trans->Weight() * ip.weight;
if (fem.has_mapping()) {
weight *= fem.mapping->ComputeDetJ(*trans, ip);
}
MFEM_VERIFY(
mapping_evaluator.EvaluateVolume(*trans, ip, mapping_context) == mapping::MappingStatus::valid,
"Quadrupole integration encountered an invalid mapping."
);
++mapping_evaluations;
const double weight = mapping_context.quadrature.weight;
const double rho_val = rho.GetValue(i, ip);
mfem::Vector phys_point(dim);
if (fem.has_mapping()) {
fem.mapping->GetPhysicalPoint(*trans, ip, phys_point);
} else {
trans->Transform(ip, phys_point);
}
const mfem::Vector &phys_point = mapping_context.mapping.physical_position;
mfem::Vector x_prime(dim);
double r_sq = 0.0;
for (int d = 0; d < dim; ++d) {
@@ -251,19 +63,17 @@ namespace mean_field::physics {
for (int m = 0; m < dim; ++m) {
for (int n = 0; n < dim; ++n) {
const double delta = (m == n) ? 1.0 : 0.0;
const double contrib =
3.0 * x_prime(m) * x_prime(n) - delta * r_sq;
const double contrib = 3.0 * x_prime(m) * x_prime(n) - delta * r_sq;
local_Q(m, n) += rho_val * contrib * weight;
}
}
}
}
MEAN_FIELD_PROFILE_COUNT("analysis::quadrupole mapping evaluations", mapping_evaluations);
mfem::DenseMatrix global_Q(dim, dim);
MPI_Allreduce(
local_Q.GetData(), global_Q.GetData(), dim * dim, MPI_DOUBLE,
MPI_SUM, fem.mesh->GetComm()
);
MPI_Allreduce(local_Q.GetData(), global_Q.GetData(), dim * dim, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm());
return global_Q;
}
@@ -289,8 +99,7 @@ namespace mean_field::physics {
}
}
const double l2_contrib =
-(utils::G / (2.0 * std::pow(r, 3))) * l2_mult_factor;
const double l2_contrib = -(utils::G / (2.0 * std::pow(r, 3))) * l2_mult_factor;
const double l0_contrib = -utils::G * total_mass / r;
@@ -298,236 +107,32 @@ namespace mean_field::physics {
return l0_contrib + l2_contrib;
}
void update_stiffness_matrix(fem::FEM &f) {
mfem::Array<int> empty_tdofs;
// ==========================================
// 1. Partially Assemble the High-Order Mass Block
// ==========================================
f.gravityContext.m_form =
std::make_unique<mfem::ParBilinearForm>(f.gravityFluxFes.get());
f.gravityContext.m_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
std::unique_ptr<mfem::VectorFEMassIntegrator> hdiv_mass_integrator;
if (f.has_mapping()) {
f.gravityContext.mapped_hdiv_mass_coeff =
std::make_unique<mapping::MappedHDivMassCoefficient>(
*f.mapping, f.mesh->Dimension()
);
hdiv_mass_integrator =
std::make_unique<mfem::VectorFEMassIntegrator>(
*f.gravityContext.mapped_hdiv_mass_coeff
);
} else {
f.gravityContext.mapped_hdiv_mass_coeff.reset();
hdiv_mass_integrator =
std::make_unique<mfem::VectorFEMassIntegrator>();
}
const mfem::FiniteElement &hdiv_element =
*f.gravityFluxFes->GetTypicalFE();
const mfem::ElementTransformation &hdiv_transformation =
*f.mesh->GetElementTransformation(0);
const quadrature::MappingKind mapping_kind =
f.has_mapping() ? quadrature::MappingKind::general
: quadrature::MappingKind::none;
f.quadratureFactory->configure_gravity_hdiv_mass(
*hdiv_mass_integrator, quadrature::QuadratureRole::discretization,
hdiv_element, hdiv_transformation, utils::DOMAINS::ALL, mapping_kind
);
f.gravityContext.m_form->AddDomainIntegrator(
hdiv_mass_integrator.release()
);
f.gravityContext.m_form->Assemble();
// ==========================================
// 2. Partially Assemble the High-Order Divergence Block
// ==========================================
f.gravityContext.b_form = std::make_unique<mfem::ParMixedBilinearForm>(
f.gravityFluxFes.get(), f.gravityPotentialFes.get()
);
f.gravityContext.b_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
auto divergence_discretization_integrator =
std::make_unique<mfem::VectorFEDivergenceIntegrator>();
const mfem::FiniteElement &divergence_discretization_test_element =
*f.gravityPotentialFes->GetTypicalFE();
f.quadratureFactory->configure_gravity_divergence(
*divergence_discretization_integrator,
quadrature::QuadratureRole::discretization, hdiv_element,
divergence_discretization_test_element, hdiv_transformation,
utils::DOMAINS::ALL, quadrature::MappingKind::none
);
f.gravityContext.b_form->AddDomainIntegrator(
divergence_discretization_integrator.release()
);
f.gravityContext.b_form->Assemble();
MFEM_VERIFY(
f.domainMapperStateless != nullptr,
"Gravity source partial assembly requires the stateless domain "
"mapper."
);
mfem::Vector displacement_true(f.displacementFes->GetTrueVSize());
displacement_true = 0.0;
const mfem::GridFunction *active_displacement =
f.mapping->GetDisplacement();
if (active_displacement != nullptr) {
grid_function_to_true_dofs(
*f.displacementFes, *active_displacement, displacement_true
);
}
auto source_form =
std::make_unique<operators::PreparedMappedGravitySourceOperator>(
f, *f.domainMapperStateless
);
source_form->Prepare(displacement_true);
f.gravityContext.source_form = std::move(source_form);
// ==========================================
// 3. Assemble Global Block Operator
// ==========================================
f.gravityContext.BT = std::make_unique<mfem::TransposeOperator>(
f.gravityContext.b_form.get()
);
f.gravityContext.block_A =
std::make_unique<mfem::BlockOperator>(f.gravityBlockTrueOffsets);
f.gravityContext.block_A->SetBlock(0, 0, f.gravityContext.m_form.get());
f.gravityContext.block_A->SetBlock(0, 1, f.gravityContext.BT.get());
f.gravityContext.block_A->SetBlock(1, 0, f.gravityContext.b_form.get());
// ==========================================
// 4. Construct a mapped Schur preconditioner
// ==========================================
mfem::Vector mass_diagonal(f.gravityFluxFes->GetTrueVSize());
f.gravityContext.m_form->AssembleDiagonal(mass_diagonal);
mfem::Vector inverse_mass_diagonal(mass_diagonal);
for (int i = 0; i < inverse_mass_diagonal.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(inverse_mass_diagonal(i)) &&
inverse_mass_diagonal(i) > 0.0,
"Mapped RT mass matrix has a non-positive or non-finite "
"diagonal "
"entry."
);
inverse_mass_diagonal(i) = 1.0 / inverse_mass_diagonal(i);
}
mfem::ParMixedBilinearForm b_preconditioner(
f.gravityFluxFes.get(), f.gravityPotentialFes.get()
);
auto divergence_preconditioner_integrator =
std::make_unique<mfem::VectorFEDivergenceIntegrator>();
const mfem::FiniteElement &divergence_trial_element =
*f.gravityFluxFes->GetTypicalFE();
const mfem::FiniteElement &divergence_test_element =
*f.gravityPotentialFes->GetTypicalFE();
const mfem::ElementTransformation &divergence_transformation =
*f.mesh->GetElementTransformation(0);
f.quadratureFactory->configure_gravity_divergence(
*divergence_preconditioner_integrator,
quadrature::QuadratureRole::preconditioner,
divergence_trial_element, divergence_test_element,
divergence_transformation, utils::DOMAINS::ALL,
quadrature::MappingKind::none
);
b_preconditioner.AddDomainIntegrator(
divergence_preconditioner_integrator.release()
);
b_preconditioner.Assemble();
b_preconditioner.Finalize();
std::unique_ptr<mfem::HypreParMatrix> b_matrix(
b_preconditioner.ParallelAssemble()
);
std::unique_ptr<mfem::HypreParMatrix> inverse_mass_b_transpose(
b_matrix->Transpose()
);
inverse_mass_b_transpose->ScaleRows(inverse_mass_diagonal);
f.gravityContext.Schur.reset(
mfem::ParMult(b_matrix.get(), inverse_mass_b_transpose.get())
);
// ==========================================
// 5. Wire Up the preconditioners
// ==========================================
f.gravityContext.prec_M =
std::make_unique<mfem::OperatorJacobiSmoother>(
mass_diagonal, empty_tdofs
);
f.gravityContext.prec_Phi->SetOperator(*f.gravityContext.Schur);
f.gravityContext.block_prec->SetDiagonalBlock(
0, f.gravityContext.prec_M.get()
);
f.gravityContext.block_prec->SetDiagonalBlock(
1, f.gravityContext.prec_Phi.get()
);
}
GravitySolution grav_potential_new(
GravitySolution solve_gravity_field(
fem::FEM &f,
const utils::Args &args,
const GravitySolveOptions &options,
const mfem::GridFunction &rho,
const mfem::GridFunction &displacement
) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("physics::solve_gravity_field", 0);
MFEM_VERIFY(f.mesh != nullptr, "Gravity initialization requires a parallel mesh.");
MFEM_VERIFY(f.densityFes != nullptr, "Gravity initialization requires the density finite-element space.");
MFEM_VERIFY(
f.mesh != nullptr,
"Gravity initialization requires a parallel mesh."
);
MFEM_VERIFY(
f.densityFes != nullptr,
"Gravity initialization requires the density finite-element space."
);
MFEM_VERIFY(
f.gravityPotentialFes != nullptr,
"Gravity initialization requires the gravity-potential "
f.gravityPotentialFes != nullptr, "Gravity initialization requires the gravity-potential "
"finite-element "
"space."
);
MFEM_VERIFY(
f.gravityFluxFes != nullptr,
"Gravity initialization requires the "
f.gravityFluxFes != nullptr, "Gravity initialization requires the "
"gravity-gradient finite-element space."
);
MFEM_VERIFY(
f.displacementFes != nullptr, "Gravity initialization requires the "
"displacement finite-element space."
);
MFEM_VERIFY(f.domainMapperStateless != nullptr, "Gravity initialization requires the stateless domain mapper.");
MFEM_VERIFY(
f.domainMapperStateless != nullptr,
"Gravity initialization requires the stateless domain mapper."
);
MFEM_VERIFY(
f.gravityContext.b_form != nullptr,
"Gravity initialization requires the divergence operator."
);
MFEM_VERIFY(
f.gravityContext.BT != nullptr,
"Gravity initialization requires the transpose divergence operator."
);
MFEM_VERIFY(
f.gravityContext.block_prec != nullptr,
"Gravity initialization requires the gravity block preconditioner."
);
MFEM_VERIFY(
rho.FESpace() == f.densityFes.get(),
"Gravity initialization requires density to use the FEM density "
rho.FESpace() == f.densityFes.get(), "Gravity initialization requires density to use the FEM density "
"space."
);
MFEM_VERIFY(
@@ -536,99 +141,153 @@ namespace mean_field::physics {
"Vec_H1 "
"space."
);
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;
constexpr auto gravity_gradient_residual_block =
utils::blocks::get_residual_block<form>(
utils::blocks::gravity_field.gradient_term
);
utils::blocks::get_residual_block<form>(utils::blocks::gravity_field.gradient_term);
constexpr auto gravity_poisson_residual_block =
utils::blocks::get_residual_block<form>(
utils::blocks::gravity_field.poisson_term
utils::blocks::get_residual_block<form>(utils::blocks::gravity_field.poisson_term);
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
const field::FieldDofGridFunctionAdapter density_adapter = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: density map", 0,
field::make_field_dof_grid_function_adapter<field::Density, DomainSchema>(*f.densityFes)
);
const field::FieldDofGridFunctionAdapter displacement_adapter = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: displacement map", 0,
field::make_field_dof_grid_function_adapter<field::Displacement, DomainSchema>(*f.displacementFes)
);
const field::FieldDofGridFunctionAdapter gravity_flux_adapter = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: flux map", 0,
field::make_field_dof_grid_function_adapter<field::Gravity, DomainSchema>(*f.gravityFluxFes)
);
const field::FieldDofGridFunctionAdapter gravity_potential_adapter = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: potential map", 0,
field::make_field_dof_grid_function_adapter<field::Gravity, DomainSchema>(*f.gravityPotentialFes)
);
const field::FieldDofMap &density_map = density_adapter.dof_map();
const field::FieldDofMap &displacement_map = displacement_adapter.dof_map();
const field::FieldDofMap &gravity_flux_map = gravity_flux_adapter.dof_map();
const field::FieldDofMap &gravity_potential_map = gravity_potential_adapter.dof_map();
const std::array<int, form::value_block_count> value_sizes{
f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(),
f.gravityFluxFes->GetTrueVSize(),
f.gravityPotentialFes->GetTrueVSize()
density_map.reduced_size(), displacement_map.reduced_size(), gravity_flux_map.reduced_size(),
gravity_potential_map.reduced_size()
};
const std::array<int, form::residual_block_count> residual_sizes{
f.gravityFluxFes->GetTrueVSize(),
f.gravityPotentialFes->GetTrueVSize()
gravity_flux_map.reduced_size(), gravity_potential_map.reduced_size()
};
const utils::blocks::form_layout<form> layout(
value_sizes, residual_sizes
const utils::blocks::form_layout<form> layout = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: block layout", 0, utils::blocks::form_layout<form>(value_sizes, residual_sizes)
);
mfem::Vector density_true;
mfem::Vector displacement_true;
grid_function_to_true_dofs(*f.densityFes, rho, density_true);
grid_function_to_true_dofs(
*f.displacementFes, displacement, displacement_true
const mfem::Vector density =
MEAN_FIELD_PROFILE_EVALUATE_WARMUP("gravity solve: gather density", 0, density_adapter.gather(rho));
const mfem::Vector reduced_displacement = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: gather displacement", 0, displacement_adapter.gather(displacement)
);
operators::context::gravity_field::GravityFieldLinearizationContext
linearization_context(f, *f.domainMapperStateless);
operators::GravityFieldJacobianOperator gravity_jacobian(
f, *f.domainMapperStateless, linearization_context,
layout.value_offsets(), layout.residual_offsets()
operators::context::gravity_field::GravityFieldLinearizationContext linearization_context =
MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: linearization context", 0,
operators::context::gravity_field::GravityFieldLinearizationContext(f, *f.domainMapperStateless)
);
operators::GravityFieldOperator gravity_operator(
f, *f.domainMapperStateless, linearization_context,
layout.value_offsets(), gravity_jacobian
operators::GravityFieldJacobianOperator gravity_jacobian = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: jacobian operator", 0,
operators::GravityFieldJacobianOperator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()
)
);
operators::context::gravity_field::GravityFieldGeometryContext
reduced_geometry_context(f, *f.domainMapperStateless);
operators::GravityFieldOperator gravity_operator = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: nonlinear operator", 0,
operators::GravityFieldOperator(
f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian
)
);
operators::ReducedGravityFieldOperator reduced_operator(
gravity_operator, reduced_geometry_context, displacement_true
operators::context::gravity_field::GravityFieldGeometryContext reduced_geometry_context =
MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: reduced geometry context", 0,
operators::context::gravity_field::GravityFieldGeometryContext(f, *f.domainMapperStateless)
);
operators::ReducedGravityFieldOperator reduced_operator = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: reduced operator", 0,
operators::ReducedGravityFieldOperator(gravity_operator, reduced_geometry_context, reduced_displacement)
);
operators::ReducedGravityFieldPreconditioner reduced_preconditioner = MEAN_FIELD_PROFILE_EVALUATE_WARMUP(
"gravity solve: preconditioner construction", 0,
operators::ReducedGravityFieldPreconditioner(f, reduced_geometry_context)
);
mfem::Vector right_hand_side;
reduced_operator.BuildRightHandSide(density_true, right_hand_side);
MEAN_FIELD_PROFILE_CALL_WARMUP(
"gravity solve: right-hand side", 0, reduced_operator.BuildRightHandSide(density, right_hand_side)
);
MFEM_VERIFY(
right_hand_side.Size() == reduced_operator.Height(),
"The reduced gravity right-hand side has the wrong size."
);
mfem::BlockVector gravity_state(
reduced_operator.GetGravityTrueOffsets()
);
mfem::BlockVector gravity_state(reduced_operator.GetGravityOffsets());
gravity_state = 0.0;
mfem::MINRESSolver minres(f.mesh->GetComm());
minres.SetOperator(reduced_operator);
minres.SetPreconditioner(*f.gravityContext.block_prec);
minres.SetRelTol(args.p.rtol);
minres.SetAbsTol(args.p.atol);
minres.SetMaxIter(args.p.max_iters);
minres.SetPrintLevel(1);
minres.Mult(right_hand_side, gravity_state);
minres.SetPreconditioner(reduced_preconditioner);
minres.SetRelTol(options.relativeTolerance);
minres.SetAbsTol(options.absoluteTolerance);
minres.SetMaxIter(options.maximumIterations);
// minres.SetPrintLevel(args.verbose ? 1 : 0);
minres.SetPrintLevel(0);
MEAN_FIELD_PROFILE_CALL_WARMUP("gravity solve: MINRES", 0, minres.Mult(right_hand_side, gravity_state));
MEAN_FIELD_PROFILE_COUNT("gravity solve: MINRES iterations", minres.GetNumIterations());
MFEM_VERIFY(
minres.GetConverged(),
"The reduced gravity solve failed to converge."
);
MFEM_VERIFY(minres.GetConverged(), "The reduced gravity solve failed to converge.");
GravitySolution solution(f);
solution.gradPhi.SetFromTrueDofs(
gravity_state.GetBlock(gravity_gradient_residual_block)
);
solution.phi.SetFromTrueDofs(
gravity_state.GetBlock(gravity_poisson_residual_block)
MEAN_FIELD_PROFILE_CALL_WARMUP(
"gravity solve: scatter solution", 0,
gravity_flux_adapter.scatter(gravity_state.GetBlock(gravity_gradient_residual_block), solution.gradPhi);
gravity_potential_adapter.scatter(gravity_state.GetBlock(gravity_poisson_residual_block), solution.phi)
);
return solution;
}
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

View File

@@ -10,23 +10,22 @@ namespace mean_field::physics {
const mfem::GridFunction &rho_ref
) {
double local_I = 0.0;
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
for (int i = 0; i < fem.mesh->GetNE(); i++) {
if (fem.mesh->GetAttribute(i) == 3)
if (!DomainSchema::template attribute_belongs_to<utils::domain::Stellar>(fem.mesh->GetAttribute(i)))
continue;
mfem::ElementTransformation *T =
fem.mesh->GetElementTransformation(i);
mfem::ElementTransformation *T = fem.mesh->GetElementTransformation(i);
using DensityField = field::Field<field::Density>;
const quadrature::Query query =
DensityField::make_query<field::Density::Form::Quadrupole>(
quadrature::QuadratureRole::diagnostic, T->OrderW(),
std::array<int, 1>{2}, utils::DOMAINS::STELLAR,
const quadrature::Query query = DensityField::make_query<field::Density::Form::Quadrupole>(
quadrature::QuadratureRole::diagnostic, T->OrderW(), std::array<int, 1>{2}, utils::DOMAINS::STELLAR,
quadrature::MappingKind::general
);
const mfem::IntegrationRule &ir =
*fem.quadratureFactory->get(query, T->GetGeometryType())
.integration_rule;
const mfem::IntegrationRule &ir = *fem.quadratureFactory->get(query, T->GetGeometryType()).integration_rule;
for (int j = 0; j < ir.GetNPoints(); j++) {
const mfem::IntegrationPoint &ip = ir.IntPoint(j);
@@ -34,22 +33,22 @@ namespace mean_field::physics {
const double rho_hat = rho_ref.GetValue(i, ip);
mfem::Vector x_phys;
fem.mapping->GetPhysicalPoint(*T, ip, x_phys);
mapping::VolumeMappingContext mapping_context;
MFEM_VERIFY(
mapping_evaluator.EvaluateVolume(*T, ip, mapping_context) == mapping::MappingStatus::valid,
"Moment-of-inertia integration encountered an invalid mapping."
);
const mfem::Vector &x_phys = mapping_context.mapping.physical_position;
const double r_cyl_sq =
x_phys(0) * x_phys(0) + x_phys(1) * x_phys(1);
const double detJ = std::fabs(fem.mapping->ComputeDetJ(*T, ip));
const double weight = T->Weight() * ip.weight * detJ;
const double r_cyl_sq = x_phys(0) * x_phys(0) + x_phys(1) * x_phys(1);
const double weight = mapping_context.quadrature.weight;
local_I += rho_hat * r_cyl_sq * weight;
}
}
double global_I = 0.0;
MPI_Allreduce(
&local_I, &global_I, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm()
);
MPI_Allreduce(&local_I, &global_I, 1, MPI_DOUBLE, MPI_SUM, fem.mesh->GetComm());
return global_I;
}

View File

@@ -0,0 +1,77 @@
module;
#include <cmath>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
module mean_field;
import :preconditioning.gravity_field;
namespace mean_field::preconditioning {
std::unique_ptr<mfem::HypreParMatrix> assembleGravityDivergenceSurrogate(const fem::FEM &f) {
if (f.mesh == nullptr || f.gravityFluxFes == nullptr || f.gravityPotentialFes == nullptr ||
f.quadratureFactory == nullptr) {
throw std::invalid_argument(
"The gravity divergence surrogate requires its mesh, gravity spaces, and quadrature policy."
);
}
mfem::ParMixedBilinearForm divergence(f.gravityFluxFes.get(), f.gravityPotentialFes.get());
auto integrator = std::make_unique<mfem::VectorFEDivergenceIntegrator>();
const mfem::FiniteElement &trialElement = *f.gravityFluxFes->GetTypicalFE();
const mfem::FiniteElement &testElement = *f.gravityPotentialFes->GetTypicalFE();
const mfem::ElementTransformation &transformation = *f.mesh->GetElementTransformation(0);
f.quadratureFactory->configure_gravity_divergence(
*integrator, quadrature::QuadratureRole::preconditioner, trialElement, testElement, transformation,
utils::DOMAINS::ALL, quadrature::MappingKind::none
);
divergence.AddDomainIntegrator(integrator.release());
divergence.Assemble();
divergence.Finalize();
std::unique_ptr<mfem::HypreParMatrix> assembled(divergence.ParallelAssemble());
if (assembled == nullptr) {
throw std::runtime_error("MFEM did not assemble the gravity divergence surrogate.");
}
return assembled;
}
std::unique_ptr<mfem::HypreParMatrix> assembleGravityPotentialSchurSurrogate(
const fem::FEM &f,
const mfem::Vector &trueMassDiagonal
) {
if (f.gravityFluxFes == nullptr || trueMassDiagonal.Size() != f.gravityFluxFes->GetTrueVSize()) {
throw std::invalid_argument(
"The gravity Schur surrogate requires one mass-diagonal entry per true gravity-gradient DOF."
);
}
mfem::Vector inverseMassDiagonal(trueMassDiagonal);
for (int index = 0; index < inverseMassDiagonal.Size(); ++index) {
const double entry = inverseMassDiagonal(index);
if (!std::isfinite(entry) || entry <= 0.0) {
throw std::invalid_argument(
"The gravity Schur surrogate encountered a non-positive or non-finite mass diagonal."
);
}
inverseMassDiagonal(index) = 1.0 / entry;
}
std::unique_ptr<mfem::HypreParMatrix> divergence = assembleGravityDivergenceSurrogate(f);
std::unique_ptr<mfem::HypreParMatrix> inverseMassDivergenceTranspose(divergence->Transpose());
inverseMassDivergenceTranspose->ScaleRows(inverseMassDiagonal);
std::unique_ptr<mfem::HypreParMatrix> schur(
mfem::ParMult(divergence.get(), inverseMassDivergenceTranspose.get())
);
if (schur == nullptr) {
throw std::runtime_error("MFEM did not assemble the gravity potential-Schur surrogate.");
}
return schur;
}
} // namespace mean_field::preconditioning

View File

@@ -0,0 +1,530 @@
#include "profile.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <mutex>
#include <set>
#include <sstream>
#include <stdexcept>
#include <utility>
namespace {
struct MpiContext {
bool active{false};
int rank{0};
int size{1};
};
void check_mpi(
const int result,
const std::string_view operation
) {
if (result == MPI_SUCCESS) {
return;
}
std::array<char, MPI_MAX_ERROR_STRING> buffer{};
int length = 0;
MPI_Error_string(result, buffer.data(), &length);
throw std::runtime_error(
"MPI profiling operation '" + std::string(operation) +
"' failed: " + std::string(buffer.data(), static_cast<std::size_t>(length))
);
}
[[nodiscard]] MpiContext get_mpi_context(const MPI_Comm communicator) {
int initialized = 0;
check_mpi(MPI_Initialized(&initialized), "MPI_Initialized");
if (initialized == 0) {
return {};
}
int finalized = 0;
check_mpi(MPI_Finalized(&finalized), "MPI_Finalized");
if (finalized != 0) {
return {};
}
if (communicator == MPI_COMM_NULL) {
throw std::invalid_argument("Profiling aggregation requires a valid MPI communicator.");
}
MpiContext context{.active = true};
check_mpi(MPI_Comm_rank(communicator, &context.rank), "MPI_Comm_rank");
check_mpi(MPI_Comm_size(communicator, &context.size), "MPI_Comm_size");
return context;
}
[[nodiscard]] std::string count_range(
const std::uint64_t minimum,
const std::uint64_t maximum
) {
if (minimum == maximum) {
return std::to_string(minimum);
}
return std::to_string(minimum) + "-" + std::to_string(maximum);
}
void write_csv_field(
std::ostream &stream,
const std::string_view field
) {
stream << '"';
for (const char character : field) {
if (character == '"') {
stream << "\"\"";
} else {
stream << character;
}
}
stream << '"';
}
} // namespace
namespace mean_field::profiling {
struct Registry::Impl {
struct Entry {
std::string label;
Statistics statistics;
};
mutable std::mutex mutex;
std::map<std::string, std::size_t, std::less<>> indices;
std::vector<Entry> entries;
};
Registry &Registry::Get() {
static Registry registry;
return registry;
}
Registry::Registry() : m_impl(std::make_unique<Impl>()) {
}
Registry::~Registry() = default;
std::size_t Registry::Register(
const std::string_view label,
const std::uint64_t warmup_count
) {
if (label.empty()) {
throw std::invalid_argument("A profiling region label cannot be empty.");
}
if (label.find('\0') != std::string_view::npos) {
throw std::invalid_argument("A profiling region label cannot contain a null byte.");
}
std::scoped_lock lock(m_impl->mutex);
if (const auto iterator = m_impl->indices.find(label); iterator != m_impl->indices.end()) {
Impl::Entry &entry = m_impl->entries[iterator->second];
entry.statistics.warmup_target = std::max(entry.statistics.warmup_target, warmup_count);
return iterator->second;
}
const std::size_t index = m_impl->entries.size();
Impl::Entry entry{.label = std::string(label)};
entry.statistics.warmup_target = warmup_count;
m_impl->entries.push_back(std::move(entry));
m_impl->indices.emplace(m_impl->entries.back().label, index);
return index;
}
void Registry::Record(
const std::string_view label,
const double seconds,
const std::uint64_t warmup_count
) {
if (!std::isfinite(seconds) || seconds < 0.0) {
throw std::invalid_argument("A profiling duration must be finite and nonnegative.");
}
const std::size_t region = Register(label, warmup_count);
std::scoped_lock lock(m_impl->mutex);
Statistics &statistics = m_impl->entries[region].statistics;
const bool is_warmup = statistics.observations < statistics.warmup_target;
++statistics.observations;
if (is_warmup) {
++statistics.warmups;
return;
}
++statistics.samples;
statistics.total_seconds += seconds;
if (statistics.samples == 1) {
statistics.minimum_seconds = seconds;
statistics.maximum_seconds = seconds;
} else {
statistics.minimum_seconds = std::min(statistics.minimum_seconds, seconds);
statistics.maximum_seconds = std::max(statistics.maximum_seconds, seconds);
}
}
void Registry::AddCount(
const std::string_view label,
const std::uint64_t work_units
) {
const std::size_t region = Register(label, 0);
std::scoped_lock lock(m_impl->mutex);
Statistics &statistics = m_impl->entries[region].statistics;
if (work_units > std::numeric_limits<std::uint64_t>::max() - statistics.work_units) {
throw std::overflow_error("A profiling work counter overflowed.");
}
statistics.work_units += work_units;
}
void Registry::Record(
const std::size_t region,
const double seconds
) noexcept {
if (!std::isfinite(seconds) || seconds < 0.0) {
return;
}
try {
std::scoped_lock lock(m_impl->mutex);
if (region >= m_impl->entries.size()) {
return;
}
Statistics &statistics = m_impl->entries[region].statistics;
const bool is_warmup = statistics.observations < statistics.warmup_target;
++statistics.observations;
if (is_warmup) {
++statistics.warmups;
return;
}
++statistics.samples;
statistics.total_seconds += seconds;
if (statistics.samples == 1) {
statistics.minimum_seconds = seconds;
statistics.maximum_seconds = seconds;
} else {
statistics.minimum_seconds = std::min(statistics.minimum_seconds, seconds);
statistics.maximum_seconds = std::max(statistics.maximum_seconds, seconds);
}
} catch (...) {
}
}
void Registry::AddCount(
const std::size_t region,
const std::uint64_t work_units
) noexcept {
try {
std::scoped_lock lock(m_impl->mutex);
if (region >= m_impl->entries.size()) {
return;
}
Statistics &statistics = m_impl->entries[region].statistics;
if (work_units > std::numeric_limits<std::uint64_t>::max() - statistics.work_units) {
statistics.work_units = std::numeric_limits<std::uint64_t>::max();
} else {
statistics.work_units += work_units;
}
} catch (...) {
}
}
void Registry::Reset() {
std::scoped_lock lock(m_impl->mutex);
for (Impl::Entry &entry : m_impl->entries) {
const std::uint64_t warmup_target = entry.statistics.warmup_target;
entry.statistics = {};
entry.statistics.warmup_target = warmup_target;
}
}
std::map<
std::string,
Statistics,
std::less<>>
Registry::Snapshot() const {
std::map<std::string, Statistics, std::less<>> snapshot;
std::scoped_lock lock(m_impl->mutex);
for (const Impl::Entry &entry : m_impl->entries) {
snapshot.emplace(entry.label, entry.statistics);
}
return snapshot;
}
std::vector<DistributedStatistics> Registry::Aggregate(const MPI_Comm communicator) const {
const std::map<std::string, Statistics, std::less<>> local_snapshot = Snapshot();
const MpiContext mpi_context = get_mpi_context(communicator);
std::vector<std::string> labels;
if (!mpi_context.active) {
labels.reserve(local_snapshot.size());
for (const auto &[label, statistics] : local_snapshot) {
(void)statistics;
labels.push_back(label);
}
} else {
std::string serialized_labels;
for (const auto &[label, statistics] : local_snapshot) {
(void)statistics;
serialized_labels.append(label);
serialized_labels.push_back('\0');
}
if (serialized_labels.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
throw std::overflow_error("The local profiling label table is too large for MPI_Allgatherv.");
}
const int local_bytes = static_cast<int>(serialized_labels.size());
std::vector<int> byte_counts(static_cast<std::size_t>(mpi_context.size));
check_mpi(
MPI_Allgather(&local_bytes, 1, MPI_INT, byte_counts.data(), 1, MPI_INT, communicator),
"MPI_Allgather(profile label sizes)"
);
std::vector<int> displacements(static_cast<std::size_t>(mpi_context.size));
int total_bytes = 0;
for (int rank = 0; rank < mpi_context.size; ++rank) {
if (byte_counts[rank] < 0 || byte_counts[rank] > std::numeric_limits<int>::max() - total_bytes) {
throw std::overflow_error("The distributed profiling label table is too large for MPI_Allgatherv.");
}
displacements[rank] = total_bytes;
total_bytes += byte_counts[rank];
}
std::vector<char> all_serialized_labels(static_cast<std::size_t>(total_bytes));
check_mpi(
MPI_Allgatherv(
serialized_labels.data(), local_bytes, MPI_CHAR, all_serialized_labels.data(), byte_counts.data(),
displacements.data(), MPI_CHAR, communicator
),
"MPI_Allgatherv(profile labels)"
);
std::set<std::string, std::less<>> unique_labels;
for (int rank = 0; rank < mpi_context.size; ++rank) {
const char *position = all_serialized_labels.data() + displacements[rank];
const char *end = position + byte_counts[rank];
while (position != end) {
const void *terminator_address =
std::memchr(position, '\0', static_cast<std::size_t>(end - position));
if (terminator_address == nullptr) {
throw std::runtime_error("A distributed profiling label table is malformed.");
}
const auto *terminator = static_cast<const char *>(terminator_address);
unique_labels.emplace(position, terminator);
position = terminator + 1;
}
}
labels.assign(unique_labels.begin(), unique_labels.end());
}
std::vector<DistributedStatistics> aggregate(labels.size());
if (labels.empty()) {
return aggregate;
}
std::vector<std::uint64_t> local_samples(labels.size(), 0);
std::vector<std::uint64_t> local_warmups(labels.size(), 0);
std::vector<std::uint64_t> local_work_units(labels.size(), 0);
std::vector<double> local_averages(labels.size(), 0.0);
std::vector<double> local_minima(labels.size(), std::numeric_limits<double>::infinity());
std::vector<double> local_maxima(labels.size(), 0.0);
std::vector<double> local_totals(labels.size(), 0.0);
for (std::size_t index = 0; index < labels.size(); ++index) {
if (const auto iterator = local_snapshot.find(labels[index]); iterator != local_snapshot.end()) {
const Statistics &statistics = iterator->second;
local_samples[index] = statistics.samples;
local_warmups[index] = statistics.warmups;
local_work_units[index] = statistics.work_units;
local_totals[index] = statistics.total_seconds;
if (statistics.samples != 0) {
local_averages[index] = statistics.total_seconds / static_cast<double>(statistics.samples);
local_minima[index] = statistics.minimum_seconds;
local_maxima[index] = statistics.maximum_seconds;
}
}
}
std::vector<std::uint64_t> minimum_samples = local_samples;
std::vector<std::uint64_t> maximum_samples = local_samples;
std::vector<std::uint64_t> maximum_warmups = local_warmups;
std::vector<std::uint64_t> minimum_work_units = local_work_units;
std::vector<std::uint64_t> maximum_work_units = local_work_units;
std::vector<double> maximum_rank_averages = local_averages;
std::vector<double> global_minima = local_minima;
std::vector<double> global_maxima = local_maxima;
std::vector<double> maximum_rank_totals = local_totals;
if (mpi_context.active) {
if (labels.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
throw std::overflow_error("There are too many profiling regions for one MPI reduction.");
}
const int count = static_cast<int>(labels.size());
check_mpi(
MPI_Allreduce(local_samples.data(), minimum_samples.data(), count, MPI_UINT64_T, MPI_MIN, communicator),
"MPI_Allreduce(minimum profile samples)"
);
check_mpi(
MPI_Allreduce(local_samples.data(), maximum_samples.data(), count, MPI_UINT64_T, MPI_MAX, communicator),
"MPI_Allreduce(maximum profile samples)"
);
check_mpi(
MPI_Allreduce(local_warmups.data(), maximum_warmups.data(), count, MPI_UINT64_T, MPI_MAX, communicator),
"MPI_Allreduce(profile warmups)"
);
check_mpi(
MPI_Allreduce(
local_work_units.data(), minimum_work_units.data(), count, MPI_UINT64_T, MPI_MIN, communicator
),
"MPI_Allreduce(minimum profile work)"
);
check_mpi(
MPI_Allreduce(
local_work_units.data(), maximum_work_units.data(), count, MPI_UINT64_T, MPI_MAX, communicator
),
"MPI_Allreduce(maximum profile work)"
);
check_mpi(
MPI_Allreduce(
local_averages.data(), maximum_rank_averages.data(), count, MPI_DOUBLE, MPI_MAX, communicator
),
"MPI_Allreduce(profile averages)"
);
check_mpi(
MPI_Allreduce(local_minima.data(), global_minima.data(), count, MPI_DOUBLE, MPI_MIN, communicator),
"MPI_Allreduce(profile minima)"
);
check_mpi(
MPI_Allreduce(local_maxima.data(), global_maxima.data(), count, MPI_DOUBLE, MPI_MAX, communicator),
"MPI_Allreduce(profile maxima)"
);
check_mpi(
MPI_Allreduce(
local_totals.data(), maximum_rank_totals.data(), count, MPI_DOUBLE, MPI_MAX, communicator
),
"MPI_Allreduce(profile totals)"
);
}
for (std::size_t index = 0; index < labels.size(); ++index) {
aggregate[index] = {
.label = labels[index],
.minimum_samples = minimum_samples[index],
.maximum_samples = maximum_samples[index],
.maximum_warmups = maximum_warmups[index],
.minimum_work_units = minimum_work_units[index],
.maximum_work_units = maximum_work_units[index],
.maximum_rank_average_seconds = maximum_rank_averages[index],
.global_minimum_seconds = std::isfinite(global_minima[index]) ? global_minima[index] : 0.0,
.global_maximum_seconds = global_maxima[index],
.maximum_rank_total_seconds = maximum_rank_totals[index]
};
}
return aggregate;
}
void Registry::Print(
const MPI_Comm communicator,
std::ostream &stream
) const {
const std::vector<DistributedStatistics> aggregate = Aggregate(communicator);
const MpiContext mpi_context = get_mpi_context(communicator);
if (mpi_context.rank != 0) {
return;
}
std::ios old_state(nullptr);
old_state.copyfmt(stream);
stream << '\n';
stream << std::left << std::setw(58) << "Profile Region" << std::right << std::setw(13) << "Samples"
<< std::setw(11) << "Warmups" << std::setw(15) << "Work/rank" << std::setw(14) << "Avg max ms"
<< std::setw(14) << "Min ms" << std::setw(14) << "Max ms" << std::setw(14) << "Total max s" << '\n';
stream << std::string(153, '-') << '\n';
for (const DistributedStatistics &statistics : aggregate) {
stream << std::left << std::setw(58) << statistics.label << std::right << std::setw(13)
<< count_range(statistics.minimum_samples, statistics.maximum_samples) << std::setw(11)
<< statistics.maximum_warmups << std::setw(15)
<< count_range(statistics.minimum_work_units, statistics.maximum_work_units) << std::setw(14)
<< std::fixed << std::setprecision(3) << 1.0e3 * statistics.maximum_rank_average_seconds
<< std::setw(14) << 1.0e3 * statistics.global_minimum_seconds << std::setw(14)
<< 1.0e3 * statistics.global_maximum_seconds << std::setw(14) << std::setprecision(6)
<< statistics.maximum_rank_total_seconds << '\n';
}
stream << std::string(153, '=') << '\n';
stream << "MPI ranks: " << mpi_context.size << "\n\n";
stream.copyfmt(old_state);
}
void Registry::Print(const MPI_Comm communicator) const {
Print(communicator, std::cout);
}
void Registry::PrintCsv(
const MPI_Comm communicator,
std::ostream &stream
) const {
const std::vector<DistributedStatistics> aggregate = Aggregate(communicator);
const MpiContext mpi_context = get_mpi_context(communicator);
if (mpi_context.rank != 0) {
return;
}
stream << "label,minimum_samples,maximum_samples,maximum_warmups,minimum_work_units,maximum_work_units,"
"maximum_rank_average_seconds,global_minimum_seconds,global_maximum_seconds,"
"maximum_rank_total_seconds,mpi_ranks\n";
for (const DistributedStatistics &statistics : aggregate) {
write_csv_field(stream, statistics.label);
stream << ',' << statistics.minimum_samples << ',' << statistics.maximum_samples << ','
<< statistics.maximum_warmups << ',' << statistics.minimum_work_units << ','
<< statistics.maximum_work_units << ',' << std::setprecision(17)
<< statistics.maximum_rank_average_seconds << ',' << statistics.global_minimum_seconds << ','
<< statistics.global_maximum_seconds << ',' << statistics.maximum_rank_total_seconds << ','
<< mpi_context.size << '\n';
}
}
Region::Region(
const std::string_view label,
const std::uint64_t warmup_count
)
: m_region(
Registry::Get().Register(
label,
warmup_count
)
) {
}
void Region::Record(const double seconds) const noexcept {
Registry::Get().Record(m_region, seconds);
}
void Region::AddCount(const std::uint64_t work_units) const noexcept {
Registry::Get().AddCount(m_region, work_units);
}
ScopedTimer::ScopedTimer(const Region &region) noexcept
: m_region(region),
m_start(std::chrono::steady_clock::now()) {
}
ScopedTimer::~ScopedTimer() noexcept {
const auto stop = std::chrono::steady_clock::now();
m_region.Record(std::chrono::duration<double>(stop - m_start).count());
}
} // namespace mean_field::profiling

View File

@@ -0,0 +1,248 @@
module;
#include <algorithm>
#include <cmath>
#include <numbers>
#include <optional>
#include <stdexcept>
#include <vector>
#include <mfem.hpp>
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<LaneEmdenPoint> 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<double>(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<LaneEmdenPoint> 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<LaneEmdenPoint> &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<LaneEmdenPoint> points = solve_lane_emden(polytropicIndex, coordinateLimit, integrationStep);
DimensionlessLaneEmdenSolution solution{
.coordinate = mfem::Vector(static_cast<int>(points.size())),
.theta = mfem::Vector(static_cast<int>(points.size())),
.thetaDerivative = mfem::Vector(static_cast<int>(points.size())),
.firstZeroCoordinate = std::nullopt
};
for (int index = 0; index < static_cast<int>(points.size()); ++index) {
solution.coordinate(index) = points[static_cast<std::size_t>(index)].coordinate;
solution.theta(index) = points[static_cast<std::size_t>(index)].value;
solution.thetaDerivative(index) = points[static_cast<std::size_t>(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<dimensions::quantity::SpecificEnthalpy>(equationOfState, centralDensity);
const double radialScaleSquared = centralEnthalpy.value() / (4.0 * std::numbers::pi_v<double> *
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<double>(sampleIndex) / static_cast<double>(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<dimensions::quantity::SpecificEnthalpy>(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

View File

@@ -0,0 +1,218 @@
module;
#include <algorithm>
#include <cmath>
#include <limits>
#include <numbers>
#include <stdexcept>
#include <mfem.hpp>
#include <mpi.h>
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<double>::epsilon() * radialScale ||
std::abs(profile.radius(surfaceIndex) - profile.stellarRadius.value()) >
64.0 * std::numeric_limits<double>::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<DomainSchema>(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<double>::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(
fem::FEM &finiteElementModel,
const RadialProfile &profile,
const dimensions::MassValue targetMass,
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.");
}
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);
double radialMomentIntegral = 0.0;
for (int index = 0; index + 1 < profile.radius.Size(); ++index) {
const double leftRadius = profile.radius(index);
const double rightRadius = profile.radius(index + 1);
const double leftIntegrand = profile.density(index) * std::pow(leftRadius, 4);
const double rightIntegrand = profile.density(index + 1) * std::pow(rightRadius, 4);
radialMomentIntegral += 0.5 * (rightRadius - leftRadius) * (leftIntegrand + rightIntegrand);
}
const double sphericalMomentOfInertia = (8.0 * std::numbers::pi / 3.0) * radialMomentIntegral;
if (!std::isfinite(sphericalMomentOfInertia) || sphericalMomentOfInertia <= 0.0) {
throw std::runtime_error("The radial profile has no finite, positive moment of inertia.");
}
const field::FieldDofGridFunctionAdapter densityAdapter =
field::make_field_dof_grid_function_adapter<field::Density, DomainSchema>(*finiteElementModel.densityFes);
const field::FieldDofGridFunctionAdapter enthalpyAdapter =
field::make_field_dof_grid_function_adapter<field::Enthalpy, DomainSchema>(*finiteElementModel.enthalpyFes);
const field::FieldDofGridFunctionAdapter gravityFluxAdapter =
field::make_field_dof_grid_function_adapter<field::Gravity, DomainSchema>(
*finiteElementModel.gravityFluxFes
);
const field::FieldDofGridFunctionAdapter gravityPotentialAdapter =
field::make_field_dof_grid_function_adapter<field::Gravity, DomainSchema>(
*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,
.sphericalMomentOfInertia = sphericalMomentOfInertia
};
}
} // namespace mean_field::seed::detail

View File

@@ -0,0 +1,638 @@
module;
#include <algorithm>
#include <chrono>
#include <cmath>
#include <complex>
#include <cstdint>
#include <limits>
#include <memory>
#include <ranges>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include <Eigen/Dense>
#include <Eigen/Eigenvalues>
#include <Eigen/SVD>
#include <mfem.hpp>
#include <mpi.h>
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<double>(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<unsigned long long>(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<std::uint64_t>(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<unsigned long long>(local.setups);
unsigned long long localRefreshes = static_cast<unsigned long long>(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<std::uint64_t>(maximumSetups);
result.refreshes = static_cast<std::uint64_t>(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<IterationResidualMeasurement> &ResidualHistoryMonitor::GetHistory() const noexcept {
return m_history;
}
DirectResidualMeasurement measureDirectResidual(
const mfem::Operator &jacobian,
const mfem::Vector &rightHandSide,
const mfem::Vector &solution,
const std::span<const operators::RootBlockDescriptor> 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<mfem::real_t *>(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<const operators::RootBlockDescriptor> 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<double>(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<mfem::Vector> basis;
basis.reserve(static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(row)], candidate, communicator);
hessenberg(row, column) += projection;
candidate.Add(-projection, basis[static_cast<std::size_t>(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<Eigen::MatrixXd> eigenSolver(projected, true);
if (eigenSolver.info() != Eigen::Success) {
throw std::runtime_error("The projected Arnoldi eigenproblem did not converge.");
}
Eigen::JacobiSVD<Eigen::MatrixXd> 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<std::size_t>(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<double>::infinity();
const double finalSubdiagonal = hessenberg(achievedDimension, achievedDimension - 1);
std::complex<double> 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<double> 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<double>::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<double>{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<double>(achievedDimension);
measurement.centroidRealPart = centroid.real();
measurement.centroidImaginaryPart = centroid.imag();
measurement.minimumMagnitude = std::numeric_limits<double>::infinity();
measurement.minimumRealPart = std::numeric_limits<double>::infinity();
measurement.maximumRealPart = -std::numeric_limits<double>::infinity();
double squaredDistanceFromOne{0.0};
double squaredClusterRadius{0.0};
for (const RitzValueMeasurement &ritz : measurement.ritzValues) {
const std::complex<double> 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<double>::infinity();
for (const RitzValueMeasurement &candidate : measurement.ritzValues) {
pairDefect = std::min(
pairDefect,
std::abs(std::complex<double>{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<Eigen::MatrixXd> 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<RitzValueMeasurement> 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<RitzValueMeasurement> 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<int>(selected.size()) > count) {
selected.resize(static_cast<std::size_t>(count));
}
return selected;
}
} // namespace mean_field::solver

View File

@@ -12,6 +12,9 @@ namespace mean_field::utils {
) {
const int dim = fem.mesh->Dimension();
x_ref = x_phys_target;
mapping::GridFunctionMappingEvaluator mapping_evaluator(
*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate
);
mfem::Array<int> init_elem;
mfem::Array<mfem::IntegrationPoint> init_ip;
@@ -29,17 +32,17 @@ namespace mean_field::utils {
mfem::Array<mfem::IntegrationPoint> origin_ip;
fem.mesh->FindPoints(P_origin, origin_elem, origin_ip, false);
if (origin_elem.Size() > 0 && origin_elem[0] >= 0 &&
fem.mapping->HasDisplacementField()) {
mfem::ElementTransformation *T0 =
fem.mesh->GetElementTransformation(origin_elem[0]);
if (origin_elem.Size() > 0 && origin_elem[0] >= 0) {
mfem::ElementTransformation *T0 = fem.mesh->GetElementTransformation(origin_elem[0]);
T0->SetIntPoint(&origin_ip[0]);
mfem::DenseMatrix J0(dim, dim), J0_inv(dim, dim);
fem.mapping->ComputeJacobian(*T0, J0);
mfem::CalcInverse(J0, J0_inv);
mapping::MappingPointContext context;
MFEM_VERIFY(
mapping_evaluator.EvaluatePoint(*T0, origin_ip[0], context) == mapping::MappingStatus::valid,
"Reference-point initialization encountered an invalid mapping."
);
J0_inv.Mult(x_phys_target, x_ref);
context.inverse_mapping_jacobian.Mult(x_phys_target, x_ref);
}
init_P.SetCol(0, x_ref);
@@ -72,9 +75,6 @@ namespace mean_field::utils {
mfem::Vector residual(dim);
mfem::Vector step(dim);
mfem::DenseMatrix J_map(dim, dim);
mfem::DenseMatrix J_map_inv(dim, dim);
int find_failures = 0;
for (int iter = 0; iter < max_iter; ++iter) {
@@ -98,12 +98,14 @@ namespace mean_field::utils {
int elemID = elem_ids[0];
const mfem::IntegrationPoint &ip = ips[0];
mfem::ElementTransformation *T =
fem.mesh->GetElementTransformation(elemID);
mfem::ElementTransformation *T = fem.mesh->GetElementTransformation(elemID);
T->SetIntPoint(&ip);
mfem::Vector current_x_phys(dim);
fem.mapping->GetPhysicalPoint(*T, ip, current_x_phys);
mapping::MappingPointContext context;
if (mapping_evaluator.EvaluatePoint(*T, ip, context) != mapping::MappingStatus::valid) {
return false;
}
const mfem::Vector &current_x_phys = context.physical_position;
for (int i = 0; i < dim; ++i) {
residual(i) = current_x_phys(i) - x_phys_target(i);
@@ -113,9 +115,7 @@ namespace mean_field::utils {
return true;
}
fem.mapping->ComputeJacobian(*T, J_map);
mfem::CalcInverse(J_map, J_map_inv);
J_map_inv.Mult(residual, step);
context.inverse_mapping_jacobian.Mult(residual, step);
double alpha = 1.0;
mfem::Vector x_ref_candidate(dim);
@@ -160,8 +160,7 @@ namespace mean_field::utils {
const mapping::COORDINATE_SPACE rspace
) {
mfem::Vector x_search;
if (vspace == mapping::COORDINATE_SPACE::PHYSICAL &&
fem.has_mapping()) {
if (vspace == mapping::COORDINATE_SPACE::PHYSICAL && fem.has_mapping()) {
GetReferencePoint(fem, x, x_search);
} else {
x_search = x;
@@ -177,8 +176,7 @@ namespace mean_field::utils {
double local_val = 0.0;
if (elem_ids.Size() > 0 && elem_ids[0] >= 0) {
const double val = u.GetValue(elem_ids[0], ips[0]);
if (rspace == mapping::COORDINATE_SPACE::PHYSICAL &&
!fem.has_mapping()) {
if (rspace == mapping::COORDINATE_SPACE::PHYSICAL && !fem.has_mapping()) {
MFEM_ABORT(
"Physical evaluation mode requested but no mapping "
"provided. Check "
@@ -189,9 +187,7 @@ namespace mean_field::utils {
}
double global_val = 0.0;
MPI_Allreduce(
&local_val, &global_val, 1, MPI_DOUBLE, MPI_MAX, fem.mesh->GetComm()
);
MPI_Allreduce(&local_val, &global_val, 1, MPI_DOUBLE, MPI_MAX, fem.mesh->GetComm());
return global_val;
}

View File

@@ -1,130 +1,21 @@
module;
#include <expected>
#include <mfem.hpp>
module mean_field;
import :boundary.contexts;
namespace mean_field::utils {
DOMAINS operator|(
DOMAINS lhs,
DOMAINS rhs
) {
return static_cast<DOMAINS>(
static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs)
);
return static_cast<DOMAINS>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
DOMAINS operator&(
DOMAINS lhs,
DOMAINS rhs
) {
return static_cast<DOMAINS>(
static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs)
);
}
void populate_element_mask(
const mfem::Mesh *mesh,
const DOMAINS domain,
mfem::Array<int> &mask
) {
const int max_attr = mesh->attributes.Max();
mask.SetSize(max_attr);
mask = 0;
if ((domain & DOMAINS::CORE) == DOMAINS::CORE && max_attr >= 1) {
mask[0] = 1;
}
if ((domain & DOMAINS::ENVELOPE) == DOMAINS::ENVELOPE &&
max_attr >= 2) {
mask[1] = 1;
}
if ((domain & DOMAINS::VACUUM) == DOMAINS::VACUUM && max_attr >= 3) {
mask[2] = 1;
}
}
void populate_domain_tdofs(
const mfem::ParFiniteElementSpace *fes,
const mfem::Array<int> &element_mask,
mfem::Array<int> &ess_tdof
) {
mfem::Array<int> vdof_marker(fes->GetVSize());
vdof_marker = 0;
for (int i = 0; i < fes->GetMesh()->GetNE(); i++) {
const int attr = fes->GetMesh()->GetAttribute(i);
if (element_mask[attr - 1]) {
mfem::Array<int> dofs;
fes->GetElementVDofs(i, dofs);
for (int j = 0; j < dofs.Size(); j++) {
int index = dofs[j];
if (index < 0)
index = -1 - index;
vdof_marker[index] = 1;
}
}
}
fes->MarkerToList(vdof_marker, ess_tdof);
}
std::expected<
boundary::Bounds,
boundary::BoundsError>
discover_bounds(
const mfem::Mesh *mesh,
const int vacuum_attr
) {
double local_min_r = std::numeric_limits<double>::max();
double local_max_r = -std::numeric_limits<double>::max();
bool found_vacuum = false;
for (int i = 0; i < mesh->GetNE(); ++i) {
if (mesh->GetAttribute(i) == vacuum_attr) {
found_vacuum = true;
mfem::Array<int> vertices;
mesh->GetElementVertices(i, vertices);
for (const int v : vertices) {
const double *coords = mesh->GetVertex(v);
double r = std::sqrt(
coords[0] * coords[0] + coords[1] * coords[1] +
coords[2] * coords[2]
);
local_min_r = std::min(local_min_r, r);
local_max_r = std::max(local_max_r, r);
}
}
}
double global_min_r, global_max_r;
int global_found_vacuum;
int l_found = found_vacuum ? 1 : 0;
MPI_Comm comm = MPI_COMM_WORLD;
if (const auto *pmesh = dynamic_cast<const mfem::ParMesh *>(mesh)) {
comm = pmesh->GetComm();
}
MPI_Allreduce(
&local_min_r, &global_min_r, 1, MPI_DOUBLE, MPI_MIN, comm
);
MPI_Allreduce(
&local_max_r, &global_max_r, 1, MPI_DOUBLE, MPI_MAX, comm
);
MPI_Allreduce(
&l_found, &global_found_vacuum, 1, MPI_INT, MPI_MAX, comm
);
if (global_found_vacuum) {
return boundary::Bounds(global_min_r, global_max_r);
}
return std::unexpected(boundary::BoundsError::CANNOT_FIND_VACUUM);
return static_cast<DOMAINS>(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
}
int get_mesh_order(const mfem::Mesh &mesh) {

View File

@@ -1,182 +1,162 @@
#pragma once
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>
#include <cstddef>
#include <cstdint>
#include <iosfwd>
#include <map>
#include <mutex>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <mpi.h>
#ifndef MEAN_FIELD_ENABLE_PROFILING
#define MEAN_FIELD_ENABLE_PROFILING 0
#endif
namespace mean_field::profiling {
struct Statistics {
unsigned long long observations{0};
unsigned long long warmups{0};
unsigned long long samples{0};
unsigned long long warmup_target{0};
std::uint64_t observations{0};
std::uint64_t warmups{0};
std::uint64_t samples{0};
std::uint64_t warmup_target{0};
std::uint64_t work_units{0};
double total_seconds{0.0};
double minimum_seconds{std::numeric_limits<double>::infinity()};
double minimum_seconds{0.0};
double maximum_seconds{0.0};
};
struct DistributedStatistics {
std::string label;
std::uint64_t minimum_samples{0};
std::uint64_t maximum_samples{0};
std::uint64_t maximum_warmups{0};
std::uint64_t minimum_work_units{0};
std::uint64_t maximum_work_units{0};
double maximum_rank_average_seconds{0.0};
double global_minimum_seconds{0.0};
double global_maximum_seconds{0.0};
double maximum_rank_total_seconds{0.0};
};
class Registry {
public:
static Registry& Get() {
static Registry registry;
return registry;
}
static Registry &Get();
void Record(const std::string& label, const double seconds, const unsigned long long warmup_count) {
std::scoped_lock lock(m_mutex);
Statistics& statistics = m_statistics[label];
Registry(const Registry &) = delete;
Registry &operator=(const Registry &) = delete;
Registry(Registry &&) = delete;
Registry &operator=(Registry &&) = delete;
statistics.warmup_target = std::max(statistics.warmup_target, warmup_count);
const bool is_warmup = statistics.observations < statistics.warmup_target;
++statistics.observations;
~Registry();
if (is_warmup) {
++statistics.warmups;
return;
}
void Record(
std::string_view label,
double seconds,
std::uint64_t warmup_count = 0
);
++statistics.samples;
statistics.total_seconds += seconds;
statistics.minimum_seconds = std::min(statistics.minimum_seconds, seconds);
statistics.maximum_seconds = std::max(statistics.maximum_seconds, seconds);
}
void AddCount(
std::string_view label,
std::uint64_t work_units
);
void Reset() {
std::scoped_lock lock(m_mutex);
m_statistics.clear();
}
void Reset();
void Print(MPI_Comm communicator) const {
const std::map<std::string, Statistics> snapshot = GetSnapshot();
[[nodiscard]] std::map<
std::string,
Statistics,
std::less<>>
Snapshot() const;
int mpi_initialized = 0;
int mpi_finalized = 0;
MPI_Initialized(&mpi_initialized);
if (mpi_initialized) MPI_Finalized(&mpi_finalized);
[[nodiscard]] std::vector<DistributedStatistics> Aggregate(MPI_Comm communicator) const;
const bool use_mpi = mpi_initialized && !mpi_finalized;
int rank = 0;
int communicator_size = 1;
void Print(
MPI_Comm communicator,
std::ostream &stream
) const;
if (use_mpi) {
MPI_Comm_rank(communicator, &rank);
MPI_Comm_size(communicator, &communicator_size);
}
void Print(MPI_Comm communicator) const;
if (rank == 0) {
std::cout << '\n';
std::cout << std::left << std::setw(42) << "Profile Region"
<< std::right << std::setw(11) << "Samples"
<< std::setw(10) << "Warmups"
<< std::setw(14) << "Avg Max ms"
<< std::setw(14) << "Min ms"
<< std::setw(14) << "Max ms"
<< std::setw(14) << "Total Max s" << '\n';
std::cout << std::string(119, '-') << '\n';
}
for (const auto& [label, local_statistics] : snapshot) {
unsigned long long minimum_samples = local_statistics.samples;
unsigned long long maximum_samples = local_statistics.samples;
unsigned long long maximum_warmups = local_statistics.warmups;
double local_average = local_statistics.samples > 0 ? local_statistics.total_seconds / static_cast<double>(local_statistics.samples) : 0.0;
double local_minimum = local_statistics.samples > 0 ? local_statistics.minimum_seconds : std::numeric_limits<double>::infinity();
double local_maximum = local_statistics.maximum_seconds;
double local_total = local_statistics.total_seconds;
double maximum_rank_average = local_average;
double global_minimum = local_minimum;
double global_maximum = local_maximum;
double maximum_rank_total = local_total;
if (use_mpi) {
MPI_Allreduce(&local_statistics.samples, &minimum_samples, 1, MPI_UNSIGNED_LONG_LONG, MPI_MIN, communicator);
MPI_Allreduce(&local_statistics.samples, &maximum_samples, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, communicator);
MPI_Allreduce(&local_statistics.warmups, &maximum_warmups, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, communicator);
MPI_Allreduce(&local_average, &maximum_rank_average, 1, MPI_DOUBLE, MPI_MAX, communicator);
MPI_Allreduce(&local_minimum, &global_minimum, 1, MPI_DOUBLE, MPI_MIN, communicator);
MPI_Allreduce(&local_maximum, &global_maximum, 1, MPI_DOUBLE, MPI_MAX, communicator);
MPI_Allreduce(&local_total, &maximum_rank_total, 1, MPI_DOUBLE, MPI_MAX, communicator);
}
if (!std::isfinite(global_minimum)) global_minimum = 0.0;
if (rank == 0) {
const std::string sample_string = minimum_samples == maximum_samples
? std::to_string(minimum_samples)
: std::to_string(minimum_samples) + "-" + std::to_string(maximum_samples);
std::cout << std::left << std::setw(100) << label
<< std::right << std::setw(11) << sample_string
<< std::setw(10) << maximum_warmups
<< std::setw(14) << std::fixed << std::setprecision(3) << 1.0e3 * maximum_rank_average
<< std::setw(14) << 1.0e3 * global_minimum
<< std::setw(14) << 1.0e3 * global_maximum
<< std::setw(14) << std::setprecision(6) << maximum_rank_total << '\n';
}
}
if (rank == 0) {
std::cout << std::string(119, '=') << '\n';
std::cout << "MPI ranks: " << communicator_size << "\n\n";
}
}
void PrintCsv(
MPI_Comm communicator,
std::ostream &stream
) const;
private:
[[nodiscard]] std::map<std::string, Statistics> GetSnapshot() const {
std::scoped_lock lock(m_mutex);
return m_statistics;
}
friend class Region;
Registry();
[[nodiscard]] std::size_t Register(
std::string_view label,
std::uint64_t warmup_count
);
void Record(
std::size_t region,
double seconds
) noexcept;
void AddCount(
std::size_t region,
std::uint64_t work_units
) noexcept;
struct Impl;
std::unique_ptr<Impl> m_impl;
};
class Region {
public:
explicit Region(
std::string_view label,
std::uint64_t warmup_count = 0
);
void Record(double seconds) const noexcept;
void AddCount(std::uint64_t work_units) const noexcept;
private:
mutable std::mutex m_mutex;
std::map<std::string, Statistics> m_statistics;
std::size_t m_region;
};
class ScopedTimer {
public:
ScopedTimer(std::string label, const unsigned long long warmup_count)
: m_label(std::move(label)),
m_warmup_count(warmup_count),
m_start(std::chrono::steady_clock::now()) {}
explicit ScopedTimer(const Region &region) noexcept;
ScopedTimer(const ScopedTimer &) = delete;
ScopedTimer &operator=(const ScopedTimer &) = delete;
ScopedTimer(ScopedTimer &&) = delete;
ScopedTimer &operator=(ScopedTimer &&) = delete;
~ScopedTimer() {
try {
const auto stop = std::chrono::steady_clock::now();
const double seconds = std::chrono::duration<double>(stop - m_start).count();
Registry::Get().Record(m_label, seconds, m_warmup_count);
} catch (...) {}
}
~ScopedTimer() noexcept;
private:
std::string m_label;
unsigned long long m_warmup_count;
const Region &m_region;
std::chrono::steady_clock::time_point m_start;
};
}
} // namespace mean_field::profiling
#define MEAN_FIELD_PROFILE_JOIN_IMPL(left, right) left##right
#define MEAN_FIELD_PROFILE_JOIN(left, right) MEAN_FIELD_PROFILE_JOIN_IMPL(left, right)
#define MEAN_FIELD_PROFILE_SCOPE_WARMUP(label, warmup_count) \
::mean_field::profiling::ScopedTimer MEAN_FIELD_PROFILE_JOIN(mean_field_profile_timer_, __COUNTER__)(label, warmup_count)
#if MEAN_FIELD_ENABLE_PROFILING
#define MEAN_FIELD_PROFILE_SCOPE(label) \
MEAN_FIELD_PROFILE_SCOPE_WARMUP(label, 1)
#define MEAN_FIELD_PROFILE_SCOPE_IMPL(label, warmup_count, identifier) \
static const ::mean_field::profiling::Region MEAN_FIELD_PROFILE_JOIN(mean_field_profile_region_, identifier)( \
label, warmup_count \
); \
const ::mean_field::profiling::ScopedTimer MEAN_FIELD_PROFILE_JOIN(mean_field_profile_timer_, identifier)( \
MEAN_FIELD_PROFILE_JOIN(mean_field_profile_region_, identifier) \
)
#define MEAN_FIELD_PROFILE_SCOPE_WARMUP(label, warmup_count) \
MEAN_FIELD_PROFILE_SCOPE_IMPL(label, warmup_count, __COUNTER__)
#define MEAN_FIELD_PROFILE_SCOPE(label) MEAN_FIELD_PROFILE_SCOPE_WARMUP(label, 1)
#define MEAN_FIELD_PROFILE_CALL_WARMUP(label, warmup_count, ...) \
do { \
@@ -184,11 +164,53 @@ namespace mean_field::profiling {
__VA_ARGS__; \
} while (false)
#define MEAN_FIELD_PROFILE_CALL(label, ...) \
MEAN_FIELD_PROFILE_CALL_WARMUP(label, 1, __VA_ARGS__)
#define MEAN_FIELD_PROFILE_CALL(label, ...) MEAN_FIELD_PROFILE_CALL_WARMUP(label, 1, __VA_ARGS__)
#define MEAN_FIELD_PROFILE_RESET() \
::mean_field::profiling::Registry::Get().Reset()
#define MEAN_FIELD_PROFILE_EVALUATE_IMPL(label, warmup_count, identifier, ...) \
([&]() -> decltype(auto) { \
MEAN_FIELD_PROFILE_SCOPE_IMPL(label, warmup_count, identifier); \
return (__VA_ARGS__); \
}())
#define MEAN_FIELD_PROFILE_PRINT(communicator) \
::mean_field::profiling::Registry::Get().Print(communicator)
#define MEAN_FIELD_PROFILE_EVALUATE_WARMUP(label, warmup_count, ...) \
MEAN_FIELD_PROFILE_EVALUATE_IMPL(label, warmup_count, __COUNTER__, __VA_ARGS__)
#define MEAN_FIELD_PROFILE_EVALUATE(label, ...) MEAN_FIELD_PROFILE_EVALUATE_WARMUP(label, 1, __VA_ARGS__)
#define MEAN_FIELD_PROFILE_COUNT_IMPL(label, work_units, identifier) \
do { \
static const ::mean_field::profiling::Region MEAN_FIELD_PROFILE_JOIN(mean_field_profile_counter_, identifier)( \
label \
); \
MEAN_FIELD_PROFILE_JOIN(mean_field_profile_counter_, identifier).AddCount(work_units); \
} while (false)
#define MEAN_FIELD_PROFILE_COUNT(label, work_units) MEAN_FIELD_PROFILE_COUNT_IMPL(label, work_units, __COUNTER__)
#define MEAN_FIELD_PROFILE_RESET() ::mean_field::profiling::Registry::Get().Reset()
#define MEAN_FIELD_PROFILE_PRINT(communicator) ::mean_field::profiling::Registry::Get().Print(communicator)
#define MEAN_FIELD_PROFILE_PRINT_CSV(communicator, stream) \
::mean_field::profiling::Registry::Get().PrintCsv(communicator, stream)
#else
#define MEAN_FIELD_PROFILE_SCOPE_WARMUP(label, warmup_count) ((void)0)
#define MEAN_FIELD_PROFILE_SCOPE(label) ((void)0)
#define MEAN_FIELD_PROFILE_CALL_WARMUP(label, warmup_count, ...) \
do { \
__VA_ARGS__; \
} while (false)
#define MEAN_FIELD_PROFILE_CALL(label, ...) MEAN_FIELD_PROFILE_CALL_WARMUP(label, 1, __VA_ARGS__)
#define MEAN_FIELD_PROFILE_EVALUATE_WARMUP(label, warmup_count, ...) (__VA_ARGS__)
#define MEAN_FIELD_PROFILE_EVALUATE(label, ...) (__VA_ARGS__)
#define MEAN_FIELD_PROFILE_COUNT(label, work_units) ((void)0)
#define MEAN_FIELD_PROFILE_RESET() ((void)0)
#define MEAN_FIELD_PROFILE_PRINT(communicator) ((void)0)
#define MEAN_FIELD_PROFILE_PRINT_CSV(communicator, stream) ((void)0)
#endif

View File

@@ -12,8 +12,7 @@ export namespace mean_field::analysis {
const fem::FEM &fem,
const mfem::GridFunction &gf,
utils::DOMAINS domain = utils::DOMAINS::ALL,
mapping::COORDINATE_SPACE coord_space =
mapping::COORDINATE_SPACE::PHYSICAL
mapping::COORDINATE_SPACE coord_space = mapping::COORDINATE_SPACE::PHYSICAL
);
mfem::Vector get_com(
@@ -34,8 +33,7 @@ export namespace mean_field::analysis {
double get_mesh_volume(
const fem::FEM &fem,
mapping::COORDINATE_SPACE coordinate_space =
mapping::COORDINATE_SPACE::PHYSICAL,
mapping::COORDINATE_SPACE coordinate_space = mapping::COORDINATE_SPACE::PHYSICAL,
utils::DOMAINS domain = utils::DOMAINS::STELLAR
);
} // namespace mean_field::analysis

View File

@@ -15,9 +15,7 @@ export namespace mean_field::boundary {
Boundaries b,
const int a
) {
return static_cast<int>(
static_cast<uint8_t>(b) - static_cast<uint8_t>(a)
);
return static_cast<int>(static_cast<uint8_t>(b) - static_cast<uint8_t>(a));
}
struct Bounds {

View File

@@ -0,0 +1,111 @@
module;
#include <cstdint>
#include <string_view>
export module mean_field:deformation.descriptors;
export namespace mean_field::deformation {
enum class SurfaceMotionKind : std::uint8_t { Radial, Normal, GeneralVector };
enum class GeometricGaugeTreatment : std::uint8_t {
Retained,
ExcludedByParameterization,
ConstrainedByPrescription
};
enum class InteriorCenterBehavior : std::uint8_t { Unspecified, FixedAtReferenceCenter, DeterminedBySurfaceMotion };
enum class VacuumOuterBoundaryBehavior : std::uint8_t {
Unspecified,
FixedAtReferenceInfinity,
DeterminedBySurfaceMotion
};
struct SurfaceDeformationDescriptor final {
std::string_view name;
int spatialDimension;
SurfaceMotionKind motionKind;
bool linearOnReferenceGeometry;
bool requiresStarShapedReferenceSurface;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
GeometricGaugeTreatment translationTreatment;
GeometricGaugeTreatment orientationTreatment;
[[nodiscard]] constexpr bool isValid() const noexcept {
return !name.empty() && spatialDimension > 0;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const SurfaceDeformationDescriptor &) const = default;
};
struct InteriorDeformationExtensionDescriptor final {
std::string_view name;
int spatialDimension;
bool linearOnReferenceGeometry;
bool requiresRadialFoliation;
bool requiresAuxiliarySolve;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
InteriorCenterBehavior centerBehavior;
[[nodiscard]] constexpr bool isValid() const noexcept {
return !name.empty() && spatialDimension > 0 && centerBehavior != InteriorCenterBehavior::Unspecified;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const InteriorDeformationExtensionDescriptor &) const = default;
};
struct VacuumDeformationExtensionDescriptor final {
std::string_view name;
int spatialDimension;
bool linearOnReferenceGeometry;
bool requiresRadialFoliation;
bool requiresAuxiliarySolve;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
VacuumOuterBoundaryBehavior outerBoundaryBehavior;
[[nodiscard]] constexpr bool isValid() const noexcept {
return !name.empty() && spatialDimension > 0 &&
outerBoundaryBehavior != VacuumOuterBoundaryBehavior::Unspecified;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const VacuumDeformationExtensionDescriptor &) const = default;
};
struct DomainDeformationDescriptor final {
SurfaceDeformationDescriptor surfaceDeformation;
InteriorDeformationExtensionDescriptor stellarInteriorExtension;
VacuumDeformationExtensionDescriptor vacuumExtension;
bool linearOnReferenceGeometry;
bool requiresAuxiliarySolve;
bool hasExactDerivativeTranspose;
bool hasExactPullbackDerivative;
[[nodiscard]] constexpr bool isValid() const noexcept {
return surfaceDeformation.isValid() && stellarInteriorExtension.isValid() && vacuumExtension.isValid() &&
surfaceDeformation.spatialDimension == stellarInteriorExtension.spatialDimension &&
surfaceDeformation.spatialDimension == vacuumExtension.spatialDimension;
}
[[nodiscard]] constexpr bool supportsExactNewtonLinearization() const noexcept {
return hasExactDerivativeTranspose && hasExactPullbackDerivative;
}
constexpr bool operator==(const DomainDeformationDescriptor &) const = default;
};
} // namespace mean_field::deformation

View File

@@ -0,0 +1,870 @@
module;
#include <algorithm>
#include <cmath>
#include <compare>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:deformation.domain_deformation;
export import :deformation.interior_extension;
export import :deformation.nodal_radial_surface;
export import :deformation.radial_extensions;
export import :deformation.surface_prescription;
export import :deformation.vacuum_extension;
export import :fem;
export import :field.mfem;
export import :utils.domain;
export namespace mean_field::deformation {
enum class VolumeDeformationOwner : std::uint8_t { StellarInterior, Vacuum };
struct DomainDeformationDiscretizationDependencies final {
const mfem::Mesh *physicalMeshIdentity{nullptr};
const mfem::ParMesh *logicalReferenceMeshIdentity{nullptr};
const mfem::ParFiniteElementSpace *surfaceScalarSpaceIdentity{nullptr};
const mfem::ParFiniteElementSpace *volumeDisplacementSpaceIdentity{nullptr};
long physicalMeshSequence{-1};
long logicalReferenceMeshSequence{-1};
long surfaceScalarSpaceSequence{-1};
long volumeDisplacementSpaceSequence{-1};
[[nodiscard]] bool isCurrent() const noexcept {
return physicalMeshIdentity != nullptr && logicalReferenceMeshIdentity != nullptr &&
surfaceScalarSpaceIdentity != nullptr && volumeDisplacementSpaceIdentity != nullptr &&
physicalMeshIdentity->GetSequence() == physicalMeshSequence &&
logicalReferenceMeshIdentity->GetSequence() == logicalReferenceMeshSequence &&
surfaceScalarSpaceIdentity->GetSequence() == surfaceScalarSpaceSequence &&
volumeDisplacementSpaceIdentity->GetSequence() == volumeDisplacementSpaceSequence;
}
};
struct DomainDeformationCompositionReport final {
int scalarTrueDofCount{0};
int stellarInteriorOwnedScalarDofCount{0};
int vacuumOwnedScalarDofCount{0};
int sharedSurfaceScalarDofCount{0};
[[nodiscard]] constexpr int assignedScalarDofCount() const noexcept {
return stellarInteriorOwnedScalarDofCount + vacuumOwnedScalarDofCount;
}
constexpr auto operator<=>(const DomainDeformationCompositionReport &) const = default;
};
struct DomainDeformationGeometryReport final {
double minimumJacobianDeterminant{std::numeric_limits<double>::infinity()};
[[nodiscard]] bool isOrientationPreserving(const double determinantFloor = 0.0) const noexcept {
return std::isfinite(minimumJacobianDeterminant) && std::isfinite(determinantFloor) &&
determinantFloor >= 0.0 && minimumJacobianDeterminant > determinantFloor;
}
};
struct PreparedDomainDeformationActionStatistics final {
std::uint64_t volumeBuildApplications{0};
std::uint64_t jacobianApplications{0};
std::uint64_t jacobianTransposeApplications{0};
std::uint64_t pullbackDerivativeApplications{0};
std::uint64_t geometryInspections{0};
constexpr auto operator<=>(const PreparedDomainDeformationActionStatistics &) const = default;
};
template <typename Candidate>
concept PreparedDomainDeformationOperator = requires(
const std::remove_cvref_t<Candidate> &preparedDeformation,
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &volumeDisplacement,
mfem::Vector &parameterDual
) {
{ preparedDeformation.descriptor() } noexcept -> std::same_as<DomainDeformationDescriptor>;
{ preparedDeformation.parameterCount() } noexcept -> std::same_as<int>;
{ preparedDeformation.surfaceDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedDeformation.volumeDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedDeformation.buildVolumeDisplacement(parameters, volumeDisplacement) } -> std::same_as<void>;
{ preparedDeformation.applyJacobian(parameters, parameterDirection, volumeDisplacement) } -> std::same_as<void>;
{
preparedDeformation.applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual)
} -> std::same_as<void>;
{
preparedDeformation.applyPullbackDerivative(
parameters, parameterDirection, volumeDisplacementDual, parameterDual
)
} -> std::same_as<void>;
};
template <
PreparedSurfaceDeformationPrescription PreparedSurface,
PreparedInteriorDeformationExtension PreparedInterior,
PreparedVacuumDeformationExtension PreparedVacuum>
class PreparedDomainDeformation final {
public:
PreparedDomainDeformation(
PreparedSurface preparedSurface,
PreparedInterior preparedInterior,
PreparedVacuum preparedVacuum,
mfem::ParFiniteElementSpace &surfaceScalarSpace,
mfem::ParFiniteElementSpace &volumeDisplacementSpace,
mfem::ParMesh &logicalReferenceMesh
)
: m_surface(std::move(preparedSurface)),
m_interior(std::move(preparedInterior)),
m_vacuum(std::move(preparedVacuum)),
m_volumeDisplacementSpace(&volumeDisplacementSpace),
m_descriptor(makeDescriptor(
m_surface,
m_interior,
m_vacuum
)),
m_surfaceDisplacementWorkspace(surfaceDisplacementSize()),
m_surfaceDirectionWorkspace(surfaceDisplacementSize()),
m_interiorVolumeWorkspace(volumeDisplacementSize()),
m_vacuumVolumeWorkspace(volumeDisplacementSize()),
m_interiorVolumeDualWorkspace(volumeDisplacementSize()),
m_vacuumVolumeDualWorkspace(volumeDisplacementSize()),
m_interiorSurfaceDualWorkspace(surfaceDisplacementSize()),
m_vacuumSurfaceDualWorkspace(surfaceDisplacementSize()),
m_surfaceDualWorkspace(surfaceDisplacementSize()),
m_interiorSurfacePullbackWorkspace(surfaceDisplacementSize()),
m_vacuumSurfacePullbackWorkspace(surfaceDisplacementSize()),
m_surfacePullbackWorkspace(surfaceDisplacementSize()),
m_parameterPullbackWorkspace(parameterCount()),
m_volumeGridFunctionWorkspace(std::make_unique<mfem::ParGridFunction>(&volumeDisplacementSpace)) {
validateCompatibility(surfaceScalarSpace, volumeDisplacementSpace, logicalReferenceMesh);
compileOwnership();
const mfem::Mesh *physicalMesh = volumeDisplacementSpace.GetMesh();
m_discretizationDependencies = {
.physicalMeshIdentity = physicalMesh,
.logicalReferenceMeshIdentity = &logicalReferenceMesh,
.surfaceScalarSpaceIdentity = &surfaceScalarSpace,
.volumeDisplacementSpaceIdentity = &volumeDisplacementSpace,
.physicalMeshSequence = physicalMesh->GetSequence(),
.logicalReferenceMeshSequence = logicalReferenceMesh.GetSequence(),
.surfaceScalarSpaceSequence = surfaceScalarSpace.GetSequence(),
.volumeDisplacementSpaceSequence = volumeDisplacementSpace.GetSequence()
};
}
PreparedDomainDeformation(const PreparedDomainDeformation &) = delete;
PreparedDomainDeformation &operator=(const PreparedDomainDeformation &) = delete;
PreparedDomainDeformation(PreparedDomainDeformation &&) noexcept = default;
PreparedDomainDeformation &operator=(PreparedDomainDeformation &&) noexcept = default;
[[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept {
return m_descriptor;
}
[[nodiscard]] int parameterCount() const noexcept {
return m_surface.parameterCount();
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return m_surface.surfaceDisplacementSize();
}
[[nodiscard]] int volumeDisplacementSize() const noexcept {
return m_interior.interiorDisplacementSize();
}
[[nodiscard]] int scalarTrueDofCount() const noexcept {
return m_interior.scalarTrueDofCount();
}
[[nodiscard]] int spatialDimension() const noexcept {
return m_descriptor.surfaceDeformation.spatialDimension;
}
[[nodiscard]] VolumeDeformationOwner volumeOwner(const int scalarTrueDof) const {
requireScalarTrueDof(scalarTrueDof);
return m_volumeOwners[static_cast<std::size_t>(scalarTrueDof)];
}
[[nodiscard]] bool isSharedSurfaceDof(const int scalarTrueDof) const {
requireScalarTrueDof(scalarTrueDof);
return m_interior.hasStellarSupport(scalarTrueDof) && m_vacuum.hasVacuumSupport(scalarTrueDof);
}
[[nodiscard]] const DomainDeformationCompositionReport &compositionReport() const noexcept {
return m_compositionReport;
}
[[nodiscard]] const DomainDeformationDiscretizationDependencies &discretizationDependencies() const noexcept {
return m_discretizationDependencies;
}
[[nodiscard]] bool matchesCurrentDiscretization() const noexcept {
return m_discretizationDependencies.isCurrent();
}
[[nodiscard]] const PreparedDomainDeformationActionStatistics &actionStatistics() const noexcept {
return m_actionStatistics;
}
[[nodiscard]] const PreparedSurface &surfaceDeformationPrescription() const noexcept {
return m_surface;
}
[[nodiscard]] const PreparedInterior &stellarInteriorExtension() const noexcept {
return m_interior;
}
[[nodiscard]] const PreparedVacuum &vacuumExtension() const noexcept {
return m_vacuum;
}
void buildVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireVolumeSize(volumeDisplacement);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
m_interior.buildInteriorDisplacement(m_surfaceDisplacementWorkspace, m_interiorVolumeWorkspace);
m_vacuum.buildVacuumDisplacement(m_surfaceDisplacementWorkspace, m_vacuumVolumeWorkspace);
mergeVolumeFields(m_interiorVolumeWorkspace, m_vacuumVolumeWorkspace, volumeDisplacement);
++m_actionStatistics.volumeBuildApplications;
}
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &volumeDisplacementDirection
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireVolumeSize(volumeDisplacementDirection);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
m_surface.applyJacobian(parameters, parameterDirection, m_surfaceDirectionWorkspace);
m_interior.applyJacobian(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_interiorVolumeWorkspace
);
m_vacuum.applyJacobian(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_vacuumVolumeWorkspace
);
mergeVolumeFields(m_interiorVolumeWorkspace, m_vacuumVolumeWorkspace, volumeDisplacementDirection);
++m_actionStatistics.jacobianApplications;
}
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDual
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireVolumeSize(volumeDisplacementDual);
requireParameterSize(parameterDual);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
splitVolumeDual(volumeDisplacementDual);
applyExtensionTransposes();
m_surface.applyJacobianTranspose(parameters, m_surfaceDualWorkspace, parameterDual);
++m_actionStatistics.jacobianTransposeApplications;
}
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDualAction
) const {
requireCurrentDiscretization();
requireParameterSize(parameters);
requireParameterSize(parameterDirection);
requireVolumeSize(volumeDisplacementDual);
requireParameterSize(parameterDualAction);
m_surface.buildSurfaceDisplacement(parameters, m_surfaceDisplacementWorkspace);
m_surface.applyJacobian(parameters, parameterDirection, m_surfaceDirectionWorkspace);
splitVolumeDual(volumeDisplacementDual);
applyExtensionTransposes();
m_interior.applyPullbackDerivative(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_interiorVolumeDualWorkspace,
m_interiorSurfacePullbackWorkspace
);
m_vacuum.applyPullbackDerivative(
m_surfaceDisplacementWorkspace, m_surfaceDirectionWorkspace, m_vacuumVolumeDualWorkspace,
m_vacuumSurfacePullbackWorkspace
);
addSurfaceFields(
m_interiorSurfacePullbackWorkspace, m_vacuumSurfacePullbackWorkspace, m_surfacePullbackWorkspace
);
m_surface.applyJacobianTranspose(parameters, m_surfacePullbackWorkspace, parameterDualAction);
m_surface.applyPullbackDerivative(
parameters, parameterDirection, m_surfaceDualWorkspace, m_parameterPullbackWorkspace
);
parameterDualAction += m_parameterPullbackWorkspace;
++m_actionStatistics.pullbackDerivativeApplications;
}
[[nodiscard]] DomainDeformationGeometryReport
inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const {
requireCurrentDiscretization();
requireVolumeSize(volumeDisplacement);
m_volumeGridFunctionWorkspace->SetFromTrueDofs(volumeDisplacement);
mfem::Mesh *mesh = m_volumeDisplacementSpace->GetMesh();
double localMinimumDeterminant = std::numeric_limits<double>::infinity();
int localGeometryIsFinite = 1;
for (int element = 0; element < mesh->GetNE(); ++element) {
mfem::ElementTransformation *transformation = mesh->GetElementTransformation(element);
const mfem::FiniteElement *finiteElement = m_volumeDisplacementSpace->GetFE(element);
// Positivity is a pointwise geometry requirement, not an
// integration-accuracy requirement. A rule only slightly
// above the displacement order can miss a narrow negative
// region of the determinant even when a downstream physics
// rule samples it. The determinant of a d-dimensional
// degree-p deformation gradient can vary at substantially
// higher order, so inspect at a conservative d*p scale.
const int geometryInspectionOrder =
std::max(finiteElement->GetOrder() + 2, 2 * spatialDimension() * finiteElement->GetOrder());
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), geometryInspectionOrder);
for (int point = 0; point < rule.GetNPoints(); ++point) {
transformation->SetIntPoint(&rule.IntPoint(point));
mfem::DenseMatrix deformationGradient;
m_volumeGridFunctionWorkspace->GetVectorGradient(*transformation, deformationGradient);
for (int component = 0; component < spatialDimension(); ++component) {
deformationGradient(component, component) += 1.0;
}
const double determinant = deformationGradient.Det();
if (!std::isfinite(determinant)) {
localGeometryIsFinite = 0;
} else {
localMinimumDeterminant = std::min(localMinimumDeterminant, determinant);
}
}
}
double globalMinimumDeterminant = 0.0;
int globalGeometryIsFinite = 0;
MPI_Allreduce(
&localMinimumDeterminant, &globalMinimumDeterminant, 1, MPI_DOUBLE, MPI_MIN,
m_volumeDisplacementSpace->GetComm()
);
MPI_Allreduce(
&localGeometryIsFinite, &globalGeometryIsFinite, 1, MPI_INT, MPI_MIN,
m_volumeDisplacementSpace->GetComm()
);
if (globalGeometryIsFinite == 0) {
globalMinimumDeterminant = std::numeric_limits<double>::quiet_NaN();
}
++m_actionStatistics.geometryInspections;
return {.minimumJacobianDeterminant = globalMinimumDeterminant};
}
[[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement,
const double determinantFloor = 0.0
) const {
if (!std::isfinite(determinantFloor) || determinantFloor < 0.0) {
throw std::invalid_argument("The mapped-geometry determinant floor must be finite and non-negative.");
}
buildVolumeDisplacement(parameters, volumeDisplacement);
const DomainDeformationGeometryReport report = inspectMappedGeometry(volumeDisplacement);
if (!report.isOrientationPreserving(determinantFloor)) {
throw std::domain_error("The prepared domain deformation inverts at least one volume element.");
}
return report;
}
private:
[[nodiscard]] static DomainDeformationDescriptor makeDescriptor(
const PreparedSurface &surface,
const PreparedInterior &interior,
const PreparedVacuum &vacuum
) noexcept {
const SurfaceDeformationDescriptor surfaceDescriptor = surface.descriptor();
const InteriorDeformationExtensionDescriptor interiorDescriptor = interior.descriptor();
const VacuumDeformationExtensionDescriptor vacuumDescriptor = vacuum.descriptor();
return {
.surfaceDeformation = surfaceDescriptor,
.stellarInteriorExtension = interiorDescriptor,
.vacuumExtension = vacuumDescriptor,
.linearOnReferenceGeometry = surfaceDescriptor.linearOnReferenceGeometry &&
interiorDescriptor.linearOnReferenceGeometry &&
vacuumDescriptor.linearOnReferenceGeometry,
.requiresAuxiliarySolve =
interiorDescriptor.requiresAuxiliarySolve || vacuumDescriptor.requiresAuxiliarySolve,
.hasExactDerivativeTranspose = surfaceDescriptor.hasExactDerivativeTranspose &&
interiorDescriptor.hasExactDerivativeTranspose &&
vacuumDescriptor.hasExactDerivativeTranspose,
.hasExactPullbackDerivative = surfaceDescriptor.hasExactPullbackDerivative &&
interiorDescriptor.hasExactPullbackDerivative &&
vacuumDescriptor.hasExactPullbackDerivative
};
}
void validateCompatibility(
mfem::ParFiniteElementSpace &surfaceScalarSpace,
mfem::ParFiniteElementSpace &volumeDisplacementSpace,
mfem::ParMesh &logicalReferenceMesh
) const {
const mfem::Mesh *physicalMesh = volumeDisplacementSpace.GetMesh();
if (!m_descriptor.isValid()) {
throw std::invalid_argument("Prepared domain deformation descriptors are incompatible.");
}
if (!m_descriptor.supportsExactNewtonLinearization()) {
throw std::invalid_argument("Prepared domain deformation requires exact transpose and pullback paths.");
}
if (physicalMesh == nullptr || surfaceScalarSpace.GetMesh() != physicalMesh) {
throw std::invalid_argument("Prepared domain deformation spaces must share one physical mesh.");
}
if (logicalReferenceMesh.GetNE() != physicalMesh->GetNE() ||
logicalReferenceMesh.GetNBE() != physicalMesh->GetNBE()) {
throw std::invalid_argument("Prepared domain deformation requires the paired logical reference mesh.");
}
if (m_surface.surfaceDisplacementSize() != m_interior.surfaceDisplacementSize() ||
m_surface.surfaceDisplacementSize() != m_vacuum.surfaceDisplacementSize()) {
throw std::invalid_argument("Prepared deformation factors have incompatible surface trace sizes.");
}
if (m_interior.interiorDisplacementSize() != m_vacuum.vacuumDisplacementSize() ||
m_interior.interiorDisplacementSize() != volumeDisplacementSpace.GetTrueVSize()) {
throw std::invalid_argument("Prepared deformation factors have incompatible volume vector sizes.");
}
if (m_interior.scalarTrueDofCount() != m_vacuum.scalarTrueDofCount() ||
volumeDisplacementSpace.GetTrueVSize() != spatialDimension() * m_interior.scalarTrueDofCount()) {
throw std::invalid_argument("Prepared deformation factors have incompatible scalar volume topology.");
}
if (volumeDisplacementSpace.GetOrdering() != mfem::Ordering::byNODES) {
throw std::invalid_argument("Prepared domain deformation requires MFEM byNODES volume ordering.");
}
}
void compileOwnership() {
m_volumeOwners.resize(static_cast<std::size_t>(scalarTrueDofCount()));
m_compositionReport.scalarTrueDofCount = scalarTrueDofCount();
for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) {
const bool hasStellarSupport = m_interior.hasStellarSupport(scalarTrueDof);
const bool hasVacuumSupport = m_vacuum.hasVacuumSupport(scalarTrueDof);
if (!hasStellarSupport && !hasVacuumSupport) {
throw std::invalid_argument("A volume displacement DOF has no deformation-extension owner.");
}
if (hasStellarSupport) {
m_volumeOwners[static_cast<std::size_t>(scalarTrueDof)] = VolumeDeformationOwner::StellarInterior;
++m_compositionReport.stellarInteriorOwnedScalarDofCount;
if (hasVacuumSupport) {
++m_compositionReport.sharedSurfaceScalarDofCount;
}
} else {
m_volumeOwners[static_cast<std::size_t>(scalarTrueDof)] = VolumeDeformationOwner::Vacuum;
++m_compositionReport.vacuumOwnedScalarDofCount;
}
}
}
[[nodiscard]] int volumeVectorDof(
const int scalarTrueDof,
const int component
) const noexcept {
return scalarTrueDof + component * scalarTrueDofCount();
}
void mergeVolumeFields(
const mfem::Vector &interiorVolume,
const mfem::Vector &vacuumVolume,
mfem::Vector &volume
) const noexcept {
for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) {
const mfem::Vector &source = volumeOwner(scalarTrueDof) == VolumeDeformationOwner::StellarInterior
? interiorVolume
: vacuumVolume;
for (int component = 0; component < spatialDimension(); ++component) {
const int vectorDof = volumeVectorDof(scalarTrueDof, component);
volume(vectorDof) = source(vectorDof);
}
}
}
void splitVolumeDual(const mfem::Vector &volumeDual) const noexcept {
m_interiorVolumeDualWorkspace = 0.0;
m_vacuumVolumeDualWorkspace = 0.0;
for (int scalarTrueDof = 0; scalarTrueDof < scalarTrueDofCount(); ++scalarTrueDof) {
mfem::Vector &destination = volumeOwner(scalarTrueDof) == VolumeDeformationOwner::StellarInterior
? m_interiorVolumeDualWorkspace
: m_vacuumVolumeDualWorkspace;
for (int component = 0; component < spatialDimension(); ++component) {
const int vectorDof = volumeVectorDof(scalarTrueDof, component);
destination(vectorDof) = volumeDual(vectorDof);
}
}
}
void applyExtensionTransposes() const {
m_interior.applyJacobianTranspose(
m_surfaceDisplacementWorkspace, m_interiorVolumeDualWorkspace, m_interiorSurfaceDualWorkspace
);
m_vacuum.applyJacobianTranspose(
m_surfaceDisplacementWorkspace, m_vacuumVolumeDualWorkspace, m_vacuumSurfaceDualWorkspace
);
addSurfaceFields(m_interiorSurfaceDualWorkspace, m_vacuumSurfaceDualWorkspace, m_surfaceDualWorkspace);
}
static void addSurfaceFields(
const mfem::Vector &interior,
const mfem::Vector &vacuum,
mfem::Vector &sum
) {
sum = interior;
sum += vacuum;
}
void requireCurrentDiscretization() const {
if (!matchesCurrentDiscretization()) {
throw std::logic_error("Prepared domain deformation discretization dependencies are stale.");
}
}
void requireParameterSize(const mfem::Vector &parameters) const {
if (parameters.Size() != parameterCount()) {
throw std::invalid_argument("Prepared domain deformation received an incompatible parameter vector.");
}
}
void requireVolumeSize(const mfem::Vector &volume) const {
if (volume.Size() != volumeDisplacementSize()) {
throw std::invalid_argument("Prepared domain deformation received an incompatible volume vector.");
}
}
void requireScalarTrueDof(const int scalarTrueDof) const {
if (scalarTrueDof < 0 || scalarTrueDof >= scalarTrueDofCount()) {
throw std::out_of_range("Scalar true DOF is outside the prepared domain deformation.");
}
}
PreparedSurface m_surface;
PreparedInterior m_interior;
PreparedVacuum m_vacuum;
mfem::ParFiniteElementSpace *m_volumeDisplacementSpace;
DomainDeformationDescriptor m_descriptor;
DomainDeformationCompositionReport m_compositionReport;
DomainDeformationDiscretizationDependencies m_discretizationDependencies;
std::vector<VolumeDeformationOwner> m_volumeOwners;
mutable PreparedDomainDeformationActionStatistics m_actionStatistics;
mutable mfem::Vector m_surfaceDisplacementWorkspace;
mutable mfem::Vector m_surfaceDirectionWorkspace;
mutable mfem::Vector m_interiorVolumeWorkspace;
mutable mfem::Vector m_vacuumVolumeWorkspace;
mutable mfem::Vector m_interiorVolumeDualWorkspace;
mutable mfem::Vector m_vacuumVolumeDualWorkspace;
mutable mfem::Vector m_interiorSurfaceDualWorkspace;
mutable mfem::Vector m_vacuumSurfaceDualWorkspace;
mutable mfem::Vector m_surfaceDualWorkspace;
mutable mfem::Vector m_interiorSurfacePullbackWorkspace;
mutable mfem::Vector m_vacuumSurfacePullbackWorkspace;
mutable mfem::Vector m_surfacePullbackWorkspace;
mutable mfem::Vector m_parameterPullbackWorkspace;
mutable std::unique_ptr<mfem::ParGridFunction> m_volumeGridFunctionWorkspace;
};
template <
PreparedSurfaceDeformationPrescription PreparedSurface,
PreparedInteriorDeformationExtension PreparedInterior,
PreparedVacuumDeformationExtension PreparedVacuum>
[[nodiscard]] auto composePreparedDomainDeformation(
PreparedSurface preparedSurface,
PreparedInterior preparedInterior,
PreparedVacuum preparedVacuum,
mfem::ParFiniteElementSpace &surfaceScalarSpace,
mfem::ParFiniteElementSpace &volumeDisplacementSpace,
mfem::ParMesh &logicalReferenceMesh
) {
return PreparedDomainDeformation<PreparedSurface, PreparedInterior, PreparedVacuum>{
std::move(preparedSurface), std::move(preparedInterior), std::move(preparedVacuum),
surfaceScalarSpace, volumeDisplacementSpace, logicalReferenceMesh
};
}
class PreparedDomainDeformationRuntime final {
public:
template <PreparedDomainDeformationOperator PreparedDeformation>
requires(!std::same_as<
std::remove_cvref_t<PreparedDeformation>,
PreparedDomainDeformationRuntime>)
explicit PreparedDomainDeformationRuntime(PreparedDeformation &&preparedDeformation)
: m_implementation(
std::make_unique<Implementation<std::remove_cvref_t<PreparedDeformation>>>(
std::forward<PreparedDeformation>(preparedDeformation)
)
) {
}
PreparedDomainDeformationRuntime(const PreparedDomainDeformationRuntime &) = delete;
PreparedDomainDeformationRuntime &operator=(const PreparedDomainDeformationRuntime &) = delete;
PreparedDomainDeformationRuntime(PreparedDomainDeformationRuntime &&) noexcept = default;
PreparedDomainDeformationRuntime &operator=(PreparedDomainDeformationRuntime &&) noexcept = default;
[[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept {
return m_implementation->descriptor();
}
[[nodiscard]] int parameterCount() const noexcept {
return m_implementation->parameterCount();
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept {
return m_implementation->surfaceDisplacementSize();
}
[[nodiscard]] int volumeDisplacementSize() const noexcept {
return m_implementation->volumeDisplacementSize();
}
[[nodiscard]] bool matchesCurrentDiscretization() const noexcept {
return m_implementation->matchesCurrentDiscretization();
}
[[nodiscard]] DomainDeformationCompositionReport compositionReport() const noexcept {
return m_implementation->compositionReport();
}
[[nodiscard]] DomainDeformationDiscretizationDependencies discretizationDependencies() const noexcept {
return m_implementation->discretizationDependencies();
}
[[nodiscard]] PreparedDomainDeformationActionStatistics actionStatistics() const noexcept {
return m_implementation->actionStatistics();
}
void buildVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement
) const {
m_implementation->buildVolumeDisplacement(parameters, volumeDisplacement);
}
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &volumeDisplacementDirection
) const {
m_implementation->applyJacobian(parameters, parameterDirection, volumeDisplacementDirection);
}
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDual
) const {
m_implementation->applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual);
}
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDualAction
) const {
m_implementation->applyPullbackDerivative(
parameters, parameterDirection, volumeDisplacementDual, parameterDualAction
);
}
[[nodiscard]] DomainDeformationGeometryReport
inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const {
return m_implementation->inspectMappedGeometry(volumeDisplacement);
}
[[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement,
const double determinantFloor = 0.0
) const {
return m_implementation->buildValidatedVolumeDisplacement(parameters, volumeDisplacement, determinantFloor);
}
private:
class Interface {
public:
virtual ~Interface() = default;
[[nodiscard]] virtual DomainDeformationDescriptor descriptor() const noexcept = 0;
[[nodiscard]] virtual int parameterCount() const noexcept = 0;
[[nodiscard]] virtual int surfaceDisplacementSize() const noexcept = 0;
[[nodiscard]] virtual int volumeDisplacementSize() const noexcept = 0;
[[nodiscard]] virtual bool matchesCurrentDiscretization() const noexcept = 0;
[[nodiscard]] virtual DomainDeformationCompositionReport compositionReport() const noexcept = 0;
[[nodiscard]] virtual DomainDeformationDiscretizationDependencies
discretizationDependencies() const noexcept = 0;
[[nodiscard]] virtual PreparedDomainDeformationActionStatistics actionStatistics() const noexcept = 0;
virtual void buildVolumeDisplacement(
const mfem::Vector &,
mfem::Vector &
) const = 0;
virtual void applyJacobian(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
virtual void applyJacobianTranspose(
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
virtual void applyPullbackDerivative(
const mfem::Vector &,
const mfem::Vector &,
const mfem::Vector &,
mfem::Vector &
) const = 0;
[[nodiscard]] virtual DomainDeformationGeometryReport inspectMappedGeometry(const mfem::Vector &) const = 0;
[[nodiscard]] virtual DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &,
mfem::Vector &,
double
) const = 0;
};
template <PreparedDomainDeformationOperator PreparedDeformation> class Implementation final : public Interface {
public:
explicit Implementation(PreparedDeformation preparedDeformation)
: m_preparedDeformation(std::move(preparedDeformation)) {
}
[[nodiscard]] DomainDeformationDescriptor descriptor() const noexcept override {
return m_preparedDeformation.descriptor();
}
[[nodiscard]] int parameterCount() const noexcept override {
return m_preparedDeformation.parameterCount();
}
[[nodiscard]] int surfaceDisplacementSize() const noexcept override {
return m_preparedDeformation.surfaceDisplacementSize();
}
[[nodiscard]] int volumeDisplacementSize() const noexcept override {
return m_preparedDeformation.volumeDisplacementSize();
}
[[nodiscard]] bool matchesCurrentDiscretization() const noexcept override {
return m_preparedDeformation.matchesCurrentDiscretization();
}
[[nodiscard]] DomainDeformationCompositionReport compositionReport() const noexcept override {
return m_preparedDeformation.compositionReport();
}
[[nodiscard]] DomainDeformationDiscretizationDependencies
discretizationDependencies() const noexcept override {
return m_preparedDeformation.discretizationDependencies();
}
[[nodiscard]] PreparedDomainDeformationActionStatistics actionStatistics() const noexcept override {
return m_preparedDeformation.actionStatistics();
}
void buildVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement
) const override {
m_preparedDeformation.buildVolumeDisplacement(parameters, volumeDisplacement);
}
void applyJacobian(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
mfem::Vector &volumeDisplacementDirection
) const override {
m_preparedDeformation.applyJacobian(parameters, parameterDirection, volumeDisplacementDirection);
}
void applyJacobianTranspose(
const mfem::Vector &parameters,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDual
) const override {
m_preparedDeformation.applyJacobianTranspose(parameters, volumeDisplacementDual, parameterDual);
}
void applyPullbackDerivative(
const mfem::Vector &parameters,
const mfem::Vector &parameterDirection,
const mfem::Vector &volumeDisplacementDual,
mfem::Vector &parameterDualAction
) const override {
m_preparedDeformation.applyPullbackDerivative(
parameters, parameterDirection, volumeDisplacementDual, parameterDualAction
);
}
[[nodiscard]] DomainDeformationGeometryReport
inspectMappedGeometry(const mfem::Vector &volumeDisplacement) const override {
return m_preparedDeformation.inspectMappedGeometry(volumeDisplacement);
}
[[nodiscard]] DomainDeformationGeometryReport buildValidatedVolumeDisplacement(
const mfem::Vector &parameters,
mfem::Vector &volumeDisplacement,
const double determinantFloor
) const override {
return m_preparedDeformation.buildValidatedVolumeDisplacement(
parameters, volumeDisplacement, determinantFloor
);
}
private:
PreparedDeformation m_preparedDeformation;
};
std::unique_ptr<Interface> m_implementation;
};
template <
utils::domain::IsSchema SchemaT = utils::domain::CoreEnvelopeVacuumDomainSchema,
SurfaceDeformationPrescription SurfacePrescription,
InteriorDeformationExtension InteriorExtension,
VacuumDeformationExtension VacuumExtension>
requires SurfaceDeformationCompilable<
SurfacePrescription,
SurfaceDeformationCompilationContext> &&
InteriorDeformationExtensionCompilable<
InteriorExtension,
RadialDeformationExtensionCompilationContext> &&
VacuumDeformationExtensionCompilable<
VacuumExtension,
RadialDeformationExtensionCompilationContext>
[[nodiscard]] auto compileDomainDeformation(
const SurfacePrescription &surfacePrescription,
const InteriorExtension &interiorExtension,
const VacuumExtension &vacuumExtension,
fem::FEM &finiteElementModel
) {
if (!finiteElementModel.okay()) {
throw std::invalid_argument("Domain deformation compilation requires a complete finite-element model.");
}
const field::ScalarBoundaryDofMap surfaceDofMap =
field::make_stellar_surface_scalar_dof_map<SchemaT>(*finiteElementModel.surfaceDeformationFes);
const SurfaceDeformationCompilationContext surfaceContext{
*finiteElementModel.surfaceDeformationFes, surfaceDofMap
};
auto preparedSurface = compileSurfaceDeformationPrescription(surfacePrescription, surfaceContext);
const RadialDeformationExtensionCompilationContext extensionContext =
makeRadialDeformationExtensionCompilationContext<SchemaT>(
*finiteElementModel.surfaceDeformationFes, *finiteElementModel.displacementFes,
*finiteElementModel.logicalReferenceMesh
);
auto preparedInterior = compileInteriorDeformationExtension(interiorExtension, extensionContext);
auto preparedVacuum = compileVacuumDeformationExtension(vacuumExtension, extensionContext);
return composePreparedDomainDeformation(
std::move(preparedSurface), std::move(preparedInterior), std::move(preparedVacuum),
*finiteElementModel.surfaceDeformationFes, *finiteElementModel.displacementFes,
*finiteElementModel.logicalReferenceMesh
);
}
} // namespace mean_field::deformation

View File

@@ -0,0 +1,63 @@
module;
#include <concepts>
#include <type_traits>
#include <mfem.hpp>
export module mean_field:deformation.interior_extension;
export import :deformation.descriptors;
export namespace mean_field::deformation {
template <typename Candidate>
concept PreparedInteriorDeformationExtension = requires(
const std::remove_cvref_t<Candidate> &preparedExtension,
const mfem::Vector &surfaceDisplacement,
const mfem::Vector &surfaceDisplacementDirection,
const mfem::Vector &interiorDisplacementDual,
mfem::Vector &interiorDisplacement,
mfem::Vector &surfaceDisplacementDual
) {
{ preparedExtension.descriptor() } noexcept -> std::same_as<InteriorDeformationExtensionDescriptor>;
{ preparedExtension.surfaceDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedExtension.interiorDisplacementSize() } noexcept -> std::same_as<int>;
{ preparedExtension.scalarTrueDofCount() } noexcept -> std::same_as<int>;
{ preparedExtension.hasStellarSupport(0) } -> std::same_as<bool>;
{
preparedExtension.buildInteriorDisplacement(surfaceDisplacement, interiorDisplacement)
} -> std::same_as<void>;
{
preparedExtension.applyJacobian(surfaceDisplacement, surfaceDisplacementDirection, interiorDisplacement)
} -> std::same_as<void>;
{
preparedExtension.applyJacobianTranspose(
surfaceDisplacement, interiorDisplacementDual, surfaceDisplacementDual
)
} -> std::same_as<void>;
{
preparedExtension.applyPullbackDerivative(
surfaceDisplacement, surfaceDisplacementDirection, interiorDisplacementDual, surfaceDisplacementDual
)
} -> std::same_as<void>;
};
template <typename Candidate>
concept InteriorDeformationExtension = requires(const std::remove_cvref_t<Candidate> &extension) {
typename std::remove_cvref_t<Candidate>::PreparedType;
requires PreparedInteriorDeformationExtension<typename std::remove_cvref_t<Candidate>::PreparedType>;
{ extension.descriptor() } noexcept -> std::same_as<InteriorDeformationExtensionDescriptor>;
{ extension.validate() } -> std::same_as<void>;
};
template <typename Extension, typename CompilationContext>
concept InteriorDeformationExtensionCompilable =
InteriorDeformationExtension<Extension> && requires(
const std::remove_cvref_t<Extension> &extension,
const std::remove_cvref_t<CompilationContext> &context
) {
{
compileInteriorDeformationExtension(extension, context)
} -> std::same_as<typename std::remove_cvref_t<Extension>::PreparedType>;
};
} // namespace mean_field::deformation

Some files were not shown because too many files have changed in this diff Show More