Compare commits

..

2 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
133 changed files with 227533 additions and 110545 deletions

View File

@@ -60,6 +60,8 @@ target_sources(mean_field
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/compactification/kelvin.cpp
libmeanfield/impl/physics/gravity.cpp
@@ -76,6 +78,7 @@ target_sources(mean_field
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
@@ -121,6 +124,8 @@ target_sources(mean_field
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
@@ -145,7 +150,13 @@ 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
@@ -155,6 +166,7 @@ target_sources(mean_field
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
@@ -216,6 +228,7 @@ target_sources(mean_field
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
@@ -272,6 +285,8 @@ add_executable(tests
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
@@ -324,6 +339,7 @@ add_executable(tests
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
@@ -346,14 +362,31 @@ add_executable(tests
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)
@@ -416,6 +449,26 @@ catch_discover_tests(
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
@@ -425,7 +478,7 @@ foreach (mean_field_mpi_ranks IN ITEMS 2 4)
${MPIEXEC_PREFLAGS}
$<TARGET_FILE:mpi_tests>
${MPIEXEC_POSTFLAGS}
"[mpi]"
"[mpi]~[single-rank]"
)
set_tests_properties(
mpi_${mean_field_mpi_ranks}_ranks

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

@@ -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

@@ -756,7 +756,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
setupTimings.problemConstructionSeconds = maximumRankSeconds(constructionStart, communicator);
announce(communicator, "projecting the n=3 Lane-Emden state");
@@ -919,7 +919,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
setupTimings.problemConstructionSeconds = maximumRankSeconds(constructionStart, communicator);
announce(communicator, "projecting the n=3 Lane-Emden state for gravity backend finalists");
@@ -1025,7 +1025,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected =
seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = radialSampleCount}));
auto dependencies = makeDependencies();

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

@@ -792,7 +792,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
@@ -851,7 +851,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
@@ -914,7 +914,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
@@ -961,7 +961,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto problem = equilibrium::discretize(model, std::move(finiteElements));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());

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

@@ -218,7 +218,7 @@ TEST_CASE(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(stellarModel, finiteElementModel);
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");

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,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

@@ -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

@@ -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

@@ -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

@@ -54,6 +54,7 @@ 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) {
@@ -68,13 +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;
// 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;
@@ -82,6 +91,11 @@ 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);
@@ -119,6 +133,8 @@ namespace mean_field::mapping::compactification {
}
RadialFactors 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;
@@ -127,12 +143,21 @@ namespace mean_field::mapping::compactification {
result.mapping_jacobian.SetSize(dimension, dimension);
for (int i = 0; i < dimension; ++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;
result.mapping_jacobian(i, j) = factors.scale * input.displacement_jacobian(i, j) + input.displaced_position(i) * scale_gradient;
}
}

View File

@@ -167,6 +167,7 @@ namespace mean_field::mapping {
m_field_value.SetSize(dimension);
m_field_jacobian.SetSize(dimension, dimension);
m_reference_field_jacobian.SetSize(dimension, dimension);
m_compactification_point.coordinate = 0.0;
m_compactification_point.coordinate_gradient.SetSize(dimension);
@@ -538,6 +539,10 @@ namespace mean_field::mapping {
return MappingStatus::valid;
}
/**
* @brief Evaluate a displacement field dof matrix at a given integration point and compute what the displacement of that point is and what the gradient of the the displacement is with respect to the computational coordinates / reference frame.
* @note There is actually nothing in this function preventing some field other than displacement from being passed through here; this should maybe be tightened.
*/
void DomainMapper::EvaluateField(
const ElementDisplacementData &field,
mfem::ElementTransformation &transformation,
@@ -551,22 +556,30 @@ namespace mean_field::mapping {
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);
if (inverse_mesh_jacobian != nullptr) {
workspace.m_reference_dshape.SetSize(element.GetDof(), m_options.dimension);
element.CalcDShape(integration_point, workspace.m_reference_dshape);
mfem::Mult(workspace.m_reference_dshape, *inverse_mesh_jacobian, workspace.m_mesh_dshape);
} else {
element.CalcPhysDShape(transformation, workspace.m_mesh_dshape);
}
value.SetSize(m_options.dimension);
dof_matrix.MultTranspose(workspace.m_shape, value);
jacobian.SetSize(m_options.dimension, m_options.dimension);
mfem::MultAtB(dof_matrix, workspace.m_mesh_dshape, jacobian);
if (inverse_mesh_jacobian != nullptr || element.GetMapType() == mfem::FiniteElement::VALUE) {
workspace.m_reference_dshape.SetSize(element.GetDof(), m_options.dimension);
element.CalcDShape(integration_point, workspace.m_reference_dshape);
// Contract DOFs before applying fixed-mesh geometry. This is the
// same DOF^T * (Dshape * J_mesh^-1), without transforming every
// basis gradient. The scratch matrix must not alias the cached
// inverse supplied by EvaluateVolumeVariation.
mfem::MultAtB(dof_matrix, workspace.m_reference_dshape, workspace.m_reference_field_jacobian);
const mfem::DenseMatrix &inverseMeshJacobian =
inverse_mesh_jacobian != nullptr ? *inverse_mesh_jacobian : transformation.InverseJacobian();
mfem::Mult(workspace.m_reference_field_jacobian, inverseMeshJacobian, jacobian);
} else {
// Retain the original finite-element-specific physical-gradient
// path for mapping types without the ordinary VALUE pullback.
workspace.m_mesh_dshape.SetSize(element.GetDof(), m_options.dimension);
element.CalcPhysDShape(transformation, workspace.m_mesh_dshape);
mfem::MultAtB(dof_matrix, workspace.m_mesh_dshape, jacobian);
}
}
MappingStatus DomainMapper::EvaluatePoint(
@@ -594,6 +607,7 @@ namespace mean_field::mapping {
context.reference_position.SetSize(m_options.dimension);
transformation.Transform(integration_point, context.reference_position);
// Get the displacement field value and its Jacobian at the integration point. Note these are in the workspace to avoid repeated allocations.
EvaluateField(
element_data.displacement, transformation, integration_point, workspace, workspace.m_field_value,
workspace.m_field_jacobian, nullptr
@@ -605,17 +619,32 @@ namespace mean_field::mapping {
}
context.displaced_position.SetSize(m_options.dimension);
// Get the position of the point in physical space by adding the displacement to the reference position. Note MFEM really dislikes raw arithmetic operators
// so we need to first assign the reference position then use the in place += operator.
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;
// Ensure that the diagonal of the displacement Jacobian is incremented by 1.0 to account for the identity mapping from reference to physical space.
// recall that r = x + d (where d is the workspace.m_field_value and x is context.reference_position) then we can differentiate this
// component wise to find the gradient of the displaced position wrt. the mesh coordinate (reference position). E.g as you move along
// the mesh coordinate how much does the physical coordinate change and in what direction. Lets call this F
// F = \frac{\partial r_i}{\partial x_j} where r is the displaced position and x is the mesh position.
// We then have F = \frac{\partial x_i}{\partial x_j} + \frac{\partial d_i}{x_j} where d is the displacement (recall r = x + d)
// By definition the first term is the identity matrix. The second term we get out of EvaluateField. Thus why we need to add the identity matrix here
for (int i = 0; i < m_options.dimension; ++i)
context.displacement_jacobian(i, i) += 1.0;
context.compactified = IsCompactifiedElement(transformation);
// This branch only runs for vacuum elements
if (context.compactified) {
// There are two things that we need to the mapping. First is a reference coordinate which stroid embeds into the mesh at mesh generation time, this is
// parameterized from 0 - 1 where 0 is the model surface and 1 is the mesh exterior (what will becomes the compactified infinity, note also we never actually evaluate at s=1; rather we define some arbitrary small tolerance to approach s=1). Lets call this s. We also need
// the gradient of s as we move along the mesh coordinates. All of this is stashes within workspace.m_compactification_point.
const MappingStatus coordinate_status = EvaluateCompactificationCoordinate(
element_data.compactification, transformation, integration_point, workspace,
workspace.m_compactification_point, nullptr
@@ -632,6 +661,8 @@ namespace mean_field::mapping {
.compactification_coordinate_gradient = workspace.m_compactification_point.coordinate_gradient
};
// This apply whatever the exterior map is to generate the new physical exterior coordinate and jacobian between physical and reference space.
// In general we have only implemented a kelvin mapping; however, in future additional mappings may be implemented.
const MappingStatus exterior_status = m_exterior_map->Evaluate(exterior_input, workspace.m_exterior_result);
if (exterior_status != MappingStatus::valid)
return exterior_status;
@@ -643,6 +674,7 @@ namespace mean_field::mapping {
context.mapping_jacobian = context.displacement_jacobian;
}
// Validation work
if (!vector_is_finite(context.physical_position) || !matrix_is_finite(context.mapping_jacobian))
return MappingStatus::non_finite_result;
@@ -650,9 +682,12 @@ namespace mean_field::mapping {
if (!std::isfinite(context.mapping_determinant))
return MappingStatus::non_finite_result;
if (context.mapping_determinant <= 0.0)
// This is the most common error we see come out of this function, specifically it is common when we try to deform the mesh too much in one step.
return MappingStatus::non_positive_determinant;
context.inverse_mapping_jacobian.SetSize(m_options.dimension, m_options.dimension);
// It can be useful to have the inverse jacobian, here we just use MFEM's build in inverse tooling.
mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian);
if (!matrix_is_finite(context.inverse_mapping_jacobian))

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

@@ -1,5 +1,6 @@
module;
#include <cmath>
#include <expected>
#include <memory>
#include <mfem.hpp>
@@ -9,6 +10,30 @@ import :operators.context.gravity_field;
namespace {
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,
@@ -295,7 +320,21 @@ namespace mean_field::operators::context::gravity_field {
const DiscretizationRevision discretization_revision,
const DisplacementRevision displacement_revision
) {
return PrepareImpl(
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
);
}
@@ -305,10 +344,23 @@ namespace mean_field::operators::context::gravity_field {
const DiscretizationRevision discretization_revision,
const DisplacementRevision displacement_revision
) {
return PrepareImpl(displacement, discretization_revision, displacement_revision, PreparationMode::primal);
auto result =
TryPrepareImpl(displacement, discretization_revision, displacement_revision, PreparationMode::primal);
if (!result.has_value()) {
throwGravityFieldPreparationRejection(result.error());
}
return std::move(result).value();
}
GravityFieldGeometryPreparation GravityFieldGeometryContext::PrepareImpl(
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,
@@ -340,19 +392,24 @@ namespace mean_field::operators::context::gravity_field {
return preparation;
}
const auto prepare_mass = [&](PreparedMappedHDivMassOperator &mass_operator) {
// 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;
const auto prepare_mass = [&](PreparedMappedHDivMassOperator &mass_operator) {
if (requires_variation) {
mass_operator.Prepare(displacement);
} else {
mass_operator.PreparePrimal(displacement);
return mass_operator.TryPrepare(displacement);
}
return mass_operator.TryPreparePrimal(displacement);
};
const auto prepare_source = [&](PreparedMappedGravitySourceOperator &source_operator) {
if (requires_variation) {
source_operator.Prepare(displacement);
} else {
source_operator.PreparePrimal(displacement);
return source_operator.TryPrepare(displacement);
}
return source_operator.TryPreparePrimal(displacement);
};
if (discretization_changed) {
@@ -361,8 +418,14 @@ namespace mean_field::operators::context::gravity_field {
auto divergence_operator = make_divergence_operator(m_fem);
auto transpose_divergence_operator = std::make_unique<mfem::TransposeOperator>(divergence_operator.get());
prepare_mass(*mass_operator);
prepare_source(*source_operator);
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);
@@ -383,8 +446,14 @@ namespace mean_field::operators::context::gravity_field {
"operator."
);
prepare_mass(*m_mass_operator);
prepare_source(*m_source_operator);
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;
@@ -510,6 +579,17 @@ namespace mean_field::operators::context::gravity_field {
GravityFieldPreparationReport GravityFieldLinearizationContext::Prepare(
const GravityFieldStateView &state,
const GravityFieldRevisions &revisions
) {
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,
@@ -554,8 +634,17 @@ namespace mean_field::operators::context::gravity_field {
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.SetSize(m_density_map.full_size());

View File

@@ -4,6 +4,7 @@ module;
#include <cstdint>
#include <limits>
#include <mfem.hpp>
#include <utility>
module mean_field;
import :operators.gravity_field;
@@ -273,6 +274,18 @@ namespace mean_field::operators {
context::gravity_field::GravityFieldPreparationReport GravityFieldOperator::Prepare(
const mfem::Vector &state,
const context::gravity_field::GravityFieldRevisions &revisions
) {
auto result = TryPrepare(state, revisions);
if (!result.has_value()) {
context::gravity_field::throwGravityFieldPreparationRejection(result.error());
}
return std::move(result).value();
}
context::gravity_field::GravityFieldPreparationResult<context::gravity_field::GravityFieldPreparationReport>
GravityFieldOperator::TryPrepare(
const mfem::Vector &state,
const context::gravity_field::GravityFieldRevisions &revisions
) {
using form = utils::blocks::gravity_field_form;
@@ -295,7 +308,7 @@ namespace mean_field::operators {
const mfem::Vector gravity_potential =
make_read_only_value_view(state, m_state_offsets, gravity_potential_block);
return m_linearization_context.Prepare(
return m_linearization_context.TryPrepare(
{.density = density,
.displacement = displacement,
.gravity_gradient = gravity_gradient,

View File

@@ -2,9 +2,12 @@ module;
#include <array>
#include <cmath>
#include <expected>
#include <optional>
#include <stdexcept>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
@@ -19,6 +22,70 @@ namespace {
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,
@@ -187,11 +254,6 @@ namespace {
"The gravity-displacement-force displacement dimension does not "
"match the mesh dimension."
);
validate_finite_vector(
displacementTrue, "The gravity-displacement-force displacement contains a "
"non-finite value."
);
}
void validate_density(
@@ -200,7 +262,6 @@ namespace {
const char *message
) {
MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message);
validate_finite_vector(density, message);
}
void validate_gravity_gradient(
@@ -209,11 +270,9 @@ namespace {
const char *message
) {
MFEM_VERIFY(gravityGradient.Size() == f.gravityFluxFes->GetTrueVSize(), message);
validate_finite_vector(gravityGradient, message);
}
void apply_gravity_displacement_force_action(
[[nodiscard]] Result apply_gravity_displacement_force_action(
const mean_field::fem::FEM &f,
const mean_field::mapping::DomainMapper &domainMapper,
const GravityDisplacementForceAction requestedAction,
@@ -223,7 +282,8 @@ namespace {
const mfem::Vector *gravityGradientVariationTrue,
const mfem::Vector *displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
const bool reportCandidateRejection
) {
validate_common_inputs(f, domainMapper, displacementTrue);
@@ -308,6 +368,34 @@ namespace {
);
}
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;
@@ -374,6 +462,8 @@ namespace {
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);
@@ -516,13 +606,19 @@ namespace {
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mean_field::mapping::MappingStatus::valid,
"Stateless mapping failed in the gravity-displacement-"
"force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex << ", status: " << static_cast<int>(mappingStatus)
);
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(
@@ -530,13 +626,19 @@ namespace {
workspace, mappingVariation
);
MFEM_VERIFY(
variationStatus == mean_field::mapping::MappingStatus::valid,
"Stateless mapping variation failed in the gravity-"
"displacement-force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute << ", quadrature point: "
<< quadratureIndex << ", status: " << static_cast<int>(variationStatus)
);
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);
@@ -624,10 +726,16 @@ namespace {
const double contribution = displacementShape(scalarDof) * forceValue(component);
MFEM_VERIFY(
std::isfinite(contribution), "The gravity-displacement-force kernel "
"encountered a non-finite contribution."
);
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;
}
@@ -641,11 +749,48 @@ namespace {
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,
@@ -654,10 +799,12 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
) {
apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::residual, &densityTrue, nullptr, &gravityGradientTrue,
nullptr, nullptr, displacementTrue, 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(
@@ -668,9 +815,9 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_gravity_displacement_force_action(
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::density, nullptr, &densityVariationTrue,
&baseGravityGradientTrue, nullptr, nullptr, displacementTrue, actionTrue
&baseGravityGradientTrue, nullptr, nullptr, displacementTrue, actionTrue, false
);
}
@@ -682,9 +829,9 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_gravity_displacement_force_action(
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::gravityGradient, &baseDensityTrue, nullptr, nullptr,
&gravityGradientVariationTrue, nullptr, displacementTrue, actionTrue
&gravityGradientVariationTrue, nullptr, displacementTrue, actionTrue, false
);
}
@@ -697,9 +844,9 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_gravity_displacement_force_action(
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::displacement, &baseDensityTrue, nullptr,
&baseGravityGradientTrue, nullptr, &displacementVariationTrue, displacementTrue, actionTrue
&baseGravityGradientTrue, nullptr, &displacementVariationTrue, displacementTrue, actionTrue, false
);
}
@@ -714,10 +861,10 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_gravity_displacement_force_action(
(void)apply_gravity_displacement_force_action(
f, domainMapper, GravityDisplacementForceAction::complete, &baseDensityTrue, &densityVariationTrue,
&baseGravityGradientTrue, &gravityGradientVariationTrue, &displacementVariationTrue, displacementTrue,
actionTrue
actionTrue, false
);
}
} // namespace mean_field::operators::kernels

View File

@@ -2,9 +2,12 @@ module;
#include <array>
#include <cmath>
#include <expected>
#include <optional>
#include <stdexcept>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
@@ -19,6 +22,70 @@ namespace {
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,
@@ -186,11 +253,6 @@ namespace {
"The rotational-displacement-force displacement dimension does "
"not match the mesh dimension."
);
validate_finite_vector(
displacementTrue, "The rotational-displacement-force displacement contains a "
"non-finite value."
);
}
void validate_density(
@@ -199,10 +261,9 @@ namespace {
const char *message
) {
MFEM_VERIFY(density.Size() == f.densityFes->GetTrueVSize(), message);
validate_finite_vector(density, message);
}
void apply_rotational_displacement_force_action(
[[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,
@@ -211,7 +272,8 @@ namespace {
const mfem::Vector *densityVariationTrue,
const mfem::Vector *displacementVariationTrue,
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
const bool reportCandidateRejection
) {
validate_common_inputs(f, domainMapper, displacementTrue);
@@ -261,6 +323,28 @@ namespace {
);
}
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;
@@ -311,6 +395,8 @@ namespace {
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);
@@ -427,13 +513,19 @@ namespace {
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mean_field::mapping::MappingStatus::valid,
"Stateless mapping failed in the rotational-"
"displacement-force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadratureIndex << ", status: " << static_cast<int>(mappingStatus)
);
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(
@@ -441,13 +533,19 @@ namespace {
workspace, mappingVariation
);
MFEM_VERIFY(
variationStatus == mean_field::mapping::MappingStatus::valid,
"Stateless mapping variation failed in the "
"rotational-displacement-force kernel. Element: "
<< elementId << ", attribute: " << transformation->Attribute << ", quadrature point: "
<< quadratureIndex << ", status: " << static_cast<int>(variationStatus)
);
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);
@@ -512,10 +610,16 @@ namespace {
const double contribution = displacementShape(scalarDof) * weightedForce(component);
MFEM_VERIFY(
std::isfinite(contribution), "The rotational-displacement-force kernel "
"encountered a non-finite contribution."
);
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;
}
@@ -529,11 +633,48 @@ namespace {
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,
@@ -542,10 +683,12 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &residualTrue
) {
apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::residual, &densityTrue, nullptr, nullptr,
displacementTrue, 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(
@@ -556,9 +699,9 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_rotational_displacement_force_action(
(void)apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::density, nullptr, &densityVariationTrue,
nullptr, displacementTrue, actionTrue
nullptr, displacementTrue, actionTrue, false
);
}
@@ -571,9 +714,9 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_rotational_displacement_force_action(
(void)apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::displacement, &baseDensityTrue, nullptr,
&displacementVariationTrue, displacementTrue, actionTrue
&displacementVariationTrue, displacementTrue, actionTrue, false
);
}
@@ -587,9 +730,9 @@ namespace mean_field::operators::kernels {
const mfem::Vector &displacementTrue,
mfem::Vector &actionTrue
) {
apply_rotational_displacement_force_action(
(void)apply_rotational_displacement_force_action(
f, domainMapper, rotation, RotationalDisplacementForceAction::complete, &baseDensityTrue,
&densityVariationTrue, &displacementVariationTrue, displacementTrue, actionTrue
&densityVariationTrue, &displacementVariationTrue, displacementTrue, actionTrue, false
);
}
} // namespace mean_field::operators::kernels

View File

@@ -3,9 +3,14 @@ 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;
@@ -18,10 +23,71 @@ namespace {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
void validate_finite_vector(const mfem::Vector &vector, const char *message) {
[[nodiscard]] bool is_finite_vector(const mfem::Vector &vector) {
for (int index = 0; index < vector.Size(); ++index) {
MFEM_VERIFY(std::isfinite(vector(index)), message);
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(
@@ -51,11 +117,8 @@ namespace {
);
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
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(
@@ -108,9 +171,8 @@ namespace mean_field::operators {
"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,
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(
@@ -126,31 +188,34 @@ namespace mean_field::operators {
const double angularVelocity,
const AngularMomentumDependencies &dependencies
) {
MFEM_VERIFY(
std::isfinite(angularVelocity),
"PreparedAngularMomentumOperator requires a finite angular-velocity coordinate."
);
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,
m_preparedDependencies.discretization, dependencies.discretization,
"A new angular-momentum discretization identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.density,
dependencies.density,
m_preparedDependencies.density, dependencies.density,
"A new angular-momentum density identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.displacement,
dependencies.displacement,
m_preparedDependencies.displacement, dependencies.displacement,
"A new angular-momentum displacement identity must change its revision."
);
validate_identity_transition(
m_preparedDependencies.rotation,
dependencies.rotation,
m_preparedDependencies.rotation, dependencies.rotation,
"A new angular-momentum rotation identity must change its revision."
);
}
@@ -159,36 +224,67 @@ namespace mean_field::operators {
!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;
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
}
);
}
m_isPrepared = false;
PreparedAngularMomentumReport report;
if (rebuildStaticPlan) {
BuildStaticPlan();
report.rebuiltStaticPlan = true;
}
if (refreshGeometry) {
RefreshGeometry(m_gravityContext.GetGeometryContext().GetDisplacementTrue());
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) {
RefreshDensity(m_gravityContext.GetDensityTrue());
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;
m_angularVelocity = angularVelocity;
report.updatedAngularVelocity = true;
}
if (refreshGeometry || refreshDensity || updateAngularVelocity) {
AssembleResidual();
auto rejection = TryAssembleResidual();
if (rejection.has_value()) {
return std::unexpected(*rejection);
}
report.assembledResidual = true;
}
m_preparedDependencies = dependencies;
m_isPrepared = true;
m_isPrepared = true;
return report;
}
@@ -204,8 +300,8 @@ namespace mean_field::operators {
}
++localStellarElementCount;
m_elements.emplace_back();
ElementPAData &data = m_elements.back();
data.elementId = elementId;
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);
@@ -215,35 +311,39 @@ namespace mean_field::operators {
const mfem::FiniteElement &densityElement = *m_fem.densityFes->GetFE(elementId);
const mfem::IntegrationRule &integrationRule =
get_moment_of_inertia_rule(m_fem, densityElement, *transformation);
data.quadraturePoints.resize(integrationRule.GetNPoints());
for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) {
QuadraturePointData &point = data.quadraturePoints[quadraturePoint];
point.integrationPoint = integrationRule.IntPoint(quadraturePoint);
point.densityShape.SetSize(densityElement.GetDof());
densityElement.CalcShape(point.integrationPoint, point.densityShape);
}
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;
MPI_Allreduce(
&localStellarElementCount,
&globalStellarElementCount,
1,
MPI_INT,
MPI_SUM,
m_fem.mesh->GetComm()
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.");
}
void PreparedAngularMomentumOperator::RefreshGeometry(const mfem::Vector &displacement) {
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."
);
validate_finite_vector(displacement, "Angular-momentum geometry contains a non-finite displacement.");
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);
@@ -254,75 +354,110 @@ namespace mean_field::operators {
if (data.compactificationDofTransformation != nullptr) {
data.compactificationDofTransformation->InvTransformPrimal(data.compactification);
}
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
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
compactificationElement, data.compactification
);
const mapping::ElementMappingData mappingData{
.displacement = displacementData,
.compactification = compactificationData
.displacement = displacementData, .compactification = compactificationData
};
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (QuadraturePointData &point : data.quadraturePoints) {
for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) {
const mapping::MappingStatus status = m_domainMapper.EvaluateVolume(
mappingData,
*transformation,
point.integrationPoint,
workspace,
point.mappingContext
mappingData, *transformation, data.integrationRule->IntPoint(quadraturePoint), workspace,
mappingContext
);
MFEM_VERIFY(
status == mapping::MappingStatus::valid && !point.mappingContext.mapping.compactified,
"Mapped angular-momentum geometry is invalid. Element: " << data.elementId
);
point.cylindricalRadiusSquared =
CylindricalRadiusSquared(point.mappingContext.mapping.physical_position);
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;
}
void PreparedAngularMomentumOperator::RefreshDensity(const mfem::Vector &density) {
MFEM_VERIFY(
density.Size() == m_fem.densityFes->GetTrueVSize(),
"Angular-momentum density has the wrong size."
);
validate_finite_vector(density, "Angular-momentum density contains a non-finite value.");
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);
}
for (QuadraturePointData &point : data.quadraturePoints) {
point.density = elementDensity * point.densityShape;
MFEM_VERIFY(std::isfinite(point.density), "Angular-momentum quadrature density is non-finite.");
if (!is_finite_vector(elementDensity)) {
return false;
}
data.densityBasis->GetValues().Mult(elementDensity, data.density);
if (!is_finite_vector(data.density)) {
return false;
}
}
return true;
}
void PreparedAngularMomentumOperator::AssembleResidual() {
std::optional<AngularMomentumPreparationRejection> PreparedAngularMomentumOperator::TryAssembleResidual() {
double localMomentOfInertia = 0.0;
for (const ElementPAData &data : m_elements) {
for (const QuadraturePointData &point : data.quadraturePoints) {
localMomentOfInertia += point.density * point.cylindricalRadiusSquared *
point.mappingContext.quadrature.weight;
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_momentOfInertia) && m_momentOfInertia >= 0.0,
"PreparedAngularMomentumOperator assembled an invalid moment of inertia."
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 {
@@ -331,23 +466,25 @@ namespace mean_field::operators {
++m_residualApplicationCount;
}
double PreparedAngularMomentumOperator::EvaluateDensityMomentActionLocal(
const mfem::Vector &densityVariation
) const {
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);
}
for (const QuadraturePointData &point : data.quadraturePoints) {
localAction += (m_elementDensityVariation * point.densityShape) *
point.cylindricalRadiusSquared * point.mappingContext.quadrature.weight;
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;
@@ -363,48 +500,42 @@ namespace mean_field::operators {
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 &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
compactificationElement, data.compactification
);
const mapping::ElementMappingData mappingData{
.displacement = baseDisplacementData,
.compactification = compactificationData
.displacement = baseDisplacementData, .compactification = compactificationData
};
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (const QuadraturePointData &point : data.quadraturePoints) {
for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) {
data.mappingContexts.Load(quadraturePoint, mappingContext);
const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation(
mappingData,
directionData,
*transformation,
point.integrationPoint,
point.mappingContext,
workspace,
variation
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(
point.mappingContext.mapping.physical_position,
variation.mapping.physical_position_variation
mappingContext.mapping.physical_position, variation.mapping.physical_position_variation
);
localAction += point.density *
(radiusSquaredVariation * point.mappingContext.quadrature.weight +
point.cylindricalRadiusSquared * variation.weight_variation);
localAction += data.density(quadraturePoint) *
(radiusSquaredVariation * data.quadratureWeights(quadraturePoint) +
data.cylindricalRadiusSquared(quadraturePoint) * variation.weight_variation);
}
}
return localAction;
@@ -419,7 +550,7 @@ namespace mean_field::operators {
densityVariation.Size() == m_gravityContext.GetDensityMap().reduced_size(),
"Angular-momentum density action has the wrong reduced size."
);
validate_finite_vector(densityVariation, "Angular-momentum density direction is non-finite.");
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));
@@ -435,11 +566,10 @@ namespace mean_field::operators {
displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(),
"Angular-momentum displacement action has the wrong reduced size."
);
validate_finite_vector(displacementVariation, "Angular-momentum displacement direction is non-finite.");
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));
action(0) = m_angularVelocity * GlobalSum(EvaluateDisplacementMomentActionLocal(m_displacementVariationTrue));
++m_actionStatistics.displacementApplications;
}
@@ -466,24 +596,22 @@ namespace mean_field::operators {
displacementVariation.Size() == m_gravityContext.GetDisplacementMap().reduced_size(),
"Angular-momentum complete action has incompatible reduced coordinates."
);
validate_finite_vector(densityVariation, "Angular-momentum density direction is non-finite.");
validate_finite_vector(displacementVariation, "Angular-momentum displacement direction is non-finite.");
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;
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
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) {
@@ -491,18 +619,22 @@ namespace mean_field::operators {
radiusSquared += relative * relative;
axialPosition += axis[static_cast<std::size_t>(component)] * relative;
}
return std::max(0.0, radiusSquared - axialPosition * axialPosition);
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();
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;
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);
@@ -514,7 +646,9 @@ namespace mean_field::operators {
double PreparedAngularMomentumOperator::GlobalSum(const double localValue) const {
double globalValue = 0.0;
MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_fem.mesh->GetComm());
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;
}
@@ -554,15 +688,15 @@ namespace mean_field::operators {
AngularMomentumConstraintReport PreparedAngularMomentumOperator::GetConstraintReport() const {
VerifyPrepared();
const double target = GetTargetAngularMomentum();
const double target = GetTargetAngularMomentum();
const double residual = m_currentAngularMomentum - target;
return {
.targetAngularMomentum = target,
.targetAngularMomentum = target,
.achievedAngularMomentum = m_currentAngularMomentum,
.momentOfInertia = m_momentOfInertia,
.angularVelocity = m_angularVelocity,
.dimensionalResidual = residual,
.scaledResidual = residual / std::max(std::abs(target), 1.0e-300)
.momentOfInertia = m_momentOfInertia,
.angularVelocity = m_angularVelocity,
.dimensionalResidual = residual,
.scaledResidual = residual / std::max(std::abs(target), 1.0e-300)
};
}

View File

@@ -3,10 +3,15 @@ module;
#include <array>
#include <cmath>
#include <cstdint>
#include <expected>
#include <limits>
#include <mfem.hpp>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mpi.h>
module mean_field;
import :operators.prepared_barotropic_closure;
@@ -148,6 +153,154 @@ namespace {
return *resolution.integration_rule;
}
using BarotropicRejection = mean_field::operators::BarotropicClosurePreparationRejection;
using BarotropicRejectionReason = mean_field::operators::BarotropicClosurePreparationRejectionReason;
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
/*
* Rejections are selected by preparation phase, then by an explicit
* detail priority. An earlier phase wins: mapping, quadrature algebra,
* then EOS evaluation. Never depend on the declaration order or the
* underlying integer representation of either public enum.
*/
[[nodiscard]] int mapping_status_priority(const mean_field::mapping::MappingStatus status) {
using Status = mean_field::mapping::MappingStatus;
switch (status) {
case Status::non_positive_determinant:
return 7;
case Status::non_finite_result:
return 6;
case Status::non_finite_input:
return 5;
case Status::outside_reference_domain:
return 4;
case Status::at_compactified_infinity:
return 3;
case Status::invalid_reference_radius:
return 2;
case Status::valid:
throw std::logic_error("A valid mapping cannot be a barotropic candidate rejection.");
case Status::invalid_dimension:
throw std::logic_error("A mapping dimension error cannot be a barotropic candidate rejection.");
}
throw std::logic_error("Unknown mapping status in barotropic candidate rejection.");
}
[[nodiscard]] mean_field::mapping::MappingStatus mapping_status_from_priority(const int priority) {
using Status = mean_field::mapping::MappingStatus;
switch (priority) {
case 7:
return Status::non_positive_determinant;
case 6:
return Status::non_finite_result;
case 5:
return Status::non_finite_input;
case 4:
return Status::outside_reference_domain;
case 3:
return Status::at_compactified_infinity;
case 2:
return Status::invalid_reference_radius;
default:
throw std::logic_error("Invalid synchronized mapping priority for barotropic preparation.");
}
}
[[nodiscard]] int eos_error_priority(const mean_field::eos::EvaluationErrorCode code) {
using Code = mean_field::eos::EvaluationErrorCode;
switch (code) {
case Code::outside_domain:
return 3;
case Code::nonfinite_input:
return 2;
case Code::nonfinite_result:
return 1;
case Code::unsupported_relation:
case Code::unsupported_derivative:
case Code::wrong_input_count:
case Code::wrong_input_quantity:
throw std::logic_error("A structural EOS error cannot be a barotropic candidate rejection.");
}
throw std::logic_error("Unknown EOS error in barotropic candidate rejection.");
}
[[nodiscard]] mean_field::eos::EvaluationErrorCode eos_error_from_priority(const int priority) {
using Code = mean_field::eos::EvaluationErrorCode;
switch (priority) {
case 3:
return Code::outside_domain;
case 2:
return Code::nonfinite_input;
case 1:
return Code::nonfinite_result;
default:
throw std::logic_error("Invalid synchronized EOS priority for barotropic preparation.");
}
}
[[nodiscard]] int rejection_priority(const BarotropicRejection &rejection) {
switch (rejection.reason) {
case BarotropicRejectionReason::mapping_failure:
return 300 + mapping_status_priority(rejection.mappingStatus);
case BarotropicRejectionReason::invalid_quadrature_data:
return 200;
case BarotropicRejectionReason::equation_of_state:
return 100 + eos_error_priority(rejection.equationOfStateError);
}
throw std::logic_error("Unknown barotropic candidate-rejection reason.");
}
[[nodiscard]] BarotropicRejection rejection_from_priority(const int priority) {
if (priority >= 300) {
return {
.reason = BarotropicRejectionReason::mapping_failure,
.mappingStatus = mapping_status_from_priority(priority - 300)
};
}
if (priority == 200) {
return {.reason = BarotropicRejectionReason::invalid_quadrature_data};
}
if (priority >= 100) {
return {
.reason = BarotropicRejectionReason::equation_of_state,
.equationOfStateError = eos_error_from_priority(priority - 100)
};
}
throw std::logic_error("Invalid synchronized barotropic candidate-rejection priority.");
}
void retain_higher_priority_rejection(
std::optional<BarotropicRejection> &current,
const BarotropicRejection candidate
) {
if (!current.has_value() || rejection_priority(candidate) > rejection_priority(*current)) {
current = candidate;
}
}
[[nodiscard]] std::optional<BarotropicRejection> synchronize_rejection(
const std::optional<BarotropicRejection> &local,
const MPI_Comm communicator
) {
const int localPriority = local.has_value() ? rejection_priority(*local) : 0;
int globalPriority = 0;
if (MPI_Allreduce(&localPriority, &globalPriority, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("PreparedBarotropicClosureOperator could not synchronize candidate validity.");
}
if (globalPriority == 0) {
return std::nullopt;
}
return rejection_from_priority(globalPriority);
}
} // namespace
namespace mean_field::operators {
@@ -256,6 +409,29 @@ namespace mean_field::operators {
PreparedBarotropicClosureReport PreparedBarotropicClosureOperator::Prepare(
const context::barotropic::BarotropicClosureStateView &state,
const context::barotropic::BarotropicClosureDependencies &dependencies
) {
auto result = TryPrepare(state, dependencies);
if (!result.has_value()) {
const BarotropicClosurePreparationRejection &rejection = result.error();
switch (rejection.reason) {
case BarotropicClosurePreparationRejectionReason::equation_of_state:
throw eos::EvaluationError(
rejection.equationOfStateError,
"PreparedBarotropicClosureOperator encountered invalid thermodynamic data."
);
case BarotropicClosurePreparationRejectionReason::invalid_quadrature_data:
throw std::domain_error("PreparedBarotropicClosureOperator encountered non-finite quadrature data.");
case BarotropicClosurePreparationRejectionReason::mapping_failure:
throw std::domain_error("PreparedBarotropicClosureOperator could not map the candidate geometry.");
}
throw std::logic_error("Unknown barotropic candidate-rejection reason.");
}
return std::move(result).value();
}
BarotropicClosurePreparationResult PreparedBarotropicClosureOperator::TryPrepare(
const context::barotropic::BarotropicClosureStateView &state,
const context::barotropic::BarotropicClosureDependencies &dependencies
) {
PreparedBarotropicClosureReport report;
report.contextReport = m_context.Prepare(state, dependencies);
@@ -274,8 +450,8 @@ namespace mean_field::operators {
m_displacementMap.scatter(m_context.GetDisplacement(), m_baseDisplacementTrue);
m_isPrepared = false;
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
std::size_t preparedElementCount{0};
mfem::Vector baseDensityLocal;
mfem::Vector baseEnthalpyLocal;
@@ -286,6 +462,7 @@ namespace mean_field::operators {
true_to_local(*m_fem.displacementFes, m_baseDisplacementTrue, displacementLocal);
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingContext mappingContext;
mfem::Array<int> compactificationDofs;
@@ -296,6 +473,7 @@ namespace mean_field::operators {
mfem::Vector densityShape;
mfem::Vector enthalpyShape;
std::optional<BarotropicClosurePreparationRejection> localRejection;
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId);
@@ -308,8 +486,10 @@ namespace mean_field::operators {
continue;
}
m_elements.emplace_back();
ElementPAData &data = m_elements.back();
if (preparedElementCount == m_elements.size()) {
m_elements.emplace_back();
}
ElementPAData &data = m_elements[preparedElementCount++];
data.elementId = elementId;
data.densityDofTransformation = m_fem.densityFes->GetElementDofs(elementId, data.densityDofs);
@@ -356,13 +536,15 @@ namespace mean_field::operators {
const mfem::IntegrationRule &integrationRule =
get_eos_rule(m_fem, m_equationOfState, densityElement, enthalpyElement, *transformation);
data.integrationRule = &integrationRule;
const int quadraturePointCount = integrationRule.GetNPoints();
const int densityDofCount = densityElement.GetDof();
const int enthalpyDofCount = enthalpyElement.GetDof();
data.densityBasis.SetSize(quadraturePointCount, densityDofCount);
data.enthalpyBasis.SetSize(quadraturePointCount, enthalpyDofCount);
data.densityBasis = m_fem.GetReferenceTables().GetScalarTable(densityElement, integrationRule);
data.enthalpyBasis = m_fem.GetReferenceTables().GetScalarTable(enthalpyElement, integrationRule);
data.displacementBasis = m_fem.GetReferenceTables().GetScalarTable(displacementElement, integrationRule);
data.inverseElementJacobians.SetSize(
quadraturePointCount, m_fem.mesh->Dimension() * m_fem.mesh->Dimension()
);
@@ -378,18 +560,21 @@ namespace mean_field::operators {
transformation->SetIntPoint(&integrationPoint);
mapping::VolumeMappingContext mappingContext;
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mapping::MappingStatus::valid,
"Stateless mapping failed while preparing the barotropic closure operator. Element: "
<< elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadraturePoint << ", status: " << static_cast<int>(mappingStatus)
mappingStatus != mapping::MappingStatus::invalid_dimension,
"Stateless mapping reported a dimension error while preparing the barotropic closure operator."
);
if (mappingStatus != mapping::MappingStatus::valid) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::mapping_failure,
.mappingStatus = mappingStatus}
);
continue;
}
MFEM_VERIFY(
!mappingContext.mapping.compactified,
@@ -403,19 +588,48 @@ namespace mean_field::operators {
}
}
densityElement.CalcShape(integrationPoint, densityShape);
enthalpyElement.CalcShape(integrationPoint, enthalpyShape);
data.densityBasis->GetValues().GetRow(quadraturePoint, densityShape);
data.enthalpyBasis->GetValues().GetRow(quadraturePoint, enthalpyShape);
for (int densityDof = 0; densityDof < densityDofCount; ++densityDof) {
data.densityBasis(quadraturePoint, densityDof) = densityShape(densityDof);
}
for (int enthalpyDof = 0; enthalpyDof < enthalpyDofCount; ++enthalpyDof) {
data.enthalpyBasis(quadraturePoint, enthalpyDof) = enthalpyShape(enthalpyDof);
if (!vector_is_finite(densityShape) || !vector_is_finite(enthalpyShape)) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::invalid_quadrature_data}
);
continue;
}
const double density = elementBaseDensity * densityShape;
const double enthalpy = elementBaseEnthalpy * enthalpyShape;
const double quadratureWeight = mappingContext.quadrature.weight;
if (!std::isfinite(quadratureWeight) || quadratureWeight <= 0.0) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::invalid_quadrature_data}
);
continue;
}
if (!std::isfinite(density)) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::invalid_quadrature_data}
);
continue;
}
if (!std::isfinite(enthalpy)) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::equation_of_state,
.equationOfStateError = eos::EvaluationErrorCode::nonfinite_input}
);
continue;
}
if (enthalpy < 0.0) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::equation_of_state,
.equationOfStateError = eos::EvaluationErrorCode::outside_domain}
);
continue;
}
const dimensions::SpecificEnthalpyValue specificEnthalpy{enthalpy};
const double eosDensity =
eos::evaluate<eos::quantity::Density>(m_equationOfState, specificEnthalpy).value();
@@ -425,17 +639,34 @@ namespace mean_field::operators {
)
.value();
MFEM_VERIFY(
std::isfinite(quadratureWeight) && quadratureWeight > 0.0 && std::isfinite(eosDensity) &&
std::isfinite(enthalpyDerivative),
"PreparedBarotropicClosureOperator encountered invalid quadrature data."
);
if (!std::isfinite(eosDensity) || !std::isfinite(enthalpyDerivative)) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::equation_of_state,
.equationOfStateError = eos::EvaluationErrorCode::nonfinite_result}
);
continue;
}
const double weightedResidual = quadratureWeight * (density - eosDensity);
const double weightedEnthalpyDerivative = quadratureWeight * enthalpyDerivative;
if (!std::isfinite(weightedResidual) || !std::isfinite(weightedEnthalpyDerivative)) {
retain_higher_priority_rejection(
localRejection, {.reason = BarotropicClosurePreparationRejectionReason::invalid_quadrature_data}
);
continue;
}
data.quadratureWeights(quadraturePoint) = quadratureWeight;
data.weightedResidual(quadraturePoint) = quadratureWeight * (density - eosDensity);
data.weightedEnthalpyDerivative(quadraturePoint) = quadratureWeight * enthalpyDerivative;
data.weightedResidual(quadraturePoint) = weightedResidual;
data.weightedEnthalpyDerivative(quadraturePoint) = weightedEnthalpyDerivative;
}
}
m_elements.resize(preparedElementCount);
if (auto globalRejection = synchronize_rejection(localRejection, m_fem.densityFes->GetComm());
globalRejection.has_value()) {
return std::unexpected(*globalRejection);
}
MFEM_VERIFY(!m_elements.empty(), "PreparedBarotropicClosureOperator found no elements in Density::Support.");
@@ -455,7 +686,7 @@ namespace mean_field::operators {
for (const ElementPAData &data : m_elements) {
elementResidual.SetSize(data.densityDofs.Size());
data.densityBasis.MultTranspose(data.weightedResidual, elementResidual);
data.densityBasis->GetValues().MultTranspose(data.weightedResidual, elementResidual);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->TransformDual(elementResidual);
@@ -485,7 +716,7 @@ namespace mean_field::operators {
elementDiagonal = 0.0;
for (int trialDof = 0; trialDof < data.densityDofs.Size(); ++trialDof) {
for (int quadraturePoint = 0; quadraturePoint < data.quadratureWeights.Size(); ++quadraturePoint) {
const double basis = data.densityBasis(quadraturePoint, trialDof);
const double basis = data.densityBasis->GetValues()(quadraturePoint, trialDof);
elementDiagonal(trialDof) += data.quadratureWeights(quadraturePoint) * basis * basis;
}
}
@@ -608,8 +839,8 @@ namespace mean_field::operators {
quadratureEnthalpyVariation.SetSize(data.quadratureWeights.Size());
quadratureAction.SetSize(data.quadratureWeights.Size());
data.densityBasis.Mult(elementDensityVariation, quadratureDensityVariation);
data.enthalpyBasis.Mult(elementEnthalpyVariation, quadratureEnthalpyVariation);
data.densityBasis->GetValues().Mult(elementDensityVariation, quadratureDensityVariation);
data.enthalpyBasis->GetValues().Mult(elementEnthalpyVariation, quadratureEnthalpyVariation);
for (int quadraturePoint = 0; quadraturePoint < quadratureAction.Size(); ++quadraturePoint) {
quadratureAction(quadraturePoint) =
@@ -618,7 +849,7 @@ namespace mean_field::operators {
}
elementAction.SetSize(data.densityDofs.Size());
data.densityBasis.MultTranspose(quadratureAction, elementAction);
data.densityBasis->GetValues().MultTranspose(quadratureAction, elementAction);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->TransformDual(elementAction);
@@ -672,14 +903,14 @@ namespace mean_field::operators {
"Prepared barotropic closure inverse-Jacobian data has an incompatible size."
);
m_referenceDShape.SetSize(displacementElement.GetDof(), dimension);
m_referenceDisplacementJacobian.SetSize(dimension, dimension);
m_quadratureDisplacementAction.SetSize(integrationRule.GetNPoints());
for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = integrationRule.IntPoint(quadraturePoint);
displacementElement.CalcDShape(integrationPoint, m_referenceDShape);
mfem::MultAtB(directionDofs, m_referenceDShape, m_referenceDisplacementJacobian);
mfem::MultAtB(
directionDofs, data.displacementBasis->GetGradients(quadraturePoint),
m_referenceDisplacementJacobian
);
double logarithmicJacobianVariation{0.0};
for (int row = 0; row < dimension; ++row) {
@@ -699,7 +930,7 @@ namespace mean_field::operators {
}
m_elementDisplacementAction.SetSize(data.densityDofs.Size());
data.densityBasis.MultTranspose(m_quadratureDisplacementAction, m_elementDisplacementAction);
data.densityBasis->GetValues().MultTranspose(m_quadratureDisplacementAction, m_elementDisplacementAction);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->TransformDual(m_elementDisplacementAction);

View File

@@ -1,6 +1,13 @@
module;
#include <cmath>
#include <expected>
#include <mfem.hpp>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mpi.h>
module mean_field;
@@ -8,6 +15,75 @@ 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) {
@@ -118,6 +194,21 @@ namespace mean_field::operators {
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);
@@ -161,20 +252,47 @@ namespace mean_field::operators {
PreparedDisplacementResidualReport report;
report.pressure = m_pressureOperator.Prepare(
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();
report.gravity = m_gravityOperator.Prepare();
auto gravityResult = m_gravityOperator.TryPrepare();
if (!gravityResult.has_value()) {
return std::unexpected(gravity_rejection(gravityResult.error()));
}
report.gravity = std::move(gravityResult).value();
report.rotation = m_rotationalOperator.Prepare(
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()) {
AssembleResidual();
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(
@@ -188,7 +306,7 @@ namespace mean_field::operators {
return report;
}
void PreparedDisplacementResidualOperator::AssembleResidual() {
std::optional<DisplacementResidualPreparationRejection> PreparedDisplacementResidualOperator::AssembleResidual() {
mfem::Vector pressureResidual;
mfem::Vector gravityResidual;
mfem::Vector rotationalResidual;
@@ -211,7 +329,10 @@ namespace mean_field::operators {
"different sizes."
);
++m_residualPreparationCount;
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 {

View File

@@ -1,14 +1,24 @@
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,
@@ -22,6 +32,69 @@ namespace {
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,
@@ -108,7 +181,10 @@ namespace mean_field::operators {
);
}
void PreparedGravityDisplacementForceOperator::PrepareElementData() {
std::expected<
void,
kernels::GravityDisplacementForceRejection>
PreparedGravityDisplacementForceOperator::TryPrepareElementData() {
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
@@ -174,6 +250,17 @@ namespace mean_field::operators {
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);
@@ -199,18 +286,25 @@ namespace mean_field::operators {
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(
status == mapping::MappingStatus::valid && !mappingContext.mapping.compactified,
"Prepared gravity force encountered an invalid stellar mapping."
!mappingContext.mapping.compactified,
"Prepared gravity force encountered compactification on a stellar element."
);
densityElement.CalcShape(integrationPoint, densityShape);
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) {
@@ -218,13 +312,46 @@ namespace mean_field::operators {
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."
@@ -236,19 +363,45 @@ namespace mean_field::operators {
return {};
}
kernels::apply_gravity_displacement_force_residual(
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);
PrepareElementData();
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 {.preparedResidual = true};
return PreparedGravityDisplacementForceReport{.preparedResidual = true};
}
void PreparedGravityDisplacementForceOperator::BuildResidual(mfem::Vector &residual) const {
@@ -331,6 +484,10 @@ namespace mean_field::operators {
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);
@@ -359,13 +516,14 @@ namespace mean_field::operators {
m_densityShape.SetSize(densityElement.GetDof());
m_displacementShape.SetSize(scalarDisplacementDofCount);
m_gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension);
m_referenceDisplacementDShape.SetSize(scalarDisplacementDofCount, dimension);
m_referenceDisplacementJacobian.SetSize(dimension, dimension);
m_displacementJacobianVariation.SetSize(dimension, dimension);
m_mappingJacobian.SetSize(dimension, dimension);
m_inverseMeshJacobian.SetSize(dimension, dimension);
m_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);
@@ -375,16 +533,28 @@ namespace mean_field::operators {
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);
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);
}
transformation->SetIntPoint(&integrationPoint);
gravityGradientElement.CalcVShape(*transformation, m_gravityGradientShape);
m_gravityGradientShape.MultTranspose(
m_elementGravityGradientVariation, m_gravityVariationReferenceValue
);
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);
@@ -392,8 +562,14 @@ namespace mean_field::operators {
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);

View File

@@ -1,10 +1,16 @@
module;
#include "profile.h"
#include <array>
#include <cmath>
#include <cstdint>
#include <expected>
#include <memory>
#include <mfem.hpp>
#include <numbers>
#include <stdexcept>
#include <string>
#include <mpi.h>
module mean_field;
import :operators.prepared_gravity_source;
@@ -12,6 +18,54 @@ import :operators.prepared_gravity_source;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[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]] mean_field::operators::GravitySourcePreparationResult synchronize_preparation_failure(
const mean_field::mapping::MappingStatus localMappingStatus,
const bool localNonFiniteArithmetic,
const MPI_Comm communicator
) {
std::array<int, 3> localFailures{0, 0, localNonFiniteArithmetic ? 1 : 0};
if (localMappingStatus != mean_field::mapping::MappingStatus::valid) {
const int encodedStatus = static_cast<int>(localMappingStatus) + 1;
localFailures[is_candidate_mapping_failure(localMappingStatus) ? 0 : 1] = encodedStatus;
}
std::array<int, 3> globalFailures{};
if (MPI_Allreduce(
localFailures.data(), globalFailures.data(), static_cast<int>(localFailures.size()), MPI_INT, MPI_MAX,
communicator
) != MPI_SUCCESS) {
throw std::runtime_error("PreparedMappedGravitySourceOperator could not synchronize candidate validity.");
}
if (globalFailures[1] != 0) {
throw std::runtime_error(
"PreparedMappedGravitySourceOperator encountered a structural mapping failure with status " +
std::to_string(globalFailures[1] - 1) + "."
);
}
if (globalFailures[0] != 0) {
return std::unexpected(
mean_field::operators::GravitySourcePreparationRejection{
.reason = mean_field::operators::GravitySourcePreparationRejectionReason::invalid_mapping,
.mappingStatus = static_cast<mean_field::mapping::MappingStatus>(globalFailures[0] - 1)
}
);
}
if (globalFailures[2] != 0) {
return std::unexpected(
mean_field::operators::GravitySourcePreparationRejection{
.reason = mean_field::operators::GravitySourcePreparationRejectionReason::non_finite_arithmetic
}
);
}
return {};
}
int get_operator_height(const mean_field::fem::FEM &f) {
MFEM_VERIFY(
f.gravityPotentialFes != nullptr, "PreparedMappedGravitySourceOperator requires the "
@@ -128,62 +182,38 @@ namespace {
.displacement = *m_displacement_data, .compactification = *m_compactification_data
};
mean_field::mapping::VolumeMappingContext mapping_context;
const mean_field::mapping::MappingStatus status = m_domain_mapper.EvaluateVolume(
mapping_data, transformation, integration_point, m_workspace, mapping_context
mapping_data, transformation, integration_point, m_workspace, m_mapping_context
);
if (status != mean_field::mapping::MappingStatus::valid) {
const mfem::FiniteElement &displacement_element = *m_fem.displacementFes->GetFE(element_id);
const mfem::FiniteElement &compactification_element = *m_fem.compactificationFes->GetFE(element_id);
mfem::Vector displacement_shape(displacement_element.GetDof());
mfem::Vector compactification_shape(compactification_element.GetDof());
mfem::Vector reference_position(m_domain_mapper.GetDimension());
mfem::Vector displacement_value(m_domain_mapper.GetDimension());
displacement_element.CalcShape(integration_point, displacement_shape);
compactification_element.CalcShape(integration_point, compactification_shape);
transformation.Transform(integration_point, reference_position);
m_displacement_data->GetDofMatrix().MultTranspose(displacement_shape, displacement_value);
const double compactification_coordinate = m_compactification_data->GetDofs() * compactification_shape;
MFEM_ABORT(
"Stateless domain mapping failed while preparing the "
"gravity "
"source operator."
<< "\nMapping status = " << static_cast<int>(status) << "\nElement ID = " << element_id
<< "\nElement attribute = " << transformation.Attribute
<< "\nIntegration-point index = " << integration_point.index << "\nIntegration point = <"
<< integration_point.x << ", " << integration_point.y << ", " << integration_point.z << ">"
<< "\nReference position = <" << reference_position(0) << ", " << reference_position(1) << ", "
<< reference_position(2) << ">"
<< "\nReference radius = " << reference_position.Norml2() << "\nDisplacement value = <"
<< displacement_value(0) << ", " << displacement_value(1) << ", " << displacement_value(2) << ">"
<< "\nDisplacement magnitude = " << displacement_value.Norml2()
<< "\nCompactification coordinate = " << compactification_coordinate
<< "\nDisplacement ordering = " << static_cast<int>(m_fem.displacementFes->GetOrdering())
);
m_mappingFailure = status;
return 0.0;
}
const double mapping_determinant = mapping_context.mapping.mapping_determinant;
MFEM_VERIFY(
std::isfinite(mapping_determinant) && mapping_determinant > 0.0,
"Prepared gravity source operator encountered a non-positive "
"or "
"non-finite mapping determinant."
);
const double mapping_determinant = m_mapping_context.mapping.mapping_determinant;
m_inverse_element_jacobian = mapping_context.quadrature.J_inv;
m_inverse_element_jacobian = m_mapping_context.quadrature.J_inv;
return 4.0 * std::numbers::pi * mean_field::utils::G * mapping_determinant;
const double value = 4.0 * std::numbers::pi * mean_field::utils::G * mapping_determinant;
if (!std::isfinite(value)) {
m_nonFiniteArithmetic = true;
return 0.0;
}
return value;
}
[[nodiscard]] const mfem::DenseMatrix &GetInverseElementJacobian() const noexcept {
return m_inverse_element_jacobian;
}
[[nodiscard]] mean_field::mapping::MappingStatus GetMappingFailure() const noexcept {
return m_mappingFailure;
}
[[nodiscard]] bool HasNonFiniteArithmetic() const noexcept {
return m_nonFiniteArithmetic;
}
private:
void LoadElement(const int element_id) {
if (element_id == m_cached_element_id) {
@@ -237,8 +267,11 @@ namespace {
std::unique_ptr<mean_field::mapping::ElementCompactificationData> m_compactification_data;
mean_field::mapping::DomainMapper::Workspace m_workspace;
mean_field::mapping::VolumeMappingContext m_mapping_context;
mfem::DenseMatrix m_inverse_element_jacobian;
int m_cached_element_id{-1};
mean_field::mapping::MappingStatus m_mappingFailure{mean_field::mapping::MappingStatus::valid};
bool m_nonFiniteArithmetic{false};
};
} // namespace
@@ -306,15 +339,32 @@ namespace mean_field::operators {
void PreparedMappedGravitySourceOperator::Prepare(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedGravitySourceOperator::Prepare linearization", 0);
PrepareImpl(displacement, PreparationMode::linearization);
auto result = TryPrepareImpl(displacement, PreparationMode::linearization);
if (!result.has_value()) {
throwGravitySourcePreparationRejection(result.error());
}
}
void PreparedMappedGravitySourceOperator::PreparePrimal(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedGravitySourceOperator::Prepare primal", 0);
PrepareImpl(displacement, PreparationMode::primal);
auto result = TryPrepareImpl(displacement, PreparationMode::primal);
if (!result.has_value()) {
throwGravitySourcePreparationRejection(result.error());
}
}
void PreparedMappedGravitySourceOperator::PrepareImpl(
GravitySourcePreparationResult PreparedMappedGravitySourceOperator::TryPrepare(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedGravitySourceOperator::TryPrepare linearization", 0);
return TryPrepareImpl(displacement, PreparationMode::linearization);
}
GravitySourcePreparationResult
PreparedMappedGravitySourceOperator::TryPreparePrimal(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedGravitySourceOperator::TryPrepare primal", 0);
return TryPrepareImpl(displacement, PreparationMode::primal);
}
GravitySourcePreparationResult PreparedMappedGravitySourceOperator::TryPrepareImpl(
const mfem::Vector &displacement,
const PreparationMode mode
) {
@@ -325,21 +375,29 @@ namespace mean_field::operators {
"with the wrong size."
);
bool localNonFiniteInput = false;
for (int i = 0; i < displacement.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(displacement(i)), "PreparedMappedGravitySourceOperator received a non-finite "
"displacement value."
localNonFiniteInput = localNonFiniteInput || !std::isfinite(displacement(i));
}
if (auto inputResult = synchronize_preparation_failure(
localNonFiniteInput ? mapping::MappingStatus::non_finite_input : mapping::MappingStatus::valid, false,
m_fem.mesh->GetComm()
);
!inputResult.has_value()) {
m_is_prepared = false;
m_has_variation_data = false;
return inputResult;
}
m_is_prepared = false;
m_has_variation_data = false;
m_displacement_true.SetSize(m_displacement_map.full_size());
m_displacement_map.scatter(displacement, m_displacement_true);
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
std::size_t prepared_element_count{0};
FrozenMappedGravitySourceCoefficient source_coefficient(m_fem, m_domain_mapper, m_displacement_true);
bool localNonFiniteQuadrature = false;
for (int element_id = 0; element_id < m_fem.mesh->GetNE(); ++element_id) {
const int attribute = m_fem.mesh->GetAttribute(element_id);
@@ -348,8 +406,10 @@ namespace mean_field::operators {
continue;
}
m_elements.emplace_back();
ElementPAData &data = m_elements.back();
if (prepared_element_count == m_elements.size()) {
m_elements.emplace_back();
}
ElementPAData &data = m_elements[prepared_element_count++];
data.element_id = element_id;
@@ -379,41 +439,68 @@ namespace mean_field::operators {
const int potential_dof_count = potential_element.GetDof();
data.density_basis.SetSize(quadrature_point_count, density_dof_count);
data.potential_basis.SetSize(quadrature_point_count, potential_dof_count);
if (density_element.GetMapType() == mfem::FiniteElement::VALUE) {
data.density_reference = m_fem.GetReferenceTables().GetScalarTable(density_element, integration_rule);
data.density_basis.SetSize(0, 0);
} else {
data.density_reference.reset();
data.density_basis.SetSize(quadrature_point_count, density_dof_count);
}
if (potential_element.GetMapType() == mfem::FiniteElement::VALUE) {
data.potential_reference =
m_fem.GetReferenceTables().GetScalarTable(potential_element, integration_rule);
data.potential_basis.SetSize(0, 0);
} else {
data.potential_reference.reset();
data.potential_basis.SetSize(quadrature_point_count, potential_dof_count);
}
const int dimension = m_fem.mesh->Dimension();
if (mode == PreparationMode::linearization) {
data.inverse_element_jacobians.SetSize(quadrature_point_count, dimension * dimension);
data.displacement_reference = m_fem.GetReferenceTables().GetScalarTable(
*m_fem.displacementFes->GetFE(element_id), integration_rule
);
}
data.quadrature_data.SetSize(quadrature_point_count);
mfem::Vector density_shape(density_dof_count);
mfem::Vector potential_shape(potential_dof_count);
mfem::Vector density_shape;
mfem::Vector potential_shape;
if (!data.density_reference) {
density_shape.SetSize(density_dof_count);
}
if (!data.potential_reference) {
potential_shape.SetSize(potential_dof_count);
}
for (int quadrature_point = 0; quadrature_point < quadrature_point_count; ++quadrature_point) {
const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(quadrature_point);
transformation.SetIntPoint(&integration_point);
// CalcPhysShape matches the scalar mixed-mass discretization,
// including the finite-element map type.
density_element.CalcPhysShape(transformation, density_shape);
potential_element.CalcPhysShape(transformation, potential_shape);
for (int i = 0; i < density_dof_count; ++i) {
data.density_basis(quadrature_point, i) = density_shape(i);
// VALUE maps use the shared reference basis. Preserve the
// physical-shape evaluation for every other scalar map type.
if (!data.density_reference) {
density_element.CalcPhysShape(transformation, density_shape);
for (int i = 0; i < density_dof_count; ++i) {
data.density_basis(quadrature_point, i) = density_shape(i);
}
}
for (int i = 0; i < potential_dof_count; ++i) {
data.potential_basis(quadrature_point, i) = potential_shape(i);
if (!data.potential_reference) {
potential_element.CalcPhysShape(transformation, potential_shape);
for (int i = 0; i < potential_dof_count; ++i) {
data.potential_basis(quadrature_point, i) = potential_shape(i);
}
}
const double coefficient_value = source_coefficient.Eval(transformation, integration_point);
if (source_coefficient.GetMappingFailure() != mapping::MappingStatus::valid ||
source_coefficient.HasNonFiniteArithmetic()) {
break;
}
if (mode == PreparationMode::linearization) {
const mfem::DenseMatrix &inverse_element_jacobian = source_coefficient.GetInverseElementJacobian();
for (int row = 0; row < dimension; ++row) {
@@ -428,15 +515,27 @@ namespace mean_field::operators {
const double quadrature_value = integration_point.weight * transformation.Weight() * coefficient_value;
MFEM_VERIFY(
std::isfinite(quadrature_value) && quadrature_value > 0.0,
"Prepared gravity source operator encountered invalid "
"quadrature data on element "
<< element_id << ", quadrature point " << quadrature_point << "."
);
if (!std::isfinite(quadrature_value) || quadrature_value <= 0.0) {
localNonFiniteQuadrature = true;
break;
}
data.quadrature_data(quadrature_point) = quadrature_value;
}
if (source_coefficient.GetMappingFailure() != mapping::MappingStatus::valid ||
source_coefficient.HasNonFiniteArithmetic() || localNonFiniteQuadrature) {
break;
}
}
m_elements.resize(prepared_element_count);
const bool localNonFiniteArithmetic = source_coefficient.HasNonFiniteArithmetic() || localNonFiniteQuadrature;
auto preparationResult = synchronize_preparation_failure(
source_coefficient.GetMappingFailure(), localNonFiniteArithmetic, m_fem.mesh->GetComm()
);
if (!preparationResult.has_value()) {
return preparationResult;
}
MFEM_VERIFY(!m_elements.empty(), "PreparedMappedGravitySourceOperator found no stellar elements.");
@@ -444,6 +543,7 @@ namespace mean_field::operators {
m_is_prepared = true;
m_has_variation_data = mode == PreparationMode::linearization;
++m_preparation_count;
return {};
}
void PreparedMappedGravitySourceOperator::Mult(
const mfem::Vector &density,
@@ -479,7 +579,7 @@ namespace mean_field::operators {
m_quadrature_action.SetSize(data.quadrature_data.Size());
// B_density * x_e
data.density_basis.Mult(m_element_input, m_quadrature_action);
data.GetDensityBasis().Mult(m_element_input, m_quadrature_action);
// D * B_density * x_e
for (int q = 0; q < m_quadrature_action.Size(); ++q) {
@@ -489,7 +589,7 @@ namespace mean_field::operators {
m_element_action.SetSize(data.potential_dofs.Size());
// B_potential^T * D * B_density * x_e
data.potential_basis.MultTranspose(m_quadrature_action, m_element_action);
data.GetPotentialBasis().MultTranspose(m_quadrature_action, m_element_action);
if (data.potential_dof_transformation != nullptr) {
data.potential_dof_transformation->TransformDual(m_element_action);
@@ -565,15 +665,15 @@ namespace mean_field::operators {
"Prepared gravity source inverse-Jacobian data has an incompatible size."
);
m_reference_displacement_dshape.SetSize(displacement_element.GetDof(), dimension);
m_reference_displacement_jacobian.SetSize(dimension, dimension);
m_quadrature_variation_action.SetSize(data.integration_rule->GetNPoints());
data.density_basis.Mult(m_element_density, m_quadrature_variation_action);
data.GetDensityBasis().Mult(m_element_density, m_quadrature_variation_action);
for (int quadrature_point = 0; quadrature_point < data.integration_rule->GetNPoints(); ++quadrature_point) {
const mfem::IntegrationPoint &integration_point = data.integration_rule->IntPoint(quadrature_point);
displacement_element.CalcDShape(integration_point, m_reference_displacement_dshape);
mfem::MultAtB(direction_dofs, m_reference_displacement_dshape, m_reference_displacement_jacobian);
mfem::MultAtB(
direction_dofs, data.displacement_reference->GetGradients(quadrature_point),
m_reference_displacement_jacobian
);
double logarithmic_jacobian_variation{0.0};
for (int row = 0; row < dimension; ++row) {
@@ -593,7 +693,7 @@ namespace mean_field::operators {
}
m_element_variation_action.SetSize(data.potential_dofs.Size());
data.potential_basis.MultTranspose(m_quadrature_variation_action, m_element_variation_action);
data.GetPotentialBasis().MultTranspose(m_quadrature_variation_action, m_element_variation_action);
if (data.potential_dof_transformation != nullptr) {
data.potential_dof_transformation->TransformDual(m_element_variation_action);
@@ -638,7 +738,7 @@ namespace mean_field::operators {
m_quadrature_action.SetSize(data.quadrature_data.Size());
data.potential_basis.Mult(m_element_input, m_quadrature_action);
data.GetPotentialBasis().Mult(m_element_input, m_quadrature_action);
for (int q = 0; q < m_quadrature_action.Size(); ++q) {
m_quadrature_action(q) *= data.quadrature_data(q);
@@ -646,7 +746,7 @@ namespace mean_field::operators {
m_element_action.SetSize(data.density_dofs.Size());
data.density_basis.MultTranspose(m_quadrature_action, m_element_action);
data.GetDensityBasis().MultTranspose(m_quadrature_action, m_element_action);
if (data.density_dof_transformation != nullptr) {
data.density_dof_transformation->TransformDual(m_element_action);

View File

@@ -1,16 +1,102 @@
module;
#include "profile.h"
#include <array>
#include <cmath>
#include <cstdint>
#include <expected>
#include <memory>
#include <mfem.hpp>
#include <optional>
#include <stdexcept>
#include <string>
#include <mpi.h>
module mean_field;
import :fem.reference_tables;
import :operators.prepared_hdiv_mass;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
[[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]] mean_field::operators::HDivMassPreparationResult synchronize_preparation_failure(
const mean_field::mapping::MappingStatus localMappingStatus,
const bool localNonFiniteArithmetic,
const MPI_Comm communicator
) {
std::array<int, 3> localFailures{0, 0, localNonFiniteArithmetic ? 1 : 0};
if (localMappingStatus != mean_field::mapping::MappingStatus::valid) {
const int encodedStatus = static_cast<int>(localMappingStatus) + 1;
localFailures[is_candidate_mapping_failure(localMappingStatus) ? 0 : 1] = encodedStatus;
}
std::array<int, 3> globalFailures{};
if (MPI_Allreduce(
localFailures.data(), globalFailures.data(), static_cast<int>(localFailures.size()), MPI_INT, MPI_MAX,
communicator
) != MPI_SUCCESS) {
throw std::runtime_error("PreparedMappedHDivMassOperator could not synchronize candidate validity.");
}
if (globalFailures[1] != 0) {
throw std::runtime_error(
"PreparedMappedHDivMassOperator encountered a structural mapping failure with status " +
std::to_string(globalFailures[1] - 1) + "."
);
}
if (globalFailures[0] != 0) {
return std::unexpected(
mean_field::operators::HDivMassPreparationRejection{
.reason = mean_field::operators::HDivMassPreparationRejectionReason::invalid_mapping,
.mappingStatus = static_cast<mean_field::mapping::MappingStatus>(globalFailures[0] - 1)
}
);
}
if (globalFailures[2] != 0) {
return std::unexpected(
mean_field::operators::HDivMassPreparationRejection{
.reason = mean_field::operators::HDivMassPreparationRejectionReason::non_finite_arithmetic
}
);
}
return {};
}
[[nodiscard]] mean_field::mapping::MappingStatus higher_priority_mapping_status(
const mean_field::mapping::MappingStatus left,
const mean_field::mapping::MappingStatus right
) noexcept {
if (left == mean_field::mapping::MappingStatus::valid) {
return right;
}
if (right == mean_field::mapping::MappingStatus::valid) {
return left;
}
const bool leftIsCandidate = is_candidate_mapping_failure(left);
const bool rightIsCandidate = is_candidate_mapping_failure(right);
if (leftIsCandidate != rightIsCandidate) {
return leftIsCandidate ? right : left;
}
return static_cast<int>(right) > static_cast<int>(left) ? right : left;
}
[[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;
}
int get_operator_size(const mean_field::fem::FEM &f) {
MFEM_VERIFY(
f.gravityFluxFes != nullptr, "PreparedMappedHDivMassOperator requires the "
@@ -270,27 +356,30 @@ namespace {
mapping_data, transformation, integration_point, m_workspace, mapping_context
);
MFEM_VERIFY(
status == mean_field::mapping::MappingStatus::valid,
"Stateless domain mapping failed while preparing the H(div) "
"mass "
"operator. Mapping status = "
<< static_cast<int>(status) << ", element ID = " << element_id
<< ", element attribute = " << transformation.Attribute
<< ", coefficient domain = " << (m_elevates_vacuum ? "vacuum" : "stellar")
);
if (status != mean_field::mapping::MappingStatus::valid) {
m_mappingFailure = higher_priority_mapping_status(m_mappingFailure, status);
mass_tensor.SetSize(m_domain_mapper.GetDimension());
mass_tensor = 0.0;
return;
}
const mfem::DenseMatrix &mapping_jacobian = mapping_context.mapping.mapping_jacobian;
const double mapping_determinant = mapping_context.mapping.mapping_determinant;
MFEM_VERIFY(
std::isfinite(mapping_determinant) && mapping_determinant > 0.0,
"Prepared H(div) mass operator encountered a non-positive or "
"non-finite mapping determinant."
);
mfem::MultAtB(mapping_jacobian, mapping_jacobian, mass_tensor);
mass_tensor *= 1.0 / mapping_determinant;
if (!matrix_is_finite(mass_tensor)) {
m_nonFiniteArithmetic = true;
mass_tensor = 0.0;
}
}
[[nodiscard]] mean_field::mapping::MappingStatus GetMappingFailure() const noexcept {
return m_mappingFailure;
}
[[nodiscard]] bool HasNonFiniteArithmetic() const noexcept {
return m_nonFiniteArithmetic;
}
private:
@@ -348,6 +437,8 @@ namespace {
mean_field::mapping::DomainMapper::Workspace m_workspace;
int m_cached_element_id{-1};
bool m_elevates_vacuum;
mean_field::mapping::MappingStatus m_mappingFailure{mean_field::mapping::MappingStatus::valid};
bool m_nonFiniteArithmetic{false};
};
} // namespace
@@ -417,7 +508,7 @@ namespace mean_field::operators {
validate_uniform_domain_discretization(f, m_vacuum_marker, vacuum_element_id);
}
void PreparedMappedHDivMassOperator::PrepareVariationData() {
mapping::MappingStatus PreparedMappedHDivMassOperator::PrepareVariationData() {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedHDivMassOperator::PrepareVariationData", 0);
m_variationElements.clear();
@@ -462,6 +553,15 @@ namespace mean_field::operators {
);
data.integrationRule = &get_hdiv_mass_rule(m_fem, m_domain_mapper, gravityGradientElement, *transformation);
const int dimension = m_domain_mapper.GetDimension();
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);
data.referenceWeights.SetSize(data.integrationRule->GetNPoints());
}
data.frozenMappingData.SetSize(
data.integrationRule->GetNPoints(), frozen_mapping_width(m_domain_mapper.GetDimension())
);
@@ -480,28 +580,61 @@ namespace mean_field::operators {
const mapping::MappingStatus status = m_domain_mapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, m_variationWorkspace, mappingContext
);
MFEM_VERIFY(
status == mapping::MappingStatus::valid,
"Prepared H(div) variation data encountered an invalid mapping. Element: "
<< elementId << ", quadrature point: " << quadraturePoint
<< ", status: " << static_cast<int>(status)
);
if (status != mapping::MappingStatus::valid) {
return status;
}
freeze_mapping_context(mappingContext, quadraturePoint, data.frozenMappingData);
if (data.gravityReferenceTable != nullptr) {
// CalcVShape_RT = reference_shape * J_mesh^T / Weight.
// Cache only this small factor, never the mapped basis.
const double meshWeight = transformation->Weight();
const mfem::DenseMatrix &meshJacobian = transformation->Jacobian();
const double inverseMeshWeight = 1.0 / meshWeight;
data.referenceWeights(quadraturePoint) = integrationPoint.weight * meshWeight;
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
const double entry = inverseMeshWeight * meshJacobian(row, column);
if (!std::isfinite(entry))
return mapping::MappingStatus::non_finite_result;
data.meshPiolaJacobians(quadraturePoint, row * dimension + column) = entry;
}
}
if (!std::isfinite(data.referenceWeights(quadraturePoint))) {
return mapping::MappingStatus::non_finite_result;
}
}
}
}
return mapping::MappingStatus::valid;
}
void PreparedMappedHDivMassOperator::Prepare(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedHDivMassOperator::Prepare linearization", 0);
PrepareImpl(displacement, PreparationMode::linearization);
auto result = TryPrepareImpl(displacement, PreparationMode::linearization);
if (!result.has_value()) {
throwHDivMassPreparationRejection(result.error());
}
}
void PreparedMappedHDivMassOperator::PreparePrimal(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedHDivMassOperator::Prepare primal", 0);
PrepareImpl(displacement, PreparationMode::primal);
auto result = TryPrepareImpl(displacement, PreparationMode::primal);
if (!result.has_value()) {
throwHDivMassPreparationRejection(result.error());
}
}
void PreparedMappedHDivMassOperator::PrepareImpl(
HDivMassPreparationResult PreparedMappedHDivMassOperator::TryPrepare(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedHDivMassOperator::TryPrepare linearization", 0);
return TryPrepareImpl(displacement, PreparationMode::linearization);
}
HDivMassPreparationResult PreparedMappedHDivMassOperator::TryPreparePrimal(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedHDivMassOperator::TryPrepare primal", 0);
return TryPrepareImpl(displacement, PreparationMode::primal);
}
HDivMassPreparationResult PreparedMappedHDivMassOperator::TryPrepareImpl(
const mfem::Vector &displacement,
const PreparationMode mode
) {
@@ -512,12 +645,18 @@ namespace mean_field::operators {
"the wrong size."
);
bool localNonFiniteInput = false;
for (int i = 0; i < displacement.Size(); ++i) {
MFEM_VERIFY(
std::isfinite(displacement(i)), "PreparedMappedHDivMassOperator received a non-finite "
"displacement "
"value."
localNonFiniteInput = localNonFiniteInput || !std::isfinite(displacement(i));
}
if (auto inputResult = synchronize_preparation_failure(
localNonFiniteInput ? mapping::MappingStatus::non_finite_input : mapping::MappingStatus::valid, false,
m_fem.mesh->GetComm()
);
!inputResult.has_value()) {
m_is_prepared = false;
m_has_variation_data = false;
return inputResult;
}
m_is_prepared = false;
@@ -540,13 +679,17 @@ namespace mean_field::operators {
m_stellar_mass_coefficient.reset();
m_vacuum_mass_coefficient.reset();
m_stellar_mass_coefficient =
auto stellarMassCoefficient =
std::make_unique<FrozenMappedHDivMassCoefficient>(m_fem, m_domain_mapper, m_displacement_true, false);
m_vacuum_mass_coefficient =
auto vacuumMassCoefficient =
std::make_unique<FrozenMappedHDivMassCoefficient>(m_fem, m_domain_mapper, m_displacement_true, true);
auto *stellarMassCoefficientView = stellarMassCoefficient.get();
auto *vacuumMassCoefficientView = vacuumMassCoefficient.get();
m_stellar_mass_coefficient = std::move(stellarMassCoefficient);
m_vacuum_mass_coefficient = std::move(vacuumMassCoefficient);
m_stellar_mass_form = std::make_unique<mfem::ParBilinearForm>(m_fem.gravityFluxFes.get());
m_vacuum_mass_form = std::make_unique<mfem::ParBilinearForm>(m_fem.gravityFluxFes.get());
m_stellar_mass_form = std::make_unique<mfem::ParBilinearForm>(m_fem.gravityFluxFes.get());
m_vacuum_mass_form = std::make_unique<mfem::ParBilinearForm>(m_fem.gravityFluxFes.get());
m_stellar_mass_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
m_vacuum_mass_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
@@ -568,15 +711,30 @@ namespace mean_field::operators {
m_stellar_mass_form->Assemble();
m_vacuum_mass_form->Assemble();
mapping::MappingStatus localMappingFailure = higher_priority_mapping_status(
stellarMassCoefficientView->GetMappingFailure(), vacuumMassCoefficientView->GetMappingFailure()
);
bool localNonFiniteArithmetic =
stellarMassCoefficientView->HasNonFiniteArithmetic() || vacuumMassCoefficientView->HasNonFiniteArithmetic();
if (mode == PreparationMode::linearization) {
PrepareVariationData();
m_has_variation_data = true;
if (localMappingFailure == mapping::MappingStatus::valid && !localNonFiniteArithmetic) {
localMappingFailure = PrepareVariationData();
}
} else {
m_variationElements.clear();
}
m_is_prepared = true;
auto preparationResult =
synchronize_preparation_failure(localMappingFailure, localNonFiniteArithmetic, m_fem.mesh->GetComm());
if (!preparationResult.has_value()) {
return preparationResult;
}
m_has_variation_data = mode == PreparationMode::linearization;
m_is_prepared = true;
++m_preparation_count;
return {};
}
void PreparedMappedHDivMassOperator::Mult(
@@ -702,7 +860,10 @@ namespace mean_field::operators {
m_elementVariationAction.SetSize(gravityGradientElement.GetDof());
m_elementVariationAction = 0.0;
m_gravityGradientValue.SetSize(dimension);
m_gravityReferenceCellValue.SetSize(dimension);
m_referenceCellDual.SetSize(dimension);
m_massTensorVariationAction.SetSize(dimension);
m_meshPiolaJacobian.SetSize(dimension, dimension);
m_gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension);
m_massTensorVariation.SetSize(dimension, dimension);
@@ -725,12 +886,33 @@ namespace mean_field::operators {
m_baseMappingContext.mapping, m_mappingVariation.mapping, m_massTensorVariation
);
transformation->SetIntPoint(&integrationPoint);
gravityGradientElement.CalcVShape(*transformation, m_gravityGradientShape);
m_gravityGradientShape.MultTranspose(m_elementGravityGradient, m_gravityGradientValue);
m_massTensorVariation.Mult(m_gravityGradientValue, m_massTensorVariationAction);
const double referenceWeight = integrationPoint.weight * transformation->Weight();
m_gravityGradientShape.AddMult(m_massTensorVariationAction, m_elementVariationAction, referenceWeight);
if (data.gravityReferenceTable != nullptr) {
const mfem::DenseMatrix &referenceShape = data.gravityReferenceTable->GetValues(quadraturePoint);
referenceShape.MultTranspose(m_elementGravityGradient, m_gravityReferenceCellValue);
for (int row = 0; row < dimension; ++row) {
for (int column = 0; column < dimension; ++column) {
m_meshPiolaJacobian(row, column) =
data.meshPiolaJacobians(quadraturePoint, row * dimension + column);
}
}
m_meshPiolaJacobian.Mult(m_gravityReferenceCellValue, m_gravityGradientValue);
m_massTensorVariation.Mult(m_gravityGradientValue, m_massTensorVariationAction);
// Move the test-side Piola transform onto the three-vector
// dual before applying the reference basis transpose.
m_meshPiolaJacobian.MultTranspose(m_massTensorVariationAction, m_referenceCellDual);
referenceShape.AddMult(
m_referenceCellDual, m_elementVariationAction, data.referenceWeights(quadraturePoint)
);
} else {
transformation->SetIntPoint(&integrationPoint);
gravityGradientElement.CalcVShape(*transformation, m_gravityGradientShape);
m_gravityGradientShape.MultTranspose(m_elementGravityGradient, m_gravityGradientValue);
m_massTensorVariation.Mult(m_gravityGradientValue, m_massTensorVariationAction);
const double referenceWeight = integrationPoint.weight * transformation->Weight();
m_gravityGradientShape.AddMult(
m_massTensorVariationAction, m_elementVariationAction, referenceWeight
);
}
}
if (data.gravityGradientDofTransformation != nullptr) {

View File

@@ -4,6 +4,11 @@ module;
#include <array>
#include <cmath>
#include <cstdint>
#include <expected>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <mfem.hpp>
@@ -18,6 +23,82 @@ namespace {
return DomainSchema::template attribute_belongs_to<mean_field::utils::domain::Vacuum>(attribute);
}
[[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(
"PreparedHydrostaticEquilibriumOperator could not synchronize mapped-geometry validity."
);
}
if (globalFailures[1] != 0) {
throw std::runtime_error(
"PreparedHydrostaticEquilibriumOperator 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 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(
"PreparedHydrostaticEquilibriumOperator could not synchronize finite-arithmetic validity."
);
}
return globalStatus != 0;
}
[[nodiscard]] bool is_finite(const mfem::Vector &vector) {
for (int entry = 0; entry < vector.Size(); ++entry) {
if (!std::isfinite(vector(entry))) {
return false;
}
}
return true;
}
[[nodiscard]] bool is_finite(const mfem::DenseMatrix &matrix) {
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;
}
void true_to_local(
const mfem::ParFiniteElementSpace &finiteElementSpace,
const mfem::Vector &trueVector,
@@ -291,8 +372,21 @@ namespace mean_field::operators {
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
auto result = TryPrepare(state, dependencies, rotation);
if (!result.has_value()) {
throwHydrostaticEquilibriumPreparationRejection(result.error());
}
return std::move(result).value();
}
HydrostaticEquilibriumPreparationResult PreparedHydrostaticEquilibriumOperator::TryPrepare(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
const bool wasPrepared = m_isPrepared;
const bool rotationObjectChanged =
!m_context.IsPrepared() || dependencies.rotation != m_context.GetDependencies().rotation;
!wasPrepared || !m_context.IsPrepared() || dependencies.rotation != m_context.GetDependencies().rotation;
PreparedHydrostaticEquilibriumReport report;
@@ -310,25 +404,58 @@ namespace mean_field::operators {
m_isPrepared = false;
if (report.contextReport.preparedStaticDependencies) {
if (report.contextReport.preparedStaticDependencies || !wasPrepared) {
PrepareStaticPlan();
}
if (report.contextReport.preparedGeometryState) {
PrepareGeometry();
PrepareAlgebraicJacobianBlocks();
if (report.contextReport.preparedGeometryState || !wasPrepared) {
const auto mappingFailure = synchronize_mapping_failure(PrepareGeometry(), m_fem.mesh->GetComm());
if (mappingFailure.has_value()) {
const auto reason = *mappingFailure == mapping::MappingStatus::non_positive_determinant
? HydrostaticEquilibriumPreparationRejectionReason::inverted_geometry
: HydrostaticEquilibriumPreparationRejectionReason::non_finite_geometry;
return std::unexpected(
HydrostaticEquilibriumPreparationRejection{.reason = reason, .mappingStatus = *mappingFailure}
);
}
if (synchronize_non_finite_failure(PrepareAlgebraicJacobianBlocks(), m_fem.mesh->GetComm())) {
return std::unexpected(
HydrostaticEquilibriumPreparationRejection{
.reason = HydrostaticEquilibriumPreparationRejectionReason::non_finite_geometry,
.mappingStatus = mapping::MappingStatus::non_finite_result
}
);
}
report.preparedAlgebraicJacobianBlocks = true;
}
if (report.contextReport.preparedRotationDependencies) {
PrepareRotation();
if (report.contextReport.preparedRotationDependencies || !wasPrepared) {
if (synchronize_non_finite_failure(PrepareRotation(), m_fem.mesh->GetComm())) {
return std::unexpected(
HydrostaticEquilibriumPreparationRejection{
.reason = HydrostaticEquilibriumPreparationRejectionReason::non_finite_residual
}
);
}
}
if (report.contextReport.preparedBaseState) {
PrepareBaseState();
if (report.contextReport.preparedBaseState || !wasPrepared) {
if (synchronize_non_finite_failure(PrepareBaseState(), m_fem.mesh->GetComm())) {
return std::unexpected(
HydrostaticEquilibriumPreparationRejection{
.reason = HydrostaticEquilibriumPreparationRejectionReason::non_finite_residual
}
);
}
FinalizeDisplacementJacobianPreparation();
AssembleCachedResidual();
++m_residualPreparationCount;
if (synchronize_non_finite_failure(AssembleCachedResidual(), m_fem.mesh->GetComm())) {
return std::unexpected(
HydrostaticEquilibriumPreparationRejection{
.reason = HydrostaticEquilibriumPreparationRejectionReason::non_finite_residual
}
);
}
report.preparedDisplacementJacobianData = true;
report.preparedResidual = true;
}
@@ -343,6 +470,16 @@ namespace mean_field::operators {
"The prepared hydrostatic residual has the wrong supported size."
);
if (report.preparedAlgebraicJacobianBlocks) {
++m_algebraicJacobianStatistics.preparations;
}
if (report.preparedDisplacementJacobianData) {
++m_displacementJacobianStatistics.preparations;
}
if (report.preparedResidual) {
++m_residualPreparationCount;
}
m_isPrepared = true;
return report;
}
@@ -351,9 +488,6 @@ namespace mean_field::operators {
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
mfem::Vector enthalpyShape;
mfem::Vector gravityPotentialShape;
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId);
@@ -391,43 +525,20 @@ namespace mean_field::operators {
data.integrationRule =
&get_hydrostatic_rule(m_fem, enthalpyElement, gravityPotentialElement, *transformation);
const int quadraturePointCount = data.integrationRule->GetNPoints();
const int enthalpyDofCount = enthalpyElement.GetDof();
const int gravityPotentialDofCount = gravityPotentialElement.GetDof();
data.enthalpyBasis.SetSize(quadraturePointCount, enthalpyDofCount);
data.gravityPotentialBasis.SetSize(quadraturePointCount, gravityPotentialDofCount);
enthalpyShape.SetSize(enthalpyDofCount);
gravityPotentialShape.SetSize(gravityPotentialDofCount);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
enthalpyElement.CalcShape(integrationPoint, enthalpyShape);
gravityPotentialElement.CalcShape(integrationPoint, gravityPotentialShape);
for (int dof = 0; dof < enthalpyDofCount; ++dof) {
data.enthalpyBasis(quadraturePoint, dof) = enthalpyShape(dof);
}
for (int dof = 0; dof < gravityPotentialDofCount; ++dof) {
data.gravityPotentialBasis(quadraturePoint, dof) = gravityPotentialShape(dof);
}
}
const fem::ReferenceTableCache &referenceTables = m_fem.GetReferenceTables();
data.enthalpyReferenceTable = referenceTables.GetScalarTable(enthalpyElement, *data.integrationRule);
data.gravityPotentialReferenceTable =
referenceTables.GetScalarTable(gravityPotentialElement, *data.integrationRule);
}
}
void PreparedHydrostaticEquilibriumOperator::PrepareGeometry() {
std::optional<mapping::MappingStatus> PreparedHydrostaticEquilibriumOperator::PrepareGeometry() {
mfem::Vector displacementLocal;
true_to_local(*m_fem.displacementFes, m_context.GetDisplacementTrue(), displacementLocal);
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingContext mappingContext;
mfem::Array<int> compactificationDofs;
@@ -478,62 +589,61 @@ namespace mean_field::operators {
data.quadratureWeights.SetSize(quadraturePointCount);
data.baseMappingContexts.resize(quadraturePointCount);
data.baseMappingContexts.SetSize(quadraturePointCount, m_fem.mesh->Dimension());
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
transformation->SetIntPoint(&integrationPoint);
mapping::VolumeMappingContext &mappingContext = data.baseMappingContexts[quadraturePoint];
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mapping::MappingStatus::valid,
"Stateless mapping failed while preparing "
"hydrostatic geometry. Element: "
<< data.elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadraturePoint << ", status: " << static_cast<int>(mappingStatus)
);
if (mappingStatus != mapping::MappingStatus::valid) {
return mappingStatus;
}
const double quadratureWeight = mappingContext.quadrature.weight;
MFEM_VERIFY(
std::isfinite(quadratureWeight) && quadratureWeight > 0.0,
"Prepared hydrostatic geometry encountered "
"an invalid quadrature weight."
);
if (!std::isfinite(quadratureWeight)) {
return mapping::MappingStatus::non_finite_result;
}
if (quadratureWeight <= 0.0) {
return mapping::MappingStatus::non_positive_determinant;
}
data.baseMappingContexts.Store(quadraturePoint, mappingContext);
data.quadratureWeights(quadraturePoint) = quadratureWeight;
for (int component = 0; component < m_fem.mesh->Dimension(); ++component) {
const double position = mappingContext.mapping.physical_position(component);
MFEM_VERIFY(
std::isfinite(position), "Prepared hydrostatic geometry encountered "
"a non-finite physical position."
);
if (!std::isfinite(position)) {
return mapping::MappingStatus::non_finite_result;
}
data.physicalPositions(quadraturePoint, component) = position;
}
}
}
return std::nullopt;
}
void PreparedHydrostaticEquilibriumOperator::PrepareAlgebraicJacobianBlocks() {
bool PreparedHydrostaticEquilibriumOperator::PrepareAlgebraicJacobianBlocks() {
for (ElementPAData &data : m_elements) {
const int quadraturePointCount = data.quadratureWeights.Size();
const mfem::DenseMatrix &enthalpyBasis = data.GetEnthalpyBasis();
const mfem::DenseMatrix &gravityPotentialBasis = data.GetGravityPotentialBasis();
const int quadraturePointCount = data.quadratureWeights.Size();
const int enthalpyDofCount = data.enthalpyBasis.Width();
const int enthalpyDofCount = enthalpyBasis.Width();
const int gravityPotentialDofCount = data.gravityPotentialBasis.Width();
const int gravityPotentialDofCount = gravityPotentialBasis.Width();
MFEM_VERIFY(
data.enthalpyBasis.Height() == quadraturePointCount &&
data.gravityPotentialBasis.Height() == quadraturePointCount,
enthalpyBasis.Height() == quadraturePointCount &&
gravityPotentialBasis.Height() == quadraturePointCount,
"Prepared hydrostatic algebraic Jacobian has "
"inconsistent quadrature data."
);
@@ -552,27 +662,32 @@ namespace mean_field::operators {
const double quadratureWeight = data.quadratureWeights(quadraturePoint);
for (int testDof = 0; testDof < enthalpyDofCount; ++testDof) {
const double weightedTestBasis = quadratureWeight * data.enthalpyBasis(quadraturePoint, testDof);
const double weightedTestBasis = quadratureWeight * enthalpyBasis(quadraturePoint, testDof);
data.bernoulliConstantJacobian(testDof) -= weightedTestBasis;
for (int trialDof = 0; trialDof < enthalpyDofCount; ++trialDof) {
data.enthalpyJacobian(testDof, trialDof) +=
weightedTestBasis * data.enthalpyBasis(quadraturePoint, trialDof);
weightedTestBasis * enthalpyBasis(quadraturePoint, trialDof);
}
for (int trialDof = 0; trialDof < gravityPotentialDofCount; ++trialDof) {
data.gravityPotentialJacobian(testDof, trialDof) +=
weightedTestBasis * data.gravityPotentialBasis(quadraturePoint, trialDof);
weightedTestBasis * gravityPotentialBasis(quadraturePoint, trialDof);
}
}
}
if (!is_finite(data.enthalpyJacobian) || !is_finite(data.gravityPotentialJacobian) ||
!is_finite(data.bernoulliConstantJacobian)) {
return true;
}
}
++m_algebraicJacobianStatistics.preparations;
return false;
}
void PreparedHydrostaticEquilibriumOperator::PrepareRotation() {
bool PreparedHydrostaticEquilibriumOperator::PrepareRotation() {
MFEM_VERIFY(m_rotation.has_value(), "Prepared hydrostatic rotation has no frozen state.");
mfem::Vector physicalPosition(m_fem.mesh->Dimension());
@@ -598,10 +713,9 @@ namespace mean_field::operators {
const double rotationPotential = m_rotation->potential(physicalPosition);
MFEM_VERIFY(
std::isfinite(rotationPotential), "Prepared hydrostatic rotation encountered "
"a non-finite potential."
);
if (!std::isfinite(rotationPotential)) {
return true;
}
data.rotationPotential(quadraturePoint) = rotationPotential;
@@ -612,18 +726,19 @@ namespace mean_field::operators {
const double gradientComponent =
m_rotation->potential_directional_derivative(physicalPosition, coordinateDirection);
MFEM_VERIFY(
std::isfinite(gradientComponent), "Prepared hydrostatic rotation encountered "
"a non-finite potential gradient."
);
if (!std::isfinite(gradientComponent)) {
return true;
}
data.rotationGradient(quadraturePoint, component) = gradientComponent;
}
}
}
return false;
}
void PreparedHydrostaticEquilibriumOperator::PrepareBaseState() {
bool PreparedHydrostaticEquilibriumOperator::PrepareBaseState() {
mfem::Vector enthalpyLocal;
mfem::Vector gravityPotentialLocal;
@@ -655,9 +770,9 @@ namespace mean_field::operators {
quadratureGravityPotential.SetSize(quadraturePointCount);
data.enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy);
data.GetEnthalpyBasis().Mult(elementEnthalpy, quadratureEnthalpy);
data.gravityPotentialBasis.Mult(elementGravityPotential, quadratureGravityPotential);
data.GetGravityPotentialBasis().Mult(elementGravityPotential, quadratureGravityPotential);
MFEM_VERIFY(
data.rotationPotential.Size() == quadraturePointCount, "Prepared hydrostatic base state has stale "
@@ -675,16 +790,17 @@ namespace mean_field::operators {
const double weightedResidual = data.quadratureWeights(quadraturePoint) * imbalance;
MFEM_VERIFY(
std::isfinite(weightedResidual), "Prepared hydrostatic base state encountered "
"a non-finite residual value."
);
if (!std::isfinite(imbalance) || !std::isfinite(weightedResidual)) {
return true;
}
data.weightedResidual(quadraturePoint) = weightedResidual;
data.hydrostaticImbalance(quadraturePoint) = imbalance;
}
}
return false;
}
void PreparedHydrostaticEquilibriumOperator::FinalizeDisplacementJacobianPreparation() {
@@ -695,7 +811,8 @@ namespace mean_field::operators {
MFEM_VERIFY(
data.baseDisplacementData.has_value() && data.compactificationData.has_value() &&
static_cast<int>(data.baseMappingContexts.size()) == quadraturePointCount &&
data.baseMappingContexts.GetPointCount() == quadraturePointCount &&
data.baseMappingContexts.GetDimension() == dimension &&
data.rotationGradient.Height() == quadraturePointCount &&
data.rotationGradient.Width() == dimension &&
data.hydrostaticImbalance.Size() == quadraturePointCount,
@@ -703,11 +820,9 @@ namespace mean_field::operators {
"has inconsistent frozen data."
);
}
++m_displacementJacobianStatistics.preparations;
}
void PreparedHydrostaticEquilibriumOperator::AssembleCachedResidual() {
bool PreparedHydrostaticEquilibriumOperator::AssembleCachedResidual() {
mfem::Vector localResidual(m_fem.enthalpyFes->GetVSize());
localResidual = 0.0;
@@ -716,7 +831,7 @@ namespace mean_field::operators {
for (const ElementPAData &data : m_elements) {
elementResidual.SetSize(data.enthalpyDofs.Size());
data.enthalpyBasis.MultTranspose(data.weightedResidual, elementResidual);
data.GetEnthalpyBasis().MultTranspose(data.weightedResidual, elementResidual);
if (data.enthalpyDofTransformation != nullptr) {
data.enthalpyDofTransformation->TransformDual(elementResidual);
@@ -729,6 +844,8 @@ namespace mean_field::operators {
m_cachedResidual.SetSize(m_context.GetEnthalpyMap().reduced_size());
m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, m_cachedResidual);
return !is_finite(m_cachedResidual);
}
void PreparedHydrostaticEquilibriumOperator::BuildResidual(mfem::Vector &residual) const {
@@ -926,12 +1043,12 @@ namespace mean_field::operators {
);
weightedVariation.SetSize(quadraturePointCount);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
weightedVariation(quadraturePoint) =
-2.0 * fractionalAngularVelocityVariation * data.quadratureWeights(quadraturePoint) *
data.rotationPotential(quadraturePoint);
weightedVariation(quadraturePoint) = -2.0 * fractionalAngularVelocityVariation *
data.quadratureWeights(quadraturePoint) *
data.rotationPotential(quadraturePoint);
}
elementAction.SetSize(data.enthalpyDofs.Size());
data.enthalpyBasis.MultTranspose(weightedVariation, elementAction);
data.GetEnthalpyBasis().MultTranspose(weightedVariation, elementAction);
if (data.enthalpyDofTransformation != nullptr) {
data.enthalpyDofTransformation->TransformDual(elementAction);
}
@@ -1063,6 +1180,7 @@ namespace mean_field::operators {
mfem::Vector elementDisplacementVariation;
mfem::Vector weightedQuadratureVariation;
mfem::Vector elementAction;
mapping::VolumeMappingContext mappingContext;
mapping::VolumeMappingVariation variation;
for (const ElementPAData &data : m_elements) {
@@ -1098,7 +1216,7 @@ namespace mean_field::operators {
const int quadraturePointCount = data.integrationRule->GetNPoints();
MFEM_VERIFY(
static_cast<int>(data.baseMappingContexts.size()) == quadraturePointCount &&
data.baseMappingContexts.GetPointCount() == quadraturePointCount &&
data.quadratureWeights.Size() == quadraturePointCount &&
data.hydrostaticImbalance.Size() == quadraturePointCount &&
data.rotationGradient.Height() == quadraturePointCount &&
@@ -1111,10 +1229,10 @@ namespace mean_field::operators {
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
data.baseMappingContexts.Load(quadraturePoint, mappingContext);
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolumeVariation(
mappingData, directionData, *transformation, integrationPoint,
data.baseMappingContexts[quadraturePoint], workspace, variation
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolumeVariation(
mappingData, directionData, *transformation, integrationPoint, mappingContext, workspace, variation
);
MFEM_VERIFY(
@@ -1147,7 +1265,7 @@ namespace mean_field::operators {
elementAction.SetSize(data.enthalpyDofs.Size());
data.enthalpyBasis.MultTranspose(weightedQuadratureVariation, elementAction);
data.GetEnthalpyBasis().MultTranspose(weightedQuadratureVariation, elementAction);
if (data.enthalpyDofTransformation != nullptr) {
data.enthalpyDofTransformation->TransformDual(elementAction);

View File

@@ -2,7 +2,13 @@ module;
#include <array>
#include <cmath>
#include <expected>
#include <mfem.hpp>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mpi.h>
module mean_field;
@@ -116,6 +122,109 @@ namespace {
) {
MFEM_VERIFY(prepared.identity == requested.identity || prepared.revision != requested.revision, message);
}
using MassRejection = mean_field::operators::MassNormalizationPreparationRejection;
using MassRejectionReason = mean_field::operators::MassNormalizationPreparationRejectionReason;
[[nodiscard]] int mapping_status_priority(const mean_field::mapping::MappingStatus status) {
using Status = mean_field::mapping::MappingStatus;
switch (status) {
case Status::non_positive_determinant:
return 7;
case Status::non_finite_result:
return 6;
case Status::non_finite_input:
return 5;
case Status::outside_reference_domain:
return 4;
case Status::at_compactified_infinity:
return 3;
case Status::invalid_reference_radius:
return 2;
case Status::valid:
throw std::logic_error("A valid mapping cannot be a mass-normalization candidate rejection.");
case Status::invalid_dimension:
throw std::logic_error("A mapping dimension error cannot be a mass-normalization candidate rejection.");
}
throw std::logic_error("Unknown mapping status in mass-normalization candidate rejection.");
}
[[nodiscard]] mean_field::mapping::MappingStatus mapping_status_from_priority(const int priority) {
using Status = mean_field::mapping::MappingStatus;
switch (priority) {
case 7:
return Status::non_positive_determinant;
case 6:
return Status::non_finite_result;
case 5:
return Status::non_finite_input;
case 4:
return Status::outside_reference_domain;
case 3:
return Status::at_compactified_infinity;
case 2:
return Status::invalid_reference_radius;
default:
throw std::logic_error("Invalid synchronized mapping priority for mass normalization.");
}
}
/*
* Phase priority is explicit and independent of enum representation:
* mapping wins over interpolation, which wins over assembled-mass
* arithmetic. The mapping detail is likewise selected explicitly.
*/
[[nodiscard]] int rejection_priority(const MassRejection &rejection) {
switch (rejection.reason) {
case MassRejectionReason::mapping_failure:
return 300 + mapping_status_priority(rejection.mappingStatus);
case MassRejectionReason::non_finite_density_interpolation:
return 200;
case MassRejectionReason::non_finite_assembled_mass:
return 100;
}
throw std::logic_error("Unknown mass-normalization candidate-rejection reason.");
}
[[nodiscard]] MassRejection rejection_from_priority(const int priority) {
if (priority >= 300) {
return {
.reason = MassRejectionReason::mapping_failure,
.mappingStatus = mapping_status_from_priority(priority - 300)
};
}
if (priority == 200) {
return {.reason = MassRejectionReason::non_finite_density_interpolation};
}
if (priority == 100) {
return {.reason = MassRejectionReason::non_finite_assembled_mass};
}
throw std::logic_error("Invalid synchronized mass-normalization candidate-rejection priority.");
}
void retain_higher_priority_rejection(
std::optional<MassRejection> &current,
const MassRejection candidate
) {
if (!current.has_value() || rejection_priority(candidate) > rejection_priority(*current)) {
current = candidate;
}
}
[[nodiscard]] std::optional<MassRejection> synchronize_rejection(
const std::optional<MassRejection> &local,
const MPI_Comm communicator
) {
const int localPriority = local.has_value() ? rejection_priority(*local) : 0;
int globalPriority = 0;
if (MPI_Allreduce(&localPriority, &globalPriority, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("PreparedMassNormalizationOperator could not synchronize candidate validity.");
}
if (globalPriority == 0) {
return std::nullopt;
}
return rejection_from_priority(globalPriority);
}
} // namespace
namespace mean_field::operators {
@@ -154,6 +263,26 @@ namespace mean_field::operators {
PreparedMassNormalizationReport PreparedMassNormalizationOperator::Prepare(
const MassNormalizationStateView &state,
const MassNormalizationDependencies &dependencies
) {
auto result = TryPrepare(state, dependencies);
if (!result.has_value()) {
const MassNormalizationPreparationRejection &rejection = result.error();
switch (rejection.reason) {
case MassNormalizationPreparationRejectionReason::mapping_failure:
throw std::domain_error("PreparedMassNormalizationOperator could not map the candidate geometry.");
case MassNormalizationPreparationRejectionReason::non_finite_density_interpolation:
throw std::domain_error("PreparedMassNormalizationOperator produced a non-finite quadrature density.");
case MassNormalizationPreparationRejectionReason::non_finite_assembled_mass:
throw std::domain_error("PreparedMassNormalizationOperator assembled a non-finite mass residual.");
}
throw std::logic_error("Unknown mass-normalization candidate-rejection reason.");
}
return std::move(result).value();
}
MassNormalizationPreparationResult PreparedMassNormalizationOperator::TryPrepare(
const MassNormalizationStateView &state,
const MassNormalizationDependencies &dependencies
) {
MFEM_VERIFY(
std::isfinite(state.targetMass) && state.targetMass > 0.0,
@@ -195,6 +324,7 @@ namespace mean_field::operators {
m_isPrepared = false;
PreparedMassNormalizationReport report;
std::optional<MassNormalizationPreparationRejection> localRejection;
if (rebuildStaticPlan) {
BuildStaticPlan();
@@ -202,27 +332,37 @@ namespace mean_field::operators {
}
if (refreshGeometry) {
RefreshGeometry(m_gravityContext.GetGeometryContext().GetDisplacementTrue());
localRejection = RefreshGeometry(m_gravityContext.GetGeometryContext().GetDisplacementTrue());
report.refreshedGeometry = true;
}
if (refreshDensity) {
RefreshDensity(m_gravityContext.GetDensityTrue());
if (auto densityRejection = RefreshDensity(m_gravityContext.GetDensityTrue());
densityRejection.has_value()) {
retain_higher_priority_rejection(localRejection, *densityRejection);
}
report.refreshedDensity = true;
}
if (auto globalRejection = synchronize_rejection(localRejection, m_fem.mesh->GetComm());
globalRejection.has_value()) {
return std::unexpected(*globalRejection);
}
if (updateTargetMass) {
m_targetMass = state.targetMass;
report.updatedTargetMass = true;
}
if (refreshGeometry || refreshDensity) {
AssembleResidual();
if (auto rejection = AssembleResidual(); rejection.has_value()) {
return std::unexpected(*rejection);
}
report.assembledResidual = true;
} else if (updateTargetMass) {
m_cachedResidual.SetSize(1);
m_cachedResidual(0) = m_currentMass - m_targetMass;
++m_preparationCount;
if (auto rejection = UpdateResidualForTargetMass(); rejection.has_value()) {
return std::unexpected(*rejection);
}
report.assembledResidual = true;
}
@@ -238,6 +378,13 @@ namespace mean_field::operators {
return Prepare({.targetMass = constraint.targetMass().value()}, dependencies);
}
MassNormalizationPreparationResult PreparedMassNormalizationOperator::TryPrepare(
const models::CompiledFixedMass &constraint,
const MassNormalizationDependencies &dependencies
) {
return TryPrepare({.targetMass = constraint.targetMass().value()}, dependencies);
}
void PreparedMassNormalizationOperator::BuildStaticPlan() {
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
@@ -273,28 +420,26 @@ namespace mean_field::operators {
const mfem::IntegrationRule &integrationRule =
get_mass_normalization_rule(m_fem, densityElement, *transformation);
data.integrationRule = &integrationRule;
data.quadraturePoints.resize(integrationRule.GetNPoints());
for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) {
QuadraturePointData &point = data.quadraturePoints[quadraturePoint];
point.integrationPoint = integrationRule.IntPoint(quadraturePoint);
point.densityShape.SetSize(densityElement.GetDof());
densityElement.CalcShape(point.integrationPoint, point.densityShape);
}
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());
}
int globalStellarElementCount = 0;
MPI_Allreduce(
&localStellarElementCount, &globalStellarElementCount, 1, MPI_INT, MPI_SUM, m_fem.mesh->GetComm()
);
if (MPI_Allreduce(
&localStellarElementCount, &globalStellarElementCount, 1, MPI_INT, MPI_SUM, m_fem.mesh->GetComm()
) != MPI_SUCCESS) {
throw std::runtime_error("PreparedMassNormalizationOperator could not count stellar elements.");
}
MFEM_VERIFY(globalStellarElementCount > 0, "PreparedMassNormalizationOperator found no stellar elements.");
}
void PreparedMassNormalizationOperator::RefreshGeometry(const mfem::Vector &displacement) {
std::optional<MassNormalizationPreparationRejection>
PreparedMassNormalizationOperator::RefreshGeometry(const mfem::Vector &displacement) {
MFEM_VERIFY(
displacement.Size() == m_fem.displacementFes->GetTrueVSize(),
"PreparedMassNormalizationOperator received a displacement "
@@ -309,6 +454,8 @@ namespace mean_field::operators {
true_to_local(*m_fem.displacementFes, displacement, displacementLocal);
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingContext mappingContext;
std::optional<MassNormalizationPreparationRejection> rejection;
for (ElementPAData &data : m_elements) {
displacementLocal.GetSubVector(data.displacementDofs, data.baseDisplacement);
@@ -340,23 +487,33 @@ namespace mean_field::operators {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (QuadraturePointData &point : data.quadraturePoints) {
for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) {
const mapping::MappingStatus status = m_domainMapper.EvaluateVolume(
mappingData, *transformation, point.integrationPoint, workspace, point.mappingContext
mappingData, *transformation, data.integrationRule->IntPoint(quadraturePoint), workspace,
mappingContext
);
MFEM_VERIFY(
status == mapping::MappingStatus::valid, "Stateless mapping failed while preparing mass "
"normalization. Element: "
<< data.elementId
<< ", attribute: " << transformation->Attribute
<< ", status: " << static_cast<int>(status)
status != mapping::MappingStatus::invalid_dimension,
"Stateless mapping reported a dimension error while preparing mass normalization."
);
if (status != mapping::MappingStatus::valid) {
retain_higher_priority_rejection(
rejection, {.reason = MassNormalizationPreparationRejectionReason::mapping_failure,
.mappingStatus = status}
);
} else {
data.mappingContexts.Store(quadraturePoint, mappingContext);
data.quadratureWeights(quadraturePoint) = mappingContext.quadrature.weight;
}
}
}
return rejection;
}
void PreparedMassNormalizationOperator::RefreshDensity(const mfem::Vector &density) {
std::optional<MassNormalizationPreparationRejection>
PreparedMassNormalizationOperator::RefreshDensity(const mfem::Vector &density) {
MFEM_VERIFY(
density.Size() == m_fem.densityFes->GetTrueVSize(),
"PreparedMassNormalizationOperator received a density vector "
@@ -371,6 +528,7 @@ namespace mean_field::operators {
true_to_local(*m_fem.densityFes, density, densityLocal);
mfem::Vector elementDensity;
std::optional<MassNormalizationPreparationRejection> rejection;
for (ElementPAData &data : m_elements) {
densityLocal.GetSubVector(data.densityDofs, elementDensity);
@@ -379,31 +537,64 @@ namespace mean_field::operators {
data.densityDofTransformation->InvTransformPrimal(elementDensity);
}
for (QuadraturePointData &point : data.quadraturePoints) {
point.density = elementDensity * point.densityShape;
MFEM_VERIFY(
std::isfinite(point.density), "PreparedMassNormalizationOperator produced a non-finite "
"quadrature density."
);
data.densityBasis->GetValues().Mult(elementDensity, data.density);
for (int quadraturePoint = 0; quadraturePoint < data.density.Size(); ++quadraturePoint) {
if (!std::isfinite(data.density(quadraturePoint))) {
retain_higher_priority_rejection(
rejection,
{.reason = MassNormalizationPreparationRejectionReason::non_finite_density_interpolation}
);
}
}
}
return rejection;
}
void PreparedMassNormalizationOperator::AssembleResidual() {
std::optional<MassNormalizationPreparationRejection> PreparedMassNormalizationOperator::AssembleResidual() {
double localMass = 0.0;
std::optional<MassNormalizationPreparationRejection> localRejection;
for (const ElementPAData &data : m_elements) {
for (const QuadraturePointData &point : data.quadraturePoints) {
localMass += point.density * point.mappingContext.quadrature.weight;
for (int quadraturePoint = 0; quadraturePoint < data.density.Size(); ++quadraturePoint) {
const double contribution = data.density(quadraturePoint) * data.quadratureWeights(quadraturePoint);
if (!std::isfinite(contribution) || !std::isfinite(localMass + contribution)) {
localRejection = {.reason = MassNormalizationPreparationRejectionReason::non_finite_assembled_mass};
continue;
}
localMass += contribution;
}
}
if (auto globalRejection = synchronize_rejection(localRejection, m_fem.mesh->GetComm());
globalRejection.has_value()) {
return globalRejection;
}
m_currentMass = GlobalSum(localMass);
MFEM_VERIFY(std::isfinite(m_currentMass), "PreparedMassNormalizationOperator assembled a non-finite mass.");
if (!std::isfinite(m_currentMass)) {
return MassNormalizationPreparationRejection{
.reason = MassNormalizationPreparationRejectionReason::non_finite_assembled_mass
};
}
return UpdateResidualForTargetMass();
}
std::optional<MassNormalizationPreparationRejection>
PreparedMassNormalizationOperator::UpdateResidualForTargetMass() {
m_cachedResidual.SetSize(1);
m_cachedResidual(0) = m_currentMass - m_targetMass;
std::optional<MassNormalizationPreparationRejection> localRejection;
if (!std::isfinite(m_cachedResidual(0))) {
localRejection = {.reason = MassNormalizationPreparationRejectionReason::non_finite_assembled_mass};
}
if (auto globalRejection = synchronize_rejection(localRejection, m_fem.mesh->GetComm());
globalRejection.has_value()) {
return globalRejection;
}
++m_preparationCount;
return std::nullopt;
}
void PreparedMassNormalizationOperator::BuildResidual(mfem::Vector &residual) const {
@@ -424,6 +615,7 @@ namespace mean_field::operators {
true_to_local(*m_fem.densityFes, densityVariation, densityVariationLocal);
mfem::Vector elementDensityVariation;
mfem::Vector quadratureDensityVariation;
double localAction = 0.0;
for (const ElementPAData &data : m_elements) {
@@ -433,8 +625,10 @@ namespace mean_field::operators {
data.densityDofTransformation->InvTransformPrimal(elementDensityVariation);
}
for (const QuadraturePointData &point : data.quadraturePoints) {
localAction += (elementDensityVariation * point.densityShape) * point.mappingContext.quadrature.weight;
quadratureDensityVariation.SetSize(data.integrationRule->GetNPoints());
data.densityBasis->GetValues().Mult(elementDensityVariation, quadratureDensityVariation);
for (int quadraturePoint = 0; quadraturePoint < quadratureDensityVariation.Size(); ++quadraturePoint) {
localAction += quadratureDensityVariation(quadraturePoint) * data.quadratureWeights(quadraturePoint);
}
}
@@ -459,6 +653,7 @@ namespace mean_field::operators {
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingVariation variation;
mapping::VolumeMappingContext mappingContext;
mfem::Vector elementDisplacementVariation;
double localAction = 0.0;
@@ -490,10 +685,11 @@ namespace mean_field::operators {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
for (const QuadraturePointData &point : data.quadraturePoints) {
for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) {
data.mappingContexts.Load(quadraturePoint, mappingContext);
const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation(
mappingData, directionData, *transformation, point.integrationPoint, point.mappingContext,
workspace, variation
mappingData, directionData, *transformation, data.integrationRule->IntPoint(quadraturePoint),
mappingContext, workspace, variation
);
MFEM_VERIFY(
@@ -503,7 +699,7 @@ namespace mean_field::operators {
<< ", status: " << static_cast<int>(status)
);
localAction += point.density * variation.weight_variation;
localAction += data.density(quadraturePoint) * variation.weight_variation;
}
}
@@ -598,14 +794,13 @@ namespace mean_field::operators {
localDual = 0.0;
mfem::Vector elementDual;
mfem::Vector weightedDual;
for (const ElementPAData &data : m_elements) {
elementDual.SetSize(data.densityDofs.Size());
elementDual = 0.0;
for (const QuadraturePointData &point : data.quadraturePoints) {
elementDual.Add(residualDual * point.mappingContext.quadrature.weight, point.densityShape);
}
weightedDual = data.quadratureWeights;
weightedDual *= residualDual;
data.densityBasis->GetValues().MultTranspose(weightedDual, elementDual);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->TransformDual(elementDual);
@@ -630,6 +825,7 @@ namespace mean_field::operators {
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingVariation variation;
mapping::VolumeMappingContext mappingContext;
mfem::Vector elementDirection;
mfem::Vector elementDual;
@@ -661,10 +857,11 @@ namespace mean_field::operators {
double elementDofAction = 0.0;
for (const QuadraturePointData &point : data.quadraturePoints) {
for (int quadraturePoint = 0; quadraturePoint < data.integrationRule->GetNPoints(); ++quadraturePoint) {
data.mappingContexts.Load(quadraturePoint, mappingContext);
const mapping::MappingStatus status = m_domainMapper.EvaluateVolumeVariation(
mappingData, directionData, *transformation, point.integrationPoint, point.mappingContext,
workspace, variation
mappingData, directionData, *transformation, data.integrationRule->IntPoint(quadraturePoint),
mappingContext, workspace, variation
);
MFEM_VERIFY(
@@ -673,7 +870,7 @@ namespace mean_field::operators {
<< data.elementId << ", status: " << static_cast<int>(status)
);
elementDofAction += point.density * variation.weight_variation;
elementDofAction += data.density(quadraturePoint) * variation.weight_variation;
}
elementDual(elementDof) = residualDual * elementDofAction;
@@ -716,7 +913,9 @@ namespace mean_field::operators {
double PreparedMassNormalizationOperator::GlobalSum(const double localValue) const {
double globalValue = 0.0;
MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_fem.mesh->GetComm());
if (MPI_Allreduce(&localValue, &globalValue, 1, MPI_DOUBLE, MPI_SUM, m_fem.mesh->GetComm()) != MPI_SUCCESS) {
throw std::runtime_error("PreparedMassNormalizationOperator could not assemble a distributed scalar.");
}
return globalValue;
}

View File

@@ -3,10 +3,14 @@ module;
#include <array>
#include <cmath>
#include <cstdint>
#include <expected>
#include <limits>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
@@ -20,6 +24,99 @@ namespace {
using PressureDomain = mean_field::field::FieldDomainT<mean_field::field::Enthalpy>;
using Rejection = mean_field::operators::PressureForcePreparationRejection;
using Reason = mean_field::operators::PressureForcePreparationRejectionReason;
[[nodiscard]] Rejection equation_of_state_rejection(const mean_field::eos::EvaluationErrorCode code) noexcept {
return {.reason = Reason::equation_of_state, .equationOfStateCode = code};
}
[[nodiscard]] Rejection mapping_rejection(const mean_field::mapping::MappingStatus status) {
MFEM_VERIFY(
status != mean_field::mapping::MappingStatus::invalid_dimension,
"Prepared pressure-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;
}
switch (rejection->reason) {
case Reason::equation_of_state:
return static_cast<int>(rejection->equationOfStateCode) + 1;
case Reason::invalid_mapping:
return 128 + static_cast<int>(rejection->mappingStatus);
case Reason::non_finite_arithmetic:
default:
return 256;
}
}
[[nodiscard]] Rejection decode_rejection(const int encoded) {
if (encoded >= 256) {
return non_finite_rejection();
}
if (encoded >= 128) {
return mapping_rejection(static_cast<mean_field::mapping::MappingStatus>(encoded - 128));
}
return equation_of_state_rejection(static_cast<mean_field::eos::EvaluationErrorCode>(encoded - 1));
}
[[nodiscard]] std::optional<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("PreparedPressureForceOperator could not synchronize candidate validity.");
}
if (globalEncoded == 0) {
return std::nullopt;
}
return decode_rejection(globalEncoded);
}
[[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;
}
[[noreturn]] void throw_rejection(const Rejection &rejection) {
switch (rejection.reason) {
case Reason::equation_of_state:
throw mean_field::eos::EvaluationError(
rejection.equationOfStateCode, "PreparedPressureForceOperator encountered invalid thermodynamic data."
);
case Reason::invalid_mapping:
throw std::domain_error("PreparedPressureForceOperator encountered an invalid mapped domain.");
case Reason::non_finite_arithmetic:
default:
throw std::domain_error("PreparedPressureForceOperator produced non-finite arithmetic.");
}
}
void verify_required_spaces(const mean_field::fem::FEM &f) {
MFEM_VERIFY(f.mesh != nullptr, "PreparedPressureForceOperator requires a mesh.");
@@ -296,11 +393,26 @@ namespace mean_field::operators {
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
) {
auto result = TryPrepare(state, dependencies);
if (!result.has_value()) {
throw_rejection(result.error());
}
return std::move(result).value();
}
std::expected<
PreparedPressureForceReport,
PressureForcePreparationRejection>
PreparedPressureForceOperator::TryPrepare(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
) {
const bool wasPrepared = m_isPrepared;
PreparedPressureForceReport report;
report.contextReport = m_context.Prepare(state, dependencies);
if (!report.contextReport.DidAnyWork() && m_isPrepared) {
if (!report.contextReport.DidAnyWork() && wasPrepared) {
return report;
}
@@ -317,20 +429,38 @@ namespace mean_field::operators {
m_isPrepared = false;
if (report.contextReport.preparedStaticDependencies) {
if (report.contextReport.preparedStaticDependencies || !wasPrepared) {
PrepareStaticPlan();
}
if (report.contextReport.preparedGeometryState) {
PrepareGeometry();
if (report.contextReport.preparedGeometryState || !wasPrepared) {
const auto globalGeometryFailure = synchronize_rejection(PrepareGeometry(), m_fem.enthalpyFes->GetComm());
if (globalGeometryFailure.has_value()) {
return std::unexpected(*globalGeometryFailure);
}
}
if (report.contextReport.preparedMaterialState) {
PrepareMaterialState();
std::optional<PressureForcePreparationRejection> localMaterialFailure;
if (report.contextReport.preparedMaterialState || !wasPrepared) {
localMaterialFailure = PrepareMaterialState();
}
const auto globalMaterialFailure = synchronize_rejection(localMaterialFailure, m_fem.enthalpyFes->GetComm());
if (globalMaterialFailure.has_value()) {
return std::unexpected(*globalMaterialFailure);
}
if (report.contextReport.preparedMaterialState || !wasPrepared) {
FinalizeDisplacementJacobianPreparation();
AssembleCachedResidual();
const auto globalAssemblyFailure =
synchronize_rejection(AssembleCachedResidual(), m_fem.enthalpyFes->GetComm());
if (globalAssemblyFailure.has_value()) {
return std::unexpected(*globalAssemblyFailure);
}
++m_enthalpyJacobianStatistics.preparations;
++m_displacementJacobianStatistics.preparations;
++m_residualPreparationCount;
@@ -362,9 +492,6 @@ namespace mean_field::operators {
m_elements.reserve(m_fem.mesh->GetNE());
mfem::Vector enthalpyShape;
mfem::DenseMatrix displacementDShape;
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId);
@@ -429,38 +556,21 @@ namespace mean_field::operators {
"an unexpected vector DOF count."
);
data.enthalpyBasis.SetSize(quadraturePointCount, enthalpyDofCount);
data.referenceTestGradients.resize(quadraturePointCount);
const fem::ReferenceTableCache &referenceTables = m_fem.GetReferenceTables();
data.enthalpyReferenceTable = referenceTables.GetScalarTable(enthalpyElement, *data.integrationRule);
data.displacementReferenceTable =
referenceTables.GetScalarTable(displacementElement, *data.integrationRule);
data.physicalTestGradients.resize(quadraturePointCount);
enthalpyShape.SetSize(enthalpyDofCount);
displacementDShape.SetSize(scalarDisplacementDofCount, dimension);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
enthalpyElement.CalcShape(integrationPoint, enthalpyShape);
displacementElement.CalcDShape(integrationPoint, displacementDShape);
for (int enthalpyDof = 0; enthalpyDof < enthalpyDofCount; ++enthalpyDof) {
data.enthalpyBasis(quadraturePoint, enthalpyDof) = enthalpyShape(enthalpyDof);
}
data.referenceTestGradients[quadraturePoint] = displacementDShape;
}
}
}
void PreparedPressureForceOperator::PrepareGeometry() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareGeometry() {
mfem::Vector displacementLocal;
true_to_local(*m_fem.displacementFes, m_baseDisplacementTrue, displacementLocal);
mapping::DomainMapper::Workspace workspace(m_fem.mesh->Dimension());
mapping::VolumeMappingContext mappingContext;
mfem::Vector elementDisplacement;
mfem::Vector elementCompactification;
@@ -504,14 +614,15 @@ namespace mean_field::operators {
const int quadraturePointCount = data.integrationRule->GetNPoints();
MFEM_VERIFY(
static_cast<int>(data.referenceTestGradients.size()) == quadraturePointCount,
data.displacementReferenceTable != nullptr &&
data.displacementReferenceTable->GetPointCount() == quadraturePointCount,
"Prepared pressure-force geometry has inconsistent "
"static gradient data."
);
data.quadratureWeights.SetSize(quadraturePointCount);
data.baseMappingContexts.resize(quadraturePointCount);
data.baseMappingContexts.SetSize(quadraturePointCount, m_fem.mesh->Dimension());
data.physicalTestGradients.resize(quadraturePointCount);
@@ -520,33 +631,30 @@ namespace mean_field::operators {
transformation->SetIntPoint(&integrationPoint);
mapping::VolumeMappingContext &mappingContext = data.baseMappingContexts[quadraturePoint];
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mapping::MappingStatus::valid,
"Stateless mapping failed while preparing "
"pressure-force geometry. Element: "
<< data.elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadraturePoint << ", status: " << static_cast<int>(mappingStatus)
);
if (mappingStatus != mapping::MappingStatus::valid) {
return mapping_rejection(mappingStatus);
}
const double quadratureWeight = mappingContext.quadrature.weight;
MFEM_VERIFY(
std::isfinite(quadratureWeight) && quadratureWeight > 0.0,
"Prepared pressure-force geometry encountered an "
"invalid quadrature weight."
);
if (!std::isfinite(quadratureWeight)) {
return mapping_rejection(mapping::MappingStatus::non_finite_result);
}
if (quadratureWeight <= 0.0) {
return mapping_rejection(mapping::MappingStatus::non_positive_determinant);
}
data.quadratureWeights(quadraturePoint) = quadratureWeight;
data.baseMappingContexts.Store(quadraturePoint, mappingContext);
data.quadratureWeights(quadraturePoint) = quadratureWeight;
const mfem::DenseMatrix &referenceTestGradient = data.referenceTestGradients[quadraturePoint];
const mfem::DenseMatrix &referenceTestGradient =
data.displacementReferenceTable->GetGradients(quadraturePoint);
mfem::DenseMatrix &physicalTestGradient = data.physicalTestGradients[quadraturePoint];
mfem::DenseMatrix &physicalTestGradient = data.physicalTestGradients[quadraturePoint];
MFEM_VERIFY(
referenceTestGradient.Width() == mappingContext.quadrature.J_inv.Height() &&
@@ -559,11 +667,15 @@ namespace mean_field::operators {
physicalTestGradient.SetSize(referenceTestGradient.Height(), mappingContext.quadrature.J_inv.Width());
mfem::Mult(referenceTestGradient, mappingContext.quadrature.J_inv, physicalTestGradient);
if (!matrix_is_finite(physicalTestGradient)) {
return non_finite_rejection();
}
}
}
return std::nullopt;
}
void PreparedPressureForceOperator::PrepareMaterialState() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareMaterialState() {
mfem::Vector enthalpyLocal;
true_to_local(*m_fem.enthalpyFes, m_baseEnthalpyTrue, enthalpyLocal);
@@ -574,6 +686,7 @@ namespace mean_field::operators {
const int dimension = m_fem.mesh->Dimension();
const mfem::Ordering::Type displacementOrdering = m_fem.displacementFes->GetOrdering();
std::optional<PressureForcePreparationRejection> materialFailure;
for (ElementPAData &data : m_elements) {
enthalpyLocal.GetSubVector(data.enthalpyDofs, elementEnthalpy);
@@ -582,9 +695,11 @@ namespace mean_field::operators {
data.enthalpyDofTransformation->InvTransformPrimal(elementEnthalpy);
}
const int quadraturePointCount = data.enthalpyBasis.Height();
const mfem::DenseMatrix &enthalpyBasis = data.GetEnthalpyBasis();
const int enthalpyDofCount = data.enthalpyBasis.Width();
const int quadraturePointCount = enthalpyBasis.Height();
const int enthalpyDofCount = enthalpyBasis.Width();
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
@@ -602,7 +717,7 @@ namespace mean_field::operators {
quadratureEnthalpy.SetSize(quadraturePointCount);
data.enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy);
enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy);
data.pressure.SetSize(quadraturePointCount);
@@ -619,6 +734,19 @@ namespace mean_field::operators {
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const double enthalpy = quadratureEnthalpy(quadraturePoint);
if (!std::isfinite(enthalpy)) {
materialFailure = equation_of_state_rejection(eos::EvaluationErrorCode::nonfinite_input);
data.pressure(quadraturePoint) = 0.0;
data.pressureDerivative(quadraturePoint) = 0.0;
continue;
}
if (enthalpy < 0.0) {
materialFailure = equation_of_state_rejection(eos::EvaluationErrorCode::outside_domain);
data.pressure(quadraturePoint) = 0.0;
data.pressureDerivative(quadraturePoint) = 0.0;
continue;
}
const dimensions::SpecificEnthalpyValue specificEnthalpy{enthalpy};
const double pressure =
eos::evaluate<eos::quantity::Pressure>(m_equationOfState, specificEnthalpy).value();
@@ -631,11 +759,12 @@ namespace mean_field::operators {
const double quadratureWeight = data.quadratureWeights(quadraturePoint);
MFEM_VERIFY(
std::isfinite(pressure) && std::isfinite(pressureDerivative),
"Prepared pressure-force material state encountered "
"a non-finite EOS value."
);
if (!std::isfinite(pressure) || !std::isfinite(pressureDerivative)) {
materialFailure = equation_of_state_rejection(eos::EvaluationErrorCode::nonfinite_result);
data.pressure(quadraturePoint) = 0.0;
data.pressureDerivative(quadraturePoint) = 0.0;
continue;
}
data.pressure(quadraturePoint) = pressure;
@@ -659,19 +788,32 @@ namespace mean_field::operators {
const double weightedTestGradient =
quadratureWeight * physicalTestGradient(scalarDof, component);
data.elementResidual(vectorDof) -= pressure * weightedTestGradient;
const double residualContribution = pressure * weightedTestGradient;
if (!std::isfinite(weightedTestGradient) || !std::isfinite(residualContribution)) {
materialFailure = non_finite_rejection();
continue;
}
data.elementResidual(vectorDof) -= residualContribution;
for (int enthalpyDof = 0; enthalpyDof < enthalpyDofCount; ++enthalpyDof) {
data.enthalpyJacobian(vectorDof, enthalpyDof) -=
pressureDerivative * weightedTestGradient *
data.enthalpyBasis(quadraturePoint, enthalpyDof);
const double jacobianContribution =
pressureDerivative * weightedTestGradient * enthalpyBasis(quadraturePoint, enthalpyDof);
if (!std::isfinite(jacobianContribution)) {
materialFailure = non_finite_rejection();
continue;
}
data.enthalpyJacobian(vectorDof, enthalpyDof) -= jacobianContribution;
}
}
}
}
}
++m_enthalpyJacobianStatistics.preparations;
if (!vector_is_finite(data.elementResidual) || !matrix_is_finite(data.enthalpyJacobian)) {
materialFailure = non_finite_rejection();
}
}
return materialFailure;
}
void PreparedPressureForceOperator::FinalizeDisplacementJacobianPreparation() {
@@ -682,8 +824,10 @@ namespace mean_field::operators {
MFEM_VERIFY(
data.baseDisplacementData.has_value() && data.compactificationData.has_value() &&
static_cast<int>(data.baseMappingContexts.size()) == quadraturePointCount &&
static_cast<int>(data.referenceTestGradients.size()) == quadraturePointCount &&
data.baseMappingContexts.GetPointCount() == quadraturePointCount &&
data.baseMappingContexts.GetDimension() == dimension &&
data.displacementReferenceTable != nullptr &&
data.displacementReferenceTable->GetPointCount() == quadraturePointCount &&
static_cast<int>(data.physicalTestGradients.size()) == quadraturePointCount &&
data.quadratureWeights.Size() == quadraturePointCount &&
data.pressure.Size() == quadraturePointCount,
@@ -693,18 +837,16 @@ namespace mean_field::operators {
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
MFEM_VERIFY(
data.referenceTestGradients[quadraturePoint].Width() == dimension &&
data.displacementReferenceTable->GetGradients(quadraturePoint).Width() == dimension &&
data.physicalTestGradients[quadraturePoint].Width() == dimension,
"Prepared pressure-force displacement Jacobian has "
"a gradient with the wrong dimension."
);
}
}
++m_displacementJacobianStatistics.preparations;
}
void PreparedPressureForceOperator::AssembleCachedResidual() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::AssembleCachedResidual() {
mfem::Vector localResidual(m_fem.displacementFes->GetVSize());
localResidual = 0.0;
@@ -729,6 +871,10 @@ namespace mean_field::operators {
* FieldDofMap::gather does not resize its destination.
*/
m_displacementMap.gather(m_fullDisplacementAction, m_cachedResidual);
if (!vector_is_finite(m_fullDisplacementAction) || !vector_is_finite(m_cachedResidual)) {
return non_finite_rejection();
}
return std::nullopt;
}
void PreparedPressureForceOperator::BuildResidual(mfem::Vector &residual) const {
@@ -816,15 +962,13 @@ namespace mean_field::operators {
mfem::Vector elementDisplacementVariation;
mfem::Vector elementAction;
mfem::DenseMatrix referenceDisplacementDShape;
mfem::DenseMatrix referenceDisplacementJacobian;
mfem::DenseMatrix inverseElementJacobian;
mfem::DenseMatrix inverseElementJacobianVariation;
mfem::DenseMatrix matrixTemporary;
mfem::DenseMatrix physicalTestGradientVariation;
const int dimension = m_fem.mesh->Dimension();
const mfem::Ordering::Type displacementOrdering = m_fem.displacementFes->GetOrdering();
const int dimension = m_fem.mesh->Dimension();
for (const ElementPAData &data : m_elements) {
MFEM_VERIFY(
@@ -849,17 +993,16 @@ namespace mean_field::operators {
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
const mapping::ElementDisplacementData directionData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacementVariation);
const int quadraturePointCount = data.integrationRule->GetNPoints();
const int quadraturePointCount = data.integrationRule->GetNPoints();
const int scalarDisplacementDofCount = displacementElement.GetDof();
const int scalarDisplacementDofCount = displacementElement.GetDof();
const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix();
const mfem::DenseMatrix directionDofs(
elementDisplacementVariation.GetData(), scalarDisplacementDofCount, dimension
);
MFEM_VERIFY(
static_cast<int>(data.baseMappingContexts.size()) == quadraturePointCount &&
data.baseMappingContexts.GetPointCount() == quadraturePointCount &&
data.pressure.Size() == quadraturePointCount,
"Prepared pressure-force displacement Jacobian has "
"stale quadrature data."
@@ -869,20 +1012,18 @@ namespace mean_field::operators {
elementAction = 0.0;
referenceDisplacementDShape.SetSize(scalarDisplacementDofCount, dimension);
referenceDisplacementJacobian.SetSize(dimension, dimension);
inverseElementJacobianVariation.SetSize(dimension, dimension);
matrixTemporary.SetSize(dimension, dimension);
physicalTestGradientVariation.SetSize(scalarDisplacementDofCount, dimension);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
const mfem::DenseMatrix &referenceTestGradient =
data.displacementReferenceTable->GetGradients(quadraturePoint);
displacementElement.CalcDShape(integrationPoint, referenceDisplacementDShape);
mfem::MultAtB(directionDofs, referenceDisplacementDShape, referenceDisplacementJacobian);
mfem::MultAtB(directionDofs, referenceTestGradient, referenceDisplacementJacobian);
const mfem::DenseMatrix &inverseElementJacobian =
data.baseMappingContexts[quadraturePoint].quadrature.J_inv;
data.baseMappingContexts.LoadInverseJacobian(quadraturePoint, inverseElementJacobian);
mfem::Mult(inverseElementJacobian, referenceDisplacementJacobian, matrixTemporary);
double logarithmicJacobianVariation{0.0};
@@ -893,39 +1034,35 @@ namespace mean_field::operators {
mfem::Mult(matrixTemporary, inverseElementJacobian, inverseElementJacobianVariation);
inverseElementJacobianVariation *= -1.0;
mfem::Mult(
data.referenceTestGradients[quadraturePoint], inverseElementJacobianVariation,
physicalTestGradientVariation
);
mfem::Mult(referenceTestGradient, inverseElementJacobianVariation, physicalTestGradientVariation);
const mfem::DenseMatrix &physicalTestGradient = data.physicalTestGradients[quadraturePoint];
const double quadratureWeight = data.quadratureWeights(quadraturePoint);
const double pressure = data.pressure(quadraturePoint);
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
);
for (int component = 0; component < dimension; ++component) {
const double *variationColumn =
physicalTestGradientVariation.GetData() + component * scalarDisplacementDofCount;
const double *physicalColumn =
physicalTestGradient.GetData() + component * scalarDisplacementDofCount;
double *actionColumn = elementAction.GetData() + component * scalarDisplacementDofCount;
const double gradientWeightVariation = data.quadratureWeights(quadraturePoint) *
physicalTestGradientVariation(scalarDof, component) +
data.quadratureWeights(quadraturePoint) *
logarithmicJacobianVariation *
physicalTestGradient(scalarDof, component);
for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) {
const double gradientWeightVariation =
quadratureWeight * variationColumn[scalarDof] +
quadratureWeight * logarithmicJacobianVariation * physicalColumn[scalarDof];
const double contribution = pressure * gradientWeightVariation;
const double contribution = data.pressure(quadraturePoint) * gradientWeightVariation;
MFEM_VERIFY(
std::isfinite(gradientWeightVariation) && std::isfinite(contribution),
"Prepared pressure-force displacement "
"Jacobian encountered a non-finite "
"contribution."
);
elementAction(vectorDof) -= contribution;
actionColumn[scalarDof] -= contribution;
}
}
}
MFEM_VERIFY(
vector_is_finite(elementAction),
"Prepared pressure-force displacement Jacobian encountered a non-finite element action."
);
if (data.displacementDofTransformation != nullptr) {
data.displacementDofTransformation->TransformDual(elementAction);
}

View File

@@ -1,7 +1,14 @@
module;
#include <array>
#include <cmath>
#include <expected>
#include <mfem.hpp>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mpi.h>
module mean_field;
@@ -10,11 +17,76 @@ 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,
@@ -119,7 +191,10 @@ namespace mean_field::operators {
);
}
void PreparedRotationalDisplacementForceOperator::PrepareElementData() {
std::expected<
void,
kernels::RotationalDisplacementForceRejection>
PreparedRotationalDisplacementForceOperator::TryPrepareElementData() {
MFEM_VERIFY(m_rotation.has_value(), "Prepared rotational force has no frozen rotation state.");
m_elements.clear();
@@ -197,9 +272,12 @@ namespace mean_field::operators {
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(
status == mapping::MappingStatus::valid && !mappingContext.mapping.compactified,
"Prepared rotational force encountered an invalid stellar mapping."
!mappingContext.mapping.compactified,
"Prepared rotational force encountered compactification on a stellar element."
);
densityElement.CalcShape(integrationPoint, densityShape);
@@ -214,8 +292,21 @@ namespace mean_field::operators {
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(
@@ -223,13 +314,29 @@ namespace mean_field::operators {
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()) {
if (!report.contextReport.DidAnyWork() && wasPrepared) {
return report;
}
@@ -245,14 +352,28 @@ namespace mean_field::operators {
"rotation state."
);
if (report.contextReport.preparedBaseState) {
kernels::apply_rotational_displacement_force_residual(
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);
PrepareElementData();
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;

View File

@@ -3,6 +3,8 @@ module;
#include <array>
#include <cmath>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <utility>
#include <mfem.hpp>
@@ -24,6 +26,133 @@ namespace {
using StellarRootForm = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form;
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection with_preparation_stage(
mean_field::operators::StellarEquilibriumPreparationRejection rejection,
const mean_field::operators::StellarEquilibriumPreparationStage stage
) noexcept {
rejection.stage = stage;
return rejection;
}
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection
make_thermodynamic_rejection(const mean_field::eos::EvaluationErrorCode code) {
using Failure = mean_field::operators::StellarEquilibriumPreparationRejection;
using Reason = mean_field::operators::StellarEquilibriumPreparationRejectionReason;
switch (code) {
case mean_field::eos::EvaluationErrorCode::outside_domain:
return Failure{.reason = Reason::thermodynamic_domain, .thermodynamicErrorCode = code};
case mean_field::eos::EvaluationErrorCode::nonfinite_input:
case mean_field::eos::EvaluationErrorCode::nonfinite_result:
return Failure{.reason = Reason::non_finite_thermodynamics, .thermodynamicErrorCode = code};
default:
throw std::logic_error(
"A non-retryable equation-of-state error was incorrectly returned as a stellar trial rejection."
);
}
}
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection
make_mapping_rejection(const mean_field::mapping::MappingStatus status) {
using Failure = mean_field::operators::StellarEquilibriumPreparationRejection;
using Reason = mean_field::operators::StellarEquilibriumPreparationRejectionReason;
return Failure{
.reason = status == mean_field::mapping::MappingStatus::non_positive_determinant
? Reason::inverted_geometry
: Reason::non_finite_geometry
};
}
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection make_gravity_rejection(
const mean_field::operators::context::gravity_field::GravityFieldPreparationRejection &rejection
) {
using ChildReason = mean_field::operators::context::gravity_field::GravityFieldPreparationRejectionReason;
if (rejection.reason == ChildReason::invalid_mapping) {
return make_mapping_rejection(rejection.mappingStatus);
}
return {.reason = mean_field::operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics};
}
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection
make_barotropic_rejection(const mean_field::operators::BarotropicClosurePreparationRejection &rejection) {
using ChildReason = mean_field::operators::BarotropicClosurePreparationRejectionReason;
switch (rejection.reason) {
case ChildReason::mapping_failure:
return make_mapping_rejection(rejection.mappingStatus);
case ChildReason::equation_of_state:
return make_thermodynamic_rejection(rejection.equationOfStateError);
case ChildReason::invalid_quadrature_data:
return {.reason = mean_field::operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics};
}
throw std::logic_error("An unknown barotropic trial rejection reached the stellar root.");
}
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection
make_displacement_rejection(const mean_field::operators::DisplacementResidualPreparationRejection &rejection) {
using ChildReason = mean_field::operators::DisplacementResidualPreparationRejectionReason;
using ChildSource = mean_field::operators::DisplacementResidualPreparationRejectionSource;
using RootStage = mean_field::operators::StellarEquilibriumPreparationStage;
const RootStage stage = [&] {
switch (rejection.source) {
case ChildSource::pressure:
return RootStage::pressure_force;
case ChildSource::gravity:
return RootStage::gravity_displacement_force;
case ChildSource::rotation:
return RootStage::rotational_displacement_force;
case ChildSource::composition:
return RootStage::displacement_composition;
}
return RootStage::displacement_residual;
}();
switch (rejection.reason) {
case ChildReason::equation_of_state: {
auto rootRejection = make_thermodynamic_rejection(rejection.equationOfStateCode);
rootRejection.stage = stage;
return rootRejection;
}
case ChildReason::invalid_mapping: {
auto rootRejection = make_mapping_rejection(rejection.mappingStatus);
rootRejection.stage = stage;
return rootRejection;
}
case ChildReason::non_finite_arithmetic:
return {
.reason = mean_field::operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics,
.stage = stage
};
}
throw std::logic_error("An unknown displacement trial rejection reached the stellar root.");
}
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection
make_mass_rejection(const mean_field::operators::MassNormalizationPreparationRejection &rejection) {
using ChildReason = mean_field::operators::MassNormalizationPreparationRejectionReason;
if (rejection.reason == ChildReason::mapping_failure) {
return make_mapping_rejection(rejection.mappingStatus);
}
return {.reason = mean_field::operators::StellarEquilibriumPreparationRejectionReason::non_finite_physics};
}
[[nodiscard]] mean_field::operators::StellarEquilibriumPreparationRejection
make_hydrostatic_rejection(const mean_field::operators::HydrostaticEquilibriumPreparationRejection &rejection) {
using ChildReason = mean_field::operators::HydrostaticEquilibriumPreparationRejectionReason;
using Failure = mean_field::operators::StellarEquilibriumPreparationRejection;
using Reason = mean_field::operators::StellarEquilibriumPreparationRejectionReason;
switch (rejection.reason) {
case ChildReason::inverted_geometry:
return Failure{.reason = Reason::inverted_geometry};
case ChildReason::non_finite_geometry:
return Failure{.reason = Reason::non_finite_geometry};
case ChildReason::non_finite_residual:
return Failure{.reason = Reason::non_finite_physics};
}
throw std::logic_error("An unknown hydrostatic trial rejection reached the stellar root.");
}
[[nodiscard]] std::array<
int,
StellarRootForm::value_block_count>
@@ -132,6 +261,15 @@ namespace {
}
}
[[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;
}
void validate_dependency_transition(
const mean_field::operators::StellarEquilibriumDependencyStamp &prepared,
const mean_field::operators::StellarEquilibriumDependencyStamp &requested,
@@ -358,11 +496,12 @@ namespace mean_field::operators {
constructionData.residualSizes,
StellarEquilibriumSpecificationModel{
equationOfState,
surface::Isobaric{
dimensions::PressureValue{surfaceConstraint.descriptor().targetPressure}},
fixedMassConstraint.specification()},
surface::Isobaric{dimensions::PressureValue{surfaceConstraint.descriptor().targetPressure}},
fixedMassConstraint.specification()
},
constructionData.pressureSurfaceRows.size()
),
m_communicator(f.mesh->GetComm()),
m_gravityStateOffsets(constructionData.gravityStateOffsets),
m_gravityContext(
f,
@@ -452,11 +591,38 @@ namespace mean_field::operators {
const mfem::Vector &state,
const StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
auto result = TryPrepare(state, dependencies, rotation);
if (!result.has_value()) {
throwStellarEquilibriumPreparationRejection(result.error());
}
return std::move(result).value();
}
StellarEquilibriumPreparationResult<PreparedStellarEquilibriumReport>
PreparedStellarEquilibriumOperator::TryPrepare(
const mfem::Vector &state,
const StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
MFEM_VERIFY(
state.Size() == Width(), "PreparedStellarEquilibriumOperator received a state with the wrong size."
);
validate_finite_vector(state, "PreparedStellarEquilibriumOperator received a non-finite state.");
const int localStateIsFinite = vector_is_finite(state) ? 1 : 0;
int globalStateIsFinite = 0;
if (MPI_Allreduce(&localStateIsFinite, &globalStateIsFinite, 1, MPI_INT, MPI_MIN, m_communicator) !=
MPI_SUCCESS) {
throw std::runtime_error("PreparedStellarEquilibriumOperator could not synchronize state validity.");
}
if (globalStateIsFinite == 0) {
m_isPrepared = false;
return std::unexpected(
StellarEquilibriumPreparationRejection{
.reason = StellarEquilibriumPreparationRejectionReason::non_finite_physics
}
);
}
const bool wasPrepared = m_isPrepared;
if (wasPrepared) {
@@ -495,9 +661,9 @@ namespace mean_field::operators {
);
}
m_isPrepared = false;
m_isPrepared = false;
const auto rootState = m_rootManifest.stateView(state);
const auto rootState = m_rootManifest.stateView(state);
const auto reducedDensity = rootState.block(utils::blocks::density_field.mass_term);
const auto surfaceDeformationParameters =
@@ -505,8 +671,7 @@ namespace mean_field::operators {
const auto gravityGradient = rootState.block(utils::blocks::gravity_field.gradient_term);
const auto gravityPotential = rootState.block(utils::blocks::gravity_field.poisson_term);
const auto reducedEnthalpy = rootState.block(utils::blocks::enthalpy_field.specific_term);
const auto bernoulli =
rootState.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term);
const auto bernoulli = rootState.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term);
const bool generatedGeometryChanged =
!wasPrepared || dependencies.discretization != m_preparedDependencies.discretization ||
@@ -515,9 +680,28 @@ namespace mean_field::operators {
PreparedStellarEquilibriumReport report;
if (generatedGeometryChanged) {
m_surfaceDeformationParameters = surfaceDeformationParameters;
m_generatedGeometryReport = m_domainDeformation.buildValidatedVolumeDisplacement(
m_surfaceDeformationParameters, m_generatedVolumeDisplacement
);
m_domainDeformation.buildVolumeDisplacement(m_surfaceDeformationParameters, m_generatedVolumeDisplacement);
const deformation::DomainDeformationGeometryReport generatedGeometry =
m_domainDeformation.inspectMappedGeometry(m_generatedVolumeDisplacement);
if (!std::isfinite(generatedGeometry.minimumJacobianDeterminant)) {
return std::unexpected(
StellarEquilibriumPreparationRejection{
.reason = StellarEquilibriumPreparationRejectionReason::non_finite_geometry,
.stage = StellarEquilibriumPreparationStage::generated_geometry,
.minimumJacobianDeterminant = generatedGeometry.minimumJacobianDeterminant
}
);
}
if (!generatedGeometry.isOrientationPreserving()) {
return std::unexpected(
StellarEquilibriumPreparationRejection{
.reason = StellarEquilibriumPreparationRejectionReason::inverted_geometry,
.stage = StellarEquilibriumPreparationStage::generated_geometry,
.minimumJacobianDeterminant = generatedGeometry.minimumJacobianDeterminant
}
);
}
m_generatedGeometryReport = generatedGeometry;
++m_generatedDisplacementDependency.revision;
++m_statistics.generatedGeometryBuilds;
report.generatedVolumeDisplacement = true;
@@ -530,31 +714,68 @@ namespace mean_field::operators {
gravityPotential
);
report.gravity = m_gravityOperator.Prepare(
auto gravityResult = m_gravityOperator.TryPrepare(
m_gravityState, make_gravity_revisions(dependencies, m_generatedDisplacementDependency)
);
if (!gravityResult.has_value()) {
return std::unexpected(with_preparation_stage(
make_gravity_rejection(gravityResult.error()), StellarEquilibriumPreparationStage::gravity
));
}
report.gravity = std::move(gravityResult).value();
report.barotropicClosure = m_barotropicClosureOperator.Prepare(
/*
* Mechanical-force preparation consumes the shared gravity context,
* but it is independent of the closure and hydrostatic rows. Prepare
* it as soon as that dependency is ready so a mapped-force rejection
* does not pay for unrelated candidate rows first.
*/
auto displacementResult = m_displacementOperator.TryPrepare(
{.enthalpy = reducedEnthalpy},
make_displacement_dependencies(dependencies, m_generatedDisplacementDependency), rotation
);
if (!displacementResult.has_value()) {
return std::unexpected(make_displacement_rejection(displacementResult.error()));
}
report.displacement = std::move(displacementResult).value();
auto barotropicClosureResult = m_barotropicClosureOperator.TryPrepare(
{.density = reducedDensity, .enthalpy = reducedEnthalpy, .displacement = m_generatedVolumeDisplacement},
make_barotropic_closure_dependencies(dependencies, m_generatedDisplacementDependency)
);
if (!barotropicClosureResult.has_value()) {
return std::unexpected(with_preparation_stage(
make_barotropic_rejection(barotropicClosureResult.error()),
StellarEquilibriumPreparationStage::barotropic_closure
));
}
report.barotropicClosure = std::move(barotropicClosureResult).value();
report.hydrostatic = m_hydrostaticOperator.Prepare(
auto hydrostaticResult = m_hydrostaticOperator.TryPrepare(
{.enthalpy = reducedEnthalpy,
.gravityPotential = gravityPotential,
.displacement = m_generatedVolumeDisplacement,
.bernoulliConstant = bernoulli(0)},
make_hydrostatic_dependencies(dependencies, m_generatedDisplacementDependency), rotation
);
if (!hydrostaticResult.has_value()) {
return std::unexpected(with_preparation_stage(
make_hydrostatic_rejection(hydrostaticResult.error()),
StellarEquilibriumPreparationStage::hydrostatic_equilibrium
));
}
report.hydrostatic = std::move(hydrostaticResult).value();
report.displacement = m_displacementOperator.Prepare(
{.enthalpy = reducedEnthalpy},
make_displacement_dependencies(dependencies, m_generatedDisplacementDependency), rotation
);
report.massNormalization = m_massNormalizationOperator.Prepare(
auto massNormalizationResult = m_massNormalizationOperator.TryPrepare(
m_fixedMassConstraint, make_mass_dependencies(dependencies, m_generatedDisplacementDependency)
);
if (!massNormalizationResult.has_value()) {
return std::unexpected(with_preparation_stage(
make_mass_rejection(massNormalizationResult.error()),
StellarEquilibriumPreparationStage::mass_normalization
));
}
report.massNormalization = std::move(massNormalizationResult).value();
report.surfaceConstraint = m_surfaceConstraintOperator.Prepare(
reducedEnthalpy, !wasPrepared || dependencies.enthalpy != m_preparedDependencies.enthalpy
@@ -636,7 +857,7 @@ namespace mean_field::operators {
direction, "PreparedStellarEquilibriumOperator received a non-finite Jacobian direction."
);
const auto rootDirection = m_rootManifest.directionView(direction);
const auto rootDirection = m_rootManifest.directionView(direction);
const auto reducedDensityDirection = rootDirection.block(utils::blocks::density_field.mass_term);
const auto surfaceDeformationDirection =
@@ -803,13 +1024,9 @@ namespace mean_field::operators {
const mfem::Vector &densityDirection
) const {
VerifyPrepared();
m_massNormalizationOperator.ApplyDensityJacobianAction(
densityDirection,
m_densityVolumeIntegralAction
);
m_massNormalizationOperator.ApplyDensityJacobianAction(densityDirection, m_densityVolumeIntegralAction);
MFEM_VERIFY(
m_densityVolumeIntegralAction.Size() == 1,
"The density-volume integral must produce one global scalar."
m_densityVolumeIntegralAction.Size() == 1, "The density-volume integral must produce one global scalar."
);
return m_densityVolumeIntegralAction(0);
}
@@ -819,13 +1036,10 @@ namespace mean_field::operators {
) const {
VerifyPrepared();
m_domainDeformation.applyJacobian(
m_surfaceDeformationParameters,
surfaceShapeDirection,
m_volumeDisplacementDirection
m_surfaceDeformationParameters, surfaceShapeDirection, m_volumeDisplacementDirection
);
m_massNormalizationOperator.ApplyDisplacementJacobianAction(
m_volumeDisplacementDirection,
m_densityVolumeIntegralAction
m_volumeDisplacementDirection, m_densityVolumeIntegralAction
);
MFEM_VERIFY(
m_densityVolumeIntegralAction.Size() == 1,
@@ -854,6 +1068,29 @@ namespace mean_field::operators {
return m_generatedVolumeDisplacement;
}
void PreparedStellarEquilibriumOperator::BuildVolumeDisplacementDirection(
const mfem::Vector &surfaceDeformationDirection,
mfem::Vector &volumeDisplacementDirection
) const {
VerifyPrepared();
MFEM_VERIFY(
surfaceDeformationDirection.Size() == m_domainDeformation.parameterCount(),
"PreparedStellarEquilibriumOperator received a surface-deformation direction with the wrong size."
);
validate_finite_vector(
surfaceDeformationDirection,
"PreparedStellarEquilibriumOperator received a non-finite surface-deformation direction."
);
MFEM_VERIFY(
volumeDisplacementDirection.Size() == m_domainDeformation.volumeDisplacementSize(),
"PreparedStellarEquilibriumOperator received a volume-displacement workspace with the wrong size."
);
m_domainDeformation.applyJacobian(
m_surfaceDeformationParameters, surfaceDeformationDirection, volumeDisplacementDirection
);
}
const mfem::Vector &PreparedStellarEquilibriumOperator::GetFullMechanicalResidual() const {
VerifyPrepared();
return m_fullMechanicalResidual;

View File

@@ -182,12 +182,11 @@ namespace mean_field::seed::detail {
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 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);
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) {
@@ -208,11 +207,11 @@ namespace mean_field::seed::detail {
);
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,
.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
};
}

View File

@@ -0,0 +1,82 @@
module;
#include <cstdint>
#include <limits>
#include <span>
#include <mfem.hpp>
export module mean_field:deformation.safe_newton_step;
export import :mapping.domain_mapper;
export namespace mean_field::deformation {
/*
* One element-local quadrature rule at which a downstream operator will
* evaluate the mapped geometry. A caller may provide several entries for
* one element when several operators use different, non-nested rules.
* The integration-rule object must outlive the call.
*/
struct NewtonStepGeometryRule final {
int element{-1};
const mfem::IntegrationRule *integrationRule{nullptr};
};
struct LargestSafeNewtonStepSizeOptions final {
double maximumStepSize{1.0};
double determinantFloor{0.0};
double fractionToBoundarySafety{0.9};
void Validate() const;
};
/*
* The estimated boundary is the first alpha in [0, maximumStepSize] at
* which any sampled mapping determinant reaches determinantFloor. When
* no such point exists, boundaryStepSize equals maximumStepSize and
* limitedByGeometry is false. stepSize is the boundary multiplied by the
* safety fraction only when geometry is limiting.
*
* Element and rule indices are local to limitingRank. limitingRule is an
* index into that rank's input span.
*/
struct LargestSafeNewtonStepSizeEstimate final {
double stepSize{0.0};
double boundaryStepSize{0.0};
double minimumDeterminantAtAcceptedState{std::numeric_limits<double>::quiet_NaN()};
double minimumDeterminantAtMaximumStepSize{std::numeric_limits<double>::quiet_NaN()};
double limitingPointDeterminantAtStepSize{std::numeric_limits<double>::quiet_NaN()};
std::uint64_t sampledQuadraturePointCount{0};
bool limitedByGeometry{false};
int limitingRank{-1};
int limitingElement{-1};
int limitingRule{-1};
int limitingQuadraturePoint{-1};
};
/*
* Estimate the largest safe alpha for
*
* displacement(alpha) = acceptedVolumeDisplacement
* + alpha * volumeNewtonDirection.
*
* Both vectors use the displacement space's true-DOF layout. The
* compactification coordinate is held fixed. The result is collective on
* displacementSpace.GetComm() and is identical on every rank.
*
* The calculation is exact for the current domain mapper: its mapping
* Jacobian is affine along a displacement direction, so each sampled
* determinant is a polynomial of degree at most the spatial dimension.
* Compactified elements require an exterior map that explicitly advertises
* the same affine contract.
*/
[[nodiscard]] 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,
std::span<const NewtonStepGeometryRule> geometryRules,
const LargestSafeNewtonStepSizeOptions &options = {}
);
} // namespace mean_field::deformation

View File

@@ -6,130 +6,124 @@ module;
#include <type_traits>
#include <utility>
#include <mpi.h>
export module mean_field:equilibrium.stellar_discretization;
export import :fem;
export import :mapping.domain_mapper;
export import :normalization.physical_riesz;
namespace mean_field::equilibrium::detail {
struct StellarEquilibriumProblemFactory;
}
export namespace mean_field::equilibrium {
/*
* An explicit, non-owning view of the numerical discretization used by a
* stellar equilibrium problem. The referenced FEM and mapper must outlive
* every problem and structure that uses this view.
*
* Ownership cannot move here yet because FEM currently also contains
* mutable field workspaces. Separating those workspaces is a prerequisite
* for shared discretization ownership by solved Structure objects.
* The complete numerical discretization used by a stellar equilibrium
* problem. Moving the FEM into stable heap storage lets the problem and a
* completed Structure transfer unique ownership without invalidating the
* references retained by prepared operators. The mapper is part of that
* owned FEM and therefore has the same lifetime.
*/
template <normalization::NormalizationPrescription Normalization>
class StellarDiscretizationFor final {
template <normalization::NormalizationPrescription Normalization> class StellarDiscretizationFor final {
public:
using NormalizationPrescriptionType = std::remove_cvref_t<Normalization>;
explicit StellarDiscretizationFor(fem::FEM &finiteElementModel)
requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized>
explicit StellarDiscretizationFor(fem::FEM &&finiteElementModel)
requires std::same_as<
NormalizationPrescriptionType,
normalization::Unnormalized>
: StellarDiscretizationFor(
finiteElementModel,
RequireDomainMapper(finiteElementModel),
std::move(finiteElementModel),
normalization::Unnormalized{}
) {
}
StellarDiscretizationFor(
fem::FEM &finiteElementModel,
const mapping::DomainMapper &domainMapper
)
requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized>
: StellarDiscretizationFor(
finiteElementModel,
domainMapper,
normalization::Unnormalized{}
) {
}
explicit StellarDiscretizationFor(fem::FEM &)
requires std::same_as<
NormalizationPrescriptionType,
normalization::Unnormalized>
= delete;
StellarDiscretizationFor(
fem::FEM &,
mapping::DomainMapper &&
) requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized> = delete;
StellarDiscretizationFor(
fem::FEM &,
const mapping::DomainMapper &&
) requires std::same_as<NormalizationPrescriptionType, normalization::Unnormalized> = delete;
StellarDiscretizationFor(
fem::FEM &finiteElementModel,
fem::FEM &&finiteElementModel,
NormalizationPrescriptionType normalizationPrescription
)
: StellarDiscretizationFor(
finiteElementModel,
RequireDomainMapper(finiteElementModel),
std::move(normalizationPrescription)
) {
}
StellarDiscretizationFor(
fem::FEM &finiteElementModel,
const mapping::DomainMapper &domainMapper,
NormalizationPrescriptionType normalizationPrescription
)
: m_finiteElementModel(std::addressof(finiteElementModel)),
m_domainMapper(std::addressof(domainMapper)),
: m_finiteElementModel(TakeOwnership(std::move(finiteElementModel))),
m_normalizationPrescription(std::move(normalizationPrescription)) {
if (!finiteElementModel.okay()) {
throw std::invalid_argument("A stellar discretization requires a complete finite-element model.");
}
}
StellarDiscretizationFor(
fem::FEM &,
mapping::DomainMapper &&,
NormalizationPrescriptionType
) = delete;
) = delete;
StellarDiscretizationFor(
fem::FEM &,
const mapping::DomainMapper &&,
NormalizationPrescriptionType
) = delete;
StellarDiscretizationFor(const StellarDiscretizationFor &) = delete;
StellarDiscretizationFor &operator=(const StellarDiscretizationFor &) = delete;
StellarDiscretizationFor(StellarDiscretizationFor &&) = default;
StellarDiscretizationFor &operator=(StellarDiscretizationFor &&) = delete;
[[nodiscard]] fem::FEM &finiteElementModel() const noexcept {
return *m_finiteElementModel;
}
[[nodiscard]] const mapping::DomainMapper &domainMapper() const noexcept {
return *m_domainMapper;
}
[[nodiscard]] const NormalizationPrescriptionType &normalizationPrescription() const noexcept {
return m_normalizationPrescription;
}
[[nodiscard]] bool isCurrent() const noexcept {
return m_finiteElementModel != nullptr && m_domainMapper != nullptr && m_finiteElementModel->okay();
}
private:
[[nodiscard]] static const mapping::DomainMapper &RequireDomainMapper(const fem::FEM &finiteElementModel) {
[[nodiscard]] const mapping::DomainMapper &domainMapper() const & {
const auto &finiteElementModel = RequireFiniteElementModel();
if (finiteElementModel.domainMapperStateless == nullptr) {
throw std::invalid_argument("A stellar discretization requires a domain mapper.");
throw std::logic_error("The stellar discretization has no domain mapper.");
}
return *finiteElementModel.domainMapperStateless;
}
fem::FEM *m_finiteElementModel;
const mapping::DomainMapper *m_domainMapper;
[[nodiscard]] const mapping::DomainMapper &domainMapper() const && = delete;
[[nodiscard]] MPI_Comm communicator() const & {
const auto &finiteElementModel = RequireFiniteElementModel();
if (finiteElementModel.mesh == nullptr) {
throw std::logic_error("The stellar discretization has no parallel mesh.");
}
return finiteElementModel.mesh->GetComm();
}
[[nodiscard]] MPI_Comm communicator() const && = delete;
[[nodiscard]] const NormalizationPrescriptionType &normalizationPrescription() const & noexcept {
return m_normalizationPrescription;
}
[[nodiscard]] const NormalizationPrescriptionType &normalizationPrescription() const && = delete;
[[nodiscard]] bool isCurrent() const noexcept {
return m_finiteElementModel != nullptr && m_finiteElementModel->okay();
}
private:
friend struct detail::StellarEquilibriumProblemFactory;
[[nodiscard]] fem::FEM &MutableFiniteElementModelForAssembly() & {
return const_cast<fem::FEM &>(RequireFiniteElementModel());
}
[[nodiscard]] const fem::FEM &RequireFiniteElementModel() const {
if (m_finiteElementModel == nullptr) {
throw std::logic_error("A moved-from stellar discretization has no finite-element model.");
}
return *m_finiteElementModel;
}
[[nodiscard]] static std::unique_ptr<fem::FEM> TakeOwnership(fem::FEM &&finiteElementModel) {
if (!finiteElementModel.okay()) {
throw std::invalid_argument("A stellar discretization requires a complete finite-element model.");
}
return std::make_unique<fem::FEM>(std::move(finiteElementModel));
}
std::unique_ptr<fem::FEM> m_finiteElementModel;
NormalizationPrescriptionType m_normalizationPrescription;
};
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor(fem::FEM &, Normalization)
-> StellarDiscretizationFor<std::remove_cvref_t<Normalization>>;
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor(fem::FEM &, const mapping::DomainMapper &, Normalization)
-> StellarDiscretizationFor<std::remove_cvref_t<Normalization>>;
StellarDiscretizationFor(
fem::FEM &&,
Normalization
) -> StellarDiscretizationFor<std::remove_cvref_t<Normalization>>;
using StellarDiscretization = StellarDiscretizationFor<normalization::Unnormalized>;
@@ -143,41 +137,17 @@ export namespace mean_field::equilibrium {
template <normalization::NormalizationPrescription Normalization>
[[nodiscard]] auto makeStellarDiscretization(
fem::FEM &finiteElementModel,
fem::FEM &&finiteElementModel,
Normalization normalizationPrescription
) {
return StellarDiscretizationFor<std::remove_cvref_t<Normalization>>{
finiteElementModel,
std::move(normalizationPrescription)
std::move(finiteElementModel), std::move(normalizationPrescription)
};
}
template <normalization::NormalizationPrescription Normalization>
[[nodiscard]] auto makeStellarDiscretization(
fem::FEM &finiteElementModel,
const mapping::DomainMapper &domainMapper,
Normalization normalizationPrescription
) {
return StellarDiscretizationFor<std::remove_cvref_t<Normalization>>{
finiteElementModel,
domainMapper,
std::move(normalizationPrescription)
};
}
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor<std::remove_cvref_t<Normalization>>
makeStellarDiscretization(
StellarDiscretizationFor<std::remove_cvref_t<Normalization>> makeStellarDiscretization(
fem::FEM &,
mapping::DomainMapper &&,
Normalization
) = delete;
template <normalization::NormalizationPrescription Normalization>
StellarDiscretizationFor<std::remove_cvref_t<Normalization>>
makeStellarDiscretization(
fem::FEM &,
const mapping::DomainMapper &&,
Normalization
) = delete;
} // namespace mean_field::equilibrium

View File

@@ -14,6 +14,7 @@ export import :utils.misc;
export import :utils.user;
export import :quadrature.mfem;
export import :field.mfem;
export import :fem.reference_tables;
export namespace mean_field::fem {
using GravityField = field::Field<field::Gravity>;
@@ -145,6 +146,13 @@ export namespace mean_field::fem {
[[nodiscard]] bool has_mapping() const {
return domainMapperStateless != nullptr && displacement != nullptr && compactificationCoordinate != nullptr;
}
[[nodiscard]] const ReferenceTableCache &GetReferenceTables() const {
return *m_reference_tables;
}
private:
std::unique_ptr<ReferenceTableCache> m_reference_tables{std::make_unique<ReferenceTableCache>()};
};
FEM setup_fem(

View File

@@ -0,0 +1,78 @@
module;
#include <memory>
#include <vector>
#include <mfem.hpp>
export module mean_field:fem.reference_tables;
export namespace mean_field::fem {
// These tables contain only reference-element data: no mesh coordinates,
// orientation, element DOF transforms, or state-dependent mapping factors.
class ScalarReferenceTable {
public:
[[nodiscard]] const mfem::DenseMatrix &GetValues() const;
// Available for GRAD elements; otherwise throws std::out_of_range.
[[nodiscard]] const mfem::DenseMatrix &GetGradients(int point) const;
[[nodiscard]] int GetPointCount() const;
[[nodiscard]] int GetDofCount() const;
[[nodiscard]] int GetDimension() const;
private:
friend class ReferenceTableCache;
ScalarReferenceTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
);
mfem::DenseMatrix m_values;
std::vector<mfem::DenseMatrix> m_gradients;
int m_dimension;
};
class VectorReferenceTable {
public:
[[nodiscard]] const mfem::DenseMatrix &GetValues(int point) const;
[[nodiscard]] int GetPointCount() const;
[[nodiscard]] int GetDofCount() const;
[[nodiscard]] int GetDimension() const;
private:
friend class ReferenceTableCache;
VectorReferenceTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
);
std::vector<mfem::DenseMatrix> m_values;
int m_dof_count;
int m_dimension;
};
// Owned by one discretization, never process-global. Finite elements must
// remain alive and immutable while this cache is used; rebuild the cache if
// their collections are replaced. Rules are keyed by their actual points
// and weights, so temporary, copied, or modified rules are safe to use.
// Returned immutable handles also keep tables alive after cache destruction.
class ReferenceTableCache {
public:
ReferenceTableCache();
~ReferenceTableCache();
ReferenceTableCache(const ReferenceTableCache &) = delete;
ReferenceTableCache &operator=(const ReferenceTableCache &) = delete;
[[nodiscard]] std::shared_ptr<const ScalarReferenceTable> GetScalarTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
) const;
[[nodiscard]] std::shared_ptr<const VectorReferenceTable> GetVectorTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
) const;
private:
struct Storage;
std::unique_ptr<Storage> m_storage;
};
} // namespace mean_field::fem

View File

@@ -31,6 +31,16 @@ export namespace mean_field::mapping::compactification {
public:
virtual ~ExteriorDomainMap() = default;
/*
* Return true when, with the reference and compactification data held
* fixed, both outputs of Evaluate are affine functions of the
* displaced position and displacement Jacobian. This is the contract
* required by the exact determinant-polynomial geometry preflight.
*/
[[nodiscard]] virtual bool IsAffineInDisplacement() const noexcept {
return false;
}
[[nodiscard]] virtual MappingStatus Evaluate(
const ExteriorMapInput &input,
ExteriorMapResult &result

View File

@@ -12,6 +12,10 @@ export namespace mean_field::mapping::compactification {
public:
explicit KelvinCompactification(options::KelvinCompactificationOptions options);
[[nodiscard]] bool IsAffineInDisplacement() const noexcept override {
return true;
}
[[nodiscard]] MappingStatus Evaluate(
const ExteriorMapInput &input,
ExteriorMapResult &result
@@ -45,4 +49,4 @@ export namespace mean_field::mapping::compactification {
options::KelvinCompactificationOptions m_options;
};
} // namespace mean_field::mapping::compactification
} // namespace mean_field::mapping::compactification

View File

@@ -80,6 +80,7 @@ export namespace mean_field::mapping {
mfem::Vector m_shape;
mfem::DenseMatrix m_reference_dshape;
mfem::DenseMatrix m_mesh_dshape;
mfem::DenseMatrix m_reference_field_jacobian;
mfem::Vector m_field_value;
mfem::DenseMatrix m_field_jacobian;

View File

@@ -0,0 +1,42 @@
module;
#include <mfem.hpp>
#include <vector>
export module mean_field:mapping.prepared_cache;
import :mapping.types;
export namespace mean_field::mapping {
// Flat, owning storage for prepared volume contexts. Load into reusable
// workspaces: no MFEM buffers or pointer aliases are owned per quadrature
// point. The layout retains every public context field without recomputing
// inverses or changing the mapper's numerical contract.
class VolumeMappingCache {
public:
void SetSize(
int point_count,
int dimension
);
void Store(
int point,
const VolumeMappingContext &context
);
void Load(
int point,
VolumeMappingContext &context
) const;
void LoadInverseJacobian(
int point,
mfem::DenseMatrix &inverse
) const;
[[nodiscard]] int GetPointCount() const;
[[nodiscard]] int GetDimension() const;
private:
[[nodiscard]] const double *GetPointData(int point) const;
std::vector<double> m_data;
int m_point_count{0};
int m_dimension{0};
int m_point_stride{0};
};
} // namespace mean_field::mapping

View File

@@ -14,6 +14,7 @@ export import :mapping.compactification;
export import :mapping.kelvin;
export import :mapping.transformations;
export import :mapping.types;
export import :mapping.prepared_cache;
export import :mapping.compactification.options;
export import :integrators.advection;
export import :integrators.centrifugal;
@@ -25,7 +26,10 @@ export import :integrators.viscosity;
export import :quadrature.policy;
export import :quadrature.mfem;
export import :solver.fields;
export import :solver.linear_backend;
export import :solver.preconditioning_diagnostics;
export import :solver.stellar_equilibrium_types;
export import :solver.stellar_equilibrium;
export import :preconditioning;
export import :normalization;
export import :utils.blocks;
@@ -83,6 +87,7 @@ export import :deformation.interior_extension;
export import :deformation.vacuum_extension;
export import :deformation.radial_extensions;
export import :deformation.domain_deformation;
export import :deformation.safe_newton_step;
export import :model.stellar;
export import :operators.root_manifest;
export import :operators.prepared_constraint;

View File

@@ -27,8 +27,8 @@ export namespace mean_field::models {
class CompiledFixedAngularMomentum final {
public:
using SpecificationType = FixedAngularMomentum;
using LayoutRequest = FixedAngularMomentumLayoutRequest;
using SpecificationType = FixedAngularMomentum;
using LayoutRequest = FixedAngularMomentumLayoutRequest;
using AngularVelocityType = typename LayoutRequest::GeneratedValueType;
using ResidualType = typename LayoutRequest::GeneratedResidualType;
using AngularVelocityField = field::AngularVelocity;
@@ -63,9 +63,8 @@ export namespace mean_field::models {
FixedAngularMomentum m_specification;
};
[[nodiscard]] inline CompiledFixedAngularMomentum compileConstraint(
const FixedAngularMomentum specification
) noexcept {
[[nodiscard]] inline CompiledFixedAngularMomentum
compileConstraint(const FixedAngularMomentum specification) noexcept {
return CompiledFixedAngularMomentum{specification};
}

View File

@@ -57,11 +57,9 @@ export namespace mean_field::models {
static constexpr std::size_t size = sizeof...(Types);
};
template <typename... ValueBlocks>
using DependsOn = ModelTypeList<ValueBlocks...>;
template <typename... ValueBlocks> using DependsOn = ModelTypeList<ValueBlocks...>;
template <typename... ResidualBlocks>
using Affects = ModelTypeList<ResidualBlocks...>;
template <typename... ResidualBlocks> using Affects = ModelTypeList<ResidualBlocks...>;
/*
* Physics vocabulary for declaring how a stellar specification couples to
@@ -83,8 +81,7 @@ export namespace mean_field::models {
* the physics-facing spelling for coupled global constraints: an
* extension names the constraint it reads, never its solver block.
*/
template <typename Specification>
struct GeneratedCoordinateOf final {
template <typename Specification> struct GeneratedCoordinateOf final {
using SpecificationType = Specification;
};
@@ -108,8 +105,7 @@ export namespace mean_field::models {
struct OwnConstraint final { };
/* The scalar constraint equation owned by another specification. */
template <typename Specification>
struct ConstraintOf final {
template <typename Specification> struct ConstraintOf final {
using SpecificationType = Specification;
};
} // namespace equation
@@ -119,8 +115,7 @@ export namespace mean_field::models {
* Runtime providers consume this vocabulary without learning backend
* row and column block types.
*/
template <typename Equation, typename State>
struct Derivative final {
template <typename Equation, typename State> struct Derivative final {
using EquationType = Equation;
using StateType = State;
};
@@ -220,11 +215,7 @@ export namespace mean_field::models {
concept PhysicalScaleRepresentedQuantity =
dimensions::PhysicalQuantityType<Quantity> &&
physicalScaleForQuantity<Quantity> != PhysicalScaleLaw::unavailable &&
requires {
typename std::bool_constant<
!static_cast<std::string_view>(
Quantity::identifier).empty()>;
};
requires { typename std::bool_constant<!static_cast<std::string_view>(Quantity::identifier).empty()>; };
namespace detail {
template <typename Candidate> [[nodiscard]] consteval bool declaredCoordinateNormalizationIsAvailable() {
@@ -270,8 +261,7 @@ export namespace mean_field::models {
case SpecificationRole::boundary_condition:
return kind == GeneratedStateKind::none;
case SpecificationRole::invariant:
return kind == GeneratedStateKind::multiplier ||
kind == GeneratedStateKind::physical_coordinate;
return kind == GeneratedStateKind::multiplier || kind == GeneratedStateKind::physical_coordinate;
case SpecificationRole::phase_condition:
case SpecificationRole::gauge_choice:
return kind == GeneratedStateKind::solver_border;
@@ -304,11 +294,12 @@ export namespace mean_field::models {
using UnavailableCoordinateNormalization =
CoordinateNormalization<RieszTopology::unavailable, PhysicalScaleLaw::unavailable>;
template <typename ValueNormalization = UnavailableCoordinateNormalization,
typename ResidualNormalization = UnavailableCoordinateNormalization>
template <
typename ValueNormalization = UnavailableCoordinateNormalization,
typename ResidualNormalization = UnavailableCoordinateNormalization>
struct GeneratedNormalization final {
using Value = ValueNormalization;
using Residual = ResidualNormalization;
using Value = ValueNormalization;
using Residual = ResidualNormalization;
static constexpr bool available = detail::declaredCoordinateNormalizationIsAvailable<Value>() &&
detail::declaredCoordinateNormalizationIsAvailable<Residual>();
@@ -321,8 +312,13 @@ export namespace mean_field::models {
CoordinateNormalization<RieszTopology::global_scalar, ValueScale>,
CoordinateNormalization<RieszTopology::global_scalar, ResidualScale>>;
template <FixedString ValueStableId = "", FixedString ValueSymbol = "", FixedString ResidualStableId = "",
FixedString ResidualSymbol = "", FixedString TargetUnits = "", FixedString ResidualUnits = "">
template <
FixedString ValueStableId = "",
FixedString ValueSymbol = "",
FixedString ResidualStableId = "",
FixedString ResidualSymbol = "",
FixedString TargetUnits = "",
FixedString ResidualUnits = "">
struct GeneratedManifest final {
private:
inline static constexpr auto valueStableIdStorage = ValueStableId;
@@ -366,21 +362,19 @@ export namespace mean_field::models {
FixedString ResidualStableId,
FixedString ResidualSymbol>
struct DimensionalScalarConstraint final {
using TargetQuantity = TargetQuantityT;
using GeneratedCoordinateQuantity = GeneratedCoordinateQuantityT;
using ConstraintResidualQuantity = ConstraintResidualQuantityT;
using TargetValue = dimensions::QuantityValue<TargetQuantity>;
static constexpr PhysicalScaleLaw targetScale =
physicalScaleForQuantity<TargetQuantity>;
using TargetQuantity = TargetQuantityT;
using GeneratedCoordinateQuantity = GeneratedCoordinateQuantityT;
using ConstraintResidualQuantity = ConstraintResidualQuantityT;
using TargetValue = dimensions::QuantityValue<TargetQuantity>;
static constexpr PhysicalScaleLaw targetScale = physicalScaleForQuantity<TargetQuantity>;
struct Normalization final {
using TargetQuantity = TargetQuantityT;
using GeneratedCoordinateQuantity = GeneratedCoordinateQuantityT;
using ConstraintResidualQuantity = ConstraintResidualQuantityT;
using TargetValue = dimensions::QuantityValue<TargetQuantity>;
static constexpr PhysicalScaleLaw targetScale =
physicalScaleForQuantity<TargetQuantity>;
using Value = CoordinateNormalization<
using TargetQuantity = TargetQuantityT;
using GeneratedCoordinateQuantity = GeneratedCoordinateQuantityT;
using ConstraintResidualQuantity = ConstraintResidualQuantityT;
using TargetValue = dimensions::QuantityValue<TargetQuantity>;
static constexpr PhysicalScaleLaw targetScale = physicalScaleForQuantity<TargetQuantity>;
using Value = CoordinateNormalization<
RieszTopology::global_scalar,
physicalScaleForQuantity<GeneratedCoordinateQuantity>>;
using Residual = CoordinateNormalization<
@@ -408,20 +402,23 @@ export namespace mean_field::models {
static constexpr std::string_view residualSymbol = residualSymbolStorage.view();
static constexpr std::string_view targetUnits = TargetQuantity::identifier;
static constexpr std::string_view residualUnits = ConstraintResidualQuantity::identifier;
static constexpr bool available =
!valueStableId.empty() && !valueSymbol.empty() &&
!residualStableId.empty() && !residualSymbol.empty() &&
!targetUnits.empty() && !residualUnits.empty();
static constexpr bool available = !valueStableId.empty() && !valueSymbol.empty() &&
!residualStableId.empty() && !residualSymbol.empty() &&
!targetUnits.empty() && !residualUnits.empty();
};
static constexpr bool dimensionallyTyped = true;
};
template <typename Specification, FixedString StableName, SpecificationRole Role,
GeneratedStateKind StateKind = GeneratedStateKind::none, typename DependsOnBlocks = ModelTypeList<>,
typename AffectedResidualBlocks = ModelTypeList<>,
typename NormalizationDefinition = UnavailableGeneratedNormalization,
typename ManifestDefinition = UnavailableGeneratedManifest>
template <
typename Specification,
FixedString StableName,
SpecificationRole Role,
GeneratedStateKind StateKind = GeneratedStateKind::none,
typename DependsOnBlocks = ModelTypeList<>,
typename AffectedResidualBlocks = ModelTypeList<>,
typename NormalizationDefinition = UnavailableGeneratedNormalization,
typename ManifestDefinition = UnavailableGeneratedManifest>
struct ModelDefinition final {
using SpecificationType = Specification;
using DependsOn = DependsOnBlocks;
@@ -447,26 +444,56 @@ export namespace mean_field::models {
template <typename Specification, FixedString Name>
using BoundaryCondition = ModelDefinition<Specification, Name, SpecificationRole::boundary_condition>;
template <typename Specification, FixedString Name, typename DependsOn = ModelTypeList<>,
typename Affects = ModelTypeList<>, typename Normalization = UnavailableGeneratedNormalization,
typename Manifest = UnavailableGeneratedManifest>
using FixedIntegralWithMultiplier =
ModelDefinition<Specification, Name, SpecificationRole::invariant, GeneratedStateKind::multiplier, DependsOn,
Affects, Normalization, Manifest>;
template <
typename Specification,
FixedString Name,
typename DependsOn = ModelTypeList<>,
typename Affects = ModelTypeList<>,
typename Normalization = UnavailableGeneratedNormalization,
typename Manifest = UnavailableGeneratedManifest>
using FixedIntegralWithMultiplier = ModelDefinition<
Specification,
Name,
SpecificationRole::invariant,
GeneratedStateKind::multiplier,
DependsOn,
Affects,
Normalization,
Manifest>;
template <typename Specification, FixedString Name, typename DependsOn = ModelTypeList<>,
typename Affects = ModelTypeList<>, typename Normalization = UnavailableGeneratedNormalization,
typename Manifest = UnavailableGeneratedManifest>
using FixedIntegralWithPhysicalCoordinate =
ModelDefinition<Specification, Name, SpecificationRole::invariant, GeneratedStateKind::physical_coordinate,
DependsOn, Affects, Normalization, Manifest>;
template <
typename Specification,
FixedString Name,
typename DependsOn = ModelTypeList<>,
typename Affects = ModelTypeList<>,
typename Normalization = UnavailableGeneratedNormalization,
typename Manifest = UnavailableGeneratedManifest>
using FixedIntegralWithPhysicalCoordinate = ModelDefinition<
Specification,
Name,
SpecificationRole::invariant,
GeneratedStateKind::physical_coordinate,
DependsOn,
Affects,
Normalization,
Manifest>;
template <typename Specification, FixedString Name, typename DependsOn = ModelTypeList<>,
typename Affects = ModelTypeList<>, typename Normalization = UnavailableGeneratedNormalization,
typename Manifest = UnavailableGeneratedManifest>
using PhaseCondition =
ModelDefinition<Specification, Name, SpecificationRole::phase_condition, GeneratedStateKind::solver_border,
DependsOn, Affects, Normalization, Manifest>;
template <
typename Specification,
FixedString Name,
typename DependsOn = ModelTypeList<>,
typename Affects = ModelTypeList<>,
typename Normalization = UnavailableGeneratedNormalization,
typename Manifest = UnavailableGeneratedManifest>
using PhaseCondition = ModelDefinition<
Specification,
Name,
SpecificationRole::phase_condition,
GeneratedStateKind::solver_border,
DependsOn,
Affects,
Normalization,
Manifest>;
struct SpecificationKey final {
SpecificationRole role;
@@ -510,35 +537,42 @@ export namespace mean_field::models {
};
namespace detail {
template <typename Candidate> struct IsModelTypeList : std::false_type {};
template <typename Candidate> struct IsModelTypeList : std::false_type { };
template <typename... Types> struct IsModelTypeList<ModelTypeList<Types...>> : std::true_type {};
template <typename... Types> struct IsModelTypeList<ModelTypeList<Types...>> : std::true_type { };
template <typename Candidate> struct IsModelDefinition : std::false_type {};
template <typename Candidate> struct IsModelDefinition : std::false_type { };
template <typename Specification, FixedString StableName, SpecificationRole Role, GeneratedStateKind StateKind,
typename DependsOn, typename Affects, typename Normalization, typename Manifest>
template <
typename Specification,
FixedString StableName,
SpecificationRole Role,
GeneratedStateKind StateKind,
typename DependsOn,
typename Affects,
typename Normalization,
typename Manifest>
struct IsModelDefinition<
ModelDefinition<Specification, StableName, Role, StateKind, DependsOn, Affects, Normalization, Manifest>>
: std::bool_constant<(StableName.view().size() > 0) &&
CompatibleSpecificationRoleAndGeneratedState<Role, StateKind> &&
IsModelTypeList<DependsOn>::value &&
IsModelTypeList<Affects>::value>{};
: std::bool_constant<
(StableName.view().size() > 0) && CompatibleSpecificationRoleAndGeneratedState<Role, StateKind> &&
IsModelTypeList<DependsOn>::value && IsModelTypeList<Affects>::value> { };
template <typename Definition, typename Candidate, bool = IsModelDefinition<Definition>::value>
struct DefinitionDescribesCandidate : std::false_type {};
struct DefinitionDescribesCandidate : std::false_type { };
template <typename Definition, typename Candidate>
struct DefinitionDescribesCandidate<Definition, Candidate, true>
: std::bool_constant<std::same_as<typename Definition::SpecificationType, Candidate>> {};
: std::bool_constant<std::same_as<typename Definition::SpecificationType, Candidate>> { };
template <typename Candidate, typename = void> struct SpecificationDefinitionFor {
static constexpr bool available = false;
};
template <typename Candidate>
struct SpecificationDefinitionFor<Candidate,
std::void_t<typename std::remove_cvref_t<Candidate>::ModelDefinition>> {
struct SpecificationDefinitionFor<
Candidate,
std::void_t<typename std::remove_cvref_t<Candidate>::ModelDefinition>> {
using Type = typename std::remove_cvref_t<Candidate>::ModelDefinition;
static constexpr bool available = DefinitionDescribesCandidate<Type, std::remove_cvref_t<Candidate>>::value;
};
@@ -570,7 +604,7 @@ export namespace mean_field::models {
template <typename Candidate>
requires detail::SpecificationDefinitionFor<std::remove_cvref_t<Candidate>>::available
struct SpecificationTraits<Candidate> {
using Definition = ModelDefinitionForT<Candidate>;
using Definition = ModelDefinitionForT<Candidate>;
static constexpr std::string_view name = Definition::name;
static constexpr SpecificationRole role = Definition::role;
@@ -604,7 +638,8 @@ export namespace mean_field::models {
"R_M">;
using TargetValue = typename ScalarDescription::TargetValue;
using ModelDefinition = FixedIntegralWithMultiplier<
FixedTotalMass, "FixedTotalMass",
FixedTotalMass,
"FixedTotalMass",
DependsOn<stellar::state::Density, stellar::state::SurfaceShape>,
Affects<stellar::equation::HydrostaticBalance>,
typename ScalarDescription::Normalization,
@@ -615,9 +650,13 @@ export namespace mean_field::models {
explicit FixedTotalMass(const TargetValue targetMass) : m_targetMass(targetMass) {
if (!std::isfinite(targetMass.value()) || targetMass.value() <= 0.0) {
throw std::invalid_argument(std::format("The fixed total mass must be finite and positive. "
"Instead M = {} was provided.",
targetMass.value()));
throw std::invalid_argument(
std::format(
"The fixed total mass must be finite and positive. "
"Instead M = {} was provided.",
targetMass.value()
)
);
}
}
@@ -651,29 +690,35 @@ export namespace mean_field::models {
"R_J">;
using TargetValue = typename ScalarDescription::TargetValue;
using ModelDefinition = FixedIntegralWithPhysicalCoordinate<
FixedAngularMomentum, "FixedAngularMomentum",
DependsOn<
stellar::state::Density,
stellar::state::SurfaceShape,
stellar::state::OwnGeneratedCoordinate>,
FixedAngularMomentum,
"FixedAngularMomentum",
DependsOn<stellar::state::Density, stellar::state::SurfaceShape, stellar::state::OwnGeneratedCoordinate>,
Affects<stellar::equation::SurfaceShapeBalance, stellar::equation::HydrostaticBalance>,
typename ScalarDescription::Normalization,
typename ScalarDescription::Manifest>;
explicit FixedAngularMomentum(const Parameters parameters)
: m_targetAngularMomentum(parameters.Jtotal), m_axis(parameters.axis), m_center(parameters.center) {
: m_targetAngularMomentum(parameters.Jtotal),
m_axis(parameters.axis),
m_center(parameters.center) {
if (!std::isfinite(m_targetAngularMomentum.value()) || m_targetAngularMomentum.value() < 0.0) {
throw std::invalid_argument(std::format("The fixed total angular momentum must be finite and "
"nonnegative. Instead J = {} was "
"provided.",
m_targetAngularMomentum.value()));
throw std::invalid_argument(
std::format(
"The fixed total angular momentum must be finite and "
"nonnegative. Instead J = {} was "
"provided.",
m_targetAngularMomentum.value()
)
);
}
double axisNormSquared = 0.0;
for (std::size_t component = 0; component < m_axis.size(); ++component) {
if (!std::isfinite(m_axis[component]) || !std::isfinite(m_center[component])) {
throw std::invalid_argument("A fixed-angular-momentum rotation axis and center must contain "
"only finite values.");
throw std::invalid_argument(
"A fixed-angular-momentum rotation axis and center must contain "
"only finite values."
);
}
axisNormSquared += m_axis[component] * m_axis[component];
}
@@ -698,11 +743,17 @@ export namespace mean_field::models {
return m_targetAngularMomentum;
}
[[nodiscard]] const std::array<double, 3> &axis() const noexcept {
[[nodiscard]] const std::array<
double,
3> &
axis() const noexcept {
return m_axis;
}
[[nodiscard]] const std::array<double, 3> &center() const noexcept {
[[nodiscard]] const std::array<
double,
3> &
center() const noexcept {
return m_center;
}
@@ -728,7 +779,8 @@ export namespace mean_field::models {
"R_rho_c">;
using TargetValue = typename ScalarDescription::TargetValue;
using ModelDefinition = PhaseCondition<
FixedCentralDensity, "FixedCentralDensity",
FixedCentralDensity,
"FixedCentralDensity",
DependsOn<stellar::state::SpecificEnthalpy>,
Affects<stellar::equation::HydrostaticBalance>,
typename ScalarDescription::Normalization,
@@ -739,9 +791,13 @@ export namespace mean_field::models {
explicit FixedCentralDensity(const TargetValue targetDensity) : m_targetDensity(targetDensity) {
if (!std::isfinite(targetDensity.value()) || targetDensity.value() <= 0.0) {
throw std::invalid_argument(std::format("The fixed central density must be finite and positive. "
"Instead rho_c = {} was provided.",
targetDensity.value()));
throw std::invalid_argument(
std::format(
"The fixed central density must be finite and positive. "
"Instead rho_c = {} was provided.",
targetDensity.value()
)
);
}
}
@@ -761,19 +817,19 @@ export namespace mean_field::models {
template <typename Query, typename... Types>
struct ModelTypeListContains<Query, ModelTypeList<Types...>>
: std::bool_constant<(std::same_as<Query, Types> || ...)> {};
: std::bool_constant<(std::same_as<Query, Types> || ...)> { };
template <typename Query, typename List>
inline constexpr bool modelTypeListContains = ModelTypeListContains<Query, List>::value;
template <typename Specification> struct ResidualFor final {
using SpecificationType = Specification;
using SpecificationType = Specification;
static constexpr std::size_t scalarArity = 1;
};
template <typename Specification> struct MultiplierFor final {
using SpecificationType = Specification;
using SpecificationType = Specification;
static constexpr std::size_t scalarArity = 1;
};
@@ -781,13 +837,13 @@ export namespace mean_field::models {
// A generated state variable that participates directly in the physical
// equations, rather than serving only as a Lagrange multiplier or border.
template <typename Specification> struct PhysicalCoordinateFor final {
using SpecificationType = Specification;
using SpecificationType = Specification;
static constexpr std::size_t scalarArity = 1;
};
template <typename Specification> struct BorderFor final {
using SpecificationType = Specification;
using SpecificationType = Specification;
static constexpr std::size_t scalarArity = 1;
};
@@ -881,15 +937,15 @@ export namespace mean_field::models {
static constexpr std::size_t generatedValueArity = Definition::generatedValueArity;
static constexpr std::size_t generatedResidualArity = Definition::generatedResidualArity;
static constexpr bool isDefined = Definition::structurallyAvailable;
static constexpr bool hasDeclarativeDefinition = Definition::structurallyAvailable;
static constexpr bool hasDeclarativeDefinition = Definition::structurallyAvailable;
};
namespace detail {
template <ModelSpecification Specification>
[[nodiscard]] consteval bool generatedScalarDimensionsAreCoherent() {
using Contribution = SpecificationContribution<Specification>;
using Contribution = SpecificationContribution<Specification>;
using Normalization = typename Contribution::Normalization;
using Manifest = typename Contribution::Manifest;
using Manifest = typename Contribution::Manifest;
if constexpr (Contribution::generatedValueArity == 0) {
return true;
@@ -899,9 +955,7 @@ export namespace mean_field::models {
typename Normalization::GeneratedCoordinateQuantity;
typename Normalization::ConstraintResidualQuantity;
typename Normalization::TargetValue;
{
Normalization::targetScale
} -> std::convertible_to<PhysicalScaleLaw>;
{ Normalization::targetScale } -> std::convertible_to<PhysicalScaleLaw>;
};
constexpr bool manifestIsTyped = requires {
typename Manifest::TargetQuantity;
@@ -918,72 +972,53 @@ export namespace mean_field::models {
} else if constexpr (!normalizationIsTyped || !manifestIsTyped) {
return false;
} else {
using TargetQuantity = typename Normalization::TargetQuantity;
using GeneratedCoordinateQuantity =
typename Normalization::GeneratedCoordinateQuantity;
using ConstraintResidualQuantity =
typename Normalization::ConstraintResidualQuantity;
using ValueNormalization = typename Normalization::Value;
using ResidualNormalization = typename Normalization::Residual;
using TargetQuantity = typename Normalization::TargetQuantity;
using GeneratedCoordinateQuantity = typename Normalization::GeneratedCoordinateQuantity;
using ConstraintResidualQuantity = typename Normalization::ConstraintResidualQuantity;
using ValueNormalization = typename Normalization::Value;
using ResidualNormalization = typename Normalization::Residual;
if constexpr (
!PhysicalScaleRepresentedQuantity<TargetQuantity> ||
!PhysicalScaleRepresentedQuantity<GeneratedCoordinateQuantity> ||
!PhysicalScaleRepresentedQuantity<ConstraintResidualQuantity>) {
!PhysicalScaleRepresentedQuantity<ConstraintResidualQuantity>
) {
return false;
} else if constexpr (!requires {
typename std::integral_constant<
PhysicalScaleLaw,
static_cast<PhysicalScaleLaw>(
Normalization::targetScale)>;
typename std::integral_constant<
PhysicalScaleLaw,
static_cast<PhysicalScaleLaw>(
ValueNormalization::scale)>;
typename std::integral_constant<
PhysicalScaleLaw,
static_cast<PhysicalScaleLaw>(
ResidualNormalization::scale)>;
typename std::bool_constant<
static_cast<std::string_view>(
Manifest::targetUnits) ==
TargetQuantity::identifier>;
typename std::bool_constant<
static_cast<std::string_view>(
Manifest::residualUnits) ==
ConstraintResidualQuantity::identifier>;
}) {
} else if constexpr (
!requires {
typename std::integral_constant<
PhysicalScaleLaw, static_cast<PhysicalScaleLaw>(Normalization::targetScale)>;
typename std::integral_constant<
PhysicalScaleLaw, static_cast<PhysicalScaleLaw>(ValueNormalization::scale)>;
typename std::integral_constant<
PhysicalScaleLaw, static_cast<PhysicalScaleLaw>(ResidualNormalization::scale)>;
typename std::bool_constant<
static_cast<std::string_view>(Manifest::targetUnits) == TargetQuantity::identifier>;
typename std::bool_constant<
static_cast<std::string_view>(Manifest::residualUnits) ==
ConstraintResidualQuantity::identifier>;
}
) {
return false;
} else if constexpr (!requires(const Specification &specification) {
specification.target();
}) {
} else if constexpr (!requires(const Specification &specification) { specification.target(); }) {
return false;
} else {
return
std::same_as<
typename Normalization::TargetValue,
dimensions::QuantityValue<TargetQuantity>> &&
Normalization::targetScale ==
physicalScaleForQuantity<TargetQuantity> &&
std::same_as<TargetQuantity, typename Manifest::TargetQuantity> &&
std::same_as<
GeneratedCoordinateQuantity,
typename Manifest::GeneratedCoordinateQuantity> &&
std::same_as<
ConstraintResidualQuantity,
typename Manifest::ConstraintResidualQuantity> &&
ValueNormalization::scale ==
physicalScaleForQuantity<GeneratedCoordinateQuantity> &&
ResidualNormalization::scale ==
physicalScaleForQuantity<ConstraintResidualQuantity> &&
static_cast<std::string_view>(Manifest::targetUnits) ==
TargetQuantity::identifier &&
static_cast<std::string_view>(Manifest::residualUnits) ==
ConstraintResidualQuantity::identifier &&
std::same_as<
std::remove_cvref_t<decltype(
std::declval<const Specification &>().target())>,
typename Normalization::TargetValue>;
return std::same_as<
typename Normalization::TargetValue, dimensions::QuantityValue<TargetQuantity>> &&
Normalization::targetScale == physicalScaleForQuantity<TargetQuantity> &&
std::same_as<TargetQuantity, typename Manifest::TargetQuantity> &&
std::same_as<
GeneratedCoordinateQuantity, typename Manifest::GeneratedCoordinateQuantity> &&
std::same_as<
ConstraintResidualQuantity, typename Manifest::ConstraintResidualQuantity> &&
ValueNormalization::scale == physicalScaleForQuantity<GeneratedCoordinateQuantity> &&
ResidualNormalization::scale == physicalScaleForQuantity<ConstraintResidualQuantity> &&
static_cast<std::string_view>(Manifest::targetUnits) == TargetQuantity::identifier &&
static_cast<std::string_view>(Manifest::residualUnits) ==
ConstraintResidualQuantity::identifier &&
std::same_as<
std::remove_cvref_t<decltype(std::declval<const Specification &>().target())>,
typename Normalization::TargetValue>;
}
}
}
@@ -993,20 +1028,17 @@ export namespace mean_field::models {
template <typename Specification>
concept CompleteGeneratedScalarDimensionsFor =
ModelSpecification<Specification> &&
detail::generatedScalarDimensionsAreCoherent<
std::remove_cvref_t<Specification>>();
detail::generatedScalarDimensionsAreCoherent<std::remove_cvref_t<Specification>>();
template <typename Specification>
concept CompleteGeneratedNormalizationFor =
ModelSpecification<Specification> &&
CompleteGeneratedScalarDimensionsFor<Specification> &&
ModelSpecification<Specification> && CompleteGeneratedScalarDimensionsFor<Specification> &&
(SpecificationContribution<std::remove_cvref_t<Specification>>::generatedValueArity == 0 ||
SpecificationContribution<std::remove_cvref_t<Specification>>::Normalization::available);
template <typename Specification>
concept CompleteGeneratedManifestFor =
ModelSpecification<Specification> &&
CompleteGeneratedScalarDimensionsFor<Specification> &&
ModelSpecification<Specification> && CompleteGeneratedScalarDimensionsFor<Specification> &&
(SpecificationContribution<std::remove_cvref_t<Specification>>::generatedValueArity == 0 ||
SpecificationContribution<std::remove_cvref_t<Specification>>::Manifest::available);
@@ -1035,7 +1067,7 @@ export namespace mean_field::models {
};
template <SpecificationRole Role, typename SpecificationSet> struct SpecificationsForRole {
using Type = ModelTypeList<>;
using Type = ModelTypeList<>;
static constexpr bool available = false;
static constexpr std::size_t count = 0;
@@ -1043,9 +1075,10 @@ export namespace mean_field::models {
template <SpecificationRole Role, ModelSpecification... Specifications>
struct SpecificationsForRole<Role, SpecificationSetStorage<Specifications...>> {
using Type = typename ConcatenateModelTypeLists<
std::conditional_t<SpecificationTraits<Specifications>::role == Role, ModelTypeList<Specifications>,
ModelTypeList<>>...>::Type;
using Type = typename ConcatenateModelTypeLists<std::conditional_t<
SpecificationTraits<Specifications>::role == Role,
ModelTypeList<Specifications>,
ModelTypeList<>>...>::Type;
static constexpr bool available = true;
static constexpr std::size_t count = Type::size;
@@ -1077,9 +1110,10 @@ export namespace mean_field::models {
};
public:
using Type = std::conditional_t<(SpecificationTraits<Specification>::key < SpecificationTraits<Head>::key),
SpecificationSetStorage<Specification, Head, Tail...>,
typename PrependSpecification<Head, InsertedTail>::Type>;
using Type = std::conditional_t<
(SpecificationTraits<Specification>::key < SpecificationTraits<Head>::key),
SpecificationSetStorage<Specification, Head, Tail...>,
typename PrependSpecification<Head, InsertedTail>::Type>;
};
template <typename Set, ModelSpecification... Specifications> struct CanonicalizeSpecifications;
@@ -1098,21 +1132,26 @@ export namespace mean_field::models {
using CanonicalSpecificationSet =
typename CanonicalizeSpecifications<SpecificationSetStorage<>, Specifications...>::Type;
template <ModelSpecification Head, ModelSpecification... Tail> consteval bool specificationKeyIsUnique() {
template <
ModelSpecification Head,
ModelSpecification... Tail>
consteval bool specificationKeyIsUnique() {
constexpr auto headKey = SpecificationTraits<Head>::key;
return ((headKey.role != SpecificationTraits<Tail>::key.role ||
headKey.stableName != SpecificationTraits<Tail>::key.stableName) &&
...);
return (
(headKey.role != SpecificationTraits<Tail>::key.role ||
headKey.stableName != SpecificationTraits<Tail>::key.stableName) &&
...
);
}
template <ModelSpecification... Specifications> struct SpecificationKeysAreUnique;
template <> struct SpecificationKeysAreUnique<> : std::true_type {};
template <> struct SpecificationKeysAreUnique<> : std::true_type { };
template <ModelSpecification Head, ModelSpecification... Tail>
struct SpecificationKeysAreUnique<Head, Tail...>
: std::bool_constant<specificationKeyIsUnique<Head, Tail...>() &&
SpecificationKeysAreUnique<Tail...>::value> {};
: std::bool_constant<
specificationKeyIsUnique<Head, Tail...>() && SpecificationKeysAreUnique<Tail...>::value> { };
template <SpecificationRole Role, ModelSpecification... Specifications>
inline constexpr std::size_t specificationRoleCount =
@@ -1122,7 +1161,7 @@ export namespace mean_field::models {
template <typename... Types>
struct ModelTypeListScalarArity<ModelTypeList<Types...>>
: std::integral_constant<std::size_t, (std::size_t{0} + ... + Types::scalarArity)> {};
: std::integral_constant<std::size_t, (std::size_t{0} + ... + Types::scalarArity)> { };
template <typename Query, typename... Types>
inline constexpr bool isOneOf = (std::same_as<Query, Types> || ...);
@@ -1136,9 +1175,10 @@ export namespace mean_field::models {
template <ModelSpecification... CanonicalSpecifications, typename... Arguments>
struct ArgumentsMatchCanonicalSpecifications<SpecificationSetStorage<CanonicalSpecifications...>, Arguments...>
: std::bool_constant<sizeof...(CanonicalSpecifications) == sizeof...(Arguments) &&
(isOneOf<std::remove_cvref_t<Arguments>, CanonicalSpecifications...> && ...) &&
((typeCount<CanonicalSpecifications, Arguments...> == 1) && ...)> {};
: std::bool_constant<
sizeof...(CanonicalSpecifications) == sizeof...(Arguments) &&
(isOneOf<std::remove_cvref_t<Arguments>, CanonicalSpecifications...> && ...) &&
((typeCount<CanonicalSpecifications, Arguments...> == 1) && ...)> { };
} // namespace detail
template <typename... Specifications>
@@ -1150,12 +1190,12 @@ export namespace mean_field::models {
concept ValidModelSpecificationPack =
(ResolvedModelSpecification<std::remove_cvref_t<Specifications>> && ...) &&
specificationKeysAreUnique<std::remove_cvref_t<Specifications>...> &&
detail::specificationRoleCount<SpecificationRole::constitutive_law,
std::remove_cvref_t<Specifications>...> == 1;
detail::specificationRoleCount<SpecificationRole::constitutive_law, std::remove_cvref_t<Specifications>...> ==
1;
template <typename... Specifications>
requires(ModelSpecification<std::remove_cvref_t<Specifications>> && ...) &&
specificationKeysAreUnique<std::remove_cvref_t<Specifications>...>
specificationKeysAreUnique<std::remove_cvref_t<Specifications>...>
using SpecificationSet = detail::CanonicalSpecificationSet<std::remove_cvref_t<Specifications>...>;
template <SpecificationRole Role, typename SpecificationSet>
@@ -1203,12 +1243,13 @@ export namespace mean_field::models {
[[nodiscard]] consteval SpecificationDescriptor specificationDescriptor() {
using Contribution = SpecificationContribution<Specification>;
return {.name = SpecificationTraits<Specification>::name,
.role = SpecificationTraits<Specification>::role,
.key = SpecificationTraits<Specification>::key,
.generatedValueArity = detail::ModelTypeListScalarArity<typename Contribution::GeneratedValues>::value,
.generatedResidualArity =
detail::ModelTypeListScalarArity<typename Contribution::GeneratedResiduals>::value};
return {
.name = SpecificationTraits<Specification>::name,
.role = SpecificationTraits<Specification>::role,
.key = SpecificationTraits<Specification>::key,
.generatedValueArity = detail::ModelTypeListScalarArity<typename Contribution::GeneratedValues>::value,
.generatedResidualArity = detail::ModelTypeListScalarArity<typename Contribution::GeneratedResiduals>::value
};
}
namespace detail {
@@ -1231,14 +1272,12 @@ export namespace mean_field::models {
std::tuple<ArgumentTypes...> &&arguments,
CanonicalArgumentsTag
)
: m_specifications(
std::get<Specifications>(std::move(arguments))...
) {
: m_specifications(std::get<Specifications>(std::move(arguments))...) {
}
public:
using SpecificationTypes = SpecificationSetStorage<Specifications...>;
using OperatorSignature = SpecificationOperatorSignature<SpecificationTypes>;
using SpecificationTypes = SpecificationSetStorage<Specifications...>;
using OperatorSignature = SpecificationOperatorSignature<SpecificationTypes>;
static constexpr bool symbolicallySquare = OperatorSignature::symbolicallySquare;
@@ -1249,22 +1288,27 @@ export namespace mean_field::models {
symbolicallySquare && (SpecificationContribution<Specifications>::hasDeclarativeDefinition && ...);
template <typename... Arguments>
requires ArgumentsMatchCanonicalSpecifications<SpecificationTypes, Arguments...>::value &&
requires ArgumentsMatchCanonicalSpecifications<
SpecificationTypes,
Arguments...>::value &&
std::constructible_from<
std::tuple<std::remove_cvref_t<Arguments>...>,
Arguments...> &&
(std::constructible_from<Specifications, Specifications &&> && ...)
(std::constructible_from<
Specifications,
Specifications &&> &&
...)
explicit SpecifiedModel(Arguments &&...arguments)
: SpecifiedModel(
std::tuple<std::remove_cvref_t<Arguments>...>{
std::forward<Arguments>(arguments)...
},
std::tuple<std::remove_cvref_t<Arguments>...>{std::forward<Arguments>(arguments)...},
CanonicalArgumentsTag{}
) {
}
template <ModelSpecification Specification>
requires isOneOf<Specification, Specifications...>
requires isOneOf<
Specification,
Specifications...>
[[nodiscard]] const Specification &specification() const noexcept {
return std::get<Specification>(m_specifications);
}
@@ -1282,10 +1326,11 @@ export namespace mean_field::models {
runtimeDescriptors = [] {
std::array<RuntimeSpecificationDescriptor, sizeof...(Specifications)> descriptors{};
std::size_t index = 0;
((descriptors[index] = {.specification = specificationDescriptor<Specifications>(),
.canonicalIndex = index,
.hasDeclarativeDefinition =
SpecificationContribution<Specifications>::hasDeclarativeDefinition},
((descriptors[index] =
{.specification = specificationDescriptor<Specifications>(),
.canonicalIndex = index,
.hasDeclarativeDefinition =
SpecificationContribution<Specifications>::hasDeclarativeDefinition},
++index),
...);
return descriptors;
@@ -1346,27 +1391,23 @@ export namespace mean_field::stellar {
} // namespace state
namespace equation {
using GravityGradientDefinition = models::stellar::equation::GravityGradientDefinition;
using PoissonEquation = models::stellar::equation::PoissonEquation;
using DensityClosure = models::stellar::equation::DensityClosure;
using SurfaceShapeBalance = models::stellar::equation::SurfaceShapeBalance;
using HydrostaticBalance = models::stellar::equation::HydrostaticBalance;
using OwnConstraint = models::stellar::equation::OwnConstraint;
using GravityGradientDefinition = models::stellar::equation::GravityGradientDefinition;
using PoissonEquation = models::stellar::equation::PoissonEquation;
using DensityClosure = models::stellar::equation::DensityClosure;
using SurfaceShapeBalance = models::stellar::equation::SurfaceShapeBalance;
using HydrostaticBalance = models::stellar::equation::HydrostaticBalance;
using OwnConstraint = models::stellar::equation::OwnConstraint;
template <typename Specification>
using ConstraintOf = models::stellar::equation::ConstraintOf<Specification>;
template <typename Specification> using ConstraintOf = models::stellar::equation::ConstraintOf<Specification>;
} // namespace equation
template <typename Equation, typename State>
using Derivative = models::stellar::Derivative<Equation, State>;
template <typename Equation, typename State> using Derivative = models::stellar::Derivative<Equation, State>;
template <typename... Quantities>
using Reads = models::DependsOn<Quantities...>;
template <typename... Quantities> using Reads = models::DependsOn<Quantities...>;
template <typename... Equations>
using Changes = models::Affects<Equations...>;
template <typename... Equations> using Changes = models::Affects<Equations...>;
using PhysicalScale = models::PhysicalScaleLaw;
using PhysicalScale = models::PhysicalScaleLaw;
template <
models::PhysicalScaleRepresentedQuantity TargetQuantity,
@@ -1393,10 +1434,8 @@ export namespace mean_field::stellar {
typename std::remove_cvref_t<Candidate>::GeneratedCoordinateQuantity;
typename std::remove_cvref_t<Candidate>::ConstraintResidualQuantity;
typename std::remove_cvref_t<Candidate>::TargetValue;
requires models::GeneratedNormalizationDefinition<
typename std::remove_cvref_t<Candidate>::Normalization>;
requires models::GeneratedManifestDefinition<
typename std::remove_cvref_t<Candidate>::Manifest>;
requires models::GeneratedNormalizationDefinition<typename std::remove_cvref_t<Candidate>::Normalization>;
requires models::GeneratedManifestDefinition<typename std::remove_cvref_t<Candidate>::Manifest>;
requires std::remove_cvref_t<Candidate>::Normalization::available;
requires std::remove_cvref_t<Candidate>::Manifest::available;
requires std::remove_cvref_t<Candidate>::dimensionallyTyped;
@@ -1407,31 +1446,43 @@ export namespace mean_field::integral {
using FixedTotalMass = models::FixedTotalMass;
using FixedAngularMomentum = models::FixedAngularMomentum;
template <typename Specification, models::FixedString Name, typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
template <
typename Specification,
models::FixedString Name,
typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
using FixedIntegralWithMultiplier =
models::FixedIntegralWithMultiplier<Specification, Name, DependsOn, Affects, Normalization, Manifest>;
template <typename Specification, models::FixedString Name, typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
template <
typename Specification,
models::FixedString Name,
typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
using FixedWithMultiplier =
FixedIntegralWithMultiplier<Specification, Name, DependsOn, Affects, Normalization, Manifest>;
template <typename Specification, models::FixedString Name, typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
template <
typename Specification,
models::FixedString Name,
typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
using FixedIntegralWithPhysicalCoordinate =
models::FixedIntegralWithPhysicalCoordinate<Specification, Name, DependsOn, Affects, Normalization, Manifest>;
template <typename Specification, models::FixedString Name, typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
template <
typename Specification,
models::FixedString Name,
typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
using FixedWithPhysicalCoordinate =
FixedIntegralWithPhysicalCoordinate<Specification, Name, DependsOn, Affects, Normalization, Manifest>;
@@ -1467,10 +1518,13 @@ export namespace mean_field::integral {
export namespace mean_field::constraint {
using FixedCentralDensity = models::FixedCentralDensity;
template <typename Specification, models::FixedString Name, typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
template <
typename Specification,
models::FixedString Name,
typename DependsOn = models::ModelTypeList<>,
typename Affects = models::ModelTypeList<>,
typename Normalization = models::UnavailableGeneratedNormalization,
typename Manifest = models::UnavailableGeneratedManifest>
using PhaseCondition = models::PhaseCondition<Specification, Name, DependsOn, Affects, Normalization, Manifest>;
template <

View File

@@ -25,10 +25,9 @@ export namespace mean_field::model {
using EquationOfStateType =
models::SpecificationForRoleT<models::SpecificationRole::constitutive_law, SpecificationTypes>;
static constexpr std::size_t specificationCount = sizeof...(CanonicalSpecifications);
static constexpr bool symbolicallySquare = Storage::symbolicallySquare;
static constexpr bool hasCompleteEquilibriumDeclaration =
Storage::hasCompleteEquilibriumDeclaration;
static constexpr std::size_t specificationCount = sizeof...(CanonicalSpecifications);
static constexpr bool symbolicallySquare = Storage::symbolicallySquare;
static constexpr bool hasCompleteEquilibriumDeclaration = Storage::hasCompleteEquilibriumDeclaration;
template <models::SpecificationRole Role>
using SpecificationsForRole = models::SpecificationsForRoleT<Role, SpecificationTypes>;
@@ -48,7 +47,9 @@ export namespace mean_field::model {
models::HasUniqueSpecificationForRole<Role, SpecificationTypes>;
template <typename... Arguments>
requires std::constructible_from<Storage, Arguments...>
requires std::constructible_from<
Storage,
Arguments...>
explicit StellarModel(Arguments &&...arguments) : m_specifications(std::forward<Arguments>(arguments)...) {
}
@@ -62,8 +63,12 @@ export namespace mean_field::model {
static constexpr bool containsSpecification = Storage::template containsSpecification<Specification>;
template <models::SpecificationRole Role>
requires models::HasUniqueSpecificationForRole<Role, SpecificationTypes>
[[nodiscard]] const models::SpecificationForRoleT<Role, SpecificationTypes> &
requires models::HasUniqueSpecificationForRole<
Role,
SpecificationTypes>
[[nodiscard]] const models::SpecificationForRoleT<
Role,
SpecificationTypes> &
specificationForRole() const noexcept {
using Specification = models::SpecificationForRoleT<Role, SpecificationTypes>;
return specification<Specification>();
@@ -74,8 +79,9 @@ export namespace mean_field::model {
}
template <typename = void>
requires models::HasUniqueSpecificationForRole<models::SpecificationRole::boundary_condition,
SpecificationTypes>
requires models::HasUniqueSpecificationForRole<
models::SpecificationRole::boundary_condition,
SpecificationTypes>
[[nodiscard]] const auto &surfaceCondition() const noexcept {
return specificationForRole<models::SpecificationRole::boundary_condition>();
}
@@ -95,20 +101,20 @@ export namespace mean_field::model {
-> StellarModel<models::SpecificationSet<std::remove_cvref_t<Specifications>...>>;
namespace detail {
template <typename Candidate, typename = void> struct IsStellarModel : std::false_type {};
template <typename Candidate, typename = void> struct IsStellarModel : std::false_type { };
template <typename SpecificationSet>
struct IsStellarModel<
StellarModel<SpecificationSet>,
std::void_t<typename StellarModel<SpecificationSet>::SpecificationTypes,
typename StellarModel<SpecificationSet>::OperatorSignature,
decltype(StellarModel<SpecificationSet>::specificationCount),
decltype(StellarModel<SpecificationSet>::hasCompleteEquilibriumDeclaration)>>
: std::true_type {};
std::void_t<
typename StellarModel<SpecificationSet>::SpecificationTypes,
typename StellarModel<SpecificationSet>::OperatorSignature,
decltype(StellarModel<SpecificationSet>::specificationCount),
decltype(StellarModel<SpecificationSet>::hasCompleteEquilibriumDeclaration)>> : std::true_type { };
template <models::SpecificationRole Role, typename Candidate, bool = IsStellarModel<Candidate>::value>
struct StellarModelRoleSelection {
using Types = models::ModelTypeList<>;
using Types = models::ModelTypeList<>;
static constexpr std::size_t count = 0;
};

View File

@@ -39,7 +39,7 @@ export namespace mean_field::normalization {
}
mfem::Vector state(stateSize);
mfem::Vector residual(residualSize);
state = 1.0;
state = 1.0;
residual = 1.0;
return {std::move(state), std::move(residual)};
}
@@ -126,7 +126,7 @@ export namespace mean_field::normalization {
}
for (int index = 0; index < input.Size(); ++index) {
const double value = input(index);
output(index) = inverse ? value / factors(index) : factors(index) * value;
output(index) = inverse ? value / factors(index) : factors(index) * value;
}
}
@@ -156,22 +156,19 @@ export namespace mean_field::normalization {
* the policy and is found by ADL, so adding a normalization family does
* not edit a library registry or switch. */
template <typename Problem>
concept RuntimePreparedNormalizationOperation =
requires(const std::remove_cvref_t<Problem> &problem) {
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType;
typename std::remove_cvref_t<Problem>::FormType;
requires RuntimePreparedNormalizationFor<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Problem>::FormType>;
{
problem.GetNormalizationPrescription()
} -> std::same_as<const typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType &>;
{
prepareStellarNormalization(
problem.GetNormalizationPrescription(),
problem)
} -> std::same_as<DiagonalNormalization>;
};
concept RuntimePreparedNormalizationOperation = requires(const std::remove_cvref_t<Problem> &problem) {
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType;
typename std::remove_cvref_t<Problem>::FormType;
requires RuntimePreparedNormalizationFor<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Problem>::FormType>;
{
problem.GetNormalizationPrescription()
} -> std::same_as<const typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType &>;
{
prepareStellarNormalization(problem.GetNormalizationPrescription(), problem)
} -> std::same_as<DiagonalNormalization>;
};
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
@@ -183,16 +180,14 @@ export namespace mean_field::normalization {
m_residualFactors(layout.residual_offsets().Last()) {
}
explicit DiagonalNormalizationBuilder(
utils::blocks::form_layout<Form> &&
) = delete;
explicit DiagonalNormalizationBuilder(utils::blocks::form_layout<Form> &&) = delete;
explicit DiagonalNormalizationBuilder(
const utils::blocks::form_layout<Form> &&
) = delete;
explicit DiagonalNormalizationBuilder(const utils::blocks::form_layout<Form> &&) = delete;
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::value_blocks>
requires utils::blocks::contains_type_v<
Block,
typename Form::value_blocks>
void SetValueBlock(
const double physicalScale,
const mfem::Vector &primalGramDiagonal
@@ -200,18 +195,17 @@ export namespace mean_field::normalization {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::value_blocks>;
RequireUnassigned(m_valueAssigned[block], "value");
AssignBlock(
m_stateFactors,
m_layout->value_offsets()[block],
m_layout->value_offsets()[block + 1] - m_layout->value_offsets()[block],
physicalScale,
primalGramDiagonal,
false
m_stateFactors, m_layout->value_offsets()[block],
m_layout->value_offsets()[block + 1] - m_layout->value_offsets()[block], physicalScale,
primalGramDiagonal, false
);
m_valueAssigned[block] = true;
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::residual_blocks>
requires utils::blocks::contains_type_v<
Block,
typename Form::residual_blocks>
void SetResidualBlock(
const double physicalScale,
const mfem::Vector &primalGramDiagonal
@@ -219,25 +213,26 @@ export namespace mean_field::normalization {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::residual_blocks>;
RequireUnassigned(m_residualAssigned[block], "residual");
AssignBlock(
m_residualFactors,
m_layout->residual_offsets()[block],
m_layout->residual_offsets()[block + 1] - m_layout->residual_offsets()[block],
physicalScale,
primalGramDiagonal,
true
m_residualFactors, m_layout->residual_offsets()[block],
m_layout->residual_offsets()[block + 1] - m_layout->residual_offsets()[block], physicalScale,
primalGramDiagonal, true
);
m_residualAssigned[block] = true;
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::value_blocks>
requires utils::blocks::contains_type_v<
Block,
typename Form::value_blocks>
void SetValueGlobal(const double physicalScale) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::value_blocks>;
SetConstantMetricValueBlock<Block>(physicalScale, BlockSize(m_layout->value_offsets(), block));
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::residual_blocks>
requires utils::blocks::contains_type_v<
Block,
typename Form::residual_blocks>
void SetResidualGlobal(const double physicalScale) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::residual_blocks>;
mfem::Vector metric(BlockSize(m_layout->residual_offsets(), block));
@@ -246,7 +241,9 @@ export namespace mean_field::normalization {
}
template <typename Block>
requires utils::blocks::contains_type_v<Block, typename Form::residual_blocks>
requires utils::blocks::contains_type_v<
Block,
typename Form::residual_blocks>
void SetHybridResidualBlock(
const double physicalScale,
const mfem::Vector &bulkPrimalGramDiagonal,
@@ -254,7 +251,7 @@ export namespace mean_field::normalization {
const double pointMetric = 1.0
) {
constexpr int block = utils::blocks::type_index_v<Block, typename Form::residual_blocks>;
const int size = BlockSize(m_layout->residual_offsets(), block);
const int size = BlockSize(m_layout->residual_offsets(), block);
if (bulkPrimalGramDiagonal.Size() != size) {
throw std::invalid_argument("The hybrid residual Gram diagonal has the wrong size.");
}
@@ -273,9 +270,7 @@ export namespace mean_field::normalization {
mfem::Vector metric(size);
for (int row = 0; row < size; ++row) {
metric(row) = isPointRow[static_cast<std::size_t>(row)]
? pointMetric
: bulkPrimalGramDiagonal(row);
metric(row) = isPointRow[static_cast<std::size_t>(row)] ? pointMetric : bulkPrimalGramDiagonal(row);
}
SetResidualBlock<Block>(physicalScale, metric);
}
@@ -345,9 +340,7 @@ export namespace mean_field::normalization {
const double metric = primalGramDiagonal(index);
ValidateMetric(metric);
const double rieszFactor = std::sqrt(metric);
const double factor = dual
? 1.0 / (physicalScale * rieszFactor)
: rieszFactor / physicalScale;
const double factor = dual ? 1.0 / (physicalScale * rieszFactor) : rieszFactor / physicalScale;
if (!std::isfinite(factor) || factor <= 0.0) {
throw std::overflow_error("A normalization factor is not finite and positive.");
}
@@ -368,7 +361,10 @@ export namespace mean_field::normalization {
const mfem::Operator &physicalJacobian,
const DiagonalNormalization &normalization
)
: mfem::Operator(normalization.ResidualSize(), normalization.StateSize()),
: mfem::Operator(
normalization.ResidualSize(),
normalization.StateSize()
),
m_physicalJacobian(&physicalJacobian),
m_normalization(&normalization),
m_physicalDirection(normalization.StateSize()),
@@ -421,7 +417,10 @@ export namespace mean_field::normalization {
const mfem::Operator &physicalInverse,
const DiagonalNormalization &normalization
)
: mfem::Operator(normalization.StateSize(), normalization.ResidualSize()),
: mfem::Operator(
normalization.StateSize(),
normalization.ResidualSize()
),
m_physicalInverse(&physicalInverse),
m_normalization(&normalization),
m_physicalResidual(normalization.ResidualSize()),
@@ -548,7 +547,7 @@ export namespace mean_field::normalization {
const mfem::Operator &,
const mfem::Operator &,
const DiagonalNormalization &&
) = delete;
) = delete;
ScaledPreconditioner(const ScaledPreconditioner &) = delete;
ScaledPreconditioner &operator=(const ScaledPreconditioner &) = delete;
@@ -557,9 +556,7 @@ export namespace mean_field::normalization {
void SetOperator(const mfem::Operator &normalizedJacobian) override {
if (normalizedJacobian.Width() != Width() || normalizedJacobian.Height() != Height()) {
throw std::invalid_argument(
"The scaled preconditioner received an incompatible normalized Jacobian."
);
throw std::invalid_argument("The scaled preconditioner received an incompatible normalized Jacobian.");
}
if (&normalizedJacobian != m_expectedNormalizedJacobian) {
throw std::invalid_argument(

View File

@@ -27,10 +27,10 @@ export namespace mean_field::normalization {
template <
RieszGeometryPolicy GeometryPolicy = ReferenceGeometry,
ReferenceScalePolicy ScalePolicy = FixedMassBranchReference>
ReferenceScalePolicy ScalePolicy = FixedMassBranchReference>
class PhysicalRieszDiagonal final : public NormalizationPrescriptionTag {
public:
using Geometry = GeometryPolicy;
using Geometry = GeometryPolicy;
using ScaleSource = ScalePolicy;
explicit PhysicalRieszDiagonal(
@@ -62,8 +62,13 @@ export namespace mean_field::normalization {
double m_gravitationalConstant;
};
PhysicalRieszDiagonal(dimensions::LengthValue, double = 1.0)
-> PhysicalRieszDiagonal<ReferenceGeometry, FixedMassBranchReference>;
PhysicalRieszDiagonal(
dimensions::LengthValue,
double = 1.0
)
-> PhysicalRieszDiagonal<
ReferenceGeometry,
FixedMassBranchReference>;
template <typename Candidate> struct IsPhysicalRieszDiagonal : std::false_type { };
@@ -71,8 +76,7 @@ export namespace mean_field::normalization {
struct IsPhysicalRieszDiagonal<PhysicalRieszDiagonal<Geometry, ScaleSource>> : std::true_type { };
template <typename Candidate>
concept PhysicalRieszDiagonalPrescription =
IsPhysicalRieszDiagonal<std::remove_cvref_t<Candidate>>::value;
concept PhysicalRieszDiagonalPrescription = IsPhysicalRieszDiagonal<std::remove_cvref_t<Candidate>>::value;
struct StellarCharacteristicScales final {
dimensions::MassValue mass;
@@ -93,7 +97,7 @@ export namespace mean_field::normalization {
const dimensions::LengthValue radius,
const double gravitationalConstant = 1.0
) {
const double massValue = mass.value();
const double massValue = mass.value();
const double radiusValue = radius.value();
if (!std::isfinite(massValue) || massValue <= 0.0) {
throw std::invalid_argument("Characteristic stellar scales require a finite, positive mass.");
@@ -107,28 +111,19 @@ export namespace mean_field::normalization {
);
}
const double radiusSquared = radiusValue * radiusValue;
const double radiusCubed = radiusSquared * radiusValue;
const double density = massValue / radiusCubed;
const double acceleration = gravitationalConstant * massValue / radiusSquared;
const double radiusSquared = radiusValue * radiusValue;
const double radiusCubed = radiusSquared * radiusValue;
const double density = massValue / radiusCubed;
const double acceleration = gravitationalConstant * massValue / radiusSquared;
const double inverseTimeSquared = gravitationalConstant * massValue / radiusCubed;
const double specificEnergy = gravitationalConstant * massValue / radiusValue;
const double pressure = gravitationalConstant * massValue * massValue /
(radiusSquared * radiusSquared);
const double specificEnergy = gravitationalConstant * massValue / radiusValue;
const double pressure = gravitationalConstant * massValue * massValue / (radiusSquared * radiusSquared);
const double angularVelocity = std::sqrt(inverseTimeSquared);
const double angularMomentum = massValue * std::sqrt(gravitationalConstant * massValue * radiusValue);
const double force = gravitationalConstant * massValue * massValue / radiusSquared;
const double force = gravitationalConstant * massValue * massValue / radiusSquared;
const double derived[] = {
density,
acceleration,
inverseTimeSquared,
specificEnergy,
pressure,
angularVelocity,
angularMomentum,
force
};
const double derived[] = {density, acceleration, inverseTimeSquared, specificEnergy,
pressure, angularVelocity, angularMomentum, force};
for (const double value : derived) {
if (!std::isfinite(value) || value <= 0.0) {
throw std::overflow_error("A derived characteristic stellar scale is not finite and positive.");
@@ -136,36 +131,38 @@ export namespace mean_field::normalization {
}
return {
.mass = mass,
.radius = radius,
.mass = mass,
.radius = radius,
.gravitationalConstant = gravitationalConstant,
.density = density,
.acceleration = acceleration,
.inverseTimeSquared = inverseTimeSquared,
.specificEnergy = specificEnergy,
.pressure = pressure,
.angularVelocity = angularVelocity,
.angularMomentum = angularMomentum,
.force = force
.density = density,
.acceleration = acceleration,
.inverseTimeSquared = inverseTimeSquared,
.specificEnergy = specificEnergy,
.pressure = pressure,
.angularVelocity = angularVelocity,
.angularMomentum = angularMomentum,
.force = force
};
}
template <RieszGeometryPolicy Geometry, ReferenceScalePolicy ScaleSource, typename Model>
template <
RieszGeometryPolicy Geometry,
ReferenceScalePolicy ScaleSource,
typename Model>
requires requires(const Model &model) {
{
model.template specification<models::FixedTotalMass>()
} -> std::same_as<const models::FixedTotalMass &>;
{ model.template specification<models::FixedTotalMass>() } -> std::same_as<const models::FixedTotalMass &>;
{
model.template specification<models::FixedTotalMass>().targetMass()
} -> std::same_as<dimensions::MassValue>;
}
[[nodiscard]] StellarCharacteristicScales deriveStellarCharacteristicScales(
const PhysicalRieszDiagonal<Geometry, ScaleSource> &prescription,
const PhysicalRieszDiagonal<
Geometry,
ScaleSource> &prescription,
const Model &model
) {
return deriveStellarCharacteristicScales(
model.template specification<models::FixedTotalMass>().targetMass(),
prescription.referenceRadius(),
model.template specification<models::FixedTotalMass>().targetMass(), prescription.referenceRadius(),
prescription.gravitationalConstant()
);
}
@@ -179,14 +176,14 @@ export namespace mean_field::normalization {
* normalization plan used by the discretization.
*/
template <models::RieszTopology Topology> struct DeclaredRieszTopology {
static constexpr bool available = false;
static constexpr bool available = false;
static constexpr RieszTopology value = RieszTopology::identity;
};
#define MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(Name) \
template <> struct DeclaredRieszTopology<models::RieszTopology::Name> { \
static constexpr bool available = true; \
static constexpr RieszTopology value = RieszTopology::Name; \
#define MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(Name) \
template <> struct DeclaredRieszTopology<models::RieszTopology::Name> { \
static constexpr bool available = true; \
static constexpr RieszTopology value = RieszTopology::Name; \
}
MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY(identity);
@@ -199,14 +196,14 @@ export namespace mean_field::normalization {
#undef MEAN_FIELD_DECLARED_RIESZ_TOPOLOGY
template <models::PhysicalScaleLaw Scale> struct DeclaredPhysicalScale {
static constexpr bool available = false;
static constexpr bool available = false;
static constexpr PhysicalScaleKind value = PhysicalScaleKind::dimensionless;
};
#define MEAN_FIELD_DECLARED_PHYSICAL_SCALE(Name) \
template <> struct DeclaredPhysicalScale<models::PhysicalScaleLaw::Name> { \
static constexpr bool available = true; \
static constexpr PhysicalScaleKind value = PhysicalScaleKind::Name; \
#define MEAN_FIELD_DECLARED_PHYSICAL_SCALE(Name) \
template <> struct DeclaredPhysicalScale<models::PhysicalScaleLaw::Name> { \
static constexpr bool available = true; \
static constexpr PhysicalScaleKind value = PhysicalScaleKind::Name; \
}
MEAN_FIELD_DECLARED_PHYSICAL_SCALE(dimensionless);
@@ -223,9 +220,8 @@ export namespace mean_field::normalization {
#undef MEAN_FIELD_DECLARED_PHYSICAL_SCALE
template <typename Declaration, typename = void>
struct CompileDeclaredPhysicalRieszCoordinate {
using Method = UnsupportedPhysicalRieszCoordinate;
template <typename Declaration, typename = void> struct CompileDeclaredPhysicalRieszCoordinate {
using Method = UnsupportedPhysicalRieszCoordinate;
static constexpr bool registered = false;
};
@@ -246,11 +242,11 @@ export namespace mean_field::normalization {
static constexpr models::PhysicalScaleLaw declaredScale =
static_cast<models::PhysicalScaleLaw>(Declaration::scale);
using Topology = DeclaredRieszTopology<declaredTopology>;
using Scale = DeclaredPhysicalScale<declaredScale>;
using Scale = DeclaredPhysicalScale<declaredScale>;
public:
static constexpr bool registered = static_cast<bool>(Declaration::available) &&
Topology::available && Scale::available;
static constexpr bool registered =
static_cast<bool>(Declaration::available) && Topology::available && Scale::available;
using Method = std::conditional_t<
registered,
PhysicalRieszCoordinate<Topology::value, Scale::value>,
@@ -259,7 +255,7 @@ export namespace mean_field::normalization {
template <typename Generated, CoordinateKind Kind, typename = void>
struct DeclaredGeneratedPhysicalRieszCoordinate {
using Method = UnsupportedPhysicalRieszCoordinate;
using Method = UnsupportedPhysicalRieszCoordinate;
static constexpr bool registered = false;
};
@@ -271,9 +267,8 @@ export namespace mean_field::normalization {
typename Generated::SpecificationType,
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Value>>
: CompileDeclaredPhysicalRieszCoordinate<
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Value> { };
: CompileDeclaredPhysicalRieszCoordinate<typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Value> { };
template <typename Generated>
struct DeclaredGeneratedPhysicalRieszCoordinate<
@@ -283,12 +278,10 @@ export namespace mean_field::normalization {
typename Generated::SpecificationType,
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Residual>>
: CompileDeclaredPhysicalRieszCoordinate<
typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Residual> { };
: CompileDeclaredPhysicalRieszCoordinate<typename models::SpecificationContribution<
typename Generated::SpecificationType>::Normalization::Residual> { };
template <typename GeneratedValues, typename GeneratedResiduals>
struct GeneratedPhysicalRieszCoverage {
template <typename GeneratedValues, typename GeneratedResiduals> struct GeneratedPhysicalRieszCoverage {
static constexpr bool complete = false;
};
@@ -297,16 +290,12 @@ export namespace mean_field::normalization {
models::ModelTypeList<GeneratedValues...>,
models::ModelTypeList<GeneratedResiduals...>> {
static constexpr bool complete =
(DeclaredGeneratedPhysicalRieszCoordinate<
GeneratedValues,
CoordinateKind::value>::registered && ...) &&
(DeclaredGeneratedPhysicalRieszCoordinate<
GeneratedResiduals,
CoordinateKind::residual>::registered && ...);
(DeclaredGeneratedPhysicalRieszCoordinate<GeneratedValues, CoordinateKind::value>::registered && ...) &&
(DeclaredGeneratedPhysicalRieszCoordinate<GeneratedResiduals, CoordinateKind::residual>::registered &&
...);
};
template <typename Specification, typename = void>
struct SpecificationPhysicalRieszCoverage {
template <typename Specification, typename = void> struct SpecificationPhysicalRieszCoverage {
static constexpr bool complete = false;
};
@@ -355,29 +344,17 @@ export namespace mean_field::normalization {
* adapter can consult the same authority without importing one another.
*/
template <typename Candidate>
concept PhysicalRieszCoreRuntime =
requires(const std::remove_cvref_t<Candidate> &core) {
{
core.GetGravityContext().GetDensityMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetGravityContext().GetGravityGradientMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetGravityContext().GetGravityPotentialMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetHydrostaticOperator().GetEnthalpyMap()
} -> std::same_as<const field::FieldDofMap &>;
{
core.GetDomainDeformation().parameterCount()
} -> std::same_as<int>;
};
concept PhysicalRieszCoreRuntime = requires(const std::remove_cvref_t<Candidate> &core) {
{ core.GetGravityContext().GetDensityMap() } -> std::same_as<const field::FieldDofMap &>;
{ core.GetGravityContext().GetGravityGradientMap() } -> std::same_as<const field::FieldDofMap &>;
{ core.GetGravityContext().GetGravityPotentialMap() } -> std::same_as<const field::FieldDofMap &>;
{ core.GetHydrostaticOperator().GetEnthalpyMap() } -> std::same_as<const field::FieldDofMap &>;
{ core.GetDomainDeformation().parameterCount() } -> std::same_as<int>;
};
namespace detail {
template <typename Generated, CoordinateKind Kind>
using GeneratedPhysicalRieszMethod =
typename DeclaredGeneratedPhysicalRieszCoordinate<Generated, Kind>::Method;
using GeneratedPhysicalRieszMethod = typename DeclaredGeneratedPhysicalRieszCoordinate<Generated, Kind>::Method;
template <typename Generated, CoordinateKind Kind, typename = void>
struct GeneratedPhysicalRieszRuntimeCoordinate : std::false_type { };
@@ -389,8 +366,7 @@ export namespace mean_field::normalization {
std::void_t<decltype(GeneratedPhysicalRieszMethod<Generated, Kind>::topology)>>
: std::bool_constant<
DeclaredGeneratedPhysicalRieszCoordinate<Generated, Kind>::registered &&
GeneratedPhysicalRieszMethod<Generated, Kind>::topology ==
RieszTopology::global_scalar> { };
GeneratedPhysicalRieszMethod<Generated, Kind>::topology == RieszTopology::global_scalar> { };
template <typename Specification, typename = void>
struct SpecificationPhysicalRieszRuntimeCoverage : std::false_type { };
@@ -420,21 +396,18 @@ export namespace mean_field::normalization {
struct SpecificationSetPhysicalRieszRuntimeCoverage : std::false_type { };
template <models::ModelSpecification... Specifications>
struct SpecificationSetPhysicalRieszRuntimeCoverage<
models::detail::SpecificationSetStorage<Specifications...>>
: std::bool_constant<
(SpecificationPhysicalRieszRuntimeCoverage<Specifications>::value && ...)> { };
struct SpecificationSetPhysicalRieszRuntimeCoverage<models::detail::SpecificationSetStorage<Specifications...>>
: std::bool_constant<(SpecificationPhysicalRieszRuntimeCoverage<Specifications>::value && ...)> { };
} // namespace detail
template <typename Specification>
concept CompleteGeneratedPhysicalRieszRuntimeNormalizationFor =
detail::SpecificationPhysicalRieszRuntimeCoverage<
std::remove_cvref_t<Specification>>::value;
detail::SpecificationPhysicalRieszRuntimeCoverage<std::remove_cvref_t<Specification>>::value;
#define MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(BlockType, TopologyValue, ScaleValue) \
template <> struct PhysicalRieszBlockTraits<BlockType> { \
using Method = PhysicalRieszCoordinate<RieszTopology::TopologyValue, PhysicalScaleKind::ScaleValue>; \
static constexpr bool registered = true; \
#define MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(BlockType, TopologyValue, ScaleValue) \
template <> struct PhysicalRieszBlockTraits<BlockType> { \
using Method = PhysicalRieszCoordinate<RieszTopology::TopologyValue, PhysicalScaleKind::ScaleValue>; \
static constexpr bool registered = true; \
}
MEAN_FIELD_PHYSICAL_RIESZ_TRAIT(
@@ -490,12 +463,9 @@ export namespace mean_field::normalization {
);
#undef MEAN_FIELD_PHYSICAL_RIESZ_TRAIT
template <typename Block>
[[nodiscard]] double physicalScale(
const StellarCharacteristicScales &scales
) {
template <typename Block> [[nodiscard]] double physicalScale(const StellarCharacteristicScales &scales) {
static_assert(PhysicalRieszBlockTraits<Block>::registered, "The block has no Physical Riesz normalization.");
using Method = typename PhysicalRieszBlockTraits<Block>::Method;
using Method = typename PhysicalRieszBlockTraits<Block>::Method;
constexpr PhysicalScaleKind scale = Method::scale;
if constexpr (scale == PhysicalScaleKind::dimensionless) {
return 1.0;
@@ -527,9 +497,7 @@ export namespace mean_field::normalization {
template <typename Values, typename Residuals> struct MakePhysicalRieszPlan;
template <typename... Values, typename... Residuals>
struct MakePhysicalRieszPlan<
utils::blocks::type_list<Values...>,
utils::blocks::type_list<Residuals...>> {
struct MakePhysicalRieszPlan<utils::blocks::type_list<Values...>, utils::blocks::type_list<Residuals...>> {
using Type = NormalizationPlan<
CoordinateComponent<
CoordinateKind::value,
@@ -544,9 +512,8 @@ export namespace mean_field::normalization {
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
using PhysicalRieszNormalizationPlanFor = typename detail::MakePhysicalRieszPlan<
typename Form::value_blocks,
typename Form::residual_blocks>::Type;
using PhysicalRieszNormalizationPlanFor =
typename detail::MakePhysicalRieszPlan<typename Form::value_blocks, typename Form::residual_blocks>::Type;
/*
* Public compile-time extension point for a normalization prescription.
@@ -557,11 +524,10 @@ export namespace mean_field::normalization {
* actually prepare.
*/
template <typename Prescription, typename Form> struct NormalizationCompilation {
using Plan = NormalizationPlan<>;
using Plan = NormalizationPlan<>;
static constexpr bool registered = false;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor = false;
template <typename PhysicalCore, typename SpecificationTypes> static constexpr bool runtimeAvailableFor = false;
};
/* Astronomy/numerics-facing package for a policy which prepares one
@@ -571,7 +537,7 @@ export namespace mean_field::normalization {
template <NormalizationPrescription Prescription, typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct RuntimePreparedNormalizationCompilation {
using Plan = RuntimePreparedNormalizationPlanFor<Prescription, Form>;
using Plan = RuntimePreparedNormalizationPlanFor<Prescription, Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
@@ -581,7 +547,7 @@ export namespace mean_field::normalization {
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<Unnormalized, Form> {
using Plan = IdentityNormalizationPlanFor<Form>;
using Plan = IdentityNormalizationPlanFor<Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
@@ -591,21 +557,18 @@ export namespace mean_field::normalization {
template <RieszGeometryPolicy Geometry, ReferenceScalePolicy ScaleSource, typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<PhysicalRieszDiagonal<Geometry, ScaleSource>, Form> {
using Plan = PhysicalRieszNormalizationPlanFor<Form>;
using Plan = PhysicalRieszNormalizationPlanFor<Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor =
registered &&
PhysicalRieszCoreRuntime<std::remove_cvref_t<PhysicalCore>> &&
detail::SpecificationSetPhysicalRieszRuntimeCoverage<
std::remove_cvref_t<SpecificationTypes>>::value;
registered && PhysicalRieszCoreRuntime<std::remove_cvref_t<PhysicalCore>> &&
detail::SpecificationSetPhysicalRieszRuntimeCoverage<std::remove_cvref_t<SpecificationTypes>>::value;
};
namespace detail {
template <typename Prescription, typename Form, typename = void>
struct NormalizationCompilationAudit {
using Plan = NormalizationPlan<>;
template <typename Prescription, typename Form, typename = void> struct NormalizationCompilationAudit {
using Plan = NormalizationPlan<>;
static constexpr bool registered = false;
};
@@ -616,34 +579,25 @@ export namespace mean_field::normalization {
Prescription,
Form,
std::void_t<
typename NormalizationCompilation<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>::Plan,
decltype(std::bool_constant<static_cast<bool>(
NormalizationCompilation<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>::registered)>{})>> {
using Compilation = NormalizationCompilation<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>;
using Plan = typename Compilation::Plan;
typename NormalizationCompilation<std::remove_cvref_t<Prescription>, std::remove_cvref_t<Form>>::Plan,
decltype(std::bool_constant<static_cast<bool>(NormalizationCompilation<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>::registered)>{})>> {
using Compilation = NormalizationCompilation<std::remove_cvref_t<Prescription>, std::remove_cvref_t<Form>>;
using Plan = typename Compilation::Plan;
static constexpr bool registered =
static_cast<bool>(Compilation::registered) &&
CompleteNormalizationFor<Plan, std::remove_cvref_t<Form>>;
static_cast<bool>(Compilation::registered) && CompleteNormalizationFor<Plan, std::remove_cvref_t<Form>>;
};
} // namespace detail
template <NormalizationPrescription Prescription, typename Form>
using NormalizationPlanFor = typename detail::NormalizationCompilationAudit<
std::remove_cvref_t<Prescription>,
Form>::Plan;
using NormalizationPlanFor =
typename detail::NormalizationCompilationAudit<std::remove_cvref_t<Prescription>, Form>::Plan;
template <typename Prescription, typename Form>
concept CompilableNormalizationFor =
detail::NormalizationCompilationAudit<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>::registered;
detail::NormalizationCompilationAudit<std::remove_cvref_t<Prescription>, std::remove_cvref_t<Form>>::registered;
/* The public runtime-preparation adapter is intentionally narrower than
* an arbitrary complete plan: every coordinate must name the exact policy
@@ -654,16 +608,10 @@ export namespace mean_field::normalization {
concept RuntimePreparedNormalizationFor =
NormalizationPrescription<std::remove_cvref_t<Prescription>> &&
utils::blocks::block_form_is_valid_v<std::remove_cvref_t<Form>> &&
CompilableNormalizationFor<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>> &&
CompilableNormalizationFor<std::remove_cvref_t<Prescription>, std::remove_cvref_t<Form>> &&
std::same_as<
NormalizationPlanFor<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>,
RuntimePreparedNormalizationPlanFor<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>>>;
NormalizationPlanFor<std::remove_cvref_t<Prescription>, std::remove_cvref_t<Form>>,
RuntimePreparedNormalizationPlanFor<std::remove_cvref_t<Prescription>, std::remove_cvref_t<Form>>>;
namespace detail {
template <
@@ -674,35 +622,22 @@ export namespace mean_field::normalization {
typename = void>
struct StellarNormalizationRuntimeAudit : std::false_type { };
template <
typename Prescription,
typename Form,
typename PhysicalCore,
typename SpecificationTypes>
template <typename Prescription, typename Form, typename PhysicalCore, typename SpecificationTypes>
struct StellarNormalizationRuntimeAudit<
Prescription,
Form,
PhysicalCore,
SpecificationTypes,
std::void_t<
std::enable_if_t<NormalizationCompilationAudit<
Prescription,
Form>::registered>,
decltype(std::bool_constant<static_cast<bool>(
NormalizationCompilation<
Prescription,
Form>::template runtimeAvailableFor<
PhysicalCore,
SpecificationTypes>)>{})>>
std::enable_if_t<NormalizationCompilationAudit<Prescription, Form>::registered>,
decltype(std::bool_constant<
static_cast<bool>(NormalizationCompilation<Prescription, Form>::
template runtimeAvailableFor<PhysicalCore, SpecificationTypes>)>{})>>
: std::bool_constant<
(std::same_as<Prescription, Unnormalized> ||
PhysicalRieszDiagonalPrescription<Prescription> ||
(std::same_as<Prescription, Unnormalized> || PhysicalRieszDiagonalPrescription<Prescription> ||
RuntimePreparedNormalizationFor<Prescription, Form>) &&
static_cast<bool>(NormalizationCompilation<
Prescription,
Form>::template runtimeAvailableFor<
PhysicalCore,
SpecificationTypes>)> { };
static_cast<bool>(NormalizationCompilation<Prescription, Form>::
template runtimeAvailableFor<PhysicalCore, SpecificationTypes>)> { };
} // namespace detail
/*
@@ -714,15 +649,10 @@ export namespace mean_field::normalization {
* assembly and global-scalar runtime preparation for every generated
* coordinate in the specification pack.
*/
template <
typename Prescription,
typename Form,
typename PhysicalCore,
typename SpecificationTypes>
concept StellarNormalizationRuntimeAvailableFor =
detail::StellarNormalizationRuntimeAudit<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>,
std::remove_cvref_t<PhysicalCore>,
std::remove_cvref_t<SpecificationTypes>>::value;
template <typename Prescription, typename Form, typename PhysicalCore, typename SpecificationTypes>
concept StellarNormalizationRuntimeAvailableFor = detail::StellarNormalizationRuntimeAudit<
std::remove_cvref_t<Prescription>,
std::remove_cvref_t<Form>,
std::remove_cvref_t<PhysicalCore>,
std::remove_cvref_t<SpecificationTypes>>::value;
} // namespace mean_field::normalization

View File

@@ -11,10 +11,7 @@ export namespace mean_field::normalization {
struct NormalizationPrescriptionTag { };
template <typename Candidate>
concept NormalizationPrescription =
std::derived_from<
std::remove_cvref_t<Candidate>,
NormalizationPrescriptionTag>;
concept NormalizationPrescription = std::derived_from<std::remove_cvref_t<Candidate>, NormalizationPrescriptionTag>;
enum class CoordinateKind { value, residual };
@@ -50,44 +47,36 @@ export namespace mean_field::normalization {
* numerical value of that factor. The owner type prevents one policy from
* silently presenting another policy's runtime map as its own plan.
*/
template <NormalizationPrescription Prescription>
struct RuntimePreparedCoordinate final {
template <NormalizationPrescription Prescription> struct RuntimePreparedCoordinate final {
using PrescriptionType = std::remove_cvref_t<Prescription>;
};
template <RieszTopology Topology, PhysicalScaleKind Scale> struct PhysicalRieszCoordinate final {
static constexpr RieszTopology topology = Topology;
static constexpr PhysicalScaleKind scale = Scale;
static constexpr RieszTopology topology = Topology;
static constexpr PhysicalScaleKind scale = Scale;
};
struct UnsupportedPhysicalRieszCoordinate final { };
template <typename Block> struct PhysicalRieszBlockTraits {
using Method = UnsupportedPhysicalRieszCoordinate;
static constexpr bool registered = false;
using Method = UnsupportedPhysicalRieszCoordinate;
static constexpr bool registered = false;
};
template <CoordinateKind Kind, typename BlockList, typename MethodType>
struct CoordinateComponent final {
template <CoordinateKind Kind, typename BlockList, typename MethodType> struct CoordinateComponent final {
using Blocks = BlockList;
using Method = MethodType;
static constexpr CoordinateKind kind = Kind;
using ValueBlocks = std::conditional_t<
Kind == CoordinateKind::value,
BlockList,
utils::blocks::type_list<>>;
using ResidualBlocks = std::conditional_t<
Kind == CoordinateKind::residual,
BlockList,
utils::blocks::type_list<>>;
using ValueBlocks = std::conditional_t<Kind == CoordinateKind::value, BlockList, utils::blocks::type_list<>>;
using ResidualBlocks =
std::conditional_t<Kind == CoordinateKind::residual, BlockList, utils::blocks::type_list<>>;
};
namespace detail {
template <typename Candidate> struct IsTypeList : std::false_type { };
template <typename... Types>
struct IsTypeList<utils::blocks::type_list<Types...>> : std::true_type { };
template <typename... Types> struct IsTypeList<utils::blocks::type_list<Types...>> : std::true_type { };
template <typename List, typename Base> struct IsUniqueDerivedBlockList : std::false_type { };
@@ -102,8 +91,7 @@ export namespace mean_field::normalization {
template <> struct IsCoordinateMethod<IdentityCoordinate> : std::true_type { };
template <NormalizationPrescription Prescription>
struct IsCoordinateMethod<RuntimePreparedCoordinate<Prescription>>
: std::true_type { };
struct IsCoordinateMethod<RuntimePreparedCoordinate<Prescription>> : std::true_type { };
template <RieszTopology Topology, PhysicalScaleKind Scale>
struct IsCoordinateMethod<PhysicalRieszCoordinate<Topology, Scale>> : std::true_type { };
@@ -121,10 +109,9 @@ export namespace mean_field::normalization {
template <RieszTopology Topology, PhysicalScaleKind Scale, typename Block>
struct MethodSupportsBlock<PhysicalRieszCoordinate<Topology, Scale>, Block>
: std::bool_constant<
PhysicalRieszBlockTraits<Block>::registered &&
std::same_as<
typename PhysicalRieszBlockTraits<Block>::Method,
PhysicalRieszCoordinate<Topology, Scale>>> { };
PhysicalRieszBlockTraits<Block>::registered && std::same_as<
typename PhysicalRieszBlockTraits<Block>::Method,
PhysicalRieszCoordinate<Topology, Scale>>> { };
template <typename Method, typename List> struct MethodSupportsEveryBlock : std::false_type { };
@@ -166,8 +153,7 @@ export namespace mean_field::normalization {
}();
static constexpr bool hasCoherentCoordinateLists = [] {
if constexpr (!hasValidKind || !IsTypeList<ValueBlocks>::value ||
!IsTypeList<ResidualBlocks>::value) {
if constexpr (!hasValidKind || !IsTypeList<ValueBlocks>::value || !IsTypeList<ResidualBlocks>::value) {
return false;
} else if constexpr (Candidate::kind == CoordinateKind::value) {
return std::same_as<ValueBlocks, Blocks> &&
@@ -182,8 +168,7 @@ export namespace mean_field::normalization {
static constexpr bool valid = hasValidKind && IsTypeList<Blocks>::value &&
IsCoordinateMethod<Method>::value && hasValidBlockList &&
hasCoherentCoordinateLists &&
MethodSupportsEveryBlock<Method, Blocks>::value;
hasCoherentCoordinateLists && MethodSupportsEveryBlock<Method, Blocks>::value;
};
template <typename... Lists> struct Concatenate;
@@ -205,23 +190,18 @@ export namespace mean_field::normalization {
template <typename List, typename Type> struct Append;
template <typename... Types, typename Appended>
struct Append<utils::blocks::type_list<Types...>, Appended> {
template <typename... Types, typename Appended> struct Append<utils::blocks::type_list<Types...>, Appended> {
using Type = utils::blocks::type_list<Types..., Appended>;
};
template <typename List, typename Type> using AppendT = typename Append<List, Type>::Type;
template <typename List, typename Type>
using AppendUniqueT = std::conditional_t<
utils::blocks::contains_type_v<Type, List>,
List,
AppendT<List, Type>>;
using AppendUniqueT = std::conditional_t<utils::blocks::contains_type_v<Type, List>, List, AppendT<List, Type>>;
template <typename Source, typename Excluded> struct ListDifference;
template <typename Excluded>
struct ListDifference<utils::blocks::type_list<>, Excluded> {
template <typename Excluded> struct ListDifference<utils::blocks::type_list<>, Excluded> {
using Type = utils::blocks::type_list<>;
};
@@ -260,10 +240,7 @@ export namespace mean_field::normalization {
};
template <typename List>
using RepeatedTypesT = typename CollectRepeatedTypes<
List,
List,
utils::blocks::type_list<>>::Type;
using RepeatedTypesT = typename CollectRepeatedTypes<List, List, utils::blocks::type_list<>>::Type;
template <typename Candidate, typename = void> struct PlanTraits {
static constexpr bool valid = false;
@@ -274,23 +251,20 @@ export namespace mean_field::normalization {
concept NormalizationComponent = detail::ComponentTraits<std::remove_cvref_t<Candidate>>::valid;
template <typename... Components> struct NormalizationPlan final {
using ComponentTypes = utils::blocks::type_list<Components...>;
using ValueBlocks = detail::ConcatenateT<typename Components::ValueBlocks...>;
using ResidualBlocks = detail::ConcatenateT<typename Components::ResidualBlocks...>;
using ComponentTypes = utils::blocks::type_list<Components...>;
using ValueBlocks = detail::ConcatenateT<typename Components::ValueBlocks...>;
using ResidualBlocks = detail::ConcatenateT<typename Components::ResidualBlocks...>;
};
namespace detail {
template <typename... Components>
struct PlanTraits<NormalizationPlan<Components...>> {
template <typename... Components> struct PlanTraits<NormalizationPlan<Components...>> {
static constexpr bool valid = (ComponentTraits<Components>::valid && ...);
};
template <typename Values, typename Residuals> struct MakeIdentityPlan;
template <typename... Values, typename... Residuals>
struct MakeIdentityPlan<
utils::blocks::type_list<Values...>,
utils::blocks::type_list<Residuals...>> {
struct MakeIdentityPlan<utils::blocks::type_list<Values...>, utils::blocks::type_list<Residuals...>> {
using Type = NormalizationPlan<
CoordinateComponent<CoordinateKind::value, utils::blocks::type_list<Values>, IdentityCoordinate>...,
CoordinateComponent<
@@ -299,30 +273,18 @@ export namespace mean_field::normalization {
IdentityCoordinate>...>;
};
template <
NormalizationPrescription Prescription,
typename Values,
typename Residuals>
template <NormalizationPrescription Prescription, typename Values, typename Residuals>
struct MakeRuntimePreparedPlan;
template <
NormalizationPrescription Prescription,
typename... Values,
typename... Residuals>
template <NormalizationPrescription Prescription, typename... Values, typename... Residuals>
struct MakeRuntimePreparedPlan<
Prescription,
utils::blocks::type_list<Values...>,
utils::blocks::type_list<Residuals...>> {
using Method = RuntimePreparedCoordinate<Prescription>;
using Type = NormalizationPlan<
CoordinateComponent<
CoordinateKind::value,
utils::blocks::type_list<Values>,
Method>...,
CoordinateComponent<
CoordinateKind::residual,
utils::blocks::type_list<Residuals>,
Method>...>;
using Type = NormalizationPlan<
CoordinateComponent<CoordinateKind::value, utils::blocks::type_list<Values>, Method>...,
CoordinateComponent<CoordinateKind::residual, utils::blocks::type_list<Residuals>, Method>...>;
};
} // namespace detail
@@ -331,46 +293,43 @@ export namespace mean_field::normalization {
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
using IdentityNormalizationPlanFor = typename detail::MakeIdentityPlan<
typename Form::value_blocks,
typename Form::residual_blocks>::Type;
using IdentityNormalizationPlanFor =
typename detail::MakeIdentityPlan<typename Form::value_blocks, typename Form::residual_blocks>::Type;
template <NormalizationPrescription Prescription, typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
using RuntimePreparedNormalizationPlanFor =
typename detail::MakeRuntimePreparedPlan<
std::remove_cvref_t<Prescription>,
typename Form::value_blocks,
typename Form::residual_blocks>::Type;
using RuntimePreparedNormalizationPlanFor = typename detail::MakeRuntimePreparedPlan<
std::remove_cvref_t<Prescription>,
typename Form::value_blocks,
typename Form::residual_blocks>::Type;
template <typename Form, typename Plan>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCoverage final {
using DeclaredValueBlocks = typename Plan::ValueBlocks;
using DeclaredValueBlocks = typename Plan::ValueBlocks;
using DeclaredResidualBlocks = typename Plan::ResidualBlocks;
using MissingValueBlocks = detail::ListDifferenceT<typename Form::value_blocks, DeclaredValueBlocks>;
using UnexpectedValueBlocks = detail::ListDifferenceT<DeclaredValueBlocks, typename Form::value_blocks>;
using RepeatedValueBlocks = detail::RepeatedTypesT<DeclaredValueBlocks>;
using MissingValueBlocks = detail::ListDifferenceT<typename Form::value_blocks, DeclaredValueBlocks>;
using UnexpectedValueBlocks = detail::ListDifferenceT<DeclaredValueBlocks, typename Form::value_blocks>;
using RepeatedValueBlocks = detail::RepeatedTypesT<DeclaredValueBlocks>;
using MissingResidualBlocks = detail::ListDifferenceT<typename Form::residual_blocks, DeclaredResidualBlocks>;
using UnexpectedResidualBlocks = detail::ListDifferenceT<DeclaredResidualBlocks, typename Form::residual_blocks>;
using RepeatedResidualBlocks = detail::RepeatedTypesT<DeclaredResidualBlocks>;
using MissingResidualBlocks = detail::ListDifferenceT<typename Form::residual_blocks, DeclaredResidualBlocks>;
using UnexpectedResidualBlocks =
detail::ListDifferenceT<DeclaredResidualBlocks, typename Form::residual_blocks>;
using RepeatedResidualBlocks = detail::RepeatedTypesT<DeclaredResidualBlocks>;
static constexpr bool hasEveryValueBlock = MissingValueBlocks::size == 0;
static constexpr bool hasOnlyValueBlocks = UnexpectedValueBlocks::size == 0;
static constexpr bool hasUniqueValueOwners = RepeatedValueBlocks::size == 0;
static constexpr bool hasEveryResidualBlock = MissingResidualBlocks::size == 0;
static constexpr bool hasOnlyResidualBlocks = UnexpectedResidualBlocks::size == 0;
static constexpr bool hasEveryValueBlock = MissingValueBlocks::size == 0;
static constexpr bool hasOnlyValueBlocks = UnexpectedValueBlocks::size == 0;
static constexpr bool hasUniqueValueOwners = RepeatedValueBlocks::size == 0;
static constexpr bool hasEveryResidualBlock = MissingResidualBlocks::size == 0;
static constexpr bool hasOnlyResidualBlocks = UnexpectedResidualBlocks::size == 0;
static constexpr bool hasUniqueResidualOwners = RepeatedResidualBlocks::size == 0;
static constexpr bool complete = hasEveryValueBlock && hasOnlyValueBlocks && hasUniqueValueOwners &&
hasEveryResidualBlock && hasOnlyResidualBlocks &&
hasUniqueResidualOwners;
hasEveryResidualBlock && hasOnlyResidualBlocks && hasUniqueResidualOwners;
};
template <typename Plan, typename Form>
concept CompleteNormalizationFor = utils::blocks::block_form_is_valid_v<Form> &&
NormalizationPlanType<Plan> &&
concept CompleteNormalizationFor = utils::blocks::block_form_is_valid_v<Form> && NormalizationPlanType<Plan> &&
NormalizationCoverage<Form, std::remove_cvref_t<Plan>>::complete;
} // namespace mean_field::normalization

View File

@@ -2,6 +2,7 @@ module;
#include <concepts>
#include <cstdint>
#include <expected>
#include <memory>
#include <span>
#include <stdexcept>
@@ -61,7 +62,7 @@ namespace mean_field::normalization::detail {
const field::ScalarBoundaryDofMap &surfaceMap
) {
mfem::Array<int> marker(finiteElements.mesh->bdr_attributes.Max());
marker = 0;
marker = 0;
constexpr int attribute = DomainSchema::template boundary_attribute<utils::domain::StellarSurface>();
if (attribute <= 0 || attribute > marker.Size()) {
throw std::invalid_argument("The reference mesh does not contain the stellar-surface boundary.");
@@ -107,25 +108,19 @@ export namespace mean_field::normalization {
* this layer never names a concrete integral or phase constraint.
*/
namespace detail {
template <typename Block>
using PhysicalRieszMethodFor = typename PhysicalRieszBlockTraits<Block>::Method;
template <typename Block> using PhysicalRieszMethodFor = typename PhysicalRieszBlockTraits<Block>::Method;
template <typename Block, typename = void>
struct IsGlobalGeneratedValueNormalization : std::false_type { };
template <typename Block, typename = void> struct IsGlobalGeneratedValueNormalization : std::false_type { };
template <typename Generated>
struct IsGlobalGeneratedValueNormalization<
utils::blocks::generated_value_block<Generated>,
std::void_t<
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_value_block<Generated>>::topology),
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_value_block<Generated>>::scale)>>
decltype(PhysicalRieszMethodFor<utils::blocks::generated_value_block<Generated>>::topology),
decltype(PhysicalRieszMethodFor<utils::blocks::generated_value_block<Generated>>::scale)>>
: std::bool_constant<
PhysicalRieszBlockTraits<
utils::blocks::generated_value_block<Generated>>::registered &&
PhysicalRieszMethodFor<
utils::blocks::generated_value_block<Generated>>::topology ==
PhysicalRieszBlockTraits<utils::blocks::generated_value_block<Generated>>::registered &&
PhysicalRieszMethodFor<utils::blocks::generated_value_block<Generated>>::topology ==
RieszTopology::global_scalar> { };
template <typename Blocks, typename Specification>
@@ -139,32 +134,26 @@ export namespace mean_field::normalization {
Generated,
Specification,
std::void_t<typename Generated::SpecificationType>>
: std::bool_constant<
std::same_as<typename Generated::SpecificationType, Specification>> { };
: std::bool_constant<std::same_as<typename Generated::SpecificationType, Specification>> { };
template <typename Specification, typename... Generated>
struct GeneratedValueBlocksBelongToSpecification<
utils::blocks::type_list<utils::blocks::generated_value_block<Generated>...>,
Specification>
: std::bool_constant<
(GeneratedCoordinateBelongsToSpecification<Generated, Specification>::value && ...)> { };
: std::bool_constant<(GeneratedCoordinateBelongsToSpecification<Generated, Specification>::value && ...)> {
};
template <typename Block, typename = void>
struct IsGlobalGeneratedResidualNormalization : std::false_type { };
template <typename Block, typename = void> struct IsGlobalGeneratedResidualNormalization : std::false_type { };
template <typename Generated>
struct IsGlobalGeneratedResidualNormalization<
utils::blocks::generated_residual_block<Generated>,
std::void_t<
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_residual_block<Generated>>::topology),
decltype(PhysicalRieszMethodFor<
utils::blocks::generated_residual_block<Generated>>::scale)>>
decltype(PhysicalRieszMethodFor<utils::blocks::generated_residual_block<Generated>>::topology),
decltype(PhysicalRieszMethodFor<utils::blocks::generated_residual_block<Generated>>::scale)>>
: std::bool_constant<
PhysicalRieszBlockTraits<
utils::blocks::generated_residual_block<Generated>>::registered &&
PhysicalRieszMethodFor<
utils::blocks::generated_residual_block<Generated>>::topology ==
PhysicalRieszBlockTraits<utils::blocks::generated_residual_block<Generated>>::registered &&
PhysicalRieszMethodFor<utils::blocks::generated_residual_block<Generated>>::topology ==
RieszTopology::global_scalar> { };
template <typename Blocks, typename Specification>
@@ -174,14 +163,13 @@ export namespace mean_field::normalization {
struct GeneratedResidualBlocksBelongToSpecification<
utils::blocks::type_list<utils::blocks::generated_residual_block<Generated>...>,
Specification>
: std::bool_constant<
(GeneratedCoordinateBelongsToSpecification<Generated, Specification>::value && ...)> { };
: std::bool_constant<(GeneratedCoordinateBelongsToSpecification<Generated, Specification>::value && ...)> {
};
template <typename Blocks> struct PrepareGeneratedValueNormalizations {
static constexpr bool registered = false;
static constexpr bool registered = false;
template <typename Form>
static constexpr bool completeFor = false;
template <typename Form> static constexpr bool completeFor = false;
template <typename Form>
static void Apply(
@@ -192,14 +180,12 @@ export namespace mean_field::normalization {
}
};
template <typename... Blocks>
struct PrepareGeneratedValueNormalizations<utils::blocks::type_list<Blocks...>> {
static constexpr bool registered =
(IsGlobalGeneratedValueNormalization<Blocks>::value && ...);
template <typename... Blocks> struct PrepareGeneratedValueNormalizations<utils::blocks::type_list<Blocks...>> {
static constexpr bool registered = (IsGlobalGeneratedValueNormalization<Blocks>::value && ...);
template <typename Form>
static constexpr bool completeFor = registered &&
utils::blocks::block_form_is_valid_v<Form> &&
static constexpr bool completeFor =
registered && utils::blocks::block_form_is_valid_v<Form> &&
(utils::blocks::contains_type_v<Blocks, typename Form::value_blocks> && ...);
template <typename Form>
@@ -220,10 +206,9 @@ export namespace mean_field::normalization {
};
template <typename Blocks> struct PrepareGeneratedResidualNormalizations {
static constexpr bool registered = false;
static constexpr bool registered = false;
template <typename Form>
static constexpr bool completeFor = false;
template <typename Form> static constexpr bool completeFor = false;
template <typename Form>
static void Apply(
@@ -236,12 +221,11 @@ export namespace mean_field::normalization {
template <typename... Blocks>
struct PrepareGeneratedResidualNormalizations<utils::blocks::type_list<Blocks...>> {
static constexpr bool registered =
(IsGlobalGeneratedResidualNormalization<Blocks>::value && ...);
static constexpr bool registered = (IsGlobalGeneratedResidualNormalization<Blocks>::value && ...);
template <typename Form>
static constexpr bool completeFor = registered &&
utils::blocks::block_form_is_valid_v<Form> &&
static constexpr bool completeFor =
registered && utils::blocks::block_form_is_valid_v<Form> &&
(utils::blocks::contains_type_v<Blocks, typename Form::residual_blocks> && ...);
template <typename Form>
@@ -261,15 +245,13 @@ export namespace mean_field::normalization {
}
};
template <typename Specification, typename = void>
struct CompileStellarSpecificationNormalization {
using ValuePreparation = PrepareGeneratedValueNormalizations<void>;
using ResidualPreparation = PrepareGeneratedResidualNormalizations<void>;
template <typename Specification, typename = void> struct CompileStellarSpecificationNormalization {
using ValuePreparation = PrepareGeneratedValueNormalizations<void>;
using ResidualPreparation = PrepareGeneratedResidualNormalizations<void>;
static constexpr bool registered = false;
static constexpr bool registered = false;
template <typename Form>
static constexpr bool completeFor = false;
template <typename Form> static constexpr bool completeFor = false;
template <typename Form>
static void Apply(
@@ -277,8 +259,7 @@ export namespace mean_field::normalization {
const StellarCharacteristicScales &
) {
static_assert(
completeFor<Form>,
"The specification has no complete generated-coordinate normalization."
completeFor<Form>, "The specification has no complete generated-coordinate normalization."
);
}
};
@@ -287,32 +268,27 @@ export namespace mean_field::normalization {
struct CompileStellarSpecificationNormalization<
Specification,
std::void_t<
typename operators::StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedValueBlocks,
typename operators::StellarEquilibriumSpecificationCompilation<Specification>::GeneratedValueBlocks,
typename operators::StellarEquilibriumSpecificationCompilation<
Specification>::GeneratedResidualBlocks>> {
using OperatorCompilation =
operators::StellarEquilibriumSpecificationCompilation<Specification>;
using ValuePreparation = PrepareGeneratedValueNormalizations<
typename OperatorCompilation::GeneratedValueBlocks>;
using ResidualPreparation = PrepareGeneratedResidualNormalizations<
typename OperatorCompilation::GeneratedResidualBlocks>;
using OperatorCompilation = operators::StellarEquilibriumSpecificationCompilation<Specification>;
using ValuePreparation =
PrepareGeneratedValueNormalizations<typename OperatorCompilation::GeneratedValueBlocks>;
using ResidualPreparation =
PrepareGeneratedResidualNormalizations<typename OperatorCompilation::GeneratedResidualBlocks>;
static constexpr bool registered = OperatorCompilation::complete &&
models::CompleteGeneratedNormalizationFor<
Specification> &&
models::CompleteGeneratedNormalizationFor<Specification> &&
GeneratedValueBlocksBelongToSpecification<
typename OperatorCompilation::GeneratedValueBlocks,
Specification>::value &&
GeneratedResidualBlocksBelongToSpecification<
typename OperatorCompilation::GeneratedResidualBlocks,
Specification>::value &&
ValuePreparation::registered &&
ResidualPreparation::registered;
ValuePreparation::registered && ResidualPreparation::registered;
template <typename Form>
static constexpr bool completeFor = registered &&
ValuePreparation::template completeFor<Form> &&
static constexpr bool completeFor = registered && ValuePreparation::template completeFor<Form> &&
ResidualPreparation::template completeFor<Form>;
template <typename Form>
@@ -366,9 +342,8 @@ export namespace mean_field::normalization {
Model,
Form,
std::void_t<typename std::remove_cvref_t<Model>::SpecificationTypes>>
: std::bool_constant<
PrepareSpecificationNormalizations<
typename std::remove_cvref_t<Model>::SpecificationTypes>::template completeFor<Form>> { };
: std::bool_constant<PrepareSpecificationNormalizations<
typename std::remove_cvref_t<Model>::SpecificationTypes>::template completeFor<Form>> { };
} // namespace detail
template <typename Specification>
@@ -400,9 +375,7 @@ export namespace mean_field::normalization {
template <typename Model, typename Form>
concept CompleteStellarNormalizationFor =
detail::StellarModelNormalizationCoverage<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Form>>::value;
detail::StellarModelNormalizationCoverage<std::remove_cvref_t<Model>, std::remove_cvref_t<Form>>::value;
/*
* Physical Riesz preparation is an optional capability of a physical
@@ -418,8 +391,7 @@ export namespace mean_field::normalization {
template <typename Problem>
concept PhysicalRieszStellarEquilibriumProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
requires {
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> && requires {
typename std::remove_cvref_t<Problem>::ModelType;
typename std::remove_cvref_t<Problem>::FormType;
typename std::remove_cvref_t<Problem>::PhysicalCoreType;
@@ -430,8 +402,7 @@ export namespace mean_field::normalization {
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Problem>::FormType>;
requires CompleteStellarNormalizationFor<
typename std::remove_cvref_t<Problem>::ModelType,
typename std::remove_cvref_t<Problem>::FormType>;
typename std::remove_cvref_t<Problem>::ModelType, typename std::remove_cvref_t<Problem>::FormType>;
requires StellarNormalizationRuntimeAvailableFor<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
typename std::remove_cvref_t<Problem>::FormType,
@@ -450,42 +421,36 @@ export namespace mean_field::normalization {
template <PhysicalRieszStellarEquilibriumProblem Problem>
[[nodiscard]] DiagonalNormalization prepareNormalization(const Problem &problem) {
using ProblemType = std::remove_cvref_t<Problem>;
using Form = typename ProblemType::FormType;
using Form = typename ProblemType::FormType;
const fem::FEM &finiteElements = problem.GetDiscretization().finiteElementModel();
const fem::FEM &finiteElements =
equilibrium::detail::StellarEquilibriumProblemFactory::FiniteElementModel(problem);
if (!finiteElements.okay()) {
throw std::invalid_argument("Physical Riesz preparation requires a current finite-element model.");
}
const auto &physical = detail::PhysicalOperator(problem);
const auto &physical = detail::PhysicalOperator(problem);
const auto &gravityContext = physical.GetGravityContext();
const auto &enthalpyMap = physical.GetHydrostaticOperator().GetEnthalpyMap();
const auto scales = deriveStellarCharacteristicScales(
problem.GetNormalizationPrescription(),
problem.GetStellarModel()
);
const auto &enthalpyMap = physical.GetHydrostaticOperator().GetEnthalpyMap();
const auto scales =
deriveStellarCharacteristicScales(problem.GetNormalizationPrescription(), problem.GetStellarModel());
mfem::Array<int> stellarMarker =
utils::domain::make_attribute_marker<utils::domain::Stellar, detail::DomainSchema>(*finiteElements.mesh);
const mfem::Vector densityDiagonal = detail::GatherDiagonal(
detail::AssembleScalarMassDiagonal(*finiteElements.densityFes, &stellarMarker),
gravityContext.GetDensityMap(),
"density"
gravityContext.GetDensityMap(), "density"
);
const mfem::Vector enthalpyDiagonal = detail::GatherDiagonal(
detail::AssembleScalarMassDiagonal(*finiteElements.enthalpyFes, &stellarMarker),
enthalpyMap,
"enthalpy"
detail::AssembleScalarMassDiagonal(*finiteElements.enthalpyFes, &stellarMarker), enthalpyMap, "enthalpy"
);
const mfem::Vector gravityGradientDiagonal = detail::GatherDiagonal(
detail::AssembleHDivMassDiagonal(*finiteElements.gravityFluxFes),
gravityContext.GetGravityGradientMap(),
detail::AssembleHDivMassDiagonal(*finiteElements.gravityFluxFes), gravityContext.GetGravityGradientMap(),
"gravity-gradient"
);
const mfem::Vector gravityPotentialDiagonal = detail::GatherDiagonal(
detail::AssembleScalarMassDiagonal(*finiteElements.gravityPotentialFes),
gravityContext.GetGravityPotentialMap(),
"gravity-potential"
gravityContext.GetGravityPotentialMap(), "gravity-potential"
);
const field::ScalarBoundaryDofMap surfaceMap =
field::make_stellar_surface_scalar_dof_map<detail::DomainSchema>(*finiteElements.surfaceDeformationFes);
@@ -524,13 +489,11 @@ export namespace mean_field::normalization {
);
const mfem::Array<int> &surfaceRows = problem.GetPressureSurfaceRows().reduced_dofs();
builder.template SetHybridResidualBlock<utils::blocks::enthalpy::specific::residual>(
physicalScale<utils::blocks::enthalpy::specific::residual>(scales),
enthalpyDiagonal,
physicalScale<utils::blocks::enthalpy::specific::residual>(scales), enthalpyDiagonal,
std::span<const int>{surfaceRows.GetData(), static_cast<std::size_t>(surfaceRows.Size())}
);
detail::PrepareSpecificationNormalizations<typename ProblemType::ModelType::SpecificationTypes>::Apply(
builder,
scales
builder, scales
);
return std::move(builder).Build();
@@ -548,16 +511,11 @@ export namespace mean_field::normalization {
!std::same_as<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
Unnormalized> &&
!PhysicalRieszDiagonalPrescription<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType> &&
RuntimePreparedNormalizationOperation<Problem>)
[[nodiscard]] DiagonalNormalization prepareNormalization(
const Problem &problem
) {
return prepareStellarNormalization(
problem.GetNormalizationPrescription(),
problem
);
!PhysicalRieszDiagonalPrescription<typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType> &&
RuntimePreparedNormalizationOperation<Problem>
)
[[nodiscard]] DiagonalNormalization prepareNormalization(const Problem &problem) {
return prepareStellarNormalization(problem.GetNormalizationPrescription(), problem);
}
/*
@@ -571,9 +529,7 @@ export namespace mean_field::normalization {
concept NormalizableStellarEquilibriumProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
requires(const std::remove_cvref_t<Problem> &problem) {
{
prepareNormalization(problem)
} -> std::same_as<DiagonalNormalization>;
{ prepareNormalization(problem) } -> std::same_as<DiagonalNormalization>;
};
struct NormalizedStellarEquilibriumStatistics final {
@@ -591,17 +547,14 @@ export namespace mean_field::normalization {
* implied.
*/
template <typename Candidate, typename Problem>
concept ProblemBoundStellarInverseFor =
NormalizableStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
std::derived_from<std::remove_cvref_t<Candidate>, mfem::Solver> &&
requires(const std::remove_cvref_t<Candidate> &inverse) {
{
inverse.GetProblem()
} -> std::same_as<const std::remove_cvref_t<Problem> &>;
{
inverse.IsCurrent()
} -> std::same_as<bool>;
};
concept ProblemBoundStellarInverseFor = NormalizableStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
std::derived_from<std::remove_cvref_t<Candidate>, mfem::Solver> &&
requires(const std::remove_cvref_t<Candidate> &inverse) {
{
inverse.GetProblem()
} -> std::same_as<const std::remove_cvref_t<Problem> &>;
{ inverse.IsCurrent() } -> std::same_as<bool>;
};
template <NormalizableStellarEquilibriumProblem Problem, typename PhysicalInverse>
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
@@ -623,11 +576,20 @@ export namespace mean_field::normalization {
using ProblemType = std::remove_cvref_t<Problem>;
public:
using Report = typename ProblemType::Report;
using PreparationResult = typename ProblemType::PreparationResult;
explicit NormalizedStellarEquilibriumOperator(ProblemType &problem)
: mfem::Operator(problem.EquationSize(), problem.StateSize()),
: mfem::Operator(
problem.EquationSize(),
problem.StateSize()
),
m_problem(&problem),
m_normalization(prepareNormalization(problem)),
m_scaledJacobian(problem.GetLinearizationOperator(), m_normalization),
m_scaledJacobian(
problem.GetLinearizationOperator(),
m_normalization
),
m_physicalState(problem.StateSize()),
m_physicalResidual(problem.EquationSize()),
m_normalizedResidual(problem.EquationSize()) {
@@ -646,7 +608,9 @@ export namespace mean_field::normalization {
const mfem::Vector &normalizedState,
const operators::StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) requires(ProblemType::generatedRotationProviderCount == 0) {
)
requires(ProblemType::generatedRotationProviderCount == 0)
{
if (normalizedState.Size() != Width()) {
throw std::invalid_argument("The normalized stellar state has the wrong size.");
}
@@ -662,10 +626,37 @@ export namespace mean_field::normalization {
return report;
}
[[nodiscard]] PreparationResult TryPrepare(
const mfem::Vector &normalizedState,
const operators::StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
)
requires(ProblemType::generatedRotationProviderCount == 0)
{
if (normalizedState.Size() != Width()) {
throw std::invalid_argument("The normalized stellar state has the wrong size.");
}
m_isPrepared = false;
m_normalization.DenormalizeState(normalizedState, m_physicalState);
auto result = m_problem->TryPrepare(m_physicalState, dependencies, rotation);
if (!result.has_value()) {
return std::unexpected(result.error());
}
m_problem->BuildResidual(m_physicalResidual);
m_normalization.NormalizeResidual(m_physicalResidual, m_normalizedResidual);
m_physicalPreparationGeneration = m_problem->GetPreparationGeneration();
m_isPrepared = true;
++m_statistics.physicalPreparations;
return result;
}
[[nodiscard]] auto Prepare(
const mfem::Vector &normalizedState,
const operators::StellarEquilibriumDependencies &dependencies
) requires(ProblemType::generatedRotationProviderCount == 1) {
)
requires(ProblemType::generatedRotationProviderCount == 1)
{
if (normalizedState.Size() != Width()) {
throw std::invalid_argument("The normalized stellar state has the wrong size.");
}
@@ -681,6 +672,30 @@ export namespace mean_field::normalization {
return report;
}
[[nodiscard]] PreparationResult TryPrepare(
const mfem::Vector &normalizedState,
const operators::StellarEquilibriumDependencies &dependencies
)
requires(ProblemType::generatedRotationProviderCount == 1)
{
if (normalizedState.Size() != Width()) {
throw std::invalid_argument("The normalized stellar state has the wrong size.");
}
m_isPrepared = false;
m_normalization.DenormalizeState(normalizedState, m_physicalState);
auto result = m_problem->TryPrepare(m_physicalState, dependencies);
if (!result.has_value()) {
return std::unexpected(result.error());
}
m_problem->BuildResidual(m_physicalResidual);
m_normalization.NormalizeResidual(m_physicalResidual, m_normalizedResidual);
m_physicalPreparationGeneration = m_problem->GetPreparationGeneration();
m_isPrepared = true;
++m_statistics.physicalPreparations;
return result;
}
void BuildResidual(mfem::Vector &normalizedResidual) const {
VerifyPrepared();
normalizedResidual = m_normalizedResidual;
@@ -701,8 +716,8 @@ export namespace mean_field::normalization {
void RefreshNormalization() {
DiagonalNormalization refreshed = prepareNormalization(*m_problem);
m_normalization = std::move(refreshed);
m_isPrepared = false;
m_normalization = std::move(refreshed);
m_isPrepared = false;
++m_statistics.normalizationPreparations;
}
@@ -735,8 +750,12 @@ export namespace mean_field::normalization {
}
template <typename PhysicalInverse>
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
[[nodiscard]] NormalizedStellarPreconditioner<Problem, std::remove_cvref_t<PhysicalInverse>>
requires ProblemBoundStellarInverseFor<
PhysicalInverse,
Problem>
[[nodiscard]] NormalizedStellarPreconditioner<
Problem,
std::remove_cvref_t<PhysicalInverse>>
MakeScaledPreconditioner(PhysicalInverse &physicalInverse) const;
[[nodiscard]] bool IsPrepared() const noexcept {
@@ -802,16 +821,15 @@ export namespace mean_field::normalization {
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
class NormalizedStellarPreconditioner final : public mfem::Solver {
private:
using ProblemType = std::remove_cvref_t<Problem>;
using NormalizedOperator = NormalizedStellarEquilibriumOperator<ProblemType>;
using ProblemType = std::remove_cvref_t<Problem>;
using NormalizedOperator = NormalizedStellarEquilibriumOperator<ProblemType>;
using PhysicalInverseType = std::remove_cvref_t<PhysicalInverse>;
[[nodiscard]] static PhysicalInverseType &RequireAssociatedPhysicalInverse(
const NormalizedOperator &normalizedOperator,
PhysicalInverseType &physicalInverse
) {
if (std::addressof(physicalInverse.GetProblem()) !=
std::addressof(normalizedOperator.GetProblem())) {
if (std::addressof(physicalInverse.GetProblem()) != std::addressof(normalizedOperator.GetProblem())) {
throw std::invalid_argument(
"A normalized stellar preconditioner and its physical inverse must belong to the same problem."
);
@@ -832,7 +850,10 @@ export namespace mean_field::normalization {
m_normalizedOperator(&normalizedOperator),
m_physicalInverse(&physicalInverse),
m_scaled(
RequireAssociatedPhysicalInverse(normalizedOperator, physicalInverse),
RequireAssociatedPhysicalInverse(
normalizedOperator,
physicalInverse
),
normalizedOperator.GetPhysicalJacobian(),
normalizedOperator,
normalizedOperator.GetNormalization()
@@ -863,8 +884,7 @@ export namespace mean_field::normalization {
}
[[nodiscard]] bool IsCurrent() const {
return m_normalizedOperator->IsPrepared() &&
m_physicalInverse->IsCurrent();
return m_normalizedOperator->IsPrepared() && m_physicalInverse->IsCurrent();
}
[[nodiscard]] PhysicalInverseType &GetPhysicalInverse() noexcept {
@@ -904,14 +924,15 @@ export namespace mean_field::normalization {
template <NormalizableStellarEquilibriumProblem Problem>
template <typename PhysicalInverse>
requires ProblemBoundStellarInverseFor<PhysicalInverse, Problem>
NormalizedStellarPreconditioner<Problem, std::remove_cvref_t<PhysicalInverse>>
requires ProblemBoundStellarInverseFor<
PhysicalInverse,
Problem>
NormalizedStellarPreconditioner<
Problem,
std::remove_cvref_t<PhysicalInverse>>
NormalizedStellarEquilibriumOperator<Problem>::MakeScaledPreconditioner(PhysicalInverse &physicalInverse) const {
VerifyPrepared();
return NormalizedStellarPreconditioner<Problem, std::remove_cvref_t<PhysicalInverse>>{
*this,
physicalInverse
};
return NormalizedStellarPreconditioner<Problem, std::remove_cvref_t<PhysicalInverse>>{*this, physicalInverse};
}
template <NormalizableStellarEquilibriumProblem Problem>

View File

@@ -1,8 +1,10 @@
module;
#include <compare>
#include <cstdint>
#include <expected>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
export module mean_field:operators.context.gravity_field;
export import :fem;
@@ -58,6 +60,23 @@ export namespace mean_field::operators::context::gravity_field {
}
};
enum class GravityFieldPreparationRejectionReason : std::uint8_t { invalid_mapping, non_finite_arithmetic };
struct GravityFieldPreparationRejection final {
GravityFieldPreparationRejectionReason reason{GravityFieldPreparationRejectionReason::invalid_mapping};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
template <typename Report>
using GravityFieldPreparationResult = std::expected<Report, GravityFieldPreparationRejection>;
[[noreturn]] inline void throwGravityFieldPreparationRejection(const GravityFieldPreparationRejection &rejection) {
if (rejection.reason == GravityFieldPreparationRejectionReason::non_finite_arithmetic) {
throw std::domain_error("Prepared gravity-field data contained non-finite arithmetic.");
}
throw std::domain_error("Prepared gravity-field data could not map the candidate geometry.");
}
class GravityFieldGeometryContext {
public:
GravityFieldGeometryContext(
@@ -76,12 +95,24 @@ export namespace mean_field::operators::context::gravity_field {
DisplacementRevision displacement_revision
);
[[nodiscard]] GravityFieldPreparationResult<GravityFieldGeometryPreparation> TryPrepare(
const mfem::Vector &displacement,
DiscretizationRevision discretization_revision,
DisplacementRevision displacement_revision
);
GravityFieldGeometryPreparation PreparePrimal(
const mfem::Vector &displacement,
DiscretizationRevision discretization_revision,
DisplacementRevision displacement_revision
);
[[nodiscard]] GravityFieldPreparationResult<GravityFieldGeometryPreparation> TryPreparePrimal(
const mfem::Vector &displacement,
DiscretizationRevision discretization_revision,
DisplacementRevision displacement_revision
);
[[nodiscard]] const PreparedMappedHDivMassOperator &GetMassOperator() const;
[[nodiscard]] const PreparedMappedGravitySourceOperator &GetSourceOperator() const;
[[nodiscard]] const mfem::Operator &GetDivergenceOperator() const;
@@ -95,7 +126,7 @@ export namespace mean_field::operators::context::gravity_field {
private:
enum class PreparationMode : std::uint8_t { primal, linearization };
GravityFieldGeometryPreparation PrepareImpl(
[[nodiscard]] GravityFieldPreparationResult<GravityFieldGeometryPreparation> TryPrepareImpl(
const mfem::Vector &displacement,
DiscretizationRevision discretization_revision,
DisplacementRevision displacement_revision,
@@ -147,6 +178,11 @@ export namespace mean_field::operators::context::gravity_field {
const GravityFieldRevisions &revisions
);
[[nodiscard]] GravityFieldPreparationResult<GravityFieldPreparationReport> TryPrepare(
const GravityFieldStateView &state,
const GravityFieldRevisions &revisions
);
[[nodiscard]] const GravityFieldGeometryContext &GetGeometryContext() const;
[[nodiscard]] const mfem::Vector &GetDensityTrue() const;
[[nodiscard]] const mfem::Vector &GetGravityGradientTrue() const;

View File

@@ -24,6 +24,13 @@ export namespace mean_field::operators {
const context::gravity_field::GravityFieldRevisions &revisions
);
[[nodiscard]] context::gravity_field::GravityFieldPreparationResult<
context::gravity_field::GravityFieldPreparationReport>
TryPrepare(
const mfem::Vector &state,
const context::gravity_field::GravityFieldRevisions &revisions
);
void Mult(
const mfem::Vector &state,
mfem::Vector &residual

View File

@@ -1,5 +1,8 @@
module;
#include <cstdint>
#include <expected>
#include <mfem.hpp>
export module mean_field:operators.kernels.gravity_displacement_force;
@@ -8,6 +11,24 @@ export import :fem;
export import :mapping.domain_mapper;
export namespace mean_field::operators::kernels {
enum class GravityDisplacementForceRejectionReason : std::uint8_t { invalid_mapping, non_finite_arithmetic };
struct GravityDisplacementForceRejection final {
GravityDisplacementForceRejectionReason reason{GravityDisplacementForceRejectionReason::invalid_mapping};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
using GravityDisplacementForceResult = std::expected<void, GravityDisplacementForceRejection>;
[[nodiscard]] 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
);
void apply_gravity_displacement_force_residual(
const fem::FEM &f,
const mapping::DomainMapper &domainMapper,

View File

@@ -1,5 +1,8 @@
module;
#include <cstdint>
#include <expected>
#include <mfem.hpp>
export module mean_field:operators.kernels.rotational_displacement_force;
@@ -9,6 +12,24 @@ export import :mapping.domain_mapper;
export import :physics.rigid_rotation;
export namespace mean_field::operators::kernels {
enum class RotationalDisplacementForceRejectionReason : std::uint8_t { invalid_mapping, non_finite_arithmetic };
struct RotationalDisplacementForceRejection final {
RotationalDisplacementForceRejectionReason reason{RotationalDisplacementForceRejectionReason::invalid_mapping};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
using RotationalDisplacementForceResult = std::expected<void, RotationalDisplacementForceRejection>;
[[nodiscard]] 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
);
/*
* Rotational contribution to the displacement row:
*

View File

@@ -2,6 +2,11 @@ module;
#include <compare>
#include <cstdint>
#include <expected>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <vector>
#include <mfem.hpp>
@@ -10,6 +15,7 @@ export module mean_field:operators.prepared_angular_momentum;
export import :fem;
export import :mapping.domain_mapper;
export import :mapping.prepared_cache;
export import :model.compiled_fixed_angular_momentum;
export import :operators.context.gravity_field;
@@ -45,6 +51,52 @@ export namespace mean_field::operators {
constexpr auto operator<=>(const PreparedAngularMomentumReport &) const = default;
};
enum class AngularMomentumPreparationRejectionReason : std::uint8_t {
inverted_geometry,
non_finite_geometry,
non_finite_angular_velocity,
non_finite_density,
negative_moment_of_inertia,
non_finite_moment_of_inertia,
non_finite_residual
};
/*
* A trial state can fail to define the angular-momentum invariant without
* violating the operator's structural contract. Keep that distinction in
* a fixed-size value so a line search can reject the candidate without
* constructing or transporting an exception.
*/
struct AngularMomentumPreparationRejection final {
AngularMomentumPreparationRejectionReason reason{AngularMomentumPreparationRejectionReason::inverted_geometry};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
double momentOfInertia{std::numeric_limits<double>::quiet_NaN()};
};
using AngularMomentumPreparationResult =
std::expected<PreparedAngularMomentumReport, AngularMomentumPreparationRejection>;
[[noreturn]] inline void
throwAngularMomentumPreparationRejection(const AngularMomentumPreparationRejection &rejection) {
switch (rejection.reason) {
case AngularMomentumPreparationRejectionReason::inverted_geometry:
throw std::domain_error("The angular-momentum trial inverts mapped geometry.");
case AngularMomentumPreparationRejectionReason::non_finite_geometry:
throw std::domain_error("The angular-momentum trial produced non-finite mapped geometry.");
case AngularMomentumPreparationRejectionReason::non_finite_angular_velocity:
throw std::domain_error("The angular-momentum trial has a non-finite angular velocity.");
case AngularMomentumPreparationRejectionReason::non_finite_density:
throw std::domain_error("The angular-momentum trial produced a non-finite interpolated density.");
case AngularMomentumPreparationRejectionReason::negative_moment_of_inertia:
throw std::domain_error("The angular-momentum trial produced a negative moment of inertia.");
case AngularMomentumPreparationRejectionReason::non_finite_moment_of_inertia:
throw std::domain_error("The angular-momentum trial produced a non-finite moment of inertia.");
case AngularMomentumPreparationRejectionReason::non_finite_residual:
throw std::domain_error("The angular-momentum trial produced a non-finite residual.");
}
throw std::logic_error("An unknown angular-momentum trial rejection was reported.");
}
struct AngularMomentumConstraintReport final {
double targetAngularMomentum;
double achievedAngularMomentum;
@@ -97,6 +149,11 @@ export namespace mean_field::operators {
const AngularMomentumDependencies &dependencies
);
[[nodiscard]] AngularMomentumPreparationResult TryPrepare(
double angularVelocity,
const AngularMomentumDependencies &dependencies
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
@@ -133,15 +190,13 @@ export namespace mean_field::operators {
[[nodiscard]] const PreparedAngularMomentumActionStatistics &GetActionStatistics() const noexcept;
[[nodiscard]] const models::CompiledFixedAngularMomentum &GetCompiledConstraint() const noexcept;
private:
struct QuadraturePointData final {
mfem::IntegrationPoint integrationPoint;
mfem::Vector densityShape;
mapping::VolumeMappingContext mappingContext;
double density{0.0};
double cylindricalRadiusSquared{0.0};
};
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
struct ElementPAData final {
int elementId{-1};
mfem::Array<int> densityDofs;
@@ -150,15 +205,20 @@ export namespace mean_field::operators {
mfem::DofTransformation *densityDofTransformation{nullptr};
mfem::DofTransformation *displacementDofTransformation{nullptr};
mfem::DofTransformation *compactificationDofTransformation{nullptr};
const mfem::IntegrationRule *integrationRule{nullptr};
mfem::Vector baseDisplacement;
mfem::Vector compactification;
std::vector<QuadraturePointData> quadraturePoints;
std::shared_ptr<const fem::ScalarReferenceTable> densityBasis;
mapping::VolumeMappingCache mappingContexts;
mfem::Vector density;
mfem::Vector quadratureWeights;
mfem::Vector cylindricalRadiusSquared;
};
void BuildStaticPlan();
void RefreshGeometry(const mfem::Vector &displacement);
void RefreshDensity(const mfem::Vector &density);
void AssembleResidual();
[[nodiscard]] std::optional<mapping::MappingStatus> RefreshGeometry(const mfem::Vector &displacement);
[[nodiscard]] bool RefreshDensity(const mfem::Vector &density);
[[nodiscard]] std::optional<AngularMomentumPreparationRejection> TryAssembleResidual();
void VerifyPrepared() const;
[[nodiscard]] double EvaluateDensityMomentActionLocal(const mfem::Vector &densityVariation) const;

View File

@@ -1,6 +1,8 @@
module;
#include <cstdint>
#include <expected>
#include <memory>
#include <mfem.hpp>
#include <vector>
@@ -22,6 +24,29 @@ export namespace mean_field::operators {
}
};
enum class BarotropicClosurePreparationRejectionReason : std::uint8_t {
mapping_failure,
invalid_quadrature_data,
equation_of_state
};
/*
* A rejected candidate is part of the nonlinear-solver control flow, not
* an exceptional API failure. Keep the payload fixed-size so it can be
* selected deterministically across ranks without allocating. Only the
* detail associated with `reason` is meaningful.
*/
struct BarotropicClosurePreparationRejection final {
BarotropicClosurePreparationRejectionReason reason{
BarotropicClosurePreparationRejectionReason::mapping_failure
};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::non_finite_result};
eos::EvaluationErrorCode equationOfStateError{eos::EvaluationErrorCode::nonfinite_result};
};
using BarotropicClosurePreparationResult =
std::expected<PreparedBarotropicClosureReport, BarotropicClosurePreparationRejection>;
class PreparedBarotropicClosureOperator final : public mfem::Operator {
public:
PreparedBarotropicClosureOperator(
@@ -40,6 +65,11 @@ export namespace mean_field::operators {
const context::barotropic::BarotropicClosureDependencies &dependencies
);
[[nodiscard]] BarotropicClosurePreparationResult TryPrepare(
const context::barotropic::BarotropicClosureStateView &state,
const context::barotropic::BarotropicClosureDependencies &dependencies
);
void Mult(
const mfem::Vector &densityVariation,
const mfem::Vector &enthalpyVariation,
@@ -68,6 +98,12 @@ export namespace mean_field::operators {
[[nodiscard]] const context::barotropic::BarotropicClosurePreparationStatistics &
GetContextPreparationStatistics() const noexcept;
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
struct ConstructionData;
@@ -103,8 +139,11 @@ export namespace mean_field::operators {
mfem::DofTransformation *enthalpyDofTransformation{nullptr};
mfem::DofTransformation *displacementDofTransformation{nullptr};
mfem::DenseMatrix densityBasis;
mfem::DenseMatrix enthalpyBasis;
const mfem::IntegrationRule *integrationRule{nullptr};
std::shared_ptr<const fem::ScalarReferenceTable> densityBasis;
std::shared_ptr<const fem::ScalarReferenceTable> enthalpyBasis;
std::shared_ptr<const fem::ScalarReferenceTable> displacementBasis;
mfem::DenseMatrix inverseElementJacobians;
mfem::Vector weightedResidual;
@@ -140,7 +179,6 @@ export namespace mean_field::operators {
mutable mfem::Vector m_elementDisplacementVariation;
mutable mfem::Vector m_quadratureDisplacementAction;
mutable mfem::Vector m_elementDisplacementAction;
mutable mfem::DenseMatrix m_referenceDShape;
mutable mfem::DenseMatrix m_referenceDisplacementJacobian;
std::uint64_t m_preparationCount{0};

View File

@@ -2,6 +2,8 @@ module;
#include <compare>
#include <cstdint>
#include <expected>
#include <optional>
#include <mfem.hpp>
@@ -18,6 +20,28 @@ export import :physics.rigid_rotation;
export import :utils.blocks;
export namespace mean_field::operators {
enum class DisplacementResidualPreparationRejectionSource : std::uint8_t {
pressure,
gravity,
rotation,
composition
};
enum class DisplacementResidualPreparationRejectionReason : std::uint8_t {
equation_of_state,
invalid_mapping,
non_finite_arithmetic
};
struct DisplacementResidualPreparationRejection final {
DisplacementResidualPreparationRejectionSource source{DisplacementResidualPreparationRejectionSource::pressure};
DisplacementResidualPreparationRejectionReason reason{
DisplacementResidualPreparationRejectionReason::equation_of_state
};
eos::EvaluationErrorCode equationOfStateCode{eos::EvaluationErrorCode::nonfinite_result};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
struct DisplacementResidualDependencyStamp final {
std::uint64_t identity{0};
std::uint64_t revision{0};
@@ -104,6 +128,15 @@ export namespace mean_field::operators {
const physics::RigidRotation &rotation
);
[[nodiscard]] std::expected<
PreparedDisplacementResidualReport,
DisplacementResidualPreparationRejection>
TryPrepare(
const DisplacementResidualStateView &state,
const DisplacementResidualDependencies &dependencies,
const physics::RigidRotation &rotation
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
@@ -154,7 +187,7 @@ export namespace mean_field::operators {
GetGravityContext() const noexcept;
private:
void AssembleResidual();
[[nodiscard]] std::optional<DisplacementResidualPreparationRejection> AssembleResidual();
void VerifyPrepared() const;
const fem::FEM &m_fem;

View File

@@ -2,6 +2,8 @@ module;
#include <compare>
#include <cstdint>
#include <expected>
#include <memory>
#include <vector>
#include <mfem.hpp>
@@ -11,7 +13,9 @@ export module mean_field:operators.prepared_gravity_displacement_force;
export import :fem;
export import :mapping.domain_mapper;
export import :operators.context.gravity_field;
export import :operators.kernels.gravity_displacement_force;
export import :utils.blocks;
import :fem.reference_tables;
export namespace mean_field::operators {
struct PreparedGravityDisplacementForceReport final {
@@ -57,6 +61,11 @@ export namespace mean_field::operators {
*/
PreparedGravityDisplacementForceReport Prepare();
[[nodiscard]] std::expected<
PreparedGravityDisplacementForceReport,
kernels::GravityDisplacementForceRejection>
TryPrepare();
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
@@ -104,9 +113,18 @@ export namespace mean_field::operators {
[[nodiscard]] const context::gravity_field::GravityFieldLinearizationContext &
GetGravityContext() const noexcept;
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
void VerifyPrepared() const;
void PrepareElementData();
[[nodiscard]] std::expected<
void,
kernels::GravityDisplacementForceRejection>
TryPrepareElementData();
void ApplyPreparedCompleteJacobianActionTrue(
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementVariationTrue,
@@ -123,6 +141,10 @@ export namespace mean_field::operators {
mfem::DofTransformation *gravityGradientDofTransformation{nullptr};
mfem::DofTransformation *displacementDofTransformation{nullptr};
const mfem::IntegrationRule *integrationRule{nullptr};
std::shared_ptr<const fem::ScalarReferenceTable> densityReferenceTable;
std::shared_ptr<const fem::ScalarReferenceTable> displacementReferenceTable;
std::shared_ptr<const fem::VectorReferenceTable> gravityReferenceTable;
mfem::DenseMatrix meshPiolaJacobians;
mfem::DenseMatrix mappingJacobians;
mfem::DenseMatrix inverseMeshJacobians;
mfem::DenseMatrix baseGravityReferenceValues;
@@ -154,16 +176,17 @@ export namespace mean_field::operators {
mutable mfem::Vector m_displacementShape;
mutable mfem::Vector m_baseGravityReferenceValue;
mutable mfem::Vector m_gravityVariationReferenceValue;
mutable mfem::Vector m_gravityVariationReferenceCellValue;
mutable mfem::Vector m_mappedBaseGravity;
mutable mfem::Vector m_mappedGravityVariation;
mutable mfem::Vector m_mappedGeometryVariation;
mutable mfem::Vector m_forceValue;
mutable mfem::DenseMatrix m_gravityGradientShape;
mutable mfem::DenseMatrix m_referenceDisplacementDShape;
mutable mfem::DenseMatrix m_referenceDisplacementJacobian;
mutable mfem::DenseMatrix m_displacementJacobianVariation;
mutable mfem::DenseMatrix m_mappingJacobian;
mutable mfem::DenseMatrix m_inverseMeshJacobian;
mutable mfem::DenseMatrix m_meshPiolaJacobian;
std::uint64_t m_residualPreparationCount{0};
mutable std::uint64_t m_residualApplicationCount{0};

View File

@@ -1,7 +1,9 @@
module;
#include <cstdint>
#include <expected>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
#include <vector>
export module mean_field:operators.prepared_gravity_source;
@@ -10,6 +12,23 @@ export import :field.mfem;
export import :mapping.domain_mapper;
export namespace mean_field::operators {
enum class GravitySourcePreparationRejectionReason : std::uint8_t { invalid_mapping, non_finite_arithmetic };
struct GravitySourcePreparationRejection final {
GravitySourcePreparationRejectionReason reason{GravitySourcePreparationRejectionReason::invalid_mapping};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
using GravitySourcePreparationResult = std::expected<void, GravitySourcePreparationRejection>;
[[noreturn]] inline void
throwGravitySourcePreparationRejection(const GravitySourcePreparationRejection &rejection) {
if (rejection.reason == GravitySourcePreparationRejectionReason::non_finite_arithmetic) {
throw std::domain_error("Prepared gravity-source data contained non-finite arithmetic.");
}
throw std::domain_error("Prepared gravity-source data could not map the candidate geometry.");
}
class PreparedMappedGravitySourceOperator final : public mfem::Operator {
public:
PreparedMappedGravitySourceOperator(
@@ -19,6 +38,8 @@ export namespace mean_field::operators {
void Prepare(const mfem::Vector &displacement);
void PreparePrimal(const mfem::Vector &displacement);
[[nodiscard]] GravitySourcePreparationResult TryPrepare(const mfem::Vector &displacement);
[[nodiscard]] GravitySourcePreparationResult TryPreparePrimal(const mfem::Vector &displacement);
void Mult(
const mfem::Vector &density,
mfem::Vector &action
@@ -37,6 +58,12 @@ export namespace mean_field::operators {
[[nodiscard]] const field::FieldDofMap &GetPotentialMap() const noexcept;
[[nodiscard]] const field::FieldDofMap &GetDisplacementMap() const noexcept;
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.element_id, *data.integration_rule);
}
}
void MultTranspose(
const mfem::Vector &potential,
mfem::Vector &action
@@ -59,6 +86,10 @@ export namespace mean_field::operators {
const mfem::IntegrationRule *integration_rule{nullptr};
// Rows are quadrature points; columns are element DOFs.
std::shared_ptr<const fem::ScalarReferenceTable> density_reference;
std::shared_ptr<const fem::ScalarReferenceTable> potential_reference;
std::shared_ptr<const fem::ScalarReferenceTable> displacement_reference;
// Non-VALUE map types retain their element-dependent physical basis.
mfem::DenseMatrix density_basis;
mfem::DenseMatrix potential_basis;
mfem::DenseMatrix inverse_element_jacobians;
@@ -66,9 +97,17 @@ export namespace mean_field::operators {
// Contains quadrature weight, mesh Jacobian, mapped Jacobian,
// and 4*pi*G.
mfem::Vector quadrature_data;
[[nodiscard]] const mfem::DenseMatrix &GetDensityBasis() const {
return density_reference ? density_reference->GetValues() : density_basis;
}
[[nodiscard]] const mfem::DenseMatrix &GetPotentialBasis() const {
return potential_reference ? potential_reference->GetValues() : potential_basis;
}
};
void PrepareImpl(
[[nodiscard]] GravitySourcePreparationResult TryPrepareImpl(
const mfem::Vector &displacement,
PreparationMode mode
);
@@ -98,7 +137,6 @@ export namespace mean_field::operators {
mutable mfem::Vector m_element_displacement_variation;
mutable mfem::Vector m_quadrature_variation_action;
mutable mfem::Vector m_element_variation_action;
mutable mfem::DenseMatrix m_reference_displacement_dshape;
mutable mfem::DenseMatrix m_reference_displacement_jacobian;
mfem::Vector m_displacement_true;

View File

@@ -1,15 +1,34 @@
module;
#include <cstdint>
#include <expected>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
#include <vector>
export module mean_field:operators.prepared_hdiv_mass;
export import :fem;
export import :field.mfem;
export import :mapping.domain_mapper;
import :fem.reference_tables;
export namespace mean_field::operators {
enum class HDivMassPreparationRejectionReason : std::uint8_t { invalid_mapping, non_finite_arithmetic };
struct HDivMassPreparationRejection final {
HDivMassPreparationRejectionReason reason{HDivMassPreparationRejectionReason::invalid_mapping};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
using HDivMassPreparationResult = std::expected<void, HDivMassPreparationRejection>;
[[noreturn]] inline void throwHDivMassPreparationRejection(const HDivMassPreparationRejection &rejection) {
if (rejection.reason == HDivMassPreparationRejectionReason::non_finite_arithmetic) {
throw std::domain_error("Prepared H(div) mass data contained non-finite arithmetic.");
}
throw std::domain_error("Prepared H(div) mass data could not map the candidate geometry.");
}
class PreparedMappedHDivMassOperator final : public mfem::Operator {
public:
PreparedMappedHDivMassOperator(
@@ -19,6 +38,8 @@ export namespace mean_field::operators {
void Prepare(const mfem::Vector &displacement);
void PreparePrimal(const mfem::Vector &displacement);
[[nodiscard]] HDivMassPreparationResult TryPrepare(const mfem::Vector &displacement);
[[nodiscard]] HDivMassPreparationResult TryPreparePrimal(const mfem::Vector &displacement);
void Mult(
const mfem::Vector &gravity_gradient,
mfem::Vector &action
@@ -38,6 +59,12 @@ export namespace mean_field::operators {
[[nodiscard]] const field::FieldDofMap &GetFluxMap() const noexcept;
[[nodiscard]] const field::FieldDofMap &GetDisplacementMap() const noexcept;
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementVariationData &data : m_variationElements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
enum class PreparationMode : std::uint8_t { primal, linearization };
@@ -51,11 +78,15 @@ export namespace mean_field::operators {
mfem::Vector baseDisplacement;
mfem::Vector compactification;
const mfem::IntegrationRule *integrationRule{nullptr};
std::shared_ptr<const fem::VectorReferenceTable> gravityReferenceTable;
// Fixed computational-mesh Piola factor, separate from J_map.
mfem::DenseMatrix meshPiolaJacobians;
mfem::Vector referenceWeights;
mfem::DenseMatrix frozenMappingData;
};
void PrepareVariationData();
void PrepareImpl(
[[nodiscard]] mapping::MappingStatus PrepareVariationData();
[[nodiscard]] HDivMassPreparationResult TryPrepareImpl(
const mfem::Vector &displacement,
PreparationMode mode
);
@@ -92,7 +123,10 @@ export namespace mean_field::operators {
mutable mfem::Vector m_elementDisplacementVariation;
mutable mfem::Vector m_elementVariationAction;
mutable mfem::Vector m_gravityGradientValue;
mutable mfem::Vector m_gravityReferenceCellValue;
mutable mfem::Vector m_referenceCellDual;
mutable mfem::Vector m_massTensorVariationAction;
mutable mfem::DenseMatrix m_meshPiolaJacobian;
mutable mfem::DenseMatrix m_gravityGradientShape;
mutable mfem::DenseMatrix m_massTensorVariation;
std::uint64_t m_preparation_count{0};

View File

@@ -3,7 +3,10 @@ module;
#include <compare>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <memory>
#include <optional>
#include <stdexcept>
#include <vector>
#include <mfem.hpp>
@@ -11,7 +14,9 @@ module;
export module mean_field:operators.prepared_hydrostatic_equilibrium;
export import :fem;
export import :fem.reference_tables;
export import :mapping.domain_mapper;
export import :mapping.prepared_cache;
export import :operators.context.hydrostatic_equilibrium;
export import :physics.rigid_rotation;
@@ -30,6 +35,35 @@ export namespace mean_field::operators {
}
};
enum class HydrostaticEquilibriumPreparationRejectionReason : std::uint8_t {
inverted_geometry,
non_finite_geometry,
non_finite_residual
};
struct HydrostaticEquilibriumPreparationRejection final {
HydrostaticEquilibriumPreparationRejectionReason reason{
HydrostaticEquilibriumPreparationRejectionReason::inverted_geometry
};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
using HydrostaticEquilibriumPreparationResult =
std::expected<PreparedHydrostaticEquilibriumReport, HydrostaticEquilibriumPreparationRejection>;
[[noreturn]] inline void
throwHydrostaticEquilibriumPreparationRejection(const HydrostaticEquilibriumPreparationRejection &rejection) {
switch (rejection.reason) {
case HydrostaticEquilibriumPreparationRejectionReason::non_finite_geometry:
throw std::domain_error("Prepared hydrostatic equilibrium encountered non-finite mapped geometry.");
case HydrostaticEquilibriumPreparationRejectionReason::non_finite_residual:
throw std::domain_error("Prepared hydrostatic equilibrium produced a non-finite residual.");
case HydrostaticEquilibriumPreparationRejectionReason::inverted_geometry:
default:
throw std::domain_error("Prepared hydrostatic equilibrium encountered inverted mapped geometry.");
}
}
struct PreparedHydrostaticAlgebraicJacobianStatistics {
std::uint64_t preparations{0};
std::uint64_t enthalpyApplications{0};
@@ -102,6 +136,12 @@ export namespace mean_field::operators {
const physics::RigidRotation &rotation
);
[[nodiscard]] HydrostaticEquilibriumPreparationResult TryPrepare(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyEnthalpyJacobianAction(
@@ -179,6 +219,12 @@ export namespace mean_field::operators {
[[nodiscard]] const field::FieldDofMap &GetDisplacementMap() const noexcept;
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
struct ElementPAData {
int elementId{-1};
@@ -195,15 +241,23 @@ export namespace mean_field::operators {
const mfem::IntegrationRule *integrationRule{nullptr};
// Rows are quadrature points and columns are element DOFs.
mfem::DenseMatrix enthalpyBasis;
mfem::DenseMatrix gravityPotentialBasis;
// Immutable reference values and gradients are shared by FE/rule.
std::shared_ptr<const fem::ScalarReferenceTable> enthalpyReferenceTable;
std::shared_ptr<const fem::ScalarReferenceTable> gravityPotentialReferenceTable;
[[nodiscard]] const mfem::DenseMatrix &GetEnthalpyBasis() const {
return enthalpyReferenceTable->GetValues();
}
[[nodiscard]] const mfem::DenseMatrix &GetGravityPotentialBasis() const {
return gravityPotentialReferenceTable->GetValues();
}
// Rows are quadrature points and columns are physical components.
mfem::DenseMatrix physicalPositions;
mfem::Vector quadratureWeights;
std::vector<mapping::VolumeMappingContext> baseMappingContexts;
mapping::VolumeMappingCache baseMappingContexts;
std::optional<mapping::ElementDisplacementData> baseDisplacementData;
@@ -221,12 +275,12 @@ export namespace mean_field::operators {
};
void PrepareStaticPlan();
void PrepareGeometry();
void PrepareAlgebraicJacobianBlocks();
void PrepareRotation();
void PrepareBaseState();
[[nodiscard]] std::optional<mapping::MappingStatus> PrepareGeometry();
[[nodiscard]] bool PrepareAlgebraicJacobianBlocks();
[[nodiscard]] bool PrepareRotation();
[[nodiscard]] bool PrepareBaseState();
void FinalizeDisplacementJacobianPreparation();
void AssembleCachedResidual();
[[nodiscard]] bool AssembleCachedResidual();
void VerifyPrepared() const;
const fem::FEM &m_fem;

View File

@@ -2,13 +2,17 @@ module;
#include <compare>
#include <cstdint>
#include <expected>
#include <memory>
#include <mfem.hpp>
#include <optional>
#include <vector>
export module mean_field:operators.prepared_mass_normalization;
export import :fem;
export import :mapping.domain_mapper;
export import :mapping.prepared_cache;
export import :model.compiled_fixed_mass;
export import :operators.context.gravity_field;
export import :operators.prepared_constraint;
@@ -59,6 +63,28 @@ export namespace mean_field::operators {
constexpr auto operator<=>(const PreparedMassNormalizationReport &) const = default;
};
enum class MassNormalizationPreparationRejectionReason : std::uint8_t {
mapping_failure,
non_finite_density_interpolation,
non_finite_assembled_mass
};
/*
* Candidate rejection is deliberately represented without text or owned
* storage. That makes the result cheap to propagate through a line search
* and gives the MPI implementation a deterministic, allocation-free value
* to select on every rank.
*/
struct MassNormalizationPreparationRejection final {
MassNormalizationPreparationRejectionReason reason{
MassNormalizationPreparationRejectionReason::mapping_failure
};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::non_finite_result};
};
using MassNormalizationPreparationResult =
std::expected<PreparedMassNormalizationReport, MassNormalizationPreparationRejection>;
struct PreparedMassNormalizationActionStatistics final {
std::uint64_t densityApplications{0};
std::uint64_t displacementApplications{0};
@@ -109,6 +135,16 @@ export namespace mean_field::operators {
const MassNormalizationDependencies &dependencies
);
[[nodiscard]] MassNormalizationPreparationResult TryPrepare(
const MassNormalizationStateView &state,
const MassNormalizationDependencies &dependencies
);
[[nodiscard]] MassNormalizationPreparationResult TryPrepare(
const models::CompiledFixedMass &constraint,
const MassNormalizationDependencies &dependencies
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
@@ -153,14 +189,13 @@ export namespace mean_field::operators {
[[nodiscard]] const context::gravity_field::GravityFieldLinearizationContext &
GetGravityContext() const noexcept;
private:
struct QuadraturePointData final {
mfem::IntegrationPoint integrationPoint;
mfem::Vector densityShape;
mapping::VolumeMappingContext mappingContext;
double density{0.0};
};
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
struct ElementPAData final {
int elementId{-1};
@@ -172,16 +207,23 @@ export namespace mean_field::operators {
mfem::DofTransformation *displacementDofTransformation{nullptr};
mfem::DofTransformation *compactificationDofTransformation{nullptr};
const mfem::IntegrationRule *integrationRule{nullptr};
mfem::Vector baseDisplacement;
mfem::Vector compactification;
std::vector<QuadraturePointData> quadraturePoints;
std::shared_ptr<const fem::ScalarReferenceTable> densityBasis;
mapping::VolumeMappingCache mappingContexts;
mfem::Vector density;
mfem::Vector quadratureWeights;
};
void BuildStaticPlan();
void RefreshGeometry(const mfem::Vector &displacement);
void RefreshDensity(const mfem::Vector &density);
void AssembleResidual();
[[nodiscard]] std::optional<MassNormalizationPreparationRejection>
RefreshGeometry(const mfem::Vector &displacement);
[[nodiscard]] std::optional<MassNormalizationPreparationRejection> RefreshDensity(const mfem::Vector &density);
[[nodiscard]] std::optional<MassNormalizationPreparationRejection> AssembleResidual();
[[nodiscard]] std::optional<MassNormalizationPreparationRejection> UpdateResidualForTargetMass();
void VerifyPrepared() const;
[[nodiscard]] double EvaluateDensityActionLocal(const mfem::Vector &densityVariation) const;

View File

@@ -3,6 +3,8 @@ module;
#include <compare>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <memory>
#include <optional>
#include <vector>
@@ -12,12 +14,26 @@ export module mean_field:operators.prepared_pressure_force;
export import :eos.polytrope;
export import :fem;
export import :fem.reference_tables;
export import :field.mfem;
export import :mapping.domain_mapper;
export import :mapping.prepared_cache;
export import :operators.context.pressure_force;
export import :utils.blocks;
export namespace mean_field::operators {
enum class PressureForcePreparationRejectionReason : std::uint8_t {
equation_of_state,
invalid_mapping,
non_finite_arithmetic
};
struct PressureForcePreparationRejection final {
PressureForcePreparationRejectionReason reason{PressureForcePreparationRejectionReason::equation_of_state};
eos::EvaluationErrorCode equationOfStateCode{eos::EvaluationErrorCode::nonfinite_result};
mapping::MappingStatus mappingStatus{mapping::MappingStatus::valid};
};
struct PreparedPressureForceReport final {
context::pressure_force::PressureForcePreparationReport contextReport;
@@ -92,6 +108,14 @@ export namespace mean_field::operators {
const context::pressure_force::PressureForceDependencies &dependencies
);
[[nodiscard]] std::expected<
PreparedPressureForceReport,
PressureForcePreparationRejection>
TryPrepare(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyEnthalpyJacobianAction(
@@ -147,6 +171,12 @@ export namespace mean_field::operators {
[[nodiscard]]
const fem::FEM &GetFEM() const noexcept;
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
struct ConstructionData;
@@ -175,10 +205,13 @@ export namespace mean_field::operators {
const mfem::IntegrationRule *integrationRule{nullptr};
/*
* Rows are quadrature points and columns are enthalpy DOFs.
*/
mfem::DenseMatrix enthalpyBasis;
// Immutable reference values and gradients are shared by FE/rule.
std::shared_ptr<const fem::ScalarReferenceTable> enthalpyReferenceTable;
std::shared_ptr<const fem::ScalarReferenceTable> displacementReferenceTable;
[[nodiscard]] const mfem::DenseMatrix &GetEnthalpyBasis() const {
return enthalpyReferenceTable->GetValues();
}
/*
* Each entry is:
@@ -187,11 +220,9 @@ export namespace mean_field::operators {
* x
* physical dimension.
*/
std::vector<mfem::DenseMatrix> referenceTestGradients;
std::vector<mfem::DenseMatrix> physicalTestGradients;
std::vector<mapping::VolumeMappingContext> baseMappingContexts;
mapping::VolumeMappingCache baseMappingContexts;
std::optional<mapping::ElementDisplacementData> baseDisplacementData;
@@ -206,12 +237,12 @@ export namespace mean_field::operators {
};
void PrepareStaticPlan();
void PrepareGeometry();
void PrepareMaterialState();
[[nodiscard]] std::optional<PressureForcePreparationRejection> PrepareGeometry();
[[nodiscard]] std::optional<PressureForcePreparationRejection> PrepareMaterialState();
void FinalizeDisplacementJacobianPreparation();
void AssembleCachedResidual();
[[nodiscard]] std::optional<PressureForcePreparationRejection> AssembleCachedResidual();
void VerifyPrepared() const;
@@ -301,4 +332,4 @@ export namespace mean_field::operators {
const PreparedPressureForceOperator &m_preparedOperator;
};
} // namespace mean_field::operators
} // namespace mean_field::operators

View File

@@ -2,6 +2,7 @@ module;
#include <compare>
#include <cstdint>
#include <expected>
#include <optional>
#include <vector>
@@ -12,6 +13,7 @@ export module mean_field:operators.prepared_rotational_displacement_force;
export import :fem;
export import :mapping.domain_mapper;
export import :operators.context.rotational_displacement_force;
export import :operators.kernels.rotational_displacement_force;
export import :physics.rigid_rotation;
export import :utils.blocks;
@@ -61,6 +63,15 @@ export namespace mean_field::operators {
const physics::RigidRotation &rotation
);
[[nodiscard]] std::expected<
PreparedRotationalDisplacementForceReport,
kernels::RotationalDisplacementForceRejection>
TryPrepare(
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
@@ -102,9 +113,18 @@ export namespace mean_field::operators {
[[nodiscard]] const context::rotational_displacement_force::RotationalDisplacementForceLinearizationContext &
GetContext() const noexcept;
template <typename Visitor> void VisitMappedGeometryRules(Visitor &&visitor) const {
for (const ElementPAData &data : m_elements) {
visitor(data.elementId, *data.integrationRule);
}
}
private:
void VerifyPrepared() const;
void PrepareElementData();
[[nodiscard]] std::expected<
void,
kernels::RotationalDisplacementForceRejection>
TryPrepareElementData();
void ApplyPreparedCompleteJacobianActionTrue(
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementVariationTrue,

View File

@@ -3,10 +3,14 @@ module;
#include <compare>
#include <concepts>
#include <cstdint>
#include <expected>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:operators.prepared_stellar_equilibrium;
@@ -72,6 +76,70 @@ export namespace mean_field::operators {
}
};
enum class StellarEquilibriumPreparationRejectionReason : std::uint8_t {
inverted_geometry,
non_finite_geometry,
thermodynamic_domain,
non_finite_thermodynamics,
inadmissible_physics,
non_finite_physics
};
enum class StellarEquilibriumPreparationStage : std::uint8_t {
unspecified,
generated_geometry,
gravity,
barotropic_closure,
hydrostatic_equilibrium,
displacement_residual,
pressure_force,
gravity_displacement_force,
rotational_displacement_force,
displacement_composition,
mass_normalization,
model_specification
};
/*
* A candidate state that cannot define a physical mapped domain is an
* expected line-search outcome, not an exceptional program failure. Keep
* this payload fixed-size so every trial can report it without allocating.
*/
struct StellarEquilibriumPreparationRejection final {
StellarEquilibriumPreparationRejectionReason reason{
StellarEquilibriumPreparationRejectionReason::inverted_geometry
};
StellarEquilibriumPreparationStage stage{StellarEquilibriumPreparationStage::unspecified};
double minimumJacobianDeterminant{std::numeric_limits<double>::quiet_NaN()};
eos::EvaluationErrorCode thermodynamicErrorCode{eos::EvaluationErrorCode::nonfinite_result};
};
template <typename Report>
using StellarEquilibriumPreparationResult = std::expected<Report, StellarEquilibriumPreparationRejection>;
[[noreturn]] inline void
throwStellarEquilibriumPreparationRejection(const StellarEquilibriumPreparationRejection &rejection) {
switch (rejection.reason) {
case StellarEquilibriumPreparationRejectionReason::non_finite_geometry:
throw std::domain_error("The prepared domain deformation has non-finite mapped geometry.");
case StellarEquilibriumPreparationRejectionReason::thermodynamic_domain:
throw eos::EvaluationError(
rejection.thermodynamicErrorCode, "The stellar state lies outside the equation-of-state domain."
);
case StellarEquilibriumPreparationRejectionReason::non_finite_thermodynamics:
throw eos::EvaluationError(
rejection.thermodynamicErrorCode, "The stellar state produced non-finite thermodynamic data."
);
case StellarEquilibriumPreparationRejectionReason::inadmissible_physics:
throw std::domain_error("The stellar state is physically inadmissible.");
case StellarEquilibriumPreparationRejectionReason::non_finite_physics:
throw std::domain_error("The stellar state produced non-finite physical data.");
case StellarEquilibriumPreparationRejectionReason::inverted_geometry:
throw std::domain_error("The prepared domain deformation inverts at least one volume element.");
}
throw std::logic_error("Unknown stellar-equilibrium candidate-rejection reason.");
}
struct PreparedStellarEquilibriumStatistics final {
std::uint64_t residualAssemblies{0};
std::uint64_t residualApplications{0};
@@ -159,6 +227,12 @@ export namespace mean_field::operators {
const physics::RigidRotation &rotation
);
[[nodiscard]] StellarEquilibriumPreparationResult<PreparedStellarEquilibriumReport> TryPrepare(
const mfem::Vector &state,
const StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
);
void BuildResidual(mfem::Vector &residual) const;
void Mult(
@@ -188,16 +262,17 @@ export namespace mean_field::operators {
[[nodiscard]] const PreparedHydrostaticEquilibriumOperator &GetHydrostaticOperator() const noexcept;
[[nodiscard]] const PreparedDisplacementResidualOperator &GetDisplacementOperator() const noexcept;
[[nodiscard]] const PreparedMassNormalizationOperator &GetMassNormalizationOperator() const noexcept;
[[nodiscard]] double ApplyDensityVolumeIntegralDensityAction(
const mfem::Vector &densityDirection
) const;
[[nodiscard]] double ApplyDensityVolumeIntegralSurfaceShapeAction(
const mfem::Vector &surfaceShapeDirection
) const;
[[nodiscard]] double ApplyDensityVolumeIntegralDensityAction(const mfem::Vector &densityDirection) const;
[[nodiscard]] double
ApplyDensityVolumeIntegralSurfaceShapeAction(const mfem::Vector &surfaceShapeDirection) const;
[[nodiscard]] const PreparedPressureSurfaceConstraint &GetSurfaceConstraintOperator() const noexcept;
[[nodiscard]] const deformation::PreparedDomainDeformationRuntime &GetDomainDeformation() const noexcept;
[[nodiscard]] const mfem::Vector &GetSurfaceDeformationParameters() const;
[[nodiscard]] const mfem::Vector &GetGeneratedVolumeDisplacement() const;
void BuildVolumeDisplacementDirection(
const mfem::Vector &surfaceDeformationDirection,
mfem::Vector &volumeDisplacementDirection
) const;
[[nodiscard]] const mfem::Vector &GetFullMechanicalResidual() const;
[[nodiscard]] const StellarEquilibriumDependencyStamp &GetGeneratedDisplacementDependency() const;
@@ -222,6 +297,7 @@ export namespace mean_field::operators {
void VerifyPrepared() const;
StellarEquilibriumRootManifest m_rootManifest;
MPI_Comm m_communicator{MPI_COMM_NULL};
mfem::Array<int> m_gravityStateOffsets;
context::gravity_field::GravityFieldLinearizationContext m_gravityContext;

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ module;
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <memory>
#include <type_traits>
#include <utility>
@@ -28,12 +29,11 @@ export namespace mean_field::equilibrium {
static constexpr bool complete = false;
};
template <model::StellarModelType Model>
struct StellarSurfaceCompilationAudit<Model, true> {
template <model::StellarModelType Model> struct StellarSurfaceCompilationAudit<Model, true> {
private:
using ModelType = std::remove_cvref_t<Model>;
using EquationOfState = typename ModelType::EquationOfStateType;
using Form = operators::CompiledStellarEquilibriumForm<ModelType>;
using ModelType = std::remove_cvref_t<Model>;
using EquationOfState = typename ModelType::EquationOfStateType;
using Form = operators::CompiledStellarEquilibriumForm<ModelType>;
using AvailableEquations = material::StellarEquilibriumThermodynamicEquations;
static constexpr bool thermodynamicsCompilable =
@@ -46,14 +46,12 @@ export namespace mean_field::equilibrium {
} else {
using ThermodynamicEquations =
material::CompiledThermodynamicEquationsT<EquationOfState, Form, AvailableEquations>;
using Formulation = typename ThermodynamicEquations::PressureSurfaceFormulation;
using CompiledSurface =
surface::CompiledPressureSurfaceConstraintT<Formulation, EquationOfState>;
using Formulation = typename ThermodynamicEquations::PressureSurfaceFormulation;
using CompiledSurface = surface::CompiledPressureSurfaceConstraintT<Formulation, EquationOfState>;
return requires(const ModelType &model) {
{
surface::compilePressureSurfaceConstraint<Formulation>(
model.surfaceCondition(),
model.equationOfState()
model.surfaceCondition(), model.equationOfState()
)
} -> std::same_as<CompiledSurface>;
};
@@ -77,8 +75,7 @@ export namespace mean_field::equilibrium {
requires operators::StellarEquilibriumSystemCompilable<std::remove_cvref_t<Candidate>>;
requires hasStellarEquilibriumSurfaceCompilation<std::remove_cvref_t<Candidate>>;
requires operators::CompilableRootManifestFor<
std::remove_cvref_t<Candidate>,
operators::CompiledStellarEquilibriumForm<std::remove_cvref_t<Candidate>>>;
std::remove_cvref_t<Candidate>, operators::CompiledStellarEquilibriumForm<std::remove_cvref_t<Candidate>>>;
requires operators::hasStellarEquilibriumCoreRuntime<std::remove_cvref_t<Candidate>>;
requires operators::hasCompleteStellarEquilibriumRuntime<std::remove_cvref_t<Candidate>>;
requires operators::stellarEquilibriumRotationProviderCount<std::remove_cvref_t<Candidate>> <= 1;
@@ -105,19 +102,16 @@ export namespace mean_field::equilibrium {
operators::StellarEquilibriumPhysicalCoreType<std::remove_cvref_t<Model>>,
typename std::remove_cvref_t<Model>::SpecificationTypes>> { };
struct StellarEquilibriumProblemFactory;
} // namespace detail
template <
StellarEquilibriumModel Model,
StellarDiscretizationType Discretization = StellarDiscretization>
template <StellarEquilibriumModel Model, StellarDiscretizationType Discretization = StellarDiscretization>
requires detail::StellarEquilibriumModelDiscretizationStructureAudit<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value
class StellarEquilibriumProblem final {
public:
using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
using NormalizationPrescriptionType = typename DiscretizationType::NormalizationPrescriptionType;
static constexpr bool hasFixedCentralDensity =
@@ -128,13 +122,15 @@ export namespace mean_field::equilibrium {
operators::stellarEquilibriumRotationProviderCount<ModelType>;
static constexpr bool symbolicallySquare = ModelType::symbolicallySquare;
using PreparedOperatorType = operators::PreparedVariadicStellarEquilibriumOperator<ModelType>;
using PhysicalCoreType = typename PreparedOperatorType::PhysicalCoreType;
using FormType = operators::CompiledStellarEquilibriumForm<ModelType>;
using JacobianFormType = operators::CompiledStellarEquilibriumJacobianForm<ModelType>;
using ManifestType = operators::EquilibriumSystemManifest<ModelType, FormType, JacobianFormType>;
using EquationOfStateType = model::EquationOfStateType<ModelType>;
using SurfaceConditionType = model::SurfaceConditionType<ModelType>;
using PreparedOperatorType = operators::PreparedVariadicStellarEquilibriumOperator<ModelType>;
using Report = typename PreparedOperatorType::Report;
using PreparationResult = typename PreparedOperatorType::PreparationResult;
using PhysicalCoreType = typename PreparedOperatorType::PhysicalCoreType;
using FormType = operators::CompiledStellarEquilibriumForm<ModelType>;
using JacobianFormType = operators::CompiledStellarEquilibriumJacobianForm<ModelType>;
using ManifestType = operators::EquilibriumSystemManifest<ModelType, FormType, JacobianFormType>;
using EquationOfStateType = model::EquationOfStateType<ModelType>;
using SurfaceConditionType = model::SurfaceConditionType<ModelType>;
using AvailableThermodynamicEquations = material::StellarEquilibriumThermodynamicEquations;
using ThermodynamicEquationsType =
material::CompiledThermodynamicEquationsT<EquationOfStateType, FormType, AvailableThermodynamicEquations>;
@@ -147,17 +143,18 @@ export namespace mean_field::equilibrium {
StellarEquilibriumProblem(
ModelType stellarModel,
DiscretizationType discretization
DiscretizationType discretization,
fem::FEM &finiteElementModel
)
: m_stellarModel(std::make_shared<ModelType>(std::move(stellarModel))),
m_discretization(std::move(discretization)),
m_compiledSurfaceConstraint(CompileSurfaceConstraint(*m_stellarModel)),
m_preparedOperator(
m_discretization.finiteElementModel(),
finiteElementModel,
m_discretization.domainMapper(),
m_stellarModel,
operators::PressureSurfaceConstraintView{m_compiledSurfaceConstraint},
CompileDefaultDomainDeformation(m_discretization.finiteElementModel())
CompileDefaultDomainDeformation(finiteElementModel)
) {
VerifyProblem();
}
@@ -176,6 +173,12 @@ export namespace mean_field::equilibrium {
return m_discretization;
}
[[nodiscard]] MPI_Comm GetCommunicator() const & {
return m_discretization.communicator();
}
[[nodiscard]] MPI_Comm GetCommunicator() const && = delete;
[[nodiscard]] const NormalizationPrescriptionType &GetNormalizationPrescription() const noexcept {
return m_discretization.normalizationPrescription();
}
@@ -236,21 +239,54 @@ export namespace mean_field::equilibrium {
const mfem::Vector &state,
const operators::StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) requires(generatedRotationProviderCount == 0) {
)
requires(generatedRotationProviderCount == 0)
{
auto report = m_preparedOperator.Prepare(state, dependencies, rotation);
++m_preparationGeneration;
return report;
}
[[nodiscard]] PreparationResult TryPrepare(
const mfem::Vector &state,
const operators::StellarEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
)
requires(generatedRotationProviderCount == 0)
{
auto result = m_preparedOperator.TryPrepare(state, dependencies, rotation);
if (!result.has_value()) {
return std::unexpected(result.error());
}
++m_preparationGeneration;
return result;
}
[[nodiscard]] auto Prepare(
const mfem::Vector &state,
const operators::StellarEquilibriumDependencies &dependencies
) requires(generatedRotationProviderCount == 1) {
)
requires(generatedRotationProviderCount == 1)
{
auto report = m_preparedOperator.Prepare(state, dependencies);
++m_preparationGeneration;
return report;
}
[[nodiscard]] PreparationResult TryPrepare(
const mfem::Vector &state,
const operators::StellarEquilibriumDependencies &dependencies
)
requires(generatedRotationProviderCount == 1)
{
auto result = m_preparedOperator.TryPrepare(state, dependencies);
if (!result.has_value()) {
return std::unexpected(result.error());
}
++m_preparationGeneration;
return result;
}
void BuildResidual(mfem::Vector &residual) const {
m_preparedOperator.BuildResidual(residual);
}
@@ -262,12 +298,18 @@ export namespace mean_field::equilibrium {
m_preparedOperator.Mult(direction, action);
}
void BuildVolumeDisplacementDirection(
const mfem::Vector &stateDirection,
mfem::Vector &volumeDisplacementDirection
) const {
m_preparedOperator.BuildVolumeDisplacementDirection(stateDirection, volumeDisplacementDirection);
}
private:
[[nodiscard]] static CompiledSurfaceConstraintType CompileSurfaceConstraint(const ModelType &stellarModel) {
return surface::compilePressureSurfaceConstraint<
typename ThermodynamicEquationsType::PressureSurfaceFormulation>(
stellarModel.surfaceCondition(),
stellarModel.equationOfState()
stellarModel.surfaceCondition(), stellarModel.equationOfState()
);
}
@@ -318,28 +360,25 @@ export namespace mean_field::equilibrium {
template <
typename Model,
typename Discretization,
bool StructurallyCompatible =
StellarEquilibriumModelDiscretizationStructureAudit<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value>
bool StructurallyCompatible = StellarEquilibriumModelDiscretizationStructureAudit<
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value>
struct StellarEquilibriumModelDiscretizationOperationAudit : std::false_type { };
template <typename Model, typename Discretization>
struct StellarEquilibriumModelDiscretizationOperationAudit<
Model,
Discretization,
true> {
struct StellarEquilibriumModelDiscretizationOperationAudit<Model, Discretization, true> {
private:
using ModelType = std::remove_cvref_t<Model>;
using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
using Problem = StellarEquilibriumProblem<ModelType, DiscretizationType>;
using Prescription = typename DiscretizationType::NormalizationPrescriptionType;
using Problem = StellarEquilibriumProblem<ModelType, DiscretizationType>;
using Prescription = typename DiscretizationType::NormalizationPrescriptionType;
public:
static constexpr bool value = [] {
if constexpr (
std::same_as<Prescription, normalization::Unnormalized> ||
normalization::PhysicalRieszDiagonalPrescription<Prescription>) {
normalization::PhysicalRieszDiagonalPrescription<Prescription>
) {
return true;
} else {
return normalization::RuntimePreparedNormalizationOperation<Problem>;
@@ -362,46 +401,99 @@ export namespace mean_field::equilibrium {
std::remove_cvref_t<Model>,
std::remove_cvref_t<Discretization>>::value;
namespace detail {
/* The structurally formed problem type is needed to probe the ADL
* operation without a recursive concept. Its constructor remains
* private, and this factory is the single construction authority after
* the complete public compatibility contract has succeeded. */
struct StellarEquilibriumProblemFactory final {
template <StellarEquilibriumModel Model, StellarDiscretizationType Discretization>
requires StellarEquilibriumModelDiscretizationCompatible<Model, Discretization>
[[nodiscard]] static auto Create(
Model &&stellarModel,
Discretization discretization
) {
using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
return StellarEquilibriumProblem<ModelType, DiscretizationType>{
std::forward<Model>(stellarModel),
std::move(discretization)
};
}
};
} // namespace detail
} // namespace mean_field::equilibrium
template <StellarEquilibriumModel Model, StellarDiscretizationType Discretization>
requires StellarEquilibriumModelDiscretizationCompatible<Model, Discretization>
namespace mean_field::equilibrium::detail {
/* The structurally formed problem type is needed to probe the ADL
* operation without a recursive concept. Its constructor remains
* private, and this factory is the single construction authority after
* the complete public compatibility contract has succeeded. */
struct StellarEquilibriumProblemFactory final {
template <
StellarEquilibriumModel Model,
StellarDiscretizationType Discretization>
requires StellarEquilibriumModelDiscretizationCompatible<
Model,
Discretization>
[[nodiscard]] static auto Create(
Model &&stellarModel,
Discretization discretization
) {
using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
fem::FEM &finiteElementModel = discretization.MutableFiniteElementModelForAssembly();
return StellarEquilibriumProblem<ModelType, DiscretizationType>{
std::forward<Model>(stellarModel), std::move(discretization), finiteElementModel
};
}
template <
StellarEquilibriumModel Model,
StellarDiscretizationType Discretization>
requires StellarEquilibriumModelDiscretizationCompatible<
Model,
Discretization>
[[nodiscard]] static auto CreateOwned(
Model &&stellarModel,
Discretization discretization
) {
using ModelType = std::remove_cvref_t<Model>;
using DiscretizationType = std::remove_cvref_t<Discretization>;
using ProblemType = StellarEquilibriumProblem<ModelType, DiscretizationType>;
fem::FEM &finiteElementModel = discretization.MutableFiniteElementModelForAssembly();
return std::unique_ptr<ProblemType>{
new ProblemType{std::forward<Model>(stellarModel), std::move(discretization), finiteElementModel}
};
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[nodiscard]] static fem::FEM &MutableFiniteElementModelForProjection(Problem &problem) {
return problem.m_discretization.MutableFiniteElementModelForAssembly();
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[nodiscard]] static const fem::FEM &FiniteElementModel(const Problem &problem) {
return problem.m_discretization.RequireFiniteElementModel();
}
};
} // namespace mean_field::equilibrium::detail
export namespace mean_field::equilibrium {
template <
StellarEquilibriumModel Model,
StellarDiscretizationType Discretization>
requires StellarEquilibriumModelDiscretizationCompatible<
Model,
Discretization>
[[nodiscard]] auto discretize(
Model &&stellarModel,
Discretization discretization
) {
return detail::StellarEquilibriumProblemFactory::Create(
std::forward<Model>(stellarModel),
std::move(discretization)
std::forward<Model>(stellarModel), std::move(discretization)
);
}
template <StellarEquilibriumModel Model>
requires StellarEquilibriumModelDiscretizationCompatible<Model, StellarDiscretization>
requires StellarEquilibriumModelDiscretizationCompatible<
Model,
StellarDiscretization>
[[nodiscard]] auto discretize(
Model &&stellarModel,
fem::FEM &finiteElementModel
fem::FEM &&finiteElementModel
) {
return discretize(std::forward<Model>(stellarModel), StellarDiscretization{finiteElementModel});
return discretize(std::forward<Model>(stellarModel), StellarDiscretization{std::move(finiteElementModel)});
}
template <StellarEquilibriumModel Model>
requires StellarEquilibriumModelDiscretizationCompatible<
Model,
StellarDiscretization>
StellarEquilibriumProblem<
std::remove_cvref_t<Model>,
StellarDiscretization>
discretize(
Model &&,
fem::FEM &
) = delete;
} // namespace mean_field::equilibrium

View File

@@ -17,10 +17,9 @@ export namespace mean_field::equilibrium {
template <StellarEquilibriumModel Model>
[[nodiscard]] auto makeStellarEquilibriumSystem(
fem::FEM &finiteElementModel,
const mapping::DomainMapper &domainMapper,
fem::FEM &&finiteElementModel,
Model &&stellarModel
) {
return discretize(std::forward<Model>(stellarModel), StellarDiscretization{finiteElementModel, domainMapper});
return discretize(std::forward<Model>(stellarModel), std::move(finiteElementModel));
}
} // namespace mean_field::equilibrium

View File

@@ -325,8 +325,14 @@ export namespace mean_field::preconditioning {
}
}
PreparedStellarPreconditioner(ProblemType &&, BlockType) = delete;
PreparedStellarPreconditioner(const ProblemType &&, BlockType) = delete;
PreparedStellarPreconditioner(
ProblemType &&,
BlockType
) = delete;
PreparedStellarPreconditioner(
const ProblemType &&,
BlockType
) = delete;
PreparedStellarPreconditioner(const PreparedStellarPreconditioner &) = delete;
PreparedStellarPreconditioner &operator=(const PreparedStellarPreconditioner &) = delete;
@@ -399,9 +405,11 @@ export namespace mean_field::preconditioning {
equilibrium::DiscretizedStellarEquilibriumProblem Problem,
SpecificationBorderBlockType Block>
requires EquilibriumCoordinateComponentFor<
Block,
typename std::remove_cvref_t<Problem>::FormType> &&
SpecificationBorderPreparableFor<Problem, Block>
Block,
typename std::remove_cvref_t<Problem>::FormType> &&
SpecificationBorderPreparableFor<
Problem,
Block>
[[nodiscard]] auto prepare(
const Problem &problem,
Block block
@@ -409,17 +417,22 @@ export namespace mean_field::preconditioning {
return PreparedStellarPreconditioner<Problem, Block>{problem, std::move(block)};
}
template <typename Problem, SpecificationBorderBlockType Block>
requires (!std::is_lvalue_reference_v<Problem>) &&
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
EquilibriumCoordinateComponentFor<
Block,
typename std::remove_cvref_t<Problem>::FormType> &&
SpecificationBorderPreparableFor<std::remove_cvref_t<Problem>, Block>
template <
typename Problem,
SpecificationBorderBlockType Block>
requires(!std::is_lvalue_reference_v<Problem>) &&
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
EquilibriumCoordinateComponentFor<
Block,
typename std::remove_cvref_t<Problem>::FormType> &&
SpecificationBorderPreparableFor<
std::remove_cvref_t<Problem>,
Block>
[[nodiscard]] auto prepare(
Problem &&,
Block
) -> PreparedStellarPreconditioner<
std::remove_cvref_t<Problem>,
std::remove_cvref_t<Block>> = delete;
)
-> PreparedStellarPreconditioner<
std::remove_cvref_t<Problem>,
std::remove_cvref_t<Block>> = delete;
} // namespace mean_field::preconditioning

View File

@@ -236,15 +236,13 @@ export namespace mean_field::preconditioning {
* assembly. The current kernels remain polytropic, but selection no
* longer embeds that closed-world type test in the descriptor concept.
*/
template <typename EquationOfState>
struct MaterialSurfaceEquationOfStateBackend {
template <typename EquationOfState> struct MaterialSurfaceEquationOfStateBackend {
static constexpr bool registered = false;
};
template <>
struct MaterialSurfaceEquationOfStateBackend<eos::Polytrope> {
template <> struct MaterialSurfaceEquationOfStateBackend<eos::Polytrope> {
static constexpr bool registered = true;
using CoreType = operators::PreparedStellarEquilibriumOperator;
using CoreType = operators::PreparedStellarEquilibriumOperator;
};
template <typename EquationOfState>
@@ -252,8 +250,7 @@ export namespace mean_field::preconditioning {
{
MaterialSurfaceEquationOfStateBackend<std::remove_cvref_t<EquationOfState>>::registered
} -> std::convertible_to<bool>;
requires MaterialSurfaceEquationOfStateBackend<
std::remove_cvref_t<EquationOfState>>::registered;
requires MaterialSurfaceEquationOfStateBackend<std::remove_cvref_t<EquationOfState>>::registered;
typename MaterialSurfaceEquationOfStateBackend<std::remove_cvref_t<EquationOfState>>::CoreType;
};
@@ -266,13 +263,11 @@ export namespace mean_field::preconditioning {
* truthful while the current kernels still consume the legacy physical
* core directly.
*/
template <typename EquationOfState, typename PhysicalCore>
struct MaterialSurfaceExecutableRuntime {
template <typename EquationOfState, typename PhysicalCore> struct MaterialSurfaceExecutableRuntime {
static constexpr bool available = false;
};
template <>
struct MaterialSurfaceExecutableRuntime<eos::Polytrope, operators::PreparedStellarEquilibriumOperator> {
template <> struct MaterialSurfaceExecutableRuntime<eos::Polytrope, operators::PreparedStellarEquilibriumOperator> {
static constexpr bool available = true;
};
@@ -280,33 +275,28 @@ export namespace mean_field::preconditioning {
concept ExecutableMaterialSurfaceRuntimeFor = requires {
{
MaterialSurfaceExecutableRuntime<
std::remove_cvref_t<EquationOfState>,
std::remove_cvref_t<PhysicalCore>>::available
std::remove_cvref_t<EquationOfState>, std::remove_cvref_t<PhysicalCore>>::available
} -> std::convertible_to<bool>;
requires MaterialSurfaceExecutableRuntime<
std::remove_cvref_t<EquationOfState>,
std::remove_cvref_t<PhysicalCore>>::available;
std::remove_cvref_t<EquationOfState>, std::remove_cvref_t<PhysicalCore>>::available;
};
template <typename Descriptor>
concept ImplementedMaterialSurfaceDescriptor =
MaterialSurfaceDescriptor<Descriptor> &&
ImplementedMaterialSurfaceEquationOfState<
typename Descriptor::ThermodynamicEquations::EquationOfStateType>;
ImplementedMaterialSurfaceEquationOfState<typename Descriptor::ThermodynamicEquations::EquationOfStateType>;
template <typename Descriptor, typename PhysicalCore>
concept MaterialSurfaceRuntimeFor =
ImplementedMaterialSurfaceDescriptor<Descriptor> && requires {
concept MaterialSurfaceRuntimeFor = ImplementedMaterialSurfaceDescriptor<Descriptor> && requires {
typename MaterialSurfaceEquationOfStateBackend<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType;
requires std::same_as<
std::remove_cvref_t<PhysicalCore>,
typename MaterialSurfaceEquationOfStateBackend<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType;
requires std::same_as<
std::remove_cvref_t<PhysicalCore>,
typename MaterialSurfaceEquationOfStateBackend<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType>;
requires ExecutableMaterialSurfaceRuntimeFor<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType,
PhysicalCore>;
};
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType>;
requires ExecutableMaterialSurfaceRuntimeFor<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType, PhysicalCore>;
};
template <typename Candidate>
concept MaterialSurfacePreconditionerProblem =
@@ -778,14 +768,14 @@ export namespace mean_field::preconditioning {
fullEnthalpyDirection = enthalpyDirection;
m_operation->Mult(m_fullDirection, m_fullAction);
const auto fullActionView = m_operation->GetRootManifest().residualView(m_fullAction);
const auto fullActionView = m_operation->GetRootManifest().residualView(m_fullAction);
const auto fullDensityAction = fullActionView.block(utils::blocks::density_field.mass_term);
const auto fullSurfaceAction =
fullActionView.block(utils::blocks::surface_deformation_field.shape_equilibrium_term);
const auto fullEnthalpyAction = fullActionView.block(utils::blocks::enthalpy_field.specific_term);
densityAction = fullDensityAction;
surfaceAction = fullSurfaceAction;
enthalpyAction = fullEnthalpyAction;
densityAction = fullDensityAction;
surfaceAction = fullSurfaceAction;
enthalpyAction = fullEnthalpyAction;
}
void ApplyEnthalpyToDensity(
@@ -1121,6 +1111,79 @@ export namespace mean_field::preconditioning {
std::uint64_t regularizedEntries{0};
};
namespace detail {
[[nodiscard]] inline DiagonalPreparationQuality regularizeMaterialSurfaceDiagonal(
mfem::Vector &diagonal,
const MaterialSurfaceDiagonalOptions options,
const MPI_Comm communicator
) {
const int localOptionsAreValid = std::isfinite(options.relativeFloor) && options.relativeFloor >= 0.0 &&
std::isfinite(options.absoluteFloor) && options.absoluteFloor > 0.0
? 1
: 0;
int globalOptionsAreValid = 0;
if (MPI_Allreduce(&localOptionsAreValid, &globalOptionsAreValid, 1, MPI_INT, MPI_MIN, communicator) !=
MPI_SUCCESS) {
throw std::runtime_error("Material-surface regularization could not validate its options.");
}
if (globalOptionsAreValid == 0) {
throw std::invalid_argument("Material-surface diagonal floors must be finite and nonnegative.");
}
double localMaximum = 0.0;
double localMinimum = std::numeric_limits<double>::infinity();
int localEntriesAreFinite = 1;
for (int index = 0; index < diagonal.Size(); ++index) {
if (!std::isfinite(diagonal(index))) {
localEntriesAreFinite = 0;
continue;
}
const double magnitude = std::abs(diagonal(index));
localMaximum = std::max(localMaximum, magnitude);
localMinimum = std::min(localMinimum, magnitude);
}
int globalEntriesAreFinite = 0;
if (MPI_Allreduce(&localEntriesAreFinite, &globalEntriesAreFinite, 1, MPI_INT, MPI_MIN, communicator) !=
MPI_SUCCESS) {
throw std::runtime_error("Material-surface regularization could not validate its diagonal.");
}
if (globalEntriesAreFinite == 0) {
throw std::invalid_argument("A material-surface diagonal contains a non-finite entry.");
}
double globalMaximum = 0.0;
double globalMinimum = 0.0;
const int maximumStatus =
MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, communicator);
const int minimumStatus =
MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator);
if (maximumStatus != MPI_SUCCESS || minimumStatus != MPI_SUCCESS) {
throw std::runtime_error("Material-surface regularization could not reduce diagonal magnitudes.");
}
const double floor = std::max(options.absoluteFloor, options.relativeFloor * globalMaximum);
std::uint64_t localRegularized = 0;
for (int index = 0; index < diagonal.Size(); ++index) {
if (std::abs(diagonal(index)) < floor) {
diagonal(index) = std::copysign(floor, diagonal(index) == 0.0 ? 1.0 : diagonal(index));
++localRegularized;
}
}
std::uint64_t globalRegularized = 0;
if (MPI_Allreduce(&localRegularized, &globalRegularized, 1, MPI_UINT64_T, MPI_SUM, communicator) !=
MPI_SUCCESS) {
throw std::runtime_error("Material-surface regularization could not count regularized entries.");
}
return {
.minimumAbsoluteEntryBeforeRegularization = globalMinimum,
.maximumAbsoluteEntryBeforeRegularization = globalMaximum,
.appliedFloor = floor,
.regularizedEntries = globalRegularized
};
}
} // namespace detail
struct SurfaceRieszCalibrationReport final {
SurfaceRieszCalibrationTarget target{SurfaceRieszCalibrationTarget::none};
int probeCount{0};
@@ -1522,40 +1585,7 @@ export namespace mean_field::preconditioning {
const MaterialSurfaceDiagonalOptions options,
const MPI_Comm communicator
) {
if (!std::isfinite(options.relativeFloor) || options.relativeFloor < 0.0 ||
!std::isfinite(options.absoluteFloor) || options.absoluteFloor <= 0.0) {
throw std::invalid_argument("Material-surface diagonal floors must be finite and nonnegative.");
}
double localMaximum = 0.0;
double localMinimum = std::numeric_limits<double>::infinity();
for (int index = 0; index < diagonal.Size(); ++index) {
if (!std::isfinite(diagonal(index))) {
throw std::invalid_argument("A material-surface diagonal contains a non-finite entry.");
}
const double magnitude = std::abs(diagonal(index));
localMaximum = std::max(localMaximum, magnitude);
localMinimum = std::min(localMinimum, magnitude);
}
double globalMaximum = 0.0;
double globalMinimum = 0.0;
MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, communicator);
MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator);
const double floor = std::max(options.absoluteFloor, options.relativeFloor * globalMaximum);
std::uint64_t localRegularized = 0;
for (int index = 0; index < diagonal.Size(); ++index) {
if (std::abs(diagonal(index)) < floor) {
diagonal(index) = std::copysign(floor, diagonal(index) == 0.0 ? 1.0 : diagonal(index));
++localRegularized;
}
}
std::uint64_t globalRegularized = 0;
MPI_Allreduce(&localRegularized, &globalRegularized, 1, MPI_UINT64_T, MPI_SUM, communicator);
return {
.minimumAbsoluteEntryBeforeRegularization = globalMinimum,
.maximumAbsoluteEntryBeforeRegularization = globalMaximum,
.appliedFloor = floor,
.regularizedEntries = globalRegularized
};
return detail::regularizeMaterialSurfaceDiagonal(diagonal, options, communicator);
}
Block m_block;
@@ -2051,40 +2081,7 @@ export namespace mean_field::preconditioning {
const MaterialSurfaceDiagonalOptions options,
const MPI_Comm communicator
) {
if (!std::isfinite(options.relativeFloor) || options.relativeFloor < 0.0 ||
!std::isfinite(options.absoluteFloor) || options.absoluteFloor <= 0.0) {
throw std::invalid_argument("Material-surface diagonal floors must be finite and nonnegative.");
}
double localMaximum = 0.0;
double localMinimum = std::numeric_limits<double>::infinity();
for (int index = 0; index < diagonal.Size(); ++index) {
if (!std::isfinite(diagonal(index))) {
throw std::invalid_argument("A material-surface diagonal contains a non-finite entry.");
}
const double magnitude = std::abs(diagonal(index));
localMaximum = std::max(localMaximum, magnitude);
localMinimum = std::min(localMinimum, magnitude);
}
double globalMaximum = 0.0;
double globalMinimum = 0.0;
MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, communicator);
MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator);
const double floor = std::max(options.absoluteFloor, options.relativeFloor * globalMaximum);
std::uint64_t localRegularized = 0;
for (int index = 0; index < diagonal.Size(); ++index) {
if (std::abs(diagonal(index)) < floor) {
diagonal(index) = std::copysign(floor, diagonal(index) == 0.0 ? 1.0 : diagonal(index));
++localRegularized;
}
}
std::uint64_t globalRegularized = 0;
MPI_Allreduce(&localRegularized, &globalRegularized, 1, MPI_UINT64_T, MPI_SUM, communicator);
return {
.minimumAbsoluteEntryBeforeRegularization = globalMinimum,
.maximumAbsoluteEntryBeforeRegularization = globalMaximum,
.appliedFloor = floor,
.regularizedEntries = globalRegularized
};
return detail::regularizeMaterialSurfaceDiagonal(diagonal, options, communicator);
}
Block m_block;
@@ -2111,7 +2108,9 @@ export namespace mean_field::preconditioning {
template <
MaterialSurfaceDescriptor Descriptor,
MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
requires MaterialSurfaceRuntimeFor<
Descriptor,
operators::PreparedStellarEquilibriumOperator>
[[nodiscard]] auto prepare(
const operators::PreparedStellarEquilibriumOperator &operation,
MaterialSurfaceBlock<
@@ -2127,12 +2126,17 @@ export namespace mean_field::preconditioning {
equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfacePreconditionerProblem<
equilibrium::StellarEquilibriumProblem<Model, Discretization>>
requires MaterialSurfacePreconditionerProblem<equilibrium::StellarEquilibriumProblem<
Model,
Discretization>>
[[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
const equilibrium::StellarEquilibriumProblem<
Model,
Discretization> &problem,
MaterialSurfaceBlock<
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model, Discretization>>,
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<
Model,
Discretization>>,
backend::Diagonal,
backend::Diagonal,
Policy> block
@@ -2144,7 +2148,9 @@ export namespace mean_field::preconditioning {
MaterialSurfaceDescriptor Descriptor,
MaterialSurfaceFactorizationPolicy Policy,
backend::ApplicationMode Mode>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
requires MaterialSurfaceRuntimeFor<
Descriptor,
operators::PreparedStellarEquilibriumOperator>
[[nodiscard]] auto prepare(
const operators::PreparedStellarEquilibriumOperator &operation,
MaterialSurfaceBlock<
@@ -2162,12 +2168,17 @@ export namespace mean_field::preconditioning {
equilibrium::StellarDiscretizationType Discretization,
MaterialSurfaceFactorizationPolicy Policy,
backend::ApplicationMode Mode>
requires MaterialSurfacePreconditionerProblem<
equilibrium::StellarEquilibriumProblem<Model, Discretization>>
requires MaterialSurfacePreconditionerProblem<equilibrium::StellarEquilibriumProblem<
Model,
Discretization>>
[[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
const equilibrium::StellarEquilibriumProblem<
Model,
Discretization> &problem,
MaterialSurfaceBlock<
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model, Discretization>>,
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<
Model,
Discretization>>,
backend::Diagonal,
backend::HypreBoomerAMG<Mode>,
Policy,

View File

@@ -9,3 +9,4 @@ export import :preconditioning.stellar_equilibrium;
export import :preconditioning.stellar_structure;
export import :preconditioning.specification_border;
export import :preconditioning.equilibrium_coordinates;
export import :preconditioning.stellar_recipe;

View File

@@ -63,7 +63,7 @@ export namespace mean_field::preconditioning {
.geometry = prepared.geometry != current.geometry,
.equationOfState = prepared.equationOfStateIdentity != current.equationOfStateIdentity,
.linearization = prepared.linearization != current.linearization ||
prepared.preparedOperatorGeneration != current.preparedOperatorGeneration
prepared.preparedOperatorGeneration != current.preparedOperatorGeneration
};
}
@@ -97,9 +97,7 @@ export namespace mean_field::preconditioning {
static constexpr bool registered = false;
};
template <
equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization>
template <equilibrium::StellarEquilibriumModel Model, equilibrium::StellarDiscretizationType Discretization>
struct StellarEquilibriumProblemTraits<equilibrium::StellarEquilibriumProblem<Model, Discretization>> {
using Problem = equilibrium::StellarEquilibriumProblem<Model, Discretization>;
using Form = typename Problem::FormType;
@@ -131,10 +129,9 @@ export namespace mean_field::preconditioning {
[[nodiscard]] static StellarPreconditionerLifecycleSnapshot Snapshot(const Problem &problem) {
const operators::StellarEquilibriumDependencies &dependencies = problem.GetLinearizationDependencies();
return {
.discretization = dependencies.discretization,
.geometry = problem.GetGeometryDependency(),
.equationOfStateIdentity =
std::addressof(problem.GetStellarModel().equationOfState()),
.discretization = dependencies.discretization,
.geometry = problem.GetGeometryDependency(),
.equationOfStateIdentity = std::addressof(problem.GetStellarModel().equationOfState()),
.linearization = dependencies,
.preparedOperatorGeneration = problem.GetPreparationGeneration()
};
@@ -145,26 +142,20 @@ export namespace mean_field::preconditioning {
concept StellarPreconditionerProblem = StellarEquilibriumProblemTraits<std::remove_cvref_t<Candidate>>::registered;
namespace detail {
template <typename Block>
struct IsGeneratedStellarValueBlock : std::false_type { };
template <typename Block> struct IsGeneratedStellarValueBlock : std::false_type { };
template <typename Generated>
struct IsGeneratedStellarValueBlock<utils::blocks::generated_value_block<Generated>>
: std::true_type { };
struct IsGeneratedStellarValueBlock<utils::blocks::generated_value_block<Generated>> : std::true_type { };
template <typename Block>
struct IsGeneratedStellarResidualBlock : std::false_type { };
template <typename Block> struct IsGeneratedStellarResidualBlock : std::false_type { };
template <typename Generated>
struct IsGeneratedStellarResidualBlock<utils::blocks::generated_residual_block<Generated>>
: std::true_type { };
struct IsGeneratedStellarResidualBlock<utils::blocks::generated_residual_block<Generated>> : std::true_type { };
template <typename Coupling>
inline constexpr bool isPurePhysicalStellarCoupling =
!IsGeneratedStellarValueBlock<
std::remove_cvref_t<typename Coupling::Value>>::value &&
!IsGeneratedStellarResidualBlock<
std::remove_cvref_t<typename Coupling::Residual>>::value;
!IsGeneratedStellarValueBlock<std::remove_cvref_t<typename Coupling::Value>>::value &&
!IsGeneratedStellarResidualBlock<std::remove_cvref_t<typename Coupling::Residual>>::value;
/* Pure structure contributions owned by a trusted backend are exact
* (core, specification, coupling) capabilities. Future cores and new
@@ -172,8 +163,7 @@ export namespace mean_field::preconditioning {
* be accompanied by an explicit preconditioner decision. Generated-
* border terms remain the responsibility of specification-border
* machinery. */
template <typename PhysicalCore, typename Specification>
struct StellarStructureBackendHandledCouplings {
template <typename PhysicalCore, typename Specification> struct StellarStructureBackendHandledCouplings {
using Type = utils::blocks::type_list<>;
};
@@ -213,17 +203,12 @@ export namespace mean_field::preconditioning {
struct StellarStructureBackendHandledCouplings<
operators::PreparedStellarEquilibriumOperator,
models::FixedCentralDensity> {
using Type = utils::blocks::type_list<
operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::enthalpy::specific::value>>;
using Type = utils::blocks::type_list<operators::StellarEquilibriumJacobianCoupling<
utils::blocks::enthalpy::specific::residual,
utils::blocks::enthalpy::specific::value>>;
};
template <
typename Coupling,
typename Model,
typename PhysicalCore,
typename ModelSpecifications>
template <typename Coupling, typename Model, typename PhysicalCore, typename ModelSpecifications>
struct EveryCouplingContributionHandled;
template <
@@ -235,30 +220,22 @@ export namespace mean_field::preconditioning {
Coupling,
Model,
PhysicalCore,
models::detail::SpecificationSetStorage<Specifications...>> final {
models::detail::SpecificationSetStorage<Specifications...>>
final {
private:
template <typename Specification>
static constexpr bool handled =
!utils::blocks::contains_type_v<
Coupling,
typename operators::StellarEquilibriumSpecificationCompilation<
Specification>::JacobianCouplings> ||
(operators::stellarEquilibriumBackendRuntimeAuthorized<
Specification,
Model> &&
typename operators::StellarEquilibriumSpecificationCompilation<Specification>::JacobianCouplings> ||
(operators::stellarEquilibriumBackendRuntimeAuthorized<Specification, Model> &&
utils::blocks::contains_type_v<
Coupling,
typename StellarStructureBackendHandledCouplings<
PhysicalCore,
Specification>::Type>) ||
operators::stellarEquilibriumSpecificationCouplingIsStructuralZero<
Specification,
Model,
Coupling>;
typename StellarStructureBackendHandledCouplings<PhysicalCore, Specification>::Type>) ||
operators::stellarEquilibriumSpecificationCouplingIsStructuralZero<Specification, Model, Coupling>;
public:
static constexpr bool value =
(handled<Specifications> && ...);
static constexpr bool value = (handled<Specifications> && ...);
};
template <
@@ -269,11 +246,7 @@ export namespace mean_field::preconditioning {
typename Unsupported>
struct CollectUnsupportedStellarStructureCouplings;
template <
typename Model,
typename PhysicalCore,
typename ModelSpecifications,
typename Unsupported>
template <typename Model, typename PhysicalCore, typename ModelSpecifications, typename Unsupported>
struct CollectUnsupportedStellarStructureCouplings<
utils::blocks::type_list<>,
Model,
@@ -299,11 +272,7 @@ export namespace mean_field::preconditioning {
private:
static constexpr bool supported =
!isPurePhysicalStellarCoupling<Head> ||
EveryCouplingContributionHandled<
Head,
Model,
PhysicalCore,
ModelSpecifications>::value;
EveryCouplingContributionHandled<Head, Model, PhysicalCore, ModelSpecifications>::value;
using Next = std::conditional_t<
supported,
utils::blocks::type_list<Unsupported...>,
@@ -318,42 +287,36 @@ export namespace mean_field::preconditioning {
Next>::Type;
};
template <typename Candidate, typename = void>
struct DefaultStellarStructurePhysicalTopologyAudit {
using ContributionCouplings = utils::blocks::type_list<>;
using UnsupportedCouplings = utils::blocks::type_list<>;
template <typename Candidate, typename = void> struct DefaultStellarStructurePhysicalTopologyAudit {
using ContributionCouplings = utils::blocks::type_list<>;
using UnsupportedCouplings = utils::blocks::type_list<>;
static constexpr bool supported = false;
};
template <model::StellarModelType Model>
requires(
operators::StellarEquilibriumSystemCompilable<
std::remove_cvref_t<Model>> &&
operators::hasStellarEquilibriumCoreRuntime<
std::remove_cvref_t<Model>>)
operators::StellarEquilibriumSystemCompilable<std::remove_cvref_t<Model>> &&
operators::hasStellarEquilibriumCoreRuntime<std::remove_cvref_t<Model>>
)
struct DefaultStellarStructurePhysicalTopologyAudit<
Model,
std::void_t<
typename operators::CompiledStellarEquilibriumSystem<
std::remove_cvref_t<Model>>::ContributionJacobianCouplings,
operators::StellarEquilibriumPhysicalCoreType<
std::remove_cvref_t<Model>>>> {
operators::StellarEquilibriumPhysicalCoreType<std::remove_cvref_t<Model>>>> {
private:
using Compilation = operators::CompiledStellarEquilibriumSystem<
std::remove_cvref_t<Model>>;
using PhysicalCore = operators::StellarEquilibriumPhysicalCoreType<
std::remove_cvref_t<Model>>;
using Compilation = operators::CompiledStellarEquilibriumSystem<std::remove_cvref_t<Model>>;
using PhysicalCore = operators::StellarEquilibriumPhysicalCoreType<std::remove_cvref_t<Model>>;
public:
using ContributionCouplings =
typename Compilation::ContributionJacobianCouplings;
using UnsupportedCouplings =
typename CollectUnsupportedStellarStructureCouplings<
ContributionCouplings,
std::remove_cvref_t<Model>,
PhysicalCore,
typename std::remove_cvref_t<Model>::SpecificationTypes,
utils::blocks::type_list<>>::Type;
using ContributionCouplings = typename Compilation::ContributionJacobianCouplings;
using UnsupportedCouplings = typename CollectUnsupportedStellarStructureCouplings<
ContributionCouplings,
std::remove_cvref_t<Model>,
PhysicalCore,
typename std::remove_cvref_t<Model>::SpecificationTypes,
utils::blocks::type_list<>>::Type;
static constexpr bool supported = UnsupportedCouplings::size == 0;
};
@@ -369,13 +332,11 @@ export namespace mean_field::preconditioning {
* detection-safe and therefore suitable for constraining factories. */
template <typename Candidate>
struct DefaultStellarStructurePhysicalTopologySupport
: detail::DefaultStellarStructurePhysicalTopologyAudit<
std::remove_cvref_t<Candidate>> { };
: detail::DefaultStellarStructurePhysicalTopologyAudit<std::remove_cvref_t<Candidate>> { };
template <typename Candidate>
inline constexpr bool defaultStellarStructurePhysicalTopologySupported =
DefaultStellarStructurePhysicalTopologySupport<
std::remove_cvref_t<Candidate>>::supported;
DefaultStellarStructurePhysicalTopologySupport<std::remove_cvref_t<Candidate>>::supported;
template <typename Candidate>
concept DefaultStellarStructurePhysicalTopologySupportedFor =
@@ -419,9 +380,8 @@ export namespace mean_field::preconditioning {
template <typename Form> struct IdentityPlanForForm;
template <typename... Values, typename... Residuals>
struct IdentityPlanForForm<utils::blocks::block_form<
utils::blocks::type_list<Values...>,
utils::blocks::type_list<Residuals...>>> {
struct IdentityPlanForForm<
utils::blocks::block_form<utils::blocks::type_list<Values...>, utils::blocks::type_list<Residuals...>>> {
static_assert(sizeof...(Values) == sizeof...(Residuals));
using Type = PreconditionerPlan<IdentityBlock<Values, Residuals>...>;

View File

@@ -0,0 +1,83 @@
module;
#include <concepts>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
export module mean_field:preconditioning.stellar_recipe;
export import :preconditioning.equilibrium_coordinates;
export namespace mean_field::preconditioning {
/*
* A stellar-preconditioner prescription is an unbound, owning value. It
* may therefore be created before a problem exists and safely moved into
* the eventual user-owned solve context. A prepared inverse is deliberately a
* separate, problem-bound object with a stable address.
*/
struct StellarPreconditionerPrescriptionTag { };
template <typename Candidate>
concept StellarPreconditionerPrescription =
std::derived_from<std::remove_cvref_t<Candidate>, StellarPreconditionerPrescriptionTag> &&
std::move_constructible<std::remove_cvref_t<Candidate>>;
struct DefaultStellarPreconditioner final : StellarPreconditionerPrescriptionTag { };
/*
* This overload is the user-facing, problem-independent factory. The
* existing makePreconditioner(problem) overload remains the low-level
* factory for the typed, unprepared block assembled below.
*/
[[nodiscard]] constexpr DefaultStellarPreconditioner makePreconditioner() noexcept {
return {};
}
template <typename Candidate, typename Problem>
concept PreparedStellarInverseFor =
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
std::derived_from<std::remove_cvref_t<Candidate>, mfem::Solver> &&
std::destructible<std::remove_cvref_t<Candidate>> &&
requires(std::remove_cvref_t<Candidate> &prepared, const std::remove_cvref_t<Candidate> &constantPrepared) {
{ constantPrepared.GetProblem() } -> std::same_as<const std::remove_cvref_t<Problem> &>;
{ constantPrepared.IsCurrent() } -> std::same_as<bool>;
prepared.Refresh();
};
/*
* Built-in preparation is intentionally policy-first. The same spelling
* can be supplied beside a third-party prescription and found by ADL,
* without adding that prescription to a central registry or switch.
* Preparation requires an already-prepared problem because the current
* physical inverse assembles state-dependent numerical data.
*/
template <DefaultStellarPreconditionerAvailableFor Problem>
[[nodiscard]] auto prepareStellarPreconditioner(
DefaultStellarPreconditioner,
const Problem &problem
) {
return preconditioning::prepare(problem, preconditioning::makePreconditioner(problem));
}
template <typename Prescription, typename Problem>
concept StellarPreconditionerRuntimeAvailableFor =
StellarPreconditionerPrescription<Prescription> &&
equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>> &&
requires(std::remove_cvref_t<Prescription> prescription, const std::remove_cvref_t<Problem> &problem) {
requires std::same_as<
decltype(prepareStellarPreconditioner(std::move(prescription), problem)),
std::remove_cvref_t<decltype(prepareStellarPreconditioner(std::move(prescription), problem))>>;
{
prepareStellarPreconditioner(std::move(prescription), problem)
} -> PreparedStellarInverseFor<std::remove_cvref_t<Problem>>;
};
template <StellarPreconditionerPrescription Prescription, typename Problem>
requires StellarPreconditionerRuntimeAvailableFor<Prescription, Problem>
using PreparedStellarInverseType = std::remove_cvref_t<decltype(prepareStellarPreconditioner(
std::declval<std::remove_cvref_t<Prescription> &&>(),
std::declval<const std::remove_cvref_t<Problem> &>()
))>;
} // namespace mean_field::preconditioning

View File

@@ -634,13 +634,11 @@ export namespace mean_field::preconditioning {
* Add future cores here only together with matching cross-coupling and
* preparation implementations.
*/
template <typename PhysicalCore>
struct StellarStructureExecutableRuntime {
template <typename PhysicalCore> struct StellarStructureExecutableRuntime {
static constexpr bool available = false;
};
template <>
struct StellarStructureExecutableRuntime<operators::PreparedStellarEquilibriumOperator> {
template <> struct StellarStructureExecutableRuntime<operators::PreparedStellarEquilibriumOperator> {
static constexpr bool available = true;
};
@@ -654,14 +652,12 @@ export namespace mean_field::preconditioning {
template <typename Descriptor, typename PhysicalCore>
concept StellarStructureRuntimeFor =
MaterialSurfaceRuntimeFor<Descriptor, PhysicalCore> &&
ExecutableStellarStructureRuntimeFor<PhysicalCore>;
MaterialSurfaceRuntimeFor<Descriptor, PhysicalCore> && ExecutableStellarStructureRuntimeFor<PhysicalCore>;
template <typename Candidate>
concept StellarStructurePreconditionerProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<Candidate> &&
DefaultStellarStructurePhysicalTopologySupportedFor<
typename std::remove_cvref_t<Candidate>::ModelType> &&
DefaultStellarStructurePhysicalTopologySupportedFor<typename std::remove_cvref_t<Candidate>::ModelType> &&
requires {
requires StellarStructureRuntimeFor<
MaterialSurfaceDescriptorFor<std::remove_cvref_t<Candidate>>,
@@ -688,8 +684,7 @@ export namespace mean_field::preconditioning {
preconditioning::prepare(problem, std::move(materialComponent));
preconditioning::prepare(
problem.GetPhysicalOperator().GetHydrostaticOperator().GetFEM(),
problem.GetPhysicalOperator().GetGravityContext().GetGeometryContext(),
std::move(gravityComponent)
problem.GetPhysicalOperator().GetGravityContext().GetGeometryContext(), std::move(gravityComponent)
);
StellarStructureCrossJacobianOperator{problem.GetPhysicalOperator()};
};
@@ -873,19 +868,30 @@ export namespace mean_field::preconditioning {
GravityFactorizationPolicy GravityPolicy,
StellarStructureFactorizationPolicy StructurePolicy>
requires StellarStructurePreparableFor<
equilibrium::StellarEquilibriumProblem<Model, Discretization>,
equilibrium::StellarEquilibriumProblem<
Model,
Discretization>,
MaterialComponent,
GravityFieldBlock<GravityMassBackend, backend::HypreBoomerAMG<Mode>, GravityPolicy>>
GravityFieldBlock<
GravityMassBackend,
backend::HypreBoomerAMG<Mode>,
GravityPolicy>>
[[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
const equilibrium::StellarEquilibriumProblem<
Model,
Discretization> &problem,
StellarStructureBlock<
MaterialComponent,
GravityFieldBlock<
GravityMassBackend,
backend::HypreBoomerAMG<Mode>,
GravityPolicy>,
typename equilibrium::StellarEquilibriumProblem<Model, Discretization>::FormType,
typename equilibrium::StellarEquilibriumProblem<Model, Discretization>::JacobianFormType,
typename equilibrium::StellarEquilibriumProblem<
Model,
Discretization>::FormType,
typename equilibrium::StellarEquilibriumProblem<
Model,
Discretization>::JacobianFormType,
StructurePolicy> structure
) {
return PreparedStellarStructureBlock<

View File

@@ -1,8 +1,8 @@
module;
#include <cmath>
#include <cstddef>
#include <concepts>
#include <cstddef>
#include <stdexcept>
#include <type_traits>
#include <utility>
@@ -59,13 +59,14 @@ export namespace mean_field::seed {
/* An explicit opt-in for a specification that leaves a radial seed unchanged. */
struct NoStateChange {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = true;
template <typename Model> static constexpr bool supports = true;
template <typename Specification, typename Model>
template <
typename Specification,
typename Model>
static void validate(
const Specification &,
const Model &,
@@ -74,7 +75,9 @@ export namespace mean_field::seed {
) noexcept {
}
template <typename Specification, typename Model>
template <
typename Specification,
typename Model>
static void initialize(
const Specification &,
const Model &,
@@ -115,19 +118,17 @@ export namespace mean_field::seed {
}
struct UnavailableRadialProjectionPhysics final {
static constexpr bool registered = false;
static constexpr bool providesRadialMass = false;
static constexpr bool registered = false;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = false;
template <typename Model> static constexpr bool supports = false;
};
struct FixedTotalMassRadialProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = true;
static constexpr bool registered = true;
static constexpr bool providesRadialMass = true;
template <typename Model>
static constexpr bool supports = true;
template <typename Model> static constexpr bool supports = true;
[[nodiscard]] static dimensions::MassValue targetMass(const models::FixedTotalMass &specification) {
return specification.targetMass();
@@ -164,10 +165,7 @@ export namespace mean_field::seed {
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = requires(
const Model &model,
const surface::Isobaric &condition
) {
static constexpr bool supports = requires(const Model &model, const surface::Isobaric &condition) {
{
eos::evaluate<dimensions::quantity::SpecificEnthalpy>(
model.equationOfState(), condition.targetPressure()
@@ -213,11 +211,10 @@ export namespace mean_field::seed {
};
struct FixedCentralDensityRadialProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = true;
template <typename Model> static constexpr bool supports = true;
template <typename Model>
static void validate(
@@ -246,11 +243,10 @@ export namespace mean_field::seed {
};
struct FixedAngularMomentumRadialProjectionPhysics final {
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
static constexpr bool registered = true;
static constexpr bool providesRadialMass = false;
template <typename Model>
static constexpr bool supports = true;
template <typename Model> static constexpr bool supports = true;
template <typename Model>
static void validate(
@@ -316,12 +312,12 @@ export namespace mean_field::seed {
};
template <typename Candidate> struct UnwrapRadialProjectionPhysics {
using Type = UnavailableRadialProjectionPhysics;
using Type = UnavailableRadialProjectionPhysics;
static constexpr bool valid = false;
};
template <typename Physics> struct UnwrapRadialProjectionPhysics<projection::Use<Physics>> {
using Type = Physics;
using Type = Physics;
static constexpr bool valid = true;
};
@@ -358,7 +354,9 @@ export namespace mean_field::seed {
}
}
template <typename Specification, typename Model>
template <
typename Specification,
typename Model>
[[nodiscard]] consteval bool radialProjectionPhysicsIsComplete() {
using Physics = typename SelectRadialProjectionPhysics<Specification>::Type;
if constexpr (!radialProjectionPhysicsRegistered<Physics>()) {
@@ -370,12 +368,9 @@ export namespace mean_field::seed {
} else if constexpr (!static_cast<bool>(Physics::template supports<Model>)) {
return false;
} else if constexpr (!requires(
const Specification &specification,
const Model &model,
const RadialProfile &profile,
const StellarEquilibriumProjectionOptions &options,
const RadialProjectionScales &scales,
RadialProjectionState &state,
const Specification &specification, const Model &model,
const RadialProfile &profile, const StellarEquilibriumProjectionOptions &options,
const RadialProjectionScales &scales, RadialProjectionState &state,
mfem::Vector coordinate
) {
Physics::validate(specification, model, profile, options);
@@ -417,21 +412,24 @@ export namespace mean_field::seed {
static constexpr std::size_t radialMassProviderCount =
(std::size_t{0} + ... +
(radialProjectionPhysicsProvidesMass<
typename SelectRadialProjectionPhysics<Specifications>::Type>()
(radialProjectionPhysicsProvidesMass<typename SelectRadialProjectionPhysics<Specifications>::Type>()
? std::size_t{1}
: std::size_t{0}));
static constexpr bool complete = radialMassProviderCount == 1 &&
(radialProjectionPhysicsIsComplete<Specifications, ModelType>() && ...);
static constexpr bool complete =
radialMassProviderCount == 1 && (radialProjectionPhysicsIsComplete<Specifications, ModelType>() && ...);
[[nodiscard]] static dimensions::MassValue targetMass(const ModelType &model) requires complete {
[[nodiscard]] static dimensions::MassValue targetMass(const ModelType &model)
requires complete
{
dimensions::MassValue result{0.0};
([&] {
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
if constexpr (radialProjectionPhysicsProvidesMass<Physics>()) {
result = Physics::targetMass(model.template specification<Specifications>());
}
}(), ...);
(
[&] {
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
if constexpr (radialProjectionPhysicsProvidesMass<Physics>()) {
result = Physics::targetMass(model.template specification<Specifications>());
}
}(),
...);
return result;
}
@@ -439,11 +437,15 @@ export namespace mean_field::seed {
const ModelType &model,
const RadialProfile &profile,
const StellarEquilibriumProjectionOptions &options
) requires complete {
([&] {
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
Physics::validate(model.template specification<Specifications>(), model, profile, options);
}(), ...);
)
requires complete
{
(
[&] {
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
Physics::validate(model.template specification<Specifications>(), model, profile, options);
}(),
...);
}
template <typename StateView>
@@ -452,27 +454,32 @@ export namespace mean_field::seed {
const RadialProjectionScales &scales,
RadialProjectionState &state,
const StateView &stateView
) requires complete {
([&] {
using Contribution = models::SpecificationContribution<Specifications>;
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
if constexpr (Contribution::generatedValueArity == 0) {
Physics::initialize(
model.template specification<Specifications>(), model, scales, state, mfem::Vector{}
);
} else {
static_assert(
Contribution::generatedValueArity == 1,
"Radial projection currently requires each specification contribution to generate at "
"most one scalar coordinate."
);
using Term = RadialProjectionCoordinateTerm<Specifications, Contribution::generatedStateKind>;
Physics::initialize(
model.template specification<Specifications>(), model, scales, state,
stateView.block(Term{})
);
}
}(), ...);
)
requires complete
{
(
[&] {
using Contribution = models::SpecificationContribution<Specifications>;
using Physics = typename SelectRadialProjectionPhysics<Specifications>::Type;
if constexpr (Contribution::generatedValueArity == 0) {
Physics::initialize(
model.template specification<Specifications>(), model, scales, state, mfem::Vector{}
);
} else {
static_assert(
Contribution::generatedValueArity == 1,
"Radial projection currently requires each specification contribution to generate at "
"most one scalar coordinate."
);
using Term =
RadialProjectionCoordinateTerm<Specifications, Contribution::generatedStateKind>;
Physics::initialize(
model.template specification<Specifications>(), model, scales, state,
stateView.block(Term{})
);
}
}(),
...);
}
};
@@ -494,21 +501,25 @@ export namespace mean_field::seed {
concept RadialProfileProjectableModel =
model::StellarModelType<Candidate> && radialProjectionIsCompilable<std::remove_cvref_t<Candidate>>;
template <equilibrium::StellarEquilibriumModel Model, equilibrium::StellarDiscretizationType Discretization>
template <
equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization>
requires RadialProfileProjectableModel<Model>
[[nodiscard]] ProjectedEquilibriumState<Model> projectRadialProfile(
const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
equilibrium::StellarEquilibriumProblem<
Model,
Discretization> &problem,
const RadialProfile &profile,
const StellarEquilibriumProjectionOptions &options = {}
) {
using Projection = detail::CompileRadialProjection<
std::remove_cvref_t<Model>,
typename std::remove_cvref_t<Model>::SpecificationTypes>;
std::remove_cvref_t<Model>, typename std::remove_cvref_t<Model>::SpecificationTypes>;
const auto &stellarModel = problem.GetStellarModel();
Projection::validate(stellarModel, profile, options);
const dimensions::MassValue targetMass = Projection::targetMass(stellarModel);
const dimensions::MassValue targetMass = Projection::targetMass(stellarModel);
const detail::ProjectedRadialFields fields = detail::projectRadialFields(
problem.GetDiscretization().finiteElementModel(), profile, targetMass, options
equilibrium::detail::StellarEquilibriumProblemFactory::MutableFiniteElementModelForProjection(problem),
profile, targetMass, options
);
mfem::Vector values(problem.StateSize());
@@ -563,11 +574,15 @@ export namespace mean_field::seed {
equilibrium::StellarDiscretizationType Discretization,
typename Strategy>
requires RadialSeedStrategyFor<
Strategy,
typename equilibrium::StellarEquilibriumProblem<Model, Discretization>::ModelType> &&
Strategy,
typename equilibrium::StellarEquilibriumProblem<
Model,
Discretization>::ModelType> &&
RadialProfileProjectableModel<Model>
[[nodiscard]] ProjectedEquilibriumState<Model> makeProjectedEquilibriumState(
const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
equilibrium::StellarEquilibriumProblem<
Model,
Discretization> &problem,
const Strategy &strategy,
const StellarEquilibriumProjectionOptions &options = {}
) {

View File

@@ -0,0 +1,868 @@
module;
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:solver.linear_backend;
export import :preconditioning.backend;
export namespace mean_field::solver {
enum class LinearSolveStatus : std::uint8_t {
converged,
maximum_iterations,
breakdown,
non_finite,
backend_failure
};
struct LinearSolveControl final {
double relativeTolerance{1.0e-8};
double absoluteTolerance{0.0};
int maximumIterations{100};
void Validate() const {
if (!std::isfinite(relativeTolerance) || relativeTolerance < 0.0) {
throw std::invalid_argument("A linear solve requires a finite, non-negative relative tolerance.");
}
if (!std::isfinite(absoluteTolerance) || absoluteTolerance < 0.0) {
throw std::invalid_argument("A linear solve requires a finite, non-negative absolute tolerance.");
}
if (maximumIterations <= 0) {
throw std::invalid_argument("A linear solve requires at least one permitted iteration.");
}
}
[[nodiscard]] double ConvergenceThreshold(const double globalRightHandSideNorm) const {
Validate();
if (!std::isfinite(globalRightHandSideNorm) || globalRightHandSideNorm < 0.0) {
throw std::invalid_argument("A linear solve requires a finite, non-negative right-hand-side norm.");
}
const double relativeThreshold = relativeTolerance * globalRightHandSideNorm;
if (!std::isfinite(relativeThreshold)) {
throw std::invalid_argument("The linear relative convergence threshold must be finite.");
}
return absoluteTolerance > relativeThreshold ? absoluteTolerance : relativeThreshold;
}
};
/*
* Numerical termination is data, not an exception. Implementations throw
* for invalid controls, configuration, dimensions, or violated lifetime
* contracts. All reported norms are communicator-global Euclidean norms.
* Solve uses the incoming correction as its initial guess and overwrites
* it with the final correction, so initialResidualNorm is ||b - A x_0||.
* Convergence remains relative to the right-hand side rather than the
* quality of a particular initial guess:
*
* ||b - A x|| <= max(absoluteTolerance,
* relativeTolerance * ||b||).
*
* For a zero right-hand side, relativeTrueResidualNorm is zero exactly
* when the true residual is zero and positive infinity otherwise. The
* true-residual fields are distinct from the backend's recurrence so
* callers never have to infer one from the other. This is deliberately
* fixed-size: recording a history is an optional backend concern whose
* storage must be owned and reserved by the prepared runtime, not allocated
* while Solve is active.
*/
struct LinearSolveReport final {
LinearSolveStatus status{LinearSolveStatus::backend_failure};
LinearSolveControl control{};
int iterations{0};
int restarts{0};
double rightHandSideNorm{0.0};
double initialResidualNorm{0.0};
double reportedResidualNorm{0.0};
double trueResidualNorm{0.0};
double relativeTrueResidualNorm{0.0};
// Includes MFEM's initial-guess residual application and the
// post-solve application used to verify the true residual.
std::uint64_t operatorApplications{0};
std::uint64_t inversePreconditionerApplications{0};
double solveSeconds{0.0};
// These totals include only completed applications. Operator time also
// includes the post-solve true-residual verification application.
double operatorSeconds{0.0};
double inversePreconditionerSeconds{0.0};
[[nodiscard]] bool Converged() const noexcept {
return status == LinearSolveStatus::converged;
}
};
struct LinearBackendConfigurationTag { };
template <typename Candidate>
concept LinearBackendConfiguration =
std::derived_from<std::remove_cvref_t<Candidate>, LinearBackendConfigurationTag> &&
std::move_constructible<std::remove_cvref_t<Candidate>> && requires {
requires std::same_as<
std::remove_cv_t<decltype(std::remove_cvref_t<Candidate>::supportedPreconditionerContract)>,
preconditioning::ApplicationContract>;
typename std::integral_constant<
preconditioning::ApplicationContract, std::remove_cvref_t<Candidate>::supportedPreconditionerContract>;
requires(
std::remove_cvref_t<Candidate>::supportedPreconditionerContract ==
preconditioning::ApplicationContract::stationary_linear ||
std::remove_cvref_t<Candidate>::supportedPreconditionerContract ==
preconditioning::ApplicationContract::flexible
);
};
} // namespace mean_field::solver
namespace mean_field::solver::detail {
template <typename Candidate>
concept StaticPreconditionerContractDeclared = requires { &std::remove_cvref_t<Candidate>::applicationContract; };
template <typename Candidate>
concept ExactStaticPreconditionerContract = requires {
requires std::same_as<
std::remove_cv_t<decltype(std::remove_cvref_t<Candidate>::applicationContract)>,
preconditioning::ApplicationContract>;
typename std::integral_constant<
preconditioning::ApplicationContract, std::remove_cvref_t<Candidate>::applicationContract>;
requires(
std::remove_cvref_t<Candidate>::applicationContract ==
preconditioning::ApplicationContract::stationary_linear ||
std::remove_cvref_t<Candidate>::applicationContract == preconditioning::ApplicationContract::flexible
);
};
template <typename Candidate, typename = void> struct StaticPreconditionerContract {
static constexpr bool declared = StaticPreconditionerContractDeclared<Candidate>;
static constexpr bool registered = false;
static constexpr preconditioning::ApplicationContract value =
preconditioning::ApplicationContract::stationary_linear;
};
template <typename Candidate>
struct StaticPreconditionerContract<Candidate, std::enable_if_t<ExactStaticPreconditionerContract<Candidate>>> {
static constexpr bool declared = true;
static constexpr auto value = std::remove_cvref_t<Candidate>::applicationContract;
static constexpr bool registered = true;
};
template <typename Candidate>
concept BackendPreconditionerContractDeclared = requires { typename std::remove_cvref_t<Candidate>::BackendType; };
template <typename Backend>
concept ExactRegisteredBackendPreconditionerContract = requires {
requires preconditioning::backend::Registered<std::remove_cvref_t<Backend>>;
requires std::same_as<
std::remove_cv_t<
decltype(preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract)>,
preconditioning::ApplicationContract>;
typename std::integral_constant<
preconditioning::ApplicationContract,
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract>;
requires(
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract ==
preconditioning::ApplicationContract::stationary_linear ||
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract ==
preconditioning::ApplicationContract::flexible
);
};
template <typename Candidate>
concept ExactBackendPreconditionerContract =
BackendPreconditionerContractDeclared<Candidate> &&
ExactRegisteredBackendPreconditionerContract<typename std::remove_cvref_t<Candidate>::BackendType>;
template <typename Candidate, typename = void> struct BackendPreconditionerContract {
static constexpr bool declared = BackendPreconditionerContractDeclared<Candidate>;
static constexpr bool registered = false;
static constexpr preconditioning::ApplicationContract value =
preconditioning::ApplicationContract::stationary_linear;
};
template <typename Candidate>
struct BackendPreconditionerContract<Candidate, std::enable_if_t<ExactBackendPreconditionerContract<Candidate>>> {
private:
using Backend = typename std::remove_cvref_t<Candidate>::BackendType;
public:
static constexpr bool declared = true;
static constexpr bool registered = true;
static constexpr preconditioning::ApplicationContract value =
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract;
};
template <typename Candidate> struct DirectPreconditionerContractAudit final {
private:
using StaticContract = StaticPreconditionerContract<Candidate>;
using BackendContract = BackendPreconditionerContract<Candidate>;
public:
static constexpr bool declarationsValid = (!StaticContract::declared || StaticContract::registered) &&
(!BackendContract::declared || BackendContract::registered);
static constexpr bool sourcesAgree = !StaticContract::registered || !BackendContract::registered ||
StaticContract::value == BackendContract::value;
static constexpr bool registered =
declarationsValid && sourcesAgree && (StaticContract::registered || BackendContract::registered);
static constexpr preconditioning::ApplicationContract value = [] {
if constexpr (StaticContract::registered) {
return StaticContract::value;
} else if constexpr (BackendContract::registered) {
return BackendContract::value;
} else {
return preconditioning::ApplicationContract::stationary_linear;
}
}();
};
template <typename Candidate>
concept PhysicalInversePreconditionerContractDeclared =
requires(const std::remove_cvref_t<Candidate> &candidate) { candidate.GetPhysicalInverse(); };
template <typename Candidate>
using PhysicalInverseType =
std::remove_cvref_t<decltype(std::declval<const std::remove_cvref_t<Candidate> &>().GetPhysicalInverse())>;
template <typename Candidate>
concept ExactPhysicalInversePreconditionerContract =
PhysicalInversePreconditionerContractDeclared<Candidate> &&
DirectPreconditionerContractAudit<PhysicalInverseType<Candidate>>::registered;
template <typename Candidate, typename = void> struct PhysicalInversePreconditionerContract {
static constexpr bool declared = PhysicalInversePreconditionerContractDeclared<Candidate>;
static constexpr bool registered = false;
static constexpr preconditioning::ApplicationContract value =
preconditioning::ApplicationContract::stationary_linear;
};
template <typename Candidate>
struct PhysicalInversePreconditionerContract<
Candidate,
std::enable_if_t<ExactPhysicalInversePreconditionerContract<Candidate>>> {
using PhysicalInverse = PhysicalInverseType<Candidate>;
using ContractAudit = DirectPreconditionerContractAudit<PhysicalInverse>;
static constexpr bool declared = true;
static constexpr bool registered = true;
static constexpr preconditioning::ApplicationContract value = ContractAudit::value;
};
template <typename Candidate> struct LinearPreconditionerContractAudit final {
private:
using StaticContract = StaticPreconditionerContract<Candidate>;
using BackendContract = BackendPreconditionerContract<Candidate>;
using PhysicalContract = PhysicalInversePreconditionerContract<Candidate>;
public:
static constexpr bool declarationsValid = (!StaticContract::declared || StaticContract::registered) &&
(!BackendContract::declared || BackendContract::registered) &&
(!PhysicalContract::declared || PhysicalContract::registered);
static constexpr bool sourcesAgree = (!StaticContract::registered || !BackendContract::registered ||
StaticContract::value == BackendContract::value) &&
(!StaticContract::registered || !PhysicalContract::registered ||
StaticContract::value == PhysicalContract::value) &&
(!BackendContract::registered || !PhysicalContract::registered ||
BackendContract::value == PhysicalContract::value);
static constexpr bool registered =
declarationsValid && sourcesAgree &&
(StaticContract::registered || BackendContract::registered || PhysicalContract::registered);
static constexpr preconditioning::ApplicationContract value = [] {
if constexpr (StaticContract::registered) {
return StaticContract::value;
} else if constexpr (BackendContract::registered) {
return BackendContract::value;
} else if constexpr (PhysicalContract::registered) {
return PhysicalContract::value;
} else {
return preconditioning::ApplicationContract::stationary_linear;
}
}();
};
} // namespace mean_field::solver::detail
export namespace mean_field::solver {
template <typename Candidate>
concept LinearPreconditionerApplicationContractAvailable =
detail::LinearPreconditionerContractAudit<std::remove_cvref_t<Candidate>>::registered;
template <LinearPreconditionerApplicationContractAvailable Candidate>
inline constexpr preconditioning::ApplicationContract linearPreconditionerApplicationContract =
detail::LinearPreconditionerContractAudit<std::remove_cvref_t<Candidate>>::value;
template <typename Configuration, typename Preconditioner>
concept LinearBackendPreconditionerCompatible =
LinearBackendConfiguration<Configuration> && LinearPreconditionerApplicationContractAvailable<Preconditioner> &&
(std::remove_cvref_t<Configuration>::supportedPreconditionerContract ==
preconditioning::ApplicationContract::flexible ||
linearPreconditionerApplicationContract<std::remove_cvref_t<Preconditioner>> ==
preconditioning::ApplicationContract::stationary_linear);
/*
* A prepared backend is bound once to the exact operator and inverse that
* its owner keeps at stable addresses and identifies the communicator on
* which it operates. The communicator supplied to preparation is borrowed;
* a backend may retain it or own a congruent duplicate. The handle returned
* by GetCommunicator is borrowed from the backend and must not be freed by
* the caller. Every prepared backend must be destroyed before MPI_Finalize.
* It owns its numerical workspaces; neither copyability nor movability is
* required. Solve treats a caller-provided, correctly sized correction
* vector as its initial guess and overwrites it with the final correction.
*/
template <typename Candidate, typename Operator, typename Preconditioner>
concept PreparedLinearBackendFor = std::derived_from<std::remove_cvref_t<Operator>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver> &&
std::destructible<std::remove_cvref_t<Candidate>> &&
requires(
std::remove_cvref_t<Candidate> &prepared,
const std::remove_cvref_t<Candidate> &constantPrepared,
const mfem::Vector &rightHandSide,
mfem::Vector &correction,
const LinearSolveControl &control
) {
{
constantPrepared.GetOperator()
} -> std::same_as<const std::remove_cvref_t<Operator> &>;
{
constantPrepared.GetPreconditioner()
} -> std::same_as<const std::remove_cvref_t<Preconditioner> &>;
{ constantPrepared.GetCommunicator() } -> std::same_as<MPI_Comm>;
{ constantPrepared.IsReady() } -> std::same_as<bool>;
{ constantPrepared.RightHandSideSize() } -> std::same_as<int>;
{ constantPrepared.CorrectionSize() } -> std::same_as<int>;
{
prepared.Solve(rightHandSide, correction, control)
} -> std::same_as<LinearSolveReport>;
};
/*
* `prepareLinearBackend` is intentionally unqualified in this detection
* boundary. A third-party configuration supplies its overload beside the
* configuration type and ADL discovers it without a library registry.
*/
template <typename Configuration, typename Operator, typename Preconditioner>
concept LinearBackendRuntimeAvailableFor =
LinearBackendPreconditionerCompatible<Configuration, Preconditioner> &&
std::derived_from<std::remove_cvref_t<Operator>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver> &&
requires(
std::remove_cvref_t<Configuration> configuration,
const std::remove_cvref_t<Operator> &operation,
std::remove_cvref_t<Preconditioner> &preconditioner,
MPI_Comm communicator
) {
requires std::same_as<
decltype(prepareLinearBackend(std::move(configuration), operation, preconditioner, communicator)),
std::remove_cvref_t<
decltype(prepareLinearBackend(std::move(configuration), operation, preconditioner, communicator))>>;
{
prepareLinearBackend(std::move(configuration), operation, preconditioner, communicator)
} -> PreparedLinearBackendFor<std::remove_cvref_t<Operator>, std::remove_cvref_t<Preconditioner>>;
};
template <LinearBackendConfiguration Configuration, typename Operator, typename Preconditioner>
requires LinearBackendRuntimeAvailableFor<Configuration, Operator, Preconditioner>
using PreparedLinearBackendType = std::remove_cvref_t<decltype(prepareLinearBackend(
std::declval<std::remove_cvref_t<Configuration> &&>(),
std::declval<const std::remove_cvref_t<Operator> &>(),
std::declval<std::remove_cvref_t<Preconditioner> &>(),
std::declval<MPI_Comm>()
))>;
} // namespace mean_field::solver
export namespace mean_field::solver::linear {
struct FGMRESOptions final {
int restartLength{50};
int printLevel{1};
void Validate() const {
if (restartLength <= 0) {
throw std::invalid_argument("MFEM FGMRES requires a positive restart length.");
}
if (printLevel < -1 || printLevel > 3) {
throw std::invalid_argument("MFEM FGMRES print level must be between -1 and 3.");
}
}
};
class FGMRES final : public LinearBackendConfigurationTag {
public:
static constexpr preconditioning::ApplicationContract supportedPreconditionerContract =
preconditioning::ApplicationContract::flexible;
FGMRES() = default;
explicit FGMRES(FGMRESOptions options) : m_options(std::move(options)) {
m_options.Validate();
}
[[nodiscard]] const FGMRESOptions &GetOptions() const noexcept {
return m_options;
}
private:
FGMRESOptions m_options{};
};
namespace detail {
template <typename Operation>
requires std::derived_from<std::remove_cvref_t<Operation>, mfem::Operator>
class CountedOperator final : public mfem::Operator {
public:
explicit CountedOperator(const Operation &operation)
: mfem::Operator(
operation.Height(),
operation.Width()
),
m_operation(std::addressof(operation)) {
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
const auto start = std::chrono::steady_clock::now();
m_operation->Mult(input, output);
m_seconds += std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
++m_applications;
}
void Reset() const noexcept {
m_applications = 0;
m_seconds = 0.0;
}
[[nodiscard]] std::uint64_t Applications() const noexcept {
return m_applications;
}
[[nodiscard]] double Seconds() const noexcept {
return m_seconds;
}
private:
const Operation *m_operation;
mutable std::uint64_t m_applications{0};
mutable double m_seconds{0.0};
};
template <typename Operation, typename Preconditioner>
requires std::derived_from<std::remove_cvref_t<Operation>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver>
class CountedPreconditioner final : public mfem::Solver {
public:
CountedPreconditioner(
const Operation &operation,
Preconditioner &preconditioner,
const CountedOperator<Operation> &countedOperation
)
: mfem::Solver(
preconditioner.Height(),
preconditioner.Width(),
false
),
m_operation(std::addressof(operation)),
m_preconditioner(std::addressof(preconditioner)),
m_countedOperation(std::addressof(countedOperation)) {
m_preconditioner->iterative_mode = false;
}
void SetOperator(const mfem::Operator &operation) override {
if (std::addressof(operation) != m_countedOperation) {
throw std::invalid_argument("The MFEM FGMRES preconditioner received an unexpected operator.");
}
m_preconditioner->SetOperator(*m_operation);
if (m_preconditioner->Height() != Height() || m_preconditioner->Width() != Width()) {
throw std::invalid_argument(
"The MFEM FGMRES preconditioner changed dimensions while binding its operator."
);
}
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
const auto start = std::chrono::steady_clock::now();
m_preconditioner->Mult(input, output);
m_seconds += std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
++m_applications;
}
void Reset() const noexcept {
m_applications = 0;
m_seconds = 0.0;
}
[[nodiscard]] std::uint64_t Applications() const noexcept {
return m_applications;
}
[[nodiscard]] double Seconds() const noexcept {
return m_seconds;
}
private:
const Operation *m_operation;
Preconditioner *m_preconditioner;
const CountedOperator<Operation> *m_countedOperation;
mutable std::uint64_t m_applications{0};
mutable double m_seconds{0.0};
};
[[nodiscard]] inline bool MpiIsUsable() noexcept {
int initialized = 0;
int finalized = 0;
return MPI_Initialized(&initialized) == MPI_SUCCESS && initialized != 0 &&
MPI_Finalized(&finalized) == MPI_SUCCESS && finalized == 0;
}
[[nodiscard]] inline bool AllRanksAgree(
const bool localValue,
const MPI_Comm communicator
) {
int local = localValue ? 1 : 0;
int global = 0;
if (MPI_Allreduce(&local, &global, 1, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not perform a communicator-wide validity check.");
}
return global != 0;
}
inline void RequireCollectivelyIdenticalConfiguration(
const FGMRESOptions &options,
const LinearSolveControl &control,
const MPI_Comm communicator
) {
const std::array<double, 2> localRealValues{control.relativeTolerance, control.absoluteTolerance};
std::array<double, 2> minimumRealValues{};
std::array<double, 2> maximumRealValues{};
const std::array<int, 3> localIntegerValues{
control.maximumIterations, options.restartLength, options.printLevel
};
std::array<int, 3> minimumIntegerValues{};
std::array<int, 3> maximumIntegerValues{};
if (MPI_Allreduce(
localRealValues.data(), minimumRealValues.data(), static_cast<int>(localRealValues.size()),
MPI_DOUBLE, MPI_MIN, communicator
) != MPI_SUCCESS ||
MPI_Allreduce(
localRealValues.data(), maximumRealValues.data(), static_cast<int>(localRealValues.size()),
MPI_DOUBLE, MPI_MAX, communicator
) != MPI_SUCCESS ||
MPI_Allreduce(
localIntegerValues.data(), minimumIntegerValues.data(), static_cast<int>(localIntegerValues.size()),
MPI_INT, MPI_MIN, communicator
) != MPI_SUCCESS ||
MPI_Allreduce(
localIntegerValues.data(), maximumIntegerValues.data(), static_cast<int>(localIntegerValues.size()),
MPI_INT, MPI_MAX, communicator
) != MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not validate its distributed configuration.");
}
if (minimumRealValues != maximumRealValues || minimumIntegerValues != maximumIntegerValues) {
throw std::invalid_argument(
"MFEM FGMRES requires identical options and solve controls on every communicator rank."
);
}
}
[[nodiscard]] inline bool LocallyFinite(const mfem::Vector &values) {
for (int index = 0; index < values.Size(); ++index) {
if (!std::isfinite(values(index))) {
return false;
}
}
return true;
}
[[nodiscard]] inline double GlobalNorm(
const mfem::Vector &values,
const MPI_Comm communicator
) {
const double localNorm = values.Norml2();
const double localNormSquared = localNorm * localNorm;
double globalNormSquared = 0.0;
if (MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, communicator) !=
MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not reduce a global vector norm.");
}
if (!std::isfinite(globalNormSquared) || globalNormSquared < 0.0) {
return std::numeric_limits<double>::quiet_NaN();
}
return std::sqrt(globalNormSquared);
}
[[nodiscard]] inline double MaximumRankValue(
const double localValue,
const MPI_Comm communicator
) {
double maximumValue = 0.0;
if (MPI_Allreduce(&localValue, &maximumValue, 1, MPI_DOUBLE, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not reduce a communicator-wide timing measurement.");
}
return maximumValue;
}
template <typename Candidate> [[nodiscard]] bool RuntimeDependencyIsCurrent(const Candidate &candidate) {
if constexpr (requires {
{ candidate.IsCurrent() } -> std::same_as<bool>;
}) {
return candidate.IsCurrent();
} else if constexpr (requires {
{ candidate.IsPrepared() } -> std::same_as<bool>;
}) {
return candidate.IsPrepared();
} else {
return true;
}
}
template <typename Operation, typename Preconditioner>
requires std::derived_from<std::remove_cvref_t<Operation>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver>
class PreparedFGMRES final {
private:
using Clock = std::chrono::steady_clock;
public:
PreparedFGMRES(
FGMRESOptions options,
const Operation &operation,
Preconditioner &preconditioner,
const MPI_Comm communicator
)
: m_options(std::move(options)),
m_operation(std::addressof(operation)),
m_preconditioner(std::addressof(preconditioner)),
m_communicator(communicator),
m_countedOperation(operation),
m_countedPreconditioner(
operation,
preconditioner,
m_countedOperation
),
m_solver(communicator),
m_rightHandSide(operation.Height()),
m_operationAction(operation.Height()),
m_trueResidual(operation.Height()) {
m_options.Validate();
if (!MpiIsUsable()) {
throw std::logic_error("MFEM FGMRES requires initialized MPI that has not been finalized.");
}
if (m_communicator == MPI_COMM_NULL) {
throw std::invalid_argument("MFEM FGMRES requires a non-null MPI communicator.");
}
if (operation.Height() <= 0 || operation.Width() <= 0 || operation.Height() != operation.Width() ||
preconditioner.Height() != operation.Width() || preconditioner.Width() != operation.Height()) {
throw std::invalid_argument(
"MFEM FGMRES requires compatible square operator and preconditioner dimensions."
);
}
m_rightHandSide = 0.0;
m_operationAction = 0.0;
m_trueResidual = 0.0;
m_solver.SetPreconditioner(m_countedPreconditioner);
m_solver.SetOperator(m_countedOperation);
m_solver.SetKDim(m_options.restartLength);
m_solver.SetPrintLevel(m_options.printLevel);
m_solver.iterative_mode = true;
}
PreparedFGMRES(const PreparedFGMRES &) = delete;
PreparedFGMRES &operator=(const PreparedFGMRES &) = delete;
PreparedFGMRES(PreparedFGMRES &&) = delete;
PreparedFGMRES &operator=(PreparedFGMRES &&) = delete;
[[nodiscard]] const Operation &GetOperator() const noexcept {
return *m_operation;
}
[[nodiscard]] const Preconditioner &GetPreconditioner() const noexcept {
return *m_preconditioner;
}
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept {
return m_communicator;
}
[[nodiscard]] bool IsReady() const {
return m_operation != nullptr && m_preconditioner != nullptr && m_communicator != MPI_COMM_NULL &&
m_operation->Height() == m_operation->Width() &&
m_preconditioner->Height() == m_operation->Width() &&
m_preconditioner->Width() == m_operation->Height() &&
m_rightHandSide.Size() == m_operation->Height() &&
m_operationAction.Size() == m_operation->Height() &&
m_trueResidual.Size() == m_operation->Height() && RuntimeDependencyIsCurrent(*m_operation) &&
RuntimeDependencyIsCurrent(*m_preconditioner);
}
[[nodiscard]] int RightHandSideSize() const noexcept {
return m_operation->Height();
}
[[nodiscard]] int CorrectionSize() const noexcept {
return m_operation->Width();
}
[[nodiscard]] LinearSolveReport Solve(
const mfem::Vector &rightHandSide,
mfem::Vector &correction,
const LinearSolveControl &control
) {
bool localConfigurationIsValid = true;
try {
m_options.Validate();
control.Validate();
} catch (const std::invalid_argument &) {
localConfigurationIsValid = false;
}
if (!AllRanksAgree(localConfigurationIsValid, m_communicator)) {
throw std::invalid_argument(
"MFEM FGMRES requires valid options and solve controls on every communicator rank."
);
}
if (!localConfigurationIsValid) {
throw std::invalid_argument("MFEM FGMRES received invalid options or solve controls.");
}
RequireCollectivelyIdenticalConfiguration(m_options, control, m_communicator);
if (!AllRanksAgree(IsReady(), m_communicator)) {
throw std::logic_error(
"MFEM FGMRES requires a complete, current prepared runtime on every communicator rank."
);
}
if (!AllRanksAgree(
rightHandSide.Size() == RightHandSideSize() && correction.Size() == CorrectionSize(),
m_communicator
)) {
throw std::invalid_argument(
"MFEM FGMRES received incompatible linear-system vectors on at least one rank."
);
}
if (!AllRanksAgree(LocallyFinite(rightHandSide), m_communicator) ||
!AllRanksAgree(LocallyFinite(correction), m_communicator)) {
throw std::invalid_argument(
"MFEM FGMRES requires finite right-hand side and initial-guess values."
);
}
m_rightHandSide = rightHandSide;
const double rightHandSideNorm = GlobalNorm(m_rightHandSide, m_communicator);
const double threshold = control.ConvergenceThreshold(rightHandSideNorm);
m_countedOperation.Reset();
m_countedPreconditioner.Reset();
m_solver.SetRelTol(0.0);
m_solver.SetAbsTol(threshold);
m_solver.SetMaxIter(control.maximumIterations);
const Clock::time_point start = Clock::now();
m_solver.Mult(m_rightHandSide, correction);
const double solveSeconds =
MaximumRankValue(std::chrono::duration<double>(Clock::now() - start).count(), m_communicator);
const bool correctionIsFinite = AllRanksAgree(LocallyFinite(correction), m_communicator);
double trueResidualNorm = std::numeric_limits<double>::quiet_NaN();
if (correctionIsFinite) {
m_countedOperation.Mult(correction, m_operationAction);
m_trueResidual = m_rightHandSide;
m_trueResidual -= m_operationAction;
if (AllRanksAgree(LocallyFinite(m_trueResidual), m_communicator)) {
trueResidualNorm = GlobalNorm(m_trueResidual, m_communicator);
}
}
const double initialResidualNorm = m_solver.GetInitialNorm();
const double reportedResidualNorm = m_solver.GetFinalNorm();
const std::uint64_t krylovIterationCount = m_countedPreconditioner.Applications();
if (krylovIterationCount > static_cast<std::uint64_t>(std::numeric_limits<int>::max())) {
throw std::overflow_error("MFEM FGMRES reported more Krylov iterations than can be represented.");
}
const int iterations = static_cast<int>(krylovIterationCount);
// A restart is an additional Krylov cycle entered after the
// initial cycle, not the residual check at a cycle boundary.
const int restarts = iterations > 0 ? (iterations - 1) / m_options.restartLength : 0;
const bool numericalValuesAreFinite = correctionIsFinite && std::isfinite(initialResidualNorm) &&
std::isfinite(reportedResidualNorm) &&
std::isfinite(trueResidualNorm);
LinearSolveStatus status = LinearSolveStatus::backend_failure;
if (!numericalValuesAreFinite) {
status = LinearSolveStatus::non_finite;
} else if (trueResidualNorm <= threshold) {
status = LinearSolveStatus::converged;
} else if (!m_solver.GetConverged() && iterations >= control.maximumIterations) {
status = LinearSolveStatus::maximum_iterations;
}
const double relativeTrueResidualNorm =
rightHandSideNorm > 0.0 ? trueResidualNorm / rightHandSideNorm
: (trueResidualNorm == 0.0 ? 0.0 : std::numeric_limits<double>::infinity());
const double operatorSeconds = MaximumRankValue(m_countedOperation.Seconds(), m_communicator);
const double inversePreconditionerSeconds =
MaximumRankValue(m_countedPreconditioner.Seconds(), m_communicator);
return {
.status = status,
.control = control,
.iterations = iterations,
.restarts = restarts,
.rightHandSideNorm = rightHandSideNorm,
.initialResidualNorm = initialResidualNorm,
.reportedResidualNorm = reportedResidualNorm,
.trueResidualNorm = trueResidualNorm,
.relativeTrueResidualNorm = relativeTrueResidualNorm,
.operatorApplications = m_countedOperation.Applications(),
.inversePreconditionerApplications = m_countedPreconditioner.Applications(),
.solveSeconds = solveSeconds,
.operatorSeconds = operatorSeconds,
.inversePreconditionerSeconds = inversePreconditionerSeconds
};
}
private:
FGMRESOptions m_options;
const Operation *m_operation;
Preconditioner *m_preconditioner;
MPI_Comm m_communicator;
CountedOperator<Operation> m_countedOperation;
CountedPreconditioner<Operation, Preconditioner> m_countedPreconditioner;
mfem::FGMRESSolver m_solver;
mfem::Vector m_rightHandSide;
mfem::Vector m_operationAction;
mfem::Vector m_trueResidual;
};
} // namespace detail
template <
typename Operation,
typename Preconditioner>
requires std::derived_from<
std::remove_cvref_t<Operation>,
mfem::Operator> &&
std::derived_from<
std::remove_cvref_t<Preconditioner>,
mfem::Solver>
[[nodiscard]] auto prepareLinearBackend(
FGMRES configuration,
const Operation &operation,
Preconditioner &preconditioner,
const MPI_Comm communicator
) {
return detail::PreparedFGMRES<Operation, Preconditioner>{
configuration.GetOptions(), operation, preconditioner, communicator
};
}
} // namespace mean_field::solver::linear

View File

@@ -0,0 +1,582 @@
module;
#include <cmath>
#include <concepts>
#include <cstdint>
#include <exception>
#include <functional>
#include <optional>
#include <span>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:solver.newton;
export import :deformation.safe_newton_step;
export import :solver.linear_backend;
export namespace mean_field::solver::nonlinear {
/*
* Backtracking counts the full Newton trial as its first trial. A
* contraction is applied only after that candidate has been rejected.
*/
struct BacktrackingOptions final {
double initialStepLength{1.0};
double contractionFactor{0.5};
double fractionToBoundarySafety{0.9};
double sufficientDecrease{1.0e-4};
double minimumStepLength{1.0e-8};
int maximumTrials{20};
void Validate() const {
if (!std::isfinite(initialStepLength) || initialStepLength <= 0.0) {
throw std::invalid_argument("Newton backtracking requires a finite, positive initial step length.");
}
if (!std::isfinite(contractionFactor) || contractionFactor <= 0.0 || contractionFactor >= 1.0) {
throw std::invalid_argument(
"Newton backtracking requires a finite contraction factor strictly between zero and one."
);
}
if (!std::isfinite(fractionToBoundarySafety) || fractionToBoundarySafety <= 0.0 ||
fractionToBoundarySafety >= 1.0) {
throw std::invalid_argument(
"Newton backtracking requires a finite fraction-to-boundary safety factor strictly between zero "
"and one."
);
}
if (!std::isfinite(sufficientDecrease) || sufficientDecrease <= 0.0 || sufficientDecrease >= 1.0) {
throw std::invalid_argument(
"Newton backtracking requires a finite sufficient-decrease factor strictly between zero and one."
);
}
if (!std::isfinite(minimumStepLength) || minimumStepLength <= 0.0 ||
minimumStepLength > initialStepLength) {
throw std::invalid_argument(
"Newton backtracking requires a finite, positive minimum step no larger than the initial step."
);
}
if (maximumTrials <= 0) {
throw std::invalid_argument("Newton backtracking requires at least one permitted trial.");
}
}
};
struct NewtonOptions final {
double relativeTolerance{1.0e-8};
double absoluteTolerance{0.0};
int maximumIterations{50};
LinearSolveControl linearSolve{};
BacktrackingOptions backtracking{};
void Validate() const {
if (!std::isfinite(relativeTolerance) || relativeTolerance < 0.0) {
throw std::invalid_argument("A Newton solve requires a finite, non-negative relative tolerance.");
}
if (!std::isfinite(absoluteTolerance) || absoluteTolerance < 0.0) {
throw std::invalid_argument("A Newton solve requires a finite, non-negative absolute tolerance.");
}
if (maximumIterations <= 0) {
throw std::invalid_argument("A Newton solve requires at least one permitted iteration.");
}
linearSolve.Validate();
backtracking.Validate();
}
[[nodiscard]] double ConvergenceThreshold(const double initialResidualNorm) const {
Validate();
if (!std::isfinite(initialResidualNorm) || initialResidualNorm < 0.0) {
throw std::invalid_argument(
"A Newton convergence threshold requires a finite, non-negative initial residual norm."
);
}
const double relativeThreshold = relativeTolerance * initialResidualNorm;
if (!std::isfinite(relativeThreshold)) {
throw std::invalid_argument("The Newton relative convergence threshold must be finite.");
}
return absoluteTolerance > relativeThreshold ? absoluteTolerance : relativeThreshold;
}
};
/*
* The MVP globalization merit is phi(x) = 0.5 ||F_normalized(x)||^2.
* A metric customization receives the solve communicator and must return
* communicator-consistent values or throw collectively. The Newton engine
* reduces every predicate that drives control flow, but it cannot make a
* rank-local exception inside an arbitrary callback collective-safe.
*/
struct NormalizedResidualMetric final { };
struct MetricEvaluation final {
double residualNorm{0.0};
double merit{0.0};
};
[[nodiscard]] inline MetricEvaluation getMetric(
const NormalizedResidualMetric &,
const mfem::Vector &normalizedResidual,
const MPI_Comm communicator
) {
if (communicator == MPI_COMM_NULL) {
throw std::invalid_argument("A nonlinear metric requires a valid communicator.");
}
double localSquaredNorm = 0.0;
for (int index = 0; index < normalizedResidual.Size(); ++index) {
const double value = static_cast<double>(normalizedResidual(index));
localSquaredNorm += value * value;
}
double globalSquaredNorm = 0.0;
if (MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, communicator) != MPI_SUCCESS) {
throw std::runtime_error("The nonlinear metric could not reduce the normalized residual norm.");
}
const double residualNorm = std::sqrt(globalSquaredNorm);
return {.residualNorm = residualNorm, .merit = 0.5 * residualNorm * residualNorm};
}
template <typename Metric = NormalizedResidualMetric>
requires std::move_constructible<std::remove_cvref_t<Metric>>
class Newton final {
public:
using MetricType = std::remove_cvref_t<Metric>;
Newton()
requires std::default_initializable<MetricType>
: Newton(
NewtonOptions{},
MetricType{}
) {
}
explicit Newton(NewtonOptions options)
requires std::default_initializable<MetricType>
: Newton(
std::move(options),
MetricType{}
) {
}
Newton(
NewtonOptions options,
MetricType metric
)
: m_options(std::move(options)),
m_metric(std::move(metric)) {
m_options.Validate();
}
[[nodiscard]] const NewtonOptions &options() const noexcept {
return m_options;
}
[[nodiscard]] const MetricType &metric() const noexcept {
return m_metric;
}
private:
NewtonOptions m_options;
[[no_unique_address]] MetricType m_metric;
};
Newton() -> Newton<NormalizedResidualMetric>;
Newton(NewtonOptions) -> Newton<NormalizedResidualMetric>;
template <typename Metric>
Newton(
NewtonOptions,
Metric
) -> Newton<std::remove_cvref_t<Metric>>;
template <typename Candidate> struct IsNewtonConfiguration : std::false_type { };
template <typename Metric> struct IsNewtonConfiguration<Newton<Metric>> : std::true_type { };
template <typename Candidate>
concept NewtonConfiguration = IsNewtonConfiguration<std::remove_cvref_t<Candidate>>::value;
enum class IterationDisposition : std::uint8_t {
unspecified,
accepted,
converged,
inadmissible_state,
non_finite_state,
non_finite_residual,
linear_solve_failure,
globalization_failure,
stagnation,
iteration_limit
};
/*
* Event spans borrow solver workspaces and are valid only for the duration
* of the callback. Copy values that must outlive the callback.
*/
struct BeforeIteration final {
int iteration{0};
double initialResidualNorm{0.0};
double residualNorm{0.0};
double relativeResidualNorm{0.0};
double merit{0.0};
MPI_Comm communicator{MPI_COMM_NULL};
std::span<const mfem::real_t> physicalState{};
std::span<const mfem::real_t> normalizedState{};
std::span<const mfem::real_t> normalizedResidual{};
};
enum class LineSearchTrialDisposition : std::uint8_t {
accepted,
inadmissible_state,
non_finite_state,
non_finite_residual,
insufficient_decrease
};
struct AfterLineSearchTrial final {
int iteration{0};
int trial{0};
double stepLength{0.0};
LineSearchTrialDisposition disposition{LineSearchTrialDisposition::insufficient_decrease};
std::string_view rejectionSource{};
std::optional<MetricEvaluation> metric{};
std::optional<double> minimumJacobianDeterminant{};
double preparationSeconds{0.0};
double metricSeconds{0.0};
MPI_Comm communicator{MPI_COMM_NULL};
std::span<const mfem::real_t> candidatePhysicalState{};
std::span<const mfem::real_t> candidateNormalizedState{};
std::span<const mfem::real_t> candidateNormalizedResidual{};
};
struct AfterIteration final {
int iteration{0};
IterationDisposition disposition{IterationDisposition::unspecified};
bool stepAccepted{false};
double acceptedStepLength{0.0};
int lineSearchTrials{0};
double initialResidualNorm{0.0};
double previousResidualNorm{0.0};
double residualNorm{0.0};
double relativeResidualNorm{0.0};
double merit{0.0};
double iterationSeconds{0.0};
double geometryPreflightSeconds{0.0};
double lineSearchSeconds{0.0};
double trialPreparationSeconds{0.0};
double metricEvaluationSeconds{0.0};
double preconditionerRefreshSeconds{0.0};
double rollbackSeconds{0.0};
std::optional<deformation::LargestSafeNewtonStepSizeEstimate> geometryPreflight{};
std::optional<LinearSolveReport> linearSolve{};
MPI_Comm communicator{MPI_COMM_NULL};
std::span<const mfem::real_t> physicalState{};
std::span<const mfem::real_t> normalizedState{};
std::span<const mfem::real_t> normalizedResidual{};
};
struct NoObserver final { };
template <typename Candidate>
concept BeforeIterationCallback = std::invocable<Candidate &, const BeforeIteration &> &&
std::same_as<std::invoke_result_t<Candidate &, const BeforeIteration &>, void>;
template <typename Candidate>
concept AfterIterationCallback = std::invocable<Candidate &, const AfterIteration &> &&
std::same_as<std::invoke_result_t<Candidate &, const AfterIteration &>, void>;
template <typename Candidate>
concept LineSearchTrialCallback =
std::invocable<Candidate &, const AfterLineSearchTrial &> &&
std::same_as<std::invoke_result_t<Candidate &, const AfterLineSearchTrial &>, void>;
template <BeforeIterationCallback BeforeCallback, AfterIterationCallback AfterCallback>
class CallbackObserver final {
public:
CallbackObserver(
BeforeCallback before,
AfterCallback after
)
: m_before(std::move(before)),
m_after(std::move(after)) {
}
void beforeIteration(const BeforeIteration &event) noexcept(std::is_nothrow_invocable_v<
BeforeCallback &,
const BeforeIteration &>) {
std::invoke(m_before, event);
}
void afterIteration(const AfterIteration &event) noexcept(std::is_nothrow_invocable_v<
AfterCallback &,
const AfterIteration &>) {
std::invoke(m_after, event);
}
private:
[[no_unique_address]] BeforeCallback m_before;
[[no_unique_address]] AfterCallback m_after;
};
template <
typename BeforeCallback,
typename AfterCallback>
requires BeforeIterationCallback<std::decay_t<BeforeCallback>> &&
AfterIterationCallback<std::decay_t<AfterCallback>> &&
std::constructible_from<
std::decay_t<BeforeCallback>,
BeforeCallback> &&
std::constructible_from<
std::decay_t<AfterCallback>,
AfterCallback>
[[nodiscard]] auto makeObserver(
BeforeCallback &&before,
AfterCallback &&after
) {
return CallbackObserver<std::decay_t<BeforeCallback>, std::decay_t<AfterCallback>>{
std::forward<BeforeCallback>(before), std::forward<AfterCallback>(after)
};
}
template <
BeforeIterationCallback BeforeCallback,
LineSearchTrialCallback TrialCallback,
AfterIterationCallback AfterCallback>
class DetailedCallbackObserver final {
public:
DetailedCallbackObserver(
BeforeCallback before,
TrialCallback trial,
AfterCallback after
)
: m_before(std::move(before)),
m_trial(std::move(trial)),
m_after(std::move(after)) {
}
void beforeIteration(const BeforeIteration &event) noexcept(std::is_nothrow_invocable_v<
BeforeCallback &,
const BeforeIteration &>) {
std::invoke(m_before, event);
}
void afterLineSearchTrial(const AfterLineSearchTrial &event) noexcept(std::is_nothrow_invocable_v<
TrialCallback &,
const AfterLineSearchTrial &>) {
std::invoke(m_trial, event);
}
void afterIteration(const AfterIteration &event) noexcept(std::is_nothrow_invocable_v<
AfterCallback &,
const AfterIteration &>) {
std::invoke(m_after, event);
}
private:
[[no_unique_address]] BeforeCallback m_before;
[[no_unique_address]] TrialCallback m_trial;
[[no_unique_address]] AfterCallback m_after;
};
template <
typename BeforeCallback,
typename TrialCallback,
typename AfterCallback>
requires BeforeIterationCallback<std::decay_t<BeforeCallback>> &&
LineSearchTrialCallback<std::decay_t<TrialCallback>> &&
AfterIterationCallback<std::decay_t<AfterCallback>> &&
std::constructible_from<
std::decay_t<BeforeCallback>,
BeforeCallback> &&
std::constructible_from<
std::decay_t<TrialCallback>,
TrialCallback> &&
std::constructible_from<
std::decay_t<AfterCallback>,
AfterCallback>
[[nodiscard]] auto makeObserver(
BeforeCallback &&before,
TrialCallback &&trial,
AfterCallback &&after
) {
return DetailedCallbackObserver<
std::decay_t<BeforeCallback>, std::decay_t<TrialCallback>, std::decay_t<AfterCallback>>{
std::forward<BeforeCallback>(before), std::forward<TrialCallback>(trial), std::forward<AfterCallback>(after)
};
}
namespace detail {
[[nodiscard]] inline double NextBacktrackingStepLength(
const double rejectedStepLength,
const double acceptedMinimumJacobianDeterminant,
const std::optional<double> rejectedMinimumJacobianDeterminant,
const bool rejectedByInvertedGeometry,
const BacktrackingOptions &options
) noexcept {
const double contractedStepLength = rejectedStepLength * options.contractionFactor;
if (!rejectedByInvertedGeometry || !rejectedMinimumJacobianDeterminant.has_value() ||
!std::isfinite(acceptedMinimumJacobianDeterminant) || acceptedMinimumJacobianDeterminant <= 0.0 ||
!std::isfinite(*rejectedMinimumJacobianDeterminant) || *rejectedMinimumJacobianDeterminant > 0.0) {
return contractedStepLength;
}
const double determinantChange = acceptedMinimumJacobianDeterminant - *rejectedMinimumJacobianDeterminant;
if (!std::isfinite(determinantChange) || determinantChange <= 0.0) {
return contractedStepLength;
}
const double estimatedBoundaryStep =
rejectedStepLength * acceptedMinimumJacobianDeterminant / determinantChange;
const double safeguardedStep = options.fractionToBoundarySafety * estimatedBoundaryStep;
if (!std::isfinite(safeguardedStep) || safeguardedStep <= 0.0 || safeguardedStep >= rejectedStepLength) {
return contractedStepLength;
}
/*
* Keep the configured backtracking ladder intact. The geometry
* certificate is used only to skip rungs that its local boundary
* estimate says are unsafe; it does not introduce a new trial
* length between two rungs. This preserves the candidates that
* ordinary backtracking would eventually test while avoiding the
* expensive preparation of the skipped, inverted geometries.
*/
if (contractedStepLength <= safeguardedStep) {
return contractedStepLength;
}
const double rung =
std::ceil(std::log(safeguardedStep / rejectedStepLength) / std::log(options.contractionFactor));
double skippedStep = rejectedStepLength * std::pow(options.contractionFactor, rung);
if (!std::isfinite(skippedStep) || skippedStep <= 0.0 || skippedStep >= rejectedStepLength) {
return contractedStepLength;
}
if (skippedStep > safeguardedStep) {
skippedStep *= options.contractionFactor;
}
return skippedStep;
}
template <typename Observer>
inline constexpr bool isNoObserver = std::same_as<std::remove_cvref_t<Observer>, NoObserver>;
template <typename Observer>
concept ObservesBeforeIteration =
!isNoObserver<Observer> &&
requires(std::remove_reference_t<Observer> &observer, const BeforeIteration &event) {
{ observer.beforeIteration(event) } -> std::same_as<void>;
};
template <typename Observer>
concept ObservesAfterIteration =
!isNoObserver<Observer> &&
requires(std::remove_reference_t<Observer> &observer, const AfterIteration &event) {
{ observer.afterIteration(event) } -> std::same_as<void>;
};
template <typename Observer>
concept ObservesLineSearchTrial =
!isNoObserver<Observer> &&
requires(std::remove_reference_t<Observer> &observer, const AfterLineSearchTrial &event) {
{ observer.afterLineSearchTrial(event) } -> std::same_as<void>;
};
template <typename Callback>
void InvokeObserverHookCollectively(
const MPI_Comm communicator,
const char *remoteFailureMessage,
Callback &&callback
) {
std::exception_ptr localFailure;
try {
std::invoke(std::forward<Callback>(callback));
} catch (...) {
localFailure = std::current_exception();
}
const int localFailureFlag = localFailure != nullptr ? 1 : 0;
int globalFailureFlag = 0;
if (MPI_Allreduce(&localFailureFlag, &globalFailureFlag, 1, MPI_INT, MPI_MAX, communicator) !=
MPI_SUCCESS) {
if (localFailure != nullptr) {
std::rethrow_exception(localFailure);
}
throw std::runtime_error("The nonlinear solver could not synchronize an observer callback.");
}
if (globalFailureFlag != 0) {
if (localFailure != nullptr) {
std::rethrow_exception(localFailure);
}
throw std::runtime_error(remoteFailureMessage);
}
}
template <typename Observer>
void InvokeBeforeIteration(
Observer &observer,
const BeforeIteration &event
) {
if constexpr (ObservesBeforeIteration<Observer>) {
if constexpr (noexcept(observer.beforeIteration(event))) {
observer.beforeIteration(event);
} else {
InvokeObserverHookCollectively(
event.communicator, "An observer before-iteration callback failed on another rank.",
[&observer, &event] { observer.beforeIteration(event); }
);
}
}
}
template <typename Observer>
void InvokeAfterIteration(
Observer &observer,
const AfterIteration &event
) {
if constexpr (ObservesAfterIteration<Observer>) {
if constexpr (noexcept(observer.afterIteration(event))) {
observer.afterIteration(event);
} else {
InvokeObserverHookCollectively(
event.communicator, "An observer after-iteration callback failed on another rank.",
[&observer, &event] { observer.afterIteration(event); }
);
}
}
}
template <typename Observer>
void InvokeAfterLineSearchTrial(
Observer &observer,
const AfterLineSearchTrial &event
) {
if constexpr (ObservesLineSearchTrial<Observer>) {
if constexpr (noexcept(observer.afterLineSearchTrial(event))) {
observer.afterLineSearchTrial(event);
} else {
InvokeObserverHookCollectively(
event.communicator, "An observer line-search callback failed on another rank.",
[&observer, &event] { observer.afterLineSearchTrial(event); }
);
}
}
}
} // namespace detail
/*
* Observers run synchronously on every solve rank. Ordinary callback
* exceptions are synchronized before the solver proceeds, so all ranks can
* unwind together; explicitly noexcept callbacks bypass that synchronization.
* A callback must still not enter an MPI collective on only a subset of
* ranks. A before/after pair is guaranteed for iterations that finish by
* returning an evaluation report. Infrastructure exceptions unwind
* immediately and do not promise an after callback.
*/
template <typename Candidate>
concept NewtonObserver = detail::isNoObserver<Candidate> || detail::ObservesBeforeIteration<Candidate> ||
detail::ObservesLineSearchTrial<Candidate> || detail::ObservesAfterIteration<Candidate>;
} // namespace mean_field::solver::nonlinear

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
export module mean_field:solver.stellar_equilibrium;
export import :solver.stellar_structure;
export import :solver.stellar_context;

View File

@@ -0,0 +1,64 @@
module;
#include <cstdint>
#include <optional>
#include <string>
export module mean_field:solver.stellar_equilibrium_types;
export import :deformation.safe_newton_step;
export import :solver.linear_backend;
export namespace mean_field::solver {
enum class StellarEquilibriumFailureReason : std::uint8_t {
unspecified,
inadmissible_state,
non_finite_state,
non_finite_residual,
linear_solve_failure,
globalization_failure,
stagnation,
iteration_limit
};
/*
* An owning, backend-neutral account of an expected numerical failure.
* Backend-specific measurements may be translated into the message or
* future common diagnostics, but are deliberately not part of this stable
* result boundary.
*/
struct StellarEquilibriumFailureReport final {
StellarEquilibriumFailureReason reason{StellarEquilibriumFailureReason::unspecified};
std::string message;
int completedNonlinearIterations{0};
std::optional<double> initialResidualNorm;
std::optional<double> finalResidualNorm;
};
/*
* Fixed-size diagnostics retained by every evaluation report. Detailed
* iteration histories belong in an observer so the default solve does not
* allocate storage proportional to the iteration count.
*/
struct StellarEquilibriumEvaluationDiagnostics final {
int attemptedNonlinearIterations{0};
int acceptedNonlinearIterations{0};
int totalLineSearchTrials{0};
int inadmissibleLineSearchTrials{0};
int nonFiniteLineSearchTrials{0};
int insufficientDecreaseTrials{0};
int geometryLimitedIterations{0};
double initialResidualNorm{0.0};
double finalResidualNorm{0.0};
double lastAcceptedStepLength{0.0};
double totalLinearSolveSeconds{0.0};
double totalGeometryPreflightSeconds{0.0};
double totalLineSearchSeconds{0.0};
double totalTrialPreparationSeconds{0.0};
double totalMetricEvaluationSeconds{0.0};
double totalPreconditionerRefreshSeconds{0.0};
double totalRollbackSeconds{0.0};
std::optional<deformation::LargestSafeNewtonStepSizeEstimate> lastGeometryPreflight;
std::optional<LinearSolveReport> lastLinearSolve;
};
} // namespace mean_field::solver

View File

@@ -0,0 +1,486 @@
module;
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:solver.stellar_structure;
export import :operators.stellar_equilibrium_problem;
export import :solver.stellar_equilibrium_types;
export namespace mean_field::solver {
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem> class StellarEquilibriumEvaluationReport;
}
namespace mean_field::solver::detail {
enum class StellarViewCertification : std::uint8_t { unavailable, checkpoint, structure };
template <typename Problem> struct StellarStructureStorage final {
using ProblemType = std::remove_cvref_t<Problem>;
StellarStructureStorage(
std::unique_ptr<ProblemType> ownedProblem,
std::unique_ptr<mfem::Vector> acceptedPhysicalState,
std::unique_ptr<physics::RigidRotation> prescribedRotation
)
: problem(std::move(ownedProblem)),
physicalState(std::move(acceptedPhysicalState)),
rotation(std::move(prescribedRotation)) {
}
std::unique_ptr<ProblemType> problem;
std::unique_ptr<mfem::Vector> physicalState;
std::unique_ptr<physics::RigidRotation> rotation;
std::uint64_t viewGeneration{0};
StellarViewCertification certification{StellarViewCertification::unavailable};
};
template <typename Problem>
[[nodiscard]] bool IsCurrentView(
const std::weak_ptr<const StellarStructureStorage<Problem>> &candidate,
const std::uint64_t generation,
const bool requireConverged
) noexcept {
const auto storage = candidate.lock();
if (storage == nullptr || storage->problem == nullptr || storage->physicalState == nullptr ||
storage->viewGeneration != generation) {
return false;
}
if (requireConverged) {
return storage->certification == StellarViewCertification::structure;
}
return storage->certification == StellarViewCertification::checkpoint ||
storage->certification == StellarViewCertification::structure;
}
template <typename Problem>
[[nodiscard]] std::shared_ptr<const StellarStructureStorage<Problem>> RequireCurrentView(
const std::weak_ptr<const StellarStructureStorage<Problem>> &candidate,
const std::uint64_t generation,
const bool requireConverged
) {
auto storage = candidate.lock();
if (storage == nullptr || storage->problem == nullptr || storage->physicalState == nullptr ||
storage->viewGeneration != generation ||
(requireConverged && storage->certification != StellarViewCertification::structure) ||
(!requireConverged && storage->certification != StellarViewCertification::checkpoint &&
storage->certification != StellarViewCertification::structure)) {
throw std::logic_error("The stellar structure view is stale or is not certified for this result.");
}
return storage;
}
template <typename Vector> [[nodiscard]] std::span<const mfem::real_t> ReadOnlySpan(const Vector &values) noexcept {
return {values.GetData(), static_cast<std::size_t>(values.Size())};
}
template <typename Problem> struct StellarEvaluationReportAccess;
} // namespace mean_field::solver::detail
export namespace mean_field::equilibrium {
/*
* These are the future owning, self-contained values. There is no public
* construction path until deep capture and its MPI-independent storage
* schema are implemented.
*/
template <DiscretizedStellarEquilibriumProblem Problem> class StellarStructure final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
StellarStructure(const StellarStructure &) = delete;
StellarStructure &operator=(const StellarStructure &) = delete;
StellarStructure(StellarStructure &&) noexcept = default;
StellarStructure &operator=(StellarStructure &&) = delete;
~StellarStructure() = default;
private:
StellarStructure() = default;
};
template <DiscretizedStellarEquilibriumProblem Problem> class StellarCheckpoint final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
StellarCheckpoint(const StellarCheckpoint &) = delete;
StellarCheckpoint &operator=(const StellarCheckpoint &) = delete;
StellarCheckpoint(StellarCheckpoint &&) noexcept = default;
StellarCheckpoint &operator=(StellarCheckpoint &&) = delete;
~StellarCheckpoint() = default;
private:
StellarCheckpoint() = default;
};
/*
* Result views weakly observe context-owned storage. valid() remains safe
* after that context is destroyed. References and spans extracted from a
* valid view remain borrowed: the context must outlive their use, and the
* next evaluate() call invalidates them along with their originating view.
*/
template <DiscretizedStellarEquilibriumProblem Problem> class StellarStructureView final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
using ModelType = typename ProblemType::ModelType;
[[nodiscard]] bool valid() const noexcept {
return solver::detail::IsCurrentView(m_storage, m_generation, true);
}
[[nodiscard]] const ModelType &model() const & {
const auto storage = RequireStorage();
return storage->problem->GetStellarModel();
}
[[nodiscard]] const ModelType &model() const && = delete;
[[nodiscard]] MPI_Comm communicator() const & {
const auto storage = RequireStorage();
return storage->problem->GetCommunicator();
}
[[nodiscard]] MPI_Comm communicator() const && = delete;
[[nodiscard]] std::span<const mfem::real_t> state() const & {
const auto storage = RequireStorage();
return solver::detail::ReadOnlySpan(*storage->physicalState);
}
[[nodiscard]] std::span<const mfem::real_t> state() const && = delete;
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const & {
const auto storage = RequireStorage();
return storage->problem->GetManifest().valueBlocks();
}
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const && = delete;
template <typename Term>
requires requires(
const typename ProblemType::ManifestType &manifest,
const mfem::Vector &physicalState,
const Term &term
) { manifest.stateView(physicalState).block(term); }
[[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &term) const & {
const auto storage = RequireStorage();
const auto block = storage->problem->GetManifest().stateView(*storage->physicalState).block(term);
return solver::detail::ReadOnlySpan(block);
}
template <typename Term> [[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &) const && = delete;
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const & {
const auto storage = RequireStorage();
if (storage->rotation == nullptr) {
return std::nullopt;
}
return *storage->rotation;
}
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const && = delete;
[[nodiscard]] physics::RigidRotation rotation() const & {
const auto storage = RequireStorage();
return storage->problem->GetPreparedOperator().GetRotation();
}
[[nodiscard]] physics::RigidRotation rotation() const && = delete;
[[nodiscard]] StellarStructure<ProblemType> capture() const {
(void)RequireStorage();
throw std::logic_error("Capturing a self-contained StellarStructure is not implemented.");
}
private:
template <DiscretizedStellarEquilibriumProblem> friend class solver::StellarEquilibriumEvaluationReport;
using Storage = solver::detail::StellarStructureStorage<ProblemType>;
StellarStructureView(
std::weak_ptr<const Storage> storage,
const std::uint64_t generation
) noexcept
: m_storage(std::move(storage)),
m_generation(generation) {
}
[[nodiscard]] std::shared_ptr<const Storage> RequireStorage() const {
return solver::detail::RequireCurrentView(m_storage, m_generation, true);
}
std::weak_ptr<const Storage> m_storage;
std::uint64_t m_generation;
};
template <DiscretizedStellarEquilibriumProblem Problem> class StellarCheckpointView final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
using ModelType = typename ProblemType::ModelType;
[[nodiscard]] bool valid() const noexcept {
return solver::detail::IsCurrentView(m_storage, m_generation, false);
}
[[nodiscard]] const ModelType &model() const & {
const auto storage = RequireStorage();
return storage->problem->GetStellarModel();
}
[[nodiscard]] const ModelType &model() const && = delete;
[[nodiscard]] MPI_Comm communicator() const & {
const auto storage = RequireStorage();
return storage->problem->GetCommunicator();
}
[[nodiscard]] MPI_Comm communicator() const && = delete;
[[nodiscard]] std::span<const mfem::real_t> state() const & {
const auto storage = RequireStorage();
return solver::detail::ReadOnlySpan(*storage->physicalState);
}
[[nodiscard]] std::span<const mfem::real_t> state() const && = delete;
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const & {
const auto storage = RequireStorage();
return storage->problem->GetManifest().valueBlocks();
}
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const && = delete;
template <typename Term>
requires requires(
const typename ProblemType::ManifestType &manifest,
const mfem::Vector &physicalState,
const Term &term
) { manifest.stateView(physicalState).block(term); }
[[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &term) const & {
const auto storage = RequireStorage();
const auto block = storage->problem->GetManifest().stateView(*storage->physicalState).block(term);
return solver::detail::ReadOnlySpan(block);
}
template <typename Term> [[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &) const && = delete;
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const & {
const auto storage = RequireStorage();
if (storage->rotation == nullptr) {
return std::nullopt;
}
return *storage->rotation;
}
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const && = delete;
[[nodiscard]] physics::RigidRotation rotation() const & {
const auto storage = RequireStorage();
return storage->problem->GetPreparedOperator().GetRotation();
}
[[nodiscard]] physics::RigidRotation rotation() const && = delete;
[[nodiscard]] StellarCheckpoint<ProblemType> capture() const {
(void)RequireStorage();
throw std::logic_error("Capturing a self-contained StellarCheckpoint is not implemented.");
}
private:
template <DiscretizedStellarEquilibriumProblem> friend class solver::StellarEquilibriumEvaluationReport;
using Storage = solver::detail::StellarStructureStorage<ProblemType>;
StellarCheckpointView(
std::weak_ptr<const Storage> storage,
const std::uint64_t generation
) noexcept
: m_storage(std::move(storage)),
m_generation(generation) {
}
[[nodiscard]] std::shared_ptr<const Storage> RequireStorage() const {
return solver::detail::RequireCurrentView(m_storage, m_generation, false);
}
std::weak_ptr<const Storage> m_storage;
std::uint64_t m_generation;
};
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarStructure<Problem> &,
const std::filesystem::path &
) {
throw std::logic_error("Serializing a StellarStructure is not implemented.");
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarStructureView<Problem> &view,
const std::filesystem::path &
) {
(void)view.state();
throw std::logic_error("Serializing a StellarStructureView is not implemented.");
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarCheckpoint<Problem> &,
const std::filesystem::path &
) {
throw std::logic_error("Serializing a StellarCheckpoint is not implemented.");
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarCheckpointView<Problem> &view,
const std::filesystem::path &
) {
(void)view.state();
throw std::logic_error("Serializing a StellarCheckpointView is not implemented.");
}
} // namespace mean_field::equilibrium
export namespace mean_field::solver {
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem>
class StellarEquilibriumEvaluationReport final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
using StructureView = equilibrium::StellarStructureView<ProblemType>;
using CheckpointView = equilibrium::StellarCheckpointView<ProblemType>;
StellarEquilibriumEvaluationReport(const StellarEquilibriumEvaluationReport &) = default;
StellarEquilibriumEvaluationReport &operator=(const StellarEquilibriumEvaluationReport &) = default;
StellarEquilibriumEvaluationReport(StellarEquilibriumEvaluationReport &&) noexcept = default;
StellarEquilibriumEvaluationReport &operator=(StellarEquilibriumEvaluationReport &&) noexcept = default;
~StellarEquilibriumEvaluationReport() = default;
[[nodiscard]] bool converged() const noexcept {
return m_converged;
}
[[nodiscard]] const StellarEquilibriumEvaluationDiagnostics &diagnostics() const & noexcept {
return m_diagnostics;
}
[[nodiscard]] const StellarEquilibriumEvaluationDiagnostics &diagnostics() const && = delete;
[[nodiscard]] int completedNonlinearIterations() const noexcept {
return m_diagnostics.acceptedNonlinearIterations;
}
[[nodiscard]] double initialResidualNorm() const noexcept {
return m_diagnostics.initialResidualNorm;
}
[[nodiscard]] double finalResidualNorm() const noexcept {
return m_diagnostics.finalResidualNorm;
}
[[nodiscard]] const StellarEquilibriumFailureReport &failure() const & {
if (!m_failure.has_value()) {
throw std::logic_error("A converged stellar-equilibrium report has no failure record.");
}
return *m_failure;
}
[[nodiscard]] const StellarEquilibriumFailureReport &failure() const && = delete;
[[nodiscard]] StructureView structureView() const {
if (!m_converged) {
throw std::logic_error("A failed stellar-equilibrium report cannot certify a structure view.");
}
StructureView view{m_storage, m_generation};
if (!view.valid()) {
throw std::logic_error("The stellar-equilibrium structure view has been invalidated.");
}
return view;
}
[[nodiscard]] CheckpointView checkpointView() const {
CheckpointView view{m_storage, m_generation};
if (!view.valid()) {
throw std::logic_error("The stellar-equilibrium checkpoint view has been invalidated.");
}
return view;
}
[[nodiscard]] CheckpointView lastAcceptedCheckpointView() const {
return checkpointView();
}
private:
friend struct detail::StellarEvaluationReportAccess<ProblemType>;
using Storage = detail::StellarStructureStorage<ProblemType>;
StellarEquilibriumEvaluationReport(
const bool converged,
StellarEquilibriumEvaluationDiagnostics diagnostics,
std::optional<StellarEquilibriumFailureReport> failure,
std::weak_ptr<const Storage> storage,
const std::uint64_t generation
)
: m_converged(converged),
m_diagnostics(std::move(diagnostics)),
m_failure(std::move(failure)),
m_storage(std::move(storage)),
m_generation(generation) {
}
bool m_converged;
StellarEquilibriumEvaluationDiagnostics m_diagnostics;
std::optional<StellarEquilibriumFailureReport> m_failure;
std::weak_ptr<const Storage> m_storage;
std::uint64_t m_generation;
};
} // namespace mean_field::solver
namespace mean_field::solver::detail {
template <typename Problem> struct StellarEvaluationReportAccess final {
using ProblemType = std::remove_cvref_t<Problem>;
using Report = StellarEquilibriumEvaluationReport<ProblemType>;
using Storage = StellarStructureStorage<ProblemType>;
[[nodiscard]] static Report Success(
const std::shared_ptr<Storage> &storage,
StellarEquilibriumEvaluationDiagnostics diagnostics
) {
if (storage == nullptr) {
throw std::invalid_argument("A stellar-equilibrium report requires owned result storage.");
}
storage->certification = StellarViewCertification::structure;
return Report{true, std::move(diagnostics), std::nullopt, storage, storage->viewGeneration};
}
[[nodiscard]] static Report Failure(
const std::shared_ptr<Storage> &storage,
StellarEquilibriumEvaluationDiagnostics diagnostics,
const StellarEquilibriumFailureReason reason,
std::string message
) {
if (storage == nullptr) {
throw std::invalid_argument("A stellar-equilibrium report requires owned result storage.");
}
storage->certification = StellarViewCertification::checkpoint;
StellarEquilibriumFailureReport failure{
.reason = reason,
.message = std::move(message),
.completedNonlinearIterations = diagnostics.acceptedNonlinearIterations,
.initialResidualNorm = diagnostics.initialResidualNorm,
.finalResidualNorm = diagnostics.finalResidualNorm
};
return Report{
false, std::move(diagnostics), std::optional<StellarEquilibriumFailureReport>{std::move(failure)},
storage, storage->viewGeneration
};
}
};
} // namespace mean_field::solver::detail

251
sandbox.cpp Normal file
View File

@@ -0,0 +1,251 @@
#include <chrono>
#include <exception>
#include <iostream>
#include <numbers>
#include <stdexcept>
#include <utility>
#include <mfem.hpp>
import mean_field;
using namespace mean_field;
namespace {
[[nodiscard]] const char *trialDispositionName(
const solver::nonlinear::LineSearchTrialDisposition disposition
) noexcept {
using Disposition = solver::nonlinear::LineSearchTrialDisposition;
switch (disposition) {
case Disposition::accepted:
return "accepted";
case Disposition::inadmissible_state:
return "inadmissible state";
case Disposition::non_finite_state:
return "non-finite state";
case Disposition::non_finite_residual:
return "non-finite residual";
case Disposition::insufficient_decrease:
default:
return "insufficient decrease";
}
}
[[nodiscard]] const char *linearStatusName(const solver::LinearSolveStatus status) noexcept {
switch (status) {
case solver::LinearSolveStatus::converged:
return "converged";
case solver::LinearSolveStatus::maximum_iterations:
return "maximum iterations";
case solver::LinearSolveStatus::breakdown:
return "breakdown";
case solver::LinearSolveStatus::non_finite:
return "non-finite";
case solver::LinearSolveStatus::backend_failure:
default:
return "backend failure";
}
}
} // namespace
int main(
int argc,
char **argv
) {
mfem::Mpi::Init(argc, argv);
int exitCode = 0;
try {
mfem::Device device("cpu");
int rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
utils::Args arguments;
arguments.mesh_file = "sandbox.smesh";
arguments.p.rtol = 1.0e-12;
arguments.p.atol = 1.0e-12;
const auto discretizationStart = std::chrono::steady_clock::now();
auto finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
if (!finiteElements.okay()) {
throw std::runtime_error("The sandbox could not construct its finite-element discretization.");
}
if (rank == 0) {
std::cout << "Finite-element setup: "
<< std::chrono::duration<double>(std::chrono::steady_clock::now() - discretizationStart).count()
<< " s\n";
}
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
constexpr double angularMomentum = 0.1;
constexpr double gravitationalConstant = utils::G;
const double polytropicConstant =
2.0 * gravitationalConstant * radius * radius / std::numbers::pi_v<double>;
std::println("K = {}", polytropicConstant);
const double centralDensity =
std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto stellarModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{angularMomentum},
.axis = {0.0, 0.0, 1.0},
.center = {0.0, 0.0, 0.0}
}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto discretization = equilibrium::makeStellarDiscretization(
std::move(finiteElements),
normalization::PhysicalRieszDiagonal{
dimensions::LengthValue{radius},
gravitationalConstant
}
);
auto preconditioner = preconditioning::makePreconditioner();
auto linearSolver = solver::linear::FGMRES({.restartLength = 40, .printLevel = 1});
const auto contextStart = std::chrono::steady_clock::now();
auto context = solver::makeContext(
std::move(stellarModel), std::move(discretization), std::move(preconditioner), std::move(linearSolver)
);
if (rank == 0) {
std::cout << "Solver context setup: "
<< std::chrono::duration<double>(std::chrono::steady_clock::now() - contextStart).count()
<< " s\n";
}
auto observer = solver::nonlinear::makeObserver(
[](const solver::nonlinear::BeforeIteration &event) {
int rank = 0;
MPI_Comm_rank(event.communicator, &rank);
if (rank == 0) {
std::cout << "Newton " << event.iteration << ": |F| = " << event.residualNorm << '\n';
}
},
[](const solver::nonlinear::AfterLineSearchTrial &event) {
int rank = 0;
MPI_Comm_rank(event.communicator, &rank);
if (rank != 0) {
return;
}
std::cout << " trial " << event.trial + 1 << ": step = " << event.stepLength
<< ", outcome = " << trialDispositionName(event.disposition);
if (!event.rejectionSource.empty()) {
std::cout << ", source = " << event.rejectionSource;
}
if (event.metric.has_value()) {
std::cout << ", |F| = " << event.metric->residualNorm;
}
if (event.minimumJacobianDeterminant.has_value()) {
std::cout << ", min(det J_map) = " << *event.minimumJacobianDeterminant;
}
std::cout << ", prepare = " << event.preparationSeconds << " s"
<< ", metric = " << event.metricSeconds << " s\n";
},
[](const solver::nonlinear::AfterIteration &event) {
int rank = 0;
MPI_Comm_rank(event.communicator, &rank);
if (rank == 0) {
if (event.geometryPreflight.has_value()) {
const auto &geometry = *event.geometryPreflight;
std::cout << " geometry preflight = " << event.geometryPreflightSeconds
<< " s, samples = " << geometry.sampledQuadraturePointCount;
if (geometry.limitedByGeometry) {
std::cout << ", safe step = " << geometry.stepSize
<< ", boundary = " << geometry.boundaryStepSize
<< ", limiting rank = " << geometry.limitingRank
<< ", element = " << geometry.limitingElement;
}
std::cout << '\n';
}
std::cout << " step = " << event.acceptedStepLength
<< ", trials = " << event.lineSearchTrials
<< ", |F| = " << event.residualNorm
<< ", iteration = " << event.iterationSeconds << " s"
<< ", line search = " << event.lineSearchSeconds << " s"
<< ", trial preparation = " << event.trialPreparationSeconds << " s"
<< ", accepted refresh = " << event.preconditionerRefreshSeconds << " s\n";
if (event.rollbackSeconds > 0.0) {
std::cout << " rollback = " << event.rollbackSeconds << " s\n";
}
if (event.linearSolve.has_value()) {
const auto &linear = *event.linearSolve;
std::cout << " FGMRES: " << linearStatusName(linear.status)
<< ", iterations = " << linear.iterations
<< ", restarts = " << linear.restarts
<< ", initial/|b| = "
<< (linear.rightHandSideNorm > 0.0
? linear.initialResidualNorm / linear.rightHandSideNorm
: 0.0)
<< ", true/|b| = " << linear.relativeTrueResidualNorm
<< ", J calls = " << linear.operatorApplications
<< ", P^-1 calls = " << linear.inversePreconditionerApplications
<< ", solve = " << linear.solveSeconds << " s"
<< ", J time = " << linear.operatorSeconds << " s"
<< ", P^-1 time = " << linear.inversePreconditionerSeconds << " s\n";
}
}
}
);
auto nonlinearSolver = solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{
.relativeTolerance = 1.0e-8,
.absoluteTolerance = 0.0,
.maximumIterations = 30,
.linearSolve = {
.relativeTolerance = 3.0e-2,
.absoluteTolerance = 0.0,
.maximumIterations = 200
},
.backtracking = {}
});
auto equilibriumSolver = solver::make(context, nonlinearSolver, observer);
const auto evaluateStart = std::chrono::steady_clock::now();
auto report = equilibriumSolver.evaluate();
if (rank == 0) {
std::cout << "Evaluation: "
<< std::chrono::duration<double>(std::chrono::steady_clock::now() - evaluateStart).count()
<< " s\n";
const auto &diagnostics = report.diagnostics();
std::cout << "Totals: linear = " << diagnostics.totalLinearSolveSeconds
<< " s, geometry preflight = " << diagnostics.totalGeometryPreflightSeconds
<< " s, line search = " << diagnostics.totalLineSearchSeconds
<< " s, trial preparation = " << diagnostics.totalTrialPreparationSeconds
<< " s, accepted refresh = " << diagnostics.totalPreconditionerRefreshSeconds
<< " s, rollback = " << diagnostics.totalRollbackSeconds
<< " s, geometry-limited iterations = " << diagnostics.geometryLimitedIterations
<< ", inadmissible trials = " << diagnostics.inadmissibleLineSearchTrials
<< ", non-finite trials = " << diagnostics.nonFiniteLineSearchTrials
<< ", insufficient-decrease trials = " << diagnostics.insufficientDecreaseTrials << '\n';
}
if (report.converged()) {
auto structureView = report.structureView();
std::cout << "Converged with " << structureView.state().size() << " state values.\n";
// These are the eventual persistence APIs. Both deliberately
// throw "not implemented" until the checkpoint schema is chosen:
// auto structure = structureView.capture();
// equilibrium::serialize(structureView, "structure.checkpoint");
} else {
auto checkpointView = report.lastAcceptedCheckpointView();
std::cerr << "Solve stopped after " << report.completedNonlinearIterations()
<< " accepted steps: " << report.failure().message << '\n'
<< "The last checkpoint has " << checkpointView.state().size() << " state values.\n";
// auto checkpoint = checkpointView.capture();
// equilibrium::serialize(checkpointView, "failed-step.checkpoint");
exitCode = 1;
}
} catch (const std::exception &error) {
std::cerr << "sandbox failure: " << error.what() << '\n';
exitCode = 2;
}
mfem::Mpi::Finalize();
return exitCode;
}

298081
sandbox.smesh

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cstdint>
#include <memory>
#include <mfem.hpp>
#include <mpi.h>
#include <stdexcept>
#include <utility>
#include <vector>
import mean_field;
namespace {
using Catch::Matchers::WithinAbs;
constexpr int dimension = 3;
[[nodiscard]] mfem::Mesh make_serial_mesh(const int attribute) {
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON, 2.0, 1.0, 1.0);
for (int element = 0; element < mesh.GetNE(); ++element) {
mesh.GetElement(element)->SetAttribute(attribute);
}
return mesh;
}
[[nodiscard]] std::unique_ptr<const mean_field::mapping::compactification::ExteriorDomainMap>
make_kelvin_compactification() {
return std::make_unique<mean_field::mapping::compactification::KelvinCompactification>(
mean_field::mapping::compactification::options::KelvinCompactificationOptions{
.r_star_ref = 1.0, .r_inf_ref = 4.0
}
);
}
struct GeometryFixture final {
mfem::Mesh serialMesh;
mfem::ParMesh mesh;
mfem::H1_FECollection displacementCollection;
mfem::ParFiniteElementSpace displacementSpace;
mfem::H1_FECollection compactificationCollection;
mfem::ParFiniteElementSpace compactificationSpace;
mfem::ParGridFunction compactificationCoordinate;
mean_field::mapping::DomainMapper mapper;
explicit GeometryFixture(const bool compactified = false)
: serialMesh(make_serial_mesh(compactified ? 2 : 1)),
mesh(
MPI_COMM_WORLD,
serialMesh
),
displacementCollection(
1,
dimension
),
displacementSpace(
&mesh,
&displacementCollection,
dimension,
mfem::Ordering::byNODES
),
compactificationCollection(
1,
dimension
),
compactificationSpace(
&mesh,
&compactificationCollection
),
compactificationCoordinate(&compactificationSpace),
mapper(
{.dimension = dimension,
.vacuum_element_attribute = 2},
make_kelvin_compactification()
) {
compactificationCoordinate = 0.0;
}
[[nodiscard]] mfem::Vector zero_true_vector() const {
mfem::Vector result(displacementSpace.GetTrueVSize());
result = 0.0;
return result;
}
template <typename Function> [[nodiscard]] mfem::Vector project_direction(Function &&function) {
mfem::VectorFunctionCoefficient coefficient(dimension, std::forward<Function>(function));
mfem::ParGridFunction field(&displacementSpace);
field.ProjectCoefficient(coefficient);
mfem::Vector result;
field.GetTrueDofs(result);
return result;
}
[[nodiscard]] std::vector<mean_field::deformation::NewtonStepGeometryRule> geometry_rules() {
std::vector<mean_field::deformation::NewtonStepGeometryRule> result;
result.reserve(static_cast<std::size_t>(mesh.GetNE()));
for (int element = 0; element < mesh.GetNE(); ++element) {
mfem::ElementTransformation *transformation = mesh.GetElementTransformation(element);
result.push_back(
{.element = element, .integrationRule = &mfem::IntRules.Get(transformation->GetGeometryType(), 2)}
);
}
return result;
}
};
void compress_x(
const mfem::Vector &position,
mfem::Vector &value
) {
value.SetSize(dimension);
value = 0.0;
value(0) = -2.0 * position(0);
}
void compress_x_and_y(
const mfem::Vector &position,
mfem::Vector &value
) {
value.SetSize(dimension);
value = 0.0;
value(0) = -2.0 * position(0);
value(1) = -2.0 * position(1);
}
void expand_x(
const mfem::Vector &position,
mfem::Vector &value
) {
value.SetSize(dimension);
value = 0.0;
value(0) = position(0);
}
} // namespace
TEST_CASE(
"Safe Newton Step Finds The First Mapping Boundary",
"[deformation][newton][geometry][mpi]"
) {
GeometryFixture fixture;
const mfem::Vector accepted = fixture.zero_true_vector();
const mfem::Vector direction = fixture.project_direction(compress_x);
const auto rules = fixture.geometry_rules();
const auto estimate = mean_field::deformation::estimate_largest_safe_newton_step_size(
fixture.mapper, fixture.displacementSpace, fixture.compactificationCoordinate, accepted, direction, rules,
{.maximumStepSize = 1.0, .determinantFloor = 0.0, .fractionToBoundarySafety = 0.8}
);
CHECK(estimate.limitedByGeometry);
CHECK_THAT(estimate.boundaryStepSize, WithinAbs(0.5, 2.0e-13));
CHECK_THAT(estimate.stepSize, WithinAbs(0.4, 2.0e-13));
CHECK_THAT(estimate.minimumDeterminantAtAcceptedState, WithinAbs(1.0, 2.0e-13));
CHECK_THAT(estimate.minimumDeterminantAtMaximumStepSize, WithinAbs(-1.0, 2.0e-13));
CHECK_THAT(estimate.limitingPointDeterminantAtStepSize, WithinAbs(0.2, 2.0e-13));
CHECK(estimate.sampledQuadraturePointCount > 0);
CHECK(estimate.limitingRank == 0);
CHECK(estimate.limitingElement >= 0);
CHECK(estimate.limitingRule >= 0);
CHECK(estimate.limitingQuadraturePoint >= 0);
}
TEST_CASE(
"Safe Newton Step Detects A Tangent Singularity Before An Admissible Endpoint",
"[deformation][newton][geometry][mpi]"
) {
GeometryFixture fixture(true);
const mfem::Vector accepted = fixture.zero_true_vector();
const mfem::Vector direction = fixture.project_direction(compress_x_and_y);
const auto rules = fixture.geometry_rules();
const auto estimate = mean_field::deformation::estimate_largest_safe_newton_step_size(
fixture.mapper, fixture.displacementSpace, fixture.compactificationCoordinate, accepted, direction, rules
);
// det(J(alpha)) = (1 - 2 alpha)^2. Both endpoints are positive;
// checking only alpha=1 would miss the singularity at alpha=1/2.
CHECK(estimate.limitedByGeometry);
CHECK_THAT(estimate.minimumDeterminantAtMaximumStepSize, WithinAbs(1.0, 3.0e-13));
CHECK_THAT(estimate.boundaryStepSize, WithinAbs(0.5, 3.0e-13));
CHECK_THAT(estimate.stepSize, WithinAbs(0.45, 3.0e-13));
CHECK_THAT(estimate.limitingPointDeterminantAtStepSize, WithinAbs(0.01, 3.0e-13));
}
TEST_CASE(
"Safe Newton Step Honors A Positive Determinant Floor",
"[deformation][newton][geometry][mpi]"
) {
GeometryFixture fixture;
const mfem::Vector accepted = fixture.zero_true_vector();
const mfem::Vector direction = fixture.project_direction(compress_x);
const auto rules = fixture.geometry_rules();
const auto estimate = mean_field::deformation::estimate_largest_safe_newton_step_size(
fixture.mapper, fixture.displacementSpace, fixture.compactificationCoordinate, accepted, direction, rules,
{.maximumStepSize = 1.0, .determinantFloor = 0.25, .fractionToBoundarySafety = 0.8}
);
CHECK(estimate.limitedByGeometry);
CHECK_THAT(estimate.boundaryStepSize, WithinAbs(0.375, 2.0e-13));
CHECK_THAT(estimate.stepSize, WithinAbs(0.3, 2.0e-13));
CHECK(estimate.limitingPointDeterminantAtStepSize > 0.25);
}
TEST_CASE(
"Safe Newton Step Leaves An Unconstrained Step Unchanged",
"[deformation][newton][geometry][mpi]"
) {
GeometryFixture fixture;
const mfem::Vector accepted = fixture.zero_true_vector();
const mfem::Vector direction = fixture.project_direction(expand_x);
const auto rules = fixture.geometry_rules();
const auto estimate = mean_field::deformation::estimate_largest_safe_newton_step_size(
fixture.mapper, fixture.displacementSpace, fixture.compactificationCoordinate, accepted, direction, rules
);
CHECK_FALSE(estimate.limitedByGeometry);
CHECK_THAT(estimate.boundaryStepSize, WithinAbs(1.0, 2.0e-13));
CHECK_THAT(estimate.stepSize, WithinAbs(1.0, 2.0e-13));
CHECK_THAT(estimate.minimumDeterminantAtMaximumStepSize, WithinAbs(2.0, 2.0e-13));
CHECK_THAT(estimate.limitingPointDeterminantAtStepSize, WithinAbs(2.0, 2.0e-13));
CHECK(estimate.limitingRank == -1);
CHECK(estimate.limitingElement == -1);
CHECK(estimate.limitingRule == -1);
CHECK(estimate.limitingQuadraturePoint == -1);
}
TEST_CASE(
"Safe Newton Step Rejects Invalid Inputs Collectively",
"[deformation][newton][geometry][mpi]"
) {
GeometryFixture fixture;
const mfem::Vector zero = fixture.zero_true_vector();
const auto rules = fixture.geometry_rules();
CHECK_THROWS_AS(
mean_field::deformation::estimate_largest_safe_newton_step_size(
fixture.mapper, fixture.displacementSpace, fixture.compactificationCoordinate, zero, zero, rules,
{.maximumStepSize = 0.0}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::deformation::estimate_largest_safe_newton_step_size(
fixture.mapper, fixture.displacementSpace, fixture.compactificationCoordinate, zero, zero, {}
),
std::invalid_argument
);
const mfem::Vector invalidAccepted = fixture.project_direction(compress_x);
CHECK_THROWS_AS(
mean_field::deformation::estimate_largest_safe_newton_step_size(
fixture.mapper, fixture.displacementSpace, fixture.compactificationCoordinate, invalidAccepted, zero, rules
),
std::domain_error
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
#include <array>
#include <memory>
#include <type_traits>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
using ScalarTable = mean_field::fem::ScalarReferenceTable;
using VectorTable = mean_field::fem::VectorReferenceTable;
using TableCache = mean_field::fem::ReferenceTableCache;
static_assert(std::is_same_v<
decltype(std::declval<const TableCache &>().GetScalarTable(
std::declval<const mfem::FiniteElement &>(),
std::declval<const mfem::IntegrationRule &>()
)),
std::shared_ptr<const ScalarTable>>);
static_assert(std::is_same_v<
decltype(std::declval<const ScalarTable &>().GetValues()),
const mfem::DenseMatrix &>);
static_assert(std::is_same_v<
decltype(std::declval<const ScalarTable &>().GetGradients(0)),
const mfem::DenseMatrix &>);
static_assert(std::is_same_v<
decltype(std::declval<const TableCache &>().GetVectorTable(
std::declval<const mfem::FiniteElement &>(),
std::declval<const mfem::IntegrationRule &>()
)),
std::shared_ptr<const VectorTable>>);
static_assert(std::is_same_v<
decltype(std::declval<const VectorTable &>().GetValues(0)),
const mfem::DenseMatrix &>);
void CheckMatrixExactly(
const mfem::DenseMatrix &actual,
const mfem::DenseMatrix &expected
) {
REQUIRE(actual.Height() == expected.Height());
REQUIRE(actual.Width() == expected.Width());
for (int column = 0; column < actual.Width(); ++column) {
for (int row = 0; row < actual.Height(); ++row) {
CHECK(actual(row, column) == expected(row, column));
}
}
}
void CheckScalarTable(
const ScalarTable &table,
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
) {
REQUIRE(table.GetPointCount() == rule.GetNPoints());
REQUIRE(table.GetDofCount() == element.GetDof());
REQUIRE(table.GetDimension() == element.GetDim());
REQUIRE(table.GetValues().Height() == rule.GetNPoints());
REQUIRE(table.GetValues().Width() == element.GetDof());
mfem::Vector shape(element.GetDof());
mfem::DenseMatrix gradient(element.GetDof(), element.GetDim());
for (int point = 0; point < rule.GetNPoints(); ++point) {
element.CalcShape(rule.IntPoint(point), shape);
element.CalcDShape(rule.IntPoint(point), gradient);
for (int dof = 0; dof < element.GetDof(); ++dof) {
CHECK(table.GetValues()(point, dof) == shape(dof));
}
CheckMatrixExactly(table.GetGradients(point), gradient);
}
}
void CheckVectorTable(
const VectorTable &table,
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
) {
REQUIRE(table.GetPointCount() == rule.GetNPoints());
REQUIRE(table.GetDofCount() == element.GetDof());
REQUIRE(table.GetDimension() == element.GetRangeDim());
mfem::DenseMatrix shape(element.GetDof(), element.GetRangeDim());
for (int point = 0; point < rule.GetNPoints(); ++point) {
element.CalcVShape(rule.IntPoint(point), shape);
CheckMatrixExactly(table.GetValues(point), shape);
}
}
mfem::IntegrationRule CopyRule(const mfem::IntegrationRule &source) {
mfem::IntegrationRule copy(source.GetNPoints());
copy.SetOrder(source.GetOrder());
for (int point = 0; point < source.GetNPoints(); ++point) {
copy.IntPoint(point) = source.IntPoint(point);
}
return copy;
}
} // namespace
TEST_CASE(
"Reference Table Cache Matches Scalar MFEM Values And Gradients",
tags::unit &tags::quadrature
) {
for (const int dimension : std::array{2, 3}) {
const auto geometry = dimension == 2 ? mfem::Geometry::SQUARE : mfem::Geometry::CUBE;
for (const int order : std::array{1, 3}) {
CAPTURE(dimension, order);
mfem::H1_FECollection h1(order, dimension);
mfem::L2_FECollection l2(order - 1, dimension);
TableCache cache;
const mfem::IntegrationRule &rule = mfem::IntRules.Get(geometry, 2 * order + 1);
for (const mfem::FiniteElement *element :
std::array{h1.FiniteElementForGeometry(geometry), l2.FiniteElementForGeometry(geometry)}) {
REQUIRE(element != nullptr);
const auto table = cache.GetScalarTable(*element, rule);
REQUIRE(table != nullptr);
CheckScalarTable(*table, *element, rule);
}
}
}
}
TEST_CASE(
"Reference Table Cache Matches RT Reference Values And Shares Equal Rules",
tags::unit &tags::quadrature
) {
for (const int dimension : std::array{2, 3}) {
const auto geometry = dimension == 2 ? mfem::Geometry::SQUARE : mfem::Geometry::CUBE;
for (const int order : std::array{0, 2}) {
CAPTURE(dimension, order);
mfem::RT_FECollection standard(order, dimension);
mfem::RT_FECollection integrated(
order, dimension, mfem::BasisType::GaussLobatto, mfem::BasisType::IntegratedGLL
);
const mfem::FiniteElement &standardElement = *standard.FiniteElementForGeometry(geometry);
const mfem::FiniteElement &integratedElement = *integrated.FiniteElementForGeometry(geometry);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(geometry, 2 * order + 3);
const mfem::IntegrationRule copiedRule = CopyRule(rule);
const TableCache cache;
const auto standardTable = cache.GetVectorTable(standardElement, rule);
const auto integratedTable = cache.GetVectorTable(integratedElement, rule);
REQUIRE(standardTable != nullptr);
REQUIRE(integratedTable != nullptr);
CHECK(cache.GetVectorTable(standardElement, copiedRule).get() == standardTable.get());
CHECK(cache.GetVectorTable(integratedElement, copiedRule).get() == integratedTable.get());
CHECK(standardTable.get() != integratedTable.get());
CheckVectorTable(*standardTable, standardElement, rule);
CheckVectorTable(*integratedTable, integratedElement, rule);
}
}
}
TEST_CASE(
"Reference Table Cache Shares Equal Rules And Distinguishes Rule Contents",
tags::unit &tags::quadrature
) {
mfem::H1_FECollection collection(3, 3);
const mfem::FiniteElement &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 7);
mfem::IntegrationRule copiedRule = CopyRule(rule);
mfem::IntegrationRule movedPointRule = CopyRule(rule);
mfem::IntegrationRule changedWeightRule = CopyRule(rule);
movedPointRule.IntPoint(0).x += 0.03125;
changedWeightRule.IntPoint(0).weight *= 1.25;
const TableCache cache;
const auto original = cache.GetScalarTable(element, rule);
const mfem::DenseMatrix originalValues(original->GetValues());
const auto copy = cache.GetScalarTable(element, copiedRule);
const auto movedPoint = cache.GetScalarTable(element, movedPointRule);
const auto changedWeight = cache.GetScalarTable(element, changedWeightRule);
CHECK(copy.get() == original.get());
CHECK(movedPointRule.GetOrder() == rule.GetOrder());
CHECK(changedWeightRule.GetOrder() == rule.GetOrder());
CHECK(movedPoint.get() != original.get());
CHECK(changedWeight.get() != original.get());
CHECK(changedWeight.get() != movedPoint.get());
CheckScalarTable(*movedPoint, element, movedPointRule);
CheckScalarTable(*changedWeight, element, changedWeightRule);
CheckMatrixExactly(original->GetValues(), originalValues);
// Rule identity is its contents, not its address, including after mutation.
copiedRule.IntPoint(0).x = movedPointRule.IntPoint(0).x;
CHECK(cache.GetScalarTable(element, copiedRule).get() == movedPoint.get());
CHECK(cache.GetScalarTable(element, rule).get() == original.get());
}
TEST_CASE(
"Reference Table Cache Distinguishes Scalar Basis Variants",
tags::unit &tags::quadrature
) {
constexpr int dimension = 3;
constexpr int order = 3;
mfem::H1_FECollection nodalH1(order, dimension, mfem::BasisType::GaussLobatto);
mfem::H1_FECollection positiveH1(order, dimension, mfem::BasisType::Positive);
mfem::L2_FECollection openL2(order, dimension, mfem::BasisType::GaussLegendre);
mfem::L2_FECollection closedL2(order, dimension, mfem::BasisType::GaussLobatto);
const mfem::IntegrationRule &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 5);
const TableCache cache;
std::array<std::shared_ptr<const ScalarTable>, 4> tables;
const std::array<const mfem::FiniteElement *, 4> elements{
nodalH1.FiniteElementForGeometry(mfem::Geometry::CUBE),
positiveH1.FiniteElementForGeometry(mfem::Geometry::CUBE),
openL2.FiniteElementForGeometry(mfem::Geometry::CUBE), closedL2.FiniteElementForGeometry(mfem::Geometry::CUBE)
};
for (std::size_t index = 0; index < elements.size(); ++index) {
REQUIRE(elements[index] != nullptr);
REQUIRE(elements[index]->GetOrder() == order);
REQUIRE(elements[index]->GetDof() == elements[0]->GetDof());
tables[index] = cache.GetScalarTable(*elements[index], rule);
CheckScalarTable(*tables[index], *elements[index], rule);
for (std::size_t previous = 0; previous < index; ++previous) {
CHECK(tables[index].get() != tables[previous].get());
}
}
}
TEST_CASE(
"Reference Table Cache Published Scalar Storage Outlives Its Cache",
tags::unit &tags::quadrature
) {
std::shared_ptr<const ScalarTable> retained;
mfem::DenseMatrix expectedValues;
mfem::DenseMatrix expectedGradient;
{
// FE objects remain immutable and alive throughout the cache lifetime.
mfem::H1_FECollection collection(3, 2);
const mfem::FiniteElement &element = *collection.FiniteElementForGeometry(mfem::Geometry::SQUARE);
const mfem::IntegrationRule rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::SQUARE, 7));
const TableCache cache;
retained = cache.GetScalarTable(element, rule);
CheckScalarTable(*retained, element, rule);
expectedValues = retained->GetValues();
expectedGradient = retained->GetGradients(0);
}
REQUIRE(retained != nullptr);
CheckMatrixExactly(retained->GetValues(), expectedValues);
CheckMatrixExactly(retained->GetGradients(0), expectedGradient);
}
TEST_CASE(
"Reference Table Cache Published RT Storage Outlives Its Cache",
tags::unit &tags::quadrature
) {
std::shared_ptr<const VectorTable> retained;
mfem::DenseMatrix firstExpected;
mfem::DenseMatrix lastExpected;
{
mfem::RT_FECollection collection(2, 3, mfem::BasisType::GaussLobatto, mfem::BasisType::IntegratedGLL);
const mfem::FiniteElement &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
const mfem::IntegrationRule rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 7));
const TableCache cache;
retained = cache.GetVectorTable(element, rule);
CheckVectorTable(*retained, element, rule);
firstExpected = retained->GetValues(0);
lastExpected = retained->GetValues(rule.GetNPoints() - 1);
}
REQUIRE(retained != nullptr);
CheckMatrixExactly(retained->GetValues(0), firstExpected);
CheckMatrixExactly(retained->GetValues(retained->GetPointCount() - 1), lastExpected);
}

View File

@@ -0,0 +1,150 @@
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <stdexcept>
import mean_field;
namespace {
using mean_field::mapping::VolumeMappingContext;
void fill_vector(
mfem::Vector &vector,
const int dimension,
const double offset
) {
vector.SetSize(dimension);
for (int i = 0; i < dimension; ++i)
vector(i) = offset + i;
}
void fill_matrix(
mfem::DenseMatrix &matrix,
const int dimension,
const double offset
) {
matrix.SetSize(dimension);
for (int j = 0; j < dimension; ++j) {
for (int i = 0; i < dimension; ++i)
matrix(i, j) = offset + 10 * j + i;
}
}
VolumeMappingContext make_context(
const int dimension,
const double offset,
const bool compactified
) {
VolumeMappingContext context;
fill_vector(context.mapping.reference_position, dimension, offset + 1);
fill_vector(context.mapping.displaced_position, dimension, offset + 2);
fill_vector(context.mapping.physical_position, dimension, offset + 3);
fill_matrix(context.mapping.displacement_jacobian, dimension, offset + 4);
fill_matrix(context.mapping.mapping_jacobian, dimension, offset + 5);
fill_matrix(context.mapping.inverse_mapping_jacobian, dimension, offset + 6);
fill_matrix(context.quadrature.J_inv, dimension, offset + 7);
context.mapping.mapping_determinant = offset + 8;
context.mapping.compactified = compactified;
context.quadrature.detJ = offset + 9;
context.quadrature.weight = offset + 10;
return context;
}
void check_vector(
const mfem::Vector &actual,
const mfem::Vector &expected
) {
REQUIRE(actual.Size() == expected.Size());
for (int i = 0; i < expected.Size(); ++i)
CHECK(actual(i) == expected(i));
}
void check_matrix(
const mfem::DenseMatrix &actual,
const mfem::DenseMatrix &expected
) {
REQUIRE(actual.Height() == expected.Height());
REQUIRE(actual.Width() == expected.Width());
for (int j = 0; j < expected.Width(); ++j) {
for (int i = 0; i < expected.Height(); ++i)
CHECK(actual(i, j) == expected(i, j));
}
}
void check_context(
const VolumeMappingContext &actual,
const VolumeMappingContext &expected
) {
check_vector(actual.mapping.reference_position, expected.mapping.reference_position);
check_vector(actual.mapping.displaced_position, expected.mapping.displaced_position);
check_vector(actual.mapping.physical_position, expected.mapping.physical_position);
check_matrix(actual.mapping.displacement_jacobian, expected.mapping.displacement_jacobian);
check_matrix(actual.mapping.mapping_jacobian, expected.mapping.mapping_jacobian);
check_matrix(actual.mapping.inverse_mapping_jacobian, expected.mapping.inverse_mapping_jacobian);
check_matrix(actual.quadrature.J_inv, expected.quadrature.J_inv);
CHECK(actual.mapping.mapping_determinant == expected.mapping.mapping_determinant);
CHECK(actual.mapping.compactified == expected.mapping.compactified);
CHECK(actual.quadrature.detJ == expected.quadrature.detJ);
CHECK(actual.quadrature.weight == expected.quadrature.weight);
}
} // namespace
TEST_CASE(
"Flat Volume Mapping Cache Preserves Every Context Field",
"[mapping][prepared-cache]"
) {
for (const int dimension : {1, 2, 3}) {
CAPTURE(dimension);
mean_field::mapping::VolumeMappingCache cache;
cache.SetSize(2, dimension);
CHECK(cache.GetPointCount() == 2);
CHECK(cache.GetDimension() == dimension);
const auto first = make_context(dimension, 0.125, false);
const auto second = make_context(dimension, -30.25, true);
cache.Store(0, first);
cache.Store(1, second);
VolumeMappingContext workspace;
cache.Load(1, workspace);
check_context(workspace, second);
const double *inverse_buffer = workspace.quadrature.J_inv.HostRead();
const double *position_buffer = workspace.mapping.physical_position.HostRead();
cache.Load(0, workspace);
check_context(workspace, first);
CHECK(workspace.quadrature.J_inv.HostRead() == inverse_buffer);
CHECK(workspace.mapping.physical_position.HostRead() == position_buffer);
mfem::DenseMatrix inverse;
cache.LoadInverseJacobian(1, inverse);
check_matrix(inverse, second.quadrature.J_inv);
const auto copy = cache;
cache.Store(1, first);
copy.Load(1, workspace);
check_context(workspace, second);
cache.Load(1, workspace);
check_context(workspace, first);
cache.SetSize(1, 3);
const auto resized = make_context(3, 13.5, true);
cache.Store(0, resized);
cache.Load(0, workspace);
check_context(workspace, resized);
}
}
TEST_CASE(
"Flat Volume Mapping Cache Rejects Invalid Indices And Dimensions",
"[mapping][prepared-cache]"
) {
mean_field::mapping::VolumeMappingCache cache;
VolumeMappingContext workspace;
CHECK_THROWS_AS(cache.SetSize(-1, 3), std::invalid_argument);
CHECK_THROWS_AS(cache.SetSize(1, 0), std::invalid_argument);
CHECK_THROWS_AS(cache.SetSize(1, 4), std::invalid_argument);
cache.SetSize(1, 3);
CHECK_THROWS_AS(cache.Load(-1, workspace), std::out_of_range);
CHECK_THROWS_AS(cache.Load(1, workspace), std::out_of_range);
CHECK_THROWS_AS(cache.Store(0, make_context(2, 0.0, false)), std::invalid_argument);
cache.SetSize(0, 2);
CHECK(cache.GetPointCount() == 0);
CHECK_THROWS_AS(cache.Load(0, workspace), std::out_of_range);
}

View File

@@ -141,55 +141,50 @@ TEST_CASE(
tags::model_specification_type_contract
) {
using namespace mean_field;
using Request = models::FixedAngularMomentumLayoutRequest;
using Form = operators::CompiledStellarEquilibriumForm<AngularMomentumPolytropicMassModel>;
using Jacobian = operators::CompiledStellarEquilibriumJacobianForm<AngularMomentumPolytropicMassModel>;
using AngularValue = utils::blocks::fixed_angular_momentum::angular_velocity::value;
using Request = models::FixedAngularMomentumLayoutRequest;
using Form = operators::CompiledStellarEquilibriumForm<AngularMomentumPolytropicMassModel>;
using Jacobian = operators::CompiledStellarEquilibriumJacobianForm<AngularMomentumPolytropicMassModel>;
using AngularValue = utils::blocks::fixed_angular_momentum::angular_velocity::value;
using AngularResidual = utils::blocks::fixed_angular_momentum::angular_velocity::residual;
STATIC_CHECK(models::ConstraintLayoutRequestType<Request>);
STATIC_CHECK(models::CompiledConstraint<models::CompiledFixedAngularMomentum>);
STATIC_CHECK(std::same_as<
typename Request::GeneratedValueType,
models::PhysicalCoordinateFor<models::FixedAngularMomentum>>);
STATIC_CHECK(std::same_as<
typename AngularValue::GeneratedType,
models::PhysicalCoordinateFor<models::FixedAngularMomentum>>);
STATIC_CHECK(std::same_as<
typename models::CompiledFixedAngularMomentum::AngularVelocityField,
field::AngularVelocity>);
STATIC_CHECK(
std::same_as<typename Request::GeneratedValueType, models::PhysicalCoordinateFor<models::FixedAngularMomentum>>
);
STATIC_CHECK(
std::same_as<typename AngularValue::GeneratedType, models::PhysicalCoordinateFor<models::FixedAngularMomentum>>
);
STATIC_CHECK(
std::same_as<typename models::CompiledFixedAngularMomentum::AngularVelocityField, field::AngularVelocity>
);
STATIC_CHECK(Form::value_block_count == 7);
STATIC_CHECK(Form::residual_block_count == 7);
STATIC_CHECK(Request::valueBlock<Form>().index == 6);
STATIC_CHECK(Request::residualBlock<Form>().index == 6);
STATIC_CHECK(utils::blocks::valid_jacobian_form<Form, Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
AngularResidual,
utils::blocks::density::mass::value,
Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
AngularResidual,
utils::blocks::surface_deformation::parameters::value,
Jacobian>);
STATIC_CHECK(
utils::blocks::has_jacobian_coupling_v<AngularResidual, utils::blocks::density::mass::value, Jacobian>
);
STATIC_CHECK(
utils::blocks::has_jacobian_coupling_v<
AngularResidual, utils::blocks::surface_deformation::parameters::value, Jacobian>
);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<AngularResidual, AngularValue, Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
utils::blocks::surface_deformation::shape_equilibrium::residual,
AngularValue,
Jacobian>);
STATIC_CHECK(utils::blocks::has_jacobian_coupling_v<
utils::blocks::enthalpy::specific::residual,
AngularValue,
Jacobian>);
STATIC_CHECK_FALSE(utils::blocks::has_jacobian_coupling_v<
utils::blocks::gravity::poisson::residual,
AngularValue,
Jacobian>);
STATIC_CHECK(
utils::blocks::has_jacobian_coupling_v<
utils::blocks::surface_deformation::shape_equilibrium::residual, AngularValue, Jacobian>
);
STATIC_CHECK(
utils::blocks::has_jacobian_coupling_v<utils::blocks::enthalpy::specific::residual, AngularValue, Jacobian>
);
STATIC_CHECK_FALSE(
utils::blocks::has_jacobian_coupling_v<utils::blocks::gravity::poisson::residual, AngularValue, Jacobian>
);
const integral::FixedAngularMomentum specification({
.Jtotal = dimensions::AngularMomentumValue{2.75},
.axis = {0.0, 3.0, 4.0},
.center = {0.25, -0.5, 0.75}
});
const integral::FixedAngularMomentum specification(
{.Jtotal = dimensions::AngularMomentumValue{2.75}, .axis = {0.0, 3.0, 4.0}, .center = {0.25, -0.5, 0.75}}
);
const models::CompiledFixedAngularMomentum compiled = models::compileConstraint(specification);
CHECK(compiled.targetAngularMomentum() == dimensions::AngularMomentumValue{2.75});
CHECK(std::abs(compiled.specification().axis()[0]) < 1.0e-15);
@@ -281,9 +276,7 @@ TEST_CASE(
) {
const mean_field::models::FixedTotalMass mass{mean_field::dimensions::MassValue{1.25}};
const mean_field::models::FixedCentralDensity centralDensity{mean_field::eos::DensityValue{2.5}};
const mean_field::models::FixedAngularMomentum angularMomentum{
mean_field::dimensions::AngularMomentumValue{0.75}
};
const mean_field::models::FixedAngularMomentum angularMomentum{mean_field::dimensions::AngularMomentumValue{0.75}};
CHECK(mass.targetMass() == mean_field::dimensions::MassValue{1.25});
CHECK(centralDensity.targetDensity() == mean_field::eos::DensityValue{2.5});
@@ -308,18 +301,17 @@ TEST_CASE(
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::models::FixedAngularMomentum({
.Jtotal = mean_field::dimensions::AngularMomentumValue{1.0},
.axis = {0.0, 0.0, 0.0}
}),
mean_field::models::FixedAngularMomentum(
{.Jtotal = mean_field::dimensions::AngularMomentumValue{1.0}, .axis = {0.0, 0.0, 0.0}}
),
std::invalid_argument
);
CHECK_THROWS_AS(
mean_field::models::FixedAngularMomentum({
.Jtotal = mean_field::dimensions::AngularMomentumValue{1.0},
.axis = {0.0, 0.0, 1.0},
.center = {0.0, std::numeric_limits<double>::quiet_NaN(), 0.0}
}),
mean_field::models::FixedAngularMomentum(
{.Jtotal = mean_field::dimensions::AngularMomentumValue{1.0},
.axis = {0.0, 0.0, 1.0},
.center = {0.0, std::numeric_limits<double>::quiet_NaN(), 0.0}}
),
std::invalid_argument
);

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