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
This commit is contained in:
@@ -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
|
||||
@@ -223,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
|
||||
@@ -279,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
|
||||
@@ -331,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
|
||||
@@ -365,10 +374,19 @@ target_link_libraries(tests PRIVATE mean_field test_mod Catch2::Catch2 Boost::bo
|
||||
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)
|
||||
|
||||
|
||||
301
experiments/GEOMETRY_QUALITY_EXPERIMENT.md
Normal file
301
experiments/GEOMETRY_QUALITY_EXPERIMENT.md
Normal 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 Gauss–Lobatto 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.
|
||||
237
experiments/GEOMETRY_QUALITY_FINDINGS_2026-09-08.md
Normal file
237
experiments/GEOMETRY_QUALITY_FINDINGS_2026-09-08.md
Normal 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.
|
||||
90
experiments/POLYTROPE_H_CONVERGENCE_2026-09-09.md
Normal file
90
experiments/POLYTROPE_H_CONVERGENCE_2026-09-09.md
Normal 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.
|
||||
377
experiments/POLYTROPE_VALIDATION.md
Normal file
377
experiments/POLYTROPE_VALIDATION.md
Normal 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 Lane–Emden 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.
|
||||
252
experiments/POLYTROPE_VALIDATION_FINDINGS_2026-09-09.md
Normal file
252
experiments/POLYTROPE_VALIDATION_FINDINGS_2026-09-09.md
Normal 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.3–1.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) |
|
||||
|
||||

|
||||
|
||||
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 Lane–Emden 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.
|
||||
482
experiments/compare_polytrope_refinement.py
Normal file
482
experiments/compare_polytrope_refinement.py
Normal 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())
|
||||
521
experiments/geometry_quality_diagnostics.hpp
Normal file
521
experiments/geometry_quality_diagnostics.hpp
Normal 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 ¢er = 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
|
||||
472
experiments/geometry_quality_experiment.cpp
Normal file
472
experiments/geometry_quality_experiment.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
155
experiments/polytrope_analytic_reference.hpp
Normal file
155
experiments/polytrope_analytic_reference.hpp
Normal 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
|
||||
221
experiments/polytrope_analytic_self_checks.hpp
Normal file
221
experiments/polytrope_analytic_self_checks.hpp
Normal 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
|
||||
395
experiments/polytrope_physical_state.hpp
Normal file
395
experiments/polytrope_physical_state.hpp
Normal 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
|
||||
435
experiments/polytrope_radial_profiles.hpp
Normal file
435
experiments/polytrope_radial_profiles.hpp
Normal 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
|
||||
445
experiments/polytrope_validation_experiment.cpp
Normal file
445
experiments/polytrope_validation_experiment.cpp
Normal 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;
|
||||
}
|
||||
339
experiments/polytrope_validation_measurements.hpp
Normal file
339
experiments/polytrope_validation_measurements.hpp
Normal 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 ¢er = 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
|
||||
96
experiments/refine_polytrope_mesh.py
Normal file
96
experiments/refine_polytrope_mesh.py
Normal 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()
|
||||
96
experiments/summarize_geometry_quality.py
Normal file
96
experiments/summarize_geometry_quality.py
Normal 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()
|
||||
287
experiments/summarize_polytrope_validation.py
Normal file
287
experiments/summarize_polytrope_validation.py
Normal 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(["", "",
|
||||
"## 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()
|
||||
@@ -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")
|
||||
|
||||
650
libmeanfield/impl/deformation/safe_newton_step.cpp
Normal file
650
libmeanfield/impl/deformation/safe_newton_step.cpp
Normal 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
|
||||
159
libmeanfield/impl/fem/reference_tables.cpp
Normal file
159
libmeanfield/impl/fem/reference_tables.cpp
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
128
libmeanfield/impl/mapping/prepared_cache.cpp
Normal file
128
libmeanfield/impl/mapping/prepared_cache.cpp
Normal 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
|
||||
@@ -311,13 +311,12 @@ 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;
|
||||
MFEM_VERIFY(
|
||||
@@ -344,6 +343,7 @@ namespace mean_field::operators {
|
||||
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);
|
||||
@@ -372,21 +372,24 @@ namespace mean_field::operators {
|
||||
.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
|
||||
);
|
||||
if (status != mapping::MappingStatus::valid) {
|
||||
return status;
|
||||
}
|
||||
if (point.mappingContext.mapping.compactified) {
|
||||
if (mappingContext.mapping.compactified) {
|
||||
return mapping::MappingStatus::at_compactified_infinity;
|
||||
}
|
||||
point.cylindricalRadiusSquared =
|
||||
CylindricalRadiusSquared(point.mappingContext.mapping.physical_position);
|
||||
if (!std::isfinite(point.cylindricalRadiusSquared)) {
|
||||
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;
|
||||
@@ -411,11 +414,9 @@ namespace mean_field::operators {
|
||||
if (!is_finite_vector(elementDensity)) {
|
||||
return false;
|
||||
}
|
||||
for (QuadraturePointData &point : data.quadraturePoints) {
|
||||
point.density = elementDensity * point.densityShape;
|
||||
if (!std::isfinite(point.density)) {
|
||||
return false;
|
||||
}
|
||||
data.densityBasis->GetValues().Mult(elementDensity, data.density);
|
||||
if (!is_finite_vector(data.density)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -424,9 +425,9 @@ namespace mean_field::operators {
|
||||
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);
|
||||
@@ -472,15 +473,18 @@ namespace mean_field::operators {
|
||||
"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;
|
||||
@@ -496,6 +500,7 @@ 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);
|
||||
@@ -515,20 +520,22 @@ namespace mean_field::operators {
|
||||
.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;
|
||||
|
||||
@@ -450,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;
|
||||
@@ -462,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;
|
||||
|
||||
@@ -485,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);
|
||||
@@ -533,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()
|
||||
);
|
||||
@@ -555,8 +560,6 @@ namespace mean_field::operators {
|
||||
|
||||
transformation->SetIntPoint(&integrationPoint);
|
||||
|
||||
mapping::VolumeMappingContext mappingContext;
|
||||
|
||||
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
|
||||
mappingData, *transformation, integrationPoint, workspace, mappingContext
|
||||
);
|
||||
@@ -585,8 +588,8 @@ namespace mean_field::operators {
|
||||
}
|
||||
}
|
||||
|
||||
densityElement.CalcShape(integrationPoint, densityShape);
|
||||
enthalpyElement.CalcShape(integrationPoint, enthalpyShape);
|
||||
data.densityBasis->GetValues().GetRow(quadraturePoint, densityShape);
|
||||
data.enthalpyBasis->GetValues().GetRow(quadraturePoint, enthalpyShape);
|
||||
|
||||
if (!vector_is_finite(densityShape) || !vector_is_finite(enthalpyShape)) {
|
||||
retain_higher_priority_rejection(
|
||||
@@ -595,13 +598,6 @@ namespace mean_field::operators {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const double density = elementBaseDensity * densityShape;
|
||||
const double enthalpy = elementBaseEnthalpy * enthalpyShape;
|
||||
const double quadratureWeight = mappingContext.quadrature.weight;
|
||||
@@ -665,6 +661,7 @@ namespace mean_field::operators {
|
||||
data.weightedEnthalpyDerivative(quadraturePoint) = weightedEnthalpyDerivative;
|
||||
}
|
||||
}
|
||||
m_elements.resize(preparedElementCount);
|
||||
|
||||
if (auto globalRejection = synchronize_rejection(localRejection, m_fem.densityFes->GetComm());
|
||||
globalRejection.has_value()) {
|
||||
@@ -689,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);
|
||||
@@ -719,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;
|
||||
}
|
||||
}
|
||||
@@ -842,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) =
|
||||
@@ -852,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);
|
||||
@@ -906,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) {
|
||||
@@ -933,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);
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
@@ -249,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);
|
||||
@@ -282,13 +294,17 @@ namespace mean_field::operators {
|
||||
"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) {
|
||||
@@ -296,6 +312,10 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +328,9 @@ namespace mean_field::operators {
|
||||
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))) {
|
||||
!std::isfinite(data.inverseMeshJacobians(quadraturePoint, entry)) ||
|
||||
(data.gravityReferenceTable != nullptr &&
|
||||
!std::isfinite(data.meshPiolaJacobians(quadraturePoint, entry)))) {
|
||||
return std::unexpected(non_finite_rejection());
|
||||
}
|
||||
}
|
||||
@@ -462,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);
|
||||
@@ -490,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);
|
||||
@@ -506,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);
|
||||
@@ -523,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);
|
||||
|
||||
@@ -182,19 +182,17 @@ 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) {
|
||||
m_mappingFailure = status;
|
||||
return 0.0;
|
||||
}
|
||||
const double mapping_determinant = mapping_context.mapping.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;
|
||||
|
||||
const double value = 4.0 * std::numbers::pi * mean_field::utils::G * mapping_determinant;
|
||||
if (!std::isfinite(value)) {
|
||||
@@ -269,6 +267,7 @@ 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};
|
||||
@@ -394,8 +393,8 @@ namespace mean_field::operators {
|
||||
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;
|
||||
@@ -407,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;
|
||||
|
||||
@@ -438,37 +439,59 @@ 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);
|
||||
@@ -505,6 +528,7 @@ namespace mean_field::operators {
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_elements.resize(prepared_element_count);
|
||||
|
||||
const bool localNonFiniteArithmetic = source_coefficient.HasNonFiniteArithmetic() || localNonFiniteQuadrature;
|
||||
auto preparationResult = synchronize_preparation_failure(
|
||||
@@ -555,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) {
|
||||
@@ -565,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);
|
||||
@@ -641,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) {
|
||||
@@ -669,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);
|
||||
@@ -714,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);
|
||||
@@ -722,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);
|
||||
|
||||
@@ -13,6 +13,8 @@ module;
|
||||
#include <mpi.h>
|
||||
|
||||
module mean_field;
|
||||
|
||||
import :fem.reference_tables;
|
||||
import :operators.prepared_hdiv_mass;
|
||||
|
||||
namespace {
|
||||
@@ -551,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())
|
||||
);
|
||||
@@ -573,6 +584,25 @@ namespace mean_field::operators {
|
||||
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;
|
||||
@@ -830,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);
|
||||
|
||||
@@ -853,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) {
|
||||
|
||||
@@ -488,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);
|
||||
|
||||
@@ -528,34 +525,10 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +538,7 @@ namespace mean_field::operators {
|
||||
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;
|
||||
|
||||
@@ -615,16 +589,14 @@ 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
|
||||
);
|
||||
|
||||
@@ -641,6 +613,7 @@ namespace mean_field::operators {
|
||||
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) {
|
||||
@@ -660,15 +633,17 @@ namespace mean_field::operators {
|
||||
|
||||
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."
|
||||
);
|
||||
@@ -687,18 +662,18 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,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 "
|
||||
@@ -836,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,
|
||||
@@ -855,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);
|
||||
@@ -1072,7 +1048,7 @@ namespace mean_field::operators {
|
||||
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);
|
||||
}
|
||||
@@ -1204,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) {
|
||||
@@ -1239,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 &&
|
||||
@@ -1252,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(
|
||||
@@ -1288,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);
|
||||
|
||||
@@ -420,17 +420,12 @@ 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;
|
||||
@@ -459,6 +454,7 @@ 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) {
|
||||
@@ -491,9 +487,10 @@ 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(
|
||||
@@ -505,6 +502,9 @@ namespace mean_field::operators {
|
||||
rejection, {.reason = MassNormalizationPreparationRejectionReason::mapping_failure,
|
||||
.mappingStatus = status}
|
||||
);
|
||||
} else {
|
||||
data.mappingContexts.Store(quadraturePoint, mappingContext);
|
||||
data.quadratureWeights(quadraturePoint) = mappingContext.quadrature.weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,9 +537,9 @@ namespace mean_field::operators {
|
||||
data.densityDofTransformation->InvTransformPrimal(elementDensity);
|
||||
}
|
||||
|
||||
for (QuadraturePointData &point : data.quadraturePoints) {
|
||||
point.density = elementDensity * point.densityShape;
|
||||
if (!std::isfinite(point.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}
|
||||
@@ -556,8 +556,8 @@ namespace mean_field::operators {
|
||||
std::optional<MassNormalizationPreparationRejection> localRejection;
|
||||
|
||||
for (const ElementPAData &data : m_elements) {
|
||||
for (const QuadraturePointData &point : data.quadraturePoints) {
|
||||
const double contribution = 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;
|
||||
@@ -615,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) {
|
||||
@@ -624,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,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;
|
||||
@@ -681,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(
|
||||
@@ -694,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,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);
|
||||
@@ -821,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;
|
||||
|
||||
@@ -852,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(
|
||||
@@ -864,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;
|
||||
|
||||
@@ -492,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);
|
||||
|
||||
@@ -559,29 +556,11 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,6 +570,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::Vector elementDisplacement;
|
||||
mfem::Vector elementCompactification;
|
||||
@@ -634,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);
|
||||
|
||||
@@ -650,9 +631,7 @@ 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
|
||||
);
|
||||
|
||||
@@ -669,11 +648,13 @@ namespace mean_field::operators {
|
||||
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() &&
|
||||
@@ -714,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);
|
||||
|
||||
@@ -734,7 +717,7 @@ namespace mean_field::operators {
|
||||
|
||||
quadratureEnthalpy.SetSize(quadraturePointCount);
|
||||
|
||||
data.enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy);
|
||||
enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy);
|
||||
|
||||
data.pressure.SetSize(quadraturePointCount);
|
||||
|
||||
@@ -814,8 +797,8 @@ namespace mean_field::operators {
|
||||
data.elementResidual(vectorDof) -= residualContribution;
|
||||
|
||||
for (int enthalpyDof = 0; enthalpyDof < enthalpyDofCount; ++enthalpyDof) {
|
||||
const double jacobianContribution = pressureDerivative * weightedTestGradient *
|
||||
data.enthalpyBasis(quadraturePoint, enthalpyDof);
|
||||
const double jacobianContribution =
|
||||
pressureDerivative * weightedTestGradient * enthalpyBasis(quadraturePoint, enthalpyDof);
|
||||
if (!std::isfinite(jacobianContribution)) {
|
||||
materialFailure = non_finite_rejection();
|
||||
continue;
|
||||
@@ -841,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,
|
||||
@@ -852,7 +837,7 @@ 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."
|
||||
@@ -978,6 +963,7 @@ namespace mean_field::operators {
|
||||
mfem::Vector elementAction;
|
||||
|
||||
mfem::DenseMatrix referenceDisplacementJacobian;
|
||||
mfem::DenseMatrix inverseElementJacobian;
|
||||
mfem::DenseMatrix inverseElementJacobianVariation;
|
||||
mfem::DenseMatrix matrixTemporary;
|
||||
mfem::DenseMatrix physicalTestGradientVariation;
|
||||
@@ -1016,7 +1002,7 @@ namespace mean_field::operators {
|
||||
);
|
||||
|
||||
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."
|
||||
@@ -1032,12 +1018,12 @@ namespace mean_field::operators {
|
||||
physicalTestGradientVariation.SetSize(scalarDisplacementDofCount, dimension);
|
||||
|
||||
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
|
||||
const mfem::DenseMatrix &referenceTestGradient = data.referenceTestGradients[quadraturePoint];
|
||||
const mfem::DenseMatrix &referenceTestGradient =
|
||||
data.displacementReferenceTable->GetGradients(quadraturePoint);
|
||||
|
||||
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};
|
||||
|
||||
@@ -1068,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;
|
||||
|
||||
82
libmeanfield/interface/deformation/safe_newton_step.cppm
Normal file
82
libmeanfield/interface/deformation/safe_newton_step.cppm
Normal 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
|
||||
@@ -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(
|
||||
|
||||
78
libmeanfield/interface/fem/reference_tables.cppm
Normal file
78
libmeanfield/interface/fem/reference_tables.cppm
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
42
libmeanfield/interface/mapping/prepared_cache.cppm
Normal file
42
libmeanfield/interface/mapping/prepared_cache.cppm
Normal 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
|
||||
@@ -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;
|
||||
@@ -86,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;
|
||||
|
||||
@@ -4,6 +4,7 @@ module;
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
@@ -14,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;
|
||||
|
||||
@@ -188,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;
|
||||
@@ -205,9 +205,14 @@ 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();
|
||||
|
||||
@@ -2,6 +2,7 @@ module;
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <mfem.hpp>
|
||||
#include <vector>
|
||||
|
||||
@@ -97,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;
|
||||
|
||||
@@ -132,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;
|
||||
@@ -169,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};
|
||||
|
||||
@@ -3,6 +3,7 @@ module;
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <mfem.hpp>
|
||||
@@ -14,6 +15,7 @@ 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 {
|
||||
@@ -111,6 +113,12 @@ 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;
|
||||
[[nodiscard]] std::expected<
|
||||
@@ -133,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;
|
||||
@@ -164,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};
|
||||
|
||||
@@ -58,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
|
||||
@@ -80,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;
|
||||
@@ -87,6 +97,14 @@ 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;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] GravitySourcePreparationResult TryPrepareImpl(
|
||||
@@ -119,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;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 };
|
||||
@@ -58,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 };
|
||||
|
||||
@@ -71,6 +78,10 @@ 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;
|
||||
};
|
||||
|
||||
@@ -112,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};
|
||||
|
||||
@@ -4,6 +4,7 @@ module;
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
@@ -13,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;
|
||||
|
||||
@@ -216,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};
|
||||
@@ -232,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;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ module;
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <mfem.hpp>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
@@ -11,6 +12,7 @@ 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;
|
||||
@@ -187,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};
|
||||
|
||||
@@ -206,10 +207,15 @@ 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();
|
||||
|
||||
@@ -4,6 +4,7 @@ module;
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,8 +14,10 @@ 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;
|
||||
|
||||
@@ -168,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;
|
||||
|
||||
@@ -196,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:
|
||||
@@ -208,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;
|
||||
|
||||
|
||||
@@ -113,6 +113,12 @@ 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;
|
||||
[[nodiscard]] std::expected<
|
||||
|
||||
@@ -269,6 +269,10 @@ export namespace mean_field::operators {
|
||||
[[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;
|
||||
|
||||
|
||||
@@ -2785,6 +2785,22 @@ export namespace mean_field::operators {
|
||||
return *m_physical;
|
||||
}
|
||||
|
||||
void BuildVolumeDisplacementDirection(
|
||||
const mfem::Vector &stateDirection,
|
||||
mfem::Vector &volumeDisplacementDirection
|
||||
) const {
|
||||
if (stateDirection.Size() != Width()) {
|
||||
throw std::invalid_argument(
|
||||
"The prepared stellar-equilibrium root received a state direction with the wrong size."
|
||||
);
|
||||
}
|
||||
const auto rootDirection = m_manifest.directionView(stateDirection);
|
||||
m_physical->BuildVolumeDisplacementDirection(
|
||||
rootDirection.block(utils::blocks::surface_deformation_field.parameters_term),
|
||||
volumeDisplacementDirection
|
||||
);
|
||||
}
|
||||
|
||||
template <models::ModelSpecification Specification>
|
||||
requires ModelType::template
|
||||
containsSpecification<Specification> [[nodiscard]] const auto &GetPreparedContribution() const noexcept {
|
||||
|
||||
@@ -298,6 +298,13 @@ 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<
|
||||
|
||||
@@ -381,7 +381,7 @@ export namespace mean_field::solver {
|
||||
export namespace mean_field::solver::linear {
|
||||
struct FGMRESOptions final {
|
||||
int restartLength{50};
|
||||
int printLevel{-1};
|
||||
int printLevel{1};
|
||||
|
||||
void Validate() const {
|
||||
if (restartLength <= 0) {
|
||||
|
||||
@@ -17,6 +17,7 @@ module;
|
||||
|
||||
export module mean_field:solver.newton;
|
||||
|
||||
export import :deformation.safe_newton_step;
|
||||
export import :solver.linear_backend;
|
||||
|
||||
export namespace mean_field::solver::nonlinear {
|
||||
@@ -264,11 +265,13 @@ export namespace mean_field::solver::nonlinear {
|
||||
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{};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
@@ -8,6 +9,7 @@ module;
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -17,6 +19,7 @@ module;
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
@@ -37,6 +40,11 @@ export namespace mean_field::solver {
|
||||
template <typename Context, typename NewtonConfiguration, typename Observer> class StellarEquilibriumSolver;
|
||||
} // namespace mean_field::solver
|
||||
|
||||
export namespace mean_field::solver::detail {
|
||||
// Internal, synchronous experiment access; not a stable solver API.
|
||||
struct StellarEquilibriumContextDiagnostics;
|
||||
}
|
||||
|
||||
namespace mean_field::solver::detail {
|
||||
template <typename Model, typename Discretization>
|
||||
using StellarContextProblem =
|
||||
@@ -256,6 +264,7 @@ export namespace mean_field::solver {
|
||||
private:
|
||||
template <typename, typename, typename> friend class StellarEquilibriumSolver;
|
||||
friend struct detail::StellarEquilibriumContextAssembly;
|
||||
friend struct detail::StellarEquilibriumContextDiagnostics;
|
||||
|
||||
using NormalizedOperatorType = detail::StellarContextNormalizedOperator<ProblemType>;
|
||||
using PhysicalInverseType = detail::StellarContextPhysicalInverse<PreconditionerPrescriptionType, ProblemType>;
|
||||
@@ -323,11 +332,16 @@ export namespace mean_field::solver {
|
||||
trialNormalizedResidual(RequireProblem(storage).EquationSize()),
|
||||
linearRightHandSide(RequireProblem(storage).EquationSize()),
|
||||
normalizedCorrection(RequireProblem(storage).StateSize()),
|
||||
physicalCorrection(RequireProblem(storage).StateSize()),
|
||||
volumeDisplacementDirection(
|
||||
RequireProblem(storage).GetPhysicalOperator().GetDomainDeformation().volumeDisplacementSize()
|
||||
),
|
||||
candidatePhysicalState(RequireProblem(storage).StateSize()),
|
||||
normalizedOperator(std::make_unique<NormalizedOperatorType>(RequireProblem(storage))) {
|
||||
ValidateInitialState();
|
||||
InitializeWorkspaces();
|
||||
PrepareInitialOperator();
|
||||
InitializeGeometryPreflightRules();
|
||||
|
||||
physicalInverse = std::unique_ptr<PhysicalInverseType>{new PhysicalInverseType(
|
||||
PreparePhysicalInverse(std::move(preconditionerPrescription), RequireProblem(storage))
|
||||
@@ -375,6 +389,9 @@ export namespace mean_field::solver {
|
||||
trialNormalizedResidual.Size() == problem->EquationSize() &&
|
||||
linearRightHandSide.Size() == problem->EquationSize() &&
|
||||
normalizedCorrection.Size() == problem->StateSize() &&
|
||||
physicalCorrection.Size() == problem->StateSize() &&
|
||||
volumeDisplacementDirection.Size() ==
|
||||
problem->GetPhysicalOperator().GetDomainDeformation().volumeDisplacementSize() &&
|
||||
candidatePhysicalState.Size() == problem->StateSize() &&
|
||||
storage->physicalState->Size() == problem->StateSize();
|
||||
}
|
||||
@@ -464,6 +481,26 @@ export namespace mean_field::solver {
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] deformation::LargestSafeNewtonStepSizeEstimate EstimateLargestSafeStepSize(
|
||||
const double maximumStepSize,
|
||||
const double fractionToBoundarySafety
|
||||
) {
|
||||
normalizedOperator->DenormalizeState(normalizedCorrection, physicalCorrection);
|
||||
const auto &physicalOperator = Problem().GetPhysicalOperator();
|
||||
Problem().BuildVolumeDisplacementDirection(physicalCorrection, volumeDisplacementDirection);
|
||||
|
||||
const fem::FEM &finiteElements =
|
||||
equilibrium::detail::StellarEquilibriumProblemFactory::FiniteElementModel(Problem());
|
||||
return deformation::estimate_largest_safe_newton_step_size(
|
||||
Problem().GetDiscretization().domainMapper(), *finiteElements.displacementFes,
|
||||
*finiteElements.compactificationCoordinate, physicalOperator.GetGeneratedVolumeDisplacement(),
|
||||
volumeDisplacementDirection, geometryPreflightRules,
|
||||
{.maximumStepSize = maximumStepSize,
|
||||
.determinantFloor = 0.0,
|
||||
.fractionToBoundarySafety = fractionToBoundarySafety}
|
||||
);
|
||||
}
|
||||
|
||||
std::shared_ptr<Storage> storage;
|
||||
DependencyLedger dependencyLedger;
|
||||
mfem::Vector acceptedNormalizedState;
|
||||
@@ -472,7 +509,10 @@ export namespace mean_field::solver {
|
||||
mfem::Vector trialNormalizedResidual;
|
||||
mfem::Vector linearRightHandSide;
|
||||
mfem::Vector normalizedCorrection;
|
||||
mfem::Vector physicalCorrection;
|
||||
mfem::Vector volumeDisplacementDirection;
|
||||
mfem::Vector candidatePhysicalState;
|
||||
std::vector<deformation::NewtonStepGeometryRule> geometryPreflightRules;
|
||||
double acceptedMinimumJacobianDeterminant{std::numeric_limits<double>::quiet_NaN()};
|
||||
std::unique_ptr<NormalizedOperatorType> normalizedOperator;
|
||||
std::unique_ptr<PhysicalInverseType> physicalInverse;
|
||||
@@ -565,12 +605,14 @@ export namespace mean_field::solver {
|
||||
|
||||
void InitializeWorkspaces() {
|
||||
normalizedOperator->NormalizeState(AcceptedPhysicalState(), acceptedNormalizedState);
|
||||
trialNormalizedState = acceptedNormalizedState;
|
||||
acceptedNormalizedResidual = 0.0;
|
||||
trialNormalizedResidual = 0.0;
|
||||
linearRightHandSide = 0.0;
|
||||
normalizedCorrection = 0.0;
|
||||
candidatePhysicalState = AcceptedPhysicalState();
|
||||
trialNormalizedState = acceptedNormalizedState;
|
||||
acceptedNormalizedResidual = 0.0;
|
||||
trialNormalizedResidual = 0.0;
|
||||
linearRightHandSide = 0.0;
|
||||
normalizedCorrection = 0.0;
|
||||
physicalCorrection = 0.0;
|
||||
volumeDisplacementDirection = 0.0;
|
||||
candidatePhysicalState = AcceptedPhysicalState();
|
||||
}
|
||||
|
||||
void PrepareInitialOperator() {
|
||||
@@ -582,6 +624,97 @@ export namespace mean_field::solver {
|
||||
trialNormalizedResidual = acceptedNormalizedResidual;
|
||||
}
|
||||
|
||||
void AppendGeometryPreflightRule(
|
||||
const int element,
|
||||
const mfem::IntegrationRule &integrationRule
|
||||
) {
|
||||
geometryPreflightRules.push_back({.element = element, .integrationRule = &integrationRule});
|
||||
}
|
||||
|
||||
void InitializeGeometryPreflightRules() {
|
||||
const ProblemType &problem = Problem();
|
||||
const fem::FEM &finiteElements =
|
||||
equilibrium::detail::StellarEquilibriumProblemFactory::FiniteElementModel(problem);
|
||||
if (finiteElements.mesh == nullptr || finiteElements.displacementFes == nullptr ||
|
||||
finiteElements.compactificationCoordinate == nullptr) {
|
||||
throw std::logic_error(
|
||||
"The Newton geometry preflight requires complete displacement geometry data."
|
||||
);
|
||||
}
|
||||
if (!problem.GetPhysicalOperator().GetDomainDeformation().descriptor().linearOnReferenceGeometry) {
|
||||
throw std::invalid_argument(
|
||||
"The Newton geometry preflight requires a domain deformation that is linear on the "
|
||||
"reference geometry."
|
||||
);
|
||||
}
|
||||
|
||||
geometryPreflightRules.clear();
|
||||
geometryPreflightRules.reserve(
|
||||
static_cast<std::size_t>(finiteElements.mesh->GetNE()) * static_cast<std::size_t>(10)
|
||||
);
|
||||
|
||||
const int dimension = problem.GetDiscretization().domainMapper().GetDimension();
|
||||
for (int element = 0; element < finiteElements.mesh->GetNE(); ++element) {
|
||||
const mfem::FiniteElement *finiteElement = finiteElements.displacementFes->GetFE(element);
|
||||
mfem::ElementTransformation *transformation =
|
||||
finiteElements.mesh->GetElementTransformation(element);
|
||||
if (finiteElement == nullptr || transformation == nullptr) {
|
||||
throw std::logic_error(
|
||||
"The Newton geometry preflight encountered incomplete element geometry data."
|
||||
);
|
||||
}
|
||||
const int geometryInspectionOrder =
|
||||
std::max(finiteElement->GetOrder() + 2, 2 * dimension * finiteElement->GetOrder());
|
||||
AppendGeometryPreflightRule(
|
||||
element, mfem::IntRules.Get(transformation->GetGeometryType(), geometryInspectionOrder)
|
||||
);
|
||||
}
|
||||
|
||||
const auto appendPreparedRules = [this](const auto &preparedOperator) {
|
||||
preparedOperator.VisitMappedGeometryRules(
|
||||
[this](const int element, const mfem::IntegrationRule &integrationRule) {
|
||||
AppendGeometryPreflightRule(element, integrationRule);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const auto &physicalOperator = problem.GetPhysicalOperator();
|
||||
const auto &gravityGeometry = physicalOperator.GetGravityContext().GetGeometryContext();
|
||||
appendPreparedRules(gravityGeometry.GetMassOperator());
|
||||
appendPreparedRules(gravityGeometry.GetSourceOperator());
|
||||
appendPreparedRules(physicalOperator.GetBarotropicClosureOperator());
|
||||
appendPreparedRules(physicalOperator.GetHydrostaticOperator());
|
||||
|
||||
const auto &displacementOperator = physicalOperator.GetDisplacementOperator();
|
||||
appendPreparedRules(displacementOperator.GetPressureOperator());
|
||||
appendPreparedRules(displacementOperator.GetGravityOperator());
|
||||
appendPreparedRules(displacementOperator.GetRotationalOperator());
|
||||
appendPreparedRules(physicalOperator.GetMassNormalizationOperator());
|
||||
|
||||
if constexpr (ProblemType::hasFixedAngularMomentum) {
|
||||
appendPreparedRules(problem.GetPreparedOperator().GetAngularMomentumConstraint());
|
||||
}
|
||||
|
||||
const auto ruleLess = [](const deformation::NewtonStepGeometryRule &left,
|
||||
const deformation::NewtonStepGeometryRule &right) {
|
||||
if (left.element != right.element) {
|
||||
return left.element < right.element;
|
||||
}
|
||||
return std::less<const mfem::IntegrationRule *>{}(left.integrationRule, right.integrationRule);
|
||||
};
|
||||
std::sort(geometryPreflightRules.begin(), geometryPreflightRules.end(), ruleLess);
|
||||
geometryPreflightRules.erase(
|
||||
std::unique(
|
||||
geometryPreflightRules.begin(), geometryPreflightRules.end(),
|
||||
[](const deformation::NewtonStepGeometryRule &left,
|
||||
const deformation::NewtonStepGeometryRule &right) {
|
||||
return left.element == right.element && left.integrationRule == right.integrationRule;
|
||||
}
|
||||
),
|
||||
geometryPreflightRules.end()
|
||||
);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto PrepareOperator(const mfem::Vector &normalizedState) {
|
||||
if constexpr (ProblemType::generatedRotationProviderCount == 0) {
|
||||
return normalizedOperator->Prepare(
|
||||
@@ -695,6 +828,42 @@ export namespace mean_field::solver {
|
||||
concept StellarEquilibriumContextType = IsStellarEquilibriumContext<std::remove_cvref_t<Candidate>>::value;
|
||||
} // namespace mean_field::solver
|
||||
|
||||
export namespace mean_field::solver::detail {
|
||||
struct StellarEquilibriumContextDiagnostics final {
|
||||
// The callback must not retain references to runtime storage. It may
|
||||
// prepare trial states, but accepted vectors must remain unchanged.
|
||||
// Restore the production preparation and correction on every exit.
|
||||
template <typename Context, typename Callback>
|
||||
static void WithState(Context &context, Callback &&callback) {
|
||||
if (context.hasActiveSolver() || !context.isReady()) {
|
||||
throw std::logic_error("Diagnostics require a ready context with no active solver.");
|
||||
}
|
||||
auto &state = *context.m_state;
|
||||
mfem::Vector savedCorrection(state.normalizedCorrection);
|
||||
context.AcquireSolver();
|
||||
try {
|
||||
state.BeginEvaluation();
|
||||
std::invoke(std::forward<Callback>(callback), state,
|
||||
equilibrium::detail::StellarEquilibriumProblemFactory::FiniteElementModel(state.Problem()));
|
||||
state.RestoreAccepted();
|
||||
state.normalizedCorrection = savedCorrection;
|
||||
context.ReleaseSolver();
|
||||
} catch (...) {
|
||||
const auto original = std::current_exception();
|
||||
try {
|
||||
state.RestoreAccepted();
|
||||
state.normalizedCorrection = savedCorrection;
|
||||
} catch (...) {
|
||||
context.ReleaseSolver();
|
||||
throw;
|
||||
}
|
||||
context.ReleaseSolver();
|
||||
std::rethrow_exception(original);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
namespace mean_field::solver::detail {
|
||||
[[nodiscard]] inline physics::RigidRotation ZeroRigidRotation() {
|
||||
mfem::Vector angularVelocity(3);
|
||||
@@ -973,12 +1142,14 @@ export namespace mean_field::solver {
|
||||
|
||||
struct IterationTimings final {
|
||||
Clock::time_point start{};
|
||||
double geometryPreflightSeconds{0.0};
|
||||
double lineSearchSeconds{0.0};
|
||||
double trialPreparationSeconds{0.0};
|
||||
double metricEvaluationSeconds{0.0};
|
||||
double preconditionerRefreshSeconds{0.0};
|
||||
double rollbackSeconds{0.0};
|
||||
double observerSeconds{0.0};
|
||||
std::optional<deformation::LargestSafeNewtonStepSizeEstimate> geometryPreflight;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -1100,14 +1271,39 @@ export namespace mean_field::solver {
|
||||
}
|
||||
|
||||
const nonlinear::MetricEvaluation previousMetric = acceptedMetric;
|
||||
bool accepted = false;
|
||||
double acceptedStepLength = 0.0;
|
||||
int lineSearchTrials = 0;
|
||||
const Clock::time_point geometryPreflightStart = Clock::now();
|
||||
timings.geometryPreflight = state.EstimateLargestSafeStepSize(
|
||||
nextLineSearchStepLength, options.backtracking.fractionToBoundarySafety
|
||||
);
|
||||
timings.geometryPreflightSeconds =
|
||||
std::chrono::duration<double>(Clock::now() - geometryPreflightStart).count();
|
||||
diagnostics.totalGeometryPreflightSeconds += timings.geometryPreflightSeconds;
|
||||
diagnostics.lastGeometryPreflight = timings.geometryPreflight;
|
||||
if (timings.geometryPreflight->limitedByGeometry) {
|
||||
++diagnostics.geometryLimitedIterations;
|
||||
}
|
||||
|
||||
if (timings.geometryPreflight->stepSize < options.backtracking.minimumStepLength) {
|
||||
diagnostics.finalResidualNorm = acceptedMetric.residualNorm;
|
||||
NotifyAfter(
|
||||
iteration, nonlinear::IterationDisposition::globalization_failure, false, 0.0, 0,
|
||||
diagnostics.initialResidualNorm, previousMetric, acceptedMetric, linearReport, timings, state
|
||||
);
|
||||
return Failure(
|
||||
state, std::move(diagnostics), StellarEquilibriumFailureReason::globalization_failure,
|
||||
"The geometry preflight found no orientation-preserving Newton step at or above the "
|
||||
"configured minimum step length."
|
||||
);
|
||||
}
|
||||
|
||||
bool accepted = false;
|
||||
double acceptedStepLength = 0.0;
|
||||
int lineSearchTrials = 0;
|
||||
nonlinear::MetricEvaluation trialMetric{};
|
||||
StellarEquilibriumFailureReason rejectionReason =
|
||||
StellarEquilibriumFailureReason::globalization_failure;
|
||||
std::string rejectionMessage = "The backtracking line search found no acceptable Newton step.";
|
||||
double stepLength = nextLineSearchStepLength;
|
||||
double stepLength = timings.geometryPreflight->stepSize;
|
||||
|
||||
const Clock::time_point lineSearchStart = Clock::now();
|
||||
try {
|
||||
@@ -1553,11 +1749,13 @@ export namespace mean_field::solver {
|
||||
.relativeResidualNorm = RelativeResidual(metric.residualNorm, initialResidualNorm),
|
||||
.merit = metric.merit,
|
||||
.iterationSeconds = DurationExcludingObserver(timings.start, timings.observerSeconds),
|
||||
.geometryPreflightSeconds = timings.geometryPreflightSeconds,
|
||||
.lineSearchSeconds = timings.lineSearchSeconds,
|
||||
.trialPreparationSeconds = timings.trialPreparationSeconds,
|
||||
.metricEvaluationSeconds = timings.metricEvaluationSeconds,
|
||||
.preconditionerRefreshSeconds = timings.preconditionerRefreshSeconds,
|
||||
.rollbackSeconds = timings.rollbackSeconds,
|
||||
.geometryPreflight = timings.geometryPreflight,
|
||||
.linearSolve = linearReport,
|
||||
.communicator = state.Problem().GetCommunicator(),
|
||||
.physicalState = detail::ReadOnlySpan(state.AcceptedPhysicalState()),
|
||||
|
||||
@@ -6,6 +6,7 @@ module;
|
||||
|
||||
export module mean_field:solver.stellar_equilibrium_types;
|
||||
|
||||
export import :deformation.safe_newton_step;
|
||||
export import :solver.linear_backend;
|
||||
|
||||
export namespace mean_field::solver {
|
||||
@@ -46,15 +47,18 @@ export namespace mean_field::solver {
|
||||
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
|
||||
|
||||
21
sandbox.cpp
21
sandbox.cpp
@@ -77,10 +77,11 @@ int main(
|
||||
|
||||
constexpr double radius = utils::RADIUS;
|
||||
constexpr double mass = utils::MASS;
|
||||
constexpr double angularMomentum = 0.05;
|
||||
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);
|
||||
|
||||
@@ -105,7 +106,7 @@ int main(
|
||||
);
|
||||
|
||||
auto preconditioner = preconditioning::makePreconditioner();
|
||||
auto linearSolver = solver::linear::FGMRES({.restartLength = 40, .printLevel = -1});
|
||||
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)
|
||||
@@ -149,6 +150,18 @@ int main(
|
||||
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
|
||||
@@ -200,11 +213,13 @@ int main(
|
||||
<< " 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, inadmissible trials = " << diagnostics.inadmissibleLineSearchTrials
|
||||
<< " s, geometry-limited iterations = " << diagnostics.geometryLimitedIterations
|
||||
<< ", inadmissible trials = " << diagnostics.inadmissibleLineSearchTrials
|
||||
<< ", non-finite trials = " << diagnostics.nonFiniteLineSearchTrials
|
||||
<< ", insufficient-decrease trials = " << diagnostics.insufficientDecreaseTrials << '\n';
|
||||
}
|
||||
|
||||
298081
sandbox.smesh
298081
sandbox.smesh
File diff suppressed because it is too large
Load Diff
257
tests/deformation/safe_newton_step.cpp
Normal file
257
tests/deformation/safe_newton_step.cpp
Normal 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
|
||||
);
|
||||
}
|
||||
262
tests/fem/reference_tables.cpp
Normal file
262
tests/fem/reference_tables.cpp
Normal 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);
|
||||
}
|
||||
150
tests/mapping/prepared_cache.cpp
Normal file
150
tests/mapping/prepared_cache.cpp
Normal 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);
|
||||
}
|
||||
@@ -60,6 +60,67 @@ TEST_CASE(
|
||||
CHECK(preparedOperator.GetPreparationCount() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Prepared Mapped Gravity Source Reuses Tables Across Preparation Modes And Rejection",
|
||||
tags::gravity_prepared_unit &tags::geometry
|
||||
) {
|
||||
auto args = test_utils::setup_args();
|
||||
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(f.okay());
|
||||
|
||||
operators::PreparedMappedGravitySourceOperator operation(f, *f.domainMapperStateless);
|
||||
const mfem::Vector densityTrue = prepared_test::make_deterministic_vector(f.densityFes->GetTrueVSize(), 0.41);
|
||||
const mfem::Vector density = operation.GetDensityMap().gather(densityTrue);
|
||||
const mfem::Vector displacementTrue = prepared_test::make_displacement(f, 0.4);
|
||||
const mfem::Vector displacement = operation.GetDisplacementMap().gather(displacementTrue);
|
||||
const mfem::Vector directionTrue = prepared_test::make_displacement(f, 0.7);
|
||||
const MPI_Comm communicator = f.mesh->GetComm();
|
||||
|
||||
operation.Prepare(displacement);
|
||||
mfem::Vector baselineAction;
|
||||
mfem::Vector baselineVariation;
|
||||
operation.Mult(density, baselineAction);
|
||||
operation.MultDisplacementVariationTrue(densityTrue, directionTrue, baselineVariation);
|
||||
|
||||
const mfem::Vector primalDisplacementTrue = prepared_test::make_displacement(f, 1.0);
|
||||
operation.PreparePrimal(operation.GetDisplacementMap().gather(primalDisplacementTrue));
|
||||
REQUIRE(operation.IsPrepared());
|
||||
CHECK_FALSE(operation.HasVariationData());
|
||||
mfem::Vector primalAction;
|
||||
mfem::Vector referenceActionTrue;
|
||||
operation.Mult(density, primalAction);
|
||||
operators::kernels::apply_mapped_source(
|
||||
f, *f.domainMapperStateless, densityTrue, primalDisplacementTrue, referenceActionTrue
|
||||
);
|
||||
CHECK_THAT(
|
||||
prepared_test::relative_error(
|
||||
primalAction, operation.GetPotentialMap().gather(referenceActionTrue), communicator
|
||||
),
|
||||
WithinAbs(0.0, 2.0e-11)
|
||||
);
|
||||
|
||||
operation.Prepare(displacement);
|
||||
REQUIRE(operation.HasVariationData());
|
||||
mfem::Vector repeatedAction;
|
||||
mfem::Vector repeatedVariation;
|
||||
operation.Mult(density, repeatedAction);
|
||||
operation.MultDisplacementVariationTrue(densityTrue, directionTrue, repeatedVariation);
|
||||
CHECK(prepared_test::relative_error(repeatedAction, baselineAction, communicator) < 2.0e-14);
|
||||
CHECK(prepared_test::relative_error(repeatedVariation, baselineVariation, communicator) < 2.0e-14);
|
||||
|
||||
const auto rejected = operation.TryPrepare(operation.GetDisplacementMap().gather(make_folding_displacement(f)));
|
||||
REQUIRE_FALSE(rejected.has_value());
|
||||
CHECK_FALSE(operation.IsPrepared());
|
||||
CHECK_FALSE(operation.HasVariationData());
|
||||
REQUIRE(operation.TryPrepare(displacement).has_value());
|
||||
REQUIRE(operation.HasVariationData());
|
||||
operation.Mult(density, repeatedAction);
|
||||
operation.MultDisplacementVariationTrue(densityTrue, directionTrue, repeatedVariation);
|
||||
CHECK(prepared_test::relative_error(repeatedAction, baselineAction, communicator) < 2.0e-14);
|
||||
CHECK(prepared_test::relative_error(repeatedVariation, baselineVariation, communicator) < 2.0e-14);
|
||||
CHECK(operation.GetPreparationCount() == 4);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Prepared Mapped Gravity Source Matches Stateless Kernel",
|
||||
tags::gravity_prepared
|
||||
|
||||
@@ -30,6 +30,61 @@ namespace {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE(
|
||||
"Prepared Hdiv Geometry Variation Preserves Reference Piola Contractions",
|
||||
tags::gravity_prepared_jacobian_accuracy
|
||||
) {
|
||||
auto args = test_utils::setup_args();
|
||||
fem::FEM f = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(f.okay());
|
||||
|
||||
operators::PreparedMappedHDivMassOperator preparedOperator(f, *f.domainMapperStateless);
|
||||
const MPI_Comm communicator = f.gravityFluxFes->GetComm();
|
||||
const mfem::Vector first = prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.31);
|
||||
const mfem::Vector second = prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.79);
|
||||
const mfem::Vector direction = prepared_test::make_displacement(f, 0.43);
|
||||
mfem::Vector firstAction;
|
||||
|
||||
bool hasStellar = false;
|
||||
bool hasVacuum = false;
|
||||
for (int element = 0; element < f.mesh->GetNE(); ++element) {
|
||||
const bool vacuum = f.domainMapperStateless->IsCompactifiedElement(*f.mesh->GetElementTransformation(element));
|
||||
hasVacuum = hasVacuum || vacuum;
|
||||
hasStellar = hasStellar || !vacuum;
|
||||
}
|
||||
const int localDomains[2]{hasStellar ? 1 : 0, hasVacuum ? 1 : 0};
|
||||
int globalDomains[2]{};
|
||||
REQUIRE(MPI_Allreduce(localDomains, globalDomains, 2, MPI_INT, MPI_MAX, communicator) == MPI_SUCCESS);
|
||||
REQUIRE(globalDomains[0] != 0);
|
||||
REQUIRE(globalDomains[1] != 0);
|
||||
|
||||
// The stateless path still constructs each physically mapped RT basis;
|
||||
// compare both an undeformed and a changed prepared geometry, including
|
||||
// Kelvin exterior elements, against the compact forward/dual contractions.
|
||||
for (const double scale : {0.0, 0.7}) {
|
||||
const mfem::Vector displacementTrue = prepared_test::make_displacement(f, scale);
|
||||
const mfem::Vector displacement = preparedOperator.GetDisplacementMap().gather(displacementTrue);
|
||||
preparedOperator.Prepare(displacement);
|
||||
preparedOperator.MultDisplacementVariationTrue(first, direction, firstAction);
|
||||
|
||||
mfem::Vector referenceAction;
|
||||
operators::kernels::apply_mapped_hdiv_mass_variation(
|
||||
f, *f.domainMapperStateless, first, displacementTrue, direction, referenceAction
|
||||
);
|
||||
const double error = prepared_test::relative_error(firstAction, referenceAction, communicator);
|
||||
INFO("Deformation scale = " << scale);
|
||||
INFO("Prepared/stateless geometry-variation relative error = " << error);
|
||||
CHECK(error < 2.0e-11);
|
||||
}
|
||||
|
||||
mfem::Vector secondAction;
|
||||
preparedOperator.MultDisplacementVariationTrue(second, direction, secondAction);
|
||||
const double firstSecond = prepared_test::global_dot(first, secondAction, communicator);
|
||||
const double secondFirst = prepared_test::global_dot(second, firstAction, communicator);
|
||||
CHECK(prepared_test::relative_scalar_error(firstSecond, secondFirst) < 2.0e-11);
|
||||
CHECK(preparedOperator.GetPreparationCount() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Prepared Mapped Hdiv Mass Reports Invalid Candidate Geometry Without Unwinding",
|
||||
tags::gravity_prepared_unit &tags::geometry
|
||||
|
||||
@@ -76,17 +76,20 @@ namespace stellar_solver_architecture_test {
|
||||
std::shared_ptr<LifetimeProbe> probe;
|
||||
double correctionValue{0.0};
|
||||
bool resizeCorrection{false};
|
||||
bool surfaceCorrectionOnly{false};
|
||||
|
||||
ScriptedBackend() = default;
|
||||
|
||||
explicit ScriptedBackend(
|
||||
std::shared_ptr<LifetimeProbe> lifetimeProbe,
|
||||
const double scriptedCorrectionValue = 0.0,
|
||||
const bool resizeScriptedCorrection = false
|
||||
const double scriptedCorrectionValue = 0.0,
|
||||
const bool resizeScriptedCorrection = false,
|
||||
const bool scriptOnlySurfaceCorrection = false
|
||||
)
|
||||
: probe(std::move(lifetimeProbe)),
|
||||
correctionValue(scriptedCorrectionValue),
|
||||
resizeCorrection(resizeScriptedCorrection) {
|
||||
resizeCorrection(resizeScriptedCorrection),
|
||||
surfaceCorrectionOnly(scriptOnlySurfaceCorrection) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,14 +101,16 @@ namespace stellar_solver_architecture_test {
|
||||
const MPI_Comm communicator,
|
||||
std::shared_ptr<LifetimeProbe> probe,
|
||||
const double correctionValue,
|
||||
const bool resizeCorrection
|
||||
const bool resizeCorrection,
|
||||
const bool surfaceCorrectionOnly
|
||||
)
|
||||
: m_operation(std::addressof(operation)),
|
||||
m_preconditioner(std::addressof(preconditioner)),
|
||||
m_communicator(communicator),
|
||||
m_probe(std::move(probe)),
|
||||
m_correctionValue(correctionValue),
|
||||
m_resizeCorrection(resizeCorrection) {
|
||||
m_resizeCorrection(resizeCorrection),
|
||||
m_surfaceCorrectionOnly(surfaceCorrectionOnly) {
|
||||
if (m_probe != nullptr) {
|
||||
m_probe->problemIdentity = std::addressof(operation.GetProblem());
|
||||
}
|
||||
@@ -181,7 +186,17 @@ namespace stellar_solver_architecture_test {
|
||||
m_probe->incomingCorrectionNorms.push_back(GlobalNorm(correction));
|
||||
}
|
||||
|
||||
correction = m_correctionValue;
|
||||
if (m_surfaceCorrectionOnly) {
|
||||
mfem::Vector physicalCorrection(CorrectionSize());
|
||||
physicalCorrection = 0.0;
|
||||
auto physicalDirection = m_operation->GetProblem().GetManifest().stateView(physicalCorrection);
|
||||
mfem::Vector surfaceDirection =
|
||||
physicalDirection.block(mean_field::utils::blocks::surface_deformation_field.parameters_term);
|
||||
surfaceDirection = m_correctionValue;
|
||||
m_operation->NormalizeState(physicalCorrection, correction);
|
||||
} else {
|
||||
correction = m_correctionValue;
|
||||
}
|
||||
if (m_probe != nullptr) {
|
||||
m_probe->returnedCorrectionNorms.push_back(GlobalNorm(correction));
|
||||
}
|
||||
@@ -226,6 +241,7 @@ namespace stellar_solver_architecture_test {
|
||||
std::shared_ptr<LifetimeProbe> m_probe;
|
||||
double m_correctionValue;
|
||||
bool m_resizeCorrection;
|
||||
bool m_surfaceCorrectionOnly;
|
||||
};
|
||||
|
||||
template <
|
||||
@@ -243,7 +259,8 @@ namespace stellar_solver_architecture_test {
|
||||
communicator,
|
||||
std::move(configuration.probe),
|
||||
configuration.correctionValue,
|
||||
configuration.resizeCorrection
|
||||
configuration.resizeCorrection,
|
||||
configuration.surfaceCorrectionOnly
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1013,11 +1030,14 @@ TEST_CASE(
|
||||
event.normalizedState.begin(), event.normalizedState.end()
|
||||
);
|
||||
CHECK(event.iterationSeconds >= 0.0);
|
||||
CHECK(event.geometryPreflightSeconds >= 0.0);
|
||||
CHECK(event.lineSearchSeconds >= 0.0);
|
||||
CHECK(event.trialPreparationSeconds >= 0.0);
|
||||
CHECK(event.metricEvaluationSeconds >= 0.0);
|
||||
CHECK(event.preconditionerRefreshSeconds >= 0.0);
|
||||
CHECK(event.rollbackSeconds >= 0.0);
|
||||
REQUIRE(event.geometryPreflight.has_value());
|
||||
CHECK(event.geometryPreflight->sampledQuadraturePointCount > 0);
|
||||
}
|
||||
);
|
||||
auto newton = solver::nonlinear::Newton(
|
||||
@@ -1050,11 +1070,14 @@ TEST_CASE(
|
||||
CHECK(report.diagnostics().nonFiniteLineSearchTrials == 0);
|
||||
CHECK(report.diagnostics().insufficientDecreaseTrials == 2);
|
||||
CHECK(report.diagnostics().totalLinearSolveSeconds >= 0.0);
|
||||
CHECK(report.diagnostics().totalGeometryPreflightSeconds >= 0.0);
|
||||
CHECK(report.diagnostics().totalLineSearchSeconds >= 0.0);
|
||||
CHECK(report.diagnostics().totalTrialPreparationSeconds >= 0.0);
|
||||
CHECK(report.diagnostics().totalMetricEvaluationSeconds >= 0.0);
|
||||
CHECK(report.diagnostics().totalPreconditionerRefreshSeconds >= 0.0);
|
||||
CHECK(report.diagnostics().totalRollbackSeconds >= 0.0);
|
||||
REQUIRE(report.diagnostics().lastGeometryPreflight.has_value());
|
||||
CHECK(report.diagnostics().lastGeometryPreflight->sampledQuadraturePointCount > 0);
|
||||
CHECK(metricState->next == metricState->evaluations.size());
|
||||
REQUIRE(observerRecord->beforeCalls == 2);
|
||||
REQUIRE(observerRecord->afterCalls == 2);
|
||||
@@ -1105,6 +1128,82 @@ TEST_CASE(
|
||||
);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Newton Geometry Preflight Caps A Surface Step Before Trial Preparation",
|
||||
"[solver][newton][geometry][preflight][backtracking][wiring]"
|
||||
) {
|
||||
using namespace mean_field;
|
||||
using namespace stellar_solver_architecture_test;
|
||||
using Catch::Approx;
|
||||
|
||||
auto finiteElements = makeFiniteElements();
|
||||
REQUIRE(finiteElements.okay());
|
||||
auto discretization = equilibrium::makeStellarDiscretization(
|
||||
std::move(finiteElements),
|
||||
normalization::PhysicalRieszDiagonal{dimensions::LengthValue{utils::RADIUS}, utils::G}
|
||||
);
|
||||
auto context = solver::makeContext(
|
||||
makeModel(), std::move(discretization), preconditioning::makePreconditioner(),
|
||||
ScriptedBackend{nullptr, -10.0, false, true}
|
||||
);
|
||||
|
||||
auto metricState = std::make_shared<MetricSequenceState>();
|
||||
metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}};
|
||||
std::optional<deformation::LargestSafeNewtonStepSizeEstimate> observedPreflight;
|
||||
auto observer = solver::nonlinear::makeObserver(
|
||||
[](const solver::nonlinear::BeforeIteration &) { },
|
||||
[&observedPreflight](const solver::nonlinear::AfterIteration &event) {
|
||||
observedPreflight = event.geometryPreflight;
|
||||
CHECK(event.geometryPreflightSeconds >= 0.0);
|
||||
CHECK(event.stepAccepted);
|
||||
CHECK(event.lineSearchTrials == 1);
|
||||
}
|
||||
);
|
||||
auto newton = solver::nonlinear::Newton(
|
||||
solver::nonlinear::NewtonOptions{
|
||||
.relativeTolerance = 0.0,
|
||||
.absoluteTolerance = 0.0,
|
||||
.maximumIterations = 1,
|
||||
.linearSolve =
|
||||
{.relativeTolerance = 0.0,
|
||||
.absoluteTolerance = std::numeric_limits<double>::max(),
|
||||
.maximumIterations = 1},
|
||||
.backtracking =
|
||||
{.initialStepLength = 1.0,
|
||||
.contractionFactor = 0.5,
|
||||
.fractionToBoundarySafety = 0.5,
|
||||
.sufficientDecrease = 1.0e-4,
|
||||
.minimumStepLength = 1.0e-8,
|
||||
.maximumTrials = 1}
|
||||
},
|
||||
SequencedMetric{metricState}
|
||||
);
|
||||
|
||||
auto equilibriumSolver = solver::make(context, std::move(newton), std::move(observer));
|
||||
const auto report = equilibriumSolver.evaluate();
|
||||
|
||||
REQUIRE_FALSE(report.converged());
|
||||
CHECK(report.failure().reason == solver::StellarEquilibriumFailureReason::iteration_limit);
|
||||
CHECK(report.diagnostics().attemptedNonlinearIterations == 1);
|
||||
CHECK(report.diagnostics().acceptedNonlinearIterations == 1);
|
||||
CHECK(report.diagnostics().totalLineSearchTrials == 1);
|
||||
CHECK(report.diagnostics().inadmissibleLineSearchTrials == 0);
|
||||
CHECK(report.diagnostics().geometryLimitedIterations == 1);
|
||||
REQUIRE(report.diagnostics().lastGeometryPreflight.has_value());
|
||||
const auto &preflight = *report.diagnostics().lastGeometryPreflight;
|
||||
CHECK(preflight.limitedByGeometry);
|
||||
CHECK(preflight.sampledQuadraturePointCount > 0);
|
||||
CHECK(preflight.minimumDeterminantAtAcceptedState > 0.0);
|
||||
CHECK(preflight.minimumDeterminantAtMaximumStepSize <= 0.0);
|
||||
CHECK(preflight.stepSize > 0.0);
|
||||
CHECK(preflight.stepSize < 1.0);
|
||||
CHECK(preflight.stepSize == Approx(0.5 * preflight.boundaryStepSize));
|
||||
CHECK(report.diagnostics().lastAcceptedStepLength == Approx(preflight.stepSize));
|
||||
REQUIRE(observedPreflight.has_value());
|
||||
CHECK(observedPreflight->stepSize == Approx(preflight.stepSize));
|
||||
CHECK(report.lastAcceptedCheckpointView().valid());
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"An Accepted Final Newton Step Reports The Iteration Limit To Its Observer",
|
||||
"[solver][newton][iteration-limit][observer][checkpoint]"
|
||||
|
||||
228
tools/profile_tests.py
Executable file
228
tools/profile_tests.py
Executable file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run bounded serial or MPI Catch2 profiling experiments.
|
||||
|
||||
Every run receives its own combined stdout/stderr log. A summary CSV is
|
||||
updated after each run so useful measurements survive a later timeout or
|
||||
interrupt. Timed-out process groups are terminated and, after a grace
|
||||
period, force-killed; this is important for MPI jobs whose launcher may
|
||||
otherwise leave workers behind.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import TextIO
|
||||
|
||||
|
||||
def positive_integer(value: str) -> int:
|
||||
parsed = int(value)
|
||||
if parsed <= 0:
|
||||
raise argparse.ArgumentTypeError("value must be positive")
|
||||
return parsed
|
||||
|
||||
|
||||
def positive_float(value: str) -> float:
|
||||
parsed = float(value)
|
||||
if not parsed > 0.0:
|
||||
raise argparse.ArgumentTypeError("value must be positive")
|
||||
return parsed
|
||||
|
||||
|
||||
def rank_list(value: str) -> list[int]:
|
||||
ranks = [positive_integer(item.strip()) for item in value.split(",") if item.strip()]
|
||||
if not ranks:
|
||||
raise argparse.ArgumentTypeError("at least one rank count is required")
|
||||
if len(set(ranks)) != len(ranks):
|
||||
raise argparse.ArgumentTypeError("rank counts must be unique")
|
||||
return ranks
|
||||
|
||||
|
||||
def safe_name(value: str) -> str:
|
||||
name = re.sub(r"[^A-Za-z0-9_.-]+", "_", value).strip("_.")
|
||||
return name[:80] or "all_tests"
|
||||
|
||||
|
||||
def terminate_process_group(process: subprocess.Popen[str], grace_seconds: float) -> None:
|
||||
if process.poll() is not None:
|
||||
return
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
return
|
||||
try:
|
||||
process.wait(timeout=grace_seconds)
|
||||
return
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
return
|
||||
process.wait()
|
||||
|
||||
|
||||
def run_bounded(
|
||||
command: list[str],
|
||||
working_directory: Path,
|
||||
log: TextIO,
|
||||
timeout_seconds: float,
|
||||
grace_seconds: float,
|
||||
) -> tuple[str, int, float]:
|
||||
start = time.perf_counter()
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=working_directory,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
start_new_session=True,
|
||||
)
|
||||
try:
|
||||
return_code = process.wait(timeout=timeout_seconds)
|
||||
status = "passed" if return_code == 0 else "failed"
|
||||
except subprocess.TimeoutExpired:
|
||||
status = "timeout"
|
||||
terminate_process_group(process, grace_seconds)
|
||||
return_code = process.returncode if process.returncode is not None else -signal.SIGKILL
|
||||
except BaseException:
|
||||
terminate_process_group(process, grace_seconds)
|
||||
raise
|
||||
return status, return_code, time.perf_counter() - start
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("catch_filter", help="Catch2 test name or tag expression")
|
||||
parser.add_argument(
|
||||
"--executable",
|
||||
type=Path,
|
||||
default=Path("cmake-build-profile-homebrew-llvm/tests"),
|
||||
help="Catch2 executable relative to --working-directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mpi-executable",
|
||||
default="mpirun",
|
||||
help="MPI launcher used for rank counts greater than one",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ranks",
|
||||
type=rank_list,
|
||||
default=[1],
|
||||
help="comma-separated rank counts, for example 1,2,4",
|
||||
)
|
||||
parser.add_argument("--repeat", type=positive_integer, default=1)
|
||||
parser.add_argument("--timeout", type=positive_float, default=300.0, help="seconds per run")
|
||||
parser.add_argument("--kill-grace", type=positive_float, default=5.0, help="seconds before SIGKILL")
|
||||
parser.add_argument("--working-directory", type=Path, default=Path.cwd())
|
||||
parser.add_argument("--output-directory", type=Path, default=Path("profile-results"))
|
||||
command_line = sys.argv[1:]
|
||||
if "--" in command_line:
|
||||
separator = command_line.index("--")
|
||||
runner_arguments = command_line[:separator]
|
||||
catch_arguments = command_line[separator + 1 :]
|
||||
else:
|
||||
runner_arguments = command_line
|
||||
catch_arguments = []
|
||||
|
||||
arguments = parser.parse_args(runner_arguments)
|
||||
arguments.catch_arguments = catch_arguments
|
||||
return arguments
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arguments = parse_arguments()
|
||||
working_directory = arguments.working_directory.resolve()
|
||||
executable = arguments.executable
|
||||
if not executable.is_absolute():
|
||||
executable = working_directory / executable
|
||||
executable = executable.resolve()
|
||||
|
||||
if not executable.is_file():
|
||||
raise FileNotFoundError(f"test executable does not exist: {executable}")
|
||||
if not os.access(executable, os.X_OK):
|
||||
raise PermissionError(f"test executable is not executable: {executable}")
|
||||
|
||||
output_directory = arguments.output_directory
|
||||
if not output_directory.is_absolute():
|
||||
output_directory = working_directory / output_directory
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
experiment = safe_name(arguments.catch_filter)
|
||||
summary_path = output_directory / f"{experiment}.csv"
|
||||
field_names = [
|
||||
"ranks",
|
||||
"repeat",
|
||||
"status",
|
||||
"exit_code",
|
||||
"wall_seconds",
|
||||
"timeout_seconds",
|
||||
"log",
|
||||
"command",
|
||||
]
|
||||
|
||||
failed = False
|
||||
with summary_path.open("w", newline="", encoding="utf-8") as summary_file:
|
||||
writer = csv.DictWriter(summary_file, fieldnames=field_names)
|
||||
writer.writeheader()
|
||||
summary_file.flush()
|
||||
|
||||
for ranks in arguments.ranks:
|
||||
for repetition in range(1, arguments.repeat + 1):
|
||||
test_command = [str(executable), arguments.catch_filter, *arguments.catch_arguments]
|
||||
command = (
|
||||
test_command
|
||||
if ranks == 1
|
||||
else [arguments.mpi_executable, "-n", str(ranks), *test_command]
|
||||
)
|
||||
log_path = output_directory / f"{experiment}.r{ranks}.run{repetition}.log"
|
||||
print(
|
||||
f"[{ranks} rank{'s' if ranks != 1 else ''}, run {repetition}/{arguments.repeat}] "
|
||||
f"timeout={arguments.timeout:.1f}s log={log_path}",
|
||||
flush=True,
|
||||
)
|
||||
with log_path.open("w", encoding="utf-8") as log:
|
||||
log.write(f"command: {json.dumps(command)}\n")
|
||||
log.flush()
|
||||
status, return_code, wall_seconds = run_bounded(
|
||||
command,
|
||||
working_directory,
|
||||
log,
|
||||
arguments.timeout,
|
||||
arguments.kill_grace,
|
||||
)
|
||||
|
||||
writer.writerow(
|
||||
{
|
||||
"ranks": ranks,
|
||||
"repeat": repetition,
|
||||
"status": status,
|
||||
"exit_code": return_code,
|
||||
"wall_seconds": f"{wall_seconds:.9f}",
|
||||
"timeout_seconds": f"{arguments.timeout:.3f}",
|
||||
"log": str(log_path),
|
||||
"command": json.dumps(command),
|
||||
}
|
||||
)
|
||||
summary_file.flush()
|
||||
print(f" {status}: {wall_seconds:.3f}s (exit {return_code})", flush=True)
|
||||
failed = failed or status != "passed"
|
||||
|
||||
print(f"summary: {summary_path}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (FileNotFoundError, PermissionError, ValueError) as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
raise SystemExit(2) from error
|
||||
Reference in New Issue
Block a user