Snapshot optimized solver and audit diagnostics before numerical experiments
This commit is contained in:
698
experiments/SYSTEM_ISOLATION_PROFILE_HARNESS.md
Normal file
698
experiments/SYSTEM_ISOLATION_PROFILE_HARNESS.md
Normal file
@@ -0,0 +1,698 @@
|
||||
# Final optimization batch: pressure, gravity source, preparation, and fixed divergence
|
||||
|
||||
Build the targets explicitly; sandbox is excluded from the default build:
|
||||
|
||||
```sh
|
||||
cmake --build cmake-build-release-homebrew \
|
||||
--target system_isolation_profile_harness sandbox tests --parallel
|
||||
```
|
||||
|
||||
Run from the repository root with a new output directory:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system final-optimizations --check full --implementation both \
|
||||
--samples 5 --iterations 10 --warmup 2 \
|
||||
--prepare-samples 3 --prepare-iterations 1 \
|
||||
--output final_optimizations_01
|
||||
```
|
||||
|
||||
This group measures:
|
||||
|
||||
- `pressure-displacement`: tensor evaluation of displacement gradients and
|
||||
tensor gradient transpose, compared with the previous optimized pointwise
|
||||
application (`cached_pointwise_reference`). The general prepared action
|
||||
and stateless kernel remain correctness references.
|
||||
- `pressure-preparation`: batched displacement and enthalpy evaluation and
|
||||
tensor assembly of the enthalpy derivative matrix, compared with the
|
||||
previous blocked matrix assembly and pointwise displacement evaluation.
|
||||
- `gravity-source-displacement`: tensor density/displacement evaluation and
|
||||
potential-test transpose, compared with the previous pointwise application.
|
||||
- `hydrostatic-preparation`: batched displacement and scalar evaluation and
|
||||
tensor assembly of the algebraic matrices, compared with the previous
|
||||
pointwise/dense preparation.
|
||||
- `gravity-schur-preparation`: reuse of the fixed divergence matrix during
|
||||
Schur assembly, compared with assembling divergence on every call. This
|
||||
timing excludes mass-diagonal preparation and AMG setup, which are unchanged.
|
||||
|
||||
Preparation timings change state/revisions before each measured call and
|
||||
verify that pressure/hydrostatic preparation actually rebuilds. Schur timings
|
||||
cycle through mass diagonals prepared from three displacement states.
|
||||
Cold hydrostatic preparation and cold divergence-cache assembly are reported
|
||||
separately. These medians measure the incremental benefit of this batch;
|
||||
they do not reuse the slower baselines from earlier batches.
|
||||
|
||||
Full harness checks compare prepared and stateless derivatives, residuals,
|
||||
algebraic actions, enthalpy diagonals, multiple states and directions, zero
|
||||
source directions, and finite differences. Hydrostatic checks also reject
|
||||
folded geometry and recover afterward. Schur checks compare cached and
|
||||
fresh assembly, change and restore the mass diagonal, check exact inverse
|
||||
scaling, clear the cache, and invalidate it with `Mesh::NodesUpdated()`.
|
||||
New unit coverage checks the scalar gradient transpose against dense
|
||||
reference gradients and adjoint products for multiple H1/L2 orders/bases,
|
||||
preparation-mode switches, rejection/recovery, and production gravity-block
|
||||
cache reuse/invalidation.
|
||||
|
||||
The divergence cache belongs to each prepared gravity block. It retains one
|
||||
assembled sparse matrix for the block's lifetime. Every refresh still forms
|
||||
a fresh weighted transpose and Schur matrix using the current mass diagonal.
|
||||
A discretization revision clears the cache; mesh, node, FE-space sequences,
|
||||
and quadrature-policy identity are also checked collectively before reuse.
|
||||
Raw computational-coordinate edits must call `Mesh::NodesUpdated()`.
|
||||
A standalone cache must be cleared after changing a quadrature policy in
|
||||
place. Physical displacement changes do not invalidate fixed divergence.
|
||||
|
||||
The tensor paths retain the existing mapper validation, quadrature, DOF
|
||||
transformations, and fallbacks for unsupported elements/rules. Metadata
|
||||
reports `pressure_tensor_elements`, `source_tensor_variation_elements`, and
|
||||
`hydro_preparation_tensor_elements`, plus divergence assembly counts. A zero
|
||||
tensor count indicates fallback, not measured tensor acceleration.
|
||||
|
||||
The new individual selectors are `gravity-source-displacement`,
|
||||
`hydrostatic-preparation`, and `gravity-schur-preparation`.
|
||||
`pressure-displacement` includes its preparation benchmark. This batch is
|
||||
selected together with `final-optimizations`.
|
||||
|
||||
Return `summary.txt`, `metadata.txt`, `timings.csv`, and `checks.csv`.
|
||||
After the checks pass, run the rebuilt sandbox with the same preflight
|
||||
setting as the previous run, and capture its Newton timings and profiles.
|
||||
Compare the residual, accepted step, trial count, determinant minimum, Krylov
|
||||
iterations, and true linear residual along with the wall times. The previous
|
||||
reference was 24.174 s for Newton 0, 8.00964 s trial preparation, 2.14648 s
|
||||
accepted refresh, and 13.6571 s linear solve (26 J and 24 preconditioner calls).
|
||||
|
||||
These source and test changes have not been compiled or executed by the agent.
|
||||
|
||||
---
|
||||
|
||||
# Application optimization batch: closure, rotation, primal mass, and trial preparation
|
||||
|
||||
Build both `system_isolation_profile_harness` and `sandbox` explicitly in
|
||||
`cmake-build-release-homebrew`; sandbox is excluded from the default build.
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system application-optimizations --check full --implementation both \
|
||||
--samples 5 --iterations 10 --warmup 2 \
|
||||
--prepare-samples 3 --prepare-iterations 1 \
|
||||
--output application_optimizations_01
|
||||
```
|
||||
|
||||
The group measures these changes against the implementations immediately before
|
||||
this batch:
|
||||
|
||||
- `barotropic-closure`, `barotropic-enthalpy`, and `barotropic-displacement`:
|
||||
tensor interpolation and transpose versus the cached pointwise path. The
|
||||
dedicated enthalpy and displacement baselines retain their dedicated entry
|
||||
points, so these ratios measure tensor acceleration without counting the
|
||||
earlier removal of zero-block work again.
|
||||
- `rotation-force`: tensor density/displacement interpolation and force
|
||||
transpose versus the cached pointwise complete action.
|
||||
- `hdiv-mass`: the new CPU application versus retained MFEM partial assembly
|
||||
(`mfem_pa_reference`), with identical prepared coefficients, quadrature,
|
||||
restrictions, orientation handling, and true-DOF transfers. The new integrator
|
||||
contracts only its marked domain, uses scratch sized to the actual order,
|
||||
and specializes the current RT2/nine-point-per-axis case. Other supported
|
||||
CPU orders use the same algorithm with runtime dimensions. Other backends
|
||||
retain MFEM's application. Mass-only runs use primal preparation.
|
||||
- `geometry-inspection`: the production trial-geometry validator with cached
|
||||
computational-mesh inverse Jacobians and tensor gradients, versus the original
|
||||
`GridFunction::GetVectorGradient` loop. Every volume element, inspection
|
||||
point, and determinant check is retained. The first cold inspection is
|
||||
reported separately; paired medians measure reuse with changing displacement.
|
||||
- `gravity-source-preparation`: reuses the mesh weight already computed during
|
||||
mapping evaluation, versus recomputing `ElementTransformation::Weight()`.
|
||||
Both paths rebuild on every measured call; the harness checks preparation
|
||||
counters and compares forward, transpose, and displacement actions.
|
||||
|
||||
The scalar tensor plans now include nodal H1 and L2 hexes. Basis reconstruction,
|
||||
native DOF permutations, and Cartesian quadrature ordering are verified before
|
||||
a plan is enabled. Unsupported elements/rules retain the dense implementation.
|
||||
Metadata reports `closure_tensor_elements`, `rotation_tensor_elements`,
|
||||
`fast_mass_elements`, and `geometry_inspection_tensor_elements` so a fallback
|
||||
cannot be mistaken for acceleration.
|
||||
|
||||
Full checks include general prepared and independent stateless comparisons,
|
||||
existing finite differences, mass symmetry/linearity/positive energy/zero input,
|
||||
changing geometries, fold rejection, and non-finite geometry rejection. The
|
||||
unit-test additions also cover both global vector orderings, curved-mesh cache
|
||||
invalidation, explicit cache clearing, tetrahedron fallback, and L2 basis
|
||||
variants. These tests are supplied for the user to compile and run.
|
||||
|
||||
The inspector stores nine doubles (72 bytes) per eligible inspection point,
|
||||
plus shared reference tables and reusable element scratch. It refreshes fixed
|
||||
geometry when the mesh, nodes, or finite-element-space sequence changes.
|
||||
Raw coordinate edits must call `Mesh::NodesUpdated()`, as with MFEM's geometry
|
||||
cache, or clear a standalone inspector with `ClearCache()`. Displacement is
|
||||
read anew for every inspection. Its mutable scratch is intended for sequential
|
||||
calls, consistent with the prepared operators.
|
||||
|
||||
Use a new output directory and return `summary.txt`, `metadata.txt`,
|
||||
`timings.csv`, and `checks.csv`. Application timings use `--samples` and
|
||||
`--iterations`; inspection/source preparation timings use `--prepare-samples`
|
||||
and `--prepare-iterations`. This group omits unrelated force-preparation
|
||||
benchmarks. Individual systems remain selectable. After checks pass, rebuild
|
||||
sandbox too and compare its printed Newton timings and convergence against the
|
||||
previous run.
|
||||
|
||||
---
|
||||
|
||||
# Runtime optimization batch: tensor applications, geometry cache, normalization
|
||||
|
||||
Build both `system_isolation_profile_harness` and `sandbox` explicitly in
|
||||
`cmake-build-release-homebrew`; sandbox is excluded from the default build.
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system runtime-optimizations --check full --implementation both \
|
||||
--samples 5 --iterations 10 --warmup 2 \
|
||||
--prepare-samples 3 --prepare-iterations 1 \
|
||||
--output runtime_optimizations_01
|
||||
```
|
||||
|
||||
This group checks and measures:
|
||||
|
||||
- `normalization-diagonals`: H(div), potential, density, enthalpy, and surface
|
||||
reference-mass diagonals, comparing volume partial assembly and direct surface
|
||||
diagonal integration to full true-DOF matrix assembly. Both diagonal norms
|
||||
are recorded, and the surface diagonal must have positive entries. These use
|
||||
the preparation sample/iteration counts.
|
||||
- `geometry-preparation`: complete H(div) preparation with the fixed-mesh
|
||||
geometry cache enabled versus disabled, changing displacement between paired
|
||||
samples. Both use the same optimized operator. Mass and displacement actions
|
||||
are checked outside the timer. The initial cold-cache preparation is reported
|
||||
separately; median timings measure reuse. Cache hit/miss/point counts are saved.
|
||||
- `hdiv-displacement`: tensor interpolation/transpose versus the **previous
|
||||
optimized pointwise** implementation (`cached_pointwise_reference`).
|
||||
- `gravity-force`: tensor application versus the **previous optimized pointwise**
|
||||
application. General prepared paths and finite differences remain independent
|
||||
checks. The older gravity preparation comparison is omitted from this group.
|
||||
|
||||
The metadata includes `tensor_variation_elements` and
|
||||
`gravity_tensor_elements`. The current hex mesh should use tensor paths on all
|
||||
eligible elements. General geometries and quadrature retain the existing paths;
|
||||
these counts distinguish real acceleration from a correct fallback.
|
||||
|
||||
Individual systems remain selectable, including the two new setup systems.
|
||||
Use a new output directory for every run. Upload `summary.txt`, `metadata.txt`,
|
||||
`timings.csv`, and `checks.csv`. Preparation timings use `--prepare-samples`
|
||||
and `--prepare-iterations`; application timings use `--samples` and
|
||||
`--iterations`.
|
||||
|
||||
## Preflight option
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/sandbox --skip-preflight
|
||||
```
|
||||
|
||||
Library option: `NewtonOptions.backtracking.geometryPreflight = false`.
|
||||
The default remains `true`; sandbox also accepts `--preflight`. Skipping avoids
|
||||
both the predictive geometry scan and its lazy rule construction. Trial mapping
|
||||
validation, finite-value checks, backtracking, and accepted-state restoration
|
||||
remain active. Skipped scans have zero reported time and an empty optional
|
||||
geometry estimate; `geometryLimitedIterations` counts only predictive limits.
|
||||
A skipped scan can trade its time for extra rejected trials on a large step.
|
||||
|
||||
## Geometry and normalization contracts
|
||||
|
||||
The mapper caches only fixed computational-mesh positions, Jacobians, inverse
|
||||
Jacobians, and weights. It recomputes displacement and compactification mapping
|
||||
data on each call. Cache keys use element and actual reference coordinates, so
|
||||
copied or changed quadrature rules cannot reuse the wrong point; quadrature
|
||||
weights are applied by each caller. Mesh and node sequence changes invalidate
|
||||
the entries. After raw coordinate changes, call `Mesh::NodesUpdated()`, as for
|
||||
MFEM's geometric-factor cache, or `DomainMapper::ClearGeometryCache()`. Clear
|
||||
the cache before destroying a mesh that the mapper will outlive. Cache entries
|
||||
consume memory proportional to the distinct element/quadrature points visited,
|
||||
and the mapper cache, like the element transformations it uses, is not thread-safe.
|
||||
|
||||
Normalization retains full matrix assembly for nonconforming, variable-order,
|
||||
and unsupported element spaces. In particular, it does not substitute the
|
||||
approximate partial-assembly diagonal for `diag(P^T A P)` on hanging-node meshes.
|
||||
Supported conforming hex volume spaces use MFEM partial assembly followed by
|
||||
exact true-DOF diagonal assembly. The surface path integrates only diagonal
|
||||
entries over marked boundary elements, using the same quadrature and physical
|
||||
shape functions as full assembly. This includes the internal star/vacuum
|
||||
interface, which MFEM's exterior boundary partial-assembly restriction omits.
|
||||
The direct surface path supports conforming scalar H1 spaces without general
|
||||
DOF transformations; other spaces retain full assembly.
|
||||
|
||||
The new tests in existing test sources cover scalar/RT tensor transposes and
|
||||
adjoint products, integrated RT bases, retained-table lifetime, general fallback,
|
||||
geometry cache invalidation, changed displacement/compactification, exact
|
||||
normalization diagonals, nonconforming fallback, and Newton acceptance/fold
|
||||
rejection with preflight skipped. Compilation and execution are left to the user.
|
||||
|
||||
---
|
||||
|
||||
# System isolation profile harness
|
||||
|
||||
This executable checks and times individual operators without constructing a
|
||||
stellar model, preconditioner, or Newton solver. Available systems are:
|
||||
|
||||
- `hdiv-displacement`: optimized versus retained prepared H(div) derivative.
|
||||
- `hdiv-mass`: new CPU primal application versus retained MFEM partial assembly.
|
||||
- `mass-normalization`: cached scalar mass row versus retained quadrature action.
|
||||
- `angular-momentum`: cached scalar angular-momentum row versus retained quadrature action.
|
||||
- `hydrostatic-displacement`: tensor evaluation versus the previous cached pointwise derivative; checks also compare the general mapping variation.
|
||||
- `stellar-derivatives`: mass normalization, angular momentum, and hydrostatic displacement.
|
||||
- `barotropic-closure`: complete density/enthalpy/displacement closure action.
|
||||
- `barotropic-enthalpy`: tensor versus cached dense dedicated enthalpy coupling.
|
||||
- `barotropic-displacement`: tensor versus cached pointwise dedicated displacement coupling.
|
||||
- `pressure-displacement`: direct pressure displacement contraction versus the old matrix products.
|
||||
- `pressure-enthalpy`: local-matrix application after blocked versus original matrix preparation.
|
||||
- `gravity-force`: complete density/displacement/gravity-gradient force derivative.
|
||||
- `rotation-force`: complete density/displacement force derivative at fixed rotation.
|
||||
- `linear-derivatives`: the seven closure/force comparisons above, plus pressure/gravity/rotation preparation timings.
|
||||
- `preparation-optimizations`: pressure displacement and enthalpy, gravity force, rotation force, their preparation comparisons, then hydrostatic displacement.
|
||||
- `geometry-inspection`: cached fixed geometry and tensor gradients versus the original geometry scan.
|
||||
- `gravity-source-preparation`: cached mesh weights versus repeated weight evaluation.
|
||||
- `application-optimizations`: the closure, rotation, primal mass, inspection, and source comparisons described above.
|
||||
- `all`: all application and preparation comparisons, including geometry inspection and normalization diagonals.
|
||||
A failure stops the affected group before its benchmarks.
|
||||
|
||||
The default remains `hdiv-displacement`. Scalar systems prepare a shared gravity
|
||||
context to supply their frozen state, but do not solve for gravity. Hydrostatic
|
||||
only runs do not construct that shared context.
|
||||
|
||||
## Preparation and hydrostatic optimization comparison
|
||||
|
||||
The current changes fuse gravity/rotation residual assembly with element-cache
|
||||
preparation, block pressure enthalpy-matrix assembly, and use tensor contractions
|
||||
for supported hydrostatic displacement elements. From the repository root,
|
||||
after the user builds the release targets:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system preparation-optimizations --check full --implementation both \
|
||||
--samples 5 --iterations 10 --warmup 2 \
|
||||
--prepare-samples 3 --prepare-iterations 1 \
|
||||
--output preparation_optimizations_01
|
||||
```
|
||||
|
||||
Return the four output files. The new preparation medians are
|
||||
`pressure-preparation.*`, `gravity-force-preparation.*`, and
|
||||
`rotation-force-preparation.*`. Every timed pair gets fresh dependency revisions
|
||||
with the same frozen physical state. Order alternates between samples.
|
||||
Preparation counters must increase once per call; an accidental no-op is an error.
|
||||
Shared gravity-context preparation and residual copying/norms are outside these
|
||||
timers. These are warm re-preparation timings; they include geometry rebuilding
|
||||
and do not measure first-construction cost or a complete Newton iteration.
|
||||
Initial and unchanged-state preparation remain separately reported.
|
||||
|
||||
Preparation uses its own sample/iteration counts to avoid accidentally repeating
|
||||
multi-second work with the application iteration count. Set `--prepare-samples 0`
|
||||
to skip preparation timings. Both implementations remain available through
|
||||
`--implementation`; correctness checks can still prepare references in an
|
||||
optimized-only timing run. Independent reference operators require additional
|
||||
memory in the harness, not in the production solver.
|
||||
|
||||
Checks compare prepared residuals and pressure enthalpy actions against the
|
||||
original preparation at five states, as well as the existing derivative and
|
||||
finite-difference checks. Hydrostatic timings use
|
||||
`cached_pointwise_reference`, isolating this change from the earlier fast-path
|
||||
optimization. Metadata reports `hydrostatic-displacement.tensor_elements`.
|
||||
On the standard mesh this should equal the stellar element count.
|
||||
|
||||
Focused regression cases:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/tests '*Tensor reference*,*Pressure*Force*,*Gravity Displacement Force*,*Rotational Displacement Force*,*Prepared Hydrostatic*'
|
||||
```
|
||||
|
||||
For a nonlinear EOS check, repeat with `--polytropic-index 3` and a new output
|
||||
directory. No stellar seed or Newton convergence is required.
|
||||
|
||||
## First comparison of closure and force derivatives
|
||||
|
||||
Explicitly build the harness target: it is excluded from the default build.
|
||||
From `/Users/tboudreaux/Programming/4DSTAR/MeanField`, use:
|
||||
|
||||
```sh
|
||||
cmake --build cmake-build-release-homebrew \
|
||||
--target system_isolation_profile_harness tests sandbox --parallel 2
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system linear-derivatives --check full --implementation both \
|
||||
--samples 5 --iterations 10 --warmup 2 \
|
||||
--output linear_derivatives_01
|
||||
```
|
||||
|
||||
Return `summary.txt`, `checks.csv`, `metadata.txt`, and `timings.csv`.
|
||||
Use a new output directory for every run. Individual system names above let you
|
||||
isolate a failure or time one operator without constructing the others.
|
||||
The group defaults to a polytrope with n=1 and K=1. To exercise a nonlinear EOS
|
||||
without creating a stellar seed, add `--polytropic-index 3`; this option applies
|
||||
only to the closure/force group and does not change the FEM mesh.
|
||||
|
||||
The complete closure comparison measures the contraction change. The two
|
||||
coupling comparisons include savings from omitting absent columns, exactly as
|
||||
the preconditioner now does. Their reference still executes the old complete
|
||||
action with explicit zeros. Pressure measures both displacement and enthalpy derivatives.
|
||||
Gravity and rotation measure their combined derivatives; their individual
|
||||
stateless column methods remain available as independent checks.
|
||||
|
||||
Full validation covers undeformed and deformed geometry, a density/enthalpy-only
|
||||
update, a gravity/rotation update, and a rotation-only update to zero angular
|
||||
velocity. Nonzero rotation uses a tilted axis and an offset center. Each state
|
||||
compares every requested direction and an exact zero direction. Column checks
|
||||
compare old/new complete actions; gravity and rotation also compare their
|
||||
independent stateless columns. Prepared gravity/rotation residuals are also
|
||||
compared with independently assembled stateless residuals at every validation
|
||||
state. Centered differences of primal residuals use two step sizes. Grouped runs check the complete closure FD;
|
||||
individual coupling runs check that coupling's FD. Unchanged dependency
|
||||
revisions must preserve preparation counts.
|
||||
|
||||
The scaled comparison uses the same unit-floor metric as the stellar group.
|
||||
An additional relative comparison is reported for non-negligible outputs,
|
||||
with tolerance 2e-10 for references and 2e-6 for finite differences. Full
|
||||
validation may take several minutes because it prepares many states; its time
|
||||
is reported separately and excluded from application samples.
|
||||
`--check reference` checks one state plus zero input and unchanged revisions;
|
||||
`--check none` skips correctness checks.
|
||||
|
||||
Initial, repeated, and unchanged preparation times are separate. The gravity
|
||||
context is counted once, separately from gravity force preparation.
|
||||
Application comparisons retain the earlier reference derivatives. The pressure
|
||||
enthalpy comparison uses independently prepared local matrices, and the separate
|
||||
preparation benchmarks compare original versus optimized preparation.
|
||||
Gravity/rotation metadata reports fast-element counts; on the standard
|
||||
3D mesh these should equal the stellar element count. Their fast paths require
|
||||
VALUE/GRAD displacement with byNODES global ordering; gravity also requires an
|
||||
H(div) reference table. Other configurations retain the previous implementation.
|
||||
Closure and pressure use their direct contractions in 3D and retain their
|
||||
previous general-dimensional calculations otherwise.
|
||||
|
||||
Focused regression cases, including material/surface coupling tests:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/tests '*Barotropic Closure*,*Pressure*Force*,*Gravity Displacement Force*,*Rotational Displacement Force*,*Material*Surface*'
|
||||
```
|
||||
|
||||
## First comparison of the scalar rows and hydrostatic derivative
|
||||
|
||||
After compiling the updated release executable, run from the repository root:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system stellar-derivatives --check full --implementation both \
|
||||
--samples 5 --iterations 20 --warmup 2 \
|
||||
--output stellar_derivatives_01
|
||||
```
|
||||
|
||||
Return `summary.txt`, `checks.csv`, `metadata.txt`, and `timings.csv`. To isolate
|
||||
one system, replace `stellar-derivatives` with `mass-normalization`,
|
||||
`angular-momentum`, or `hydrostatic-displacement`. The scalar benchmarks apply
|
||||
the complete row (density/displacement and, for angular momentum, Omega).
|
||||
The hydrostatic benchmark applies only its displacement derivative.
|
||||
|
||||
Full checks compare four states: undeformed, deformed, changed density, and
|
||||
changed rotation including negative Omega. They use several directions, with
|
||||
an element-scale perturbation in the second displacement direction. The
|
||||
rotation axis is tilted and its center is displaced from the origin. Checks
|
||||
cover separate scalar columns and zero inputs, the mass transpose identity,
|
||||
lazy row construction, reuse on rotation/target-only updates, and invalidation
|
||||
on geometry/density updates. Two centered differences perturb density,
|
||||
displacement and Omega together for the scalar rows; the hydrostatic difference
|
||||
holds the other fields and rotation fixed. Finite differences use the existing
|
||||
primal residual preparation, which has not been replaced by the new row code.
|
||||
|
||||
For these three systems, errors are
|
||||
`norm(actual-reference) / max(1, norm(actual), norm(reference))`; the unit floor
|
||||
also makes zero scalar columns testable. Reference tolerance is 2e-11 and finite
|
||||
difference tolerance is 2e-6. Lifecycle checks use exact counts. `--check reference`
|
||||
checks one state and all requested directions; `--check none` performs no
|
||||
correctness checks. These runs do not establish stellar accuracy or MPI correctness.
|
||||
|
||||
Preparation is reported separately for the shared gravity context and each
|
||||
selected operator, on initial construction and on re-preparation. Scalar rows
|
||||
are built lazily in production on first Jacobian use. The harness explicitly
|
||||
times `PrepareJacobianRows()` before warmup, including in reference-only runs,
|
||||
so that repeated-application medians do not hide that cost in one sample.
|
||||
`explicit_row_prepare_seconds` records this first-use cost. When both paths are
|
||||
timed, the summary also reports the break-even application count and the
|
||||
reference/optimized ratio for 1, 10 and 25 calls including one row assembly.
|
||||
A break-even value of -1 means no measured per-application saving. These ratios
|
||||
exclude the common state/context preparation costs, which are reported separately.
|
||||
Hydrostatic comparison uses the same augmented geometry cache for both paths;
|
||||
it does not independently measure the previous preparation time.
|
||||
|
||||
Metadata reports scalar row eligibility and the hydrostatic fast-element count.
|
||||
On the standard mesh both scalar flags should be 1 and the hydrostatic count
|
||||
should equal the stellar element count. The optimized scalar rows retain two
|
||||
local vectors per operator; hydrostatic adds nine doubles (72 bytes) per eligible
|
||||
quadrature point. Reference tables are shared across operators with matching
|
||||
elements and quadrature rules. Old mapping caches remain for reference/fallback.
|
||||
|
||||
The user-run `stellar_derivatives_01` comparison passed all checks and measured
|
||||
885x and 1349x repeated scalar applications, and 1.94x hydrostatic displacement.
|
||||
These exclude common preparation. The closure/force changes described below
|
||||
have not been compiled or executed by the coding agent.
|
||||
|
||||
## H(div) comparison
|
||||
|
||||
From the repository root, run:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system hdiv-displacement \
|
||||
--check full \
|
||||
--implementation both \
|
||||
--samples 5 --iterations 3 --warmup 1 \
|
||||
--output hdiv_isolation_01
|
||||
```
|
||||
|
||||
The output directory must not exist. This uses `sandbox.smesh`, the production
|
||||
quadrature policy and registered element orders, and one MPI rank on the CPU.
|
||||
It performs no equilibrium solve. Directions and gravity coefficients are
|
||||
deterministic; the geometry is a small nonuniform polynomial displacement with
|
||||
shear and cross terms. `--deformation 0` selects the undeformed timing state;
|
||||
the default is `0.7`, multiplying displacements whose coefficients are of order
|
||||
0.01–0.04. This argument is not a fractional stellar radius or rotation rate.
|
||||
|
||||
Full validation checks two geometries (zero and the selected deformation), all
|
||||
configured directions against the retained prepared derivative, and the first
|
||||
direction against the independent stateless kernel. It also checks the selected
|
||||
geometry against centered differences of the primal mass operator at two step
|
||||
sizes. Those differences hold the gravity coefficients fixed. A failed check
|
||||
stops the run before timing and returns status 3. Execution errors return 2;
|
||||
successful requested checks and timing return 0. With `--check none`, status 0
|
||||
means execution completed, not that correctness was checked.
|
||||
|
||||
Return `summary.txt`, `checks.csv`, `metadata.txt`, and `timings.csv` for analysis.
|
||||
There is no need to run the full stellar model for this first comparison.
|
||||
|
||||
## Timing and interpretation
|
||||
|
||||
- `metadata.txt`: mesh path, geometry parameter, build/compiler information,
|
||||
problem sizes, domain element counts, quadrature count, and how many elements
|
||||
use the new derivative path. For the standard 3D Kelvin case, the last count
|
||||
should equal the total element count.
|
||||
- `summary.txt`: finite-element setup, operator construction, initial and repeat
|
||||
preparation, validation time, median application times and their ratio.
|
||||
- `timings.csv`: every measured sample, application count, seconds per
|
||||
application, and the last output norm. Warmup is excluded. A/B execution order
|
||||
alternates between samples. Output norms and file writes are outside the
|
||||
application timer. Each sample cycles through the same direction sequence.
|
||||
- `checks.csv`: numerical errors, tolerances, and pass/fail status. The derivative
|
||||
comparison tolerance is 2e-11; centered differences use 2e-6 to allow subtraction
|
||||
error. A numerical failure is not permission to relax a tolerance.
|
||||
|
||||
For `hdiv-displacement`, `prepared_reference` is the previous prepared application
|
||||
algorithm, retained in the same executable and using the same prepared state,
|
||||
quadrature and field maps. This is the relevant application speedup comparison.
|
||||
Both paths include true/local vector transfers and element assembly. Preparation
|
||||
has already constructed the new cache in either case: this comparison does not
|
||||
measure the old preparation cost or old peak memory use.
|
||||
|
||||
For `hdiv-mass`, `optimized` is the new CPU contraction and
|
||||
`mfem_pa_reference` is the retained MFEM application using the same PA data.
|
||||
This ratio measures the primal mass improvement from the latest batch. Full
|
||||
checks still compare the independent stateless kernel, which reevaluates mapping.
|
||||
Mass-only runs use primal preparation and report zero variation-cache elements.
|
||||
|
||||
The new tangent cache adds 12 doubles per eligible quadrature point (96 bytes),
|
||||
plus shared reference displacement tables. Existing frozen mapping data remains
|
||||
available for the reference/fallback implementation. Initial preparation may
|
||||
therefore increase; compare preparation plus the application's actual number of
|
||||
repeated calls when judging the total benefit. The earlier `hdiv_isolation_01`
|
||||
user run measured a 3.13x median application speedup on the standard mesh; this
|
||||
is separate from the new scalar/hydrostatic optimizations.
|
||||
|
||||
For a longer optimized-only profiling window after correctness passes:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/system_isolation_profile_harness \
|
||||
--system hdiv-displacement --implementation optimized --check none \
|
||||
--samples 10 --iterations 20 --warmup 3 \
|
||||
--output hdiv_optimized_profile_01
|
||||
```
|
||||
|
||||
Use a separate directory for each run. Avoid concurrent builds or other heavy
|
||||
jobs while collecting comparisons. To inspect another resolution, use
|
||||
`--refinements N`; to change the number of direction/flux pairs, use
|
||||
`--directions N`. These are local operator tests, not demonstrations of final
|
||||
stellar accuracy or distributed correctness.
|
||||
|
||||
## Derivative implementation
|
||||
|
||||
For computational-mesh coordinates X, the mapped H(div) mass tensor is
|
||||
|
||||
\[
|
||||
M = J^T J / \det J, \qquad J = \partial x_{physical}/\partial X.
|
||||
\]
|
||||
|
||||
The fast path contracts shared reference displacement gradients before applying
|
||||
the cached mesh inverse. In the stellar domain, delta J is grad(delta u). For
|
||||
Kelvin compactification, the existing Kelvin radial-factor calculation supplies
|
||||
the fixed scale and its gradient:
|
||||
|
||||
\[
|
||||
\delta J = a\,\nabla_X\delta u + \delta u (\nabla_X a)^T.
|
||||
\]
|
||||
|
||||
Only `a * J_mesh^-1` and `grad_X(a)` are added to the point cache. The directional
|
||||
mass action on a computational H(div) vector g is evaluated as
|
||||
|
||||
\[
|
||||
\delta M g = \frac{\delta J^T(Jg) + J^T[\delta Jg -
|
||||
\operatorname{tr}(J^{-1}\delta J)Jg]}{\det J}.
|
||||
\]
|
||||
|
||||
This avoids computing unused inverse-mapping, inverse-element, physical-position,
|
||||
and quadrature-weight derivatives, and avoids constructing the full mass-tensor
|
||||
derivative. The fixed computational-mesh Piola factors, signed DOFs, primal/dual
|
||||
DOF transformations, quadrature weights and true/local transfers are retained.
|
||||
There is no change to equations, integration order, or solver tolerances.
|
||||
|
||||
The specialization requires three dimensions, an H(div) reference table, and
|
||||
VALUE/GRAD displacement elements. Compactified elements additionally require
|
||||
the concrete Kelvin map and a differentiable compactification element. Other
|
||||
maps use the retained general implementation; merely declaring a map affine
|
||||
does not select the Kelvin specialization.
|
||||
|
||||
## Compile and regression checks
|
||||
|
||||
Configure the release build with
|
||||
`CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST`, so compiling test targets
|
||||
does not execute them to enumerate cases. Discovery occurs when the user runs
|
||||
CTest. Configure/build commands are:
|
||||
|
||||
```sh
|
||||
cmake -S . -B cmake-build-release-homebrew \
|
||||
-DCMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST
|
||||
cmake --build cmake-build-release-homebrew \
|
||||
--target system_isolation_profile_harness tests sandbox --parallel 2
|
||||
```
|
||||
|
||||
Focused regression cases for the user to run:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/tests '*Prepared Hdiv*,*Prepared Mapped Hdiv*,Kelvin Prepared Jacobian Factors*'
|
||||
```
|
||||
|
||||
Focused scalar/hydrostatic regression cases for the user to run after building:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/tests '*Prepared Mass Normalization*,*Prepared Angular Momentum*,*Prepared Hydrostatic*'
|
||||
```
|
||||
|
||||
These existing suites now also check retained-reference agreement, lazy row
|
||||
construction, selective row invalidation, reuse across unrelated updates, and
|
||||
angular-momentum rejection/recovery after rows have already been built. Existing
|
||||
finite differences, transpose pairing and context lifecycle tests remain active.
|
||||
|
||||
## Closure and force implementation
|
||||
|
||||
All changes preserve quadrature, equations, field maps, signed element DOFs,
|
||||
primal/dual transformations, and true/local transfers. There is no tolerance
|
||||
based dropping of closure or vacuum terms.
|
||||
|
||||
- Closure couplings have supported-coordinate entry points for enthalpy alone
|
||||
and displacement alone. The preconditioner calls these rather than executing
|
||||
a complete action with known zero columns. The complete action still includes
|
||||
every column. Its geometric scalar remains
|
||||
`weightedResidual * tr(K * du/dxi)`, evaluated with a small direct contraction.
|
||||
- Pressure uses the existing physical test gradients G and computes H=du/dx.
|
||||
Its displacement contribution is `p*w*(G*H-tr(H)*G)`. This avoids constructing
|
||||
the inverse-Jacobian variation and the matrix of all gradient variations.
|
||||
- Gravity evaluates the existing expression
|
||||
`w_ref * [F*(delta_rho*g + rho*delta_g) + rho*(du/dxi)*(J_mesh^-1*g)]`.
|
||||
The last term contracts the reference gradients with a vector directly.
|
||||
Density interpolation and test-basis transpose are done per element; H(div)
|
||||
reference interpolation keeps the existing mesh Piola factor.
|
||||
- Rotation uses shared reference value/gradient tables, bulk interpolation and
|
||||
transpose application, and a cached 3x3 centrifugal acceleration derivative.
|
||||
The force variation is
|
||||
`w * [(delta_rho + rho*delta_log_w)*a + rho*delta_a]`.
|
||||
The acceleration derivative is rebuilt with the existing preparation lifecycle.
|
||||
|
||||
The new paths add per-element scratch buffers reused between elements and
|
||||
shared reference tables for rotation; they do not add large per-point dense
|
||||
Jacobian blocks. Existing data and reference paths are retained. Reference
|
||||
applications do not increment production column/complete application counters.
|
||||
|
||||
## Scalar and hydrostatic derivatives
|
||||
|
||||
For the supported stellar VALUE/GRAD displacement field, let `B_a` be the
|
||||
reference basis, `K` the inverse deformed element Jacobian, and `w` the mapped
|
||||
quadrature weight. An element displacement direction satisfies
|
||||
|
||||
```
|
||||
delta x_c = sum_a B_a delta u_ac
|
||||
delta log(w) = sum_acj K_jc (dB_a/dxi_j) delta u_ac
|
||||
```
|
||||
|
||||
The mass row integrates `w delta rho + rho w delta log(w)`. The moment-of-inertia
|
||||
row integrates `w r_perp^2 delta rho + rho w [r_perp^2 delta log(w) +
|
||||
2 r_perp dot delta x]`. Angular momentum applies `Omega delta I + I delta Omega`.
|
||||
The local coefficient vectors are assembled with signed element DOFs and dual
|
||||
DOF transformations, then paired with prolonged true-DOF directions. The final
|
||||
scalar retains the existing MPI sum. Keeping these as local duals avoids
|
||||
double-counting shared true DOFs. Mass transpose uses the transpose prolongation.
|
||||
|
||||
Hydrostatic displacement contracts
|
||||
`w [(h + Phi - Psi - C) delta log(w) - grad(Psi) dot delta x]`, then applies the
|
||||
existing enthalpy test-basis transpose and assembly. It does not construct a
|
||||
full mapping variation or repeatedly evaluate reference shapes. Only 3D interior
|
||||
VALUE/GRAD elements use its fast path. Scalar row caching supports interior
|
||||
VALUE/GRAD elements (mass in dimensions 1-3; angular momentum is already 3D).
|
||||
Unsupported configurations retain the original general application. Density or
|
||||
geometry changes invalidate scalar rows; Omega/target-only updates reuse them.
|
||||
The equations, quadrature rules and solver tolerances are unchanged.
|
||||
|
||||
These include comparison with the stateless derivative, the general exterior-map
|
||||
fallback, primal-to-linearization transitions, candidate rejection/recovery,
|
||||
and Kelvin factor checks. The harness adds independent finite differences and
|
||||
multiple direction pairs. Compilation alone does not validate their outcomes.
|
||||
|
||||
## Preparation and tensor implementation details
|
||||
|
||||
Gravity forms the residual quadrature force from the already computed
|
||||
`rho * w_ref * F * g_ref`; rotation uses `rho * w * acceleration`.
|
||||
Each then applies the cached displacement test-basis transpose. The optimized
|
||||
preparation visits geometry once, retains element DOF transforms and true/local
|
||||
assembly, and synchronizes candidate rejection before collective assembly.
|
||||
`PrepareReference` retains the separate stateless residual pass. Switching
|
||||
preparation implementations invalidates the relevant cached result even if
|
||||
physical dependency revisions are unchanged.
|
||||
|
||||
Pressure retains the same local enthalpy matrix and its application. It packs
|
||||
`-P'(h) * w * grad(test)` once per element and accumulates 32-row by 8-column
|
||||
matrix tiles with contiguous row access. The original per-quadrature assembly
|
||||
remains available through `PrepareReference`. EOS, mapping and finite-result
|
||||
checks remain active; no quadrature points or matrix entries are dropped.
|
||||
|
||||
The scalar reference-table cache now owns small one-dimensional value/gradient
|
||||
tables and a copied lexicographic-to-native DOF map for H1 hexahedra.
|
||||
Cartesian quadrature ordering and the reconstruction of the existing full
|
||||
values/gradients are checked once before publishing tensor support. Unsupported
|
||||
elements/rules use the existing path. Evaluation contracts x, y and z in turn
|
||||
and produces displacement values and reference gradients, then the hydrostatic
|
||||
operator applies the same inverse-Jacobian contraction, physical coefficients,
|
||||
test-basis transpose and assembly. Scratch is reused for one element; no dense
|
||||
per-element displacement Jacobian is added. The immutable tables own all tensor
|
||||
data, including when the FE, quadrature rule or cache that created them is gone.
|
||||
429
experiments/plateau_diagnostic.cpp
Normal file
429
experiments/plateau_diagnostic.cpp
Normal file
@@ -0,0 +1,429 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mfem.hpp>
|
||||
#include <numbers>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
import mean_field;
|
||||
#include "plateau_enthalpy_step_diagnostics.hpp"
|
||||
#include "plateau_physical_diagnostics.hpp"
|
||||
#include "plateau_preconditioner_diagnostics.hpp"
|
||||
|
||||
using namespace mean_field;
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
struct Options {
|
||||
double n = 1., angularMomentum = 0., atol = 0., rtol = 1e-8, linearTolerance = .03;
|
||||
int maxNewton = 12, maxLinear = 200, restart = 40;
|
||||
bool probes = false, noCentral = false;
|
||||
std::string mesh = "sandbox.smesh";
|
||||
std::filesystem::path output, probeState;
|
||||
};
|
||||
Options Parse(int argc, char **argv) {
|
||||
Options o;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
auto v = [&]() {
|
||||
if (++i >= argc)
|
||||
throw std::invalid_argument("Missing value");
|
||||
return std::string(argv[i]);
|
||||
};
|
||||
if (a == "--index")
|
||||
o.n = std::stod(v());
|
||||
else if (a == "--angular-momentum")
|
||||
o.angularMomentum = std::stod(v());
|
||||
else if (a == "--output")
|
||||
o.output = v();
|
||||
else if (a == "--mesh")
|
||||
o.mesh = v();
|
||||
else if (a == "--max-newton")
|
||||
o.maxNewton = std::stoi(v());
|
||||
else if (a == "--max-linear")
|
||||
o.maxLinear = std::stoi(v());
|
||||
else if (a == "--restart")
|
||||
o.restart = std::stoi(v());
|
||||
else if (a == "--linear-tol")
|
||||
o.linearTolerance = std::stod(v());
|
||||
else if (a == "--atol")
|
||||
o.atol = std::stod(v());
|
||||
else if (a == "--rtol")
|
||||
o.rtol = std::stod(v());
|
||||
else if (a == "--probe-state")
|
||||
o.probeState = v();
|
||||
else if (a == "--probes")
|
||||
o.probes = true;
|
||||
else if (a == "--no-central")
|
||||
o.noCentral = true;
|
||||
else
|
||||
throw std::invalid_argument("Unknown option: " + a);
|
||||
}
|
||||
if (o.output.empty() || o.n < 1 || o.n >= 5 || o.maxNewton < 0 || o.maxLinear < 1 || o.restart < 1)
|
||||
throw std::invalid_argument("Invalid options");
|
||||
if (std::filesystem::exists(o.output))
|
||||
throw std::runtime_error("Output directory already exists");
|
||||
std::filesystem::create_directories(o.output);
|
||||
return o;
|
||||
}
|
||||
std::ofstream File(const std::filesystem::path &p) {
|
||||
std::ofstream s(p);
|
||||
s.exceptions(std::ios::failbit | std::ios::badbit);
|
||||
s << std::setprecision(17);
|
||||
return s;
|
||||
}
|
||||
double Norm(std::span<const mfem::real_t> v, int offset, int size) {
|
||||
long double sum = 0;
|
||||
for (int i = offset; i < offset + size; ++i)
|
||||
sum += (long double)v[i] * v[i];
|
||||
return std::sqrt(sum);
|
||||
}
|
||||
double Norm(const mfem::Vector &v, int offset, int size) {
|
||||
return Norm(std::span<const mfem::real_t>{v.GetData(), (size_t)v.Size()}, offset, size);
|
||||
}
|
||||
struct Scales {
|
||||
double K, rho, xi, mu;
|
||||
};
|
||||
Scales Derive(double n) {
|
||||
if (n == 1.)
|
||||
return {2. * utils::G / std::numbers::pi, std::numbers::pi / 4., std::numbers::pi, std::numbers::pi};
|
||||
auto le = seed::integrateLaneEmden(n, 2000., 1e-3);
|
||||
if (!le.firstZeroCoordinate)
|
||||
throw std::runtime_error("No Lane Emden root");
|
||||
const double xi = *le.firstZeroCoordinate, mu = -xi * xi * le.thetaDerivative(le.thetaDerivative.Size() - 1);
|
||||
const double rho = xi * xi * xi / (4 * std::numbers::pi * mu);
|
||||
return {4 * std::numbers::pi * utils::G * std::pow(rho, 1. - 1. / n) / ((n + 1) * xi * xi), rho, xi, mu};
|
||||
}
|
||||
void SaveVector(const std::filesystem::path &p, const mfem::Vector &v) {
|
||||
auto s = File(p);
|
||||
s << v.Size() << '\n';
|
||||
v.Print(s, 1);
|
||||
}
|
||||
|
||||
template <class State>
|
||||
void FrozenProbes(State &s, const fem::FEM &f, const Options &o, const mfem::Vector *physicalBase = nullptr) {
|
||||
auto rows = s.Problem().GetManifest().residualBlocks();
|
||||
mfem::Vector frozenState(s.acceptedNormalizedState);
|
||||
if (physicalBase)
|
||||
s.normalizedOperator->NormalizeState(*physicalBase, frozenState);
|
||||
auto restoreFrozen = [&]() {
|
||||
s.trialNormalizedState = frozenState;
|
||||
auto p = s.PrepareTrial();
|
||||
if (!p)
|
||||
throw std::runtime_error("Frozen diagnostic state is inadmissible");
|
||||
};
|
||||
restoreFrozen();
|
||||
s.physicalInverse->Refresh();
|
||||
const mfem::Vector base(s.trialNormalizedResidual);
|
||||
if (base.Norml2() == 0.)
|
||||
return;
|
||||
auto results = File(o.output / "frozen_linear.csv");
|
||||
results << "restart,tolerance,iterations,converged,true_relative,seconds\n";
|
||||
auto histories = File(o.output / "frozen_history.csv");
|
||||
histories << "restart,tolerance,iteration,reported_norm,final\n";
|
||||
auto blocks = File(o.output / "frozen_blocks.csv");
|
||||
blocks << "restart,tolerance,block,F_norm,Jp_plus_F_norm,global_correction_norm\n";
|
||||
mfem::Vector best, baselineAction(base.Size());
|
||||
for (int restart : {40, 160}) {
|
||||
mfem::FGMRESSolver linear(MPI_COMM_WORLD);
|
||||
solver::ResidualHistoryMonitor monitor;
|
||||
linear.SetPreconditioner(*s.normalizedInverse);
|
||||
linear.SetOperator(*s.normalizedOperator);
|
||||
linear.SetKDim(restart);
|
||||
linear.SetRelTol(0.);
|
||||
linear.SetAbsTol(o.linearTolerance * base.Norml2());
|
||||
linear.SetMaxIter(o.maxLinear);
|
||||
linear.SetPrintLevel(-1);
|
||||
linear.SetMonitor(monitor);
|
||||
linear.iterative_mode = false;
|
||||
mfem::Vector b(base), p(base.Size()), action(base.Size());
|
||||
b *= -1.;
|
||||
p = 0.;
|
||||
const auto start = Clock::now();
|
||||
linear.Mult(b, p);
|
||||
double seconds = std::chrono::duration<double>(Clock::now() - start).count();
|
||||
s.normalizedOperator->Mult(p, action);
|
||||
action += base;
|
||||
const double rel = action.Norml2() / base.Norml2();
|
||||
results << restart << ',' << o.linearTolerance << ',' << linear.GetNumIterations() << ','
|
||||
<< linear.GetConverged() << ',' << rel << ',' << seconds << '\n';
|
||||
results.flush();
|
||||
for (const auto &h : monitor.GetHistory())
|
||||
histories << restart << ',' << o.linearTolerance << ',' << h.iteration << ',' << h.reportedNorm << ','
|
||||
<< h.final << '\n';
|
||||
histories.flush();
|
||||
for (const auto &r : rows)
|
||||
blocks << restart << ',' << o.linearTolerance << ',' << r.stableId << ',' << Norm(base, r.offset, r.size)
|
||||
<< ',' << Norm(action, r.offset, r.size) << ',' << p.Norml2() << '\n';
|
||||
blocks.flush();
|
||||
std::cout << "Frozen restart=" << restart << " true=" << rel << " seconds=" << seconds << std::endl;
|
||||
if (restart == 40)
|
||||
best = p;
|
||||
}
|
||||
s.normalizedCorrection = best;
|
||||
s.normalizedOperator->Mult(best, baselineAction);
|
||||
auto positivity = File(o.output / "enthalpy_step_bound.csv");
|
||||
positivity << "metric,value\n";
|
||||
for (const auto &[key, value] : experiment::plateau_diagnostics::MeasureEnthalpyStep(s, f, o.n, physicalBase))
|
||||
positivity << key << ',' << value << '\n';
|
||||
positivity.flush();
|
||||
SaveVector(o.output / "probe_normalized_correction.txt", best);
|
||||
auto fd = File(o.output / "finite_differences.csv");
|
||||
fd << "epsilon,admissible,block,absolute_error,relative_to_Jp,action_norm\n";
|
||||
const auto safe = s.EstimateLargestSafeStepSize(1., .9);
|
||||
if (!std::isfinite(safe.stepSize) || safe.stepSize <= 0.)
|
||||
return;
|
||||
auto trials = File(o.output / "frozen_trials.csv");
|
||||
trials << "alpha,admissible,rejection_reason,F_norm,merit_ratio\n";
|
||||
// Diagnostic trials are never committed. Each samples the same frozen Newton direction.
|
||||
for (double a : {1., .5, .1, .01, .001, .0001, .00001}) {
|
||||
const double epsilon = std::min(1., safe.stepSize) * a;
|
||||
s.trialNormalizedState = frozenState;
|
||||
s.trialNormalizedState.Add(epsilon, best);
|
||||
const auto prep = s.PrepareTrial();
|
||||
if (!prep) {
|
||||
trials << epsilon << ",0," << int(prep.error().reason) << ",nan,nan\n";
|
||||
fd << epsilon << ",0,all,nan,nan,nan\n";
|
||||
} else {
|
||||
trials << epsilon << ",1,-1," << s.trialNormalizedResidual.Norml2() << ','
|
||||
<< std::pow(s.trialNormalizedResidual.Norml2() / base.Norml2(), 2) << '\n';
|
||||
mfem::Vector error(s.trialNormalizedResidual);
|
||||
error -= base;
|
||||
error /= epsilon;
|
||||
error -= baselineAction;
|
||||
for (const auto &r : rows) {
|
||||
double e = Norm(error, r.offset, r.size), j = Norm(baselineAction, r.offset, r.size);
|
||||
fd << epsilon << ",1," << r.stableId << ',' << e << ',' << e / std::max(j, 1e-300) << ',' << j << '\n';
|
||||
}
|
||||
}
|
||||
trials.flush();
|
||||
fd.flush();
|
||||
restoreFrozen();
|
||||
}
|
||||
}
|
||||
|
||||
template <class Context> int RunContext(Context &context, const Options &o, const Scales &scales) {
|
||||
std::vector<operators::RootBlockDescriptor> rows, values;
|
||||
mfem::Vector residualFactors;
|
||||
std::vector<int> centralRows;
|
||||
int centralOffset = -1;
|
||||
auto metadata = File(o.output / "metadata.txt");
|
||||
metadata << "n=" << o.n << "\nK=" << scales.K << "\nrho_c=" << scales.rho << "\nJ=" << o.angularMomentum
|
||||
<< "\nM=1\nR_reference=1\nxi1=" << scales.xi << "\nmu1=" << scales.mu << "\ncentral_phase=" << !o.noCentral
|
||||
<< "\nrestart=" << o.restart << "\nlinear_tol=" << o.linearTolerance << "\nmax_linear=" << o.maxLinear
|
||||
<< "\nmax_newton=" << o.maxNewton << "\natol=" << o.atol << "\nrtol=" << o.rtol
|
||||
<< "\ncompiler=" << __VERSION__ << "\ncompiled=" << __DATE__ << ' ' << __TIME__ << '\n';
|
||||
metadata.flush();
|
||||
if (!o.probeState.empty()) {
|
||||
if (o.maxNewton != 0)
|
||||
throw std::invalid_argument("--probe-state requires --max-newton 0");
|
||||
metadata << "probe_state=" << o.probeState << '\n';
|
||||
metadata.flush();
|
||||
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &f) {
|
||||
std::ifstream input(o.probeState);
|
||||
int count = 0;
|
||||
input >> count;
|
||||
if (count != s.AcceptedPhysicalState().Size())
|
||||
throw std::runtime_error("Snapshot size mismatch");
|
||||
mfem::Vector snapshot(count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
input >> snapshot(i);
|
||||
if (!input || !std::isfinite(snapshot(i)))
|
||||
throw std::runtime_error("Invalid snapshot");
|
||||
}
|
||||
s.normalizedOperator->NormalizeState(snapshot, s.trialNormalizedState);
|
||||
auto prep = s.PrepareTrial();
|
||||
if (!prep)
|
||||
throw std::runtime_error("Snapshot is inadmissible");
|
||||
std::cout << "Snapshot F=" << s.trialNormalizedResidual.Norml2() << std::endl;
|
||||
experiment::plateau_diagnostics::InspectPreconditioner(s, o.output);
|
||||
FrozenProbes(s, f, o, &snapshot);
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
auto physicalMetrics = [&](auto &s, const fem::FEM &f, const std::string &label) {
|
||||
std::cout << label << " physical diagnostics" << std::endl;
|
||||
experiment::polytrope_validation::PhysicalState physical(s, f);
|
||||
auto out = File(o.output / (label + "_physical.csv"));
|
||||
out << "quadrature,metric,value\n";
|
||||
for (int q : {14, 18}) {
|
||||
auto metrics = experiment::plateau_diagnostics::MeasurePhysical(physical, o.n, scales.K, q);
|
||||
metrics["normalized_bordered_residual"] = physical.normalizedBorderedResidualNorm;
|
||||
metrics["normalized_unbordered_residual"] = physical.normalizedUnborderedResidualNorm;
|
||||
metrics["central_border"] = physical.centralBorder;
|
||||
metrics["normalized_border_action"] = physical.normalizedCentralBorderActionNorm;
|
||||
metrics["minimum_sampled_jacobian"] = s.acceptedMinimumJacobianDeterminant;
|
||||
metrics["mass_absolute_error"] = std::abs(metrics["mass"] - 1.);
|
||||
metrics["angular_momentum_absolute_error"] = std::abs(metrics["angular_momentum_z"] - o.angularMomentum);
|
||||
std::cout << " q=" << q << " virial_h=" << metrics["enthalpy_virial_error"] << " M=" << metrics["mass"]
|
||||
<< std::endl;
|
||||
for (auto &[k, v] : metrics)
|
||||
out << q << ',' << k << ',' << v << '\n';
|
||||
out.flush();
|
||||
}
|
||||
};
|
||||
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &f) {
|
||||
auto r = s.Problem().GetManifest().residualBlocks(), v = s.Problem().GetManifest().valueBlocks();
|
||||
rows.assign(r.begin(), r.end());
|
||||
values.assign(v.begin(), v.end());
|
||||
residualFactors = s.normalizedOperator->GetNormalization().ResidualFactors();
|
||||
for (const auto &b : values)
|
||||
if (b.stableId == "fixed_central_density.border")
|
||||
centralOffset = b.offset;
|
||||
experiment::polytrope_validation::PhysicalState physical(s, f);
|
||||
centralRows = physical.centralHydrostaticRows;
|
||||
auto layout = File(o.output / "layout.csv");
|
||||
layout << "kind,block,offset,size,scale\n";
|
||||
for (auto &b : rows)
|
||||
layout << "residual," << b.stableId << ',' << b.offset << ',' << b.size << ',' << b.scale << '\n';
|
||||
for (auto &b : values)
|
||||
layout << "value," << b.stableId << ',' << b.offset << ',' << b.size << ',' << b.scale << '\n';
|
||||
physicalMetrics(s, f, "initial");
|
||||
});
|
||||
auto history = File(o.output / "newton.csv");
|
||||
history << "iteration,accepted,F,alpha,trials,linear_status,linear_iterations,linear_true,seconds,linear_seconds,"
|
||||
"prepare_seconds,refresh_seconds,geometry_limited,geometry_safe_step,central_border,unbordered_F\n";
|
||||
auto blocklog = File(o.output / "blocks.csv");
|
||||
blocklog << "iteration,stage,block,normalized_norm,physical_norm,max_abs_normalized\n";
|
||||
auto triallog = File(o.output / "trials.csv");
|
||||
triallog << "iteration,trial,alpha,disposition,source,F,min_jacobian\n";
|
||||
auto record = [&](int it, const char *stage, std::span<const mfem::real_t> residual) {
|
||||
for (const auto &b : rows) {
|
||||
long double ps = 0;
|
||||
double mx = 0;
|
||||
for (int i = b.offset; i < b.offset + b.size; ++i) {
|
||||
const double p = residual[i] / residualFactors(i);
|
||||
ps += (long double)p * p;
|
||||
mx = std::max(mx, std::abs(residual[i]));
|
||||
}
|
||||
blocklog << it << ',' << stage << ',' << b.stableId << ',' << Norm(residual, b.offset, b.size) << ','
|
||||
<< std::sqrt(ps) << ',' << mx << '\n';
|
||||
}
|
||||
blocklog.flush();
|
||||
};
|
||||
auto observer = solver::nonlinear::makeObserver(
|
||||
[&](const solver::nonlinear::BeforeIteration &e) {
|
||||
record(e.iteration, "before", e.normalizedResidual);
|
||||
std::cout << "Newton " << e.iteration << " F=" << e.residualNorm << std::endl;
|
||||
},
|
||||
[&](const solver::nonlinear::AfterLineSearchTrial &e) {
|
||||
triallog << e.iteration << ',' << e.trial << ',' << e.stepLength << ',' << int(e.disposition) << ','
|
||||
<< e.rejectionSource << ',' << (e.metric ? e.metric->residualNorm : NAN) << ','
|
||||
<< e.minimumJacobianDeterminant.value_or(NAN) << '\n';
|
||||
triallog.flush();
|
||||
},
|
||||
[&](const solver::nonlinear::AfterIteration &e) {
|
||||
record(e.iteration, "after", e.normalizedResidual);
|
||||
double border = centralOffset < 0 ? 0 : e.physicalState[centralOffset];
|
||||
mfem::Vector unbordered((int)e.normalizedResidual.size());
|
||||
for (int i = 0; i < unbordered.Size(); ++i)
|
||||
unbordered(i) = e.normalizedResidual[i];
|
||||
for (int i : centralRows)
|
||||
unbordered(i) -= residualFactors(i) * border;
|
||||
history << e.iteration << ',' << e.stepAccepted << ',' << e.residualNorm << ',' << e.acceptedStepLength
|
||||
<< ',' << e.lineSearchTrials << ',' << (e.linearSolve ? int(e.linearSolve->status) : -1) << ','
|
||||
<< (e.linearSolve ? e.linearSolve->iterations : 0) << ','
|
||||
<< (e.linearSolve ? e.linearSolve->relativeTrueResidualNorm : NAN) << ',' << e.iterationSeconds
|
||||
<< ',' << (e.linearSolve ? e.linearSolve->solveSeconds : 0) << ',' << e.trialPreparationSeconds
|
||||
<< ',' << e.preconditionerRefreshSeconds << ','
|
||||
<< (e.geometryPreflight ? e.geometryPreflight->limitedByGeometry : false) << ','
|
||||
<< (e.geometryPreflight ? e.geometryPreflight->stepSize : NAN) << ',' << border << ','
|
||||
<< unbordered.Norml2() << '\n';
|
||||
history.flush();
|
||||
std::cout << " alpha=" << e.acceptedStepLength << " F=" << e.residualNorm
|
||||
<< " linear=" << (e.linearSolve ? e.linearSolve->relativeTrueResidualNorm : NAN)
|
||||
<< " lambda=" << border << " unbordered=" << unbordered.Norml2()
|
||||
<< " seconds=" << e.iterationSeconds << std::endl;
|
||||
});
|
||||
bool converged = false;
|
||||
if (o.maxNewton > 0) {
|
||||
auto report = [&]() {
|
||||
auto solve = solver::make(context,
|
||||
solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{
|
||||
.relativeTolerance = o.rtol,
|
||||
.absoluteTolerance = o.atol,
|
||||
.maximumIterations = o.maxNewton,
|
||||
.linearSolve = {.relativeTolerance = o.linearTolerance,
|
||||
.absoluteTolerance = 0.,
|
||||
.maximumIterations = o.maxLinear}}),
|
||||
observer);
|
||||
return solve.evaluate();
|
||||
}();
|
||||
converged = report.converged();
|
||||
metadata << "converged=" << converged << "\naccepted_steps=" << report.completedNonlinearIterations() << '\n';
|
||||
if (!converged)
|
||||
metadata << "failure=" << report.failure().message << '\n';
|
||||
metadata.flush();
|
||||
}
|
||||
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &f) {
|
||||
SaveVector(o.output / "accepted_state.txt", s.AcceptedPhysicalState());
|
||||
physicalMetrics(s, f, "final");
|
||||
const auto &schur = s.physicalInverse->GetGroupedPreconditioner().GetFactorization().GetSchurComplement();
|
||||
auto sh = File(o.output / "border_schur.csv");
|
||||
for (int i = 0; i < schur.Height(); ++i) {
|
||||
for (int j = 0; j < schur.Width(); ++j)
|
||||
sh << (j ? "," : "") << schur(i, j);
|
||||
sh << '\n';
|
||||
}
|
||||
if (o.probes) {
|
||||
experiment::plateau_diagnostics::InspectPreconditioner(s, o.output);
|
||||
FrozenProbes(s, f, o);
|
||||
}
|
||||
});
|
||||
return converged ? 0 : 3;
|
||||
}
|
||||
template <class Model> int Setup(Model model, fem::FEM f, const Options &o, const Scales &scales) {
|
||||
auto d = equilibrium::makeStellarDiscretization(
|
||||
std::move(f), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{1.}, utils::G});
|
||||
auto context = solver::makeContext(std::move(model), std::move(d), preconditioning::makePreconditioner(),
|
||||
solver::linear::FGMRES({.restartLength = o.restart, .printLevel = -1}),
|
||||
seed::LaneEmden({.centralDensity = dimensions::DensityValue{scales.rho}}));
|
||||
return RunContext(context, o, scales);
|
||||
}
|
||||
int Run(const Options &o) {
|
||||
const auto scales = Derive(o.n);
|
||||
std::cout << "n=" << o.n << " K=" << scales.K << " rho_c=" << scales.rho << " J=" << o.angularMomentum << std::endl;
|
||||
utils::Args args;
|
||||
args.mesh_file = o.mesh;
|
||||
args.p.rtol = 1e-12;
|
||||
args.p.atol = 1e-12;
|
||||
auto f = fem::setup_fem(o.mesh, args, 0);
|
||||
if (!f.okay())
|
||||
throw std::runtime_error("FEM setup failed");
|
||||
auto eos = eos::Polytrope({.n = o.n, .K = scales.K});
|
||||
auto surf = surface::Isobaric({.Psurf = dimensions::PressureValue{0.}});
|
||||
auto mass = integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.}});
|
||||
auto angular = integral::FixedAngularMomentum(
|
||||
{.Jtotal = dimensions::AngularMomentumValue{o.angularMomentum}, .axis = {0., 0., 1.}, .center = {0., 0., 0.}});
|
||||
if (o.noCentral)
|
||||
return Setup(model::StellarModel(eos, surf, mass, angular), std::move(f), o, scales);
|
||||
return Setup(model::StellarModel(eos, surf, mass, angular,
|
||||
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{scales.rho}})),
|
||||
std::move(f), o, scales);
|
||||
}
|
||||
} // namespace
|
||||
int main(int argc, char **argv) {
|
||||
mfem::Mpi::Init(argc, argv);
|
||||
int result = 0;
|
||||
try {
|
||||
int count;
|
||||
MPI_Comm_size(MPI_COMM_WORLD, &count);
|
||||
if (count != 1)
|
||||
throw std::runtime_error("Single rank only");
|
||||
mfem::Device device("cpu");
|
||||
std::cout << std::setprecision(12);
|
||||
result = Run(Parse(argc, argv));
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "Diagnostic failure: " << e.what() << std::endl;
|
||||
result = 2;
|
||||
}
|
||||
mfem::Mpi::Finalize();
|
||||
return result;
|
||||
}
|
||||
93
experiments/plateau_diagnostic_README.md
Normal file
93
experiments/plateau_diagnostic_README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Plateau diagnostics
|
||||
|
||||
This opt-in experiment runs the current production context, operator, normalization,
|
||||
preconditioner and Newton implementation. It adds observations and uncommitted frozen
|
||||
trials. It does not select a replacement numerical algorithm.
|
||||
|
||||
Build from the repository root:
|
||||
|
||||
```sh
|
||||
cmake --build cmake-build-release-homebrew --target plateau_diagnostic -j 2
|
||||
```
|
||||
|
||||
Run a calibrated spherical family with `G=M=R_reference=1`:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/plateau_diagnostic \
|
||||
--index 1.5 --angular-momentum 0 --max-newton 8 \
|
||||
--output /absolute/path/to/new-results
|
||||
```
|
||||
|
||||
The output directory must not already exist. The mesh defaults to the repository's
|
||||
`sandbox.smesh`; `--mesh` selects another mesh. The driver is restricted to one MPI
|
||||
rank. An MPI runtime must be available even for a single-rank run.
|
||||
|
||||
For each index, the driver obtains K and central density from the Lane–Emden
|
||||
solution so the spherical reference mass and radius are one. Index one uses its
|
||||
analytic constants. Other indices use the production radial integrator, so this
|
||||
calibration is seed-consistent, not an independent exact reference. Angular momentum
|
||||
sets a physical target, not a fraction of critical rotation. `--no-central` omits
|
||||
the central phase for controlled comparisons; it changes the model specification.
|
||||
|
||||
Default solver controls are restart 40, 200 linear iterations, linear relative
|
||||
tolerance 0.03, Newton relative tolerance 1e-8 and absolute tolerance zero. Override
|
||||
with `--restart`, `--max-linear`, `--linear-tol`, `--rtol`, `--atol`, and
|
||||
`--max-newton`. Exit 0 means the requested Newton criterion was met, 3 means it was
|
||||
not met (or a seed-only inspection was requested), and 2 means setup/diagnostics
|
||||
failed. A nonconverged result still saves the retained accepted state and its
|
||||
physical measurements.
|
||||
|
||||
## Frozen investigations
|
||||
|
||||
Append `--probes` to inspect the retained state after a solve. This compares cold
|
||||
FGMRES windows 40 and 160 at the same iteration budget and independently recomputes
|
||||
both true residuals. It also exports normalization consistency checks, the scalar
|
||||
Schur matrix and its coordinate labels, inverse-diagonal quality, and full-Jacobian
|
||||
actions on preconditioner-completed scalar directions.
|
||||
|
||||
To inspect a previously saved physical root vector without repeating Newton:
|
||||
|
||||
```sh
|
||||
./cmake-build-release-homebrew/plateau_diagnostic \
|
||||
--index 1.5 --angular-momentum 0 --max-newton 0 \
|
||||
--probe-state /absolute/path/to/previous/accepted_state.txt \
|
||||
--output /absolute/path/to/new-frozen-results
|
||||
```
|
||||
|
||||
Use the same mesh, compiled field orders, index, K/central-density calibration, and
|
||||
model flags. The snapshot is prepared as an uncommitted trial inside the existing
|
||||
diagnostic hook; the context's accepted state is unchanged. This is a frozen
|
||||
inspection facility, not a Newton continuation/restart facility. The saved vector
|
||||
is dimension-checked, but does not contain a mesh/model fingerprint.
|
||||
|
||||
Finite-difference and nonlinear-trial probes always use the **window-40 direction**,
|
||||
even if window 160 obtains a better correction. They compute the exact sampled
|
||||
enthalpy step bounds for both production closure and pressure-force quadrature.
|
||||
Every trial is restored and none is committed. Finite differences are one-sided;
|
||||
interpret their epsilon trend and absolute error, particularly for tiny Jacobian
|
||||
actions. `global_correction_norm` in the block table is the whole-vector norm.
|
||||
|
||||
## Physical measurements
|
||||
|
||||
The observer integrates over stellar material only, at independent quadrature
|
||||
orders 14 and 18. It reconstructs physical positions and Piola-transformed gravity
|
||||
flux. For zero surface pressure it measures
|
||||
|
||||
- `W = 1/2 integral rho Phi`, `W_force = -integral rho x dot gradPhi`;
|
||||
- rotational energy T and both `P(h)` and `P(rho)` pressure integrals;
|
||||
- energy and force virials `abs(2T + W + 3 integral P) / abs(W_energy)`;
|
||||
- mass, angular momentum, center of mass, Bernoulli and EOS mismatch, hydrostatic
|
||||
gradient mismatch, sampled density/enthalpy signs, and geometry quality.
|
||||
|
||||
Pressure is evaluated with the actual polytropic index, including noninteger n.
|
||||
Signed finite DG density is retained in mass/gravity/rotation integrals. Any
|
||||
negative density sample makes the complete `P(rho)` pressure and virial undefined
|
||||
(NaN); `P(h)` remains available for positive enthalpy. The observer reports the
|
||||
negative-density mass and volume fractions instead of silently clipping them.
|
||||
NaN in `P(rho)` alone is not a quantitative measure of the global physical error.
|
||||
Quadrature samples are not a global positivity certificate.
|
||||
|
||||
`normalized_unbordered_residual` removes the central phase's artificial source
|
||||
from the hydrostatic equation. A small bordered residual can otherwise hide a
|
||||
pointwise modification at the central vertex. Virial success and Newton stopping
|
||||
success are deliberately separate measurements.
|
||||
226
experiments/plateau_enthalpy_step_diagnostics.hpp
Normal file
226
experiments/plateau_enthalpy_step_diagnostics.hpp
Normal file
@@ -0,0 +1,226 @@
|
||||
#pragma once
|
||||
|
||||
// Include after import mean_field. This observer does not change accepted
|
||||
// coefficients, the prepared operator, or the Newton correction.
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
namespace experiment::plateau_diagnostics {
|
||||
// Computes the forward h>=0 boundary along the CURRENT normalizedCorrection
|
||||
// on exactly the production closure and pressure-force quadrature points.
|
||||
// n must match the active polytropic EOS. Scalar
|
||||
// enthalpy coefficients pull back unchanged under the moving-domain map,
|
||||
// so h_q(alpha)=h_q(0)+alpha*dh_q: no geometry derivative is needed here.
|
||||
//
|
||||
// Source convention: prepared_barotropic_closure.cpp get_eos_rule (lines
|
||||
// 102-157) and TryPrepare's basis contraction/domain check (lines 593-632).
|
||||
// The production operator rejects h<0 strictly, despite the standalone
|
||||
// Polytrope EOS extension to h<=0. Pressure-force uses the PressureForce
|
||||
// query with extraOrder=ceil(n*p_h), as in prepared_pressure_force.cpp
|
||||
// lines 226-282, and also rejects h<0 (lines 787-803). Its tensor and dense
|
||||
// contraction paths sample the same polynomial and can differ at roundoff.
|
||||
// The union bound concerns h>=0 only; geometry and other finite-data tests
|
||||
// can still impose stricter limits.
|
||||
//
|
||||
// alpha_h=+infinity means no sampled enthalpy upper bound in this direction.
|
||||
// alpha_h=NaN means the accepted base was already invalid or a sampled value
|
||||
// was nonfinite. alpha_h=0 means a sampled h=0 has dh<0. A step exactly at
|
||||
// the reported boundary can differ at roundoff due to floating-point state
|
||||
// addition and contraction. alpha_h remains the closure alias;
|
||||
// closure_alpha_h, pressure_alpha_h, and alpha_thermodynamic are explicit.
|
||||
// No candidate is prepared or committed here. physicalBase optionally
|
||||
// selects a caller-owned frozen trial state instead of the accepted state;
|
||||
// its coefficients must use this context's physical root layout.
|
||||
template <class State>
|
||||
std::map<std::string, double> MeasureEnthalpyStep(State &state, const mean_field::fem::FEM &fem, const double n,
|
||||
const mfem::Vector *physicalBase = nullptr) {
|
||||
if (!std::isfinite(n) || n < 1.0)
|
||||
throw std::invalid_argument("Enthalpy step diagnostics require finite n>=1.");
|
||||
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
|
||||
using ClosureDomain = mean_field::field::FieldDomainT<mean_field::field::Density>;
|
||||
using PressureDomain = mean_field::field::FieldDomainT<mean_field::field::Enthalpy>;
|
||||
using EnthalpyField = mean_field::field::Field<mean_field::field::Enthalpy>;
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(fem.mesh->GetComm(), &ranks);
|
||||
if (ranks != 1)
|
||||
throw std::invalid_argument("Enthalpy step diagnostics require exactly one MPI rank.");
|
||||
if (!fem.enthalpyFes || !fem.densityFes || !fem.displacementFes || !fem.quadratureFactory) {
|
||||
throw std::invalid_argument("Enthalpy step diagnostics received an incomplete FEM.");
|
||||
}
|
||||
const double maximumExtra = n * mean_field::field::Enthalpy::Scalar::familyOrder;
|
||||
if (!std::isfinite(maximumExtra) || maximumExtra > std::numeric_limits<int>::max()) {
|
||||
throw std::invalid_argument("Enthalpy step diagnostics received an invalid effective EOS order.");
|
||||
}
|
||||
int offset = -1, size = -1;
|
||||
for (const auto &block : state.Problem().GetManifest().valueBlocks()) {
|
||||
if (block.stableId == "specific_enthalpy") {
|
||||
offset = block.offset;
|
||||
size = block.size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mfem::Vector physicalCorrection;
|
||||
state.normalizedOperator->DenormalizeState(state.normalizedCorrection, physicalCorrection);
|
||||
const auto &accepted = physicalBase ? *physicalBase : state.AcceptedPhysicalState();
|
||||
mfem::ParGridFunction h(fem.enthalpyFes.get()), dh(fem.enthalpyFes.get());
|
||||
const auto adapter =
|
||||
mean_field::field::make_field_dof_grid_function_adapter<mean_field::field::Enthalpy, DomainSchema>(
|
||||
*fem.enthalpyFes);
|
||||
if (offset < 0 || size != adapter.dof_map().reduced_size() || offset + size > accepted.Size() ||
|
||||
physicalCorrection.Size() != accepted.Size()) {
|
||||
throw std::invalid_argument("Enthalpy step diagnostics found an incompatible enthalpy block.");
|
||||
}
|
||||
mfem::Vector reducedH(size), reducedDh(size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
reducedH(i) = accepted(offset + i);
|
||||
reducedDh(i) = physicalCorrection(offset + i);
|
||||
}
|
||||
adapter.scatter(reducedH, h);
|
||||
adapter.scatter(reducedDh, dh);
|
||||
|
||||
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||
const double inf = std::numeric_limits<double>::infinity();
|
||||
const auto scan = [&](const bool pressure) -> std::map<std::string, double> {
|
||||
const int extraOrder =
|
||||
static_cast<int>(std::ceil((pressure ? n : n - 1.0) * mean_field::field::Enthalpy::Scalar::familyOrder));
|
||||
double alpha = inf, minH = inf, minDh = inf, minFullStepH = inf;
|
||||
double limitingH = nan, limitingDh = nan;
|
||||
int limitingElement = -1, limitingPoint = -1, limitingAttribute = -1;
|
||||
std::array<double, 3> limitingIp{nan, nan, nan};
|
||||
int minimumHElement = -1, minimumHPoint = -1;
|
||||
int rulePointCountMin = std::numeric_limits<int>::max(), rulePointCountMax = 0;
|
||||
std::uint64_t samples = 0, nonfiniteSamples = 0, negativeBaseSamples = 0, decreasingSamples = 0,
|
||||
negativeFullStepSamples = 0;
|
||||
mfem::Array<int> dofs;
|
||||
mfem::Vector localH, localDh, shape;
|
||||
for (int element = 0; element < fem.mesh->GetNE(); ++element) {
|
||||
auto *transformation = fem.mesh->GetElementTransformation(element);
|
||||
const bool inSupport =
|
||||
pressure ? DomainSchema::template attribute_belongs_to<PressureDomain>(transformation->Attribute)
|
||||
: DomainSchema::template attribute_belongs_to<ClosureDomain>(transformation->Attribute);
|
||||
if (!inSupport)
|
||||
continue;
|
||||
const auto &densityElement = *fem.densityFes->GetFE(element);
|
||||
const auto &enthalpyElement = *fem.enthalpyFes->GetFE(element);
|
||||
if (enthalpyElement.GetOrder() != mean_field::field::Enthalpy::Scalar::familyOrder ||
|
||||
(!pressure && densityElement.GetOrder() != mean_field::field::Density::Scalar::familyOrder) ||
|
||||
(pressure && fem.displacementFes->GetFE(element)->GetOrder() !=
|
||||
mean_field::field::Displacement::Vector::familyOrder)) {
|
||||
throw std::invalid_argument(
|
||||
"Enthalpy step diagnostics require the registered production field orders.");
|
||||
}
|
||||
const auto query =
|
||||
pressure ? EnthalpyField::template make_query<mean_field::field::Enthalpy::Form::PressureForce>(
|
||||
mean_field::quadrature::QuadratureRole::discretization, transformation->OrderW(),
|
||||
std::array<int, 1>{extraOrder}, mean_field::utils::DOMAINS::STELLAR,
|
||||
mean_field::quadrature::MappingKind::general)
|
||||
: EnthalpyField::template make_query<mean_field::field::Enthalpy::Form::EosClosureSource>(
|
||||
mean_field::quadrature::QuadratureRole::discretization, transformation->OrderW(),
|
||||
std::array<int, 1>{extraOrder}, mean_field::utils::DOMAINS::STELLAR,
|
||||
mean_field::quadrature::MappingKind::general);
|
||||
const auto resolution = fem.quadratureFactory->get(query, transformation->GetGeometryType());
|
||||
if (!resolution.integration_rule)
|
||||
throw std::runtime_error("Missing production EOS quadrature rule.");
|
||||
const auto &rule = *resolution.integration_rule;
|
||||
rulePointCountMin = std::min(rulePointCountMin, rule.GetNPoints());
|
||||
rulePointCountMax = std::max(rulePointCountMax, rule.GetNPoints());
|
||||
const auto basis = fem.GetReferenceTables().GetScalarTable(enthalpyElement, rule);
|
||||
auto *dofTransformation = fem.enthalpyFes->GetElementDofs(element, dofs);
|
||||
h.GetSubVector(dofs, localH);
|
||||
dh.GetSubVector(dofs, localDh);
|
||||
if (dofTransformation) {
|
||||
dofTransformation->InvTransformPrimal(localH);
|
||||
dofTransformation->InvTransformPrimal(localDh);
|
||||
}
|
||||
shape.SetSize(enthalpyElement.GetDof());
|
||||
for (int q = 0; q < rule.GetNPoints(); ++q) {
|
||||
++samples;
|
||||
basis->GetValues().GetRow(q, shape);
|
||||
const double hq = localH * shape;
|
||||
const double dhq = localDh * shape;
|
||||
if (!std::isfinite(hq) || !std::isfinite(dhq)) {
|
||||
++nonfiniteSamples;
|
||||
continue;
|
||||
}
|
||||
if (hq < minH) {
|
||||
minH = hq;
|
||||
minimumHElement = element;
|
||||
minimumHPoint = q;
|
||||
}
|
||||
minDh = std::min(minDh, dhq);
|
||||
minFullStepH = std::min(minFullStepH, hq + dhq);
|
||||
negativeBaseSamples += hq < 0.0;
|
||||
negativeFullStepSamples += hq + dhq < 0.0;
|
||||
if (dhq < 0.0) {
|
||||
++decreasingSamples;
|
||||
const double candidate = -hq / dhq;
|
||||
if (candidate < alpha) {
|
||||
alpha = candidate;
|
||||
limitingH = hq;
|
||||
limitingDh = dhq;
|
||||
limitingElement = element;
|
||||
limitingPoint = q;
|
||||
limitingAttribute = transformation->Attribute;
|
||||
const auto &ip = rule.IntPoint(q);
|
||||
limitingIp = {ip.x, ip.y, ip.z};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool valid = samples > 0 && nonfiniteSamples == 0 && negativeBaseSamples == 0;
|
||||
return {{"alpha_h", valid ? alpha : nan},
|
||||
{"alpha_h_raw_crossing", alpha},
|
||||
{"alpha_h_clipped_to_one", valid ? std::min(1.0, alpha) : nan},
|
||||
{"enthalpy_base_admissible", valid ? 1.0 : 0.0},
|
||||
{"eos_extra_order", static_cast<double>(extraOrder)},
|
||||
{"samples", static_cast<double>(samples)},
|
||||
{"nonfinite_samples", static_cast<double>(nonfiniteSamples)},
|
||||
{"negative_base_samples", static_cast<double>(negativeBaseSamples)},
|
||||
{"decreasing_samples", static_cast<double>(decreasingSamples)},
|
||||
{"predicted_negative_full_step_samples", static_cast<double>(negativeFullStepSamples)},
|
||||
{"minimum_h", samples > nonfiniteSamples ? minH : nan},
|
||||
{"minimum_dh", samples > nonfiniteSamples ? minDh : nan},
|
||||
{"predicted_minimum_h_full_step", samples > nonfiniteSamples ? minFullStepH : nan},
|
||||
{"minimum_h_element", static_cast<double>(minimumHElement)},
|
||||
{"minimum_h_point", static_cast<double>(minimumHPoint)},
|
||||
{"limiting_element", static_cast<double>(limitingElement)},
|
||||
{"limiting_point", static_cast<double>(limitingPoint)},
|
||||
{"limiting_attribute", static_cast<double>(limitingAttribute)},
|
||||
{"limiting_h", limitingH},
|
||||
{"limiting_dh", limitingDh},
|
||||
{"limiting_ip_x", limitingIp[0]},
|
||||
{"limiting_ip_y", limitingIp[1]},
|
||||
{"limiting_ip_z", limitingIp[2]},
|
||||
{"quadrature_points_per_element_minimum", samples ? static_cast<double>(rulePointCountMin) : nan},
|
||||
{"quadrature_points_per_element_maximum", static_cast<double>(rulePointCountMax)}};
|
||||
};
|
||||
auto result = scan(false);
|
||||
const auto pressure = scan(true);
|
||||
result["closure_alpha_h"] = result.at("alpha_h");
|
||||
result["closure_samples"] = result.at("samples");
|
||||
for (const auto &[name, value] : pressure)
|
||||
result["pressure_" + name] = value;
|
||||
const bool unionValid =
|
||||
result.at("enthalpy_base_admissible") != 0.0 && pressure.at("enthalpy_base_admissible") != 0.0;
|
||||
const double unionAlpha = unionValid ? std::min(result.at("alpha_h"), pressure.at("alpha_h")) : nan;
|
||||
result["alpha_thermodynamic"] = unionAlpha;
|
||||
result["alpha_thermodynamic_clipped_to_one"] = unionValid ? std::min(1.0, unionAlpha) : nan;
|
||||
result["thermodynamic_base_admissible"] = unionValid ? 1.0 : 0.0;
|
||||
// 0=closure, 1=pressure-force, -1=invalid or no finite upper bound.
|
||||
result["thermodynamic_limiting_family"] =
|
||||
unionValid && std::isfinite(unionAlpha) ? (pressure.at("alpha_h") < result.at("alpha_h") ? 1.0 : 0.0) : -1.0;
|
||||
result["thermodynamic_minimum_h"] = std::isfinite(result.at("minimum_h")) && std::isfinite(pressure.at("minimum_h"))
|
||||
? std::min(result.at("minimum_h"), pressure.at("minimum_h"))
|
||||
: nan;
|
||||
return result;
|
||||
}
|
||||
} // namespace experiment::plateau_diagnostics
|
||||
319
experiments/plateau_physical_diagnostics.hpp
Normal file
319
experiments/plateau_physical_diagnostics.hpp
Normal file
@@ -0,0 +1,319 @@
|
||||
#pragma once
|
||||
|
||||
// Experiment-only observer. Include after `import mean_field;`, with the
|
||||
// repository's experiments directory on the include path. The live-state
|
||||
// PhysicalState constructor is generic in n and rotation; its saved-file replay
|
||||
// constructor is intentionally not used here.
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "polytrope_physical_state.hpp"
|
||||
|
||||
namespace experiment::plateau_diagnostics {
|
||||
using Measurements = std::map<std::string, double>;
|
||||
|
||||
// Assumes the production zero-surface-pressure problem, hence no external
|
||||
// pressure surface term in 2T + W + 3 integral(P). Both the gravitational
|
||||
// energy W=.5 integral(rho phi) and force W=-integral(rho x.grad(phi)) are
|
||||
// reported. The latter uses the independently represented physical RT flux,
|
||||
// not the broken gradient of the L2 potential. PhysicalState::Evaluate
|
||||
// already applies the deformation Piola transform after MFEM's reference
|
||||
// mesh Piola transform: no additional vector transformation belongs here.
|
||||
//
|
||||
// Call independently with orders 14 and 18 to assess quadrature sensitivity.
|
||||
// These are quadrature-sampled extrema, not global pointwise certificates.
|
||||
// Negative rho is not clipped: signed mass, W, T, J, and rho(h) mismatch are
|
||||
// preserved. P(rho) is undefined for negative rho, so its complete integral
|
||||
// and associated virials are NaN whenever any such samples occur. P(h) uses
|
||||
// the production EOS extension rho(h)=0, P(h)=0 for h<=0. Negative samples
|
||||
// and their volume/mass are explicitly reported.
|
||||
//
|
||||
// Invalid mapping/field samples invalidate all complete volume integrals;
|
||||
// they are counted instead of aborting measurement or silently dropping
|
||||
// their contribution. Parameters that cannot describe a polytrope throw.
|
||||
inline Measurements MeasurePhysical(experiment::polytrope_validation::PhysicalState &state, const double n,
|
||||
const double K, const int quadratureOrder = 14) {
|
||||
if (!std::isfinite(n) || n < 1.0 || !std::isfinite(K) || K <= 0.0 || quadratureOrder < 0) {
|
||||
throw std::invalid_argument(
|
||||
"Physical diagnostics require finite n>=1, finite K>0, and nonnegative quadrature order.");
|
||||
}
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(state.finiteElements.mesh->GetComm(), &ranks);
|
||||
if (ranks != 1)
|
||||
throw std::invalid_argument("Physical diagnostics require exactly one MPI rank.");
|
||||
|
||||
const long double index = n;
|
||||
const long double constant = K;
|
||||
const long double enthalpyFactor = (index + 1.0L) * constant;
|
||||
const auto &omega = state.rotation.angular_velocity();
|
||||
const auto ¢er = state.rotation.center();
|
||||
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||
const long double inf = std::numeric_limits<long double>::infinity();
|
||||
std::uint64_t samples = 0, validSamples = 0, invalidSamples = 0;
|
||||
std::uint64_t negativeRhoSamples = 0, negativeHSamples = 0;
|
||||
std::uint64_t invalidEnthalpyEosSamples = 0, invalidDensityEosSamples = 0;
|
||||
long double volume = 0.0L, mass = 0.0L, binding = 0.0L, forceBinding = 0.0L;
|
||||
long double pressureH = 0.0L, pressureRhoNonnegative = 0.0L, kinetic = 0.0L;
|
||||
long double negativeRhoVolume = 0.0L, negativeHVolume = 0.0L, negativeRhoMassMagnitude = 0.0L;
|
||||
long double closureSquared = 0.0L, closureIntegral = 0.0L, closureMax = 0.0L;
|
||||
long double pressureMismatchSquared = 0.0L, pressureMismatchVolume = 0.0L;
|
||||
long double bernoulliMean = 0.0L, bernoulliCenteredSquared = 0.0L, bernoulliRawSquared = 0.0L;
|
||||
long double bernoulliMin = inf, bernoulliMax = -inf;
|
||||
long double rhoMin = inf, rhoMax = -inf, hMin = inf, hMax = -inf, rhoFromHMax = 0.0L;
|
||||
long double minDeterminant = inf, inertiaZ = 0.0L;
|
||||
long double gradientMismatchSquared = 0.0L, hydrostaticGradientSquared = 0.0L;
|
||||
long double drhodhPositiveMin = inf, drhodhPositiveMax = 0.0L;
|
||||
std::array<long double, 3> firstMoment{}, angularMomentum{}, gravitationalForce{};
|
||||
experiment::polytrope_validation::PhysicalPoint point;
|
||||
mfem::Vector centrifugalGradient(3);
|
||||
|
||||
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) {
|
||||
++samples;
|
||||
const auto &ip = rule.IntPoint(q);
|
||||
if (state.Evaluate(element, ip, point) != mean_field::mapping::MappingStatus::valid) {
|
||||
++invalidSamples;
|
||||
continue;
|
||||
}
|
||||
// Evaluate may reuse MFEM's cached transformation; restore ip.
|
||||
transformation->SetIntPoint(&ip);
|
||||
const double referenceDeterminant = transformation->Jacobian().Det();
|
||||
const long double weight =
|
||||
static_cast<long double>(ip.weight) * referenceDeterminant * point.mapping.mapping_determinant;
|
||||
if (!(referenceDeterminant > 0.0) || !std::isfinite(referenceDeterminant) ||
|
||||
!(point.mapping.mapping_determinant > 0.0) || !std::isfinite(point.mapping.mapping_determinant) ||
|
||||
!(weight > 0.0L) || !std::isfinite(weight)) {
|
||||
++invalidSamples;
|
||||
continue;
|
||||
}
|
||||
const auto &position = point.mapping.physical_position;
|
||||
state.rotation.potential_gradient(position, centrifugalGradient);
|
||||
bool finite = std::isfinite(point.rho) && std::isfinite(point.h) && std::isfinite(point.phi) &&
|
||||
std::isfinite(point.rotationPotential);
|
||||
for (int d = 0; d < 3; ++d) {
|
||||
finite = finite && std::isfinite(position(d)) && std::isfinite(point.gravityGradientPhysical(d)) &&
|
||||
std::isfinite(point.potentialGradientPhysical(d)) &&
|
||||
std::isfinite(point.enthalpyGradientPhysical(d)) && std::isfinite(centrifugalGradient(d));
|
||||
}
|
||||
if (!finite) {
|
||||
++invalidSamples;
|
||||
continue;
|
||||
}
|
||||
++validSamples;
|
||||
const long double rho = point.rho;
|
||||
const long double h = point.h;
|
||||
const long double phi = point.phi;
|
||||
const long double psi = point.rotationPotential;
|
||||
const long double rhoH = h > 0.0L ? std::pow(h / enthalpyFactor, index) : 0.0L;
|
||||
const long double pH = h > 0.0L ? rhoH * h / (index + 1.0L) : 0.0L;
|
||||
const long double pRho = rho >= 0.0L ? constant * std::pow(rho, 1.0L + 1.0L / index) : 0.0L;
|
||||
const bool finiteHEos = std::isfinite(rhoH) && std::isfinite(pH);
|
||||
const bool finiteRhoEos = rho >= 0.0L && std::isfinite(pRho);
|
||||
if (!finiteHEos)
|
||||
++invalidEnthalpyEosSamples;
|
||||
if (rho >= 0.0L && !finiteRhoEos)
|
||||
++invalidDensityEosSamples;
|
||||
|
||||
volume += weight;
|
||||
mass += weight * rho;
|
||||
binding += 0.5L * weight * rho * phi;
|
||||
kinetic += weight * rho * psi;
|
||||
const long double specificBernoulli = h + phi - psi;
|
||||
if (validSamples == 1) {
|
||||
// Exact initialization avoids a spurious first-sample
|
||||
// variance from rounding an O(C) Bernoulli value.
|
||||
bernoulliMean = specificBernoulli;
|
||||
} else {
|
||||
const long double delta = specificBernoulli - bernoulliMean;
|
||||
bernoulliMean += weight / volume * delta;
|
||||
bernoulliCenteredSquared += weight * delta * (specificBernoulli - bernoulliMean);
|
||||
}
|
||||
const long double rawBernoulli = specificBernoulli - state.bernoulliConstant;
|
||||
bernoulliRawSquared += weight * rawBernoulli * rawBernoulli;
|
||||
bernoulliMin = std::min(bernoulliMin, specificBernoulli);
|
||||
bernoulliMax = std::max(bernoulliMax, specificBernoulli);
|
||||
rhoMin = std::min(rhoMin, rho);
|
||||
rhoMax = std::max(rhoMax, rho);
|
||||
hMin = std::min(hMin, h);
|
||||
hMax = std::max(hMax, h);
|
||||
minDeterminant = std::min(minDeterminant, static_cast<long double>(point.mapping.mapping_determinant));
|
||||
if (rho < 0.0L) {
|
||||
++negativeRhoSamples;
|
||||
negativeRhoVolume += weight;
|
||||
negativeRhoMassMagnitude -= weight * rho;
|
||||
}
|
||||
if (h < 0.0L) {
|
||||
++negativeHSamples;
|
||||
negativeHVolume += weight;
|
||||
}
|
||||
if (finiteHEos) {
|
||||
pressureH += weight * pH;
|
||||
const long double closure = rho - rhoH;
|
||||
closureIntegral += weight * closure;
|
||||
closureSquared += weight * closure * closure;
|
||||
closureMax = std::max(closureMax, std::abs(closure));
|
||||
rhoFromHMax = std::max(rhoFromHMax, rhoH);
|
||||
if (h > 0.0L) {
|
||||
const long double derivative = index * rhoH / h;
|
||||
drhodhPositiveMin = std::min(drhodhPositiveMin, derivative);
|
||||
drhodhPositiveMax = std::max(drhodhPositiveMax, derivative);
|
||||
}
|
||||
}
|
||||
if (finiteRhoEos)
|
||||
pressureRhoNonnegative += weight * pRho;
|
||||
if (finiteHEos && finiteRhoEos) {
|
||||
const long double pressureMismatch = pRho - pH;
|
||||
pressureMismatchSquared += weight * pressureMismatch * pressureMismatch;
|
||||
pressureMismatchVolume += weight;
|
||||
}
|
||||
|
||||
std::array<long double, 3> radius{}, velocity{};
|
||||
for (int d = 0; d < 3; ++d)
|
||||
radius[d] = static_cast<long double>(position(d)) - center(d);
|
||||
velocity[0] =
|
||||
static_cast<long double>(omega(1)) * radius[2] - static_cast<long double>(omega(2)) * radius[1];
|
||||
velocity[1] =
|
||||
static_cast<long double>(omega(2)) * radius[0] - static_cast<long double>(omega(0)) * radius[2];
|
||||
velocity[2] =
|
||||
static_cast<long double>(omega(0)) * radius[1] - static_cast<long double>(omega(1)) * radius[0];
|
||||
angularMomentum[0] += weight * rho * (radius[1] * velocity[2] - radius[2] * velocity[1]);
|
||||
angularMomentum[1] += weight * rho * (radius[2] * velocity[0] - radius[0] * velocity[2]);
|
||||
angularMomentum[2] += weight * rho * (radius[0] * velocity[1] - radius[1] * velocity[0]);
|
||||
inertiaZ += weight * rho * (radius[0] * radius[0] + radius[1] * radius[1]);
|
||||
for (int d = 0; d < 3; ++d) {
|
||||
const long double flux = point.gravityGradientPhysical(d);
|
||||
forceBinding -= weight * rho * position(d) * flux;
|
||||
firstMoment[d] += weight * rho * position(d);
|
||||
gravitationalForce[d] -= weight * rho * flux;
|
||||
const long double mismatch = flux - point.potentialGradientPhysical(d);
|
||||
const long double hydrostatic = point.enthalpyGradientPhysical(d) + flux - centrifugalGradient(d);
|
||||
gradientMismatchSquared += weight * mismatch * mismatch;
|
||||
hydrostaticGradientSquared += weight * hydrostatic * hydrostatic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool complete = invalidSamples == 0 && validSamples > 0 && volume > 0.0L && std::isfinite(volume);
|
||||
const bool validHEos = complete && invalidEnthalpyEosSamples == 0;
|
||||
const bool validRhoEos = complete && negativeRhoSamples == 0 && invalidDensityEosSamples == 0;
|
||||
const bool validBinding = complete && std::isfinite(binding) && binding < 0.0L;
|
||||
const auto value = [nan](const long double v, const bool valid = true) -> double {
|
||||
const double result = static_cast<double>(v);
|
||||
return valid && std::isfinite(result) ? result : nan;
|
||||
};
|
||||
const auto rms = [volume](const long double sum) {
|
||||
if (!(volume > 0.0L) || !std::isfinite(volume) || !std::isfinite(sum)) {
|
||||
return std::numeric_limits<long double>::quiet_NaN();
|
||||
}
|
||||
return std::sqrt(std::max(0.0L, sum / volume));
|
||||
};
|
||||
const auto relative = [nan, &value](const long double a, const long double denominator, const bool valid) {
|
||||
return valid && denominator > 0.0L ? value(a / denominator) : nan;
|
||||
};
|
||||
const bool validPressureMismatch = validHEos && pressureMismatchVolume > 0.0L && invalidDensityEosSamples == 0;
|
||||
const double pressureMismatchRms =
|
||||
validPressureMismatch ? value(std::sqrt(pressureMismatchSquared / pressureMismatchVolume)) : nan;
|
||||
const long double rhoScale = std::max({std::abs(rhoMin), std::abs(rhoMax), rhoFromHMax});
|
||||
const long double hScale = std::max(std::abs(hMin), std::abs(hMax));
|
||||
const long double absoluteBinding = std::abs(binding);
|
||||
const long double volumeRadius = std::cbrt(3.0L * volume / (4.0L * std::numbers::pi_v<long double>));
|
||||
|
||||
Measurements result{
|
||||
{"quadrature_order", static_cast<double>(quadratureOrder)},
|
||||
{"polytropic_index", n},
|
||||
{"polytropic_constant", K},
|
||||
{"stellar_samples", static_cast<double>(samples)},
|
||||
{"valid_stellar_samples", static_cast<double>(validSamples)},
|
||||
{"invalid_stellar_samples", static_cast<double>(invalidSamples)},
|
||||
{"complete_volume_measurement", complete ? 1.0 : 0.0},
|
||||
{"pressure_from_enthalpy_defined_everywhere", validHEos ? 1.0 : 0.0},
|
||||
{"pressure_from_density_defined_everywhere", validRhoEos ? 1.0 : 0.0},
|
||||
{"invalid_enthalpy_eos_samples", static_cast<double>(invalidEnthalpyEosSamples)},
|
||||
{"invalid_density_eos_samples", static_cast<double>(invalidDensityEosSamples)},
|
||||
{"volume", value(volume, complete)},
|
||||
{"mass", value(mass, complete)},
|
||||
{"volume_radius", value(volumeRadius, complete)},
|
||||
{"binding_energy", value(binding, complete)},
|
||||
{"force_binding_energy", value(forceBinding, complete)},
|
||||
{"kinetic_energy", value(kinetic, complete)},
|
||||
{"pressure_integral", value(pressureRhoNonnegative, validRhoEos)},
|
||||
{"enthalpy_pressure_integral", value(pressureH, validHEos)},
|
||||
{"nonnegative_density_pressure_integral",
|
||||
value(pressureRhoNonnegative, complete && invalidDensityEosSamples == 0)},
|
||||
{"kinetic_to_binding_ratio", relative(kinetic, absoluteBinding, validBinding)},
|
||||
{"gravity_energy_consistency", relative(std::abs(binding - forceBinding), absoluteBinding, validBinding)},
|
||||
{"moment_of_inertia_z", value(inertiaZ, complete)},
|
||||
{"angular_velocity_norm", state.rotation.angular_velocity().Norml2()},
|
||||
{"angular_momentum_z", value(angularMomentum[2], complete)},
|
||||
{"minimum_density", value(rhoMin)},
|
||||
{"maximum_density", value(rhoMax)},
|
||||
{"minimum_enthalpy", value(hMin)},
|
||||
{"maximum_enthalpy", value(hMax)},
|
||||
{"minimum_mapping_determinant", value(minDeterminant)},
|
||||
{"negative_density_samples", static_cast<double>(negativeRhoSamples)},
|
||||
{"negative_enthalpy_samples", static_cast<double>(negativeHSamples)},
|
||||
{"negative_density_volume_fraction", relative(negativeRhoVolume, volume, complete)},
|
||||
{"negative_enthalpy_volume_fraction", relative(negativeHVolume, volume, complete)},
|
||||
{"negative_density_mass_magnitude", value(negativeRhoMassMagnitude, complete)},
|
||||
{"negative_density_mass_fraction", relative(negativeRhoMassMagnitude, std::abs(mass), complete)},
|
||||
{"bernoulli_constant", state.bernoulliConstant},
|
||||
{"bernoulli_mean", value(bernoulliMean, complete)},
|
||||
{"bernoulli_mean_minus_constant", value(bernoulliMean - state.bernoulliConstant, complete)},
|
||||
{"bernoulli_raw_rms", value(rms(bernoulliRawSquared), complete)},
|
||||
{"bernoulli_mean_subtracted_rms", value(rms(bernoulliCenteredSquared), complete)},
|
||||
{"bernoulli_raw_rms_scaled", relative(rms(bernoulliRawSquared), hScale, complete)},
|
||||
{"bernoulli_mean_subtracted_rms_scaled", relative(rms(bernoulliCenteredSquared), hScale, complete)},
|
||||
{"bernoulli_sampled_range", value(bernoulliMax - bernoulliMin, complete)},
|
||||
{"eos_density_mismatch_integral", value(closureIntegral, validHEos)},
|
||||
{"eos_density_mismatch_rms", value(rms(closureSquared), validHEos)},
|
||||
{"eos_density_mismatch_maximum", value(closureMax, validHEos)},
|
||||
{"eos_density_mismatch_rms_scaled", relative(rms(closureSquared), rhoScale, validHEos)},
|
||||
{"eos_density_mismatch_maximum_scaled", relative(closureMax, rhoScale, validHEos)},
|
||||
{"eos_density_scale_sampled_maximum", value(rhoScale, validHEos)},
|
||||
{"eos_enthalpy_scale_sampled_maximum", value(hScale, complete)},
|
||||
{"eos_drhodh_positive_h_minimum", value(drhodhPositiveMin, validHEos)},
|
||||
{"eos_drhodh_positive_h_maximum", value(drhodhPositiveMax, validHEos)},
|
||||
{"eos_pressure_mismatch_rms_on_nonnegative_density", pressureMismatchRms},
|
||||
{"gravity_gradient_vs_broken_potential_gradient_rms", value(rms(gradientMismatchSquared), complete)},
|
||||
{"hydrostatic_gradient_rms", value(rms(hydrostaticGradientSquared), complete)}};
|
||||
for (const auto &entry :
|
||||
std::array<std::pair<std::string, long double>, 2>{{{"", pressureRhoNonnegative}, {"enthalpy_", pressureH}}}) {
|
||||
const bool validPressure = entry.first.empty() ? validRhoEos : validHEos;
|
||||
const long double energyNumerator = 2.0L * kinetic + binding + 3.0L * entry.second;
|
||||
const long double forceNumerator = 2.0L * kinetic + forceBinding + 3.0L * entry.second;
|
||||
result[entry.first + "virial_signed"] =
|
||||
relative(energyNumerator, absoluteBinding, validBinding && validPressure);
|
||||
result[entry.first + "virial_error"] =
|
||||
relative(std::abs(energyNumerator), absoluteBinding, validBinding && validPressure);
|
||||
// Use |W_energy| for both definitions to compare their numerators.
|
||||
result[entry.first + "force_virial_signed"] =
|
||||
relative(forceNumerator, absoluteBinding, validBinding && validPressure);
|
||||
result[entry.first + "force_virial_error"] =
|
||||
relative(std::abs(forceNumerator), absoluteBinding, validBinding && validPressure);
|
||||
}
|
||||
long double omegaDotAngularMomentum = 0.0L;
|
||||
for (int d = 0; d < 3; ++d) {
|
||||
result["center_of_mass_" + std::to_string(d)] = complete && mass != 0.0L ? value(firstMoment[d] / mass) : nan;
|
||||
result["angular_momentum_about_rotation_center_" + std::to_string(d)] = value(angularMomentum[d], complete);
|
||||
result["net_gravitational_force_" + std::to_string(d)] = value(gravitationalForce[d], complete);
|
||||
result["rotation_center_" + std::to_string(d)] = center(d);
|
||||
result["angular_velocity_" + std::to_string(d)] = omega(d);
|
||||
omegaDotAngularMomentum += static_cast<long double>(omega(d)) * angularMomentum[d];
|
||||
}
|
||||
result["rigid_rotation_energy_identity_absolute_error"] =
|
||||
value(std::abs(omegaDotAngularMomentum - 2.0L * kinetic), complete);
|
||||
return result;
|
||||
}
|
||||
} // namespace experiment::plateau_diagnostics
|
||||
230
experiments/plateau_preconditioner_diagnostics.hpp
Normal file
230
experiments/plateau_preconditioner_diagnostics.hpp
Normal file
@@ -0,0 +1,230 @@
|
||||
#pragma once
|
||||
|
||||
// Experiment-only, single-rank inspection. Include after `import mean_field;`.
|
||||
// Requires an already-prepared accepted context. No physical/trial states are
|
||||
// changed. Cost: three full Jv plus one Jv per scalar border coordinate, and
|
||||
// corresponding inexpensive inverse actions. No extra Krylov solves.
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace experiment::plateau_diagnostics {
|
||||
namespace preconditioner_detail {
|
||||
inline std::ofstream Open(const std::filesystem::path &path) {
|
||||
std::ofstream file(path);
|
||||
file.exceptions(std::ios::failbit | std::ios::badbit);
|
||||
file << std::setprecision(17);
|
||||
return file;
|
||||
}
|
||||
inline double Norm(const mfem::Vector &vector, int offset, int size) {
|
||||
long double sum = 0.0L;
|
||||
for (int i = offset; i < offset + size; ++i) {
|
||||
sum += static_cast<long double>(vector(i)) * vector(i);
|
||||
}
|
||||
return std::sqrt(sum);
|
||||
}
|
||||
inline double RelativeDifference(const mfem::Vector &left, const mfem::Vector &right) {
|
||||
mfem::Vector difference(left);
|
||||
difference -= right;
|
||||
return difference.Norml2() / std::max({left.Norml2(), right.Norml2(), 1.0e-300});
|
||||
}
|
||||
} // namespace preconditioner_detail
|
||||
|
||||
template <class State> void InspectPreconditioner(State &state, const std::filesystem::path &outputDirectory) {
|
||||
namespace detail = preconditioner_detail;
|
||||
namespace normalization = mean_field::normalization;
|
||||
int ranks = 0;
|
||||
MPI_Comm_size(state.Problem().GetCommunicator(), &ranks);
|
||||
if (ranks != 1)
|
||||
throw std::invalid_argument("Preconditioner diagnostics require one MPI rank.");
|
||||
if (!state.normalizedOperator->IsPrepared())
|
||||
throw std::logic_error("Preconditioner diagnostics require a prepared accepted state.");
|
||||
state.physicalInverse->Refresh();
|
||||
const auto &grouped = state.physicalInverse->GetGroupedPreconditioner();
|
||||
const auto &structure = grouped.GetStructurePreconditioner();
|
||||
const auto &material = structure.GetMaterialSurfacePreconditioner();
|
||||
const auto &couplings = grouped.GetCouplings();
|
||||
const auto &coordinates = state.physicalInverse->GetCoordinateMap();
|
||||
const auto &normalizationMap = state.normalizedOperator->GetNormalization();
|
||||
const auto values = state.Problem().GetManifest().valueBlocks();
|
||||
const auto residuals = state.Problem().GetManifest().residualBlocks();
|
||||
const int n = state.Problem().StateSize();
|
||||
const int k = couplings.StructureSize();
|
||||
const int borderSize = couplings.BorderSize();
|
||||
mfem::Vector groupedL(n), groupedV(n);
|
||||
coordinates.PackResidual(normalizationMap.ResidualFactors(), groupedL);
|
||||
coordinates.PackCorrection(normalizationMap.StateFactors(), groupedV);
|
||||
|
||||
auto ranges = detail::Open(outputDirectory / "normalization_factor_ranges.csv");
|
||||
ranges << "kind,block,size,minimum,maximum,max_over_min,rms,manifest_scale\n";
|
||||
auto recordFactors = [&](const char *kind, const auto &blocks, const mfem::Vector &factors) {
|
||||
for (const auto &block : blocks) {
|
||||
double minimum = std::numeric_limits<double>::infinity(), maximum = 0.0;
|
||||
for (int i = block.offset; i < block.offset + block.size; ++i) {
|
||||
minimum = std::min(minimum, factors(i));
|
||||
maximum = std::max(maximum, factors(i));
|
||||
}
|
||||
ranges << kind << ',' << block.stableId << ',' << block.size << ',' << minimum << ',' << maximum << ','
|
||||
<< maximum / minimum << ','
|
||||
<< (block.size ? detail::Norm(factors, block.offset, block.size) / std::sqrt(block.size) : 0.0)
|
||||
<< ',' << block.scale << '\n';
|
||||
}
|
||||
};
|
||||
recordFactors("value", values, normalizationMap.StateFactors());
|
||||
recordFactors("residual", residuals, normalizationMap.ResidualFactors());
|
||||
|
||||
auto diagonal = detail::Open(outputDirectory / "preconditioner_diagonal_quality.csv");
|
||||
diagonal << "block,min_abs_before,max_abs_before,applied_floor,regularized_entries\n";
|
||||
auto recordDiagonal = [&](const char *name, const auto &quality) {
|
||||
diagonal << name << ',' << quality.minimumAbsoluteEntryBeforeRegularization << ','
|
||||
<< quality.maximumAbsoluteEntryBeforeRegularization << ',' << quality.appliedFloor << ','
|
||||
<< quality.regularizedEntries << '\n';
|
||||
};
|
||||
recordDiagonal("density", material.GetDensityDiagonalQuality());
|
||||
recordDiagonal("surface", material.GetSurfaceDiagonalQuality());
|
||||
recordDiagonal("enthalpy", material.GetEnthalpyDiagonalQuality());
|
||||
|
||||
// Map grouped scalar indices back to public manifest labels.
|
||||
auto groupedLabel = [](int index, const auto &coordinateRanges, const auto &blocks) {
|
||||
for (const auto &range : coordinateRanges) {
|
||||
if (index < range.preconditionerOffset || index >= range.preconditionerOffset + range.size)
|
||||
continue;
|
||||
const int rootIndex = range.equilibriumOffset + index - range.preconditionerOffset;
|
||||
for (const auto &block : blocks)
|
||||
if (rootIndex >= block.offset && rootIndex < block.offset + block.size)
|
||||
return std::string(block.stableId);
|
||||
}
|
||||
return std::string("unknown");
|
||||
};
|
||||
std::vector<std::string> valueLabels, residualLabels;
|
||||
auto layout = detail::Open(outputDirectory / "border_normalization_layout.csv");
|
||||
layout << "index,value,residual,state_to_normalized,residual_to_normalized\n";
|
||||
for (int j = 0; j < borderSize; ++j) {
|
||||
valueLabels.push_back(groupedLabel(k + j, coordinates.GetCorrectionRanges(), values));
|
||||
residualLabels.push_back(groupedLabel(k + j, coordinates.GetResidualRanges(), residuals));
|
||||
layout << j << ',' << valueLabels.back() << ',' << residualLabels.back() << ',' << groupedV(k + j) << ','
|
||||
<< groupedL(k + j) << '\n';
|
||||
}
|
||||
const auto &schur = grouped.GetFactorization().GetSchurComplement();
|
||||
mfem::DenseMatrix scaledSchur(borderSize, borderSize);
|
||||
auto schurEntries = detail::Open(outputDirectory / "border_schur_entries.csv");
|
||||
auto schurMatrix = detail::Open(outputDirectory / "border_schur_normalized_matrix.csv");
|
||||
schurEntries << "row,column,residual,value,physical_entry,normalized_entry\n";
|
||||
for (int i = 0; i < borderSize; ++i) {
|
||||
for (int j = 0; j < borderSize; ++j) {
|
||||
scaledSchur(i, j) = groupedL(k + i) * schur(i, j) / groupedV(k + j);
|
||||
schurEntries << i << ',' << j << ',' << residualLabels[i] << ',' << valueLabels[j] << ',' << schur(i, j)
|
||||
<< ',' << scaledSchur(i, j) << '\n';
|
||||
schurMatrix << (j ? "," : "") << scaledSchur(i, j);
|
||||
}
|
||||
schurMatrix << '\n';
|
||||
}
|
||||
|
||||
auto consistency = detail::Open(outputDirectory / "normalization_consistency.csv");
|
||||
consistency << "check,relative_difference\n";
|
||||
// One deterministic RHS balanced across residual blocks. This is a
|
||||
// coordinate-consistency check, not a condition-number estimator.
|
||||
mfem::Vector rhs(n);
|
||||
rhs = 0.0;
|
||||
for (const auto &block : residuals) {
|
||||
for (int i = 0; i < block.size; ++i)
|
||||
rhs(block.offset + i) = std::sin(0.173 * static_cast<double>(block.offset + i + 1));
|
||||
const double blockNorm = detail::Norm(rhs, block.offset, block.size);
|
||||
if (blockNorm > 0.0)
|
||||
for (int i = 0; i < block.size; ++i)
|
||||
rhs(block.offset + i) /= blockNorm;
|
||||
}
|
||||
mfem::Vector groupedRhs(n), recoveredRhs(n);
|
||||
coordinates.PackResidual(rhs, groupedRhs);
|
||||
coordinates.UnpackResidual(groupedRhs, recoveredRhs);
|
||||
consistency << "residual_coordinate_roundtrip," << detail::RelativeDifference(rhs, recoveredRhs) << '\n';
|
||||
coordinates.PackCorrection(rhs, groupedRhs);
|
||||
coordinates.UnpackCorrection(groupedRhs, recoveredRhs);
|
||||
consistency << "correction_coordinate_roundtrip," << detail::RelativeDifference(rhs, recoveredRhs) << '\n';
|
||||
|
||||
mfem::Vector baseCorrection(n), baseAction(n), physicalRhs(n), physicalCorrection(n);
|
||||
mfem::Vector expectedCorrection(n), physicalAction(n), expectedAction(n);
|
||||
state.normalizedInverse->Mult(rhs, baseCorrection);
|
||||
state.normalizedOperator->Mult(baseCorrection, baseAction);
|
||||
normalizationMap.DenormalizeResidual(rhs, physicalRhs);
|
||||
state.physicalInverse->Mult(physicalRhs, physicalCorrection);
|
||||
normalizationMap.NormalizeState(physicalCorrection, expectedCorrection);
|
||||
state.Problem().GetLinearizationOperator().Mult(physicalCorrection, physicalAction);
|
||||
normalizationMap.NormalizeResidual(physicalAction, expectedAction);
|
||||
consistency << "inverse_wrapper_vs_explicit_physical,"
|
||||
<< detail::RelativeDifference(baseCorrection, expectedCorrection) << '\n';
|
||||
consistency << "jacobian_wrapper_vs_explicit_physical," << detail::RelativeDifference(baseAction, expectedAction)
|
||||
<< '\n';
|
||||
|
||||
// Change only variable coordinates, leaving the residual norm and physical
|
||||
// inverse unchanged. Jhat Phat^-1 must be invariant up to roundoff.
|
||||
mfem::Vector alternateFactors(normalizationMap.StateFactors());
|
||||
int blockIndex = 0;
|
||||
for (const auto &block : values) {
|
||||
const double multiplier = std::pow(10.0, static_cast<double>((blockIndex++ % 7) - 3));
|
||||
for (int i = block.offset; i < block.offset + block.size; ++i)
|
||||
alternateFactors(i) *= multiplier;
|
||||
}
|
||||
normalization::DiagonalNormalization alternateMap(std::move(alternateFactors),
|
||||
mfem::Vector(normalizationMap.ResidualFactors()));
|
||||
normalization::ScaledInverseOperator alternateInverse(*state.physicalInverse, alternateMap);
|
||||
normalization::ScaledJacobianOperator alternateJacobian(state.Problem().GetLinearizationOperator(), alternateMap);
|
||||
mfem::Vector alternateCorrection(n), alternateAction(n), alternatePhysicalCorrection(n);
|
||||
alternateInverse.Mult(rhs, alternateCorrection);
|
||||
alternateJacobian.Mult(alternateCorrection, alternateAction);
|
||||
alternateMap.DenormalizeState(alternateCorrection, alternatePhysicalCorrection);
|
||||
consistency << "state_scaling_cancelled_right_preconditioned_action,"
|
||||
<< detail::RelativeDifference(baseAction, alternateAction) << '\n';
|
||||
consistency << "state_scaling_cancelled_physical_correction,"
|
||||
<< detail::RelativeDifference(physicalCorrection, alternatePhysicalCorrection) << '\n';
|
||||
|
||||
// Each direction is [-Atilde^-1 B y; y], where y is a unit NORMALIZED
|
||||
// scalar coordinate. Its true stellar Jacobian action exposes structure
|
||||
// cancellation errors; its border action should equal the scaled Schur
|
||||
// column. These directions are probes, never proposed Newton steps.
|
||||
auto columns = detail::Open(outputDirectory / "border_completed_column_actions.csv");
|
||||
columns << "column,value,kind,block,physical_norm,normalized_norm\n";
|
||||
auto borderDefects = detail::Open(outputDirectory / "border_completed_schur_consistency.csv");
|
||||
borderDefects << "column,value,border_relative_difference,true_structure_action_norm,normalized_direction_norm\n";
|
||||
mfem::Vector scalarDirection(borderSize), structureRhs(k), structureCorrection(k);
|
||||
mfem::Vector groupedDirection(n), rootDirection(n), normalizedDirection(n);
|
||||
mfem::Vector normalizedAction(n), rootAction(n), groupedAction(n);
|
||||
for (int j = 0; j < borderSize; ++j) {
|
||||
scalarDirection = 0.0;
|
||||
scalarDirection(j) = 1.0 / groupedV(k + j);
|
||||
couplings.ApplyBorderToStructure(scalarDirection, structureRhs);
|
||||
structure.Mult(structureRhs, structureCorrection);
|
||||
for (int i = 0; i < k; ++i)
|
||||
groupedDirection(i) = -structureCorrection(i);
|
||||
for (int i = 0; i < borderSize; ++i)
|
||||
groupedDirection(k + i) = scalarDirection(i);
|
||||
coordinates.UnpackCorrection(groupedDirection, rootDirection);
|
||||
normalizationMap.NormalizeState(rootDirection, normalizedDirection);
|
||||
state.normalizedOperator->Mult(normalizedDirection, normalizedAction);
|
||||
normalizationMap.DenormalizeResidual(normalizedAction, rootAction);
|
||||
coordinates.PackResidual(normalizedAction, groupedAction);
|
||||
for (const auto &block : values)
|
||||
columns << j << ',' << valueLabels[j] << ",direction," << block.stableId << ','
|
||||
<< detail::Norm(rootDirection, block.offset, block.size) << ','
|
||||
<< detail::Norm(normalizedDirection, block.offset, block.size) << '\n';
|
||||
for (const auto &block : residuals)
|
||||
columns << j << ',' << valueLabels[j] << ",action," << block.stableId << ','
|
||||
<< detail::Norm(rootAction, block.offset, block.size) << ','
|
||||
<< detail::Norm(normalizedAction, block.offset, block.size) << '\n';
|
||||
mfem::Vector actual(borderSize), expected(borderSize);
|
||||
for (int i = 0; i < borderSize; ++i) {
|
||||
actual(i) = groupedAction(k + i);
|
||||
expected(i) = scaledSchur(i, j);
|
||||
}
|
||||
borderDefects << j << ',' << valueLabels[j] << ',' << detail::RelativeDifference(actual, expected) << ','
|
||||
<< detail::Norm(groupedAction, 0, k) << ',' << normalizedDirection.Norml2() << '\n';
|
||||
}
|
||||
}
|
||||
} // namespace experiment::plateau_diagnostics
|
||||
1682
experiments/system_isolation_profile_harness.cpp
Normal file
1682
experiments/system_isolation_profile_harness.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user