Snapshot optimized solver and audit diagnostics before numerical experiments

This commit is contained in:
2026-09-16 14:34:06 -04:00
parent 75cc638739
commit 80cc3d83d7
64 changed files with 11969 additions and 267 deletions

View File

@@ -382,6 +382,17 @@ target_link_libraries(geometry_quality_experiment PRIVATE mean_field)
add_executable(polytrope_validation_experiment EXCLUDE_FROM_ALL experiments/polytrope_validation_experiment.cpp)
target_link_libraries(polytrope_validation_experiment PRIVATE mean_field)
# Opt-in plateau measurements; uses the production solver without numerical changes.
add_executable(plateau_diagnostic EXCLUDE_FROM_ALL experiments/plateau_diagnostic.cpp)
target_link_libraries(plateau_diagnostic PRIVATE mean_field)
# Standalone operator validation and timing, without constructing a Newton solve.
add_executable(system_isolation_profile_harness EXCLUDE_FROM_ALL experiments/system_isolation_profile_harness.cpp)
target_link_libraries(system_isolation_profile_harness PRIVATE mean_field)
if (MEAN_FIELD_ENABLE_IPO)
set_property(TARGET system_isolation_profile_harness PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
endif ()
add_executable(mpi_tests
tests/mpi/mpi_test_main.cpp
tests/mpi/distributed_execution.cpp

View File

@@ -0,0 +1,105 @@
# Deferred Removal Registry
This registry records migration-era code that may become removable after the
physics-facing `StellarModel` and `StellarEquilibriumProblem` APIs have replaced
all legacy construction paths. Nothing listed here is currently approved for
removal, and no listed code was removed during the migration.
## `mean_field::models::Model<...>`
- Location: `libmeanfield/interface/models/specifications.cppm`
- Current status: required compatibility storage
- Current consumers: root-manifest tests and the implementation of
`mean_field::model::StellarModel`
- Removal gate: migrate remaining direct consumers to the public
`mean_field::model::StellarModel` type, then move the canonical storage into
an implementation namespace
- Evidence required before removal: no direct consumers found by repository
search and the complete specification/type-contract suite passes
## Legacy structure-based `mean_field::models::StellarModel`
- Location: `libmeanfield/interface/models/stellar_model.cppm`
- Current status: retained legacy API, but no longer used by the authoritative
`StellarEquilibriumProblem` construction path
- Current consumers: direct prepared-operator tests, experiments, and
heterogeneous runtime-model views. Its polytropic initial-seed method now
delegates to the physics-facing Lane-Emden seed generator.
- Removal gate: numerical operators consume compiled specification
contributions directly, initial-seed generation has a physics-facing owner,
and deformation policies have migrated into the typed model
- Evidence required before removal: legacy-adapter equivalence tests pass for
all supported formulations and repository search finds no direct consumers
## `PolytropicStructure` as a mass-owning adapter
- Location: `libmeanfield/interface/models/structure/polytropic.cppm`
- Current status: compatibility adapter for remaining legacy API consumers; it
no longer owns Lane-Emden integration and is not constructed by
`StellarEquilibriumProblem`
- Potentially obsolete responsibility: ownership of the fixed-total-mass
target, which now belongs to `FixedTotalMass`
- Removal gate: migrate remaining callers to `seed::LaneEmden` and the typed
model. Lane-Emden generation is now separate from invariant ownership.
- Evidence required before removal: initial-seed regression tests and
model-driven equilibrium equivalence tests pass
## Legacy combined prepared operators
- Locations:
`libmeanfield/interface/operators/prepared_fixed_angular_momentum_stellar_equilibrium.cppm`
and
`libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm`
- Current status: retained as migration-era source references, but removed from
the build graph and no longer re-exported by the public `mean_field` module
- Physics-facing replacement:
`PreparedVariadicStellarEquilibriumOperator<Model>`, selected solely by
`StellarEquilibriumProblem<Model, Discretization>`
- Reactivation gate: migrate the source snapshots to the model-driven manifest
constructor; they intentionally do not compile against the authoritative API
- Removal gate: delete the source snapshots after the historical comparison
window closes
- Evidence required before removal: the full variadic residual/Jacobian
equivalence suite and distributed tests pass without either partition being
imported by a user translation unit
## Completed: typed equilibrium-problem adapter removal
- Removed the former typed system's `NumericalModelAdapter`, its
`PolytropicStructure`, and the corresponding legacy model storage.
- The typed problem now owns its compiled surface constraint and constructs the
prepared operator directly from specification-generated contributions.
- Default radial deformation is compiled at the numerical equilibrium-problem
boundary rather than being smuggled through a legacy stellar model.
## `StellarEquilibriumSystem` compatibility vocabulary
- Location: `libmeanfield/interface/operators/stellar_equilibrium_system.cppm`
- Current status: source-compatible aliases and factory forwarding to
`StellarEquilibriumProblem` and `equilibrium::discretize`
- Removal gate: migrate remaining external consumers and provide a documented
deprecation interval
- Evidence required before removal: repository search finds only the explicit
compatibility type-contract test
## Root-oriented public aliases and accessors
- Locations:
`libmeanfield/interface/operators/root_manifest.cppm`,
`libmeanfield/interface/operators/prepared_stellar_equilibrium.cppm`, and
`libmeanfield/interface/operators/prepared_central_density_stellar_equilibrium.cppm`
- Current status: retained for source compatibility
- Physics-facing replacements: `EquilibriumSystemManifest`,
`EquilibriumStateView`, `EquilibriumResidualView`, and
`StellarEquilibriumProblem::GetManifest()`
- Removal gate: all solver and test consumers use equilibrium-system
terminology
- Evidence required before removal: repository search finds no uses of the old
aliases/accessors and all manifest/type-contract tests pass
## Conclusion for this phase
No code examined during this migration is presently truly dead. Each candidate
still has active consumers or carries a compatibility/lifetime responsibility.
The entries above are therefore deferred migration candidates, not deletion
recommendations.

View File

@@ -0,0 +1,548 @@
# MeanField FieldDof Migration Handoff
**Repository:** `/Users/tboudreaux/Programming/4DSTAR/MeanField`
**Prepared:** 2026-08-28
**Primary objective:** make registered field/domain support, rather than raw MFEM true-vector size, define the nonlinear and prepared-operator coordinate systems.
## 1. Scope and relationship to the earlier review
This document is the continuation handoff for the work performed after the earlier **Rotating Polytrope Review** summary. The receiving agent is expected to receive that earlier summary as well, so this document does not reconstruct every pre-migration investigation. It concentrates on:
- the final FieldDof architecture and its invariants;
- the gravity, barotrope, prepared-operator, and root-state migrations completed in this work;
- the canonical `FieldDofMap`/`GridFunction` adapter;
- the replacement and hardening of the domain-mapping system;
- dead-code and file cleanup;
- failures encountered during migration and what they taught us;
- final validation evidence;
- the small amount of cleanup that remains.
## 2. Executive status
The functional FieldDof migration is complete for the currently registered production fields and the coupled stellar-equilibrium operator stack.
The final solver-facing convention is:
```text
registered field + DomainSchema
|
v
FieldDofMap
|
reduced solver coordinates
|
prepared operator/context boundary
|
explicit scatter/gather
|
full MFEM true vectors
|
element kernels / MFEM
```
The complete Catch2 inventory was covered by three sharded runs:
| Validation group | Test cases | Assertion executions | Result |
|---|---:|---:|---|
| `[gravity]` | 58 | 3,746,701 | Pass |
| `[barotrope]` | 88 | 192,694 | Pass |
| `~[gravity]~[barotrope]` | 176 | 136,493 | Pass |
| **Total executions** | **322** | **4,075,888** | **Pass** |
There are 320 unique registered test cases. Two prepared stellar-equilibrium tests carry both `[gravity]` and `[barotrope]`, so the three filters intentionally execute those twice:
- `Prepared Stellar Equilibrium Has A Restoring Jacobian Around An N3 Polytrope`
- `Prepared Stellar Equilibrium Matches The Analytic N1 Lane Emden State Up To The Mixed Projection Floor`
Therefore:
```text
58 gravity + 88 barotrope + 176 remainder - 2 overlap = 320 unique tests
```
No MFEM abort, MPI abort, assertion failure, crash signature, or empty shard was present in the final 24 logs.
The migration is not yet packaged as a clean commit. The worktree is intentionally dirty relative to `HEAD` and contains the migration. Preserve it.
## 3. Git and validation provenance
Important commits:
- `0f3ca8050b78f0c5c1ef98d485b9fbc2ba06183c` — the user-created mid-migration checkpoint.
- `177ae8b38aa34d88c07203376961145c5403ccaa` (`HEAD` when this document was written) — `feat(support): continued migration to new field dof support system`.
The final migration, hardening, deletions, and mapper consolidation are uncommitted changes on top of `177ae8b`.
Final validation logs are under:
```text
${TMPDIR:-/tmp}/meanfield-migration-validation
```
On this machine that resolved to:
```text
/var/folders/v3/xfq17d692dg_gzdwv4yzmrkw0000gp/T/meanfield-migration-validation
```
The authoritative files are:
```text
gravity-final-shard-{0..7}.txt
barotrope-final-shard-{0..7}.txt
migration-remainder-shard-{0..7}.txt
```
Do not confuse these with older repository-root `gravity-shard-*.txt` files from August 24. Those older logs captured intermediate failures and were not evidence for the final state.
### Post-validation rename caveat
After the 24-shard validation completed, the user renamed:
- `DomainMapperStateless` to `DomainMapper`;
- `DomainMapperStatelessOptions` to `DomainMapperOptions`.
The public interface and most implementation state were compiled after the rename, but at handoff time `libmeanfield/impl/mapping/domain_mapper.cpp` had a modification timestamp two minutes newer than the latest test executable. The source-level audit found no remaining `DomainMapperStateless`, `DomainMapperStatelessOptions`, `domain_mapper_new`, or `PolytropicBarotrope` type/file references, and `git diff --check` passed. Nevertheless, run one compile-only check on the exact final tree before committing.
## 4. Final field and domain model
### 4.1 Registered support is the source of truth
Field support is declared in `libmeanfield/interface/field/field_registry.cppm`:
- `Density::Support = DomainSupport<Stellar>`;
- `Enthalpy::Support = DomainSupport<Stellar>`;
- `Gravity::Support = DomainSupport<All>`;
- `Displacement::Support = DomainSupport<All>`;
- `BarotropicConstant::Support = NonSpatialSupport`.
This distinction is now operational. Density and enthalpy solver vectors omit vacuum-only true DOFs. Gravity and displacement currently map over all domains and ordinarily produce identity maps. Global scalars are not routed through spatial `FieldDofMap` factories.
The concrete production schema is `CoreEnvelopeVacuumDomainSchema`. Field support is resolved through the schema's material bindings rather than through hard-coded assumptions such as “stellar attributes are 1 and 2” or “vacuum is attribute 3.” Tests deliberately exercise alternate material bindings to protect this property.
### 4.2 Supported continuous DOFs use union semantics
The active set is the union of DOFs touched by supported elements. It is not computed by removing every DOF that also touches an unsupported element.
This matters for continuous H1 fields. An enthalpy DOF shared by a stellar element and a vacuum element remains part of the stellar field because it participates in supported stellar elements and supplies the physical interface trace.
### 4.3 Parallel support resolution
`resolve_field_dof_support` performs the production parallel resolution:
1. Build a local vector-DOF marker from supported elements.
2. Decode MFEM's signed/oriented DOF indices.
3. Synchronize the marker across shared parallel DOFs.
4. Convert locally owned active vector DOFs to true DOFs.
5. Produce active/inactive markers and sorted index lists.
The synchronization step ensures that a shared interface DOF is active on every rank carrying it when any neighboring supported element activates it.
The implementation deliberately rejects nonconforming `ParFiniteElementSpace` objects. Supporting them correctly would require an explicit constraint/conforming-DOF projection; silently treating local DOFs as ordinary true DOFs was rejected as unsafe.
## 5. `FieldDofMap` contract
`FieldDofMap` is the canonical immutable indexing relation between a full MFEM true vector and a dense reduced field vector.
It stores:
- `full_size()` — MFEM true-vector size;
- `reduced_size()` — supported field-vector size;
- `reduced_to_true()` — canonical forward indices;
- `true_to_reduced()` — inverse indices with `-1` for inactive true DOFs.
Construction requires the reduced-to-true list to be sorted, unique, in range, and strictly increasing. This makes reduced ordering deterministic and consistent with MFEM marker-to-list ordering.
Important operations and semantics:
- `gather(full)` selects supported true DOFs into reduced order.
- `scatter(reduced)` creates a complete full true vector and zeros unsupported entries.
- `scatter_into(reduced, full)` overwrites supported entries while preserving existing unsupported entries.
- `scatter_add(reduced, full, scale)` accumulates into supported entries only.
- All overloads validate exact source and destination sizes.
- Destination-vector overloads do not resize their output, so views into `mfem::BlockVector` remain valid.
- `is_identity()` is exact because canonical ordering guarantees `reduced_to_true[i] == i` whenever all true DOFs are active.
The object intentionally contains no mesh, field, domain, or solver policy after construction. Those semantics belong to the factory step; consumers get a small indexing object.
## 6. Canonical `FieldDofMap`/`GridFunction` adapter
`FieldDofGridFunctionAdapter` was added as the only canonical bridge between reduced field vectors and MFEM `GridFunction` objects.
It binds a `FieldDofMap` to the exact `FiniteElementSpace` from which the map was constructed. Matching vector sizes are not enough: a grid function using another FE-space object is rejected. This prevents silent cross-space corruption when two spaces happen to have equal true-vector sizes.
Operations:
- `gather(GridFunction)` calls `GetTrueDofs` and gathers into reduced coordinates.
- `scatter(reduced, GridFunction)` applies projection semantics and calls `SetFromTrueDofs`.
- `scatter_into(reduced, GridFunction)` preserves unsupported existing true-DOF values.
- `make_field_dof_grid_function_adapter<Field, Schema>(space)` is the typed construction path.
The gravity initialization/solve now uses these adapters for density, displacement, gravity flux, and gravity potential. The returned `GravitySolution` remains an MFEM-facing pair of `ParGridFunction`s, while the solve itself is performed in reduced coordinates.
## 7. Domain mapping replacement and hardening
### 7.1 Architectural replacement
The old stateful mapping implementation was removed. The canonical `DomainMapper` now owns only mapping policy:
- dimension;
- vacuum material attribute;
- the exterior compactification map.
Element field values are supplied explicitly through `ElementMappingData`, and temporary storage is supplied through `DomainMapper::Workspace`. Point, volume, face, and first-variation evaluation are explicit operations. This eliminates hidden displacement state and cross-state cache contamination inside the mapper itself.
The implementation formerly stored in `libmeanfield/impl/mapping/domain_mapper_new.cpp` is now consolidated into:
```text
libmeanfield/impl/mapping/domain_mapper.cpp
```
`domain_mapper_new.cpp` was deleted and removed from CMake.
### 7.2 `GridFunctionMappingEvaluator`
`GridFunctionMappingEvaluator` is the stateful convenience layer for consumers that naturally own MFEM grid functions. It references displacement and compactification grid functions, extracts element-local data, and delegates actual geometry evaluation to `DomainMapper`.
Hardening added in the final stage:
- constructor validation for attached spaces;
- same-mesh validation;
- displacement vector-dimension validation;
- scalar compactification validation;
- mesh/domain-mapper dimension validation;
- exact FE-space binding checks after construction;
- detection of FE-space sequence changes;
- explicit, idempotent `InvalidateCache()`;
- eager `Refresh()` of a currently cached element;
- a validated no-op when `Refresh()` is called before any element is cached;
- lazy reload after space-sequence invalidation;
- correct application of MFEM `DofTransformation::InvTransformPrimal` to element displacement DOFs;
- support for both `byNODES` and `byVDIM` displacement ordering.
The evaluator owns mutable workspace/cache state and is explicitly not thread-safe. Consumers must call `InvalidateCache()` or `Refresh()` after mutating grid-function values. FE-space sequence changes are detected automatically, but arbitrary value changes are not observable without explicit invalidation.
Integrator consumers were updated to invalidate mapping caches at assembly entry. Analysis, physics, coefficient, utility, and integrator consumers now use `GridFunctionMappingEvaluator` rather than the removed stateful mapper API.
## 8. Gravity-system migration
The gravity migration covered kernels, prepared operators, contexts, coupled operators, the reduced solve, tests, and downstream consumers.
### 8.1 Boundary decision: kernels remain full-true-DOF code
Low-level element kernels deliberately continue to accept full MFEM true vectors. Their interface comments now state this boundary explicitly. Examples include:
- mapped H(div) mass and its geometry variation;
- mapped density source and its geometry variation;
- gravity displacement force residual and all Jacobian actions;
- hydrostatic and pressure/rotation kernels used by prepared consumers.
This was a deliberate choice. Element assembly naturally indexes MFEM spaces. Making kernels understand reduced field support would duplicate mapping logic and mix algebra with support policy.
Prepared operators and contexts own the maps and perform the only scatter/gather operations around those kernels.
### 8.2 Prepared gravity components
`PreparedMappedGravitySourceOperator` now owns density, potential, and displacement maps. Its public `Mult` and `MultTranspose` use reduced density/potential coordinates; it expands to full true vectors internally.
`PreparedMappedHDivMassOperator` owns flux and displacement maps. It accepts reduced flux/displacement vectors, maintains full internal work vectors, and exposes both:
- a reduced diagonal for the reduced mass block;
- a full true diagonal for the Schur construction.
Both stellar and compactified-vacuum H(div) contributions are included. Positivity is tested across the complete gravity domain.
### 8.3 Gravity contexts and revision routing
`GravityFieldGeometryContext` owns the displacement map and the geometry-dependent prepared mass/source/divergence operators.
`GravityFieldLinearizationContext` owns density, displacement, gravity-gradient, and gravity-potential maps, together with the full true work vectors required by kernels.
Typed revision stamps control selective invalidation for:
- discretization;
- displacement;
- density;
- gravity gradient;
- gravity potential.
Tests verify first preparation, no-op repeat preparation, selective rebuild/update behavior, and independent contexts without shared cache contamination.
### 8.4 Coupled gravity operator and Jacobian
`GravityFieldOperator`, `GravityFieldJacobianOperator`, and their state/residual offsets now use `reduced_size()` for every registered field block.
The coupled gravity state is:
```text
[ density(reduced), displacement(reduced), gravity-gradient(reduced), gravity-potential(reduced) ]
```
The gravity residual is:
```text
[ gradient-equation(reduced flux), Poisson-equation(reduced potential) ]
```
MFEM divergence operators remain full-space objects. Their operands/actions are explicitly scattered and gathered at the operator boundary.
### 8.5 Reduced solve and preconditioner
`solve_gravity_field` now:
1. validates that the supplied grid functions use the expected FEM spaces;
2. creates typed field/grid-function adapters;
3. gathers density and displacement;
4. builds the reduced block layout;
5. prepares the reduced gravity operator and geometry context;
6. solves the reduced mixed system with MINRES;
7. scatters the reduced flux and potential solution back to `ParGridFunction`s.
The reduced block preconditioner uses:
- `OperatorJacobiSmoother` on the reduced positive H(div) mass diagonal;
- a full-space approximate Schur complement `B diag(M)^{-1} B^T`;
- `HypreBoomerAMG` for the potential Schur block;
- explicit potential scatter/gather around the full-space AMG object.
This split is important. The first attempted reduced preconditioner encountered zero diagonal entries because inactive/full-space structure was being handed to a smoother that requires a positive diagonal. A later attempt produced immediate MINRES non-convergence. The final implementation makes the coordinate space of each preconditioner component explicit and is protected by the positive-diagonal and deformed-MINRES tests.
## 9. Barotrope, pressure, hydrostatic, displacement, and root migration
The FieldDof conversion was propagated beyond gravity so the coupled root operator has one consistent coordinate convention.
Map-aware contexts now include:
- `BarotropicClosureLinearizationContext`;
- `PressureForceLinearizationContext`;
- `HydrostaticEquilibriumContext`;
- `RotationalDisplacementForceLinearizationContext`;
- gravity geometry and linearization contexts.
Map-aware prepared operators include:
- barotropic closure;
- pressure force;
- hydrostatic equilibrium;
- gravity displacement force;
- rotation displacement force;
- composed displacement residual;
- mass normalization;
- mapped gravity source;
- mapped H(div) mass;
- complete stellar equilibrium.
Density and enthalpy are reduced at public prepared-operator boundaries. Displacement, gravity flux, and gravity potential also use `FieldDofMap`, even where their maps are identities today. This avoids hard-coding the current support choice into the coupled algebra.
`PreparedStellarEquilibriumOperator` constructs every block size from maps. Its root state is:
```text
[ density, displacement, gravity-gradient, gravity-potential, enthalpy, Bernoulli constant ]
```
Spatial blocks are in registered FieldDof coordinates; the Bernoulli constant remains a one-entry nonspatial block. The operator extracts reduced views, packs the gravity substate, routes the exact child dependencies, and assembles the residual only when a dependency or child preparation changes.
This removed the earlier ambiguous mixture in which some children expected full MFEM true vectors while others expected reduced field vectors.
## 10. EOS migration
The duplicate `physics::PolytropicBarotrope` implementation was removed.
The canonical EOS is now:
```cpp
mean_field::eos::Polytrope
```
It implements the `EquationOfState` interface and supplies density/pressure/enthalpy conversions and derivatives, including defined surface behavior at zero enthalpy/density and validation of material parameters and physical inputs.
Deleted legacy module:
```text
libmeanfield/interface/physics/barotrope.cppm
```
The file was removed from CMake, and no `PolytropicBarotrope` reference remains in production code, tests, or experiments. Local variables named `barotrope` may still refer to an `eos::Polytrope`; those names are descriptive variables, not the deleted class.
## 11. Dead and legacy code removed
The following dead implementations/modules were deleted:
- `libmeanfield/impl/mapping/domain_mapper_new.cpp` — merged into canonical `domain_mapper.cpp`;
- `libmeanfield/interface/physics/barotrope.cppm` — duplicate EOS implementation;
- `libmeanfield/interface/physics/context.cppm` — obsolete `GravityContext` owning the old assembled gravity solve/preconditioner stack.
CMake was updated so only the canonical mapper, EOS, and gravity context/operator stack are compiled.
Legacy-comparison gravity tests tied to the removed assembled implementation were removed when they ceased to compare two supported production paths. Analytic, variational, operator-identity, projection-floor, and self-consistency tests now provide the regression protection instead.
## 12. File and symbol renaming
Completed:
- canonical implementation file is `libmeanfield/impl/mapping/domain_mapper.cpp`;
- `_new` mapper filename removed;
- `DomainMapperStateless` renamed to `DomainMapper`;
- `DomainMapperStatelessOptions` renamed to `DomainMapperOptions`.
Intentionally not yet completed:
- the `fem::FEM` member remains named `domainMapperStateless`;
- many local variables and diagnostics still say “stateless domain mapper”;
- mapper test titles still begin with `Stateless Domain Mapper`.
These are naming cleanup items, not two competing mapper implementations. The user planned to perform broad identifier renames with IDE tooling. A reasonable final name for the FEM member is `domainMapper`.
## 13. Test infrastructure and tag cleanup
Tests use the typed compile-time tag system in `tests/test_helpers.cppm`:
- `make_tag` creates a Catch2 tag;
- `sub_tag` and `operator&` compose tag sets;
- canonical leaf/composite constants prevent hand-written duplicated tag strings.
New/normalized families include:
- `field_dof_unit` and `field_dof_integration`;
- `mapping_evaluator_unit`;
- gravity unit, integration, operator, prepared, context, kernel, accuracy, analytic, and consistency families;
- barotrope prepared, EOS, context, hydrostatic, pressure/quadrature, and mass-normalization families;
- rotation prepared/context/kernel/analytic families.
Touched gravity and prepared tests were converted to these typed tags. The guiding rule is to compose leaf semantics exactly once so output does not repeat inherited `[physics]` or `[solver]` tags.
### Tests added during the final uncommitted migration
Genuinely new adapter/evaluator/diagonal/schema coverage includes:
- `Domain Schema Builds Exact MFEM Attribute Markers`;
- `Field DOF Grid Function Adapter Gathers Exactly The Supported True DOFs`;
- `Field DOF Grid Function Adapter Scatter Projects And Round Trips Reduced Fields`;
- `Field DOF Grid Function Adapter Scatter Into Preserves Unsupported True DOFs`;
- `Field DOF Grid Function Adapter Is Exact For Identity Vector Field Maps`;
- `Field DOF Grid Function Adapter Rejects Incompatible Maps Spaces And Vectors`;
- `Grid Function Mapping Evaluator Refreshes A Cached Displacement Element`;
- `Grid Function Mapping Evaluator Invalidates A Cached Compactification Element`;
- `Grid Function Mapping Evaluator Rejects Invalid Fields And Rebinding`;
- `Prepared Mapped Hdiv Mass Diagonal Is Positive Across Both Domains`.
The gravity analytic tests formerly prefixed `New ...` were promoted to canonical names after removal of the obsolete comparison path. EOS test titles were similarly changed from `Polytropic Barotrope ...` to `Polytropic EOS ...`.
Important gravity coverage now includes:
- kernel equality, linearity, vacuum exclusion, and centered geometry variations;
- prepared source/mass identities and preparation reuse;
- full-column and simultaneous Jacobian comparisons;
- context selective invalidation and isolation;
- reduced block dimensions and routing;
- deformed MINRES solve;
- uniform sphere, homogeneous/deformed ellipsoid, Ferrers, monopole, and exterior compactification comparisons;
- virial and volume-preserving-deformation self-consistency;
- separation of physical exterior error from the finite-element projection floor.
Important barotrope/root coverage includes:
- EOS identities, derivatives, surface behavior, and invalid inputs;
- reduced barotropic closure under multiple EOS/geometry conditions;
- pressure residual and Jacobian comparisons;
- hydrostatic algebraic/displacement/complete Jacobians;
- mass-normalization routing and differences;
- displacement residual composition and MFEM routing;
- six-by-six stellar-equilibrium layout;
- child invalidation and complete centered differences;
- analytic N=1 and N=3 stellar states.
## 14. Migration failures and lessons
Several failures were valuable because they identified mixed-coordinate boundaries.
### Density-size abort in mapped-source variation
An early gravity shard aborted because a reduced density vector reached `apply_mapped_source_variation`, whose element indexing requires a full density true vector.
Resolution: retain the full-vector kernel contract and scatter in the prepared/context layer. Tests were migrated so directions and states use the correct representation at each boundary.
### Zero diagonal in `OperatorJacobiSmoother`
The first reduced gravity preconditioner attempted to construct a Jacobi smoother from a diagonal containing unsupported/full-space zeros.
Resolution: assemble and smooth the reduced positive mass diagonal, while retaining a separately assembled full true diagonal for the full divergence-based Schur approximation.
### Immediate MINRES non-convergence
After removing the zero-diagonal abort, an intermediate preconditioner arrangement stopped at iteration zero without convergence.
Resolution: use a consistent block preconditioner—reduced mass Jacobi plus full Schur AMG with explicit potential scatter/gather—and add a deformed reduced-system solve test.
### Stale shard files
Repository-root gravity shard files from August 24 showed several old analytic failures and an MPI abort. They were initially easy to mistake for the new run.
Resolution: use labeled logs in the dedicated temporary validation directory and verify modification times before diagnosing failures.
### Cached grid-function values
`GridFunctionMappingEvaluator` caches copies of element-local field DOFs. Mutating a referenced grid function does not automatically change an already cached element.
Resolution: expose/document `InvalidateCache()` and `Refresh()`, harden FE-space change/rebinding detection, update integrator entry points, and add direct mutation/cache tests.
## 15. Known limitations and remaining work
The primary FieldDof migration is complete. Remaining items are cleanup or explicitly bounded future work:
1. **Compile the exact post-rename tree.** `libmeanfield/impl/mapping/domain_mapper.cpp` is newer than the latest test executable.
2. **Finish cosmetic mapper naming.** Rename `domainMapperStateless`, related local variables/messages, and optionally mapper test titles.
3. **Commit the migration.** Review the large dirty worktree carefully and preserve unrelated user artifacts. Do not reset it.
4. **Nonconforming meshes remain unsupported by FieldDof resolution.** Add the correct conforming-constraint projection before lifting the explicit rejection.
5. **`GridFunctionMappingEvaluator` is not thread-safe.** Use one evaluator/workspace per thread if threaded assembly is introduced.
6. **Value mutation requires cache invalidation.** Keep the assembly-entry invalidations when refactoring integrators.
7. **Quadrature `Query.domain` remains legacy metadata.** Physical element selection in migrated prepared closure/pressure paths already comes from field support plus `DomainSchema`; the metadata can be migrated independently with the quadrature subsystem.
8. **The legacy `GravityMomentumIntegrator` exact geometry-Jacobian mode still aborts intentionally.** Its message states that exact geometry variation has not been wired into that legacy integrator. The prepared gravity-displacement path does have the tested exact variation. Do not mistake the old integrator limitation for an incomplete FieldDof conversion.
9. **No monolithic one-process full-suite run was performed.** The three filters cover every registered test case, but a full unsharded invocation would additionally probe process-global/order interaction. Given the suite's runtime, the user accepted complete partitioned coverage as sufficient.
## 16. Recommended immediate continuation
The next agent should:
1. Read this document and the earlier Rotating Polytrope Review summary.
2. Inspect `git status --short`; assume all existing changes belong to the user/migration.
3. Run a compile-only check of `cmake-build-debug-homebrew` after the final mapper rename.
4. Search again for:
```text
DomainMapperStateless
DomainMapperStatelessOptions
domain_mapper_new
PolytropicBarotrope
```
The first item will still appear as the lower-camel FEM member `domainMapperStateless`; the old PascalCase types and `_new` file should not appear.
5. If the user wants the cosmetic rename, use IDE-aware rename for the FEM member and related identifiers, then compile again. The change is broad but mechanical.
6. Review `git diff --check`, the three intended deletions, and CMake source lists.
7. Create a new checkpoint/final migration commit only after the user confirms the desired naming cleanup.
If a final regression rerun is desired after cosmetic renaming, the most efficient confidence gate is:
```bash
./cmake-build-debug-homebrew/tests \
"Field DOF Grid Function Adapter*,Grid Function Mapping Evaluator*,Prepared Mapped Hdiv Mass Diagonal Is Positive Across Both Domains,Reduced Gravity Field Operator Solves Deformed Gravity System With MINRES,Prepared Stellar Equilibrium*"
```
The already completed 24-shard run remains the substantive migration validation.
## 17. Invariants future changes must preserve
- Solver/prepared public spatial states are in registered FieldDof coordinates.
- Low-level MFEM element kernels receive full true vectors.
- Crossing that boundary must be visible as a map or adapter operation.
- Block sizes come from `FieldDofMap::reduced_size()`, never raw `GetTrueVSize()` unless the object is explicitly full-space.
- Field support comes from the field registry plus a `DomainSchema`, not numeric attribute conventions.
- Shared continuous interface DOFs use union-of-supported-elements semantics.
- `scatter` means projection/zero outside support; `scatter_into` means preserve outside support.
- Grid-function adapters validate FE-space identity, not merely equal sizes.
- Mapping evaluator caches must be invalidated after value mutation.
- Prepared contexts own their linearization maps, revisions, and full-vector workspaces.
- Test reference kernels must be passed full true vectors; solver/operator calls must be passed reduced vectors.
These invariants are the durable result of the migration and should be treated as the design contract going forward.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
# Stellar-equilibrium preconditioning campaign — 2026-09-03
## Scope
This campaign established and tested a compile-time composable right-preconditioner for the current bordered stellar-equilibrium Jacobian. The reference problem is the zero-rotation, `n = 3` polytrope with an isobaric surface, fixed total mass, and fixed central density, discretized on `sandbox.smesh` and linearized at the projected Lane–Emden seed.
The complete operator remains matrix-free. Sparse or dense matrices are assembled only for individual surrogate blocks.
## Recommended policy before the nonlinear-solver phase
The best-supported complete-system policy retains:
- an uncalibrated surface Riesz diagonal;
- surface-then-material triangular factorization;
- independent material/surface and gravity subsystems;
- gravity approximate LDU;
- an exact dense Schur correction for the small specification border.
The automatic gravity policy now uses an order-5 fixed matrix-free Chebyshev inverse for the mapped H(div) mass operator and three fixed BoomerAMG cycles for the potential Schur surrogate. The low-level composition API can still select order 4 for a slightly cheaper application, diagonal mass inversion, and any valid fixed-cycle configuration.
Surface scalar calibration, the surface H1 mass-plus-stiffness surrogate, material approximate LDU, and cross-subsystem stellar factorizations remain explicit opt-in experiments. None has earned promotion to the default.
## What the measurements showed
### Material–surface subsystem
The uncalibrated surface-then-material factorization reached a true relative residual of approximately `6.42e-6` in 40 iterations on the manufactured subsystem problem. Operator-action scalar calibration improved that isolated residual to approximately `1.38e-6`, and material LDU with the same calibration reached approximately `1.11e-6`.
That isolated improvement did not transfer to the complete operator. The surface Jacobian spans roughly three orders of magnitude across tested surface modes, so a single scalar cannot be a uniformly accurate inverse. A right-preconditioned-action fit was added because it optimizes the operation actually used by `J M^-1`; it reduced the isolated balanced recovery defect from approximately `1.010` to `0.848` and was stable from one to sixteen probes. It is retained as an experiment, not a default.
The H1 mass-plus-stiffness surface surrogate also remains experimental. Its fit residual and extreme gains showed that this two-term local differential model does not reliably represent the restricted surface Jacobian on the current discretization.
### Gravity subsystem
With a diagonal H(div) mass inverse and one fixed BoomerAMG cycle, gravity approximate LDU was decisively better than block-diagonal or one-sided triangular factorizations. It reached `1.59e-8` after 100 iterations, versus approximately `1e-6` for the triangular policies and `2.84e-3` for block diagonal.
The new backend sweep separated two approximation errors:
- the mapped H(div) mass inverse;
- the scalar potential-Schur inverse supplied by fixed-cycle BoomerAMG.
Two AMG cycles reduced the measured Schur recovery defect from `0.169` to `0.083` and made the diagonal-mass gravity solve converge in 87 iterations. A fixed matrix-free Chebyshev polynomial applied to the exact partially assembled H(div) mass operator improved the coupled gravity iteration count much more strongly. Representative converged results were:
| H(div) mass inverse | AMG cycles | iterations | solve time (s) | true relative residual |
|---|---:|---:|---:|---:|
| diagonal | 2 | 87 | 5.43 | `8.51e-9` |
| Chebyshev order 3 | 2 | 47 | 5.82 | `8.27e-9` |
| Chebyshev order 4 | 2 | 44 | 6.80 | `8.49e-9` |
| Chebyshev order 4 | 3 | 43 | 7.36 | `7.23e-9` |
| Chebyshev order 5 | 3 | 42 | 8.48 | `8.00e-9` |
For the isolated gravity solve, order 3–4 is the useful quality/cost region: order 5 buys only one or two additional iterations at materially higher preconditioner cost. The complete-system comparison below nevertheless shows that order 5 earns its small end-to-end cost there. MFEM 4.9.1 supports Chebyshev orders only in `[1, 5]`; the backend now validates that range and turns an otherwise fatal MFEM abort into an ordinary configuration error.
### Complete bordered stellar operator
On the block-balanced manufactured correction, the original complete default reached a true relative residual of `3.47e-4` in 36 iterations. Identity reached only `0.783`. Every previously tested calibrated or more strongly coupled composition was worse (`0.0107`–`0.0413`) and substantially more expensive.
Holding every other block and factorization fixed, two AMG cycles alone produced only `3.46e-4`. Chebyshev order 3 with two AMG cycles improved this to `2.24e-4`; Chebyshev order 4 with three AMG cycles reached `1.43e-4`; and the final order-5/AMG-3 challenger reached `1.19e-4`. Relative to order 4, order 5 reduced the fixed-budget residual by another 16.7% and the correction error from `2.844` to `2.219`, while increasing the matched total solve time only from `181.16` s to `182.00` s. Relative to the diagonal-mass control, it reduced the fixed-budget residual by 65.6%. This evidence supports promoting Chebyshev-5/AMG-3 as the automatic gravity default, while retaining order 4 as the nearby quality/cost option.
For that default result, 93.8% of the final squared residual was in the gravity-gradient equation and 6.2% was in Poisson balance. Material, surface, and specification-border residuals were negligible globally. This motivated the H(div) Chebyshev work rather than further surface tuning.
The short Arnoldi projection is useful as a diagnostic, but not as a policy oracle. In the complete system the identity operator had a much smaller 12-vector projected condition proxy than the successful default even though its FGMRES result was dramatically worse. True residual reduction and Jacobian applications therefore remain the primary decision metrics.
### Physical Newton right-hand side
The physical experiment solves `J delta = -R(u)` at the same projected Lane–Emden state and evaluates seven damped nonlinear trial states for every correction. All trial states in the first comparison were finite, nonnegative in density, and orientation preserving.
The physical right-hand side exposed a solver-level issue that the manufactured problem does not: all candidates stagnated at the 20-vector restart. The current default reduced the true linear residual only to `0.999800` in 48 iterations; uncalibrated material-then-gravity reached `0.999092` but cost more than three times as much per solve. The right-hand side is dominated by barotropic closure (`3.51e-5`), while its gravity rows are around `1e-11`; the unscaled Euclidean Krylov norm and restart therefore do not provide a physically balanced convergence measure. This is evidence for explicit equation scaling and restart studies in the solver phase, not evidence that the complete preconditioner should adopt the expensive cross factorization.
## Correctness and lifecycle guarantees added
- Fixed and adaptive backend application modes remain distinct; only stationary-linear configurations are Arnoldi admissible.
- The matrix-free Chebyshev backend owns its diagonal and essential-DOF list, uses a deterministic power estimate, is fixed-order linear, and reconstructs itself on geometry refresh.
- Chebyshev configuration validates polynomial order, power-iteration count, a strictly positive random seed, a power-method tolerance in `(0, 1)`, size, and strict positivity of the assembled mass diagonal.
- Gravity composition is generic over a compile-time compatible mass backend, so the new backend propagates through the stellar-structure and specification-border layers without a runtime type registry.
- Prepared composite blocks explicitly prohibit unsafe copying and moving when their factorization stores pointers into their own members.
- Unit tests cover backend compatibility, linearity, positivity, statistics, invalid configuration, and composite ownership. Integration tests cover geometry staleness and explicit refresh without repreparing the physical geometry.
- The focused gravity integration suite passes with both one and two MPI ranks. It applies the Chebyshev-backed block after an ordinary geometry refresh, then forces a discretization revision that reconstructs the underlying matrix-free operator, verifies that stale application and stale inverse access are rejected, refreshes the owner, and verifies finite repeatable post-rebind actions. Full numerical quality comparisons in this campaign remain single-rank.
## Reproducible artifacts
- `preconditioning_p9_surface_calibration_objectives.csv`
- `preconditioning_p10_full_stellar.csv`
- `preconditioning_p10_physical_newton.csv`
- `preconditioning_p4_gravity_chebyshev_sweep_v2.csv`
- `preconditioning_p10_gravity_backend_finalists.csv`
- `preconditioning_p10_gravity_backend_finalists_v2.csv`
The experiment sources are `experiments/material_surface_preconditioning.cpp`, `experiments/gravity_preconditioning.cpp`, and `experiments/full_stellar_preconditioning.cpp`.
## Remaining work
1. In the solver phase, introduce physically meaningful equation/value scaling and compare restarted with unrestarted or larger-restart FGMRES on the physical Newton right-hand side.
2. Validate the selected policy across mesh refinement, nonzero rotation, deformed geometry, and more than one MPI rank.
3. Revisit surface preconditioning only with a richer surrogate or multilevel representation; do not promote a scalar or H1 fit solely from isolated measurements.
4. Treat adaptive inner solves as FGMRES-only and exclude them from fixed-operator Arnoldi studies.

126
docs/profiling_and_mpi.md Normal file
View File

@@ -0,0 +1,126 @@
# Profiling and MPI validation
This guide describes the profiling and distributed-test tools added after
checkpoint `25510008ddf9810eaf9821149734d526c76b087a`.
## Build modes
The profiling macros compile out by default. For a profiling build, enable them
and leave IPO off so profile attribution is easier to interpret:
```bash
./format
cmake -S . -B cmake-build-profile-homebrew-llvm -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DMEAN_FIELD_ENABLE_PROFILING=ON \
-DMEAN_FIELD_ENABLE_IPO=OFF
./format
cmake --build cmake-build-profile-homebrew-llvm --target tests mpi_tests -j 6
```
For production timings, use profiling off and IPO on:
```bash
./format
cmake -S . -B cmake-build-release-homebrew -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DMEAN_FIELD_ENABLE_PROFILING=OFF \
-DMEAN_FIELD_ENABLE_IPO=ON
./format
cmake --build cmake-build-release-homebrew --target tests mpi_tests experiments -j 6
```
Always run `./format` immediately before a command that can compile.
## Bounded test runner
`tools/profile_tests.py` accepts a Catch2 filter, rank counts, repetitions, and
a per-run timeout. It writes one log per run and updates the summary CSV after
every repetition.
```bash
python3 tools/profile_tests.py \
--executable cmake-build-profile-homebrew-llvm/tests \
--ranks 1 --repeat 1 --timeout 360 \
--output-directory /private/tmp/meanfield-profile \
"Gravity Field Virial Consistency Across Volume Preserving Deformation" \
-- --reporter console
```
MPI sweep:
```bash
python3 tools/profile_tests.py \
--executable cmake-build-release-homebrew/tests \
--ranks 1,2,4 --repeat 3 --timeout 360 \
--output-directory /private/tmp/meanfield-rank-sweep \
"Gravity Field Virial Consistency Across Volume Preserving Deformation"
```
Arguments after `--` go to Catch2. Runner options may appear before or after the
filter. On timeout, the runner sends `SIGTERM` to the entire process group,
waits for `--kill-grace`, and then sends `SIGKILL` if necessary. This prevents
MPI workers from surviving a timed-out launcher.
The CSV records rank count, repetition, status, exit code, wall time, timeout,
log path, and exact command. Profile-region output is in the corresponding log.
## Profile columns
- `Samples`: recorded observations after warmups.
- `Warmups`: discarded warmup observations.
- `Work/rank`: minimum-to-maximum work-unit count across ranks.
- `Avg max ms`: maximum per-rank average duration.
- `Min ms` and `Max ms`: global extrema across all samples and ranks.
- `Total max s`: maximum accumulated duration on any rank.
MPI aggregation first unions label sets across ranks. A region may therefore be
visited on only one rank without causing collective-order divergence.
## MPI regression suite
The dedicated `mpi_tests` executable covers distributed semantics without
replicating the complete serial suite on every process. Registered CTest jobs
use two and four ranks:
```bash
ctest --test-dir cmake-build-release-homebrew \
--label-regex mpi --output-on-failure --timeout 180
```
Or build and run through the convenience target (remember that the target can
compile, so format first):
```bash
./format
cmake --build cmake-build-release-homebrew --target check_mpi
```
The suite checks communicator splitting, global element coverage, mapped H(div)
mass symmetry and linearity, divergence adjointness, a complete distributed
gravity analysis/solve, and profiling with rank-local labels. Catch2 uses a
fixed synchronized seed so all ranks enter tests and collectives in the same
order. Only rank zero emits the report.
The current measurements use multiple MPI ranks on one host. The suite is
appropriate for a multi-node launcher, but inter-host fabric behavior still
needs validation on actual multi-node hardware.
## Known high-level limitation
The reduced gravity and virial paths run on multiple ranks. The complete
spectral equilibrium experiment does not yet do so because logical radial
surface interpolation explicitly requires one rank. Supporting that path needs
distributed ownership and transpose-consistent communication for interpolation
rows whose surface element may be remote. This is a separate architectural
feature, not a failure of the corrected gravity kernels.
## Current reference results
- Prepared spectral Jacobian, one rank: 3.964707 s.
- Gravity virial: 205.843 s (one rank), 118.926 s (two), 65.210 s (four).
- Centrifugal h refinement: 254.984 s, historically 525.601 s.
- Release MPI CTest jobs: 18.12 s (two ranks), 10.11 s (four ranks).
- Full release suite: 410 cases and 5,086,850 assertions passed in 2,807.831 s.
No tolerance or resolution was changed to obtain these results.

View File

@@ -0,0 +1,700 @@
# Extending stellar-model physics
MeanField treats a stellar model as a compile-time collection of independent physics specifications. An EOS, a surface condition, an integral constraint, and a phase condition each describe only their own contribution. The library then infers the model type, generated scalar blocks, residual blocks, Jacobian edges, normalization, and manifest entries from that collection.
The important rule is: **add one declaration and, where needed, one implementation per physics specification—never a specialization for a whole combination of constraints.** A new integral must compose with every already-supported EOS, surface, and other constraint without naming those combinations.
Every downstream layer folds the same canonical specification pack. The symbolic compiler appends generated blocks and Jacobian edges, the manifest and normalization layers project metadata for those exact blocks, the prepared equilibrium operator owns one statically typed runtime slot per specification, and the default preconditioner folds the corresponding border actions. “Variadic” therefore means one inferred C++ type and one inferred block system for each pack, not a runtime list and not a library of hand-written pack combinations.
## The small physics-facing contract
A specification is an ordinary physics class with:
- a `Parameters` aggregate;
- a constructor from `Parameters`; and
- one nested `ModelDefinition` made from a role-specific alias.
The declaration is the single source used by `SpecificationTraits` and `SpecificationContribution`; an extension author does not specialize either trait.
### EOS
The EOS still uses the existing relation vocabulary. `ModelDefinition` only tells the stellar-model compiler which role the object has.
```cpp
class LinearBarotrope final {
public:
struct Parameters final {
double densityPerEnthalpy;
};
using ModelDefinition = mean_field::eos::ConstitutiveLaw<
LinearBarotrope,
"LinearBarotrope">;
using Relations = mean_field::eos::RelationCatalog<
mean_field::eos::DensityFromSpecificEnthalpy>;
explicit constexpr LinearBarotrope(Parameters p) noexcept
: m_densityPerEnthalpy(p.densityPerEnthalpy) {
}
[[nodiscard]] constexpr mean_field::dimensions::DensityValue evaluate(
mean_field::eos::DensityFromSpecificEnthalpy,
mean_field::dimensions::SpecificEnthalpyValue h
) const noexcept {
return mean_field::dimensions::DensityValue{m_densityPerEnthalpy * h.value()};
}
[[nodiscard]] constexpr auto partialDerivative(
mean_field::eos::DensityFromSpecificEnthalpy,
mean_field::eos::WithRespectTo<mean_field::dimensions::quantity::SpecificEnthalpy>,
mean_field::dimensions::SpecificEnthalpyValue
) const noexcept {
return mean_field::eos::PartialDerivative<
mean_field::dimensions::quantity::Density,
mean_field::dimensions::quantity::SpecificEnthalpy>{m_densityPerEnthalpy};
}
private:
double m_densityPerEnthalpy;
};
static_assert(mean_field::eos::EquationOfStateModel<LinearBarotrope>);
static_assert(mean_field::models::ModelSpecification<LinearBarotrope>);
```
The explicit quantity type is what prevents accidental pressure/density/enthalpy interchange.
### Surface condition
A surface condition declares the physical quantity it fixes. Describing a new condition is intentionally separate from implementing its finite-element carrier equation.
```cpp
class IsothermalSurface final {
public:
struct Parameters final {
mean_field::dimensions::TemperatureValue Tsurface;
};
using ModelDefinition = mean_field::surface::BoundaryCondition<
IsothermalSurface,
"IsothermalSurface">;
using PhysicalQuantity = mean_field::dimensions::quantity::Temperature;
using TargetValue = mean_field::dimensions::TemperatureValue;
explicit constexpr IsothermalSurface(Parameters p) noexcept
: m_target(p.Tsurface) {
}
[[nodiscard]] constexpr TargetValue targetTemperature() const noexcept {
return m_target;
}
private:
TargetValue m_target;
};
```
### Integral constraint
An integral says which stellar state variables it reads (`stellar::Reads`), which equations its generated coordinate changes (`stellar::Changes`), how its generated value and residual are normalized, and how they appear in diagnostics. The ordinary declaration uses physics names; the equilibrium compiler translates them to numerical blocks.
```cpp
class PreparedFixedBaryonMass;
class FixedBaryonMass final {
public:
struct Parameters final {
mean_field::dimensions::MassValue Mbaryon;
};
using ScalarDescription = mean_field::stellar::ScalarConstraint<
mean_field::dimensions::quantity::Mass,
mean_field::dimensions::quantity::SpecificEnergy,
mean_field::dimensions::quantity::Mass,
"fixed_baryon_mass.multiplier", "C_b",
"fixed_baryon_mass.residual", "R_Mb">;
using TargetValue = typename ScalarDescription::TargetValue;
using ModelDefinition = mean_field::integral::FixedScalarWithMultiplier<
FixedBaryonMass,
"FixedBaryonMass",
mean_field::stellar::Reads<
mean_field::stellar::state::Density,
mean_field::stellar::state::SurfaceShape>,
mean_field::stellar::Changes<
mean_field::stellar::equation::HydrostaticBalance>,
ScalarDescription>;
using EquilibriumPhysics =
mean_field::operators::LocalSpecificationEquilibriumPhysics<
PreparedFixedBaryonMass>;
explicit FixedBaryonMass(Parameters p) : m_target(p.Mbaryon) {
}
// This conventional accessor lets the generic manifest read the target.
[[nodiscard]] TargetValue target() const noexcept {
return m_target;
}
private:
TargetValue m_target;
};
```
`FixedIntegralWithPhysicalCoordinate` has the same declaration shape, but its generated scalar is a physical state coordinate rather than a Lagrange multiplier. `FixedAngularMomentum`, whose generated coordinate is angular velocity, is the current example.
`stellar::ScalarConstraint` names three independent physical quantities: the
target, the generated Newton coordinate, and the appended constraint residual.
The wrapper derives numerical scale laws and diagnostic unit labels from those
strong quantity types, so a total energy cannot accidentally be normalized as
a specific energy merely because two free-form strings agree. The quantities
need not be the same: `FixedCentralDensity` has a density target and a
specific-enthalpy phase residual. The wrapper contains no assembly logic. The
lower-level `FixedIntegralWithMultiplier` spelling remains available for
unusual compatibility declarations; opting either its normalization or
manifest into typed dimensional metadata requires the complete typed pair. The
current generated-constraint
compiler supports exactly one scalar value and one scalar residual per
generated specification; a distributed or multi-component generated quantity
requires an explicit extension of that compiler boundary and is not obtained by
switching to the lower-level spelling.
If the target and appended residual have different quantity types, the
specification also supplies the physical reference used to report and scale
that residual:
```cpp
[[nodiscard]] mean_field::dimensions::SpecificEnergyValue
residualReference() const noexcept {
return m_enthalpyReference;
}
```
The return type must be exactly the `QuantityValue` for
`ScalarDescription::ConstraintResidualQuantity`. Omitting this accessor, or
returning the target's quantity instead, makes
`CompilableRootManifestFor<Model, Form>` false. Thus a density target cannot
silently provide the scale for an energy-valued equation. The built-in
`FixedCentralDensity` phase condition is the deliberate special case: its
specific-enthalpy reference is derived from the selected EOS when the
constraint is compiled.
When an integral residual also depends on the coordinate generated by that same specification, use `stellar::state::OwnGeneratedCoordinate`. The compiler resolves that relative name to the specification's exact generated type. Prefer the exported `mean_field::stellar::{state,equation}` names and the `stellar::Reads`, `stellar::Changes`, `stellar::ScalarConstraint`, and `stellar::Derivative` wrappers in ordinary physics code. These wrappers are aliases over the compiler's canonical types: they improve readability without introducing a parallel implementation or weakening any compile-time check.
### Phase condition
A phase condition uses the same vocabulary but generates a solver-border coordinate. It fixes a degeneracy; it is not automatically a new physical invariant.
```cpp
class FixedCentralEnthalpy final {
public:
struct Parameters final {
mean_field::dimensions::SpecificEnthalpyValue hcenter;
};
using ScalarDescription = mean_field::stellar::ScalarConstraint<
mean_field::dimensions::quantity::SpecificEnthalpy,
mean_field::dimensions::quantity::SpecificEnthalpy,
mean_field::dimensions::quantity::SpecificEnthalpy,
"fixed_central_enthalpy.border", "lambda_hc",
"fixed_central_enthalpy.residual", "R_hc">;
using TargetValue = typename ScalarDescription::TargetValue;
using ModelDefinition = mean_field::constraint::ScalarPhaseCondition<
FixedCentralEnthalpy,
"FixedCentralEnthalpy",
mean_field::stellar::Reads<
mean_field::stellar::state::SpecificEnthalpy>,
mean_field::stellar::Changes<
mean_field::stellar::equation::HydrostaticBalance>,
ScalarDescription>;
explicit FixedCentralEnthalpy(Parameters p) : m_target(p.hcenter) {
}
[[nodiscard]] auto target() const noexcept {
return m_target;
}
private:
TargetValue m_target;
};
```
Generated arity is inferred by the role alias: a constitutive law or boundary condition generates no scalar pair, while the three scalar constraint aliases each generate one balanced scalar value/residual pair. This zero-or-one-pair rule is the deliberate current generated-constraint boundary, not a claim that arbitrary-rank new fields are already supported. A malformed normalization or manifest remains inspectable but fails its corresponding completeness query.
### Exact residual and Jacobian providers
A declaration determines the complete provider topology. The nested `EquilibriumPhysics` alias above connects that declaration to its numerical formulas.
Most prepared physics classes keep the simple `Physics(const Specification&)`
constructor. A specification which declares both `state::Density` and
`state::SurfaceShape` in `Reads` may instead request the current physical
density-volume integral through
`stellar::DensityVolumeIntegralContext<Specification>`:
- `integrateDensity(rho)` evaluates the distributed, mapped-volume finite-element
integral and returns `dimensions::MassValue`;
- `linearizeDensityIntegral(deltaRho)` returns its exact density action; and
- `linearizeSurfaceShapeIntegral(deltaShape)` returns its exact geometry action.
Both declarations are mandatory because `integral rho dV` depends on the
deformed volume. A density-only declaration is rejected before the context type
can be formed, so it cannot hide a missing geometry column in the inferred
Jacobian. A future fixed/reference-geometry integral would be a separate,
explicitly named service rather than a weakened form of this one.
The context is a small, copyable, non-owning handle. It exposes neither the
physical core nor FEM objects, uses no heap type erasure, and delegates global
reduction and reusable scratch storage to the physical core. Its numerical
methods are valid in `PrepareAfterPhysical` and subsequent residual, Jacobian,
or border-action work, after the core has prepared the current state; a physics
constructor should only retain the handle. This service is deliberately scoped
to the current core's density-volume integral rather than being an untyped
general-purpose backend escape hatch.
`PreparedFixedBaryonMass` is still an ordinary concrete class. It does not need
a model template, block list, offset table, or MFEM access. A representative
context-using shape is:
```cpp
class PreparedFixedBaryonMass final {
public:
using Report = BaryonMassPreparationReport;
using IntegralContext =
mean_field::stellar::DensityVolumeIntegralContext<FixedBaryonMass>;
PreparedFixedBaryonMass(
const FixedBaryonMass &specification,
IntegralContext integrals
) : m_target(specification.target()), m_integrals(integrals) {
}
template <typename State>
[[nodiscard]] Report PrepareAfterPhysical(const State &state) {
// Only names from stellar::Reads plus this specification's generated
// coordinate are visible here.
const auto density = state.density();
const auto multiplier = state.generatedCoordinate();
m_currentMass = m_integrals.integrateDensity(density);
m_massResidual = m_currentMass.value() - m_target.value();
prepareHydrostaticTerm(multiplier);
m_prepared = true;
return {};
}
template <typename Row>
[[nodiscard]] auto AddResidual(
mean_field::stellar::equation::OwnConstraint,
Row &row
) const {
return row.add(m_massResidual);
}
template <typename Row>
[[nodiscard]] auto AddResidual(
mean_field::stellar::equation::HydrostaticBalance,
Row &row
) const {
return row.add(m_hydrostaticTerm);
}
template <typename Direction, typename Row>
[[nodiscard]] auto AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::OwnConstraint,
mean_field::stellar::state::Density>,
const Direction &direction,
Row &row
) const {
return row.add(
m_integrals.linearizeDensityIntegral(direction.density()).value());
}
template <typename Direction, typename Row>
[[nodiscard]] auto AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::OwnConstraint,
mean_field::stellar::state::SurfaceShape>,
const Direction &direction,
Row &row
) const {
return row.add(m_integrals
.linearizeSurfaceShapeIntegral(direction.surfaceShape()).value());
}
// Reads is conservative for all changed equations. This particular
// derivative is mathematically absent, so say that explicitly.
template <typename Direction, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::state::Density>,
const Direction &,
Row &
) const noexcept {
return mean_field::stellar::zeroDerivative;
}
template <typename Direction, typename Row>
[[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::state::SurfaceShape>,
const Direction &,
Row &
) const noexcept {
return mean_field::stellar::zeroDerivative;
}
template <typename Direction, typename Row>
[[nodiscard]] auto AddJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::state::OwnGeneratedCoordinate>,
const Direction &direction,
Row &row
) const {
return row.add(applyMultiplierDerivative(
direction.generatedCoordinate()));
}
[[nodiscard]] bool IsPrepared() const noexcept {
return m_prepared;
}
private:
// Physics data and helper functions; no solver block bookkeeping.
FixedBaryonMass::TargetValue m_target;
IntegralContext m_integrals;
mean_field::dimensions::MassValue m_currentMass{0.0};
double m_massResidual{0.0};
double m_hydrostaticTerm{0.0};
bool m_prepared{false};
};
```
The hydrostatic helper formula above is a placeholder for the extension's actual physics; the integral and its two directional actions are the real typed service contract. The compiler derives these calls from `Reads` and `Changes`:
- it requests one `AddResidual(EquationTag, Row&)` provider for the specification's own constraint equation and for each distinct equation in `Changes`;
- it requests `OwnConstraint` derivatives with respect to every state in `Reads`; and
- it requests each `Changes` equation's derivatives with respect to every state in `Reads` plus `OwnGeneratedCoordinate`.
The compiler, not the physics class, enumerates and invokes this set. Every Jacobian call receives a direction containing exactly one declared state and a row that can modify exactly one declared equation. Named accessors such as `density()`, `specificEnthalpy()`, and `generatedCoordinate()` are available only when that source is the selected derivative. `value()` is the backend-independent accessor for a one-source extension tag that does not yet have a named convenience accessor.
Every provider returns one of two statements:
- return the token from `row.add(scalar_or_vector)` or, for a residual entry, `row.addEntry(index, value)`; or
- return `stellar::structuralZero` for an intentionally zero residual contribution, and conventionally `stellar::zeroDerivative` for an intentionally zero derivative.
This is stronger than a broad callback that is merely callable. Omitting one inferred overload makes `CompleteStellarEquilibriumPhysicsProvider<Physics, Specification, Model>` false at compile time. Within a callback, adding twice is rejected at runtime. Returning `ContributionAdded` without one addition, or returning `StructuralZero` after an addition, is also rejected immediately. Separate specifications may still add to the same assembled equation; additive composition across specification slots is intentional.
The exact row API exposes no assignment, mutable root vector, block offset, or MFEM alias synchronization. Consequently one specification cannot redirect a derivative to another legal row or erase the physical core or another specification's contribution. The lower-level `SpecificationEquilibriumPhysics` spelling remains available when a concrete implementation genuinely depends on the complete `Model` type, but it uses this same exact adapter and guarantees.
For the current isobaric surface, the pressure condition owns selected rows of the specific-enthalpy equation. Exact residual and Jacobian rows automatically suppress contributions on those replaced rows. A constraint computes its ordinary full hydrostatic contribution; it does not reproduce the surface-row mask.
## Composition and model access
Construction order does not affect the inferred type:
```cpp
auto model = mean_field::model::StellarModel(
LinearBarotrope({.densityPerEnthalpy = 2.0}),
IsothermalSurface({.Tsurface = mean_field::dimensions::TemperatureValue{1.0e6}}),
FixedBaryonMass({.Mbaryon = mean_field::dimensions::MassValue{1.0}}),
FixedCentralEnthalpy({.hcenter = mean_field::dimensions::SpecificEnthalpyValue{0.25}})
);
using Model = std::remove_cvref_t<decltype(model)>;
static_assert(Model::template specificationRoleCount<
mean_field::models::SpecificationRole::invariant> == 1);
const auto &eos = model.equationOfState();
const auto &surface = model.surfaceCondition();
const auto &mass = model.specification<FixedBaryonMass>();
```
`equationOfState()` is always available because a valid model has exactly one constitutive law. `surfaceCondition()` and `specificationForRole<Role>()` are available only when that role is unique. Roles such as integral constraints may occur many times; use `SpecificationsForRole<Role>` at compile time and `specification<ExactType>()` at runtime.
Stable names are identities within a role. Two independently developed invariants therefore choose descriptive stable names; they do not coordinate global numeric ordinals. Reusing a stable name in the same role is rejected at compile time, while the same readable name may intentionally appear in two different roles. Manifest lookup always compares the full `(role, stable name)` identity.
### Coupling constraints to one another
Global constraints are not required to be mutually independent. A specification can refer to another specification in the same model using physics-level owner tags:
```cpp
class CoupledVirialCondition final {
public:
using ModelDefinition = mean_field::integral::FixedScalarWithMultiplier<
CoupledVirialCondition,
"CoupledVirialCondition",
mean_field::stellar::Reads<
mean_field::stellar::state::Density,
mean_field::stellar::state::GeneratedCoordinateOf<FixedBaryonMass>>,
mean_field::stellar::Changes<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::equation::ConstraintOf<FixedBaryonMass>>,
VirialScalarDescription>;
// Parameters, target(), and the two local physics packages follow the
// same pattern as an uncoupled scalar constraint.
};
```
`GeneratedCoordinateOf<FixedBaryonMass>` means “read the scalar coordinate owned by this exact specification type.” `ConstraintOf<FixedBaryonMass>` means “my generated coordinate changes that specification's scalar equation.” `OwnGeneratedCoordinate` and `OwnConstraint` are the corresponding relative names for the declaring specification. These declarations generate ordinary `stellar::Derivative<Equation, State>` calls, so cross-constraint formulas use the same exact residual/Jacobian and preconditioner protocols as every other edge. For a cross-generated direction, the equilibrium view's generic one-source accessor is `value()`; a border-preconditioner direction may use `values()` or `generatedCoordinate<Owner>()`.
The compiler resolves owner references after collecting the complete canonical specification pack, so declaration order is irrelevant. A referenced owner that is absent, does not generate the requested coordinate/residual, or cannot be mapped into the final form makes symbolic compilation fail. It is never converted into a runtime lookup or silently omitted.
## Capability ladder
Description is deliberately easier than executable solver support. Each rung is
an ordinary compile-time capability query, so an unsupported physics component
is rejected at the public overload boundary instead of producing an error deep
inside a constructor or solver body.
For a model declaration, the first two rungs are:
```cpp
static_assert(mean_field::models::SelfDescribingModelSpecification<FixedBaryonMass>);
static_assert(mean_field::models::ModelSpecification<FixedBaryonMass>);
static_assert(mean_field::operators::StellarEquilibriumSpecificationCompilable<FixedBaryonMass>);
static_assert(mean_field::operators::StellarEquilibriumSymbolicallyCompilable<Model>);
static_assert(mean_field::models::CompleteGeneratedScalarDimensionsFor<FixedBaryonMass>);
static_assert(mean_field::models::CompleteGeneratedNormalizationFor<FixedBaryonMass>);
static_assert(mean_field::normalization::CompleteGeneratedPhysicalRieszNormalizationFor<FixedBaryonMass>);
static_assert(mean_field::models::CompleteGeneratedManifestFor<FixedBaryonMass>);
using Form = mean_field::operators::CompiledStellarEquilibriumForm<Model>;
// This example's new surface has no manifest/runtime adapter yet.
static_assert(!mean_field::operators::CompilableRootManifestFor<Model, Form>);
// These become true after the per-specification runtime, EOS core, and surface
// implementations are added. They are intentionally false for this example.
static_assert(!mean_field::operators::StellarEquilibriumPhysicsAvailableFor<
FixedBaryonMass, Model>);
static_assert(!mean_field::operators::hasCompleteStellarEquilibriumRuntime<Model>);
static_assert(!mean_field::operators::hasStellarEquilibriumCoreRuntime<Model>);
```
`Model::hasCompleteEquilibriumDeclaration` reports only that the model's physics declarations are structurally complete and balanced. It deliberately makes no claim about backend support. The clearly named query for block and Jacobian compilation is `StellarEquilibriumSymbolicallyCompilable<Model>`; `StellarEquilibriumSystemCompilable<Model>` remains its compatibility spelling. Only `CompiledStellarEquilibriumSystem<Model>` exposes an equilibrium-system compilation classification. This distinction lets an astronomer write and inspect a new physical declaration even when one of its physics names has not yet been mapped to a numerical block.
Once a model has an executable core, surface runtime, manifest, and all of its
per-specification residual/Jacobian actions, it satisfies
`equilibrium::StellarEquilibriumModel` and can be discretized into a `Problem`.
The remaining solver-facing capabilities are then checked on that inferred
problem type:
```cpp
// The selected discretization prescription has a valid preparation path.
static_assert(mean_field::normalization::NormalizableStellarEquilibriumProblem<Problem>);
using PhysicalRieszDiscretization = mean_field::equilibrium::StellarDiscretizationFor<
mean_field::normalization::PhysicalRieszDiagonal<>>;
using PhysicalRieszProblem = mean_field::equilibrium::StellarEquilibriumProblem<
Model, PhysicalRieszDiscretization>;
// These stronger queries are specifically for the Physical Riesz path.
static_assert(mean_field::normalization::PhysicalRieszStellarEquilibriumProblem<
PhysicalRieszProblem>);
static_assert(mean_field::normalization::NormalizableStellarEquilibriumProblem<
PhysicalRieszProblem>);
static_assert(mean_field::normalization::PhysicalRieszStellarEquilibriumCore<
typename PhysicalRieszProblem::PhysicalCoreType>);
// The EOS-specific material/surface surrogate supports this exact core.
static_assert(mean_field::preconditioning::MaterialSurfacePreconditionerProblem<Problem>);
static_assert(mean_field::preconditioning::StellarStructurePreconditionerProblem<Problem>);
// Every generated border has an action, and the structure block is supported.
static_assert(mean_field::preconditioning::CompleteSpecificationBorderActionsFor<Problem>);
static_assert(mean_field::preconditioning::DefaultStellarPreconditionerAvailableFor<Problem>);
```
The complete ladder is:
1. **Model description:** the object is constructible and has one valid, self-owned definition.
2. **Blocks and Jacobian:** its block lists are valid, generated rows and columns balance, and every declared edge endpoint exists in the final model. The compiler performs two passes, so one constraint may name a block generated by another constraint without depending on declaration order.
3. **Executable equilibrium root:** `StellarEquilibriumCoreRuntime<EOS>` selects the concrete physical `CoreType` for the constitutive law. Surface compilation/runtime, root-manifest construction, and every specification's nested `EquilibriumPhysics` package (or core-authorized library backend entry) are checked separately. For an ordinary nested package, `CompleteStellarEquilibriumPhysicsProvider` proves that every compiler-enumerated residual row and derivative has an exact provider. Each call receives one read-only source and one additive-only row; the current isobaric row-replacement mask is applied by that row. Passing these checks makes one `PreparedVariadicStellarEquilibriumOperator<Model>`; it does not select a hand-written class for the particular constraint pack.
4. **Normalization:** `NormalizableStellarEquilibriumProblem<Problem>` means the prescription stored in the discretization has a valid `prepareNormalization(problem)` operation. `Unnormalized` is the identity path and therefore remains available to a lean alternate core. A Physical Riesz discretization additionally requires `PhysicalRieszStellarEquilibriumCore<Problem::PhysicalCoreType>`, a structural contract for the density, enthalpy, gravity-gradient, gravity-potential, and surface-deformation maps used to assemble the physical Gram diagonals. It also requires complete per-specification physical-scale contributions. A new core can opt in by exposing that physical interface; it does not inherit from or name the Polytrope core.
5. **Material and stellar-structure preconditioner:** `MaterialSurfacePreconditionerProblem<Problem>` checks that the EOS-specific material/surface backend is registered for the problem's exact selected core type. `StellarStructurePreconditionerProblem<Problem>` is the corresponding public capability for the default coupled material/surface-plus-gravity structure block; it currently inherits the same support boundary.
6. **Generated border and full default preconditioner:** `CompleteSpecificationBorderPhysicsProvider` proves the exact border-incident derivatives for one physics action. `CompleteSpecificationBorderActionsFor<Problem>` folds that requirement over the model's specifications. `DefaultStellarPreconditionerAvailableFor<Problem>` is the final conjunction of a discretized equilibrium problem, complete per-specification border actions, and a supported stellar-structure block. `makePreconditioner(problem)` is available only at this rung.
No rung is specialized for a complete EOS/surface/constraint combination. The
model type determines the packs, the compiler folds their declarations, and
each numerical layer folds the corresponding per-component hooks. If a query
fails, implement the missing hook for the individual EOS, surface, integral, or
phase condition; do not weaken the query and do not add a model-pack
specialization.
### Adding another normalization family
A normalization prescription is a compile-time discretization choice, not a
model constraint. Adding one does not add cases to `discretize`, the stellar
operator, or the solver-facing adapter. Its public
`NormalizationCompilation<Prescription, Form>` specialization owns both the
complete coordinate plan and the early runtime-compatibility declaration for
the selected physical core and specification pack. The library independently
validates that the advertised plan covers every inferred value and residual
block exactly once.
Most new policies use only the public problem/manifest interface when they
prepare their diagonal factors. They can reuse
`RuntimePreparedNormalizationCompilation` rather than spelling core or
specification concepts:
```cpp
namespace my_normalization {
class UniformDiagonal final
: public mean_field::normalization::NormalizationPrescriptionTag {
public:
UniformDiagonal(double stateFactor, double residualFactor);
[[nodiscard]] double stateFactor() const noexcept;
[[nodiscard]] double residualFactor() const noexcept;
};
} // namespace my_normalization
namespace mean_field::normalization {
template <typename Form>
requires mean_field::utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<my_normalization::UniformDiagonal, Form>
: RuntimePreparedNormalizationCompilation<
my_normalization::UniformDiagonal,
Form> { };
} // namespace mean_field::normalization
```
The policy's namespace then provides
`prepareStellarNormalization(const UniformDiagonal&, const Problem&) ->
DiagonalNormalization`. It is found from the exact policy argument carried by
`Problem::NormalizationPrescriptionType`; no central registry or built-in-policy
switch is edited. The generated plan labels every block with
`RuntimePreparedCoordinate<UniformDiagonal>` rather than falsely claiming an
identity or Physical Riesz formula, and the returned diagonal is dimension-checked
against the complete inferred problem. `discretize` rejects a policy
whose compilation omits `runtimeAvailableFor`, and
also rejects a third-party runtime policy whose plan does not name that exact
policy on every coordinate. Thus merely providing a preparation function cannot
bypass or contradict the compile-time plan. The same `discretize` boundary
rejects a policy whose preparation operation is absent or returns the wrong
type, before a solver-facing problem is constructed. A policy that requires private facilities
of a particular physical core writes its own `runtimeAvailableFor<Core,
Specifications>` predicate in the same compilation specialization; Physical
Riesz is the reference example. Ordinary EOS, surface, and constraint authors
do not use this extension point—their dimensional scalar wrappers continue to
generate the appropriate per-block normalization metadata automatically.
## Radial seeds and border preconditioning
The optional numerical hooks also compose per specification. A specification that intentionally leaves a spherical seed unchanged can say so without touching the seed compiler:
```cpp
using RadialProjection = mean_field::seed::projection::Use<
mean_field::seed::projection::NoStateChange>;
```
A specification with a generated coordinate supplies a similarly wrapped physics rule. The rule receives `RadialProjectionScales`, `RadialProjectionState`, and only its own generated coordinate. It does not calculate block offsets or inspect the other constraints. `RadialProfileProjectableModel<Model>` is true only when every specification has a complete rule and exactly one rule supplies the radial mass scale. Thus a newly introduced EOS, surface condition, or constraint cannot be advertised as projectable and then fail deep inside the function body.
For an ordinary generated constraint, no second Jacobian implementation is
needed for border preconditioning. The default adapter projects the same exact
`AddJacobianAction` providers from `EquilibriumPhysics` onto the subset whose
row or source is a generated border. This is why the `FixedBaryonMass`
declaration above needs only its `EquilibriumPhysics` alias: the residual,
Jacobian, and default border preconditioner have one source of numerical
formulas.
An advanced implementation may deliberately provide a different border
approximation. In that case it opts in explicitly:
```cpp
using SpecificationBorderPhysics =
mean_field::preconditioning::LocalSpecificationBorderPhysics<MyPreparedBorderAction>;
```
The action uses the same astronomy-facing `stellar::Derivative<Equation, State>` vocabulary as the equilibrium Jacobian. For example, a phase condition declared with `Reads<SpecificEnthalpy>` and `Changes<HydrostaticBalance>` has two border-incident derivatives:
```cpp
class MyPreparedBorderAction {
public:
explicit MyPreparedBorderAction(const PreparedMyPhaseCondition &physics);
template <typename Direction, typename Row>
[[nodiscard]] auto ApplyJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::OwnConstraint,
mean_field::stellar::state::SpecificEnthalpy>,
const Direction &direction,
Row &row
) const {
return row.add(
evaluatePhaseDerivative(direction.specificEnthalpy()));
}
template <typename Direction, typename Row>
[[nodiscard]] auto ApplyJacobianAction(
mean_field::stellar::Derivative<
mean_field::stellar::equation::HydrostaticBalance,
mean_field::stellar::state::OwnGeneratedCoordinate>,
const Direction &direction,
Row &row
) const {
return row.add(
evaluateMultiplierAction(direction.generatedCoordinate()(0)));
}
};
```
The framework derives the full derivative list from `Reads` and `Changes`, then partitions it into structure-to-border, border-to-structure, and border-to-border edges. It invokes only the border-incident edges. Cross-constraint edges are partitioned in exactly the same way, including edges between two generated borders.
A purely structure-to-structure derivative is checked separately. The default
structure preconditioner accepts it only when the selected physical core names
that exact `(specification, coupling)` as a backend capability or the nested
equilibrium provider returns `stellar::StructuralZero`. A custom nonzero term
on even an otherwise familiar base edge therefore makes
`DefaultStellarPreconditionerAvailableFor<Problem>` false instead of being
silently dropped.
Each direction contains exactly one source and each row can update exactly one destination. For this example, asking the first direction for `density()` or the second row to write a gravity-potential equation is ill-formed. An exact-zero border derivative returns `stellar::zeroDerivative`. Missing an incident overload makes `CompleteSpecificationBorderPhysicsProvider<Action, Specification, Problem>` false at compile time; `PreparedSpecificationBorderPhysicsActionFor` additionally checks construction and move requirements. The single-use `row.add(...)` token has the same duplicate and result-consistency checks as equilibrium assembly.
For an explicit approximation, the action constructor receives only the
prepared equilibrium physics for its own specification. It can snapshot
state-dependent coefficients without access to the enclosing problem, physical
core, or other constraints. `LocalSpecificationBorderPhysics<Action>` is the
common wrapper for a concrete local class. `SpecificationBorderPhysics<ActionTemplate>`
is available when the action genuinely needs the inferred `Problem` type; both
names use the same adapter and neither reimplements pack traversal.
The library handles problem binding, operation partitioning, offsets, alias synchronization, surface-row ownership, and aggregation. The preconditioner lifecycle includes the prepared-operator generation, so changing state-dependent contribution data invalidates and rebuilds the border Schur complement even when the caller reuses the same dependency stamps.
These contracts establish structural completeness and safe composition; they do not establish preconditioner quality. The current default structure solve and generated-border Schur approximation are provisional numerical choices. A problem satisfying `DefaultStellarPreconditionerAvailableFor` has every required action and a registered backend, but it may still be poorly conditioned or ineffective for a new regime. Iteration counts, scaled spectra, parameter continuation behavior, and nonlinear convergence must be measured separately for each important physical family.
## What works end to end today
As of the current checkpoint:
- arbitrary well-formed self-described specifications can form distinct canonical `StellarModel` types, subject to the model-pack role and balance contracts;
- generated scalar blocks, residual rows, and Jacobian edges are inferred from `stellar::Reads` and `stellar::Changes`, including named edges between constraints;
- the residual/Jacobian runtime and generated-border preconditioner enumerate those inferred rows and edges and require exact providers or explicit structural zeros;
- generated Physical Riesz declarations and generated manifest metadata are projected generically, while Physical Riesz field diagonals are available to any selected core satisfying `PhysicalRieszStellarEquilibriumCore`;
- the variadic outer layers currently wrap a fixed five-field barotropic physical core: density, surface deformation, gravity gradient, gravity potential, and specific enthalpy;
- each generated constraint currently adds exactly one scalar value block and one scalar residual block around that core;
- the executable equilibrium kernels are registered for `eos::Polytrope` with `surface::Isobaric`;
- the default material/surface and stellar-structure preconditioner kernels are likewise registered for that Polytrope/Isobaric backend and its exact physical core;
- per-specification runtime and prepared border actions exist for `FixedTotalMass`, `FixedAngularMomentum`, and `FixedCentralDensity`; and
- `makePreconditioner(problem)` is exposed only when `DefaultStellarPreconditionerAvailableFor<Problem>` proves both the structure backend and every generated border action are present.
The five-field statement is a real boundary, not merely a missing convenience alias. A scalar constraint over those existing fields can compose without changing the core. A genuinely new distributed field—magnetic induction, temperature, composition, or another PDE state—requires extending the shared physical-field vocabulary and mapping, the selected executable core and discretization, its residual/Jacobian implementation, normalization metadata, manifest, and the applicable structure preconditioner. Supplying a backend block type in `Reads` does not by itself make that field part of the physical core. This work is still per new field/equation family, not per complete combination of constraints.
The nonbarotropic EOS and isothermal-surface mocks in the compile-contract tests are symbolic coverage. They prove that EOS choice, surface choice, and constraint packs produce distinct canonical types and correctly inferred block/Jacobian structures; they are not executable stellar models and do not claim a compatible five-field core or default preconditioner. Their rejection by `equilibrium::StellarEquilibriumModel` and `discretize` is intentional.
A new EOS or surface can therefore be described and type-checked today, but `discretize` will reject it until its per-EOS core or per-surface compilation/runtime support (including carrier/manifest handling) is registered. A new integral or phase condition can obtain its scalar block/Jacobian pair, normalization, and ordinary `target()` manifest structure from `ModelDefinition`, but still needs its residual/Jacobian runtime. The ordinary constraint places that concrete implementation on itself as `EquilibriumPhysics = operators::LocalSpecificationEquilibriumPhysics<MyPreparedPhysics>`; its exact providers are reused automatically by the default generated-border preconditioner. Only a deliberately distinct border approximation adds `SpecificationBorderPhysics`. Neither path specializes a combination-level registry.
The aggregate `StellarEquilibriumRuntimeContribution` registry is not an alternative astronomy-facing API. It can access the complete model, FEM objects, mapper, dependency stamps, and physical core, so an entry is selected only when the exact specification also appears in the selected concrete core type's `BackendSpecifications` list. A raw specialization—or a custom EOS that merely reuses the built-in core type—cannot add itself to that core's list and is therefore ignored. A custom core author may authorize its own backend entries in that single list; if an authorized entry and a nested `EquilibriumPhysics` package both exist for the same specification, the model is rejected as genuinely ambiguous. Ordinary EOS, surface, and constraint authors should use the restricted nested packages and never manage this list.
An alternate executable core may choose the identity `Unnormalized` path without implementing Physical Riesz maps. Selecting `PhysicalRieszDiagonal` for that same core fails `PhysicalRieszStellarEquilibriumProblem` and `NormalizableStellarEquilibriumProblem` cleanly. Likewise, an executable problem whose EOS/core pair has no material-surface backend fails `MaterialSurfacePreconditionerProblem`, while a problem with one missing constraint action fails `CompleteSpecificationBorderActionsFor`; neither failure requires nor invites a combination-specific fallback. These are availability and structural-correctness claims, not evidence that the current preconditioner will deliver acceptable Krylov or nonlinear convergence.
Those remaining hooks are per physics component. Adding `FixedMagneticEnergy` over existing state fields should require work for `FixedMagneticEnergy` itself. Adding a genuinely new magnetic field/equation additionally requires the shared field-family work listed above—not new implementations for `Polytrope + Isobaric + FixedTotalMass + FixedMagneticEnergy`, then another for every other constraint combination. A converged rotating solution and its nonlinear solver remain downstream goals; the compile-time architecture and provisional preconditioner do not, by themselves, establish that convergence result.

View 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.

View 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;
}

View 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.

View 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

View 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 &center = 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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
module;
#include <algorithm>
#include <array>
#include <limits>
#include <cmath>
#include <functional>
#include <map>
@@ -11,12 +13,54 @@ module;
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
import :fem.reference_tables;
namespace mean_field::fem {
namespace {
// Three one-dimensional contractions, with x fastest throughout.
// Input/output must not alias either scratch array.
void tensor_values(
int dx, int dy, int dz, int q, const double *bx, const double *by, const double *bz,
const double *input, double *output, double *sx, double *sxy, bool transpose
) {
if (!transpose) {
for (int k = 0; k < dz; ++k) for (int j = 0; j < dy; ++j) for (int x = 0; x < q; ++x) {
double v = 0.0;
for (int i = 0; i < dx; ++i) v += bx[x + q*i] * input[i + dx*(j + dy*k)];
sx[x + q*(j + dy*k)] = v;
}
for (int k = 0; k < dz; ++k) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
double v = 0.0;
for (int j = 0; j < dy; ++j) v += by[y + q*j] * sx[x + q*(j + dy*k)];
sxy[x + q*(y + q*k)] = v;
}
for (int z = 0; z < q; ++z) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
double v = 0.0;
for (int k = 0; k < dz; ++k) v += bz[z + q*k] * sxy[x + q*(y + q*k)];
output[x + q*(y + q*z)] = v;
}
} else {
for (int k = 0; k < dz; ++k) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
double v = 0.0;
for (int z = 0; z < q; ++z) v += bz[z + q*k] * input[x + q*(y + q*z)];
sxy[x + q*(y + q*k)] = v;
}
for (int k = 0; k < dz; ++k) for (int j = 0; j < dy; ++j) for (int x = 0; x < q; ++x) {
double v = 0.0;
for (int y = 0; y < q; ++y) v += by[y + q*j] * sxy[x + q*(y + q*k)];
sx[x + q*(j + dy*k)] = v;
}
for (int k = 0; k < dz; ++k) for (int j = 0; j < dy; ++j) for (int i = 0; i < dx; ++i) {
double v = 0.0;
for (int x = 0; x < q; ++x) v += bx[x + q*i] * sx[x + q*(j + dy*k)];
output[i + dx*(j + dy*k)] = v;
}
}
}
struct ReferenceTableKey {
const mfem::FiniteElement *element;
std::vector<std::array<double, 4>> points;
@@ -112,6 +156,7 @@ namespace mean_field::fem {
element.CalcDShape(point, gradient);
}
}
PrepareTensorEvaluation3D(element, rule);
}
const mfem::DenseMatrix &ScalarReferenceTable::GetValues() const {
@@ -130,6 +175,207 @@ namespace mean_field::fem {
return m_dimension;
}
void ScalarReferenceTable::PrepareTensorEvaluation3D(
const mfem::FiniteElement &element, const mfem::IntegrationRule &rule
) {
// Nodal H1 and L2 hexes share the same verified tensor contract.
// Other elements and non-Cartesian rules keep the existing dense path.
const auto *hex = dynamic_cast<const mfem::NodalTensorFiniteElement *>(&element);
if (dynamic_cast<const mfem::H1_HexahedronElement *>(&element) == nullptr &&
dynamic_cast<const mfem::L2_HexahedronElement *>(&element) == nullptr) return;
if (hex == nullptr || element.GetMapType() != mfem::FiniteElement::VALUE ||
element.GetDerivType() != mfem::FiniteElement::GRAD || rule.GetNPoints() == 0) return;
const int d = element.GetOrder() + 1;
const int q = static_cast<int>(std::lround(std::cbrt(rule.GetNPoints())));
if (d * d * d != element.GetDof() || q * q * q != rule.GetNPoints()) return;
for (int z = 0; z < q; ++z) {
for (int y = 0; y < q; ++y) {
for (int x = 0; x < q; ++x) {
const auto &point = rule.IntPoint(x + q * (y + q * z));
if (point.x != rule.IntPoint(x).x ||
point.y != rule.IntPoint(q * y).y ||
point.z != rule.IntPoint(q * q * z).z) return;
}
}
}
const auto &map = hex->GetDofMap();
if (map.Size() != 0 && map.Size() != element.GetDof()) return;
m_tensorDofMap.SetSize(element.GetDof());
std::vector<bool> seen(element.GetDof(), false);
for (int i = 0; i < element.GetDof(); ++i) {
const int native = map.Size() == 0 ? i : map[i];
if (native < 0 || native >= element.GetDof() || seen[native]) return;
seen[native] = true;
m_tensorDofMap[i] = native;
}
mfem::Vector b(d), g(d);
for (int axis = 0; axis < 3; ++axis) {
m_tensorValues[axis].SetSize(q, d);
m_tensorGradients[axis].SetSize(q, d);
for (int i = 0; i < q; ++i) {
const double coordinate = axis == 0 ? rule.IntPoint(i).x :
axis == 1 ? rule.IntPoint(q * i).y : rule.IntPoint(q * q * i).z;
hex->GetBasis1D().Eval(coordinate, b, g);
for (int j = 0; j < d; ++j) {
m_tensorValues[axis](i, j) = b(j);
m_tensorGradients[axis](i, j) = g(j);
}
}
}
// Verify the native DOF map and factorization once per shared table.
// This also guards against an unexpected MFEM basis convention.
const auto agrees = [](double actual, double expected) {
return std::isfinite(actual) && std::abs(actual - expected) <=
64.0 * std::numeric_limits<double>::epsilon() * std::max(1.0, std::abs(expected));
};
for (int z = 0; z < q; ++z) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
const int point = x + q * (y + q * z);
for (int k = 0; k < d; ++k) for (int j = 0; j < d; ++j) for (int i = 0; i < d; ++i) {
const int native = m_tensorDofMap[i + d * (j + d * k)];
const double bx = m_tensorValues[0](x, i), by = m_tensorValues[1](y, j),
bz = m_tensorValues[2](z, k);
if (!agrees(bx * by * bz, m_values(point, native)) ||
!agrees(m_tensorGradients[0](x, i) * by * bz, m_gradients[point](native, 0)) ||
!agrees(bx * m_tensorGradients[1](y, j) * bz, m_gradients[point](native, 1)) ||
!agrees(bx * by * m_tensorGradients[2](z, k), m_gradients[point](native, 2))) return;
}
}
m_tensorDofsPerAxis = d;
m_tensorPointsPerAxis = q;
}
bool ScalarReferenceTable::HasTensorEvaluation3D() const noexcept {
return m_tensorPointsPerAxis != 0;
}
void ScalarReferenceTable::EvaluateTensor3D(
const mfem::DenseMatrix &elementDofs, mfem::DenseMatrix &valuesAndGradients,
mfem::Vector &workspace
) const {
MFEM_VERIFY(HasTensorEvaluation3D() && elementDofs.Height() == GetDofCount(),
"Tensor evaluation requires a compatible verified hexahedral reference table.");
const int d = m_tensorDofsPerAxis, q = m_tensorPointsPerAxis;
const int nx = q * d * d, nxy = q * q * d, nq = q * q * q;
workspace.SetSize(2 * nx + 3 * nxy);
double *xValue = workspace.HostWrite(), *xGradient = xValue + nx;
double *xyValue = xGradient + nx, *xyDx = xyValue + nxy, *xyDy = xyDx + nxy;
valuesAndGradients.SetSize(nq, 4 * elementDofs.Width());
const double *u = elementDofs.Data();
double *out = valuesAndGradients.Data();
const double *bx = m_tensorValues[0].Data(), *by = m_tensorValues[1].Data(),
*bz = m_tensorValues[2].Data(), *gx = m_tensorGradients[0].Data(),
*gy = m_tensorGradients[1].Data(), *gz = m_tensorGradients[2].Data();
const int ndof = GetDofCount();
for (int c = 0; c < elementDofs.Width(); ++c) {
// Contract x, retaining the value and x derivative.
for (int k = 0; k < d; ++k) for (int j = 0; j < d; ++j) for (int x = 0; x < q; ++x) {
double value = 0.0, dx = 0.0;
for (int i = 0; i < d; ++i) {
const double coefficient = u[m_tensorDofMap[i + d * (j + d * k)] + c * ndof];
value += bx[x + q * i] * coefficient;
dx += gx[x + q * i] * coefficient;
}
const int index = x + q * (j + d * k);
xValue[index] = value; xGradient[index] = dx;
}
// Contract y; propagate dx and form dy.
for (int k = 0; k < d; ++k) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
double value = 0.0, dx = 0.0, dy = 0.0;
for (int j = 0; j < d; ++j) {
const int index = x + q * (j + d * k);
value += by[y + q * j] * xValue[index];
dx += by[y + q * j] * xGradient[index];
dy += gy[y + q * j] * xValue[index];
}
const int index = x + q * (y + q * k);
xyValue[index] = value; xyDx[index] = dx; xyDy[index] = dy;
}
// Contract z and write all four fields in quadrature ordering.
for (int z = 0; z < q; ++z) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
double value = 0.0, dx = 0.0, dy = 0.0, dz = 0.0;
for (int k = 0; k < d; ++k) {
const int index = x + q * (y + q * k);
value += bz[z + q * k] * xyValue[index];
dx += bz[z + q * k] * xyDx[index];
dy += bz[z + q * k] * xyDy[index];
dz += gz[z + q * k] * xyValue[index];
}
const int point = x + q * (y + q * z);
out[point + nq * (4 * c)] = value;
out[point + nq * (4 * c + 1)] = dx;
out[point + nq * (4 * c + 2)] = dy;
out[point + nq * (4 * c + 3)] = dz;
}
}
}
void ScalarReferenceTable::ApplyGradientTransposeTensor3D(
const mfem::DenseMatrix &quadratureDuals, mfem::DenseMatrix &elementDuals,
mfem::Vector &workspace
) const {
MFEM_VERIFY(HasTensorEvaluation3D() && quadratureDuals.Height() == GetPointCount() &&
quadratureDuals.Width() % 3 == 0, "Tensor gradient transpose has incompatible duals.");
const int d = m_tensorDofsPerAxis, q = m_tensorPointsPerAxis, n = GetDofCount();
workspace.SetSize(n + q*d*d + q*q*d);
double *lex = workspace.HostWrite(), *sx = lex + n, *sxy = sx + q*d*d;
elementDuals.SetSize(n, quadratureDuals.Width()/3);
elementDuals = 0.0;
for (int c = 0; c < elementDuals.Width(); ++c) for (int a = 0; a < 3; ++a) {
tensor_values(d, d, d, q,
(a == 0 ? m_tensorGradients[0] : m_tensorValues[0]).Data(),
(a == 1 ? m_tensorGradients[1] : m_tensorValues[1]).Data(),
(a == 2 ? m_tensorGradients[2] : m_tensorValues[2]).Data(),
quadratureDuals.GetColumn(3*c+a), lex, sx, sxy, true);
for (int i = 0; i < n; ++i) elementDuals(m_tensorDofMap[i], c) += lex[i];
}
}
void ScalarReferenceTable::EvaluateValuesTensor3D(
const mfem::Vector &elementDofs, mfem::Vector &values, mfem::Vector &workspace
) const {
MFEM_VERIFY(HasTensorEvaluation3D() && elementDofs.Size() == GetDofCount(),
"Tensor values require compatible element DOFs.");
const int d = m_tensorDofsPerAxis, q = m_tensorPointsPerAxis, n = GetDofCount();
workspace.SetSize(n + q*d*d + q*q*d);
double *lex = workspace.HostWrite(), *sx = lex + n, *sxy = sx + q*d*d;
const double *u = elementDofs.HostRead();
for (int i = 0; i < n; ++i) lex[i] = u[m_tensorDofMap[i]];
values.SetSize(GetPointCount());
tensor_values(d, d, d, q, m_tensorValues[0].Data(), m_tensorValues[1].Data(),
m_tensorValues[2].Data(), lex, values.HostWrite(), sx, sxy, false);
}
void ScalarReferenceTable::ApplyTransposeTensor3D(
const mfem::Vector &quadratureDuals, mfem::Vector &elementDuals, mfem::Vector &workspace
) const {
MFEM_VERIFY(HasTensorEvaluation3D() && quadratureDuals.Size() == GetPointCount(),
"Tensor transpose requires compatible quadrature duals.");
const int d = m_tensorDofsPerAxis, q = m_tensorPointsPerAxis, n = GetDofCount();
workspace.SetSize(n + q*d*d + q*q*d);
double *lex = workspace.HostWrite(), *sx = lex + n, *sxy = sx + q*d*d;
tensor_values(d, d, d, q, m_tensorValues[0].Data(), m_tensorValues[1].Data(),
m_tensorValues[2].Data(), quadratureDuals.HostRead(), lex, sx, sxy, true);
elementDuals.SetSize(n);
double *out = elementDuals.HostWrite();
for (int i = 0; i < n; ++i) out[m_tensorDofMap[i]] = lex[i];
}
void ScalarReferenceTable::ApplyTransposeTensor3D(
const mfem::DenseMatrix &quadratureDuals, mfem::DenseMatrix &elementDuals, mfem::Vector &workspace
) const {
MFEM_VERIFY(HasTensorEvaluation3D() && quadratureDuals.Height() == GetPointCount(),
"Tensor transpose requires compatible quadrature duals.");
const int d = m_tensorDofsPerAxis, q = m_tensorPointsPerAxis, n = GetDofCount();
workspace.SetSize(n + q*d*d + q*q*d);
double *lex = workspace.HostWrite(), *sx = lex + n, *sxy = sx + q*d*d;
elementDuals.SetSize(n, quadratureDuals.Width());
for (int c = 0; c < quadratureDuals.Width(); ++c) {
tensor_values(d, d, d, q, m_tensorValues[0].Data(), m_tensorValues[1].Data(),
m_tensorValues[2].Data(), quadratureDuals.GetColumn(c), lex, sx, sxy, true);
for (int i = 0; i < n; ++i) elementDuals(m_tensorDofMap[i], c) = lex[i];
}
}
VectorReferenceTable::VectorReferenceTable(
const mfem::FiniteElement &element,
const mfem::IntegrationRule &rule
@@ -142,6 +388,122 @@ namespace mean_field::fem {
values.SetSize(m_dof_count, m_dimension);
element.CalcVShape(rule.IntPoint(q), values);
}
PrepareTensorEvaluation3D(element, rule);
}
void VectorReferenceTable::PrepareTensorEvaluation3D(
const mfem::FiniteElement &element, const mfem::IntegrationRule &rule
) {
const auto *rt = dynamic_cast<const mfem::RT_HexahedronElement *>(&element);
if (rt == nullptr || element.GetMapType() != mfem::FiniteElement::H_DIV || rule.GetNPoints() == 0) return;
const int q = static_cast<int>(std::lround(std::cbrt(rule.GetNPoints())));
if (q*q*q != rule.GetNPoints()) return;
for (int z = 0; z < q; ++z) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
const auto &point = rule.IntPoint(x + q*(y + q*z));
if (point.x != rule.IntPoint(x).x || point.y != rule.IntPoint(q*y).y ||
point.z != rule.IntPoint(q*q*z).z) return;
}
int d = 2;
while (3*d*(d-1)*(d-1) < m_dof_count) ++d;
if (3*d*(d-1)*(d-1) != m_dof_count) return;
const auto &map = rt->GetDofMap();
if (map.Size() != m_dof_count) return;
m_tensorDofMap = map;
std::vector<bool> seen(m_dof_count, false);
for (int i = 0; i < m_dof_count; ++i) {
const int native = map[i] < 0 ? -1-map[i] : map[i];
if (native < 0 || native >= m_dof_count || seen[native]) return;
seen[native] = true;
}
const int block = m_dof_count / 3;
const auto basis = [&](int point, int lex, int component) {
const int encoded = map[lex], native = encoded < 0 ? -1-encoded : encoded;
return (encoded < 0 ? -1.0 : 1.0) * m_values[point](native, component);
};
// Extract owned 1D factors from the full basis. This supports nodal and
// integrated open bases without retaining DofToQuad pointers to rules.
for (int c = 0; c < 3; ++c) {
const int dims[3]{c == 0 ? d : d-1, c == 1 ? d : d-1, c == 2 ? d : d-1};
int anchor = 0;
for (int point = 1; point < q*q*q; ++point)
if (std::abs(basis(point, c*block, c)) > std::abs(basis(anchor, c*block, c))) anchor = point;
const double base = basis(anchor, c*block, c);
if (!std::isfinite(base) || base == 0.0) return;
const int coordinates[3]{anchor % q, (anchor/q) % q, anchor/(q*q)};
const int strides[3]{1, dims[0], dims[0]*dims[1]};
for (int axis = 0; axis < 3; ++axis) {
auto &b = m_tensorValues[c][axis];
b.SetSize(q, dims[axis]);
for (int i = 0; i < dims[axis]; ++i) for (int t = 0; t < q; ++t) {
int xyz[3]{coordinates[0], coordinates[1], coordinates[2]};
xyz[axis] = t;
b(t, i) = basis(xyz[0] + q*(xyz[1] + q*xyz[2]), c*block + strides[axis]*i, c) /
(axis == 0 ? 1.0 : base);
}
}
// Check every native basis entry, including zero components.
for (int k = 0; k < dims[2]; ++k) for (int j = 0; j < dims[1]; ++j)
for (int i = 0; i < dims[0]; ++i) {
const int lex = c*block + i + dims[0]*(j + dims[1]*k);
for (int z = 0; z < q; ++z) for (int y = 0; y < q; ++y) for (int x = 0; x < q; ++x) {
const int point = x + q*(y + q*z);
const double product = m_tensorValues[c][0](x,i) * m_tensorValues[c][1](y,j) *
m_tensorValues[c][2](z,k);
for (int component = 0; component < 3; ++component) {
const double expected = basis(point, lex, component);
const double actual = component == c ? product : 0.0;
if (!std::isfinite(actual) || std::abs(actual - expected) >
128.0 * std::numeric_limits<double>::epsilon() * std::max(1.0, std::abs(expected))) return;
}
}
}
}
m_tensorClosedDofs = d;
m_tensorPointsPerAxis = q;
}
bool VectorReferenceTable::HasTensorEvaluation3D() const noexcept {
return m_tensorPointsPerAxis != 0;
}
void VectorReferenceTable::EvaluateTensor3D(
const mfem::Vector &elementDofs, mfem::DenseMatrix &values, mfem::Vector &workspace
) const {
MFEM_VERIFY(HasTensorEvaluation3D() && elementDofs.Size() == m_dof_count,
"RT tensor evaluation requires compatible element DOFs.");
const int d = m_tensorClosedDofs, q = m_tensorPointsPerAxis, block = m_dof_count/3;
workspace.SetSize(block + q*d*d + q*q*d);
double *lex = workspace.HostWrite(), *sx = lex + block, *sxy = sx + q*d*d;
values.SetSize(q*q*q, 3);
for (int c = 0; c < 3; ++c) {
for (int i = 0; i < block; ++i) {
const int encoded = m_tensorDofMap[c*block+i];
lex[i] = encoded < 0 ? -elementDofs(-1-encoded) : elementDofs(encoded);
}
tensor_values(c == 0 ? d : d-1, c == 1 ? d : d-1, c == 2 ? d : d-1, q,
m_tensorValues[c][0].Data(), m_tensorValues[c][1].Data(), m_tensorValues[c][2].Data(),
lex, values.GetColumn(c), sx, sxy, false);
}
}
void VectorReferenceTable::ApplyTransposeTensor3D(
const mfem::DenseMatrix &quadratureDuals, mfem::Vector &elementDuals, mfem::Vector &workspace
) const {
MFEM_VERIFY(HasTensorEvaluation3D() && quadratureDuals.Height() == GetPointCount() && quadratureDuals.Width() == 3,
"RT tensor transpose requires three quadrature dual components.");
const int d = m_tensorClosedDofs, q = m_tensorPointsPerAxis, block = m_dof_count/3;
workspace.SetSize(block + q*d*d + q*q*d);
double *lex = workspace.HostWrite(), *sx = lex + block, *sxy = sx + q*d*d;
elementDuals.SetSize(m_dof_count);
for (int c = 0; c < 3; ++c) {
tensor_values(c == 0 ? d : d-1, c == 1 ? d : d-1, c == 2 ? d : d-1, q,
m_tensorValues[c][0].Data(), m_tensorValues[c][1].Data(), m_tensorValues[c][2].Data(),
quadratureDuals.GetColumn(c), lex, sx, sxy, true);
for (int i = 0; i < block; ++i) {
const int encoded = m_tensorDofMap[c*block+i];
if (encoded < 0) elementDuals(-1-encoded) = -lex[i]; else elementDuals(encoded) = lex[i];
}
}
}
const mfem::DenseMatrix &VectorReferenceTable::GetValues(const int point) const {
@@ -156,4 +518,132 @@ namespace mean_field::fem {
int VectorReferenceTable::GetDimension() const {
return m_dimension;
}
struct PreparedGeometryInspector::Storage {
struct Element {
std::shared_ptr<const ScalarReferenceTable> basis;
mfem::DenseMatrix inverseJacobians; // rows: points; columns: row-major entries
};
const mfem::ParFiniteElementSpace *space{nullptr};
const mfem::Mesh *mesh{nullptr};
long spaceSequence{-1}, meshSequence{-1}, nodesSequence{-1};
std::unique_ptr<ReferenceTableCache> tables;
std::vector<Element> elements;
std::unique_ptr<mfem::ParGridFunction> field;
mfem::Array<int> dofs;
mfem::Vector elementDofs, workspace;
mfem::DenseMatrix dofMatrix, samples, gradient;
std::uint64_t builds{0};
void Clear() {
elements.clear();
tables.reset();
field.reset();
space = nullptr;
mesh = nullptr;
}
};
PreparedGeometryInspector::PreparedGeometryInspector() : m_storage(std::make_unique<Storage>()) {}
PreparedGeometryInspector::~PreparedGeometryInspector() = default;
PreparedGeometryInspector::PreparedGeometryInspector(PreparedGeometryInspector &&) noexcept = default;
PreparedGeometryInspector &PreparedGeometryInspector::operator=(PreparedGeometryInspector &&) noexcept = default;
void PreparedGeometryInspector::ClearCache() const { m_storage->Clear(); }
int PreparedGeometryInspector::GetTensorElementCount() const noexcept {
int count = 0;
for (const auto &element : m_storage->elements)
count += element.basis && element.basis->HasTensorEvaluation3D();
return count;
}
std::uint64_t PreparedGeometryInspector::GetGeometryBuildCount() const noexcept { return m_storage->builds; }
double PreparedGeometryInspector::Inspect(
mfem::ParFiniteElementSpace &space, const mfem::Vector &displacement, bool reference
) const {
MFEM_VERIFY(displacement.Size() == space.GetTrueVSize(), "Geometry inspection received incompatible DOFs.");
auto &s = *m_storage;
auto *mesh = space.GetMesh();
const int dimension = mesh->SpaceDimension();
MFEM_VERIFY(space.GetVDim() == dimension, "Geometry inspection requires a spatial displacement vector.");
if (s.space != &space || s.mesh != mesh || s.spaceSequence != space.GetSequence() ||
s.meshSequence != mesh->GetSequence() || s.nodesSequence != mesh->GetNodesSequence()) {
s.Clear();
s.space = &space; s.mesh = mesh;
s.spaceSequence = space.GetSequence(); s.meshSequence = mesh->GetSequence();
s.nodesSequence = mesh->GetNodesSequence();
s.field = std::make_unique<mfem::ParGridFunction>(&space);
s.tables = std::make_unique<ReferenceTableCache>();
s.elements.resize(mesh->GetNE());
}
s.field->SetFromTrueDofs(displacement);
double minimum = std::numeric_limits<double>::infinity();
int finite = 1;
auto accumulate = [&](double determinant) {
if (!std::isfinite(determinant)) finite = 0;
else minimum = std::min(minimum, determinant);
};
for (int e = 0; e < mesh->GetNE(); ++e) {
auto &entry = s.elements[e];
auto &transformation = *mesh->GetElementTransformation(e);
const auto &element = *space.GetFE(e);
// Preserve the existing d*p geometry inspection order, including
// vacuum elements and points beyond the physics quadrature.
const int order = std::max(element.GetOrder() + 2, 2 * dimension * element.GetOrder());
const auto &rule = mfem::IntRules.Get(transformation.GetGeometryType(), order);
if (!reference && dimension == 3 && element.GetDim() == 3 &&
element.GetMapType() == mfem::FiniteElement::VALUE &&
dynamic_cast<const mfem::H1_HexahedronElement *>(&element)) {
if (!entry.basis) {
entry.basis = s.tables->GetScalarTable(element, rule);
if (entry.basis->HasTensorEvaluation3D()) {
entry.inverseJacobians.SetSize(rule.GetNPoints(), 9);
for (int q = 0; q < rule.GetNPoints(); ++q) {
transformation.SetIntPoint(&rule.IntPoint(q));
const auto &inverse = transformation.InverseJacobian();
for (int a = 0; a < 3; ++a) for (int b = 0; b < 3; ++b)
entry.inverseJacobians(q, a*3+b) = inverse(a,b);
}
++s.builds;
}
}
}
if (!reference && entry.basis && entry.basis->HasTensorEvaluation3D()) {
auto *dofTransformation = space.GetElementVDofs(e, s.dofs);
s.field->GetSubVector(s.dofs, s.elementDofs);
if (dofTransformation) dofTransformation->InvTransformPrimal(s.elementDofs);
const int nd = element.GetDof();
s.dofMatrix.SetSize(nd, 3);
// GetElementVDofs groups local entries by component for either
// global ordering, as used by GridFunction::GetVectorGradientHat.
for (int c = 0; c < 3; ++c) for (int i = 0; i < nd; ++i)
s.dofMatrix(i,c) = s.elementDofs(i+c*nd);
entry.basis->EvaluateTensor3D(s.dofMatrix, s.samples, s.workspace);
s.gradient.SetSize(3);
for (int q = 0; q < rule.GetNPoints(); ++q) {
for (int c = 0; c < 3; ++c) for (int b = 0; b < 3; ++b) {
double value = c == b ? 1.0 : 0.0;
for (int a = 0; a < 3; ++a)
value += s.samples(q, 4*c+a+1) * entry.inverseJacobians(q, a*3+b);
s.gradient(c,b) = value;
}
accumulate(s.gradient.Det());
}
} else {
for (int q = 0; q < rule.GetNPoints(); ++q) {
transformation.SetIntPoint(&rule.IntPoint(q));
s.field->GetVectorGradient(transformation, s.gradient);
for (int c = 0; c < dimension; ++c) s.gradient(c,c) += 1.0;
accumulate(s.gradient.Det());
}
}
}
double globalMinimum = 0.0;
int globalFinite = 0;
const int minimumStatus = MPI_Allreduce(&minimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, space.GetComm());
const int finiteStatus = MPI_Allreduce(&finite, &globalFinite, 1, MPI_INT, MPI_MIN, space.GetComm());
if (minimumStatus != MPI_SUCCESS || finiteStatus != MPI_SUCCESS)
throw std::runtime_error("Could not reduce the mapped-geometry inspection.");
return globalFinite ? globalMinimum : std::numeric_limits<double>::quiet_NaN();
}
} // namespace mean_field::fem

View File

@@ -242,6 +242,23 @@ namespace mean_field::mapping::compactification {
return MappingStatus::valid;
}
MappingStatus KelvinCompactification::EvaluateJacobianVariationFactors(
const double coordinate,
const mfem::Vector &coordinateGradient,
double &scale,
mfem::Vector &scaleGradient
) const {
if (coordinateGradient.Size() <= 0) return MappingStatus::invalid_dimension;
if (!vector_is_finite(coordinateGradient)) return MappingStatus::non_finite_input;
RadialFactors factors;
const MappingStatus status = ComputeRadialFactors(coordinate, factors);
if (status != MappingStatus::valid) return status;
scale = factors.scale;
scaleGradient = coordinateGradient;
scaleGradient *= factors.scale_derivative;
return vector_is_finite(scaleGradient) ? MappingStatus::valid : MappingStatus::non_finite_result;
}
std::string_view KelvinCompactification::GetName() const noexcept {
return "KelvinCompactification";
}

View File

@@ -1,6 +1,9 @@
module;
#include <cmath>
#include <array>
#include <map>
#include <tuple>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
@@ -32,6 +35,65 @@ namespace {
} // namespace
namespace mean_field::mapping {
struct DomainMapper::GeometryCache {
bool enabled{true};
const mfem::Mesh *mesh{nullptr};
long sequence{-1}, nodesSequence{-1};
std::map<std::tuple<int, double, double, double>, FixedMeshGeometry> points;
std::uint64_t hits{0}, misses{0};
};
DomainMapper::~DomainMapper() = default;
DomainMapper::DomainMapper(DomainMapper &&) noexcept = default;
DomainMapper &DomainMapper::operator=(DomainMapper &&) noexcept = default;
void DomainMapper::SetGeometryCacheEnabled(bool enabled) const { m_geometryCache->enabled = enabled; }
bool DomainMapper::IsGeometryCacheEnabled() const noexcept { return m_geometryCache->enabled; }
void DomainMapper::ClearGeometryCache() const {
auto &cache = *m_geometryCache;
cache.points.clear();
cache.mesh = nullptr;
cache.sequence = cache.nodesSequence = -1;
cache.hits = cache.misses = 0;
}
DomainMapper::GeometryCacheStatistics DomainMapper::GetGeometryCacheStatistics() const {
const auto &cache = *m_geometryCache;
return {.hits = cache.hits, .misses = cache.misses, .points = cache.points.size()};
}
const DomainMapper::FixedMeshGeometry *DomainMapper::GetFixedMeshGeometry(
mfem::ElementTransformation &transformation, const mfem::IntegrationPoint &point
) const {
transformation.SetIntPoint(&point);
auto &cache = *m_geometryCache;
const auto *mesh = transformation.mesh;
if (!cache.enabled || mesh == nullptr || transformation.ElementType != mfem::ElementTransformation::ELEMENT ||
transformation.ElementNo < 0 || transformation.ElementNo >= mesh->GetNE() ||
transformation.GetSpaceDim() != m_options.dimension) return nullptr;
const std::array<double, 3> coordinates{
point.x, m_options.dimension > 1 ? point.y : 0.0, m_options.dimension > 2 ? point.z : 0.0};
for (double coordinate : coordinates) if (!std::isfinite(coordinate)) return nullptr;
if (cache.mesh != mesh || cache.sequence != mesh->GetSequence() || cache.nodesSequence != mesh->GetNodesSequence()) {
cache.points.clear();
cache.mesh = mesh;
cache.sequence = mesh->GetSequence();
cache.nodesSequence = mesh->GetNodesSequence();
}
const auto key = std::tuple{transformation.ElementNo, coordinates[0], coordinates[1], coordinates[2]};
const auto found = cache.points.find(key);
if (found != cache.points.end()) { ++cache.hits; return &found->second; }
FixedMeshGeometry geometry;
transformation.Transform(point, geometry.position);
geometry.jacobian = transformation.Jacobian();
geometry.inverseJacobian = transformation.InverseJacobian();
geometry.weight = transformation.Weight();
++cache.misses;
if (!vector_is_finite(geometry.position) || !matrix_is_finite(geometry.jacobian) ||
!matrix_is_finite(geometry.inverseJacobian) || !std::isfinite(geometry.weight) || geometry.weight <= 0.0)
return nullptr;
return &cache.points.emplace(key, std::move(geometry)).first->second;
}
ElementCompactificationData::ElementCompactificationData(
const mfem::FiniteElement &element,
const mfem::Vector &dofs
@@ -195,7 +257,8 @@ namespace mean_field::mapping {
const utils::DomainMapperOptions options,
std::unique_ptr<const compactification::ExteriorDomainMap> exterior_map
)
: m_options(options),
: m_geometryCache(std::make_unique<GeometryCache>()),
m_options(options),
m_exterior_map(std::move(exterior_map)) {
if (m_options.dimension <= 0)
throw std::invalid_argument("The domain-mapping dimension must be positive.");
@@ -588,6 +651,16 @@ namespace mean_field::mapping {
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
MappingPointContext &context
) const {
const auto *meshGeometry = GetFixedMeshGeometry(transformation, integration_point);
return EvaluatePointImpl(element_data, transformation, integration_point, workspace, context, meshGeometry);
}
MappingStatus DomainMapper::EvaluatePointImpl(
const ElementMappingData &element_data, mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point, Workspace &workspace,
MappingPointContext &context, const FixedMeshGeometry *meshGeometry,
const ReferenceDisplacementSample *displacementSample
) const {
ValidateElementData(element_data);
@@ -602,16 +675,28 @@ namespace mean_field::mapping {
"element data."
);
transformation.SetIntPoint(&integration_point);
context.reference_position.SetSize(m_options.dimension);
transformation.Transform(integration_point, context.reference_position);
if (meshGeometry) context.reference_position = meshGeometry->position;
else transformation.Transform(integration_point, context.reference_position);
// Get the displacement field value and its Jacobian at the integration point. Note these are in the workspace to avoid repeated allocations.
EvaluateField(
element_data.displacement, transformation, integration_point, workspace, workspace.m_field_value,
workspace.m_field_jacobian, nullptr
);
if (displacementSample) {
if (element_data.displacement.GetElement().GetMapType() != mfem::FiniteElement::VALUE ||
displacementSample->value.Size() != m_options.dimension ||
displacementSample->gradient.Height() != m_options.dimension ||
displacementSample->gradient.Width() != m_options.dimension)
return MappingStatus::invalid_dimension;
workspace.m_field_value = displacementSample->value;
workspace.m_field_jacobian.SetSize(m_options.dimension);
mfem::Mult(displacementSample->gradient,
meshGeometry ? meshGeometry->inverseJacobian : transformation.InverseJacobian(),
workspace.m_field_jacobian);
} else {
EvaluateField(
element_data.displacement, transformation, integration_point, workspace, workspace.m_field_value,
workspace.m_field_jacobian, meshGeometry ? &meshGeometry->inverseJacobian : nullptr
);
}
if (!vector_is_finite(context.reference_position) || !vector_is_finite(workspace.m_field_value) ||
!matrix_is_finite(workspace.m_field_jacobian)) {
@@ -647,7 +732,7 @@ namespace mean_field::mapping {
// the gradient of s as we move along the mesh coordinates. All of this is stashes within workspace.m_compactification_point.
const MappingStatus coordinate_status = EvaluateCompactificationCoordinate(
element_data.compactification, transformation, integration_point, workspace,
workspace.m_compactification_point, nullptr
workspace.m_compactification_point, meshGeometry ? &meshGeometry->inverseJacobian : nullptr
);
if (coordinate_status != MappingStatus::valid)
@@ -701,21 +786,25 @@ namespace mean_field::mapping {
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
VolumeMappingContext &context
VolumeMappingContext &context,
const ReferenceDisplacementSample *displacementSample
) const {
const auto *meshGeometry = GetFixedMeshGeometry(transformation, integration_point);
const MappingStatus point_status =
EvaluatePoint(element_data, transformation, integration_point, workspace, context.mapping);
EvaluatePointImpl(element_data, transformation, integration_point, workspace, context.mapping, meshGeometry, displacementSample);
if (point_status != MappingStatus::valid)
return point_status;
mfem::Mult(context.mapping.mapping_jacobian, transformation.Jacobian(), workspace.m_full_element_jacobian);
mfem::Mult(context.mapping.mapping_jacobian,
meshGeometry ? meshGeometry->jacobian : transformation.Jacobian(), workspace.m_full_element_jacobian);
context.quadrature.J_inv.SetSize(m_options.dimension, m_options.dimension);
mfem::CalcInverse(workspace.m_full_element_jacobian, context.quadrature.J_inv);
context.quadrature.detJ = context.mapping.mapping_determinant;
context.quadrature.weight =
integration_point.weight * transformation.Weight() * context.mapping.mapping_determinant;
integration_point.weight * (meshGeometry ? meshGeometry->weight : transformation.Weight()) *
context.mapping.mapping_determinant;
if (!matrix_is_finite(context.quadrature.J_inv) || !std::isfinite(context.quadrature.weight))
return MappingStatus::non_finite_result;
@@ -846,7 +935,8 @@ namespace mean_field::mapping {
);
if (inverse_mesh_jacobian == nullptr) {
transformation.SetIntPoint(&integration_point);
if (const auto *geometry = GetFixedMeshGeometry(transformation, integration_point))
inverse_mesh_jacobian = &geometry->inverseJacobian;
}
EvaluateField(

View File

@@ -0,0 +1,162 @@
#pragma once
#include <mfem.hpp>
namespace {
// U and G are n x 3, component/derivative columns respectively. Keep the
// nine reductions together instead of invoking a general GEMM per point.
// The returned 3 x 3 matrix is column-major: A(c,j) = sum_i U(i,c) G(i,j).
inline void displacement_gradient_3d(
const mfem::DenseMatrix &direction, const mfem::DenseMatrix &gradients, double *a
) {
const int n = direction.Height();
const double *u = direction.Data();
const double *g = gradients.Data();
for (int j = 0; j < 3; ++j) {
double x = 0.0, y = 0.0, z = 0.0;
const double *gj = g + j * n;
for (int i = 0; i < n; ++i) {
const double v = gj[i];
x += u[i] * v;
y += u[i + n] * v;
z += u[i + 2 * n] * v;
}
a[3 * j] = x; a[3 * j + 1] = y; a[3 * j + 2] = z;
}
}
inline void vector_basis_value_3d(
const mfem::DenseMatrix &basis, const mfem::Vector &coefficients, double *value
) {
const int n = basis.Height();
const double *b = basis.Data();
const double *v = coefficients.HostRead();
double x = 0.0, y = 0.0, z = 0.0;
for (int i = 0; i < n; ++i) {
x += b[i] * v[i]; y += b[i + n] * v[i]; z += b[i + 2 * n] * v[i];
}
value[0] = x; value[1] = y; value[2] = z;
}
// Matrix cache layout: nq x 9, with row-major matrix entries in columns.
inline void cached_matrix_vector_3d(
const mfem::DenseMatrix &cache, const int point, const double *v, double *out
) {
const int nq = cache.Height();
const double *m = cache.Data() + point;
for (int r = 0; r < 3; ++r)
out[r] = m[(3 * r) * nq] * v[0] + m[(3 * r + 1) * nq] * v[1] +
m[(3 * r + 2) * nq] * v[2];
}
inline double displacement_trace_3d(
const mfem::DenseMatrix &direction, const mfem::DenseMatrix &gradients,
const mfem::DenseMatrix &inverseJacobians, const int point
) {
double a[9];
displacement_gradient_3d(direction, gradients, a);
const double *k = inverseJacobians.Data() + point;
const int nq = inverseJacobians.Height();
double trace = 0.0;
for (int j = 0; j < 3; ++j)
for (int c = 0; c < 3; ++c) trace += k[(3 * j + c) * nq] * a[c + 3 * j];
return trace;
}
// (du/dxi) v, without constructing du/dxi. v is fixed at this point.
inline void displacement_directional_value_3d(
const mfem::DenseMatrix &direction, const mfem::DenseMatrix &gradients,
const double *v, double *out
) {
const int n = direction.Height();
const double *u = direction.Data();
const double *g = gradients.Data();
double x = 0.0, y = 0.0, z = 0.0;
for (int i = 0; i < n; ++i) {
const double d = g[i] * v[0] + g[i + n] * v[1] + g[i + 2 * n] * v[2];
x += u[i] * d; y += u[i + n] * d; z += u[i + 2 * n] * d;
}
out[0] = x; out[1] = y; out[2] = z;
}
inline void pressure_displacement_action_3d(
const mfem::DenseMatrix &direction, const mfem::DenseMatrix &physicalGradients,
const double pressureWeight, mfem::Vector &action
) {
// H = du/dx; d(G dx) = (tr(H) G - G H) dx.
double h[9];
displacement_gradient_3d(direction, physicalGradients, h);
const double trace = h[0] + h[4] + h[8];
const int n = direction.Height();
const double *g = physicalGradients.Data();
double *r = action.HostReadWrite();
for (int c = 0; c < 3; ++c) {
const double a = pressureWeight * (h[3 * c] - (c == 0 ? trace : 0.0));
const double b = pressureWeight * (h[3 * c + 1] - (c == 1 ? trace : 0.0));
const double d = pressureWeight * (h[3 * c + 2] - (c == 2 ? trace : 0.0));
for (int i = 0; i < n; ++i) r[i + c * n] += a * g[i] + b * g[i + n] + d * g[i + 2 * n];
}
}
// Element VDofs are component-blocked, including for a byVDIM global space;
// this is the ordering consumed by ElementDisplacementDataFromElementVDofs.
// For an ordinary interior VALUE field, dx = du and
// d(log det J_element) = tr(J_element^-1 * du/dxi).
inline void add_interior_functional_row(
const mfem::DenseMatrix &gradients,
const mfem::DenseMatrix &values,
const int point,
const mfem::DenseMatrix &inverseElementJacobian,
const double determinantCoefficient,
const double *positionCoefficient,
mfem::Vector &row
) {
const int n = gradients.Height();
const int dimension = gradients.Width();
const int nq = values.Height();
const double *g = gradients.Data();
const double *b = values.Data() + point;
double *r = row.HostReadWrite();
for (int component = 0; component < dimension; ++component) {
for (int i = 0; i < n; ++i) {
double coefficient = positionCoefficient[component] * b[i * nq];
for (int derivative = 0; derivative < dimension; ++derivative) {
coefficient += determinantCoefficient * inverseElementJacobian(derivative, component) *
g[i + derivative * n];
}
r[i + component * n] += coefficient;
}
}
}
inline double apply_interior_functional(
const mfem::DenseMatrix &direction,
const mfem::DenseMatrix &gradients,
const mfem::DenseMatrix &values,
const int point,
const double *inverseElementJacobian,
const double determinantCoefficient,
const double *positionCoefficient
) {
const int n = direction.Height();
const int dimension = direction.Width();
const int nq = values.Height();
const double *u = direction.Data();
const double *g = gradients.Data();
const double *b = values.Data() + point;
double result = 0.0;
for (int component = 0; component < dimension; ++component) {
double value = 0.0;
for (int i = 0; i < n; ++i) value += u[i + component * n] * b[i * nq];
result += positionCoefficient[component] * value;
for (int derivative = 0; derivative < dimension; ++derivative) {
double gradient = 0.0;
for (int i = 0; i < n; ++i)
gradient += u[i + component * n] * g[i + derivative * n];
result += determinantCoefficient * inverseElementJacobian[derivative + component * dimension] *
gradient;
}
}
return result;
}
}

View File

@@ -11,6 +11,7 @@ module;
#include <mfem.hpp>
#include <mpi.h>
#include "interior_displacement_contractions.hpp"
module mean_field;
@@ -228,6 +229,7 @@ namespace mean_field::operators {
const bool updateAngularVelocity = !m_isPrepared || dependencies.rotation != m_preparedDependencies.rotation ||
angularVelocity != m_angularVelocity;
if (refreshGeometry || refreshDensity) m_jacobianRowsReady = false;
m_isPrepared = false;
if (synchronize_non_finite_failure(
!std::isfinite(angularVelocity), m_fem.mesh->GetComm(), "angular-velocity validity"
@@ -466,8 +468,99 @@ namespace mean_field::operators {
++m_residualApplicationCount;
}
bool PreparedAngularMomentumOperator::PrepareJacobianRows() const {
VerifyPrepared();
if (m_jacobianRowsReady) return m_jacobianRowsSupported;
m_jacobianRowsSupported = false;
for (const ElementPAData &data : m_elements) {
const auto &element = *m_fem.displacementFes->GetFE(data.elementId);
if (element.GetMapType() != mfem::FiniteElement::VALUE ||
element.GetDerivType() != mfem::FiniteElement::GRAD ||
m_domainMapper.IsCompactifiedElement(*m_fem.mesh->GetElementTransformation(data.elementId))) {
m_jacobianRowsReady = true;
return false;
}
}
m_densityJacobianLocal.SetSize(m_fem.densityFes->GetVSize());
m_displacementJacobianLocal.SetSize(m_fem.displacementFes->GetVSize());
m_densityJacobianLocal = 0.0;
m_displacementJacobianLocal = 0.0;
mfem::Vector densityRow, displacementRow, weightedRadius;
mapping::VolumeMappingContext mappingContext;
const auto &axis = m_constraint.specification().axis();
const auto &center = m_constraint.specification().center();
for (const ElementPAData &data : m_elements) {
const int nq = data.integrationRule->GetNPoints();
weightedRadius.SetSize(nq);
for (int q = 0; q < nq; ++q)
weightedRadius(q) = data.quadratureWeights(q) * data.cylindricalRadiusSquared(q);
densityRow.SetSize(data.densityDofs.Size());
data.densityBasis->GetValues().MultTranspose(weightedRadius, densityRow);
displacementRow.SetSize(data.displacementDofs.Size());
displacementRow = 0.0;
const auto basis = m_fem.GetReferenceTables().GetScalarTable(
*m_fem.displacementFes->GetFE(data.elementId), *data.integrationRule);
for (int q = 0; q < nq; ++q) {
data.mappingContexts.Load(q, mappingContext);
double relative[3], positionCoefficient[3], axialPosition = 0.0;
for (int c = 0; c < 3; ++c) {
relative[c] = mappingContext.mapping.physical_position(c) - center[c];
axialPosition += axis[c] * relative[c];
}
for (int c = 0; c < 3; ++c)
positionCoefficient[c] = 2.0 * data.density(q) * data.quadratureWeights(q) *
(relative[c] - axis[c] * axialPosition);
add_interior_functional_row(
basis->GetGradients(q), basis->GetValues(), q, mappingContext.quadrature.J_inv,
data.density(q) * weightedRadius(q), positionCoefficient, displacementRow);
}
if (data.densityDofTransformation) data.densityDofTransformation->TransformDual(densityRow);
if (data.displacementDofTransformation) data.displacementDofTransformation->TransformDual(displacementRow);
m_densityJacobianLocal.AddElementVector(data.densityDofs, densityRow);
m_displacementJacobianLocal.AddElementVector(data.displacementDofs, displacementRow);
}
verify_finite_vector(m_densityJacobianLocal, "Prepared angular-momentum density row is non-finite.");
verify_finite_vector(m_displacementJacobianLocal, "Prepared angular-momentum displacement row is non-finite.");
m_jacobianRowsSupported = true;
m_jacobianRowsReady = true;
++m_jacobianRowPreparationCount;
return true;
}
std::uint64_t PreparedAngularMomentumOperator::GetJacobianRowPreparationCount() const noexcept {
return m_jacobianRowPreparationCount;
}
double PreparedAngularMomentumOperator::EvaluateDensityMomentActionLocal(const mfem::Vector &direction) const {
if (!PrepareJacobianRows()) return EvaluateDensityMomentActionLocalReference(direction);
true_to_local(*m_fem.densityFes, direction, m_densityVariationLocal);
return m_densityJacobianLocal * m_densityVariationLocal;
}
double PreparedAngularMomentumOperator::EvaluateDisplacementMomentActionLocal(const mfem::Vector &direction) const {
if (!PrepareJacobianRows()) return EvaluateDisplacementMomentActionLocalReference(direction);
true_to_local(*m_fem.displacementFes, direction, m_displacementVariationLocal);
return m_displacementJacobianLocal * m_displacementVariationLocal;
}
void PreparedAngularMomentumOperator::ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation, const mfem::Vector &displacementVariation,
const double angularVelocityVariation, mfem::Vector &action
) const {
VerifyPrepared();
verify_finite_vector(densityVariation, "Angular-momentum density direction is non-finite.");
verify_finite_vector(displacementVariation, "Angular-momentum displacement direction is non-finite.");
MFEM_VERIFY(std::isfinite(angularVelocityVariation), "Angular-velocity direction is non-finite.");
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
action.SetSize(1);
action(0) = m_angularVelocity * GlobalSum(EvaluateDensityMomentActionLocalReference(m_densityVariationTrue) +
EvaluateDisplacementMomentActionLocalReference(m_displacementVariationTrue)) +
m_momentOfInertia * angularVelocityVariation;
}
double
PreparedAngularMomentumOperator::EvaluateDensityMomentActionLocal(const mfem::Vector &densityVariation) const {
PreparedAngularMomentumOperator::EvaluateDensityMomentActionLocalReference(const mfem::Vector &densityVariation) const {
MFEM_VERIFY(
densityVariation.Size() == m_fem.densityFes->GetTrueVSize(),
"Angular-momentum density action has the wrong true-vector size."
@@ -490,7 +583,7 @@ namespace mean_field::operators {
return localAction;
}
double PreparedAngularMomentumOperator::EvaluateDisplacementMomentActionLocal(
double PreparedAngularMomentumOperator::EvaluateDisplacementMomentActionLocalReference(
const mfem::Vector &displacementVariation
) const {
MFEM_VERIFY(

View File

@@ -1,5 +1,7 @@
module;
#include "interior_displacement_contractions.hpp"
#include <array>
#include <cmath>
#include <cstdint>
@@ -730,10 +732,66 @@ namespace mean_field::operators {
}
void PreparedBarotropicClosureOperator::Mult(
const mfem::Vector &densityVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
MultImpl(densityVariation, enthalpyVariation, displacementVariation, action, true, true);
}
void PreparedBarotropicClosureOperator::MultReference(
const mfem::Vector &densityVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
MultImpl(densityVariation, enthalpyVariation, displacementVariation, action, false, false);
}
void PreparedBarotropicClosureOperator::MultCachedReference(
const mfem::Vector &densityVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
MultImpl(densityVariation, enthalpyVariation, displacementVariation, action, true, false);
}
int PreparedBarotropicClosureOperator::GetTensorElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements)
count += data.densityBasis->HasTensorEvaluation3D() &&
data.enthalpyBasis->HasTensorEvaluation3D() && data.displacementBasis->HasTensorEvaluation3D();
return count;
}
void PreparedBarotropicClosureOperator::ApplyEnthalpyJacobianAction(
const mfem::Vector &enthalpyVariation, mfem::Vector &action, const bool useTensorPath
) const {
VerifyPrepared();
MFEM_VERIFY(enthalpyVariation.Size() == m_enthalpyMap.reduced_size(),
"The supported enthalpy variation has the wrong size.");
validate_finite_vector(enthalpyVariation, "The enthalpy variation contains a non-finite value.");
m_enthalpyMap.scatter(enthalpyVariation, m_enthalpyVariationTrue);
ApplyThermodynamicActionFull(nullptr, m_enthalpyVariationTrue, m_fullThermodynamicAction, useTensorPath);
action.SetSize(m_densityMap.reduced_size());
m_densityMap.gather(m_fullThermodynamicAction, action);
}
void PreparedBarotropicClosureOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation, mfem::Vector &action, const bool useTensorPath
) const {
VerifyPrepared();
MFEM_VERIFY(displacementVariation.Size() == m_displacementMap.reduced_size(),
"The supported displacement variation has the wrong size.");
validate_finite_vector(displacementVariation, "The displacement variation contains a non-finite value.");
m_displacementMap.scatter(displacementVariation, m_displacementVariationTrue);
ApplyDisplacementActionFull(m_displacementVariationTrue, m_fullDisplacementAction, true, useTensorPath);
action.SetSize(m_densityMap.reduced_size());
m_densityMap.gather(m_fullDisplacementAction, action);
}
void PreparedBarotropicClosureOperator::MultImpl(
const mfem::Vector &densityVariation,
const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation,
mfem::Vector &action
mfem::Vector &action,
const bool useFastPath, const bool useTensorPath
) const {
VerifyPrepared();
@@ -758,9 +816,9 @@ namespace mean_field::operators {
m_enthalpyMap.scatter(enthalpyVariation, m_enthalpyVariationTrue);
m_displacementMap.scatter(displacementVariation, m_displacementVariationTrue);
ApplyThermodynamicActionFull(m_densityVariationTrue, m_enthalpyVariationTrue, m_fullThermodynamicAction);
ApplyThermodynamicActionFull(&m_densityVariationTrue, m_enthalpyVariationTrue, m_fullThermodynamicAction, useTensorPath);
ApplyDisplacementActionFull(m_displacementVariationTrue, m_fullDisplacementAction);
ApplyDisplacementActionFull(m_displacementVariationTrue, m_fullDisplacementAction, useFastPath, useTensorPath);
MFEM_VERIFY(
m_fullThermodynamicAction.Size() == m_densityMap.full_size() &&
@@ -797,12 +855,13 @@ namespace mean_field::operators {
}
void PreparedBarotropicClosureOperator::ApplyThermodynamicActionFull(
const mfem::Vector &densityVariationTrue,
const mfem::Vector *densityVariationTrue,
const mfem::Vector &enthalpyVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue, const bool useTensorPath
) const {
MFEM_VERIFY(
densityVariationTrue.Size() == m_densityMap.full_size(), "The full density variation has the wrong size."
densityVariationTrue == nullptr || densityVariationTrue->Size() == m_densityMap.full_size(),
"The full density variation has the wrong size."
);
MFEM_VERIFY(
enthalpyVariationTrue.Size() == m_enthalpyMap.full_size(), "The full enthalpy variation has the wrong size."
@@ -811,7 +870,7 @@ namespace mean_field::operators {
mfem::Vector densityVariationLocal;
mfem::Vector enthalpyVariationLocal;
true_to_local(*m_fem.densityFes, densityVariationTrue, densityVariationLocal);
if (densityVariationTrue) true_to_local(*m_fem.densityFes, *densityVariationTrue, densityVariationLocal);
true_to_local(*m_fem.enthalpyFes, enthalpyVariationTrue, enthalpyVariationLocal);
mfem::Vector localAction(m_fem.densityFes->GetVSize());
@@ -825,10 +884,10 @@ namespace mean_field::operators {
mfem::Vector elementAction;
for (const ElementPAData &data : m_elements) {
densityVariationLocal.GetSubVector(data.densityDofs, elementDensityVariation);
if (densityVariationTrue) densityVariationLocal.GetSubVector(data.densityDofs, elementDensityVariation);
enthalpyVariationLocal.GetSubVector(data.enthalpyDofs, elementEnthalpyVariation);
if (data.densityDofTransformation != nullptr) {
if (densityVariationTrue && data.densityDofTransformation != nullptr) {
data.densityDofTransformation->InvTransformPrimal(elementDensityVariation);
}
if (data.enthalpyDofTransformation != nullptr) {
@@ -839,17 +898,25 @@ namespace mean_field::operators {
quadratureEnthalpyVariation.SetSize(data.quadratureWeights.Size());
quadratureAction.SetSize(data.quadratureWeights.Size());
data.densityBasis->GetValues().Mult(elementDensityVariation, quadratureDensityVariation);
data.enthalpyBasis->GetValues().Mult(elementEnthalpyVariation, quadratureEnthalpyVariation);
if (densityVariationTrue) {
if (useTensorPath && data.densityBasis->HasTensorEvaluation3D())
data.densityBasis->EvaluateValuesTensor3D(elementDensityVariation, quadratureDensityVariation, m_tensorWorkspace);
else data.densityBasis->GetValues().Mult(elementDensityVariation, quadratureDensityVariation);
}
if (useTensorPath && data.enthalpyBasis->HasTensorEvaluation3D())
data.enthalpyBasis->EvaluateValuesTensor3D(elementEnthalpyVariation, quadratureEnthalpyVariation, m_tensorWorkspace);
else data.enthalpyBasis->GetValues().Mult(elementEnthalpyVariation, quadratureEnthalpyVariation);
for (int quadraturePoint = 0; quadraturePoint < quadratureAction.Size(); ++quadraturePoint) {
quadratureAction(quadraturePoint) =
data.quadratureWeights(quadraturePoint) * quadratureDensityVariation(quadraturePoint) -
(densityVariationTrue ? data.quadratureWeights(quadraturePoint) * quadratureDensityVariation(quadraturePoint) : 0.0) -
data.weightedEnthalpyDerivative(quadraturePoint) * quadratureEnthalpyVariation(quadraturePoint);
}
elementAction.SetSize(data.densityDofs.Size());
data.densityBasis->GetValues().MultTranspose(quadratureAction, elementAction);
if (useTensorPath && data.densityBasis->HasTensorEvaluation3D())
data.densityBasis->ApplyTransposeTensor3D(quadratureAction, elementAction, m_tensorWorkspace);
else data.densityBasis->GetValues().MultTranspose(quadratureAction, elementAction);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->TransformDual(elementAction);
@@ -863,7 +930,8 @@ namespace mean_field::operators {
void PreparedBarotropicClosureOperator::ApplyDisplacementActionFull(
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
const bool useFastPath, const bool useTensorPath
) const {
MFEM_VERIFY(
displacementVariationTrue.Size() == m_displacementMap.full_size(),
@@ -905,8 +973,29 @@ namespace mean_field::operators {
m_referenceDisplacementJacobian.SetSize(dimension, dimension);
m_quadratureDisplacementAction.SetSize(integrationRule.GetNPoints());
const bool tensor = useTensorPath && data.displacementBasis->HasTensorEvaluation3D();
if (tensor) data.displacementBasis->EvaluateTensor3D(directionDofs, m_tensorDisplacement, m_tensorWorkspace);
for (int quadraturePoint = 0; quadraturePoint < integrationRule.GetNPoints(); ++quadraturePoint) {
if (tensor) {
double trace = 0.0;
for (int row = 0; row < 3; ++row) for (int column = 0; column < 3; ++column)
trace += data.inverseElementJacobians(quadraturePoint, row * 3 + column) *
m_tensorDisplacement(quadraturePoint, 4 * column + row + 1);
m_quadratureDisplacementAction(quadraturePoint) = data.weightedResidual(quadraturePoint) * trace;
MFEM_VERIFY(std::isfinite(m_quadratureDisplacementAction(quadraturePoint)),
"Prepared barotropic closure displacement action encountered a non-finite quadrature value.");
continue;
}
if (useFastPath && dimension == 3) {
const double trace = displacement_trace_3d(
directionDofs, data.displacementBasis->GetGradients(quadraturePoint),
data.inverseElementJacobians, quadraturePoint);
m_quadratureDisplacementAction(quadraturePoint) = data.weightedResidual(quadraturePoint) * trace;
MFEM_VERIFY(std::isfinite(m_quadratureDisplacementAction(quadraturePoint)),
"Prepared barotropic closure displacement action encountered a non-finite quadrature value.");
continue;
}
mfem::MultAtB(
directionDofs, data.displacementBasis->GetGradients(quadraturePoint),
m_referenceDisplacementJacobian
@@ -930,7 +1019,9 @@ namespace mean_field::operators {
}
m_elementDisplacementAction.SetSize(data.densityDofs.Size());
data.densityBasis->GetValues().MultTranspose(m_quadratureDisplacementAction, m_elementDisplacementAction);
if (useTensorPath && data.densityBasis->HasTensorEvaluation3D())
data.densityBasis->ApplyTransposeTensor3D(m_quadratureDisplacementAction, m_elementDisplacementAction, m_tensorWorkspace);
else data.densityBasis->GetValues().MultTranspose(m_quadratureDisplacementAction, m_elementDisplacementAction);
if (data.densityDofTransformation != nullptr) {
data.densityDofTransformation->TransformDual(m_elementDisplacementAction);

View File

@@ -1,5 +1,7 @@
module;
#include "interior_displacement_contractions.hpp"
#include <cmath>
#include <expected>
#include <optional>
@@ -184,7 +186,7 @@ namespace mean_field::operators {
std::expected<
void,
kernels::GravityDisplacementForceRejection>
PreparedGravityDisplacementForceOperator::TryPrepareElementData() {
PreparedGravityDisplacementForceOperator::TryPrepareElementData(const bool assembleResidual) {
m_elements.clear();
m_elements.reserve(m_fem.mesh->GetNE());
@@ -206,8 +208,14 @@ namespace mean_field::operators {
mfem::Vector elementCompactification;
mfem::Vector densityShape;
mfem::Vector baseGravityReferenceValue;
mfem::Vector baseGravityCellValue;
mfem::DenseMatrix gravityGradientShape;
if (assembleResidual) {
m_localAction.SetSize(m_fem.displacementFes->GetVSize());
m_localAction = 0.0;
}
mfem::Vector residualComponent;
const int dimension = m_domainMapper.GetDimension();
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
@@ -262,6 +270,11 @@ namespace mean_field::operators {
data.meshPiolaJacobians.SetSize(data.integrationRule->GetNPoints(), dimension * dimension);
}
data.fastApplication = dimension == 3 && m_fem.displacementFes->GetOrdering() == mfem::Ordering::byNODES &&
displacementElement.GetMapType() == mfem::FiniteElement::VALUE &&
displacementElement.GetDerivType() == mfem::FiniteElement::GRAD &&
data.gravityReferenceTable != nullptr;
const mapping::ElementDisplacementData displacementData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementBaseDisplacement);
const mapping::ElementCompactificationData compactificationData(
@@ -280,6 +293,7 @@ namespace mean_field::operators {
densityShape.SetSize(densityElement.GetDof());
gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension);
baseGravityReferenceValue.SetSize(dimension);
if (assembleResidual) m_quadratureForce.SetSize(quadraturePointCount, dimension);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
@@ -297,14 +311,26 @@ namespace mean_field::operators {
for (int dof = 0; dof < densityElement.GetDof(); ++dof) {
densityShape(dof) = data.densityReferenceTable->GetValues()(quadraturePoint, dof);
}
gravityGradientElement.CalcVShape(*transformation, gravityGradientShape);
gravityGradientShape.MultTranspose(elementBaseGravityGradient, baseGravityReferenceValue);
const auto *meshGeometry = m_domainMapper.GetFixedMeshGeometry(*transformation, integrationPoint);
const double meshWeight = meshGeometry ? meshGeometry->weight : transformation->Weight();
if (assembleResidual && meshGeometry && data.gravityReferenceTable) {
baseGravityCellValue.SetSize(dimension);
data.gravityReferenceTable->GetValues(quadraturePoint).MultTranspose(
elementBaseGravityGradient, baseGravityCellValue);
meshGeometry->jacobian.Mult(baseGravityCellValue, baseGravityReferenceValue);
baseGravityReferenceValue /= meshWeight;
} else {
gravityGradientElement.CalcVShape(*transformation, gravityGradientShape);
gravityGradientShape.MultTranspose(elementBaseGravityGradient, baseGravityReferenceValue);
}
data.baseDensityValues(quadraturePoint) = elementBaseDensity * densityShape;
data.referenceWeights(quadraturePoint) = integrationPoint.weight * transformation->Weight();
data.referenceWeights(quadraturePoint) = integrationPoint.weight * meshWeight;
const mfem::DenseMatrix &inverseMeshJacobian = transformation->InverseJacobian();
const mfem::DenseMatrix &meshJacobian = transformation->Jacobian();
const double inverseMeshWeight = 1.0 / transformation->Weight();
const mfem::DenseMatrix &inverseMeshJacobian =
meshGeometry ? meshGeometry->inverseJacobian : transformation->InverseJacobian();
const mfem::DenseMatrix &meshJacobian =
meshGeometry ? meshGeometry->jacobian : transformation->Jacobian();
const double inverseMeshWeight = 1.0 / meshWeight;
for (int row = 0; row < dimension; ++row) {
data.baseGravityReferenceValues(quadraturePoint, row) = baseGravityReferenceValue(row);
for (int column = 0; column < dimension; ++column) {
@@ -335,8 +361,40 @@ namespace mean_field::operators {
}
}
}
if (assembleResidual) {
for (int row = 0; row < dimension; ++row) {
double mapped = 0.0;
for (int column = 0; column < dimension; ++column)
mapped += mappingContext.mapping.mapping_jacobian(row, column) *
baseGravityReferenceValue(column);
m_quadratureForce(quadraturePoint, row) =
data.referenceWeights(quadraturePoint) * data.baseDensityValues(quadraturePoint) * mapped;
}
}
}
if (assembleResidual) {
const int ndof = displacementElement.GetDof();
MFEM_VERIFY(data.displacementDofs.Size() == ndof * dimension,
"Prepared force residual has an invalid displacement element size.");
m_elementAction.SetSize(data.displacementDofs.Size());
residualComponent.SetSize(ndof);
for (int component = 0; component < dimension; ++component) {
data.displacementReferenceTable->GetValues().MultTranspose(
m_quadratureForce.Data() + component * quadraturePointCount, residualComponent.GetData());
for (int dof = 0; dof < ndof; ++dof)
m_elementAction(vector_dof_index(m_fem.displacementFes->GetOrdering(),
dof, component, ndof, dimension)) = residualComponent(dof);
}
if (!vector_is_finite(m_elementAction)) return std::unexpected(non_finite_rejection());
if (data.displacementDofTransformation != nullptr)
data.displacementDofTransformation->TransformDual(m_elementAction);
m_localAction.AddElementVector(data.displacementDofs, m_elementAction);
}
}
if (assembleResidual && !vector_is_finite(m_localAction))
return std::unexpected(non_finite_rejection());
return {};
}
@@ -352,6 +410,17 @@ namespace mean_field::operators {
PreparedGravityDisplacementForceReport,
kernels::GravityDisplacementForceRejection>
PreparedGravityDisplacementForceOperator::TryPrepare() {
return TryPrepareImpl(true);
}
PreparedGravityDisplacementForceReport PreparedGravityDisplacementForceOperator::PrepareReference() {
auto result = TryPrepareImpl(false);
if (!result.has_value()) throw_rejection(result.error());
return std::move(result).value();
}
std::expected<PreparedGravityDisplacementForceReport, kernels::GravityDisplacementForceRejection>
PreparedGravityDisplacementForceOperator::TryPrepareImpl(const bool useFastPreparation) {
MFEM_VERIFY(
m_gravityContext.IsPrepared(), "PreparedGravityDisplacementForceOperator requires the shared "
"gravity linearization context to be prepared first."
@@ -359,20 +428,24 @@ namespace mean_field::operators {
const context::gravity_field::GravityFieldRevisions &requestedRevisions = m_gravityContext.GetRevisions();
if (m_isPrepared && relevant_revisions_match(requestedRevisions, m_preparedRevisions)) {
if (m_isPrepared && m_preparedWithFastResidual == useFastPreparation &&
relevant_revisions_match(requestedRevisions, m_preparedRevisions)) {
return {};
}
m_isPrepared = false;
/*
* Build the reusable element plan before assembling the residual.
* This pass stops at the first invalid mapped quadrature point, so a
* rejected line-search candidate need not traverse the full stateless
* residual kernel. Synchronize before proceeding so every rank takes
* the same branch.
*/
const auto elementResult = TryPrepareElementData();
const bool inputsFinite = vector_is_finite(m_gravityContext.GetDensityTrue()) &&
vector_is_finite(m_gravityContext.GetGravityGradientTrue()) &&
vector_is_finite(m_gravityContext.GetGeometryContext().GetDisplacementTrue());
const auto inputStatus = synchronize_rejection(
inputsFinite ? std::optional<Rejection>{} : std::optional<Rejection>{non_finite_rejection()},
m_fem.mesh->GetComm());
if (!inputStatus.has_value()) return std::unexpected(inputStatus.error());
// The production path assembles local residual contributions while
// preparing the geometry. Synchronize validity before the true-DOF sum.
const auto elementResult = TryPrepareElementData(useFastPreparation);
const std::optional<Rejection> localElementRejection =
elementResult.has_value() ? std::optional<Rejection>{} : std::optional<Rejection>{elementResult.error()};
auto synchronizedElement = synchronize_rejection(localElementRejection, m_fem.mesh->GetComm());
@@ -380,12 +453,14 @@ namespace mean_field::operators {
return std::unexpected(synchronizedElement.error());
}
auto residualResult = kernels::try_apply_gravity_displacement_force_residual(
m_fem, m_domainMapper, m_gravityContext.GetDensityTrue(), m_gravityContext.GetGravityGradientTrue(),
m_gravityContext.GetGeometryContext().GetDisplacementTrue(), m_actionTrue
);
if (!residualResult.has_value()) {
return std::unexpected(residualResult.error());
if (useFastPreparation) {
local_to_true(*m_fem.displacementFes, m_localAction, m_actionTrue);
} else {
auto residualResult = kernels::try_apply_gravity_displacement_force_residual(
m_fem, m_domainMapper, m_gravityContext.GetDensityTrue(), m_gravityContext.GetGravityGradientTrue(),
m_gravityContext.GetGeometryContext().GetDisplacementTrue(), m_actionTrue
);
if (!residualResult.has_value()) return std::unexpected(residualResult.error());
}
m_cachedResidual.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, m_cachedResidual);
@@ -397,6 +472,7 @@ namespace mean_field::operators {
if (!synchronized.has_value()) {
return std::unexpected(synchronized.error());
}
m_preparedWithFastResidual = useFastPreparation;
m_preparedRevisions = requestedRevisions;
++m_residualPreparationCount;
m_isPrepared = true;
@@ -471,7 +547,8 @@ namespace mean_field::operators {
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &gravityGradientVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
const bool useFastPath, const bool useTensorPath
) const {
true_to_local(*m_fem.densityFes, densityVariationTrue, m_densityVariationLocal);
true_to_local(*m_fem.gravityFluxFes, gravityGradientVariationTrue, m_gravityGradientVariationLocal);
@@ -513,6 +590,60 @@ namespace mean_field::operators {
const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix();
const int scalarDisplacementDofCount = displacementElement.GetDof();
if (useFastPath && data.fastApplication) {
const int nq = data.integrationRule->GetNPoints();
m_quadratureDensityVariation.SetSize(nq);
data.densityReferenceTable->GetValues().Mult(m_elementDensityVariation, m_quadratureDensityVariation);
m_quadratureForce.SetSize(nq, 3);
double *force = m_quadratureForce.Data();
const bool tensorDisplacement = useTensorPath && data.displacementReferenceTable->HasTensorEvaluation3D();
const bool tensorGravity = useTensorPath && data.gravityReferenceTable->HasTensorEvaluation3D();
if (tensorDisplacement)
data.displacementReferenceTable->EvaluateTensor3D(directionDofs, m_tensorDisplacement, m_tensorWorkspace);
if (tensorGravity)
data.gravityReferenceTable->EvaluateTensor3D(m_elementGravityGradientVariation, m_tensorGravity, m_tensorWorkspace);
for (int q = 0; q < nq; ++q) {
double base[3], cellVariation[3], meshVariation[3], material[3], mapped[3];
double referenceDirection[3], geometryVariation[3];
for (int c = 0; c < 3; ++c) base[c] = data.baseGravityReferenceValues(q, c);
if (tensorGravity) {
for (int c = 0; c < 3; ++c) cellVariation[c] = m_tensorGravity(q, c);
} else vector_basis_value_3d(data.gravityReferenceTable->GetValues(q),
m_elementGravityGradientVariation, cellVariation);
cached_matrix_vector_3d(data.meshPiolaJacobians, q, cellVariation, meshVariation);
const double rho = data.baseDensityValues(q);
const double drho = m_quadratureDensityVariation(q);
for (int c = 0; c < 3; ++c) material[c] = drho * base[c] + rho * meshVariation[c];
cached_matrix_vector_3d(data.mappingJacobians, q, material, mapped);
cached_matrix_vector_3d(data.inverseMeshJacobians, q, base, referenceDirection);
if (tensorDisplacement) {
for (int c = 0; c < 3; ++c) {
geometryVariation[c] = 0.0;
for (int axis = 0; axis < 3; ++axis)
geometryVariation[c] += m_tensorDisplacement(q, 4*c + 1 + axis) * referenceDirection[axis];
}
} else displacement_directional_value_3d(directionDofs,
data.displacementReferenceTable->GetGradients(q), referenceDirection, geometryVariation);
for (int c = 0; c < 3; ++c)
force[q + c * nq] = data.referenceWeights(q) * (mapped[c] + rho * geometryVariation[c]);
}
m_elementAction.SetSize(data.displacementDofs.Size());
double *element = m_elementAction.HostWrite();
if (tensorDisplacement) {
data.displacementReferenceTable->ApplyTransposeTensor3D(
m_quadratureForce, m_tensorElementDual, m_tensorWorkspace);
for (int i = 0; i < m_elementAction.Size(); ++i) element[i] = m_tensorElementDual.Data()[i];
} else {
for (int c = 0; c < 3; ++c)
data.displacementReferenceTable->GetValues().MultTranspose(
force + c * nq, element + c * scalarDisplacementDofCount);
}
if (data.displacementDofTransformation)
data.displacementDofTransformation->TransformDual(m_elementAction);
m_localAction.AddElementVector(data.displacementDofs, m_elementAction);
continue;
}
m_densityShape.SetSize(densityElement.GetDof());
m_displacementShape.SetSize(scalarDisplacementDofCount);
m_gravityGradientShape.SetSize(gravityGradientElement.GetDof(), dimension);
@@ -628,6 +759,52 @@ namespace mean_field::operators {
++m_completeJacobianStatistics.applications;
}
void PreparedGravityDisplacementForceOperator::ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
const mfem::Vector &gravityGradientVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size());
m_gravityGradientVariationTrue.SetSize(m_gravityContext.GetGravityGradientMap().full_size());
m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size());
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_gravityContext.GetGravityGradientMap().scatter(gravityGradientVariation, m_gravityGradientVariationTrue);
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
ApplyPreparedCompleteJacobianActionTrue(
m_densityVariationTrue, m_displacementVariationTrue, m_gravityGradientVariationTrue, m_actionTrue, false
);
action.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, action);
}
void PreparedGravityDisplacementForceOperator::ApplyCompleteJacobianActionCachedReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
const mfem::Vector &gravityGradientVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_gravityContext.GetDensityMap().full_size());
m_gravityGradientVariationTrue.SetSize(m_gravityContext.GetGravityGradientMap().full_size());
m_displacementVariationTrue.SetSize(m_gravityContext.GetDisplacementMap().full_size());
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_gravityContext.GetGravityGradientMap().scatter(gravityGradientVariation, m_gravityGradientVariationTrue);
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
ApplyPreparedCompleteJacobianActionTrue(
m_densityVariationTrue, m_displacementVariationTrue, m_gravityGradientVariationTrue, m_actionTrue, true, false
);
action.SetSize(m_gravityContext.GetDisplacementMap().reduced_size());
m_gravityContext.GetDisplacementMap().gather(m_actionTrue, action);
}
bool PreparedGravityDisplacementForceOperator::IsPrepared() const noexcept {
if (!m_isPrepared || !m_gravityContext.IsPrepared()) {
return false;

View File

@@ -345,6 +345,11 @@ namespace mean_field::operators {
}
}
void PreparedMappedGravitySourceOperator::PrepareReference(const mfem::Vector &displacement) {
auto result = TryPrepareImpl(displacement, PreparationMode::linearization, false);
if (!result.has_value()) throwGravitySourcePreparationRejection(result.error());
}
void PreparedMappedGravitySourceOperator::PreparePrimal(const mfem::Vector &displacement) {
MEAN_FIELD_PROFILE_SCOPE_WARMUP("PreparedMappedGravitySourceOperator::Prepare primal", 0);
auto result = TryPrepareImpl(displacement, PreparationMode::primal);
@@ -366,7 +371,7 @@ namespace mean_field::operators {
GravitySourcePreparationResult PreparedMappedGravitySourceOperator::TryPrepareImpl(
const mfem::Vector &displacement,
const PreparationMode mode
const PreparationMode mode, const bool useCachedMeshWeight
) {
MFEM_VERIFY(
displacement.Size() == m_displacement_map.reduced_size(),
@@ -513,7 +518,12 @@ namespace mean_field::operators {
transformation.SetIntPoint(&integration_point);
const double quadrature_value = integration_point.weight * transformation.Weight() * coefficient_value;
// EvaluateVolume already cached the computational mesh geometry.
// Resetting IntPoint above invalidates MFEM's local weight cache.
const auto *fixed = useCachedMeshWeight ?
m_domain_mapper.GetFixedMeshGeometry(transformation, integration_point) : nullptr;
const double meshWeight = fixed ? fixed->weight : transformation.Weight();
const double quadrature_value = integration_point.weight * meshWeight * coefficient_value;
if (!std::isfinite(quadrature_value) || quadrature_value <= 0.0) {
localNonFiniteQuadrature = true;
@@ -608,9 +618,27 @@ namespace mean_field::operators {
}
void PreparedMappedGravitySourceOperator::MultDisplacementVariationTrue(
const mfem::Vector &densityTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &densityTrue, const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const {
MultDisplacementVariationTrueImpl(densityTrue, displacementVariationTrue, actionVariationTrue, true);
}
void PreparedMappedGravitySourceOperator::MultDisplacementVariationTrueReference(
const mfem::Vector &densityTrue, const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const {
MultDisplacementVariationTrueImpl(densityTrue, displacementVariationTrue, actionVariationTrue, false);
}
int PreparedMappedGravitySourceOperator::GetTensorVariationElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements)
count += data.displacement_reference && data.displacement_reference->HasTensorEvaluation3D();
return count;
}
void PreparedMappedGravitySourceOperator::MultDisplacementVariationTrueImpl(
const mfem::Vector &densityTrue, const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue, const bool useTensorPath
) const {
MFEM_VERIFY(
m_is_prepared,
@@ -667,10 +695,15 @@ namespace mean_field::operators {
m_reference_displacement_jacobian.SetSize(dimension, dimension);
m_quadrature_variation_action.SetSize(data.integration_rule->GetNPoints());
data.GetDensityBasis().Mult(m_element_density, m_quadrature_variation_action);
if (useTensorPath && data.density_reference && data.density_reference->HasTensorEvaluation3D())
data.density_reference->EvaluateValuesTensor3D(m_element_density, m_quadrature_variation_action, m_tensorWorkspace);
else data.GetDensityBasis().Mult(m_element_density, m_quadrature_variation_action);
const bool tensor = useTensorPath && data.displacement_reference &&
data.displacement_reference->HasTensorEvaluation3D();
if (tensor) data.displacement_reference->EvaluateTensor3D(direction_dofs, m_tensorSamples, m_tensorWorkspace);
for (int quadrature_point = 0; quadrature_point < data.integration_rule->GetNPoints(); ++quadrature_point) {
mfem::MultAtB(
if (!tensor) mfem::MultAtB(
direction_dofs, data.displacement_reference->GetGradients(quadrature_point),
m_reference_displacement_jacobian
);
@@ -680,7 +713,8 @@ namespace mean_field::operators {
for (int column = 0; column < dimension; ++column) {
logarithmic_jacobian_variation +=
data.inverse_element_jacobians(quadrature_point, row * dimension + column) *
m_reference_displacement_jacobian(column, row);
(tensor ? m_tensorSamples(quadrature_point, 4*column+row+1) :
m_reference_displacement_jacobian(column, row));
}
}
@@ -693,7 +727,9 @@ namespace mean_field::operators {
}
m_element_variation_action.SetSize(data.potential_dofs.Size());
data.GetPotentialBasis().MultTranspose(m_quadrature_variation_action, m_element_variation_action);
if (useTensorPath && data.potential_reference && data.potential_reference->HasTensorEvaluation3D())
data.potential_reference->ApplyTransposeTensor3D(m_quadrature_variation_action, m_element_variation_action, m_tensorWorkspace);
else data.GetPotentialBasis().MultTranspose(m_quadrature_variation_action, m_element_variation_action);
if (data.potential_dof_transformation != nullptr) {
data.potential_dof_transformation->TransformDual(m_element_variation_action);

View File

@@ -9,6 +9,7 @@ module;
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
#include <mpi.h>
@@ -16,6 +17,7 @@ module mean_field;
import :fem.reference_tables;
import :operators.prepared_hdiv_mass;
import :mapping.kelvin;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
@@ -304,6 +306,124 @@ namespace {
}
}
// Keep MFEM's PA assembly, restrictions, signed RT ordering, and diagonal.
// Only the CPU element contraction changes. Scratch is sized to the actual
// rule, and the two domain forms visit only their marked elements.
class TensorHDivMassIntegrator final : public mfem::VectorFEMassIntegrator {
public:
TensorHDivMassIntegrator(mfem::MatrixCoefficient &coefficient, const mfem::Array<int> &marker)
: mfem::VectorFEMassIntegrator(coefficient), m_marker(marker) {}
void AssemblePA(const mfem::FiniteElementSpace &space) override {
mfem::VectorFEMassIntegrator::AssemblePA(space);
m_activeElements.clear();
m_supported = !mfem::Device::Allows(~mfem::Backend::CPU) &&
dim == 3 && trial_fetype == mfem::FiniteElement::DIV &&
test_fetype == trial_fetype && dofs1D == dofs1Dtest;
if (!m_supported) return;
for (int e = 0; e < space.GetNE(); ++e) {
const int attribute = space.GetMesh()->GetAttribute(e);
if (attribute > 0 && attribute <= m_marker.Size() && m_marker[attribute - 1])
m_activeElements.push_back(e);
}
}
[[nodiscard]] int FastElementCount() const noexcept {
return m_supported ? static_cast<int>(m_activeElements.size()) : 0;
}
[[nodiscard]] bool UseFastApplication() const noexcept { return m_useFast; }
void SetFastApplication(bool value) const noexcept { m_useFast = value; }
void AddMultPA(const mfem::Vector &x, mfem::Vector &y) const override {
if (!m_useFast || !m_supported) {
mfem::VectorFEMassIntegrator::AddMultPA(x, y);
return;
}
// The current RT2 / nine-point-per-axis rule is specialized; other
// supported CPU orders use exactly the same algorithm at runtime.
if (dofs1D == 4 && quad1D == 9) Apply<4, 9>(x, y);
else Apply<0, 0>(x, y);
}
private:
template <int D, int Q>
void Apply(const mfem::Vector &xVector, mfem::Vector &yVector) const {
const int d = D == 0 ? dofs1D : D, q = Q == 0 ? quad1D : Q;
const int nq = q*q*q, nd = 3*d*(d-1)*(d-1), entries = symmetric ? 6 : 9;
MFEM_VERIFY(xVector.Size() == ne*nd && yVector.Size() == ne*nd,
"H(div) PA element vectors have incompatible sizes.");
m_workspace.SetSize(3*nq + q*d*d + q*q*d);
double *values = m_workspace.HostWrite(), *sx = values + 3*nq, *sxy = sx + q*d*d;
const double *x = xVector.HostRead(), *op = pa_data.HostRead();
double *y = yVector.HostReadWrite();
const double *bc = mapsC->B.HostRead(), *bo = mapsO->B.HostRead();
for (const int e : m_activeElements) {
int offset = e*nd;
for (int c = 0; c < 3; ++c) {
const int dx = c == 0 ? d : d-1, dy = c == 1 ? d : d-1, dz = c == 2 ? d : d-1;
const double *bx = c == 0 ? bc : bo, *by = c == 1 ? bc : bo, *bz = c == 2 ? bc : bo;
for (int k = 0; k < dz; ++k) for (int j = 0; j < dy; ++j) for (int a = 0; a < q; ++a) {
double v = 0.0;
for (int i = 0; i < dx; ++i) v += bx[a + q*i]*x[offset + i + dx*(j + dy*k)];
sx[a + q*(j + dy*k)] = v;
}
for (int k = 0; k < dz; ++k) for (int b = 0; b < q; ++b) for (int a = 0; a < q; ++a) {
double v = 0.0;
for (int j = 0; j < dy; ++j) v += by[b + q*j]*sx[a + q*(j + dy*k)];
sxy[a + q*(b + q*k)] = v;
}
for (int z = 0; z < q; ++z) for (int b = 0; b < q; ++b) for (int a = 0; a < q; ++a) {
double v = 0.0;
for (int k = 0; k < dz; ++k) v += bz[z + q*k]*sxy[a + q*(b + q*k)];
values[a + q*(b + q*z) + nq*c] = v;
}
offset += dx*dy*dz;
}
const double *metric = op + e*nq*entries;
for (int p = 0; p < nq; ++p) {
const double u = values[p], v = values[p+nq], w = values[p+2*nq];
const double a00 = metric[p], a01 = metric[p+nq], a02 = metric[p+2*nq];
const double a10 = symmetric ? a01 : metric[p+3*nq];
const double a11 = metric[p+(symmetric ? 3 : 4)*nq];
const double a12 = metric[p+(symmetric ? 4 : 5)*nq];
const double a20 = symmetric ? a02 : metric[p+6*nq];
const double a21 = symmetric ? a12 : metric[p+7*nq];
const double a22 = metric[p+(symmetric ? 5 : 8)*nq];
values[p] = a00*u + a01*v + a02*w;
values[p+nq] = a10*u + a11*v + a12*w;
values[p+2*nq] = a20*u + a21*v + a22*w;
}
offset = e*nd;
for (int c = 0; c < 3; ++c) {
const int dx = c == 0 ? d : d-1, dy = c == 1 ? d : d-1, dz = c == 2 ? d : d-1;
const double *bx = c == 0 ? bc : bo, *by = c == 1 ? bc : bo, *bz = c == 2 ? bc : bo;
for (int k = 0; k < dz; ++k) for (int b = 0; b < q; ++b) for (int a = 0; a < q; ++a) {
double v = 0.0;
for (int z = 0; z < q; ++z) v += bz[z + q*k]*values[a + q*(b + q*z) + nq*c];
sxy[a + q*(b + q*k)] = v;
}
for (int k = 0; k < dz; ++k) for (int j = 0; j < dy; ++j) for (int a = 0; a < q; ++a) {
double v = 0.0;
for (int b = 0; b < q; ++b) v += by[b + q*j]*sxy[a + q*(b + q*k)];
sx[a + q*(j + dy*k)] = v;
}
for (int k = 0; k < dz; ++k) for (int j = 0; j < dy; ++j) for (int i = 0; i < dx; ++i) {
double v = 0.0;
for (int a = 0; a < q; ++a) v += bx[a + q*i]*sx[a + q*(j + dy*k)];
y[offset + i + dx*(j + dy*k)] += v;
}
offset += dx*dy*dz;
}
}
}
mfem::Array<int> m_marker;
std::vector<int> m_activeElements;
mutable mfem::Vector m_workspace;
bool m_supported{false};
mutable bool m_useFast{true};
};
class FrozenMappedHDivMassCoefficient final : public mfem::MatrixCoefficient {
public:
FrozenMappedHDivMassCoefficient(
@@ -520,6 +640,11 @@ namespace mean_field::operators {
mfem::Vector elementDisplacement;
mfem::Vector elementCompactification;
mapping::VolumeMappingContext mappingContext;
mfem::DenseMatrix inverseMeshJacobian(3), compactificationGradients;
mfem::Vector coordinateGradient(3), scaleGradient(3);
const auto *kelvin = dynamic_cast<const mapping::compactification::KelvinCompactification *>(
&m_domain_mapper.GetExteriorMap()
);
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
m_variationElements.emplace_back();
@@ -566,6 +691,24 @@ namespace mean_field::operators {
data.integrationRule->GetNPoints(), frozen_mapping_width(m_domain_mapper.GetDimension())
);
const bool compactified = m_domain_mapper.IsCompactifiedElement(*transformation);
std::shared_ptr<const fem::ScalarReferenceTable> compactificationTable;
if (dimension == 3 && data.gravityReferenceTable != nullptr &&
displacementElement.GetMapType() == mfem::FiniteElement::VALUE &&
displacementElement.GetDerivType() == mfem::FiniteElement::GRAD &&
displacementElement.GetDim() == 3 &&
(!compactified || (kelvin != nullptr &&
compactificationElement.GetDerivType() == mfem::FiniteElement::GRAD))) {
data.displacementReferenceTable =
m_fem.GetReferenceTables().GetScalarTable(displacementElement, *data.integrationRule);
data.tangentFactors.SetSize(12, data.integrationRule->GetNPoints());
if (compactified) {
compactificationTable = m_fem.GetReferenceTables().GetScalarTable(
compactificationElement, *data.integrationRule
);
}
}
const mapping::ElementDisplacementData displacementData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, data.baseDisplacement);
const mapping::ElementCompactificationData compactificationData(
@@ -584,11 +727,42 @@ namespace mean_field::operators {
return status;
}
freeze_mapping_context(mappingContext, quadraturePoint, data.frozenMappingData);
if (data.displacementReferenceTable != nullptr) {
// Use the same fixed-mesh inverse as EvaluateVolumeVariation.
mfem::Mult(mappingContext.quadrature.J_inv, mappingContext.mapping.mapping_jacobian,
inverseMeshJacobian);
double scale = 1.0;
scaleGradient = 0.0;
if (compactified) {
const auto &values = compactificationTable->GetValues();
double coordinate = 0.0;
for (int dof = 0; dof < data.compactification.Size(); ++dof)
coordinate += values(quadraturePoint, dof) * data.compactification(dof);
compactificationGradients.SetSize(compactificationElement.GetDof(), dimension);
mfem::Mult(compactificationTable->GetGradients(quadraturePoint), inverseMeshJacobian,
compactificationGradients);
compactificationGradients.MultTranspose(data.compactification, coordinateGradient);
const auto factorStatus = kelvin->EvaluateJacobianVariationFactors(
coordinate, coordinateGradient, scale, scaleGradient
);
if (factorStatus != mapping::MappingStatus::valid) return factorStatus;
}
double *factors = data.tangentFactors.GetColumn(quadraturePoint);
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column) {
const double value = scale * inverseMeshJacobian(row, column);
if (!std::isfinite(value)) return mapping::MappingStatus::non_finite_result;
factors[3 * row + column] = value;
}
factors[9 + row] = scaleGradient(row);
}
}
if (data.gravityReferenceTable != nullptr) {
// CalcVShape_RT = reference_shape * J_mesh^T / Weight.
// Cache only this small factor, never the mapped basis.
const double meshWeight = transformation->Weight();
const mfem::DenseMatrix &meshJacobian = transformation->Jacobian();
const auto *meshGeometry = m_domain_mapper.GetFixedMeshGeometry(*transformation, integrationPoint);
const double meshWeight = meshGeometry ? meshGeometry->weight : transformation->Weight();
const mfem::DenseMatrix &meshJacobian = meshGeometry ? meshGeometry->jacobian : transformation->Jacobian();
const double inverseMeshWeight = 1.0 / meshWeight;
data.referenceWeights(quadraturePoint) = integrationPoint.weight * meshWeight;
for (int row = 0; row < dimension; ++row) {
@@ -693,8 +867,11 @@ namespace mean_field::operators {
m_stellar_mass_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
m_vacuum_mass_form->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
auto stellar_integrator = std::make_unique<mfem::VectorFEMassIntegrator>(*m_stellar_mass_coefficient);
auto vacuum_integrator = std::make_unique<mfem::VectorFEMassIntegrator>(*m_vacuum_mass_coefficient);
auto stellar_integrator = std::make_unique<TensorHDivMassIntegrator>(*m_stellar_mass_coefficient, m_stellar_marker);
auto vacuum_integrator = std::make_unique<TensorHDivMassIntegrator>(*m_vacuum_mass_coefficient, m_vacuum_marker);
auto *stellarIntegratorView = stellar_integrator.get();
auto *vacuumIntegratorView = vacuum_integrator.get();
m_fastMassElements = 0;
m_fem.quadratureFactory->configure_gravity_hdiv_mass(
*stellar_integrator, quadrature::QuadratureRole::discretization, stellar_element, stellar_transformation,
@@ -731,6 +908,7 @@ namespace mean_field::operators {
return preparationResult;
}
m_fastMassElements = stellarIntegratorView->FastElementCount() + vacuumIntegratorView->FastElementCount();
m_has_variation_data = mode == PreparationMode::linearization;
m_is_prepared = true;
++m_preparation_count;
@@ -791,10 +969,71 @@ namespace mean_field::operators {
}
}
void PreparedMappedHDivMassOperator::MultReference(
const mfem::Vector &gravityGradient, mfem::Vector &action
) const {
MFEM_VERIFY(m_is_prepared && m_stellar_mass_form && m_vacuum_mass_form,
"The H(div) mass reference requires a prepared operator.");
const auto *stellar = dynamic_cast<const TensorHDivMassIntegrator *>((*m_stellar_mass_form->GetDBFI())[0]);
const auto *vacuum = dynamic_cast<const TensorHDivMassIntegrator *>((*m_vacuum_mass_form->GetDBFI())[0]);
MFEM_VERIFY(stellar && vacuum, "The H(div) mass forms have unexpected integrators.");
struct RestoreMode {
const TensorHDivMassIntegrator *integrator;
bool previous;
~RestoreMode() { integrator->SetFastApplication(previous); }
} restoreStellar{stellar, stellar->UseFastApplication()}, restoreVacuum{vacuum, vacuum->UseFastApplication()};
stellar->SetFastApplication(false);
vacuum->SetFastApplication(false);
Mult(gravityGradient, action);
}
void PreparedMappedHDivMassOperator::MultDisplacementVariationTrue(
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const {
MEAN_FIELD_PROFILE_SCOPE("PreparedMappedHDivMassOperator::MultDisplacementVariationTrue");
MultDisplacementVariationTrueImpl(gravityGradientTrue, displacementVariationTrue, actionVariationTrue, true, true);
}
void PreparedMappedHDivMassOperator::MultDisplacementVariationTrueReference(
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const {
MEAN_FIELD_PROFILE_SCOPE("PreparedMappedHDivMassOperator::MultDisplacementVariationTrueReference");
MultDisplacementVariationTrueImpl(gravityGradientTrue, displacementVariationTrue, actionVariationTrue, false, false);
}
void PreparedMappedHDivMassOperator::MultDisplacementVariationTrueCachedReference(
const mfem::Vector &gravityGradientTrue, const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const {
MultDisplacementVariationTrueImpl(gravityGradientTrue, displacementVariationTrue, actionVariationTrue, true, false);
}
int PreparedMappedHDivMassOperator::GetTensorVariationElementCount() const noexcept {
if (!m_is_prepared || !m_has_variation_data) return 0;
int count = 0;
for (const auto &data : m_variationElements)
if (data.displacementReferenceTable && data.displacementReferenceTable->HasTensorEvaluation3D() &&
data.gravityReferenceTable && data.gravityReferenceTable->HasTensorEvaluation3D()) ++count;
return count;
}
int PreparedMappedHDivMassOperator::GetFastVariationElementCount() const noexcept {
if (!m_is_prepared || !m_has_variation_data) return 0;
int count = 0;
for (const auto &data : m_variationElements)
if (data.displacementReferenceTable != nullptr) ++count;
return count;
}
void PreparedMappedHDivMassOperator::MultDisplacementVariationTrueImpl(
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue,
const bool useFastPath, const bool useTensorPath
) const {
MFEM_VERIFY(
m_is_prepared, "PreparedMappedHDivMassOperator must be prepared before applying a displacement variation."
@@ -837,6 +1076,11 @@ namespace mean_field::operators {
data.displacementDofTransformation->InvTransformPrimal(m_elementDisplacementVariation);
}
if (useFastPath && data.displacementReferenceTable != nullptr) {
AddFastElementVariation(data, useTensorPath);
continue;
}
const mfem::FiniteElement &gravityGradientElement = *m_fem.gravityFluxFes->GetFE(data.elementId);
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(data.elementId);
@@ -924,6 +1168,111 @@ namespace mean_field::operators {
local_to_true(*m_fem.gravityFluxFes, m_localVariationAction, actionVariationTrue);
}
void PreparedMappedHDivMassOperator::AddFastElementVariation(
const ElementVariationData &data, const bool useTensorPath
) const {
// GetElementVDofs always returns component blocks, independently of the
// global FES ordering. Signed DOFs and primal transforms were applied by
// the caller; preserve the dual transform and scatter below.
const int dofs = data.displacementReferenceTable->GetDofCount();
MFEM_VERIFY(m_elementDisplacementVariation.Size() == 3 * dofs,
"Prepared H(div) tangent has an incompatible displacement layout.");
const auto &values = data.displacementReferenceTable->GetValues();
const bool compactified = data.frozenMappingData(0, 47) != 0.0;
m_elementVariationAction.SetSize(m_elementGravityGradient.Size());
m_elementVariationAction = 0.0;
m_gravityReferenceCellValue.SetSize(3);
m_referenceCellDual.SetSize(3);
const bool tensorDisplacement = useTensorPath && data.displacementReferenceTable->HasTensorEvaluation3D();
const bool tensorGravity = useTensorPath && data.gravityReferenceTable->HasTensorEvaluation3D();
if (tensorDisplacement) {
const mfem::DenseMatrix direction(m_elementDisplacementVariation.GetData(), dofs, 3);
data.displacementReferenceTable->EvaluateTensor3D(direction, m_tensorDisplacement, m_tensorWorkspace);
}
if (tensorGravity) {
data.gravityReferenceTable->EvaluateTensor3D(m_elementGravityGradient, m_tensorGravity, m_tensorWorkspace);
m_tensorDual.SetSize(data.integrationRule->GetNPoints(), 3);
}
for (int q = 0; q < data.integrationRule->GetNPoints(); ++q) {
const auto &gradients = data.displacementReferenceTable->GetGradients(q);
const double *factors = data.tangentFactors.GetColumn(q);
double deltaJ[3][3]{};
for (int component = 0; component < 3; ++component) {
const double *coefficients = m_elementDisplacementVariation.GetData() + component * dofs;
double value = 0.0;
double referenceGradient[3]{};
if (tensorDisplacement) {
value = compactified ? m_tensorDisplacement(q, 4*component) : 0.0;
for (int axis = 0; axis < 3; ++axis)
referenceGradient[axis] = m_tensorDisplacement(q, 4*component + 1 + axis);
} else {
for (int dof = 0; dof < dofs; ++dof) {
const double coefficient = coefficients[dof];
if (compactified) value += coefficient * values(q, dof);
for (int axis = 0; axis < 3; ++axis)
referenceGradient[axis] += coefficient * gradients(dof, axis);
}
}
for (int column = 0; column < 3; ++column) {
deltaJ[component][column] = value * factors[9 + column];
for (int axis = 0; axis < 3; ++axis)
deltaJ[component][column] += referenceGradient[axis] * factors[3 * axis + column];
MFEM_VERIFY(std::isfinite(deltaJ[component][column]),
"Prepared H(div) tangent produced a non-finite mapping derivative.");
}
}
const auto &referenceShape = data.gravityReferenceTable->GetValues(q);
if (tensorGravity) {
for (int c = 0; c < 3; ++c) m_gravityReferenceCellValue(c) = m_tensorGravity(q, c);
} else referenceShape.MultTranspose(m_elementGravityGradient, m_gravityReferenceCellValue);
double piola[3][3], jacobian[3][3];
double gravity[3]{}, mappedGravity[3]{}, mappedVariation[3]{};
double trace = 0.0;
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column) {
piola[row][column] = data.meshPiolaJacobians(q, 3 * row + column);
jacobian[row][column] = data.frozenMappingData(q, 18 + 3 * row + column);
gravity[row] += piola[row][column] * m_gravityReferenceCellValue(column);
trace += data.frozenMappingData(q, 27 + 3 * row + column) * deltaJ[column][row];
}
}
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column) {
mappedGravity[row] += jacobian[row][column] * gravity[column];
mappedVariation[row] += deltaJ[row][column] * gravity[column];
}
}
// M = J^T J / det(J). Apply delta M directly to the three-vector:
// [delta J^T (Jg) + J^T(delta J g - tr(J^-1 delta J) Jg)] / det(J).
const double inverseDeterminant = 1.0 / data.frozenMappingData(q, 45);
double massAction[3]{};
for (int column = 0; column < 3; ++column) {
for (int row = 0; row < 3; ++row) {
massAction[column] += deltaJ[row][column] * mappedGravity[row] +
jacobian[row][column] * (mappedVariation[row] - trace * mappedGravity[row]);
}
massAction[column] *= inverseDeterminant;
MFEM_VERIFY(std::isfinite(massAction[column]),
"Prepared H(div) tangent produced a non-finite mass action.");
}
for (int column = 0; column < 3; ++column) {
double dual = 0.0;
for (int row = 0; row < 3; ++row) dual += piola[row][column] * massAction[row];
m_referenceCellDual(column) = dual;
}
if (tensorGravity) {
for (int c = 0; c < 3; ++c) m_tensorDual(q, c) = data.referenceWeights(q) * m_referenceCellDual(c);
} else referenceShape.AddMult(m_referenceCellDual, m_elementVariationAction, data.referenceWeights(q));
}
if (tensorGravity)
data.gravityReferenceTable->ApplyTransposeTensor3D(m_tensorDual, m_elementVariationAction, m_tensorWorkspace);
if (data.gravityGradientDofTransformation != nullptr)
data.gravityGradientDofTransformation->TransformDual(m_elementVariationAction);
m_localVariationAction.AddElementVector(data.gravityGradientDofs, m_elementVariationAction);
}
void PreparedMappedHDivMassOperator::AssembleDiagonal(mfem::Vector &diagonal) const {
mfem::Vector true_diagonal;
AssembleTrueDiagonal(true_diagonal);

View File

@@ -11,6 +11,7 @@ module;
#include <utility>
#include <mfem.hpp>
#include "interior_displacement_contractions.hpp"
module mean_field;
@@ -384,7 +385,30 @@ namespace mean_field::operators {
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
const bool wasPrepared = m_isPrepared;
return TryPrepareImpl(state, dependencies, rotation, true);
}
HydrostaticEquilibriumPreparationResult PreparedHydrostaticEquilibriumOperator::TryPrepareReference(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
return TryPrepareImpl(state, dependencies, rotation, false);
}
PreparedHydrostaticEquilibriumReport PreparedHydrostaticEquilibriumOperator::PrepareReference(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation
) {
auto result = TryPrepareReference(state, dependencies, rotation);
if (!result.has_value()) throwHydrostaticEquilibriumPreparationRejection(result.error());
return std::move(result).value();
}
HydrostaticEquilibriumPreparationResult PreparedHydrostaticEquilibriumOperator::TryPrepareImpl(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation, const bool useTensorPreparation
) {
const bool wasPrepared = m_isPrepared && m_preparedWithTensorPreparation == useTensorPreparation;
const bool rotationObjectChanged =
!wasPrepared || !m_context.IsPrepared() || dependencies.rotation != m_context.GetDependencies().rotation;
@@ -409,7 +433,7 @@ namespace mean_field::operators {
}
if (report.contextReport.preparedGeometryState || !wasPrepared) {
const auto mappingFailure = synchronize_mapping_failure(PrepareGeometry(), m_fem.mesh->GetComm());
const auto mappingFailure = synchronize_mapping_failure(PrepareGeometry(useTensorPreparation), m_fem.mesh->GetComm());
if (mappingFailure.has_value()) {
const auto reason = *mappingFailure == mapping::MappingStatus::non_positive_determinant
? HydrostaticEquilibriumPreparationRejectionReason::inverted_geometry
@@ -419,7 +443,7 @@ namespace mean_field::operators {
);
}
if (synchronize_non_finite_failure(PrepareAlgebraicJacobianBlocks(), m_fem.mesh->GetComm())) {
if (synchronize_non_finite_failure(PrepareAlgebraicJacobianBlocks(useTensorPreparation), m_fem.mesh->GetComm())) {
return std::unexpected(
HydrostaticEquilibriumPreparationRejection{
.reason = HydrostaticEquilibriumPreparationRejectionReason::non_finite_geometry,
@@ -441,7 +465,7 @@ namespace mean_field::operators {
}
if (report.contextReport.preparedBaseState || !wasPrepared) {
if (synchronize_non_finite_failure(PrepareBaseState(), m_fem.mesh->GetComm())) {
if (synchronize_non_finite_failure(PrepareBaseState(useTensorPreparation), m_fem.mesh->GetComm())) {
return std::unexpected(
HydrostaticEquilibriumPreparationRejection{
.reason = HydrostaticEquilibriumPreparationRejectionReason::non_finite_residual
@@ -480,6 +504,7 @@ namespace mean_field::operators {
++m_residualPreparationCount;
}
m_preparedWithTensorPreparation = useTensorPreparation;
m_isPrepared = true;
return report;
}
@@ -529,10 +554,18 @@ namespace mean_field::operators {
data.enthalpyReferenceTable = referenceTables.GetScalarTable(enthalpyElement, *data.integrationRule);
data.gravityPotentialReferenceTable =
referenceTables.GetScalarTable(gravityPotentialElement, *data.integrationRule);
const auto &displacementElement = *m_fem.displacementFes->GetFE(elementId);
if (m_fem.mesh->Dimension() == 3 &&
displacementElement.GetMapType() == mfem::FiniteElement::VALUE &&
displacementElement.GetDerivType() == mfem::FiniteElement::GRAD &&
!m_domainMapper.IsCompactifiedElement(*transformation)) {
data.displacementReferenceTable =
referenceTables.GetScalarTable(displacementElement, *data.integrationRule);
}
}
}
std::optional<mapping::MappingStatus> PreparedHydrostaticEquilibriumOperator::PrepareGeometry() {
std::optional<mapping::MappingStatus> PreparedHydrostaticEquilibriumOperator::PrepareGeometry(const bool useTensorPreparation) {
mfem::Vector displacementLocal;
true_to_local(*m_fem.displacementFes, m_context.GetDisplacementTrue(), displacementLocal);
@@ -544,6 +577,8 @@ namespace mean_field::operators {
mfem::Vector elementDisplacement;
mfem::Vector elementCompactification;
mfem::Vector sampleValue(3), tensorWorkspace;
mfem::DenseMatrix sampleGradient(3), tensorSamples;
for (ElementPAData &data : m_elements) {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(data.elementId);
@@ -590,14 +625,27 @@ namespace mean_field::operators {
data.quadratureWeights.SetSize(quadraturePointCount);
data.baseMappingContexts.SetSize(quadraturePointCount, m_fem.mesh->Dimension());
if (data.displacementReferenceTable != nullptr)
data.displacementInverseJacobians.SetSize(9, quadraturePointCount);
const bool tensor = useTensorPreparation && data.displacementReferenceTable &&
data.displacementReferenceTable->HasTensorEvaluation3D();
if (tensor) data.displacementReferenceTable->EvaluateTensor3D(
data.baseDisplacementData->GetDofMatrix(), tensorSamples, tensorWorkspace);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
transformation->SetIntPoint(&integrationPoint);
if (tensor) for (int c = 0; c < 3; ++c) {
sampleValue(c) = tensorSamples(quadraturePoint, 4*c);
for (int a = 0; a < 3; ++a)
sampleGradient(c,a) = tensorSamples(quadraturePoint, 4*c+a+1);
}
const mapping::DomainMapper::ReferenceDisplacementSample sample{sampleValue, sampleGradient};
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
mappingData, *transformation, integrationPoint, workspace, mappingContext, tensor ? &sample : nullptr
);
if (mappingStatus != mapping::MappingStatus::valid) {
@@ -615,6 +663,16 @@ namespace mean_field::operators {
data.baseMappingContexts.Store(quadraturePoint, mappingContext);
data.quadratureWeights(quadraturePoint) = quadratureWeight;
if (data.displacementInverseJacobians.Width() != 0) {
if (mappingContext.mapping.compactified) {
data.displacementInverseJacobians.SetSize(0, 0);
} else {
double *inverse = data.displacementInverseJacobians.Data() + 9 * quadraturePoint;
for (int c = 0; c < 3; ++c)
for (int r = 0; r < 3; ++r)
inverse[r + 3 * c] = mappingContext.quadrature.J_inv(r, c);
}
}
for (int component = 0; component < m_fem.mesh->Dimension(); ++component) {
const double position = mappingContext.mapping.physical_position(component);
@@ -631,7 +689,8 @@ namespace mean_field::operators {
return std::nullopt;
}
bool PreparedHydrostaticEquilibriumOperator::PrepareAlgebraicJacobianBlocks() {
bool PreparedHydrostaticEquilibriumOperator::PrepareAlgebraicJacobianBlocks(const bool useTensorPreparation) {
mfem::Vector dual, column, workspace;
for (ElementPAData &data : m_elements) {
const mfem::DenseMatrix &enthalpyBasis = data.GetEnthalpyBasis();
const mfem::DenseMatrix &gravityPotentialBasis = data.GetGravityPotentialBasis();
@@ -658,7 +717,21 @@ namespace mean_field::operators {
data.gravityPotentialJacobian = 0.0;
data.bernoulliConstantJacobian = 0.0;
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
if (useTensorPreparation && data.enthalpyReferenceTable->HasTensorEvaluation3D()) {
dual.SetSize(quadraturePointCount);
auto assemble = [&](const mfem::DenseMatrix &basis, mfem::DenseMatrix &matrix) {
for (int j = 0; j < basis.Width(); ++j) {
for (int q = 0; q < quadraturePointCount; ++q)
dual(q) = data.quadratureWeights(q)*basis(q,j);
data.enthalpyReferenceTable->ApplyTransposeTensor3D(dual, column, workspace);
for (int i = 0; i < enthalpyDofCount; ++i) matrix(i,j) = column(i);
}
};
assemble(enthalpyBasis, data.enthalpyJacobian);
assemble(gravityPotentialBasis, data.gravityPotentialJacobian);
for (int q = 0; q < quadraturePointCount; ++q) dual(q) = -data.quadratureWeights(q);
data.enthalpyReferenceTable->ApplyTransposeTensor3D(dual, data.bernoulliConstantJacobian, workspace);
} else for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const double quadratureWeight = data.quadratureWeights(quadraturePoint);
for (int testDof = 0; testDof < enthalpyDofCount; ++testDof) {
@@ -738,7 +811,7 @@ namespace mean_field::operators {
return false;
}
bool PreparedHydrostaticEquilibriumOperator::PrepareBaseState() {
bool PreparedHydrostaticEquilibriumOperator::PrepareBaseState(const bool useTensorPreparation) {
mfem::Vector enthalpyLocal;
mfem::Vector gravityPotentialLocal;
@@ -750,6 +823,7 @@ namespace mean_field::operators {
mfem::Vector elementGravityPotential;
mfem::Vector quadratureEnthalpy;
mfem::Vector quadratureGravityPotential;
mfem::Vector workspace;
for (ElementPAData &data : m_elements) {
enthalpyLocal.GetSubVector(data.enthalpyDofs, elementEnthalpy);
@@ -770,9 +844,12 @@ namespace mean_field::operators {
quadratureGravityPotential.SetSize(quadraturePointCount);
data.GetEnthalpyBasis().Mult(elementEnthalpy, quadratureEnthalpy);
data.GetGravityPotentialBasis().Mult(elementGravityPotential, quadratureGravityPotential);
if (useTensorPreparation && data.enthalpyReferenceTable->HasTensorEvaluation3D())
data.enthalpyReferenceTable->EvaluateValuesTensor3D(elementEnthalpy, quadratureEnthalpy, workspace);
else data.GetEnthalpyBasis().Mult(elementEnthalpy, quadratureEnthalpy);
if (useTensorPreparation && data.gravityPotentialReferenceTable->HasTensorEvaluation3D())
data.gravityPotentialReferenceTable->EvaluateValuesTensor3D(elementGravityPotential, quadratureGravityPotential, workspace);
else data.GetGravityPotentialBasis().Mult(elementGravityPotential, quadratureGravityPotential);
MFEM_VERIFY(
data.rotationPotential.Size() == quadraturePointCount, "Prepared hydrostatic base state has stale "
@@ -1156,6 +1233,38 @@ namespace mean_field::operators {
void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
ApplyDisplacementJacobianActionImpl(displacementVariation, action, true, true);
}
void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianActionReference(
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
ApplyDisplacementJacobianActionImpl(displacementVariation, action, false, false);
}
void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianActionCachedReference(
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
ApplyDisplacementJacobianActionImpl(displacementVariation, action, true, false);
}
int PreparedHydrostaticEquilibriumOperator::GetTensorDisplacementElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements)
count += data.displacementInverseJacobians.Width() != 0 && data.displacementReferenceTable != nullptr &&
data.displacementReferenceTable->HasTensorEvaluation3D();
return count;
}
int PreparedHydrostaticEquilibriumOperator::GetFastDisplacementElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements) count += data.displacementInverseJacobians.Width() != 0;
return count;
}
void PreparedHydrostaticEquilibriumOperator::ApplyDisplacementJacobianActionImpl(
const mfem::Vector &displacementVariation, mfem::Vector &action, const bool useFastPath, const bool useTensorPath
) const {
VerifyPrepared();
@@ -1226,8 +1335,42 @@ namespace mean_field::operators {
);
weightedQuadratureVariation.SetSize(quadraturePointCount);
const bool tensor = useFastPath && useTensorPath && data.displacementInverseJacobians.Width() != 0 &&
data.displacementReferenceTable->HasTensorEvaluation3D();
if (tensor) data.displacementReferenceTable->EvaluateTensor3D(
directionData.GetDofMatrix(), m_tensorDisplacementValuesAndGradients, m_tensorDisplacementWorkspace);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
if (tensor) {
const double *inverse = data.displacementInverseJacobians.Data() + 9 * quadraturePoint;
double trace = 0.0, position = 0.0;
for (int c = 0; c < 3; ++c) {
position += data.rotationGradient(quadraturePoint, c) *
m_tensorDisplacementValuesAndGradients(quadraturePoint, 4 * c);
for (int derivative = 0; derivative < 3; ++derivative)
trace += inverse[derivative + 3 * c] *
m_tensorDisplacementValuesAndGradients(quadraturePoint, 4 * c + 1 + derivative);
}
const double value = data.quadratureWeights(quadraturePoint) *
(data.hydrostaticImbalance(quadraturePoint) * trace - position);
MFEM_VERIFY(std::isfinite(value), "Prepared hydrostatic tensor action is non-finite.");
weightedQuadratureVariation(quadraturePoint) = value;
continue;
}
if (useFastPath && data.displacementInverseJacobians.Width() != 0) {
const double weight = data.quadratureWeights(quadraturePoint);
double positionCoefficient[3];
for (int c = 0; c < 3; ++c)
positionCoefficient[c] = -weight * data.rotationGradient(quadraturePoint, c);
const double value = apply_interior_functional(
directionData.GetDofMatrix(), data.displacementReferenceTable->GetGradients(quadraturePoint),
data.displacementReferenceTable->GetValues(), quadraturePoint,
data.displacementInverseJacobians.Data() + 9 * quadraturePoint,
weight * data.hydrostaticImbalance(quadraturePoint), positionCoefficient);
MFEM_VERIFY(std::isfinite(value), "Prepared hydrostatic displacement action is non-finite.");
weightedQuadratureVariation(quadraturePoint) = value;
continue;
}
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
data.baseMappingContexts.Load(quadraturePoint, mappingContext);
@@ -1279,7 +1422,7 @@ namespace mean_field::operators {
action.SetSize(m_context.GetEnthalpyMap().reduced_size());
m_context.GetEnthalpyMap().gather(m_fullEnthalpyAction, action);
++m_displacementJacobianStatistics.applications;
if (useFastPath && useTensorPath) ++m_displacementJacobianStatistics.applications;
}
void PreparedHydrostaticEquilibriumOperator::ApplyCompleteJacobianAction(

View File

@@ -9,6 +9,7 @@ module;
#include <utility>
#include <mpi.h>
#include "interior_displacement_contractions.hpp"
module mean_field;
@@ -321,6 +322,7 @@ namespace mean_field::operators {
const bool updateTargetMass = !m_isPrepared || dependencies.targetMass != m_preparedDependencies.targetMass ||
state.targetMass != m_targetMass;
if (refreshGeometry || refreshDensity) m_jacobianRowsReady = false;
m_isPrepared = false;
PreparedMassNormalizationReport report;
@@ -603,7 +605,83 @@ namespace mean_field::operators {
++m_residualApplicationCount;
}
double PreparedMassNormalizationOperator::EvaluateDensityActionLocal(const mfem::Vector &densityVariation) const {
bool PreparedMassNormalizationOperator::PrepareJacobianRows() const {
VerifyPrepared();
if (m_jacobianRowsReady) return m_jacobianRowsSupported;
m_jacobianRowsSupported = false;
const int dimension = m_fem.mesh->Dimension();
for (const ElementPAData &data : m_elements) {
const auto &element = *m_fem.displacementFes->GetFE(data.elementId);
if (dimension < 1 || dimension > 3 || element.GetMapType() != mfem::FiniteElement::VALUE ||
element.GetDerivType() != mfem::FiniteElement::GRAD ||
m_domainMapper.IsCompactifiedElement(*m_fem.mesh->GetElementTransformation(data.elementId))) {
m_jacobianRowsReady = true;
return false;
}
}
m_densityJacobianLocal.SetSize(m_fem.densityFes->GetVSize());
m_displacementJacobianLocal.SetSize(m_fem.displacementFes->GetVSize());
m_densityJacobianLocal = 0.0;
m_displacementJacobianLocal = 0.0;
mfem::Vector densityRow, displacementRow;
mapping::VolumeMappingContext mappingContext;
constexpr double zeroPosition[3]{0.0, 0.0, 0.0};
for (const ElementPAData &data : m_elements) {
densityRow.SetSize(data.densityDofs.Size());
data.densityBasis->GetValues().MultTranspose(data.quadratureWeights, densityRow);
displacementRow.SetSize(data.displacementDofs.Size());
displacementRow = 0.0;
const auto basis = m_fem.GetReferenceTables().GetScalarTable(
*m_fem.displacementFes->GetFE(data.elementId), *data.integrationRule);
for (int q = 0; q < data.integrationRule->GetNPoints(); ++q) {
data.mappingContexts.Load(q, mappingContext);
add_interior_functional_row(
basis->GetGradients(q), basis->GetValues(), q, mappingContext.quadrature.J_inv,
data.density(q) * data.quadratureWeights(q), zeroPosition, displacementRow);
}
if (data.densityDofTransformation) data.densityDofTransformation->TransformDual(densityRow);
if (data.displacementDofTransformation) data.displacementDofTransformation->TransformDual(displacementRow);
m_densityJacobianLocal.AddElementVector(data.densityDofs, densityRow);
m_displacementJacobianLocal.AddElementVector(data.displacementDofs, displacementRow);
}
validate_finite_vector(m_densityJacobianLocal, "Prepared mass density row is non-finite.");
validate_finite_vector(m_displacementJacobianLocal, "Prepared mass displacement row is non-finite.");
m_jacobianRowsSupported = true;
m_jacobianRowsReady = true;
++m_jacobianRowPreparationCount;
return true;
}
std::uint64_t PreparedMassNormalizationOperator::GetJacobianRowPreparationCount() const noexcept {
return m_jacobianRowPreparationCount;
}
double PreparedMassNormalizationOperator::EvaluateDensityActionLocal(const mfem::Vector &direction) const {
if (!PrepareJacobianRows()) return EvaluateDensityActionLocalReference(direction);
true_to_local(*m_fem.densityFes, direction, m_densityVariationLocal);
return m_densityJacobianLocal * m_densityVariationLocal;
}
double PreparedMassNormalizationOperator::EvaluateDisplacementActionLocal(const mfem::Vector &direction) const {
if (!PrepareJacobianRows()) return EvaluateDisplacementActionLocalReference(direction);
true_to_local(*m_fem.displacementFes, direction, m_displacementVariationLocal);
return m_displacementJacobianLocal * m_displacementVariationLocal;
}
void PreparedMassNormalizationOperator::ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation, const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
VerifyPrepared();
validate_finite_vector(densityVariation, "Mass-normalization density direction is non-finite.");
validate_finite_vector(displacementVariation, "Mass-normalization displacement direction is non-finite.");
m_gravityContext.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_gravityContext.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
action.SetSize(1);
action(0) = GlobalSum(EvaluateDensityActionLocalReference(m_densityVariationTrue) +
EvaluateDisplacementActionLocalReference(m_displacementVariationTrue));
}
double PreparedMassNormalizationOperator::EvaluateDensityActionLocalReference(const mfem::Vector &densityVariation) const {
MFEM_VERIFY(
densityVariation.Size() == m_fem.densityFes->GetTrueVSize(),
"Mass-normalization density action received a vector with the "
@@ -635,7 +713,7 @@ namespace mean_field::operators {
return localAction;
}
double PreparedMassNormalizationOperator::EvaluateDisplacementActionLocal(
double PreparedMassNormalizationOperator::EvaluateDisplacementActionLocalReference(
const mfem::Vector &displacementVariation
) const {
MFEM_VERIFY(
@@ -898,8 +976,18 @@ namespace mean_field::operators {
VerifyPrepared();
MFEM_VERIFY(std::isfinite(residualDual), "Mass-normalization transpose action received a non-finite dual.");
AssembleDensityTransposeAction(residualDual, densityDual);
AssembleDisplacementTransposeAction(residualDual, displacementDual);
if (PrepareJacobianRows()) {
mfem::Vector trueDual;
local_to_true(*m_fem.densityFes, m_densityJacobianLocal, trueDual);
densityDual = m_gravityContext.GetDensityMap().gather(trueDual);
densityDual *= residualDual;
local_to_true(*m_fem.displacementFes, m_displacementJacobianLocal, trueDual);
displacementDual = m_gravityContext.GetDisplacementMap().gather(trueDual);
displacementDual *= residualDual;
} else {
AssembleDensityTransposeAction(residualDual, densityDual);
AssembleDisplacementTransposeAction(residualDual, displacementDual);
}
++m_actionStatistics.transposeApplications;
}

View File

@@ -1,5 +1,8 @@
module;
#include "interior_displacement_contractions.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
@@ -406,13 +409,42 @@ namespace mean_field::operators {
PreparedPressureForceOperator::TryPrepare(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
) {
return TryPrepareImpl(state, dependencies, true);
}
PreparedPressureForceReport PreparedPressureForceOperator::PrepareReference(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
) {
auto result = TryPrepareImpl(state, dependencies, false, false);
if (!result.has_value()) throw_rejection(result.error());
return std::move(result).value();
}
PreparedPressureForceReport PreparedPressureForceOperator::PrepareCachedReference(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
) {
auto result = TryPrepareImpl(state, dependencies, true, false);
if (!result.has_value()) throw_rejection(result.error());
return std::move(result).value();
}
std::expected<PreparedPressureForceReport, PressureForcePreparationRejection>
PreparedPressureForceOperator::TryPrepareImpl(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies,
const bool useFastPreparation, const bool useTensorPreparation
) {
const bool wasPrepared = m_isPrepared;
const bool implementationChanged = m_preparedWithFastMaterialAssembly != useFastPreparation ||
m_preparedWithTensorPreparation != useTensorPreparation;
PreparedPressureForceReport report;
report.contextReport = m_context.Prepare(state, dependencies);
if (!report.contextReport.DidAnyWork() && wasPrepared) {
if (!report.contextReport.DidAnyWork() && wasPrepared && !implementationChanged) {
return report;
}
@@ -433,16 +465,16 @@ namespace mean_field::operators {
PrepareStaticPlan();
}
if (report.contextReport.preparedGeometryState || !wasPrepared) {
const auto globalGeometryFailure = synchronize_rejection(PrepareGeometry(), m_fem.enthalpyFes->GetComm());
if (report.contextReport.preparedGeometryState || !wasPrepared || implementationChanged) {
const auto globalGeometryFailure = synchronize_rejection(PrepareGeometry(useTensorPreparation), m_fem.enthalpyFes->GetComm());
if (globalGeometryFailure.has_value()) {
return std::unexpected(*globalGeometryFailure);
}
}
std::optional<PressureForcePreparationRejection> localMaterialFailure;
if (report.contextReport.preparedMaterialState || !wasPrepared) {
localMaterialFailure = PrepareMaterialState();
if (report.contextReport.preparedMaterialState || !wasPrepared || implementationChanged) {
localMaterialFailure = PrepareMaterialState(useFastPreparation, useTensorPreparation);
}
const auto globalMaterialFailure = synchronize_rejection(localMaterialFailure, m_fem.enthalpyFes->GetComm());
@@ -450,7 +482,7 @@ namespace mean_field::operators {
return std::unexpected(*globalMaterialFailure);
}
if (report.contextReport.preparedMaterialState || !wasPrepared) {
if (report.contextReport.preparedMaterialState || !wasPrepared || implementationChanged) {
FinalizeDisplacementJacobianPreparation();
const auto globalAssemblyFailure =
@@ -482,6 +514,8 @@ namespace mean_field::operators {
"supported displacement size."
);
m_preparedWithTensorPreparation = useTensorPreparation;
m_preparedWithFastMaterialAssembly = useFastPreparation;
m_isPrepared = true;
return report;
@@ -564,7 +598,7 @@ namespace mean_field::operators {
}
}
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareGeometry() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareGeometry(const bool useTensorPreparation) {
mfem::Vector displacementLocal;
true_to_local(*m_fem.displacementFes, m_baseDisplacementTrue, displacementLocal);
@@ -574,6 +608,8 @@ namespace mean_field::operators {
mfem::Vector elementDisplacement;
mfem::Vector elementCompactification;
mfem::Vector sampleValue(3), tensorWorkspace;
mfem::DenseMatrix sampleGradient(3), tensorSamples;
for (ElementPAData &data : m_elements) {
MFEM_VERIFY(data.integrationRule != nullptr, "Prepared pressure-force geometry has no integration rule.");
@@ -626,13 +662,24 @@ namespace mean_field::operators {
data.physicalTestGradients.resize(quadraturePointCount);
const bool tensor = useTensorPreparation && data.displacementReferenceTable &&
data.displacementReferenceTable->HasTensorEvaluation3D();
if (tensor) data.displacementReferenceTable->EvaluateTensor3D(
data.baseDisplacementData->GetDofMatrix(), tensorSamples, tensorWorkspace);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
transformation->SetIntPoint(&integrationPoint);
if (tensor) for (int c = 0; c < 3; ++c) {
sampleValue(c) = tensorSamples(quadraturePoint, 4*c);
for (int a = 0; a < 3; ++a)
sampleGradient(c,a) = tensorSamples(quadraturePoint, 4*c+a+1);
}
const mapping::DomainMapper::ReferenceDisplacementSample sample{sampleValue, sampleGradient};
const mapping::MappingStatus mappingStatus = m_domainMapper.EvaluateVolume(
mappingData, *transformation, integrationPoint, workspace, mappingContext
mappingData, *transformation, integrationPoint, workspace, mappingContext, tensor ? &sample : nullptr
);
if (mappingStatus != mapping::MappingStatus::valid) {
@@ -675,7 +722,9 @@ namespace mean_field::operators {
return std::nullopt;
}
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareMaterialState() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareMaterialState(
const bool useFastPreparation, const bool useTensorPreparation
) {
mfem::Vector enthalpyLocal;
true_to_local(*m_fem.enthalpyFes, m_baseEnthalpyTrue, enthalpyLocal);
@@ -687,6 +736,8 @@ namespace mean_field::operators {
const mfem::Ordering::Type displacementOrdering = m_fem.displacementFes->GetOrdering();
std::optional<PressureForcePreparationRejection> materialFailure;
mfem::DenseMatrix weightedGradients;
mfem::DenseMatrix tensorFactors, tensorInverse;
for (ElementPAData &data : m_elements) {
enthalpyLocal.GetSubVector(data.enthalpyDofs, elementEnthalpy);
@@ -717,7 +768,9 @@ namespace mean_field::operators {
quadratureEnthalpy.SetSize(quadraturePointCount);
enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy);
if (useTensorPreparation && data.enthalpyReferenceTable->HasTensorEvaluation3D())
data.enthalpyReferenceTable->EvaluateValuesTensor3D(elementEnthalpy, quadratureEnthalpy, m_tensorWorkspace);
else enthalpyBasis.Mult(elementEnthalpy, quadratureEnthalpy);
data.pressure.SetSize(quadraturePointCount);
@@ -796,14 +849,72 @@ namespace mean_field::operators {
data.elementResidual(vectorDof) -= residualContribution;
for (int enthalpyDof = 0; enthalpyDof < enthalpyDofCount; ++enthalpyDof) {
const double jacobianContribution =
pressureDerivative * weightedTestGradient * enthalpyBasis(quadraturePoint, enthalpyDof);
if (!std::isfinite(jacobianContribution)) {
materialFailure = non_finite_rejection();
continue;
if (!useFastPreparation) {
for (int enthalpyDof = 0; enthalpyDof < enthalpyDofCount; ++enthalpyDof) {
const double jacobianContribution = pressureDerivative * weightedTestGradient *
enthalpyBasis(quadraturePoint, enthalpyDof);
if (!std::isfinite(jacobianContribution)) {
materialFailure = non_finite_rejection();
continue;
}
data.enthalpyJacobian(vectorDof, enthalpyDof) -= jacobianContribution;
}
}
}
}
}
if (useFastPreparation && useTensorPreparation &&
data.displacementReferenceTable->HasTensorEvaluation3D() && !materialFailure.has_value()) {
// Build each enthalpy column by a tensor gradient transpose.
// These are the same -w P'(h) grad(test) B_h entries.
tensorFactors.SetSize(quadraturePointCount, 9);
m_tensorDuals.SetSize(quadraturePointCount, 9);
for (int q = 0; q < quadraturePointCount; ++q) {
data.baseMappingContexts.LoadInverseJacobian(q, tensorInverse);
const double w = -data.pressureDerivative(q)*data.quadratureWeights(q);
for (int c = 0; c < 3; ++c) for (int a = 0; a < 3; ++a)
tensorFactors(q, 3*c+a) = w*tensorInverse(a,c);
}
for (int col = 0; col < enthalpyDofCount; ++col) {
for (int a = 0; a < 9; ++a) for (int q = 0; q < quadraturePointCount; ++q)
m_tensorDuals(q,a) = tensorFactors(q,a)*enthalpyBasis(q,col);
data.displacementReferenceTable->ApplyGradientTransposeTensor3D(
m_tensorDuals, m_tensorElement, m_tensorWorkspace);
for (int c = 0; c < 3; ++c) for (int i = 0; i < scalarDisplacementDofCount; ++i)
data.enthalpyJacobian(i+c*scalarDisplacementDofCount,col) = m_tensorElement(i,c);
}
} else if (useFastPreparation && !materialFailure.has_value()) {
// J_h = -sum_q w_q P'(h_q) grad(test)_q B_h(q).
// Pack once, then accumulate small contiguous output tiles.
// The old loop is retained above as the independent reference.
weightedGradients.SetSize(displacementDofCount, quadraturePointCount);
for (int q = 0; q < quadraturePointCount; ++q) {
for (int c = 0; c < dimension; ++c) {
for (int i = 0; i < scalarDisplacementDofCount; ++i) {
const int row = vector_dof_index(
displacementOrdering, i, c, scalarDisplacementDofCount, dimension);
weightedGradients(row, q) = -data.pressureDerivative(q) *
(data.quadratureWeights(q) * data.physicalTestGradients[q](i, c));
}
}
}
const double *g = weightedGradients.Data();
const double *b = enthalpyBasis.Data();
double *j = data.enthalpyJacobian.Data();
constexpr int rowBlock = 32, columnBlock = 8;
for (int colBegin = 0; colBegin < enthalpyDofCount; colBegin += columnBlock) {
const int colEnd = std::min(colBegin + columnBlock, enthalpyDofCount);
for (int rowBegin = 0; rowBegin < displacementDofCount; rowBegin += rowBlock) {
const int rowEnd = std::min(rowBegin + rowBlock, displacementDofCount);
for (int q = 0; q < quadraturePointCount; ++q) {
const double *gradient = g + q * displacementDofCount;
for (int col = colBegin; col < colEnd; ++col) {
const double basis = b[q + col * quadraturePointCount];
double *column = j + col * displacementDofCount;
for (int row = rowBegin; row < rowEnd; ++row)
column[row] += gradient[row] * basis;
}
data.enthalpyJacobian(vectorDof, enthalpyDof) -= jacobianContribution;
}
}
}
@@ -938,8 +1049,32 @@ namespace mean_field::operators {
}
void PreparedPressureForceOperator::ApplyDisplacementJacobianAction(
const mfem::Vector &displacementVariation,
mfem::Vector &action
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
ApplyDisplacementJacobianActionImpl(displacementVariation, action, true);
}
void PreparedPressureForceOperator::ApplyDisplacementJacobianActionReference(
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
ApplyDisplacementJacobianActionImpl(displacementVariation, action, false, false);
}
void PreparedPressureForceOperator::ApplyDisplacementJacobianActionCachedReference(
const mfem::Vector &displacementVariation, mfem::Vector &action
) const {
ApplyDisplacementJacobianActionImpl(displacementVariation, action, true, false);
}
int PreparedPressureForceOperator::GetTensorElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements)
count += data.displacementReferenceTable->HasTensorEvaluation3D();
return count;
}
void PreparedPressureForceOperator::ApplyDisplacementJacobianActionImpl(
const mfem::Vector &displacementVariation, mfem::Vector &action, const bool useFastPath, const bool useTensorPath
) const {
VerifyPrepared();
@@ -1017,7 +1152,39 @@ namespace mean_field::operators {
matrixTemporary.SetSize(dimension, dimension);
physicalTestGradientVariation.SetSize(scalarDisplacementDofCount, dimension);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const bool tensor = useTensorPath && data.displacementReferenceTable->HasTensorEvaluation3D();
if (tensor) {
data.displacementReferenceTable->EvaluateTensor3D(directionDofs, m_tensorSamples, m_tensorWorkspace);
m_tensorDuals.SetSize(quadraturePointCount, 9);
for (int q = 0; q < quadraturePointCount; ++q) {
data.baseMappingContexts.LoadInverseJacobian(q, inverseElementJacobian);
double gradient[3][3]{};
for (int c = 0; c < 3; ++c) for (int j = 0; j < 3; ++j)
for (int a = 0; a < 3; ++a)
gradient[c][j] += m_tensorSamples(q, 4*c+a+1)*inverseElementJacobian(a,j);
const double trace = gradient[0][0]+gradient[1][1]+gradient[2][2];
const double weight = data.pressure(q)*data.quadratureWeights(q);
// d(-P grad(test) dx): grad_ref(test) J^-1 (du/dx - tr(du/dx) I).
for (int c = 0; c < 3; ++c) for (int a = 0; a < 3; ++a) {
double value = 0.0;
for (int j = 0; j < 3; ++j)
value += inverseElementJacobian(a,j)*(gradient[j][c]-(j==c ? trace : 0.0));
m_tensorDuals(q, 3*c+a) = weight*value;
}
}
data.displacementReferenceTable->ApplyGradientTransposeTensor3D(
m_tensorDuals, m_tensorElement, m_tensorWorkspace);
for (int c = 0; c < 3; ++c) for (int i = 0; i < scalarDisplacementDofCount; ++i)
elementAction(i+c*scalarDisplacementDofCount) = m_tensorElement(i,c);
}
for (int quadraturePoint = 0; !tensor && quadraturePoint < quadraturePointCount; ++quadraturePoint) {
if (useFastPath && dimension == 3) {
pressure_displacement_action_3d(
directionDofs, data.physicalTestGradients[quadraturePoint],
data.pressure(quadraturePoint) * data.quadratureWeights(quadraturePoint), elementAction);
continue;
}
const mfem::DenseMatrix &referenceTestGradient =
data.displacementReferenceTable->GetGradients(quadraturePoint);
@@ -1076,7 +1243,7 @@ namespace mean_field::operators {
m_displacementMap.gather(m_fullDisplacementAction, action);
++m_displacementJacobianStatistics.applications;
if (useFastPath) ++m_displacementJacobianStatistics.applications;
}
void PreparedPressureForceOperator::ApplyCompleteJacobianAction(

View File

@@ -1,5 +1,7 @@
module;
#include "interior_displacement_contractions.hpp"
#include <array>
#include <cmath>
#include <expected>
@@ -14,6 +16,7 @@ module mean_field;
import :operators.kernels.rotational_displacement_force;
import :operators.prepared_rotational_displacement_force;
import :fem.reference_tables;
namespace {
using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
@@ -194,7 +197,7 @@ namespace mean_field::operators {
std::expected<
void,
kernels::RotationalDisplacementForceRejection>
PreparedRotationalDisplacementForceOperator::TryPrepareElementData() {
PreparedRotationalDisplacementForceOperator::TryPrepareElementData(const bool assembleResidual) {
MFEM_VERIFY(m_rotation.has_value(), "Prepared rotational force has no frozen rotation state.");
m_elements.clear();
@@ -214,7 +217,20 @@ namespace mean_field::operators {
mfem::Vector densityShape;
mfem::Vector potentialGradient;
if (assembleResidual) {
m_localAction.SetSize(m_fem.displacementFes->GetVSize());
m_localAction = 0.0;
}
mfem::Vector residualComponent;
const int dimension = m_domainMapper.GetDimension();
// Rigid rotation has a position-independent acceleration derivative.
m_centrifugalJacobian.SetSize(dimension, dimension);
mfem::Vector unit(dimension), derivative(dimension);
for (int c = 0; c < dimension; ++c) {
unit = 0.0; unit(c) = 1.0;
m_rotation->potential_gradient_directional_derivative(unit, derivative);
for (int r = 0; r < dimension; ++r) m_centrifugalJacobian(r, c) = -derivative(r);
}
for (int elementId = 0; elementId < m_fem.mesh->GetNE(); ++elementId) {
mfem::ElementTransformation *transformation = m_fem.mesh->GetElementTransformation(elementId);
@@ -249,6 +265,11 @@ namespace mean_field::operators {
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(elementId);
const mfem::FiniteElement &compactificationElement = *m_fem.compactificationFes->GetFE(elementId);
data.integrationRule = &get_rotation_force_rule(m_fem, *transformation);
data.densityReferenceTable = m_fem.GetReferenceTables().GetScalarTable(densityElement, *data.integrationRule);
data.displacementReferenceTable = m_fem.GetReferenceTables().GetScalarTable(displacementElement, *data.integrationRule);
data.fastApplication = dimension == 3 && m_fem.displacementFes->GetOrdering() == mfem::Ordering::byNODES &&
displacementElement.GetMapType() == mfem::FiniteElement::VALUE &&
displacementElement.GetDerivType() == mfem::FiniteElement::GRAD;
const mapping::ElementDisplacementData displacementData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementBaseDisplacement);
@@ -266,6 +287,7 @@ namespace mean_field::operators {
data.quadratureWeights.SetSize(quadraturePointCount);
densityShape.SetSize(densityElement.GetDof());
potentialGradient.SetSize(dimension);
if (assembleResidual) m_quadratureForce.SetSize(quadraturePointCount, dimension);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
@@ -304,8 +326,36 @@ namespace mean_field::operators {
}
}
}
if (assembleResidual) {
const double weightedDensity =
data.quadratureWeights(quadraturePoint) * data.baseDensityValues(quadraturePoint);
for (int component = 0; component < dimension; ++component)
m_quadratureForce(quadraturePoint, component) = -weightedDensity * potentialGradient(component);
}
}
if (assembleResidual) {
const int ndof = displacementElement.GetDof();
MFEM_VERIFY(data.displacementDofs.Size() == ndof * dimension,
"Prepared force residual has an invalid displacement element size.");
m_elementAction.SetSize(data.displacementDofs.Size());
residualComponent.SetSize(ndof);
for (int component = 0; component < dimension; ++component) {
data.displacementReferenceTable->GetValues().MultTranspose(
m_quadratureForce.Data() + component * quadraturePointCount, residualComponent.GetData());
for (int dof = 0; dof < ndof; ++dof)
m_elementAction(vector_dof_index(m_fem.displacementFes->GetOrdering(),
dof, component, ndof, dimension)) = residualComponent(dof);
}
if (!vector_is_finite(m_elementAction)) return std::unexpected(non_finite_rejection());
if (data.displacementDofTransformation != nullptr)
data.displacementDofTransformation->TransformDual(m_elementAction);
m_localAction.AddElementVector(data.displacementDofs, m_elementAction);
}
}
if (assembleResidual && !vector_is_finite(m_localAction))
return std::unexpected(non_finite_rejection());
return {};
}
@@ -328,15 +378,36 @@ namespace mean_field::operators {
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation
) {
return TryPrepareImpl(state, dependencies, rotation, true);
}
PreparedRotationalDisplacementForceReport PreparedRotationalDisplacementForceOperator::PrepareReference(
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation
) {
auto result = TryPrepareImpl(state, dependencies, rotation, false);
if (!result.has_value()) throw_rejection(result.error());
return std::move(result).value();
}
std::expected<PreparedRotationalDisplacementForceReport, kernels::RotationalDisplacementForceRejection>
PreparedRotationalDisplacementForceOperator::TryPrepareImpl(
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation,
const bool useFastPreparation
) {
const bool wasPrepared = m_isPrepared;
const bool implementationChanged = m_preparedWithFastResidual != useFastPreparation;
const bool rotationChanged =
!m_context.IsPrepared() || dependencies.rotation != m_context.GetDependencies().rotation;
PreparedRotationalDisplacementForceReport report;
report.contextReport = m_context.Prepare(state, dependencies);
if (!report.contextReport.DidAnyWork() && wasPrepared) {
if (!report.contextReport.DidAnyWork() && wasPrepared && !implementationChanged) {
return report;
}
@@ -352,29 +423,38 @@ namespace mean_field::operators {
"rotation state."
);
if (report.contextReport.preparedBaseState || !wasPrepared) {
auto residualResult = kernels::try_apply_rotational_displacement_force_residual(
m_fem, m_domainMapper, *m_rotation, m_context.GetBaseDensityTrue(), m_context.GetDisplacementTrue(),
m_actionTrue
);
if (!residualResult.has_value()) {
return std::unexpected(residualResult.error());
if (report.contextReport.preparedBaseState || !wasPrepared || implementationChanged) {
const bool inputsFinite = vector_is_finite(m_context.GetBaseDensityTrue()) &&
vector_is_finite(m_context.GetDisplacementTrue());
const auto inputStatus = synchronize_rejection(
inputsFinite ? std::optional<Rejection>{} : std::optional<Rejection>{non_finite_rejection()},
m_fem.mesh->GetComm());
if (!inputStatus.has_value()) return std::unexpected(inputStatus.error());
// Retain the original two-pass path for an independent residual
// comparison. The production path evaluates the geometry once.
if (!useFastPreparation) {
auto residualResult = kernels::try_apply_rotational_displacement_force_residual(
m_fem, m_domainMapper, *m_rotation, m_context.GetBaseDensityTrue(), m_context.GetDisplacementTrue(),
m_actionTrue);
if (!residualResult.has_value()) return std::unexpected(residualResult.error());
}
const auto elementResult = TryPrepareElementData(useFastPreparation);
const std::optional<Rejection> localRejection = elementResult.has_value()
? std::optional<Rejection>{} : std::optional<Rejection>{elementResult.error()};
const auto elementStatus = synchronize_rejection(localRejection, m_fem.mesh->GetComm());
if (!elementStatus.has_value()) return std::unexpected(elementStatus.error());
if (useFastPreparation) local_to_true(*m_fem.displacementFes, m_localAction, m_actionTrue);
m_cachedResidual.SetSize(m_context.GetDisplacementMap().reduced_size());
m_context.GetDisplacementMap().gather(m_actionTrue, m_cachedResidual);
const auto residualStatus = synchronize_rejection(
vector_is_finite(m_cachedResidual) ? std::optional<Rejection>{}
: std::optional<Rejection>{non_finite_rejection()},
m_fem.mesh->GetComm());
if (!residualStatus.has_value()) return std::unexpected(residualStatus.error());
const auto elementResult = TryPrepareElementData();
std::optional<Rejection> localRejection = elementResult.has_value()
? std::optional<Rejection>{}
: std::optional<Rejection>{elementResult.error()};
if (!vector_is_finite(m_cachedResidual)) {
localRejection = non_finite_rejection();
}
auto synchronized = synchronize_rejection(localRejection, m_fem.mesh->GetComm());
if (!synchronized.has_value()) {
return std::unexpected(synchronized.error());
}
m_preparedWithFastResidual = useFastPreparation;
++m_residualPreparationCount;
report.preparedResidual = true;
}
@@ -437,7 +517,8 @@ namespace mean_field::operators {
void PreparedRotationalDisplacementForceOperator::ApplyPreparedCompleteJacobianActionTrue(
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
const bool useFastPath, const bool useTensorPath
) const {
true_to_local(*m_fem.densityFes, densityVariationTrue, m_densityVariationLocal);
true_to_local(*m_fem.displacementFes, displacementVariationTrue, m_displacementVariationLocal);
@@ -466,6 +547,61 @@ namespace mean_field::operators {
const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix();
const int scalarDisplacementDofCount = displacementElement.GetDof();
if (useFastPath && data.fastApplication) {
const int nq = data.integrationRule->GetNPoints();
m_quadratureDensityVariation.SetSize(nq);
if (useTensorPath && data.densityReferenceTable->HasTensorEvaluation3D())
data.densityReferenceTable->EvaluateValuesTensor3D(m_elementDensityVariation, m_quadratureDensityVariation, m_tensorWorkspace);
else data.densityReferenceTable->GetValues().Mult(m_elementDensityVariation, m_quadratureDensityVariation);
m_quadraturePositionVariation.SetSize(nq, 3);
m_quadratureForce.SetSize(nq, 3);
double *position = m_quadraturePositionVariation.Data();
double *force = m_quadratureForce.Data();
const double *u = directionDofs.Data();
const auto &values = data.displacementReferenceTable->GetValues();
const bool tensor = useTensorPath && data.displacementReferenceTable->HasTensorEvaluation3D();
if (tensor) {
data.displacementReferenceTable->EvaluateTensor3D(directionDofs, m_tensorDisplacement, m_tensorWorkspace);
for (int c = 0; c < 3; ++c) for (int q = 0; q < nq; ++q)
position[q + c * nq] = m_tensorDisplacement(q, 4 * c);
} else {
for (int c = 0; c < 3; ++c)
values.Mult(u + c * scalarDisplacementDofCount, position + c * nq);
}
for (int q = 0; q < nq; ++q) {
double trace = 0.0;
if (tensor) {
for (int row = 0; row < 3; ++row) for (int column = 0; column < 3; ++column)
trace += data.inverseElementJacobians(q, row * 3 + column) *
m_tensorDisplacement(q, 4 * column + row + 1);
} else trace = displacement_trace_3d(directionDofs,
data.displacementReferenceTable->GetGradients(q), data.inverseElementJacobians, q);
const double rho = data.baseDensityValues(q);
const double densityWeight = m_quadratureDensityVariation(q) + rho * trace;
for (int c = 0; c < 3; ++c) {
const double da = m_centrifugalJacobian(c, 0) * position[q] +
m_centrifugalJacobian(c, 1) * position[q + nq] +
m_centrifugalJacobian(c, 2) * position[q + 2 * nq];
force[q + c * nq] = data.quadratureWeights(q) *
(densityWeight * data.centrifugalAccelerations(q, c) + rho * da);
}
}
m_elementAction.SetSize(data.displacementDofs.Size());
double *element = m_elementAction.HostWrite();
if (tensor) {
data.displacementReferenceTable->ApplyTransposeTensor3D(m_quadratureForce, m_tensorElement, m_tensorWorkspace);
for (int c = 0; c < 3; ++c) for (int i = 0; i < scalarDisplacementDofCount; ++i)
element[i + c * scalarDisplacementDofCount] = m_tensorElement(i, c);
} else {
for (int c = 0; c < 3; ++c)
values.MultTranspose(force + c * nq, element + c * scalarDisplacementDofCount);
}
if (data.displacementDofTransformation)
data.displacementDofTransformation->TransformDual(m_elementAction);
m_localAction.AddElementVector(data.displacementDofs, m_elementAction);
continue;
}
m_densityShape.SetSize(densityElement.GetDof());
m_displacementShape.SetSize(scalarDisplacementDofCount);
m_referenceDisplacementDShape.SetSize(scalarDisplacementDofCount, dimension);
@@ -546,6 +682,50 @@ namespace mean_field::operators {
++m_completeJacobianStatistics.applications;
}
void PreparedRotationalDisplacementForceOperator::ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_context.GetDensityMap().full_size());
m_displacementVariationTrue.SetSize(m_context.GetDisplacementMap().full_size());
m_context.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_context.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
ApplyPreparedCompleteJacobianActionTrue(m_densityVariationTrue, m_displacementVariationTrue, m_actionTrue, false);
action.SetSize(m_context.GetDisplacementMap().reduced_size());
m_context.GetDisplacementMap().gather(m_actionTrue, action);
}
void PreparedRotationalDisplacementForceOperator::ApplyCompleteJacobianActionCachedReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const {
VerifyPrepared();
m_densityVariationTrue.SetSize(m_context.GetDensityMap().full_size());
m_displacementVariationTrue.SetSize(m_context.GetDisplacementMap().full_size());
m_context.GetDensityMap().scatter(densityVariation, m_densityVariationTrue);
m_context.GetDisplacementMap().scatter(displacementVariation, m_displacementVariationTrue);
ApplyPreparedCompleteJacobianActionTrue(m_densityVariationTrue, m_displacementVariationTrue, m_actionTrue, true, false);
action.SetSize(m_context.GetDisplacementMap().reduced_size());
m_context.GetDisplacementMap().gather(m_actionTrue, action);
}
int PreparedRotationalDisplacementForceOperator::GetTensorElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements)
count += data.fastApplication && data.densityReferenceTable->HasTensorEvaluation3D() &&
data.displacementReferenceTable->HasTensorEvaluation3D();
return count;
}
bool PreparedRotationalDisplacementForceOperator::IsPrepared() const noexcept {
return m_isPrepared && m_rotation.has_value() && m_context.MatchesDependencies(m_preparedDependencies);
}

View File

@@ -3,6 +3,7 @@ module;
#include <cmath>
#include <memory>
#include <mfem.hpp>
#include <mpi.h>
#include <stdexcept>
module mean_field;
@@ -41,9 +42,33 @@ namespace mean_field::preconditioning {
return assembled;
}
const mfem::HypreParMatrix &PreparedGravityDivergenceCache::Get(const fem::FEM &f) {
if (!f.mesh || !f.gravityFluxFes || !f.gravityPotentialFes || !f.quadratureFactory)
throw std::invalid_argument("Fixed divergence caching requires a complete gravity discretization.");
const auto *mesh = f.mesh.get();
const auto *flux = f.gravityFluxFes.get(), *potential = f.gravityPotentialFes.get();
const int localChanged = !m_matrix || m_mesh != mesh || m_flux != flux || m_potential != potential ||
m_policy != f.quadratureFactory.get() || m_meshSequence != mesh->GetSequence() ||
m_nodesSequence != mesh->GetNodesSequence() || m_fluxSequence != flux->GetSequence() ||
m_potentialSequence != potential->GetSequence();
int changed = 0;
if (MPI_Allreduce(&localChanged, &changed, 1, MPI_INT, MPI_MAX, f.mesh->GetComm()) != MPI_SUCCESS)
throw std::runtime_error("Could not synchronize fixed divergence cache validity.");
if (changed) {
auto matrix = assembleGravityDivergenceSurrogate(f);
m_matrix = std::move(matrix);
m_mesh = mesh; m_flux = flux; m_potential = potential; m_policy = f.quadratureFactory.get();
m_meshSequence = mesh->GetSequence(); m_nodesSequence = mesh->GetNodesSequence();
m_fluxSequence = flux->GetSequence(); m_potentialSequence = potential->GetSequence();
++m_builds;
}
return *m_matrix;
}
std::unique_ptr<mfem::HypreParMatrix> assembleGravityPotentialSchurSurrogate(
const fem::FEM &f,
const mfem::Vector &trueMassDiagonal
const mfem::Vector &trueMassDiagonal,
PreparedGravityDivergenceCache *divergenceCache
) {
if (f.gravityFluxFes == nullptr || trueMassDiagonal.Size() != f.gravityFluxFes->GetTrueVSize()) {
throw std::invalid_argument(
@@ -62,12 +87,14 @@ namespace mean_field::preconditioning {
inverseMassDiagonal(index) = 1.0 / entry;
}
std::unique_ptr<mfem::HypreParMatrix> divergence = assembleGravityDivergenceSurrogate(f);
std::unique_ptr<mfem::HypreParMatrix> inverseMassDivergenceTranspose(divergence->Transpose());
std::unique_ptr<mfem::HypreParMatrix> uncachedDivergence;
if (!divergenceCache) uncachedDivergence = assembleGravityDivergenceSurrogate(f);
const mfem::HypreParMatrix &divergence = divergenceCache ? divergenceCache->Get(f) : *uncachedDivergence;
std::unique_ptr<mfem::HypreParMatrix> inverseMassDivergenceTranspose(divergence.Transpose());
inverseMassDivergenceTranspose->ScaleRows(inverseMassDiagonal);
std::unique_ptr<mfem::HypreParMatrix> schur(
mfem::ParMult(divergence.get(), inverseMassDivergenceTranspose.get())
mfem::ParMult(&divergence, inverseMassDivergenceTranspose.get())
);
if (schur == nullptr) {
throw std::runtime_error("MFEM did not assemble the gravity potential-Schur surrogate.");

View File

@@ -23,6 +23,7 @@ export import :deformation.radial_extensions;
export import :deformation.surface_prescription;
export import :deformation.vacuum_extension;
export import :fem;
import :fem.reference_tables;
export import :field.mfem;
export import :utils.domain;
@@ -141,8 +142,7 @@ export namespace mean_field::deformation {
m_interiorSurfacePullbackWorkspace(surfaceDisplacementSize()),
m_vacuumSurfacePullbackWorkspace(surfaceDisplacementSize()),
m_surfacePullbackWorkspace(surfaceDisplacementSize()),
m_parameterPullbackWorkspace(parameterCount()),
m_volumeGridFunctionWorkspace(std::make_unique<mfem::ParGridFunction>(&volumeDisplacementSpace)) {
m_parameterPullbackWorkspace(parameterCount()) {
validateCompatibility(surfaceScalarSpace, volumeDisplacementSpace, logicalReferenceMesh);
compileOwnership();
@@ -322,55 +322,8 @@ export namespace mean_field::deformation {
requireCurrentDiscretization();
requireVolumeSize(volumeDisplacement);
m_volumeGridFunctionWorkspace->SetFromTrueDofs(volumeDisplacement);
mfem::Mesh *mesh = m_volumeDisplacementSpace->GetMesh();
double localMinimumDeterminant = std::numeric_limits<double>::infinity();
int localGeometryIsFinite = 1;
for (int element = 0; element < mesh->GetNE(); ++element) {
mfem::ElementTransformation *transformation = mesh->GetElementTransformation(element);
const mfem::FiniteElement *finiteElement = m_volumeDisplacementSpace->GetFE(element);
// Positivity is a pointwise geometry requirement, not an
// integration-accuracy requirement. A rule only slightly
// above the displacement order can miss a narrow negative
// region of the determinant even when a downstream physics
// rule samples it. The determinant of a d-dimensional
// degree-p deformation gradient can vary at substantially
// higher order, so inspect at a conservative d*p scale.
const int geometryInspectionOrder =
std::max(finiteElement->GetOrder() + 2, 2 * spatialDimension() * finiteElement->GetOrder());
const mfem::IntegrationRule &rule =
mfem::IntRules.Get(transformation->GetGeometryType(), geometryInspectionOrder);
for (int point = 0; point < rule.GetNPoints(); ++point) {
transformation->SetIntPoint(&rule.IntPoint(point));
mfem::DenseMatrix deformationGradient;
m_volumeGridFunctionWorkspace->GetVectorGradient(*transformation, deformationGradient);
for (int component = 0; component < spatialDimension(); ++component) {
deformationGradient(component, component) += 1.0;
}
const double determinant = deformationGradient.Det();
if (!std::isfinite(determinant)) {
localGeometryIsFinite = 0;
} else {
localMinimumDeterminant = std::min(localMinimumDeterminant, determinant);
}
}
}
double globalMinimumDeterminant = 0.0;
int globalGeometryIsFinite = 0;
MPI_Allreduce(
&localMinimumDeterminant, &globalMinimumDeterminant, 1, MPI_DOUBLE, MPI_MIN,
m_volumeDisplacementSpace->GetComm()
);
MPI_Allreduce(
&localGeometryIsFinite, &globalGeometryIsFinite, 1, MPI_INT, MPI_MIN,
m_volumeDisplacementSpace->GetComm()
);
if (globalGeometryIsFinite == 0) {
globalMinimumDeterminant = std::numeric_limits<double>::quiet_NaN();
}
const double globalMinimumDeterminant =
m_geometryInspector.Inspect(*m_volumeDisplacementSpace, volumeDisplacement);
++m_actionStatistics.geometryInspections;
return {.minimumJacobianDeterminant = globalMinimumDeterminant};
}
@@ -579,7 +532,7 @@ export namespace mean_field::deformation {
mutable mfem::Vector m_vacuumSurfacePullbackWorkspace;
mutable mfem::Vector m_surfacePullbackWorkspace;
mutable mfem::Vector m_parameterPullbackWorkspace;
mutable std::unique_ptr<mfem::ParGridFunction> m_volumeGridFunctionWorkspace;
mutable fem::PreparedGeometryInspector m_geometryInspector;
};
template <

View File

@@ -1,5 +1,7 @@
module;
#include <array>
#include <cstdint>
#include <memory>
#include <vector>
@@ -19,7 +21,37 @@ export namespace mean_field::fem {
[[nodiscard]] int GetDofCount() const;
[[nodiscard]] int GetDimension() const;
// Available only for a verified nodal H1/L2 hexahedron and Cartesian quadrature
// ordering (x fastest). Results have columns [value, dx, dy, dz] for
// each input component. Workspace is caller-owned; tables stay immutable.
[[nodiscard]] bool HasTensorEvaluation3D() const noexcept;
void EvaluateTensor3D(
const mfem::DenseMatrix &elementDofs,
mfem::DenseMatrix &valuesAndGradients,
mfem::Vector &workspace
) const;
// Transpose of the reference gradient. Input columns are [dx,dy,dz]
// per output component. Callers supply all physical factors and weights.
void ApplyGradientTransposeTensor3D(const mfem::DenseMatrix &quadratureDuals,
mfem::DenseMatrix &elementDuals,
mfem::Vector &workspace) const;
// Value-only scalar contractions avoid forming unused gradients.
void EvaluateValuesTensor3D(const mfem::Vector &elementDofs, mfem::Vector &values,
mfem::Vector &workspace) const;
void ApplyTransposeTensor3D(const mfem::Vector &quadratureDuals, mfem::Vector &elementDuals,
mfem::Vector &workspace) const;
// Apply the transpose of the value basis to quadrature duals. No weights
// are introduced here; callers supply their already weighted duals.
void ApplyTransposeTensor3D(
const mfem::DenseMatrix &quadratureDuals, mfem::DenseMatrix &elementDuals,
mfem::Vector &workspace
) const;
private:
void PrepareTensorEvaluation3D(const mfem::FiniteElement &element, const mfem::IntegrationRule &rule);
friend class ReferenceTableCache;
ScalarReferenceTable(
const mfem::FiniteElement &element,
@@ -29,6 +61,11 @@ export namespace mean_field::fem {
mfem::DenseMatrix m_values;
std::vector<mfem::DenseMatrix> m_gradients;
int m_dimension;
int m_tensorDofsPerAxis{0};
int m_tensorPointsPerAxis{0};
mfem::Array<int> m_tensorDofMap;
std::array<mfem::DenseMatrix, 3> m_tensorValues;
std::array<mfem::DenseMatrix, 3> m_tensorGradients;
};
class VectorReferenceTable {
@@ -37,8 +74,16 @@ export namespace mean_field::fem {
[[nodiscard]] int GetPointCount() const;
[[nodiscard]] int GetDofCount() const;
[[nodiscard]] int GetDimension() const;
[[nodiscard]] bool HasTensorEvaluation3D() const noexcept;
void EvaluateTensor3D(
const mfem::Vector &elementDofs, mfem::DenseMatrix &values, mfem::Vector &workspace
) const;
void ApplyTransposeTensor3D(
const mfem::DenseMatrix &quadratureDuals, mfem::Vector &elementDuals, mfem::Vector &workspace
) const;
private:
void PrepareTensorEvaluation3D(const mfem::FiniteElement &element, const mfem::IntegrationRule &rule);
friend class ReferenceTableCache;
VectorReferenceTable(
const mfem::FiniteElement &element,
@@ -48,6 +93,9 @@ export namespace mean_field::fem {
std::vector<mfem::DenseMatrix> m_values;
int m_dof_count;
int m_dimension;
int m_tensorClosedDofs{0}, m_tensorPointsPerAxis{0};
mfem::Array<int> m_tensorDofMap;
std::array<std::array<mfem::DenseMatrix, 3>, 3> m_tensorValues;
};
// Owned by one discretization, never process-global. Finite elements must
@@ -75,4 +123,29 @@ export namespace mean_field::fem {
struct Storage;
std::unique_ptr<Storage> m_storage;
};
// The production mapped-geometry inspection, independently benchmarkable.
// Keeps the original conservative rule and checks every volume element.
// Mesh/FES/node sequences invalidate the fixed inverse-Jacobian cache;
// raw node edits require Mesh::NodesUpdated() or ClearCache(). Displacement
// is always re-evaluated. Like prepared operators, scratch is not thread-safe.
class PreparedGeometryInspector {
public:
PreparedGeometryInspector();
~PreparedGeometryInspector();
PreparedGeometryInspector(const PreparedGeometryInspector &) = delete;
PreparedGeometryInspector &operator=(const PreparedGeometryInspector &) = delete;
PreparedGeometryInspector(PreparedGeometryInspector &&) noexcept;
PreparedGeometryInspector &operator=(PreparedGeometryInspector &&) noexcept;
// Collective minimum of det(I + grad(u)); NaN if any sampled value is
// non-finite. reference=true retains GridFunction::GetVectorGradient.
[[nodiscard]] double Inspect(mfem::ParFiniteElementSpace &space,
const mfem::Vector &displacement, bool reference = false) const;
void ClearCache() const;
[[nodiscard]] int GetTensorElementCount() const noexcept;
[[nodiscard]] std::uint64_t GetGeometryBuildCount() const noexcept;
private:
struct Storage;
std::unique_ptr<Storage> m_storage;
};
} // namespace mean_field::fem

View File

@@ -34,6 +34,16 @@ export namespace mean_field::mapping::compactification {
[[nodiscard]] double GetReferenceInfinityRadius() const noexcept;
[[nodiscard]] double GetCoordinateTolerance() const noexcept;
// Factors in delta J = scale * grad(delta u) + delta u (grad scale)^T.
// They depend only on the fixed compactification coordinate, so prepared
// operators can compute them once instead of evaluating the full map.
[[nodiscard]] MappingStatus EvaluateJacobianVariationFactors(
double coordinate,
const mfem::Vector &coordinateGradient,
double &scale,
mfem::Vector &scaleGradient
) const;
private:
struct RadialFactors {
double coordinate;

View File

@@ -1,6 +1,8 @@
module;
#include "mean_field.h"
#include <cstddef>
#include <cstdint>
export module mean_field:mapping.domain_mapper;
export import :mapping.types;
@@ -108,8 +110,35 @@ export namespace mean_field::mapping {
DomainMapper(const DomainMapper &) = delete;
DomainMapper &operator=(const DomainMapper &) = delete;
DomainMapper(DomainMapper &&) = default;
DomainMapper &operator=(DomainMapper &&) = default;
~DomainMapper();
DomainMapper(DomainMapper &&) noexcept;
DomainMapper &operator=(DomainMapper &&) noexcept;
struct FixedMeshGeometry {
mfem::Vector position;
mfem::DenseMatrix jacobian, inverseJacobian;
double weight{0.0};
};
struct GeometryCacheStatistics {
std::uint64_t hits{0}, misses{0};
std::size_t points{0};
};
// Only computational-mesh geometry is cached, never displacement or
// compactification state. MFEM mesh/node sequence changes invalidate it.
// After raw coordinate edits call Mesh::NodesUpdated(), as required by
// MFEM's own geometric-factor cache, or ClearGeometryCache(). A mesh
// must outlive its cached entries; clear before destroying/replacing it.
// Like MFEM element transformations, this cache is not thread-safe.
void SetGeometryCacheEnabled(bool enabled) const;
[[nodiscard]] bool IsGeometryCacheEnabled() const noexcept;
void ClearGeometryCache() const;
[[nodiscard]] GeometryCacheStatistics GetGeometryCacheStatistics() const;
// Sets the transformation's integration point. nullptr means use MFEM
// directly (disabled cache, non-mesh transformation, or unsupported data).
// Returned data remains valid until cache invalidation or mapper destruction.
[[nodiscard]] const FixedMeshGeometry *GetFixedMeshGeometry(
mfem::ElementTransformation &transformation, const mfem::IntegrationPoint &point
) const;
[[nodiscard]] MappingStatus EvaluatePoint(
const ElementMappingData &element_data,
@@ -119,12 +148,19 @@ export namespace mean_field::mapping {
MappingPointContext &context
) const;
// A sample from the same element DOFs and integration point. Its
// gradient is with respect to finite-element coordinates, not the mesh.
struct ReferenceDisplacementSample {
const mfem::Vector &value;
const mfem::DenseMatrix &gradient;
};
[[nodiscard]] MappingStatus EvaluateVolume(
const ElementMappingData &element_data,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
Workspace &workspace,
VolumeMappingContext &context
VolumeMappingContext &context,
const ReferenceDisplacementSample *displacementSample = nullptr
) const;
[[nodiscard]] MappingStatus EvaluateFace(
@@ -172,6 +208,12 @@ export namespace mean_field::mapping {
[[nodiscard]] const compactification::ExteriorDomainMap &GetExteriorMap() const noexcept;
private:
[[nodiscard]] MappingStatus EvaluatePointImpl(
const ElementMappingData &element_data, mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point, Workspace &workspace,
MappingPointContext &context, const FixedMeshGeometry *meshGeometry,
const ReferenceDisplacementSample *displacementSample = nullptr
) const;
void ValidateElementData(const ElementMappingData &element_data) const;
void EvaluateField(
@@ -214,6 +256,8 @@ export namespace mean_field::mapping {
FaceElementSide side
);
struct GeometryCache;
mutable std::unique_ptr<GeometryCache> m_geometryCache;
utils::DomainMapperOptions m_options;
std::unique_ptr<const compactification::ExteriorDomainMap> m_exterior_map;
};

View File

@@ -1,6 +1,7 @@
module;
#include <concepts>
#include <mpi.h>
#include <cstdint>
#include <expected>
#include <memory>
@@ -22,9 +23,45 @@ export import :utils.domain;
namespace mean_field::normalization::detail {
using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema;
[[nodiscard]] inline bool CanAssembleExactPartialDiagonal(mfem::ParFiniteElementSpace &space, bool hdiv) {
// For hanging nodes, diag(P^T A P) contains cross terms. MFEM's PA
// diagonal is approximate there, so retain assembled exact diagonals.
int eligible = space.GetMesh()->Conforming() && !space.IsVariableOrder() && space.GetVDim() == 1;
for (int e = 0; eligible && e < space.GetNE(); ++e) {
const auto *fe = space.GetFE(e);
eligible = fe->GetGeomType() == mfem::Geometry::CUBE &&
(hdiv ? dynamic_cast<const mfem::RT_HexahedronElement *>(fe) != nullptr :
dynamic_cast<const mfem::NodalTensorFiniteElement *>(fe) != nullptr);
}
int allEligible = 0;
if (MPI_Allreduce(&eligible, &allEligible, 1, MPI_INT, MPI_MIN, space.GetComm()) != MPI_SUCCESS)
throw std::runtime_error("Could not choose a collective normalization assembly path.");
return allEligible != 0;
}
[[nodiscard]] inline mfem::Vector AssembleMassDiagonal(
mfem::ParBilinearForm &mass, mfem::ParFiniteElementSpace &space, bool partial
) {
if (partial) {
mass.SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
mass.Assemble();
mfem::Vector diagonal(space.GetTrueVSize());
mass.AssembleDiagonal(diagonal);
return diagonal;
}
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (!matrix) throw std::runtime_error("Reference Riesz mass assembly failed.");
mfem::Vector diagonal;
matrix->GetDiag(diagonal);
return diagonal;
}
[[nodiscard]] inline mfem::Vector AssembleScalarMassDiagonal(
mfem::ParFiniteElementSpace &space,
mfem::Array<int> *domainMarker = nullptr
mfem::Array<int> *domainMarker = nullptr,
bool reference = false
) {
mfem::ParBilinearForm mass(&space);
if (domainMarker == nullptr) {
@@ -32,34 +69,80 @@ namespace mean_field::normalization::detail {
} else {
mass.AddDomainIntegrator(new mfem::MassIntegrator(), *domainMarker);
}
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (matrix == nullptr) {
throw std::runtime_error("Reference scalar Riesz mass assembly failed.");
}
mfem::Vector diagonal;
matrix->GetDiag(diagonal);
return diagonal;
return AssembleMassDiagonal(mass, space, !reference && CanAssembleExactPartialDiagonal(space, false));
}
[[nodiscard]] inline mfem::Vector AssembleHDivMassDiagonal(mfem::ParFiniteElementSpace &space) {
[[nodiscard]] inline mfem::Vector AssembleHDivMassDiagonal(mfem::ParFiniteElementSpace &space, bool reference = false) {
mfem::ParBilinearForm mass(&space);
mass.AddDomainIntegrator(new mfem::VectorFEMassIntegrator());
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (matrix == nullptr) {
throw std::runtime_error("Reference H(div) Riesz mass assembly failed.");
return AssembleMassDiagonal(mass, space, !reference && CanAssembleExactPartialDiagonal(space, true));
}
[[nodiscard]] inline bool CanAssembleDirectBoundaryDiagonal(
mfem::ParFiniteElementSpace &space, int attribute
) {
int eligible = !space.Nonconforming() && !space.IsVariableOrder() && space.GetVDim() == 1 &&
dynamic_cast<const mfem::H1_FECollection *>(space.FEColl()) != nullptr;
mfem::Array<int> dofs;
for (int b = 0; eligible && b < space.GetNBE(); ++b) {
if (space.GetBdrAttribute(b) != attribute) continue;
// A general DOF transformation would require transforming the
// full element matrix before extracting its diagonal.
eligible = space.GetBdrElementVDofs(b, dofs) == nullptr;
}
mfem::Vector diagonal;
matrix->GetDiag(diagonal);
return diagonal;
int allEligible = 0;
if (MPI_Allreduce(&eligible, &allEligible, 1, MPI_INT, MPI_MIN, space.GetComm()) != MPI_SUCCESS)
throw std::runtime_error("Could not choose a collective surface normalization assembly path.");
return allEligible != 0;
}
[[nodiscard]] inline mfem::Vector AssembleDirectBoundaryDiagonal(
mfem::ParFiniteElementSpace &space, int attribute
) {
mfem::Vector localDiagonal(space.GetVSize());
localDiagonal = 0.0;
mfem::Array<int> dofs;
mfem::Vector shape, elementDiagonal;
// GetNBE includes tagged internal interfaces. MFEM's boundary PA face
// restriction uses FaceType::Boundary and omits the star/vacuum interface.
for (int b = 0; b < space.GetNBE(); ++b) {
if (space.GetBdrAttribute(b) != attribute) continue;
const auto &element = *space.GetBE(b);
auto &transformation = *space.GetBdrElementTransformation(b);
space.GetBdrElementVDofs(b, dofs);
shape.SetSize(element.GetDof());
elementDiagonal.SetSize(element.GetDof());
elementDiagonal = 0.0;
// Identical rule and physical shapes to MassIntegrator's default
// element assembly; only the diagonal of w * shape * shape^T is formed.
const auto &rule = mfem::MassIntegrator::GetRule(element, element, transformation);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto &point = rule.IntPoint(q);
transformation.SetIntPoint(&point);
element.CalcPhysShape(transformation, shape);
const double weight = transformation.Weight() * point.weight;
for (int i = 0; i < element.GetDof(); ++i)
elementDiagonal(i) += weight * shape(i) * shape(i);
}
for (int i = 0; i < dofs.Size(); ++i) {
const int dof = dofs[i] < 0 ? -1 - dofs[i] : dofs[i];
localDiagonal(dof) += elementDiagonal(i); // orientation signs are squared
}
}
mfem::Vector trueDiagonal(space.GetTrueVSize());
// Conforming H1 prolongation only copies scalar DOFs, so summing the
// local diagonal gives diag(P^T A P) exactly, including shared DOFs.
if (const auto *prolongation = space.GetProlongationMatrix())
prolongation->MultTranspose(localDiagonal, trueDiagonal);
else
trueDiagonal = localDiagonal;
return trueDiagonal;
}
[[nodiscard]] inline mfem::Vector AssembleSurfaceMassDiagonal(
const fem::FEM &finiteElements,
const field::ScalarBoundaryDofMap &surfaceMap
const field::ScalarBoundaryDofMap &surfaceMap,
bool reference = false
) {
mfem::Array<int> marker(finiteElements.mesh->bdr_attributes.Max());
marker = 0;
@@ -69,16 +152,12 @@ namespace mean_field::normalization::detail {
}
marker[attribute - 1] = 1;
if (!reference && CanAssembleDirectBoundaryDiagonal(*finiteElements.surfaceDeformationFes, attribute))
return surfaceMap.gather(AssembleDirectBoundaryDiagonal(*finiteElements.surfaceDeformationFes, attribute));
mfem::ParBilinearForm mass(finiteElements.surfaceDeformationFes.get());
mass.AddBoundaryIntegrator(new mfem::MassIntegrator(), marker);
mass.Assemble();
mass.Finalize();
std::unique_ptr<mfem::HypreParMatrix> matrix(mass.ParallelAssemble());
if (matrix == nullptr) {
throw std::runtime_error("Reference surface Riesz mass assembly failed.");
}
mfem::Vector ambientDiagonal;
matrix->GetDiag(ambientDiagonal);
auto ambientDiagonal = AssembleMassDiagonal(mass, *finiteElements.surfaceDeformationFes, false);
return surfaceMap.gather(ambientDiagonal);
}
@@ -100,6 +179,19 @@ namespace mean_field::normalization::detail {
} // namespace mean_field::normalization::detail
export namespace mean_field::normalization {
// Isolated verification/profiling entry points. reference=true retains the
// full assembled true-DOF diagonal; production uses exact PA for supported
// volume spaces and direct marked-boundary quadrature for supported surfaces.
[[nodiscard]] inline mfem::Vector AssembleReferenceScalarMassDiagonal(
mfem::ParFiniteElementSpace &space, mfem::Array<int> *domainMarker = nullptr, bool reference = false
) { return detail::AssembleScalarMassDiagonal(space, domainMarker, reference); }
[[nodiscard]] inline mfem::Vector AssembleReferenceHDivMassDiagonal(
mfem::ParFiniteElementSpace &space, bool reference = false
) { return detail::AssembleHDivMassDiagonal(space, reference); }
[[nodiscard]] inline mfem::Vector AssembleReferenceSurfaceMassDiagonal(
const fem::FEM &finiteElements, const field::ScalarBoundaryDofMap &surfaceMap, bool reference = false
) { return detail::AssembleSurfaceMassDiagonal(finiteElements, surfaceMap, reference); }
/*
* Runtime preparation paired with the compile-time normalization plan.
* The operator compiler is the authority for which blocks a specification

View File

@@ -156,6 +156,16 @@ export namespace mean_field::operators {
void BuildResidual(mfem::Vector &residual) const;
// Rows differentiate I_axis; changing Omega alone reuses both rows.
[[nodiscard]] bool PrepareJacobianRows() const;
[[nodiscard]] std::uint64_t GetJacobianRowPreparationCount() const noexcept;
void ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
double angularVelocityVariation,
mfem::Vector &action
) const;
void ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
@@ -223,6 +233,8 @@ export namespace mean_field::operators {
[[nodiscard]] double EvaluateDensityMomentActionLocal(const mfem::Vector &densityVariation) const;
[[nodiscard]] double EvaluateDisplacementMomentActionLocal(const mfem::Vector &displacementVariation) const;
[[nodiscard]] double EvaluateDensityMomentActionLocalReference(const mfem::Vector &densityVariation) const;
[[nodiscard]] double EvaluateDisplacementMomentActionLocalReference(const mfem::Vector &displacementVariation) const;
[[nodiscard]] double CylindricalRadiusSquared(const mfem::Vector &physicalPosition) const noexcept;
[[nodiscard]] double CylindricalRadiusSquaredVariation(
const mfem::Vector &physicalPosition,
@@ -238,6 +250,11 @@ export namespace mean_field::operators {
std::vector<ElementPAData> m_elements;
AngularMomentumDependencies m_preparedDependencies;
mfem::Vector m_cachedResidual;
mutable mfem::Vector m_densityJacobianLocal;
mutable mfem::Vector m_displacementJacobianLocal;
mutable bool m_jacobianRowsReady{false};
mutable bool m_jacobianRowsSupported{false};
mutable std::uint64_t m_jacobianRowPreparationCount{0};
mutable mfem::Vector m_densityVariationTrue;
mutable mfem::Vector m_displacementVariationTrue;
mutable mfem::Vector m_densityVariationLocal;

View File

@@ -82,6 +82,20 @@ export namespace mean_field::operators {
mfem::Vector &action
) const override;
// Supported-coordinate coupling actions; omit identically absent blocks.
void ApplyEnthalpyJacobianAction(const mfem::Vector &enthalpyVariation, mfem::Vector &action,
bool useTensorPath = true) const;
void ApplyDisplacementJacobianAction(const mfem::Vector &displacementVariation, mfem::Vector &action,
bool useTensorPath = true) const;
// Retained complete action for isolation/reference comparisons.
void MultReference(const mfem::Vector &densityVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation, mfem::Vector &action) const;
// Previous optimized pointwise actions, sharing exactly the same state.
void MultCachedReference(const mfem::Vector &densityVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation, mfem::Vector &action) const;
[[nodiscard]] int GetTensorElementCount() const noexcept;
void BuildResidual(mfem::Vector &residual) const;
// Exact diagonal of the prepared density-to-closure block, expressed
@@ -118,15 +132,19 @@ export namespace mean_field::operators {
void VerifyPrepared() const;
void MultImpl(const mfem::Vector &densityVariation, const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation, mfem::Vector &action, bool useFastPath, bool useTensorPath) const;
void ApplyThermodynamicActionFull(
const mfem::Vector &densityVariationTrue,
const mfem::Vector *densityVariationTrue,
const mfem::Vector &enthalpyVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue, bool useTensorPath = true
) const;
void ApplyDisplacementActionFull(
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
bool useFastPath = true, bool useTensorPath = true
) const;
struct ElementPAData {
@@ -180,6 +198,8 @@ export namespace mean_field::operators {
mutable mfem::Vector m_quadratureDisplacementAction;
mutable mfem::Vector m_elementDisplacementAction;
mutable mfem::DenseMatrix m_referenceDisplacementJacobian;
mutable mfem::DenseMatrix m_tensorDisplacement;
mutable mfem::Vector m_tensorWorkspace;
std::uint64_t m_preparationCount{0};
bool m_isPrepared{false};

View File

@@ -66,6 +66,9 @@ export namespace mean_field::operators {
kernels::GravityDisplacementForceRejection>
TryPrepare();
// Retained two-pass preparation for isolation benchmarks and validation.
PreparedGravityDisplacementForceReport PrepareReference();
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
@@ -90,6 +93,31 @@ export namespace mean_field::operators {
mfem::Vector &action
) const;
void ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
const mfem::Vector &gravityGradientVariation,
mfem::Vector &action
) const;
void ApplyCompleteJacobianActionCachedReference(
const mfem::Vector &densityVariation, const mfem::Vector &displacementVariation,
const mfem::Vector &gravityGradientVariation, mfem::Vector &action
) const;
[[nodiscard]] int GetTensorElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements)
if (data.fastApplication && data.displacementReferenceTable->HasTensorEvaluation3D() &&
data.gravityReferenceTable && data.gravityReferenceTable->HasTensorEvaluation3D()) ++count;
return count;
}
[[nodiscard]] int GetFastElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements) count += data.fastApplication;
return count;
}
[[nodiscard]] bool IsPrepared() const noexcept;
[[nodiscard]] std::uint64_t GetResidualPreparationCount() const noexcept;
@@ -124,16 +152,20 @@ export namespace mean_field::operators {
[[nodiscard]] std::expected<
void,
kernels::GravityDisplacementForceRejection>
TryPrepareElementData();
TryPrepareElementData(bool assembleResidual);
[[nodiscard]] std::expected<PreparedGravityDisplacementForceReport, kernels::GravityDisplacementForceRejection>
TryPrepareImpl(bool useFastPreparation);
void ApplyPreparedCompleteJacobianActionTrue(
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementVariationTrue,
const mfem::Vector &gravityGradientVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
bool useFastPath = true, bool useTensorPath = true
) const;
struct ElementPAData {
int elementId{-1};
bool fastApplication{false};
mfem::Array<int> densityDofs;
mfem::Array<int> gravityGradientDofs;
mfem::Array<int> displacementDofs;
@@ -168,6 +200,8 @@ export namespace mean_field::operators {
mutable mfem::Vector m_gravityGradientVariationLocal;
mutable mfem::Vector m_displacementVariationLocal;
mutable mfem::Vector m_localAction;
mutable mfem::Vector m_quadratureDensityVariation;
mutable mfem::DenseMatrix m_quadratureForce;
mutable mfem::Vector m_elementDensityVariation;
mutable mfem::Vector m_elementGravityGradientVariation;
mutable mfem::Vector m_elementDisplacementVariation;
@@ -199,6 +233,9 @@ export namespace mean_field::operators {
mutable PreparedGravityDisplacementForceCompleteStatistics m_completeJacobianStatistics;
mutable mfem::DenseMatrix m_tensorDisplacement, m_tensorGravity, m_tensorElementDual;
mutable mfem::Vector m_tensorWorkspace;
bool m_preparedWithFastResidual{true};
bool m_isPrepared{false};
};

View File

@@ -38,6 +38,8 @@ export namespace mean_field::operators {
void Prepare(const mfem::Vector &displacement);
void PreparePrimal(const mfem::Vector &displacement);
// Previous weight-recomputation path for isolated preparation comparisons.
void PrepareReference(const mfem::Vector &displacement);
[[nodiscard]] GravitySourcePreparationResult TryPrepare(const mfem::Vector &displacement);
[[nodiscard]] GravitySourcePreparationResult TryPreparePrimal(const mfem::Vector &displacement);
void Mult(
@@ -64,6 +66,11 @@ export namespace mean_field::operators {
}
}
void MultDisplacementVariationTrueReference(
const mfem::Vector &densityTrue, const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue) const;
[[nodiscard]] int GetTensorVariationElementCount() const noexcept;
void MultTranspose(
const mfem::Vector &potential,
mfem::Vector &action
@@ -109,7 +116,7 @@ export namespace mean_field::operators {
[[nodiscard]] GravitySourcePreparationResult TryPrepareImpl(
const mfem::Vector &displacement,
PreparationMode mode
PreparationMode mode, bool useCachedMeshWeight = true
);
const fem::FEM &m_fem;
@@ -137,6 +144,11 @@ export namespace mean_field::operators {
mutable mfem::Vector m_element_displacement_variation;
mutable mfem::Vector m_quadrature_variation_action;
mutable mfem::Vector m_element_variation_action;
void MultDisplacementVariationTrueImpl(const mfem::Vector &densityTrue,
const mfem::Vector &displacementVariationTrue, mfem::Vector &actionVariationTrue,
bool useTensorPath) const;
mutable mfem::DenseMatrix m_tensorSamples;
mutable mfem::Vector m_tensorWorkspace;
mutable mfem::DenseMatrix m_reference_displacement_jacobian;
mfem::Vector m_displacement_true;

View File

@@ -44,11 +44,29 @@ export namespace mean_field::operators {
const mfem::Vector &gravity_gradient,
mfem::Vector &action
) const override;
// Unmodified MFEM partial-assembly application on the same prepared data.
void MultReference(const mfem::Vector &gravityGradient, mfem::Vector &action) const;
[[nodiscard]] int GetFastMassElementCount() const noexcept { return m_fastMassElements; }
void MultDisplacementVariationTrue(
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const;
// Retained prepared implementation for isolated A/B validation and
// profiling. Uses the same prepared state, quadrature and DOF maps.
void MultDisplacementVariationTrueReference(
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const;
// Previous optimized pointwise path, excluding the new tensor contractions.
void MultDisplacementVariationTrueCachedReference(
const mfem::Vector &gravityGradientTrue, const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue
) const;
[[nodiscard]] int GetFastVariationElementCount() const noexcept;
[[nodiscard]] int GetTensorVariationElementCount() const noexcept;
void AssembleDiagonal(mfem::Vector &diagonal) const override;
void AssembleTrueDiagonal(mfem::Vector &diagonal) const;
@@ -79,6 +97,11 @@ export namespace mean_field::operators {
mfem::Vector compactification;
const mfem::IntegrationRule *integrationRule{nullptr};
std::shared_ptr<const fem::VectorReferenceTable> gravityReferenceTable;
std::shared_ptr<const fem::ScalarReferenceTable> displacementReferenceTable;
// One contiguous column per quadrature point: scale*J_mesh^-1
// (row-major, nine entries), followed by grad(scale). 3D VALUE
// displacement with an interior or Kelvin map; empty otherwise.
mfem::DenseMatrix tangentFactors;
// Fixed computational-mesh Piola factor, separate from J_map.
mfem::DenseMatrix meshPiolaJacobians;
mfem::Vector referenceWeights;
@@ -86,6 +109,13 @@ export namespace mean_field::operators {
};
[[nodiscard]] mapping::MappingStatus PrepareVariationData();
void MultDisplacementVariationTrueImpl(
const mfem::Vector &gravityGradientTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionVariationTrue,
bool useFastPath, bool useTensorPath
) const;
void AddFastElementVariation(const ElementVariationData &data, bool useTensorPath) const;
[[nodiscard]] HDivMassPreparationResult TryPrepareImpl(
const mfem::Vector &displacement,
PreparationMode mode
@@ -129,9 +159,12 @@ export namespace mean_field::operators {
mutable mfem::DenseMatrix m_meshPiolaJacobian;
mutable mfem::DenseMatrix m_gravityGradientShape;
mutable mfem::DenseMatrix m_massTensorVariation;
mutable mfem::DenseMatrix m_tensorDisplacement, m_tensorGravity, m_tensorDual;
mutable mfem::Vector m_tensorWorkspace;
std::uint64_t m_preparation_count{0};
bool m_is_prepared{false};
bool m_has_variation_data{false};
bool m_single_rank{true};
int m_fastMassElements{0};
};
} // namespace mean_field::operators

View File

@@ -142,6 +142,14 @@ export namespace mean_field::operators {
const physics::RigidRotation &rotation
);
HydrostaticEquilibriumPreparationResult TryPrepareReference(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation);
PreparedHydrostaticEquilibriumReport PrepareReference(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation);
void BuildResidual(mfem::Vector &residual) const;
void ApplyEnthalpyJacobianAction(
@@ -179,6 +187,18 @@ export namespace mean_field::operators {
mfem::Vector &action
) const;
void ApplyDisplacementJacobianActionReference(
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const;
// Previous cached pointwise implementation, retained for benchmarking
// the tensor change separately from the older general mapping path.
void ApplyDisplacementJacobianActionCachedReference(
const mfem::Vector &displacementVariation, mfem::Vector &action
) const;
[[nodiscard]] int GetFastDisplacementElementCount() const noexcept;
[[nodiscard]] int GetTensorDisplacementElementCount() const noexcept;
void ApplyCompleteJacobianAction(
const mfem::Vector &enthalpyVariation,
const mfem::Vector &gravityPotentialVariation,
@@ -244,6 +264,10 @@ export namespace mean_field::operators {
// Immutable reference values and gradients are shared by FE/rule.
std::shared_ptr<const fem::ScalarReferenceTable> enthalpyReferenceTable;
std::shared_ptr<const fem::ScalarReferenceTable> gravityPotentialReferenceTable;
std::shared_ptr<const fem::ScalarReferenceTable> displacementReferenceTable;
// Columns are quadrature points; each holds a column-major inverse
// deformed element Jacobian. Empty for the general mapping path.
mfem::DenseMatrix displacementInverseJacobians;
[[nodiscard]] const mfem::DenseMatrix &GetEnthalpyBasis() const {
return enthalpyReferenceTable->GetValues();
@@ -274,14 +298,24 @@ export namespace mean_field::operators {
mfem::Vector bernoulliConstantJacobian;
};
HydrostaticEquilibriumPreparationResult TryPrepareImpl(
const context::hydrostatic::HydrostaticEquilibriumStateView &state,
const context::hydrostatic::HydrostaticEquilibriumDependencies &dependencies,
const physics::RigidRotation &rotation, bool useTensorPreparation);
void PrepareStaticPlan();
[[nodiscard]] std::optional<mapping::MappingStatus> PrepareGeometry();
[[nodiscard]] bool PrepareAlgebraicJacobianBlocks();
[[nodiscard]] std::optional<mapping::MappingStatus> PrepareGeometry(bool useTensorPreparation);
[[nodiscard]] bool PrepareAlgebraicJacobianBlocks(bool useTensorPreparation);
[[nodiscard]] bool PrepareRotation();
[[nodiscard]] bool PrepareBaseState();
[[nodiscard]] bool PrepareBaseState(bool useTensorPreparation);
void FinalizeDisplacementJacobianPreparation();
[[nodiscard]] bool AssembleCachedResidual();
void VerifyPrepared() const;
void ApplyDisplacementJacobianActionImpl(
const mfem::Vector &displacementVariation, mfem::Vector &action, bool useFastPath, bool useTensorPath
) const;
mutable mfem::DenseMatrix m_tensorDisplacementValuesAndGradients;
mutable mfem::Vector m_tensorDisplacementWorkspace;
const fem::FEM &m_fem;
const mapping::DomainMapper &m_domainMapper;
@@ -307,6 +341,7 @@ export namespace mean_field::operators {
mutable PreparedHydrostaticCompleteJacobianStatistics m_completeJacobianStatistics;
bool m_preparedWithTensorPreparation{true};
bool m_isPrepared{false};
};

View File

@@ -147,6 +147,16 @@ export namespace mean_field::operators {
void BuildResidual(mfem::Vector &residual) const;
// Lazy, local-dual row assembly. Invalidated only by density/geometry.
// Returns false when this discretization uses the general fallback.
[[nodiscard]] bool PrepareJacobianRows() const;
[[nodiscard]] std::uint64_t GetJacobianRowPreparationCount() const noexcept;
void ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const;
void ApplyDensityJacobianAction(
const mfem::Vector &densityVariation,
mfem::Vector &action
@@ -230,6 +240,9 @@ export namespace mean_field::operators {
[[nodiscard]] double EvaluateDisplacementActionLocal(const mfem::Vector &displacementVariation) const;
[[nodiscard]] double EvaluateDensityActionLocalReference(const mfem::Vector &densityVariation) const;
[[nodiscard]] double EvaluateDisplacementActionLocalReference(const mfem::Vector &displacementVariation) const;
void AssembleDensityTransposeAction(
double residualDual,
mfem::Vector &densityDual
@@ -250,8 +263,16 @@ export namespace mean_field::operators {
MassNormalizationDependencies m_preparedDependencies;
mfem::Vector m_cachedResidual;
mutable mfem::Vector m_densityJacobianLocal;
mutable mfem::Vector m_displacementJacobianLocal;
mutable bool m_jacobianRowsReady{false};
mutable bool m_jacobianRowsSupported{false};
mutable std::uint64_t m_jacobianRowPreparationCount{0};
mutable mfem::Vector m_densityVariationTrue;
mutable mfem::Vector m_displacementVariationTrue;
mutable mfem::Vector m_densityVariationLocal;
mutable mfem::Vector m_displacementVariationLocal;
double m_currentMass{0.0};
double m_targetMass{0.0};

View File

@@ -116,6 +116,20 @@ export namespace mean_field::operators {
const context::pressure_force::PressureForceDependencies &dependencies
);
// Original quadrature-by-quadrature matrix assembly for validation.
PreparedPressureForceReport PrepareReference(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
);
// Previous blocked matrix preparation and pointwise field evaluation.
PreparedPressureForceReport PrepareCachedReference(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies);
void ApplyDisplacementJacobianActionCachedReference(
const mfem::Vector &displacementVariation, mfem::Vector &action) const;
[[nodiscard]] int GetTensorElementCount() const noexcept;
void BuildResidual(mfem::Vector &residual) const;
void ApplyEnthalpyJacobianAction(
@@ -128,6 +142,10 @@ export namespace mean_field::operators {
mfem::Vector &action
) const;
void ApplyDisplacementJacobianActionReference(
const mfem::Vector &displacementVariation, mfem::Vector &action
) const;
void ApplyCompleteJacobianAction(
const mfem::Vector &enthalpyVariation,
const mfem::Vector &displacementVariation,
@@ -178,6 +196,8 @@ export namespace mean_field::operators {
}
private:
void ApplyDisplacementJacobianActionImpl(const mfem::Vector &displacementVariation,
mfem::Vector &action, bool useFastPath, bool useTensorPath = true) const;
struct ConstructionData;
[[nodiscard]]
@@ -237,8 +257,14 @@ export namespace mean_field::operators {
};
void PrepareStaticPlan();
[[nodiscard]] std::optional<PressureForcePreparationRejection> PrepareGeometry();
[[nodiscard]] std::optional<PressureForcePreparationRejection> PrepareMaterialState();
[[nodiscard]] std::optional<PressureForcePreparationRejection> PrepareGeometry(bool useTensorPreparation);
[[nodiscard]] std::optional<PressureForcePreparationRejection> PrepareMaterialState(bool useFastPreparation, bool useTensorPreparation);
[[nodiscard]] std::expected<PreparedPressureForceReport, PressureForcePreparationRejection>
TryPrepareImpl(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies,
bool useFastPreparation, bool useTensorPreparation = true
);
void FinalizeDisplacementJacobianPreparation();
@@ -291,6 +317,10 @@ export namespace mean_field::operators {
mutable PreparedPressureForceCompleteJacobianStatistics m_completeJacobianStatistics;
mutable mfem::DenseMatrix m_tensorSamples, m_tensorDuals, m_tensorElement;
mutable mfem::Vector m_tensorWorkspace;
bool m_preparedWithTensorPreparation{true};
bool m_preparedWithFastMaterialAssembly{true};
bool m_isPrepared{false};
};

View File

@@ -4,6 +4,7 @@ module;
#include <cstdint>
#include <expected>
#include <optional>
#include <memory>
#include <vector>
#include <mfem.hpp>
@@ -11,6 +12,7 @@ module;
export module mean_field:operators.prepared_rotational_displacement_force;
export import :fem;
import :fem.reference_tables;
export import :mapping.domain_mapper;
export import :operators.context.rotational_displacement_force;
export import :operators.kernels.rotational_displacement_force;
@@ -72,6 +74,13 @@ export namespace mean_field::operators {
const physics::RigidRotation &rotation
);
// Retained two-pass preparation for isolation benchmarks and validation.
PreparedRotationalDisplacementForceReport PrepareReference(
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation
);
void BuildResidual(mfem::Vector &residual) const;
void ApplyDensityJacobianAction(
@@ -90,6 +99,23 @@ export namespace mean_field::operators {
mfem::Vector &action
) const;
void ApplyCompleteJacobianActionReference(
const mfem::Vector &densityVariation,
const mfem::Vector &displacementVariation,
mfem::Vector &action
) const;
void ApplyCompleteJacobianActionCachedReference(
const mfem::Vector &densityVariation, const mfem::Vector &displacementVariation, mfem::Vector &action
) const;
[[nodiscard]] int GetTensorElementCount() const noexcept;
[[nodiscard]] int GetFastElementCount() const noexcept {
int count = 0;
for (const auto &data : m_elements) count += data.fastApplication;
return count;
}
[[nodiscard]] bool IsPrepared() const noexcept;
[[nodiscard]] const context::rotational_displacement_force::RotationalDisplacementForcePreparationStatistics &
@@ -124,20 +150,31 @@ export namespace mean_field::operators {
[[nodiscard]] std::expected<
void,
kernels::RotationalDisplacementForceRejection>
TryPrepareElementData();
TryPrepareElementData(bool assembleResidual);
[[nodiscard]] std::expected<PreparedRotationalDisplacementForceReport, kernels::RotationalDisplacementForceRejection>
TryPrepareImpl(
const context::rotational_displacement_force::RotationalDisplacementForceStateView &state,
const context::rotational_displacement_force::RotationalDisplacementForceDependencies &dependencies,
const physics::RigidRotation &rotation,
bool useFastPreparation
);
void ApplyPreparedCompleteJacobianActionTrue(
const mfem::Vector &densityVariationTrue,
const mfem::Vector &displacementVariationTrue,
mfem::Vector &actionTrue
mfem::Vector &actionTrue,
bool useFastPath = true, bool useTensorPath = true
) const;
struct ElementPAData {
int elementId{-1};
bool fastApplication{false};
mfem::Array<int> densityDofs;
mfem::Array<int> displacementDofs;
mfem::DofTransformation *densityDofTransformation{nullptr};
mfem::DofTransformation *displacementDofTransformation{nullptr};
const mfem::IntegrationRule *integrationRule{nullptr};
std::shared_ptr<const fem::ScalarReferenceTable> densityReferenceTable;
std::shared_ptr<const fem::ScalarReferenceTable> displacementReferenceTable;
mfem::DenseMatrix inverseElementJacobians;
mfem::DenseMatrix centrifugalAccelerations;
mfem::Vector baseDensityValues;
@@ -150,6 +187,7 @@ export namespace mean_field::operators {
context::rotational_displacement_force::RotationalDisplacementForceLinearizationContext m_context;
std::optional<physics::RigidRotation> m_rotation;
mfem::DenseMatrix m_centrifugalJacobian;
mfem::Vector m_cachedResidual;
std::vector<ElementPAData> m_elements;
mutable mfem::Vector m_densityVariationTrue;
@@ -158,6 +196,11 @@ export namespace mean_field::operators {
mutable mfem::Vector m_densityVariationLocal;
mutable mfem::Vector m_displacementVariationLocal;
mutable mfem::Vector m_localAction;
mutable mfem::Vector m_quadratureDensityVariation;
mutable mfem::DenseMatrix m_quadratureForce;
mutable mfem::DenseMatrix m_quadraturePositionVariation;
mutable mfem::DenseMatrix m_tensorDisplacement, m_tensorElement;
mutable mfem::Vector m_tensorWorkspace;
mutable mfem::Vector m_elementDensityVariation;
mutable mfem::Vector m_elementDisplacementVariation;
mutable mfem::Vector m_elementAction;
@@ -181,6 +224,7 @@ export namespace mean_field::operators {
mutable PreparedRotationalDisplacementForceCompleteStatistics m_completeJacobianStatistics;
bool m_preparedWithFastResidual{true};
bool m_isPrepared{false};
};

View File

@@ -411,9 +411,27 @@ export namespace mean_field::preconditioning {
[[nodiscard]] std::unique_ptr<mfem::HypreParMatrix> assembleGravityDivergenceSurrogate(const fem::FEM &f);
// Fixed computational-mesh divergence, owned by one prepared block.
// FES/mesh/node sequences and policy identity invalidate it. Raw node edits
// require Mesh::NodesUpdated(). Clear explicitly if the policy changes in place.
class PreparedGravityDivergenceCache {
public:
[[nodiscard]] const mfem::HypreParMatrix &Get(const fem::FEM &f);
void Clear() noexcept { m_matrix.reset(); }
[[nodiscard]] std::uint64_t GetBuildCount() const noexcept { return m_builds; }
private:
std::unique_ptr<mfem::HypreParMatrix> m_matrix;
const mfem::Mesh *m_mesh{nullptr};
const mfem::ParFiniteElementSpace *m_flux{nullptr}, *m_potential{nullptr};
const quadrature::RuleFactory *m_policy{nullptr};
long m_meshSequence{-1}, m_nodesSequence{-1}, m_fluxSequence{-1}, m_potentialSequence{-1};
std::uint64_t m_builds{0};
};
[[nodiscard]] std::unique_ptr<mfem::HypreParMatrix> assembleGravityPotentialSchurSurrogate(
const fem::FEM &f,
const mfem::Vector &trueMassDiagonal
const mfem::Vector &trueMassDiagonal,
PreparedGravityDivergenceCache *divergenceCache = nullptr
);
struct GravityFieldBlockPreparationReport final {
@@ -548,6 +566,7 @@ export namespace mean_field::preconditioning {
RefreshMassInverse(geometryContext);
report.rebuiltMassInverse = true;
if (report.discretizationChanged) m_fixedDivergence.Clear();
auto potentialSchur = AssemblePotentialSchur(f, geometryContext);
m_potentialSchurInverse.Refresh(*potentialSchur);
m_potentialSchurSurrogate = std::move(potentialSchur);
@@ -586,6 +605,10 @@ export namespace mean_field::preconditioning {
return m_factorization;
}
[[nodiscard]] std::uint64_t GetDivergenceAssemblyCount() const noexcept {
return m_fixedDivergence.GetBuildCount();
}
[[nodiscard]] const PreparedGravityFieldBlockStatistics &GetStatistics() const noexcept {
return m_statistics;
}
@@ -632,13 +655,13 @@ export namespace mean_field::preconditioning {
}
}
[[nodiscard]] static std::unique_ptr<mfem::HypreParMatrix> AssemblePotentialSchur(
[[nodiscard]] std::unique_ptr<mfem::HypreParMatrix> AssemblePotentialSchur(
const fem::FEM &f,
const operators::context::gravity_field::GravityFieldGeometryContext &geometryContext
) {
mfem::Vector trueMassDiagonal;
geometryContext.GetMassOperator().AssembleTrueDiagonal(trueMassDiagonal);
return assembleGravityPotentialSchurSurrogate(f, trueMassDiagonal);
return assembleGravityPotentialSchurSurrogate(f, trueMassDiagonal, &m_fixedDivergence);
}
Block m_block;
@@ -647,6 +670,7 @@ export namespace mean_field::preconditioning {
field::FieldDofMap m_potentialMap;
ReducedGravityDivergenceOperator m_divergence;
PreparedMassInverse m_massInverse;
PreparedGravityDivergenceCache m_fixedDivergence;
std::unique_ptr<mfem::HypreParMatrix> m_potentialSchurSurrogate;
backend::PreparedHypreBoomerAMG<Mode> m_potentialSchurInverse;
ReducedFieldSolverAdapter m_reducedPotentialSchurInverse;

View File

@@ -784,9 +784,7 @@ export namespace mean_field::preconditioning {
) const {
VerifyBlock(enthalpy, 2, "enthalpy direction");
VerifyBlock(densityAction, 0, "density action");
m_operation->GetBarotropicClosureOperator().Mult(
m_zeroDensity, enthalpy, ZeroVolumeDisplacement(), densityAction
);
m_operation->GetBarotropicClosureOperator().ApplyEnthalpyJacobianAction(enthalpy, densityAction);
}
void ApplySurfaceToMaterial(
@@ -798,8 +796,8 @@ export namespace mean_field::preconditioning {
VerifyBlock(densityAction, 0, "density action");
VerifyBlock(enthalpyAction, 2, "enthalpy action");
GenerateDisplacement(surface);
m_operation->GetBarotropicClosureOperator().Mult(
m_zeroDensity, m_zeroEnthalpy, m_volumeDisplacement, densityAction
m_operation->GetBarotropicClosureOperator().ApplyDisplacementJacobianAction(
m_volumeDisplacement, densityAction
);
m_operation->GetHydrostaticOperator().ApplyDisplacementJacobianAction(m_volumeDisplacement, enthalpyAction);
m_operation->GetSurfaceConstraintOperator().ApplyJacobianRows(m_zeroEnthalpy, enthalpyAction);

View File

@@ -32,6 +32,9 @@ export namespace mean_field::solver::nonlinear {
double sufficientDecrease{1.0e-4};
double minimumStepLength{1.0e-8};
int maximumTrials{20};
// Skipping the predictive scan retains trial mapping validation and
// ordinary backtracking. Useful for small, warm-start Newton steps.
bool geometryPreflight{true};
void Validate() const {
if (!std::isfinite(initialStepLength) || initialStepLength <= 0.0) {

View File

@@ -143,11 +143,12 @@ namespace mean_field::solver::detail {
};
std::array<double, 9> minimumRealValues{};
std::array<double, 9> maximumRealValues{};
const std::array<int, 3> localIntegerValues{
options.maximumIterations, options.linearSolve.maximumIterations, options.backtracking.maximumTrials
const std::array<int, 4> localIntegerValues{
options.maximumIterations, options.linearSolve.maximumIterations, options.backtracking.maximumTrials,
static_cast<int>(options.backtracking.geometryPreflight)
};
std::array<int, 3> minimumIntegerValues{};
std::array<int, 3> maximumIntegerValues{};
std::array<int, 4> minimumIntegerValues{};
std::array<int, 4> maximumIntegerValues{};
if (MPI_Allreduce(
localRealValues.data(), minimumRealValues.data(), static_cast<int>(localRealValues.size()), MPI_DOUBLE,
@@ -341,7 +342,6 @@ export namespace mean_field::solver {
ValidateInitialState();
InitializeWorkspaces();
PrepareInitialOperator();
InitializeGeometryPreflightRules();
physicalInverse = std::unique_ptr<PhysicalInverseType>{new PhysicalInverseType(
PreparePhysicalInverse(std::move(preconditionerPrescription), RequireProblem(storage))
@@ -485,6 +485,10 @@ export namespace mean_field::solver {
const double maximumStepSize,
const double fractionToBoundarySafety
) {
if (!geometryPreflightRulesInitialized) {
InitializeGeometryPreflightRules();
geometryPreflightRulesInitialized = true;
}
normalizedOperator->DenormalizeState(normalizedCorrection, physicalCorrection);
const auto &physicalOperator = Problem().GetPhysicalOperator();
Problem().BuildVolumeDisplacementDirection(physicalCorrection, volumeDisplacementDirection);
@@ -513,6 +517,7 @@ export namespace mean_field::solver {
mfem::Vector volumeDisplacementDirection;
mfem::Vector candidatePhysicalState;
std::vector<deformation::NewtonStepGeometryRule> geometryPreflightRules;
bool geometryPreflightRulesInitialized{false};
double acceptedMinimumJacobianDeterminant{std::numeric_limits<double>::quiet_NaN()};
std::unique_ptr<NormalizedOperatorType> normalizedOperator;
std::unique_ptr<PhysicalInverseType> physicalInverse;
@@ -1271,31 +1276,33 @@ export namespace mean_field::solver {
}
const nonlinear::MetricEvaluation previousMetric = acceptedMetric;
const Clock::time_point geometryPreflightStart = Clock::now();
timings.geometryPreflight = state.EstimateLargestSafeStepSize(
nextLineSearchStepLength, options.backtracking.fractionToBoundarySafety
);
timings.geometryPreflightSeconds =
std::chrono::duration<double>(Clock::now() - geometryPreflightStart).count();
diagnostics.totalGeometryPreflightSeconds += timings.geometryPreflightSeconds;
diagnostics.lastGeometryPreflight = timings.geometryPreflight;
if (timings.geometryPreflight->limitedByGeometry) {
++diagnostics.geometryLimitedIterations;
}
if (timings.geometryPreflight->stepSize < options.backtracking.minimumStepLength) {
diagnostics.finalResidualNorm = acceptedMetric.residualNorm;
NotifyAfter(
iteration, nonlinear::IterationDisposition::globalization_failure, false, 0.0, 0,
diagnostics.initialResidualNorm, previousMetric, acceptedMetric, linearReport, timings, state
if (options.backtracking.geometryPreflight) {
const Clock::time_point geometryPreflightStart = Clock::now();
timings.geometryPreflight = state.EstimateLargestSafeStepSize(
nextLineSearchStepLength, options.backtracking.fractionToBoundarySafety
);
return Failure(
state, std::move(diagnostics), StellarEquilibriumFailureReason::globalization_failure,
"The geometry preflight found no orientation-preserving Newton step at or above the "
"configured minimum step length."
);
}
timings.geometryPreflightSeconds =
std::chrono::duration<double>(Clock::now() - geometryPreflightStart).count();
diagnostics.totalGeometryPreflightSeconds += timings.geometryPreflightSeconds;
diagnostics.lastGeometryPreflight = timings.geometryPreflight;
if (timings.geometryPreflight->limitedByGeometry) {
++diagnostics.geometryLimitedIterations;
}
if (timings.geometryPreflight->stepSize < options.backtracking.minimumStepLength) {
diagnostics.finalResidualNorm = acceptedMetric.residualNorm;
NotifyAfter(
iteration, nonlinear::IterationDisposition::globalization_failure, false, 0.0, 0,
diagnostics.initialResidualNorm, previousMetric, acceptedMetric, linearReport, timings, state
);
return Failure(
state, std::move(diagnostics), StellarEquilibriumFailureReason::globalization_failure,
"The geometry preflight found no orientation-preserving Newton step at or above the "
"configured minimum step length."
);
}
}
bool accepted = false;
double acceptedStepLength = 0.0;
int lineSearchTrials = 0;
@@ -1303,7 +1310,8 @@ export namespace mean_field::solver {
StellarEquilibriumFailureReason rejectionReason =
StellarEquilibriumFailureReason::globalization_failure;
std::string rejectionMessage = "The backtracking line search found no acceptable Newton step.";
double stepLength = timings.geometryPreflight->stepSize;
double stepLength = timings.geometryPreflight ?
timings.geometryPreflight->stepSize : nextLineSearchStepLength;
const Clock::time_point lineSearchStart = Clock::now();
try {

View File

@@ -3,6 +3,7 @@
#include <iostream>
#include <numbers>
#include <stdexcept>
#include <string_view>
#include <utility>
#include <mfem.hpp>
@@ -52,6 +53,20 @@ int main(
int argc,
char **argv
) {
bool geometryPreflight = true;
for (int i = 1; i < argc; ++i) {
const std::string_view option(argv[i]);
if (option == "--skip-preflight") geometryPreflight = false;
else if (option == "--preflight") geometryPreflight = true;
else if (option == "--help") {
std::cout << "Usage: sandbox [--skip-preflight|--preflight]\n"
"Trial geometry validation and backtracking remain enabled.\n";
return 0;
} else {
std::cerr << "Unknown sandbox option: " << option << '\n';
return 2;
}
}
mfem::Mpi::Init(argc, argv);
int exitCode = 0;
@@ -77,13 +92,16 @@ int main(
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
constexpr double angularMomentum = 0.1;
constexpr double angularMomentum = 0.00;
constexpr double gravitationalConstant = utils::G;
const double polytropicConstant =
2.0 * gravitationalConstant * radius * radius / std::numbers::pi_v<double>;
// const double polytropicConstant = 0.3639391; // Use for n=3 test
std::println("K = {}", polytropicConstant);
const double centralDensity =
std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
//const double centralDensity =12.9351146; // use for n=3 test
auto stellarModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
@@ -201,9 +219,10 @@ int main(
.absoluteTolerance = 0.0,
.maximumIterations = 200
},
.backtracking = {}
.backtracking = {.geometryPreflight = geometryPreflight}
});
if (rank == 0) std::cout << "Geometry preflight: " << (geometryPreflight ? "enabled" : "skipped") << '\n';
auto equilibriumSolver = solver::make(context, nonlinearSolver, observer);
const auto evaluateStart = std::chrono::steady_clock::now();
auto report = equilibriumSolver.evaluate();

View File

@@ -1,5 +1,9 @@
#include <algorithm>
#include <array>
#include <algorithm>
#include <cmath>
#include <memory>
#include <limits>
#include <type_traits>
#include <utility>
@@ -260,3 +264,295 @@ TEST_CASE(
CheckMatrixExactly(retained->GetValues(0), firstExpected);
CheckMatrixExactly(retained->GetValues(retained->GetPointCount() - 1), lastExpected);
}
TEST_CASE("Tensor reference evaluation matches native H1 hex values and gradients",
tags::unit &tags::quadrature) {
for (const int order : {1, 2, 4}) {
for (const int basis : {mfem::BasisType::GaussLobatto, mfem::BasisType::ClosedUniform}) {
mfem::H1_FECollection collection(order, 3, basis);
const auto &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
const auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 2 * order + 3));
const TableCache cache;
const auto table = cache.GetScalarTable(element, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::DenseMatrix dofs(element.GetDof(), 3), actual;
mfem::Vector workspace;
for (int c = 0; c < 3; ++c) for (int i = 0; i < element.GetDof(); ++i)
dofs(i, c) = std::sin(0.37 * (i + 1) + c) + 0.21 * std::cos(0.13 * i - c);
table->EvaluateTensor3D(dofs, actual, workspace);
REQUIRE(actual.Height() == rule.GetNPoints());
REQUIRE(actual.Width() == 12);
for (int q = 0; q < rule.GetNPoints(); ++q) for (int c = 0; c < 3; ++c) {
for (int derivative = 0; derivative < 4; ++derivative) {
double expected = 0.0;
for (int i = 0; i < element.GetDof(); ++i)
expected += dofs(i, c) * (derivative == 0 ? table->GetValues()(q, i) :
table->GetGradients(q)(i, derivative - 1));
CHECK(std::abs(actual(q, 4 * c + derivative) - expected) <
2.0e-12 * std::max(1.0, std::abs(expected)));
}
}
// Reused caller scratch must not retain a previous direction.
dofs = 0.0;
table->EvaluateTensor3D(dofs, actual, workspace);
for (int c = 0; c < actual.Width(); ++c) for (int q = 0; q < actual.Height(); ++q)
CHECK(actual(q, c) == 0.0);
}
}
}
TEST_CASE("Tensor reference evaluation rejects non-Cartesian rules and owns its data",
tags::unit &tags::quadrature) {
std::shared_ptr<const ScalarTable> retained;
mfem::DenseMatrix input, expected;
{
mfem::H1_FECollection collection(3, 3);
const auto &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 7));
const TableCache cache;
retained = cache.GetScalarTable(element, rule);
REQUIRE(retained->HasTensorEvaluation3D());
input.SetSize(element.GetDof(), 1);
input = 1.0;
mfem::Vector scratch;
retained->EvaluateTensor3D(input, expected, scratch);
rule.IntPoint(1).y += 0.001;
CHECK_FALSE(cache.GetScalarTable(element, rule)->HasTensorEvaluation3D());
const auto &tet = *collection.FiniteElementForGeometry(mfem::Geometry::TETRAHEDRON);
CHECK_FALSE(cache.GetScalarTable(tet, mfem::IntRules.Get(mfem::Geometry::TETRAHEDRON, 7))
->HasTensorEvaluation3D());
}
mfem::DenseMatrix actual;
mfem::Vector scratch;
retained->EvaluateTensor3D(input, actual, scratch);
CheckMatrixExactly(actual, expected);
}
TEST_CASE("Tensor Reference Transposes Match Dense Assembly And Adjoint Products", tags::unit &tags::quadrature) {
for (int order : {1, 2, 4}) {
mfem::H1_FECollection h1(order, 3);
const auto &fe = *h1.FiniteElementForGeometry(mfem::Geometry::CUBE);
const auto &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3);
TableCache cache;
auto table = cache.GetScalarTable(fe, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::DenseMatrix dual(rule.GetNPoints(), 3), actual;
for (int c = 0; c < 3; ++c) for (int q = 0; q < dual.Height(); ++q)
dual(q,c) = std::sin(0.17*(q+1)*(c+1));
mfem::Vector workspace, expected(fe.GetDof());
for (int repeat = 0; repeat < 2; ++repeat) {
if (repeat) dual = 0.0;
table->ApplyTransposeTensor3D(dual, actual, workspace);
for (int c = 0; c < 3; ++c) {
table->GetValues().MultTranspose(dual.GetColumn(c), expected.GetData());
for (int i = 0; i < fe.GetDof(); ++i)
CHECK(std::abs(actual(i,c)-expected(i)) <= 2.0e-12 * std::max(1.0, expected.Normlinf()));
}
}
}
for (int order : {0, 1, 3}) for (int basis : {mfem::BasisType::GaussLegendre, mfem::BasisType::IntegratedGLL}) {
CAPTURE(order, basis);
mfem::RT_FECollection rt(order, 3, mfem::BasisType::GaussLobatto, basis);
const auto &fe = *rt.FiniteElementForGeometry(mfem::Geometry::CUBE);
auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+5));
TableCache cache;
auto table = cache.GetVectorTable(fe, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::Vector dofs(fe.GetDof()), assembled, expected(fe.GetDof()), point(3), workspace;
mfem::DenseMatrix values, dual(rule.GetNPoints(), 3);
for (int i = 0; i < dofs.Size(); ++i) dofs(i) = std::sin(0.53*(i+1));
for (int c = 0; c < 3; ++c) for (int q = 0; q < dual.Height(); ++q) dual(q,c) = std::cos(0.31*(q+1)+c);
for (int repeat = 0; repeat < 2; ++repeat) {
if (repeat) { dofs = 0.0; dual = 0.0; }
table->EvaluateTensor3D(dofs, values, workspace);
expected = 0.0;
double primalProduct = 0.0;
for (int q = 0; q < rule.GetNPoints(); ++q) {
table->GetValues(q).MultTranspose(dofs, point);
for (int c = 0; c < 3; ++c) {
CHECK(std::abs(values(q,c)-point(c)) <= 2.0e-11 * std::max(1.0, point.Normlinf()));
primalProduct += values(q,c)*dual(q,c);
point(c) = dual(q,c);
}
table->GetValues(q).AddMult(point, expected);
}
table->ApplyTransposeTensor3D(dual, assembled, workspace);
mfem::Vector difference(assembled); difference -= expected;
CHECK(difference.Norml2() <= 2.0e-11 * std::max(1.0, expected.Norml2()));
CHECK(std::abs(primalProduct - dofs*assembled) <= 2.0e-11 * std::max(1.0, std::abs(primalProduct)));
}
rule.IntPoint(rule.GetNPoints()-1).x += 0.017;
CHECK_FALSE(cache.GetVectorTable(fe, rule)->HasTensorEvaluation3D());
}
}
TEST_CASE("RT Tensor Plans Own Their Factors After Cache And FE Destruction", tags::unit &tags::quadrature) {
std::shared_ptr<const VectorTable> table;
mfem::Vector dofs, expected;
mfem::DenseMatrix values, dual;
{
mfem::RT_FECollection rt(2, 3);
TableCache cache;
auto rule = CopyRule(mfem::IntRules.Get(mfem::Geometry::CUBE, 9));
const auto &fe = *rt.FiniteElementForGeometry(mfem::Geometry::CUBE);
table = cache.GetVectorTable(fe, rule);
REQUIRE(table->HasTensorEvaluation3D());
dofs.SetSize(fe.GetDof()); dofs = 0.3;
dual.SetSize(rule.GetNPoints(), 3); dual = 0.17;
mfem::Vector workspace;
table->ApplyTransposeTensor3D(dual, expected, workspace);
}
mfem::Vector workspace, actual;
table->EvaluateTensor3D(dofs, values, workspace);
table->ApplyTransposeTensor3D(dual, actual, workspace);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
}
TEST_CASE("L2 Tensor Values And Transposes Match Dense Reference", tags::unit &tags::quadrature) {
for (int order : {0, 1, 3}) for (int basis : {mfem::BasisType::GaussLegendre, mfem::BasisType::OpenUniform}) {
mfem::L2_FECollection collection(order, 3, basis);
const auto &element = *collection.FiniteElementForGeometry(mfem::Geometry::CUBE);
const auto &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3);
TableCache cache;
const auto table = cache.GetScalarTable(element, rule);
REQUIRE(table->HasTensorEvaluation3D());
mfem::Vector dofs(element.GetDof()), dual(rule.GetNPoints()), values, expected(rule.GetNPoints()),
transpose, expectedTranspose(element.GetDof()), workspace;
for (int i = 0; i < dofs.Size(); ++i) dofs(i) = std::sin(0.37*(i+1));
for (int q = 0; q < dual.Size(); ++q) dual(q) = std::cos(0.19*(q+1));
table->EvaluateValuesTensor3D(dofs, values, workspace);
table->GetValues().Mult(dofs, expected);
mfem::Vector difference(values); difference -= expected;
CHECK(difference.Norml2() <= 2.0e-12*std::max(1.0, expected.Norml2()));
table->ApplyTransposeTensor3D(dual, transpose, workspace);
table->GetValues().MultTranspose(dual, expectedTranspose);
difference = transpose; difference -= expectedTranspose;
CHECK(difference.Norml2() <= 2.0e-12*std::max(1.0, expectedTranspose.Norml2()));
CHECK(std::abs((values*dual) - (dofs*transpose)) <= 2.0e-12*std::max(1.0, std::abs(values*dual)));
dofs = 0.0; dual = 0.0;
table->EvaluateValuesTensor3D(dofs, values, workspace);
table->ApplyTransposeTensor3D(dual, transpose, workspace);
CHECK(values.Norml2() == 0.0);
CHECK(transpose.Norml2() == 0.0);
}
}
TEST_CASE("Geometry Inspector Reuses And Invalidates Fixed Geometry", tags::unit &tags::geometry) {
for (const auto ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM}) {
auto serial = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON);
serial.SetCurvature(2);
mfem::ParMesh mesh(MPI_COMM_WORLD, serial);
mfem::H1_FECollection collection(2, 3);
mfem::ParFiniteElementSpace space(&mesh, &collection, 3, ordering);
mfem::ParGridFunction field(&space);
mfem::VectorFunctionCoefficient displacement(3, [](const mfem::Vector &x, mfem::Vector &u) {
u.SetSize(3);
u(0) = 0.08*x(0) + 0.02*x(1)*x(2);
u(1) = -0.03*x(1) + 0.01*x(0)*x(2);
u(2) = 0.04*x(2);
});
field.ProjectCoefficient(displacement);
mfem::Vector state; field.GetTrueDofs(state);
mean_field::fem::PreparedGeometryInspector inspector;
auto compare = [&] {
const double fast = inspector.Inspect(space, state);
const double reference = inspector.Inspect(space, state, true);
CHECK(std::isfinite(fast));
CHECK(std::abs(fast-reference) <= 2.0e-12*std::max(1.0, std::abs(reference)));
return fast;
};
compare();
CHECK(inspector.GetTensorElementCount() == mesh.GetNE());
const auto builds = inspector.GetGeometryBuildCount();
state *= 0.5;
const double before = compare();
CHECK(inspector.GetGeometryBuildCount() == builds);
// Same FE/mesh topology, changed curved computational coordinates.
mfem::VectorFunctionCoefficient stretch(3, [](const mfem::Vector &x, mfem::Vector &y) {
y = x; y(0) = 1.2*x(0); y(1) += 0.04*x(0)*x(2);
});
mesh.Transform(stretch);
const double after = compare();
CHECK(std::abs(after-before) > 1.0e-6);
CHECK(inspector.GetGeometryBuildCount() == builds + mesh.GetNE());
inspector.ClearCache();
CHECK(std::abs(compare()-after) < 2.0e-14);
CHECK(inspector.GetGeometryBuildCount() == builds + 2*mesh.GetNE());
mfem::VectorFunctionCoefficient folded(3, [](const mfem::Vector &x, mfem::Vector &u) {
u.SetSize(3); u = 0.0; u(0) = -2.0*x(0);
});
field.ProjectCoefficient(folded); field.GetTrueDofs(state);
CHECK(compare() < 0.0);
if (state.Size()) state(0) = std::numeric_limits<double>::quiet_NaN();
CHECK_FALSE(std::isfinite(inspector.Inspect(space, state)));
CHECK_FALSE(std::isfinite(inspector.Inspect(space, state, true)));
}
}
TEST_CASE("Geometry Inspector Preserves Tetrahedron Fallback", tags::unit &tags::geometry) {
auto serial = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::TETRAHEDRON);
mfem::ParMesh mesh(MPI_COMM_WORLD, serial);
mfem::H1_FECollection collection(1, 3);
mfem::ParFiniteElementSpace space(&mesh, &collection, 3);
mfem::Vector state(space.GetTrueVSize()); state = 0.0;
mean_field::fem::PreparedGeometryInspector inspector;
CHECK(inspector.Inspect(space, state) == 1.0);
CHECK(inspector.Inspect(space, state, true) == 1.0);
CHECK(inspector.GetTensorElementCount() == 0);
CHECK(inspector.GetGeometryBuildCount() == 0);
}
TEST_CASE("Tensor Gradient Transposes Match Dense Assembly And Adjoint Products", tags::unit &tags::quadrature) {
const auto check = [](const mfem::FiniteElement &element, const mfem::IntegrationRule &rule) {
TableCache cache;
const auto table = cache.GetScalarTable(element, rule);
REQUIRE(table->HasTensorEvaluation3D());
constexpr int components = 2;
mfem::DenseMatrix dofs(element.GetDof(), components), dual(rule.GetNPoints(), 3*components),
values, actual, expected(element.GetDof(), components);
mfem::Vector workspace;
for (int c = 0; c < components; ++c) for (int i = 0; i < dofs.Height(); ++i)
dofs(i,c) = std::sin(0.37*(i+1)+c);
for (int c = 0; c < dual.Width(); ++c) for (int q = 0; q < dual.Height(); ++q)
dual(q,c) = std::cos(0.23*(q+1)+0.31*c);
for (int repeat = 0; repeat < 2; ++repeat) {
if (repeat) dual = 0.0;
table->EvaluateTensor3D(dofs, values, workspace);
table->ApplyGradientTransposeTensor3D(dual, actual, workspace);
expected = 0.0;
double forwardProduct = 0.0, transposeProduct = 0.0, scale = 1.0;
for (int c = 0; c < components; ++c) for (int q = 0; q < rule.GetNPoints(); ++q)
for (int a = 0; a < 3; ++a) {
forwardProduct += values(q,4*c+a+1)*dual(q,3*c+a);
for (int i = 0; i < element.GetDof(); ++i)
expected(i,c) += table->GetGradients(q)(i,a)*dual(q,3*c+a);
}
for (int c = 0; c < components; ++c) for (int i = 0; i < dofs.Height(); ++i)
scale = std::max(scale, std::abs(expected(i,c)));
for (int c = 0; c < components; ++c) for (int i = 0; i < dofs.Height(); ++i) {
CHECK(std::abs(actual(i,c)-expected(i,c)) <= 2.0e-11*scale);
transposeProduct += dofs(i,c)*actual(i,c);
if (repeat) CHECK(actual(i,c) == 0.0);
}
CHECK(std::abs(forwardProduct-transposeProduct) <=
2.0e-11*std::max(1.0, std::abs(forwardProduct)));
}
};
for (int order : {1, 2, 4}) {
CAPTURE(order);
mfem::H1_FECollection collection(order, 3);
check(*collection.FiniteElementForGeometry(mfem::Geometry::CUBE),
mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3));
}
for (int order : {0, 1, 3}) for (int basis : {mfem::BasisType::GaussLegendre, mfem::BasisType::OpenUniform}) {
CAPTURE(order, basis);
mfem::L2_FECollection collection(order, 3, basis);
check(*collection.FiniteElementForGeometry(mfem::Geometry::CUBE),
mfem::IntRules.Get(mfem::Geometry::CUBE, 2*order+3));
}
}

View File

@@ -1,3 +1,4 @@
#include <algorithm>
#include <array>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
@@ -102,6 +103,43 @@ namespace {
}
} // namespace
TEST_CASE("Kelvin Prepared Jacobian Factors Match Full Variation", tags::unit &tags::mapping &tags::kelvin) {
mapping::compactification::KelvinCompactification kelvin({.r_star_ref = 1.3, .r_inf_ref = 3.1});
const auto position = make_vector(1.1, 0.2, -0.1);
const auto coordinateGradient = make_vector(0.2, 0.03, -0.01);
auto jacobian = make_identity();
jacobian(0, 1) = 0.07;
const auto direction = make_vector(-0.02, 0.07, 0.04);
auto directionJacobian = make_identity();
directionJacobian *= 0.03;
directionJacobian(1, 2) = -0.05;
for (const double coordinate : {0.0, 0.4, 0.93, 0.999}) {
const mapping::compactification::ExteriorMapInput input{
position, position, jacobian, coordinate, coordinateGradient};
mapping::compactification::ExteriorMapResult base;
REQUIRE(kelvin.Evaluate(input, base) == mapping::MappingStatus::valid);
mapping::compactification::ExteriorMapVariation variation;
REQUIRE(kelvin.EvaluateVariation(input, base, {direction, directionJacobian}, variation) == mapping::MappingStatus::valid);
double scale = 0.0;
mfem::Vector scaleGradient;
REQUIRE(kelvin.EvaluateJacobianVariationFactors(coordinate, coordinateGradient, scale, scaleGradient) == mapping::MappingStatus::valid);
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column) {
const double expected = variation.mapping_jacobian_variation(row, column);
CHECK_THAT(scale * directionJacobian(row, column) + direction(row) * scaleGradient(column),
WithinAbs(expected, 1.0e-12 * std::max(1.0, std::abs(expected))));
}
}
}
double scale = 0.0;
mfem::Vector gradient;
CHECK(kelvin.EvaluateJacobianVariationFactors(1.0, coordinateGradient, scale, gradient) == mapping::MappingStatus::at_compactified_infinity);
CHECK(kelvin.EvaluateJacobianVariationFactors(-0.1, coordinateGradient, scale, gradient) == mapping::MappingStatus::outside_reference_domain);
auto invalid = coordinateGradient;
invalid(1) = std::numeric_limits<double>::quiet_NaN();
CHECK(kelvin.EvaluateJacobianVariationFactors(0.4, invalid, scale, gradient) == mapping::MappingStatus::non_finite_input);
}
TEST_CASE(
"Kelvin Compactification Validates Its Configuration",
tags::unit &tags::mapping &tags::kelvin

View File

@@ -2870,3 +2870,80 @@ TEST_CASE(
CHECK_THROWS_AS(evaluator.Refresh(), std::invalid_argument);
CHECK_THROWS_AS(evaluator.EvaluatePoint(*transformation, integration_point, context), std::invalid_argument);
}
TEST_CASE("Fixed Mesh Geometry Cache Reuses Factors And Invalidates On Mesh Updates", "[mapping][geometry-cache]") {
SingleElementFixture fixture;
mapping::DomainMapper mapper({.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification());
mapping::DomainMapper::Workspace workspace(dimension);
const auto &rule = mfem::IntRules.Get(mfem::Geometry::CUBE, 5);
const auto &point = rule.IntPoint(0);
auto zero = fixture.MakeZeroElementDofs();
mapping::ElementDisplacementData displacement(fixture.GetElement(), zero);
ElementMappingDataOwner data(displacement);
auto compare = [&](const mfem::IntegrationPoint &ip) {
auto &transformation = *fixture.mesh.GetElementTransformation(0);
mapping::VolumeMappingContext fast, reference;
mapper.SetGeometryCacheEnabled(true);
REQUIRE(mapper.EvaluateVolume(data.Get(), transformation, ip, workspace, fast) == mapping::MappingStatus::valid);
mapper.SetGeometryCacheEnabled(false);
REQUIRE(mapper.EvaluateVolume(data.Get(), transformation, ip, workspace, reference) == mapping::MappingStatus::valid);
check_vector(fast.mapping.physical_position, reference.mapping.physical_position);
check_matrix(fast.mapping.mapping_jacobian, reference.mapping.mapping_jacobian);
check_matrix(fast.quadrature.J_inv, reference.quadrature.J_inv);
CHECK_THAT(fast.quadrature.weight, WithinAbs(reference.quadrature.weight, 1.0e-12));
mapper.SetGeometryCacheEnabled(true);
return fast.quadrature.weight;
};
const double originalWeight = compare(point);
CHECK(mapper.GetGeometryCacheStatistics().misses == 1);
compare(point);
CHECK(mapper.GetGeometryCacheStatistics().hits == 1);
auto changedPoint = point;
changedPoint.x += 0.013;
compare(changedPoint);
CHECK(mapper.GetGeometryCacheStatistics().points == 2);
changedPoint.weight *= 0.5;
compare(changedPoint);
CHECK(mapper.GetGeometryCacheStatistics().points == 2); // weights are supplied by the caller
fixture.mesh.Transform([](const mfem::Vector &x, mfem::Vector &y) {
y = x; y(0) *= 1.2; y(1) += 0.17;
});
const double newWeight = compare(point);
CHECK(newWeight > originalWeight);
CHECK(mapper.GetGeometryCacheStatistics().points == 1);
CHECK(mapper.GetGeometryCacheStatistics().misses == 3);
mapper.ClearGeometryCache();
CHECK(mapper.GetGeometryCacheStatistics().points == 0);
compare(point);
CHECK(mapper.GetGeometryCacheStatistics().misses == 1);
}
TEST_CASE("Geometry Cache Does Not Freeze Displacement Or Compactification State", "[mapping][geometry-cache]") {
SingleElementFixture fixture;
mapping::DomainMapper mapper({.dimension = dimension, .vacuum_element_attribute = 3}, make_kelvin_compactification());
mapping::DomainMapper::Workspace workspace(dimension);
const auto &point = mfem::IntRules.Get(mfem::Geometry::CUBE, 3).IntPoint(0);
for (double scale : {0.0, 0.02, -0.01}) {
auto &transformation = *fixture.mesh.GetElementTransformation(0);
auto dofs = fixture.MakeZeroElementDofs(); dofs = scale;
mapping::ElementDisplacementData displacement(fixture.GetElement(), dofs);
auto compactificationDofs = make_constant_compactification_dofs(fixture.GetElement(), 0.1 + scale);
mapping::ElementCompactificationData compactification(fixture.GetElement(), compactificationDofs);
mapping::ElementMappingData data{displacement, compactification};
for (int attribute : {1, 3}) {
transformation.Attribute = attribute;
mapping::VolumeMappingContext fast, reference;
mapper.SetGeometryCacheEnabled(true);
const auto fastStatus = mapper.EvaluateVolume(data, transformation, point, workspace, fast);
mapper.SetGeometryCacheEnabled(false);
const auto referenceStatus = mapper.EvaluateVolume(data, transformation, point, workspace, reference);
REQUIRE(fastStatus == referenceStatus);
if (fastStatus == mapping::MappingStatus::valid) {
check_vector(fast.mapping.physical_position, reference.mapping.physical_position);
check_matrix(fast.mapping.mapping_jacobian, reference.mapping.mapping_jacobian);
check_matrix(fast.quadrature.J_inv, reference.quadrature.J_inv);
}
}
}
CHECK(mapper.GetGeometryCacheStatistics().points == 1);
}

View File

@@ -13,6 +13,7 @@
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
import test_helpers;
@@ -997,3 +998,83 @@ TEST_CASE(
CHECK(physicalInverse.IsCurrent());
CHECK(scaledInverse.IsCurrent());
}
TEST_CASE("Direct Reference Mass Diagonals Match Full True DOF Assembly", "[normalization][diagonal][assembly]") {
using namespace mean_field;
auto args = test_utils::setup_args();
auto f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
auto compare = [](const mfem::Vector &a, const mfem::Vector &b) {
REQUIRE(a.Size() == b.Size());
mfem::Vector difference(a); difference -= b;
CHECK(difference.Norml2() <= 2.0e-11 * std::max(1.0, b.Norml2()));
};
compare(normalization::AssembleReferenceHDivMassDiagonal(*f.gravityFluxFes),
normalization::AssembleReferenceHDivMassDiagonal(*f.gravityFluxFes, true));
for (auto *space : {f.gravityPotentialFes.get(), f.densityFes.get(), f.enthalpyFes.get()}) {
compare(normalization::AssembleReferenceScalarMassDiagonal(*space),
normalization::AssembleReferenceScalarMassDiagonal(*space, nullptr, true));
mfem::Array<int> marker(f.mesh->attributes.Max());
marker = 0; marker[f.mesh->GetAttribute(0)-1] = 1;
compare(normalization::AssembleReferenceScalarMassDiagonal(*space, &marker),
normalization::AssembleReferenceScalarMassDiagonal(*space, &marker, true));
}
auto surfaceMap = field::make_stellar_surface_scalar_dof_map(*f.surfaceDeformationFes);
// The stellar surface is a tagged internal interface, which an exterior-only
// boundary face restriction would omit even though volume diagonals pass.
using Schema = utils::domain::CoreEnvelopeVacuumDomainSchema;
constexpr int surfaceAttribute = Schema::boundary_attribute<utils::domain::StellarSurface>();
int localFaceCounts[2] = {0, 0}, globalFaceCounts[2] = {0, 0};
for (int b = 0; b < f.mesh->GetNBE(); ++b) {
if (f.mesh->GetBdrAttribute(b) != surfaceAttribute) continue;
++localFaceCounts[0];
const int face = f.mesh->GetBdrElementFaceIndex(b);
if (f.mesh->GetFaceInformation(face).IsOfFaceType(mfem::FaceType::Interior))
++localFaceCounts[1];
}
REQUIRE(MPI_Allreduce(localFaceCounts, globalFaceCounts, 2, MPI_INT, MPI_SUM,
f.surfaceDeformationFes->GetComm()) == MPI_SUCCESS);
REQUIRE(globalFaceCounts[0] > 0);
REQUIRE(globalFaceCounts[1] == globalFaceCounts[0]);
const auto surface = normalization::AssembleReferenceSurfaceMassDiagonal(f, surfaceMap);
const auto referenceSurface = normalization::AssembleReferenceSurfaceMassDiagonal(f, surfaceMap, true);
REQUIRE(surface.Size() == referenceSurface.Size());
for (int i = 0; i < surface.Size(); ++i) {
CAPTURE(i, surface(i), referenceSurface(i));
CHECK(std::isfinite(surface(i)));
CHECK(surface(i) > 0.0);
CHECK(std::isfinite(referenceSurface(i)));
CHECK(referenceSurface(i) > 0.0);
}
mfem::Vector difference(surface);
difference -= referenceSurface;
const double errorNorm = difference.Norml2(), referenceNorm = referenceSurface.Norml2();
const double localSquares[2] = {errorNorm * errorNorm, referenceNorm * referenceNorm};
double globalSquares[2] = {0.0, 0.0};
REQUIRE(MPI_Allreduce(localSquares, globalSquares, 2, MPI_DOUBLE, MPI_SUM,
f.surfaceDeformationFes->GetComm()) == MPI_SUCCESS);
REQUIRE(globalSquares[1] > 0.0);
CHECK(std::sqrt(globalSquares[0] / globalSquares[1]) <= 2.0e-11);
}
TEST_CASE("Nonconforming Reference Diagonals Retain Exact Assembled Cross Terms", "[normalization][diagonal][nonconforming]") {
using namespace mean_field;
auto serial = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON);
serial.EnsureNCMesh();
mfem::Array<int> refine(1); refine[0] = 0;
serial.GeneralRefinement(refine);
REQUIRE(serial.Nonconforming());
mfem::ParMesh mesh(MPI_COMM_WORLD, serial);
mfem::H1_FECollection h1(2, 3);
mfem::RT_FECollection rt(1, 3);
mfem::ParFiniteElementSpace scalar(&mesh, &h1), flux(&mesh, &rt);
auto actual = normalization::AssembleReferenceScalarMassDiagonal(scalar);
auto expected = normalization::AssembleReferenceScalarMassDiagonal(scalar, nullptr, true);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
actual = normalization::AssembleReferenceHDivMassDiagonal(flux);
expected = normalization::AssembleReferenceHDivMassDiagonal(flux, true);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
}

View File

@@ -601,6 +601,15 @@ TEST_CASE(
CHECK(preparedOperator.IsPrepared());
CHECK(preparedOperator.GetResidualPreparationCount() == 2);
CHECK(preparedOperator.GetResidualApplicationCount() == 1);
mfem::Vector updatedResidual, referenceResidual;
preparedOperator.BuildResidual(updatedResidual);
CHECK(preparedOperator.PrepareReference().DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
CHECK(gravity_displacement_force_test_utils::relative_difference(
updatedResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareReference().DidAnyWork());
CHECK(preparedOperator.Prepare().DidAnyWork());
CHECK_FALSE(preparedOperator.Prepare().DidAnyWork());
}
TEST_CASE(
@@ -665,6 +674,19 @@ TEST_CASE(
densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, completeAction
);
mfem::Vector retainedAction;
preparedOperator.ApplyCompleteJacobianActionReference(
densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, retainedAction);
CHECK(preparedOperator.GetFastElementCount() > 0);
CHECK(preparedOperator.GetTensorElementCount() == preparedOperator.GetFastElementCount());
CHECK(gravity_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
preparedOperator.ApplyCompleteJacobianActionCachedReference(
densityDirectionReduced, displacementDirectionReduced, gravityGradientDirectionReduced, retainedAction);
CHECK(gravity_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
mfem::Vector summedColumns(densityAction);
summedColumns += gravityAction;
summedColumns += displacementAction;

View File

@@ -312,6 +312,10 @@ TEST_CASE(
operation.ApplyCompleteJacobianAction(
reducedDensityDirection, reducedDisplacementDirection, angularVelocityDirection, completeAction
);
mfem::Vector preparedReference;
operation.ApplyCompleteJacobianActionReference(
reducedDensityDirection, reducedDisplacementDirection, angularVelocityDirection, preparedReference);
CHECK(angular_momentum_test_utils::relativeError(completeAction(0), preparedReference(0)) < 2.0e-11);
CHECK(
angular_momentum_test_utils::relativeError(
completeAction(0), densityAction(0) + geometryAction(0) + angularVelocityAction(0)
@@ -409,10 +413,26 @@ TEST_CASE(
operation.Prepare(0.52, dependencies);
const auto preparationCount = operation.GetPreparationCount();
const double moment = operation.GetMomentOfInertia();
CHECK(operation.GetJacobianRowPreparationCount() == 0);
const auto rhoDirection = gravityContext.GetDensityMap().gather(
angular_momentum_test_utils::projectDensityDirection(finiteElements, 0.67));
const auto uDirection = gravityContext.GetDisplacementMap().gather(
angular_momentum_test_utils::projectDisplacementDirection(finiteElements, -0.53));
auto checkRows = [&](std::uint64_t expectedBuilds) {
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == expectedBuilds);
mfem::Vector actual, reference;
operation.ApplyCompleteJacobianAction(rhoDirection, uDirection, -0.19, actual);
operation.ApplyCompleteJacobianActionReference(rhoDirection, uDirection, -0.19, reference);
CHECK(angular_momentum_test_utils::relativeError(actual(0), reference(0)) < 2.0e-11);
CHECK(operation.GetJacobianRowPreparationCount() == expectedBuilds);
};
checkRows(1);
const auto repeated = operation.Prepare(0.52, dependencies);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(operation.GetPreparationCount() == preparationCount);
checkRows(1);
++gravityPotentialRevision;
angular_momentum_test_utils::prepareGravityContext(
@@ -420,6 +440,7 @@ TEST_CASE(
);
const auto unrelatedPotential = operation.Prepare(0.52, dependencies);
CHECK_FALSE(unrelatedPotential.DidAnyWork());
checkRows(1);
const double residualBeforeRotation = angular_momentum_test_utils::residual(operation);
++dependencies.rotation.revision;
@@ -429,6 +450,7 @@ TEST_CASE(
CHECK_FALSE(rotationOnly.refreshedDensity);
CHECK_FALSE(rotationOnly.refreshedGeometry);
CHECK(operation.GetMomentOfInertia() == moment);
checkRows(1);
CHECK(
angular_momentum_test_utils::residual(operation) - residualBeforeRotation ==
Catch::Approx((0.81 - 0.52) * moment).epsilon(3.0e-15)
@@ -443,6 +465,8 @@ TEST_CASE(
CHECK(densityOnly.refreshedDensity);
CHECK_FALSE(densityOnly.refreshedGeometry);
CHECK_FALSE(densityOnly.updatedAngularVelocity);
CHECK(operation.GetJacobianRowPreparationCount() == 1);
checkRows(2);
displacement = angular_momentum_test_utils::projectDisplacementDirection(finiteElements, 0.87);
++dependencies.displacement.revision;
@@ -453,6 +477,8 @@ TEST_CASE(
CHECK(geometryOnly.refreshedGeometry);
CHECK_FALSE(geometryOnly.refreshedDensity);
CHECK_FALSE(geometryOnly.updatedAngularVelocity);
CHECK(operation.GetJacobianRowPreparationCount() == 2);
checkRows(3);
}
TEST_CASE(
@@ -488,6 +514,8 @@ TEST_CASE(
const auto initial = operation.TryPrepare(angularVelocity, dependencies);
REQUIRE(initial.has_value());
const std::uint64_t initialPreparationCount = operation.GetPreparationCount();
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == 1);
const std::uint64_t successfulPreparations = initialPreparationCount;
@@ -513,6 +541,8 @@ TEST_CASE(
const auto recovered = operation.TryPrepare(angularVelocity, dependencies);
REQUIRE(recovered.has_value());
CHECK(operation.IsPrepared());
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == 2);
++dependencies.rotation.revision;
const auto nonFiniteAngularVelocity = operation.TryPrepare(std::numeric_limits<double>::quiet_NaN(), dependencies);
@@ -527,4 +557,6 @@ TEST_CASE(
++dependencies.rotation.revision;
REQUIRE(operation.TryPrepare(angularVelocity, dependencies).has_value());
CHECK(operation.IsPrepared());
REQUIRE(operation.PrepareJacobianRows());
CHECK(operation.GetJacobianRowPreparationCount() == 3);
}

View File

@@ -526,6 +526,18 @@ namespace prepared_barotropic_closure_test_utils {
const MPI_Comm communicator = f.mesh->GetComm();
mfem::Vector retained, enthalpyOnly, displacementOnly;
preparedOperator.MultReference(densityVariation, enthalpyVariation, displacementVariation, retained);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyVariation, enthalpyOnly);
preparedOperator.ApplyDisplacementJacobianAction(displacementVariation, displacementOnly);
CHECK(relative_error(preparedCompleteAction, retained, communicator) < 2.0e-12);
CHECK(relative_error(enthalpyOnly, referenceEnthalpyAction, communicator) < 2.0e-12);
CHECK(relative_error(displacementOnly, referenceDisplacementAction, communicator) < 2.0e-12);
preparedOperator.ApplyEnthalpyJacobianAction(zeroEnthalpy, enthalpyOnly);
preparedOperator.ApplyDisplacementJacobianAction(zeroDisplacement, displacementOnly);
CHECK(enthalpyOnly.Norml2() == 0.0);
CHECK(displacementOnly.Norml2() == 0.0);
CHECK(relative_error(preparedResidual, referenceResidual, communicator) < 2.0e-12);
CHECK(relative_error(preparedDensityAction, referenceDensityAction, communicator) < 2.0e-12);
CHECK(relative_error(preparedEnthalpyAction, referenceEnthalpyAction, communicator) < 2.0e-12);
@@ -831,6 +843,16 @@ namespace prepared_barotropic_closure_test_utils {
mfem::Vector preparedAction;
preparedOperator.BuildResidual(preparedResidual);
preparedOperator.Mult(densityVariation, enthalpyVariation, displacementVariation, preparedAction);
CHECK(preparedOperator.GetTensorElementCount() > 0);
mfem::Vector cached, tensorColumn, cachedColumn;
preparedOperator.MultCachedReference(densityVariation, enthalpyVariation, displacementVariation, cached);
CHECK(relative_error(preparedAction, cached, communicator) < 2.0e-11);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyVariation, tensorColumn);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyVariation, cachedColumn, false);
CHECK(relative_error(tensorColumn, cachedColumn, communicator) < 2.0e-11);
preparedOperator.ApplyDisplacementJacobianAction(displacementVariation, tensorColumn);
preparedOperator.ApplyDisplacementJacobianAction(displacementVariation, cachedColumn, false);
CHECK(relative_error(tensorColumn, cachedColumn, communicator) < 2.0e-11);
mfem::Vector fullReferenceResidual;
mean_field::operators::kernels::apply_barotropic_closure(

View File

@@ -69,6 +69,7 @@ TEST_CASE(
REQUIRE(f.okay());
operators::PreparedMappedGravitySourceOperator operation(f, *f.domainMapperStateless);
operators::PreparedMappedGravitySourceOperator oldWeights(f, *f.domainMapperStateless);
const mfem::Vector densityTrue = prepared_test::make_deterministic_vector(f.densityFes->GetTrueVSize(), 0.41);
const mfem::Vector density = operation.GetDensityMap().gather(densityTrue);
const mfem::Vector displacementTrue = prepared_test::make_displacement(f, 0.4);
@@ -81,6 +82,21 @@ TEST_CASE(
mfem::Vector baselineVariation;
operation.Mult(density, baselineAction);
operation.MultDisplacementVariationTrue(densityTrue, directionTrue, baselineVariation);
REQUIRE(operation.GetTensorVariationElementCount() > 0);
mfem::Vector pointwiseVariation, zeroDirection(directionTrue), zeroVariation;
operation.MultDisplacementVariationTrueReference(densityTrue, directionTrue, pointwiseVariation);
CHECK(prepared_test::relative_error(baselineVariation, pointwiseVariation, communicator) < 2.0e-12);
zeroDirection = 0.0;
operation.MultDisplacementVariationTrue(densityTrue, zeroDirection, zeroVariation);
CHECK(zeroVariation.Norml2() == 0.0);
operation.MultDisplacementVariationTrue(densityTrue, directionTrue, pointwiseVariation);
CHECK(prepared_test::relative_error(baselineVariation, pointwiseVariation, communicator) < 2.0e-14);
oldWeights.PrepareReference(displacement);
mfem::Vector oldAction, oldVariation;
oldWeights.Mult(density, oldAction);
oldWeights.MultDisplacementVariationTrue(densityTrue, directionTrue, oldVariation);
CHECK(prepared_test::relative_error(baselineAction, oldAction, communicator) < 2.0e-14);
CHECK(prepared_test::relative_error(baselineVariation, oldVariation, communicator) < 2.0e-14);
const mfem::Vector primalDisplacementTrue = prepared_test::make_displacement(f, 1.0);
operation.PreparePrimal(operation.GetDisplacementMap().gather(primalDisplacementTrue));

View File

@@ -1,6 +1,7 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cmath>
#include <memory>
#include <mfem.hpp>
#include <stdexcept>
@@ -12,6 +13,27 @@ using Catch::Matchers::WithinAbs;
namespace prepared_test = gravity_prepared_test_utils;
namespace {
// Deliberately not a KelvinCompactification dynamic type: the prepared
// operator must keep arbitrary exterior maps on the general derivative.
class DelegatingExteriorMap final : public mapping::compactification::ExteriorDomainMap {
public:
explicit DelegatingExteriorMap(mapping::compactification::options::KelvinCompactificationOptions options)
: kelvin(options) {}
mapping::MappingStatus Evaluate(const mapping::compactification::ExteriorMapInput &input,
mapping::compactification::ExteriorMapResult &result) const override {
return kelvin.Evaluate(input, result);
}
mapping::MappingStatus EvaluateVariation(const mapping::compactification::ExteriorMapInput &input,
const mapping::compactification::ExteriorMapResult &result,
const mapping::compactification::ExteriorMapDirection &direction,
mapping::compactification::ExteriorMapVariation &variation) const override {
return kelvin.EvaluateVariation(input, result, direction, variation);
}
std::string_view GetName() const noexcept override { return "DelegatingExteriorMap"; }
private:
mapping::compactification::KelvinCompactification kelvin;
};
[[nodiscard]] mfem::Vector make_folding_displacement(const mean_field::fem::FEM &f) {
mfem::ParGridFunction field(f.displacementFes.get());
mfem::VectorFunctionCoefficient coefficient(
@@ -65,9 +87,15 @@ TEST_CASE(
const mfem::Vector displacementTrue = prepared_test::make_displacement(f, scale);
const mfem::Vector displacement = preparedOperator.GetDisplacementMap().gather(displacementTrue);
preparedOperator.Prepare(displacement);
CHECK(preparedOperator.GetFastVariationElementCount() == f.mesh->GetNE());
CHECK(preparedOperator.GetTensorVariationElementCount() == f.mesh->GetNE());
preparedOperator.MultDisplacementVariationTrue(first, direction, firstAction);
mfem::Vector referenceAction;
preparedOperator.MultDisplacementVariationTrueCachedReference(first, direction, referenceAction);
CHECK(prepared_test::relative_error(firstAction, referenceAction, communicator) < 2.0e-11);
preparedOperator.MultDisplacementVariationTrueReference(first, direction, referenceAction);
CHECK(prepared_test::relative_error(firstAction, referenceAction, communicator) < 2.0e-11);
operators::kernels::apply_mapped_hdiv_mass_variation(
f, *f.domainMapperStateless, first, displacementTrue, direction, referenceAction
);
@@ -85,6 +113,39 @@ TEST_CASE(
CHECK(preparedOperator.GetPreparationCount() == 2);
}
TEST_CASE("Prepared Hdiv Tangent Retains General Exterior Map Fallback",
tags::gravity_prepared_jacobian_accuracy) {
auto args = test_utils::setup_args();
auto f = fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
mapping::DomainMapper mapper(args.domain_mapper_options, std::make_unique<DelegatingExteriorMap>(args.kelvin_options));
operators::PreparedMappedHDivMassOperator prepared(f, mapper);
const auto base = prepared_test::make_displacement(f, 0.7);
const auto direction = prepared_test::make_displacement(f, 0.43);
const auto flux = prepared_test::make_deterministic_vector(f.gravityFluxFes->GetTrueVSize(), 0.31);
prepared.Prepare(prepared.GetDisplacementMap().gather(base));
int stellarElements = 0;
for (int element = 0; element < f.mesh->GetNE(); ++element)
stellarElements += !mapper.IsCompactifiedElement(*f.mesh->GetElementTransformation(element));
CHECK(prepared.GetFastVariationElementCount() == stellarElements);
mfem::Vector actual, expected;
prepared.MultDisplacementVariationTrue(flux, direction, actual);
operators::kernels::apply_mapped_hdiv_mass_variation(f, mapper, flux, base, direction, expected);
CHECK(prepared_test::relative_error(actual, expected, f.mesh->GetComm()) < 2.0e-11);
prepared.PreparePrimal(prepared.GetDisplacementMap().gather(base));
CHECK_FALSE(prepared.HasVariationData());
CHECK(prepared.GetFastVariationElementCount() == 0);
CHECK(prepared.GetFastMassElementCount() == f.mesh->GetNE());
mfem::Vector primal, primalReference;
const auto reducedFlux = prepared.GetFluxMap().gather(flux);
prepared.Mult(reducedFlux, primal);
prepared.MultReference(reducedFlux, primalReference);
CHECK(prepared_test::relative_error(primal, primalReference, f.mesh->GetComm()) < 2.0e-11);
prepared.Prepare(prepared.GetDisplacementMap().gather(base));
prepared.MultDisplacementVariationTrue(flux, direction, actual);
CHECK(prepared_test::relative_error(actual, expected, f.mesh->GetComm()) < 2.0e-11);
}
TEST_CASE(
"Prepared Mapped Hdiv Mass Reports Invalid Candidate Geometry Without Unwinding",
tags::gravity_prepared_unit &tags::geometry
@@ -144,6 +205,13 @@ TEST_CASE(
mfem::Vector prepared_action;
prepared_operator.Mult(gravity_gradient, prepared_action);
CHECK(prepared_operator.GetFastMassElementCount() == f.mesh->GetNE());
mfem::Vector mfemAction;
prepared_operator.MultReference(gravity_gradient, mfemAction);
CHECK(prepared_test::relative_error(prepared_action, mfemAction, communicator) < 2.0e-11);
// Reference calls must restore the optimized mode and leave PA data intact.
prepared_operator.Mult(gravity_gradient, mfemAction);
CHECK(prepared_test::relative_error(prepared_action, mfemAction, communicator) < 2.0e-14);
mfem::Vector reference_action_true;
operators::kernels::apply_mapped_hdiv_mass(
f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, reference_action_true

View File

@@ -361,3 +361,80 @@ TEST_CASE(
CHECK(recovered->preparedResidual);
CHECK(preparedOperator.IsPrepared());
}
TEST_CASE(
"Hydrostatic Tensor Preparation Matches Pointwise Preparation And Rebuilds On Mode Changes",
tags::barotrope_hydrostatic_prepared_residual &tags::unit
) {
const auto args = test_utils::setup_args();
auto f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
using namespace prepared_hydrostatic_test_utils;
mean_field::operators::PreparedHydrostaticEquilibriumOperator fast(f, *f.domainMapperStateless);
mean_field::operators::PreparedHydrostaticEquilibriumOperator reference(f, *f.domainMapperStateless);
auto h = make_enthalpy(f), phi = make_gravity_potential(f);
const auto dh = make_enthalpy(f, 0.53), dp = make_gravity_potential(f, 0.71);
const auto du = field_dof_test_utils::make_supported_displacement(f, 0.31);
const auto rotation = make_rotation();
auto dependencies = make_dependencies();
for (double scale : {0.0, 0.42, 0.73}) {
CAPTURE(scale);
const auto displacement = field_dof_test_utils::make_supported_displacement(f, scale);
h *= 1.01; phi *= 0.99;
++dependencies.displacement.revision;
++dependencies.enthalpy.revision;
++dependencies.gravityPotential.revision;
const auto state = make_state(h, phi, displacement, 0.39);
REQUIRE(fast.Prepare(state, dependencies, rotation).DidAnyWork());
REQUIRE(reference.PrepareReference(state, dependencies, rotation).DidAnyWork());
CHECK(fast.GetTensorDisplacementElementCount() > 0);
const auto compare = [&](auto &&apply) {
mfem::Vector a, b;
apply(fast, a); apply(reference, b);
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-11);
};
compare([](auto &op, auto &out) { op.BuildResidual(out); });
compare([&](auto &op, auto &out) { op.ApplyEnthalpyJacobianAction(dh, out); });
compare([&](auto &op, auto &out) { op.ApplyGravityPotentialJacobianAction(dp, out); });
compare([](auto &op, auto &out) { op.ApplyBernoulliConstantJacobianAction(0.13, out); });
compare([&](auto &op, auto &out) { op.ApplyDisplacementJacobianAction(du, out); });
compare([&](auto &op, auto &out) { op.ApplyCompleteJacobianAction(dh, dp, 0.13, du, out); });
compare([](auto &op, auto &out) { op.AssembleEnthalpyJacobianDiagonal(out); });
CHECK_FALSE(fast.Prepare(state, dependencies, rotation).DidAnyWork());
CHECK_FALSE(reference.PrepareReference(state, dependencies, rotation).DidAnyWork());
CHECK(fast.PrepareReference(state, dependencies, rotation).DidAnyWork());
compare([](auto &op, auto &out) { op.BuildResidual(out); });
CHECK_FALSE(fast.PrepareReference(state, dependencies, rotation).DidAnyWork());
CHECK(fast.Prepare(state, dependencies, rotation).DidAnyWork());
compare([&](auto &op, auto &out) { op.ApplyCompleteJacobianAction(dh, dp, 0.13, du, out); });
CHECK_FALSE(fast.Prepare(state, dependencies, rotation).DidAnyWork());
}
mfem::ParGridFunction foldedField(f.displacementFes.get());
mfem::VectorFunctionCoefficient fold(3, [](const mfem::Vector &x, mfem::Vector &u) {
u = x; u *= -2.0;
});
foldedField.ProjectCoefficient(fold);
mfem::Vector foldedTrue; foldedField.GetTrueDofs(foldedTrue);
const auto folded = fast.GetDisplacementMap().gather(foldedTrue);
++dependencies.displacement.revision;
const auto rejectedState = make_state(h, phi, folded, 0.39);
const auto rejected = fast.TryPrepare(rejectedState, dependencies, rotation);
const auto referenceRejected = reference.TryPrepareReference(rejectedState, dependencies, rotation);
REQUIRE_FALSE(rejected.has_value());
REQUIRE_FALSE(referenceRejected.has_value());
CHECK(rejected.error().reason == referenceRejected.error().reason);
CHECK(rejected.error().mappingStatus == mean_field::mapping::MappingStatus::non_positive_determinant);
CHECK_FALSE(fast.IsPrepared());
CHECK_FALSE(reference.IsPrepared());
const auto recovered = make_state(h, phi, du, 0.39);
++dependencies.displacement.revision;
REQUIRE(fast.TryPrepare(recovered, dependencies, rotation).has_value());
REQUIRE(reference.TryPrepareReference(recovered, dependencies, rotation).has_value());
mfem::Vector a, b;
fast.BuildResidual(a); reference.BuildResidual(b);
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-11);
}

View File

@@ -198,6 +198,16 @@ TEST_CASE(
CHECK(statistics.applications == 3);
CHECK(centeredDifferenceError < 2.0e-7);
CHECK(linearityError < 5.0e-12);
int stellarElements = 0;
preparedOperator.VisitMappedGeometryRules([&](int, const mfem::IntegrationRule &) { ++stellarElements; });
CHECK(preparedOperator.GetFastDisplacementElementCount() == stellarElements);
mfem::Vector preparedReference;
preparedOperator.ApplyDisplacementJacobianActionReference(firstVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
firstAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
preparedOperator.ApplyDisplacementJacobianActionReference(secondVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
secondAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
}
TEST_CASE(
@@ -344,4 +354,13 @@ TEST_CASE(
CHECK(displacementStatistics.applications == 4);
CHECK(geometryReferenceError < 2.0e-7);
CHECK(geometryEffect > 1.0e-8);
mfem::Vector preparedReference;
preparedOperator.ApplyDisplacementJacobianActionReference(displacementVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
geometryUpdatedAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
preparedOperator.ApplyDisplacementJacobianActionCachedReference(displacementVariation, preparedReference);
CHECK(prepared_hydrostatic_displacement_test_utils::relative_error(
geometryUpdatedAction, preparedReference, f.mesh->GetComm()) < 2.0e-11);
CHECK(preparedOperator.GetTensorDisplacementElementCount() > 0);
CHECK(preparedOperator.GetDisplacementJacobianStatistics().applications == 4);
}

View File

@@ -439,10 +439,26 @@ TEST_CASE(
massOperator.Prepare({.targetMass = 1.0}, dependencies);
const std::uint64_t preparationCount = massOperator.GetPreparationCount();
CHECK(massOperator.GetJacobianRowPreparationCount() == 0);
const auto rhoDirection = gravityContext.GetDensityMap().gather(
mass_normalization_test_utils::make_density_direction(f, 0.67));
const auto uDirection = gravityContext.GetDisplacementMap().gather(
mass_normalization_test_utils::make_displacement_direction(f, -0.53));
auto checkRows = [&](std::uint64_t expectedBuilds) {
REQUIRE(massOperator.PrepareJacobianRows());
CHECK(massOperator.GetJacobianRowPreparationCount() == expectedBuilds);
mfem::Vector actual, reference;
massOperator.ApplyCompleteJacobianAction(rhoDirection, uDirection, actual);
massOperator.ApplyCompleteJacobianActionReference(rhoDirection, uDirection, reference);
CHECK(mass_normalization_test_utils::relative_error(actual(0), reference(0)) < 2.0e-11);
CHECK(massOperator.GetJacobianRowPreparationCount() == expectedBuilds);
};
checkRows(1);
const auto repeated = massOperator.Prepare({.targetMass = 1.0}, dependencies);
CHECK_FALSE(repeated.DidAnyWork());
CHECK(massOperator.GetPreparationCount() == preparationCount);
checkRows(1);
++gravityPotentialRevision;
mass_normalization_test_utils::prepare_gravity_context(
@@ -451,6 +467,7 @@ TEST_CASE(
const auto potentialOnly = massOperator.Prepare({.targetMass = 1.0}, dependencies);
CHECK_FALSE(potentialOnly.DidAnyWork());
checkRows(1);
const double residualBeforeTargetChange = mass_normalization_test_utils::residual_value(massOperator);
const double massBeforeTargetChange = massOperator.GetCurrentMass();
@@ -463,6 +480,7 @@ TEST_CASE(
CHECK_FALSE(targetOnly.refreshedGeometry);
CHECK_FALSE(targetOnly.refreshedDensity);
CHECK(massOperator.GetCurrentMass() == massBeforeTargetChange);
checkRows(1);
CHECK(
mass_normalization_test_utils::relative_error(
mass_normalization_test_utils::residual_value(massOperator) - residualBeforeTargetChange, -0.4
@@ -481,6 +499,8 @@ TEST_CASE(
CHECK(densityOnly.assembledResidual);
CHECK_FALSE(densityOnly.refreshedGeometry);
CHECK(massOperator.GetCurrentMass() != massBeforeDensityChange);
CHECK(massOperator.GetJacobianRowPreparationCount() == 1);
checkRows(2);
displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.87);
++dependencies.displacement.revision;
@@ -492,6 +512,8 @@ TEST_CASE(
CHECK(geometryOnly.refreshedGeometry);
CHECK(geometryOnly.assembledResidual);
CHECK_FALSE(geometryOnly.refreshedDensity);
CHECK(massOperator.GetJacobianRowPreparationCount() == 2);
checkRows(3);
}
TEST_CASE(
@@ -587,6 +609,11 @@ TEST_CASE(
massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, displacementAction);
massOperator.ApplyCompleteJacobianAction(reducedDensityDirection, reducedDisplacementDirection, completeAction);
mfem::Vector preparedReference;
massOperator.ApplyCompleteJacobianActionReference(
reducedDensityDirection, reducedDisplacementDirection, preparedReference);
CHECK(mass_normalization_test_utils::relative_error(completeAction(0), preparedReference(0)) < 2.0e-11);
CHECK(
mass_normalization_test_utils::relative_error(completeAction(0), densityAction(0) + displacementAction(0)) <
2.0e-15

View File

@@ -439,6 +439,15 @@ TEST_CASE(
mfem::Vector kernelDisplacementActionTrue;
preparedOperator.ApplyDisplacementJacobianAction(displacementDirection, preparedDisplacementAction);
REQUIRE(preparedOperator.GetTensorElementCount() > 0);
mfem::Vector cachedDisplacementAction;
preparedOperator.ApplyDisplacementJacobianActionCachedReference(displacementDirection, cachedDisplacementAction);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedDisplacementAction, cachedDisplacementAction, f.mesh->GetComm()) < 2.0e-12);
mfem::Vector retainedDisplacementAction;
preparedOperator.ApplyDisplacementJacobianActionReference(displacementDirection, retainedDisplacementAction);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedDisplacementAction, retainedDisplacementAction, f.mesh->GetComm()) < 2.0e-12);
mean_field::operators::kernels::apply_pressure_force_displacement_action(
f, *f.domainMapperStateless, equationOfState, enthalpyTrue, displacementDirectionTrue, displacementTrue,
@@ -465,6 +474,30 @@ TEST_CASE(
prepared_pressure_force_test_utils::relative_difference(fusedAction, expectedFusedAction, f.mesh->GetComm()) <
2.0e-12
);
mfem::Vector originalResidual, referenceResidual, referenceEnthalpy;
preparedOperator.BuildResidual(originalResidual);
const auto state = mean_field::operators::context::pressure_force::PressureForceStateView{
.enthalpy = enthalpy, .displacement = displacement};
const auto dependencies = prepared_pressure_force_test_utils::make_dependencies();
CHECK(preparedOperator.PrepareReference(state, dependencies).DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, referenceEnthalpy);
CHECK(prepared_pressure_force_test_utils::relative_difference(
originalResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedEnthalpyAction, referenceEnthalpy, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareReference(state, dependencies).DidAnyWork());
CHECK(preparedOperator.PrepareCachedReference(state, dependencies).DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
preparedOperator.ApplyEnthalpyJacobianAction(enthalpyDirection, referenceEnthalpy);
CHECK(prepared_pressure_force_test_utils::relative_difference(
originalResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK(prepared_pressure_force_test_utils::relative_difference(
preparedEnthalpyAction, referenceEnthalpy, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareCachedReference(state, dependencies).DidAnyWork());
CHECK(preparedOperator.Prepare(state, dependencies).DidAnyWork());
CHECK_FALSE(preparedOperator.Prepare(state, dependencies).DidAnyWork());
}
TEST_CASE(

View File

@@ -551,6 +551,19 @@ TEST_CASE(
CHECK(rotationReport.preparedResidual);
CHECK(preparedOperator.GetResidualPreparationCount() == 3);
CHECK(preparedOperator.GetResidualApplicationCount() == 1);
mfem::Vector updatedResidual, referenceResidual;
preparedOperator.BuildResidual(updatedResidual);
CHECK(preparedOperator.PrepareReference(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
preparedOperator.BuildResidual(referenceResidual);
CHECK(rotational_displacement_force_test_utils::relative_difference(
updatedResidual, referenceResidual, f.mesh->GetComm()) < 2.0e-12);
CHECK_FALSE(preparedOperator.PrepareReference(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
CHECK(preparedOperator.Prepare(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
CHECK_FALSE(preparedOperator.Prepare(
{.density = density, .displacement = displacement}, dependencies, rotation).DidAnyWork());
}
TEST_CASE(
@@ -598,6 +611,16 @@ TEST_CASE(
preparedOperator.ApplyCompleteJacobianAction(densityDirection, displacementDirection, completeAction);
mfem::Vector retainedAction;
preparedOperator.ApplyCompleteJacobianActionReference(densityDirection, displacementDirection, retainedAction);
CHECK(preparedOperator.GetFastElementCount() > 0);
CHECK(preparedOperator.GetTensorElementCount() == preparedOperator.GetFastElementCount());
CHECK(rotational_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
preparedOperator.ApplyCompleteJacobianActionCachedReference(densityDirection, displacementDirection, retainedAction);
CHECK(rotational_displacement_force_test_utils::relative_difference(
completeAction, retainedAction, f.mesh->GetComm()) < 2.0e-12);
mfem::Vector summedColumns(densityAction);
summedColumns += displacementAction;

View File

@@ -2,6 +2,7 @@
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <catch2/catch_approx.hpp>
@@ -313,6 +314,8 @@ TEST_CASE(
preconditioning::GravityApproximateLDU{}
);
auto chebyshevPrepared = preconditioning::prepare(geometry.finiteElements, geometry.context, chebyshevBlock);
CHECK(prepared.GetDivergenceAssemblyCount() == 1);
CHECK(chebyshevPrepared.GetDivergenceAssemblyCount() == 1);
const auto unchanged = prepared.Refresh(geometry.finiteElements, geometry.context);
const auto unchangedChebyshev = chebyshevPrepared.Refresh(geometry.finiteElements, geometry.context);
@@ -338,6 +341,8 @@ TEST_CASE(
const std::uint64_t sourcePreparations = geometry.context.GetSourceOperator().GetPreparationCount();
const auto changed = prepared.Refresh(geometry.finiteElements, geometry.context);
const auto changedChebyshev = chebyshevPrepared.Refresh(geometry.finiteElements, geometry.context);
CHECK(prepared.GetDivergenceAssemblyCount() == 1);
CHECK(chebyshevPrepared.GetDivergenceAssemblyCount() == 1);
CHECK(changed.geometryChanged);
CHECK_FALSE(changed.discretizationChanged);
@@ -377,6 +382,7 @@ TEST_CASE(
CHECK(reconstructed.rebuiltPotentialSchur);
CHECK(chebyshevPrepared.IsCurrent());
CHECK(chebyshevPrepared.GetMassInverse().GetStatistics().setups == 3);
CHECK(chebyshevPrepared.GetDivergenceAssemblyCount() == 2);
mfem::Vector firstReconstructedAction(chebyshevPrepared.Height());
mfem::Vector secondReconstructedAction(chebyshevPrepared.Height());
@@ -391,3 +397,52 @@ TEST_CASE(
CHECK(std::isfinite(firstReconstructedAction(index)));
}
}
TEST_CASE(
"Fixed Gravity Divergence Cache Preserves Changing Mass Weights And Invalidates",
tags::preconditioning_gravity_integration
) {
const auto args = test_utils::setup_args();
auto f = mean_field::fem::setup_fem(args.mesh_file, args, 0);
REQUIRE(f.okay());
preconditioning::PreparedGravityDivergenceCache cache;
mfem::Vector diagonal(f.gravityFluxFes->GetTrueVSize()); diagonal = 1.0;
const auto x = gravity_prepared_test_utils::make_deterministic_vector(f.gravityPotentialFes->GetTrueVSize(), 0.37);
auto initial = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache);
mfem::Vector initialAction(initial->Height()); initial->Mult(x, initialAction);
REQUIRE(initialAction.Norml2() > 0.0);
REQUIRE(cache.GetBuildCount() == 1);
for (double scale : {2.0, 0.5, 1.0}) {
CAPTURE(scale);
diagonal = scale;
auto fast = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache);
auto reference = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal);
mfem::Vector a(fast->Height()), b(reference->Height()), expected(initialAction);
fast->Mult(x, a); reference->Mult(x, b); expected /= scale;
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-12);
CHECK(gravity_prepared_test_utils::relative_error(a, expected, f.mesh->GetComm()) < 2.0e-12);
CHECK(cache.GetBuildCount() == 1);
initial->Mult(x, a);
CHECK(gravity_prepared_test_utils::relative_error(a, initialAction, f.mesh->GetComm()) < 2.0e-14);
}
for (double bad : {0.0, -1.0, std::numeric_limits<double>::quiet_NaN()}) {
diagonal = bad;
CHECK_THROWS_AS(
preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache), std::invalid_argument);
CHECK(cache.GetBuildCount() == 1);
}
diagonal = 1.0;
const auto checkRebuild = [&] {
auto fast = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal, &cache);
auto reference = preconditioning::assembleGravityPotentialSchurSurrogate(f, diagonal);
mfem::Vector a(fast->Height()), b(reference->Height());
fast->Mult(x, a); reference->Mult(x, b);
CHECK(gravity_prepared_test_utils::relative_error(a, b, f.mesh->GetComm()) < 2.0e-12);
};
cache.Clear(); checkRebuild();
CHECK(cache.GetBuildCount() == 2);
f.mesh->NodesUpdated(); checkRebuild();
CHECK(cache.GetBuildCount() == 3);
checkRebuild();
CHECK(cache.GetBuildCount() == 3);
}

View File

@@ -1552,3 +1552,48 @@ TEST_CASE(
CHECK(report.lastAcceptedCheckpointView().valid());
}
}
TEST_CASE("Skipping Geometry Preflight Retains Trial Acceptance And Fold Rejection",
"[solver][newton][geometry][preflight][backtracking][wiring]") {
using namespace mean_field;
using namespace stellar_solver_architecture_test;
for (bool folding : {false, true}) {
CAPTURE(folding);
auto finiteElements = makeFiniteElements();
auto discretization = equilibrium::makeStellarDiscretization(
std::move(finiteElements),
normalization::PhysicalRieszDiagonal{dimensions::LengthValue{utils::RADIUS}, utils::G});
auto context = solver::makeContext(
makeModel(), std::move(discretization), preconditioning::makePreconditioner(),
ScriptedBackend{nullptr, folding ? -10.0 : 0.0, false, true});
auto metricState = std::make_shared<MetricSequenceState>();
metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}};
bool observed = false;
auto observer = solver::nonlinear::makeObserver(
[](const solver::nonlinear::BeforeIteration &) {},
[&](const solver::nonlinear::AfterIteration &event) {
observed = true;
CHECK_FALSE(event.geometryPreflight.has_value());
CHECK(event.geometryPreflightSeconds == 0.0);
CHECK(event.lineSearchTrials == 1);
CHECK(event.stepAccepted == !folding);
});
auto newton = solver::nonlinear::Newton(
solver::nonlinear::NewtonOptions{
.relativeTolerance = 0.0, .absoluteTolerance = 0.0, .maximumIterations = 1,
.linearSolve = {.relativeTolerance = 0.0, .absoluteTolerance = std::numeric_limits<double>::max(),
.maximumIterations = 1},
.backtracking = {.maximumTrials = 1, .geometryPreflight = false}},
SequencedMetric{metricState});
auto solverInstance = solver::make(context, std::move(newton), std::move(observer));
const auto report = solverInstance.evaluate();
CHECK(observed);
CHECK_FALSE(report.diagnostics().lastGeometryPreflight.has_value());
CHECK(report.diagnostics().totalGeometryPreflightSeconds == 0.0);
CHECK(report.diagnostics().geometryLimitedIterations == 0);
CHECK(report.diagnostics().totalLineSearchTrials == 1);
CHECK(report.diagnostics().acceptedNonlinearIterations == (folding ? 0 : 1));
CHECK(report.diagnostics().inadmissibleLineSearchTrials == (folding ? 1 : 0));
CHECK(report.lastAcceptedCheckpointView().valid());
}
}