Reuse prepared gravity force for stellar cross preconditioning

This commit is contained in:
2026-09-17 16:28:56 -04:00
parent 7f7d7b1cfa
commit fac147774a
3 changed files with 131 additions and 2 deletions

View File

@@ -0,0 +1,40 @@
# Prepared gravity-to-material preconditioner action
The stellar-structure gravity-to-material coupling used
`PreparedDisplacementResidualOperator::ApplyGravityGradientJacobianAction`, which
delegates to the standalone gravity-gradient force column. That column evaluates
the legacy geometry kernel during each application. The full stellar Jacobian
instead uses the already prepared complete gravity-force action, including its
cached tensor path.
The cross preconditioner now calls that same prepared gravity-force action with
zero density and displacement variations. These zero vectors are allocated and
initialized once with the cross operator. For a fixed prepared state,
\[
D F_{\mathrm{gravity\ force}}[0,0,\delta q]
= (D_q F_{\mathrm{gravity\ force}})\delta q.
\]
The existing domain-deformation adjoint then maps this mechanical action into the
shape row. Gravity-potential coupling into hydrostatic equilibrium and pressure
boundary-row replacement remain unchanged. Pressure and rotational complete
actions are not invoked: they have no gravity-gradient column.
This is a preconditioner execution-path change. It changes no equation,
quadrature selection, cross-block approximation, factorization policy, default,
or full Newton-Jacobian implementation. Prepared and legacy force evaluations
may differ by floating-point evaluation order; numerical equality is tested with
the existing `2e-12` scaled norm criterion.
The material-to-gravity cross action already uses the same prepared gravity
Jacobian as the full stellar operator and is unchanged. This patch does not
optimize other material/surface policies or standalone partial-column APIs.
The existing stellar-structure FEM integration test now compares three dense
gravity directions against both the legacy partial-column calculation and the
restricted full Jacobian, checking surface and enthalpy rows separately. It
repeats after density and shape changes, checks repeated application after full-J
scratch use, and verifies that these applications do not reprepare the force.
The tests are supplied for the coordinated build; no timing improvement is
claimed before measurement on the frozen benchmark state.

View File

@@ -413,6 +413,8 @@ export namespace mean_field::preconditioning {
m_gravityDirection(operation.GetGravityJacobianOperator().Width()),
m_volumeDisplacement(operation.GetDomainDeformation().volumeDisplacementSize()),
m_mechanicalAction(operation.GetDomainDeformation().volumeDisplacementSize()),
m_zeroDensity(operation.GetGravityContext().GetDensityMap().reduced_size()),
m_zeroDisplacement(operation.GetGravityContext().GetDisplacementMap().reduced_size()),
m_zeroEnthalpy(operation.GetBarotropicClosureOperator().GetEnthalpySize()) {
const auto &context = operation.GetGravityContext();
m_materialOffsets[0] = 0;
@@ -425,6 +427,8 @@ export namespace mean_field::preconditioning {
m_combinedOffsets[0] = 0;
m_combinedOffsets[1] = MaterialSize();
m_combinedOffsets[2] = Height();
m_zeroDensity = 0.0;
m_zeroDisplacement = 0.0;
m_zeroEnthalpy = 0.0;
}
@@ -488,8 +492,12 @@ export namespace mean_field::preconditioning {
auto enthalpyAction = MaterialBlock(materialAction, 2);
densityAction = 0.0;
m_operation->GetDisplacementOperator().ApplyGravityGradientJacobianAction(
gravityGradientDirection, m_mechanicalAction
// Restrict the same prepared tensor action used by the full Jacobian.
// The standalone gravity-gradient column still uses a legacy kernel
// that rebuilds geometry during every application. Zero variations
// select this column without changing its quadrature or derivative.
m_operation->GetDisplacementOperator().GetGravityOperator().ApplyCompleteJacobianAction(
m_zeroDensity, m_zeroDisplacement, gravityGradientDirection, m_mechanicalAction
);
m_operation->GetDomainDeformation().applyJacobianTranspose(
m_operation->GetSurfaceDeformationParameters(), m_mechanicalAction, surfaceAction
@@ -615,6 +623,8 @@ export namespace mean_field::preconditioning {
mutable mfem::Vector m_gravityDirection;
mutable mfem::Vector m_volumeDisplacement;
mutable mfem::Vector m_mechanicalAction;
mfem::Vector m_zeroDensity;
mfem::Vector m_zeroDisplacement;
mfem::Vector m_zeroEnthalpy;
};

View File

@@ -413,6 +413,70 @@ TEST_CASE(
CHECK(relativeError(crossGravityPotential, expectedGravityPotential) <= 2.0e-12);
CHECK(relativeError(crossAction, expected) <= 2.0e-12);
const auto checkPreparedGravityColumn = [&](const int revision) {
CAPTURE(revision);
const auto &mechanics = physical.GetDisplacementOperator();
const auto preparationCount = mechanics.GetGravityOperator().GetResidualPreparationCount();
mfem::Vector gravityInput(cross.GravitySize()), actual(cross.MaterialSize()), legacy(cross.MaterialSize());
mfem::Vector repeated(cross.MaterialSize()), rootInput(physical.Width()), rootAction(physical.Height());
mfem::Vector mechanicalReference(physical.GetDomainDeformation().volumeDisplacementSize());
mfem::Vector zeroEnthalpy(physical.GetBarotropicClosureOperator().GetEnthalpySize());
zeroEnthalpy = 0.0;
for (const double phase : {0.19, 0.53, 0.87}) {
CAPTURE(phase);
for (int index = 0; index < gravityInput.Size(); ++index) {
gravityInput(index) = 0.01 * std::sin(phase * (index + 1));
}
const mfem::Vector gradientInput(gravityInput.GetData(), gravityOffsets[1]);
const mfem::Vector potentialInput(
gravityInput.GetData() + gravityOffsets[1], gravityOffsets[2] - gravityOffsets[1]
);
legacy = 0.0;
mfem::Vector legacySurface(legacy, materialOffsets[1], materialOffsets[2] - materialOffsets[1]);
mfem::Vector legacyEnthalpy(legacy, materialOffsets[2], materialOffsets[3] - materialOffsets[2]);
// Keep the independent legacy column as the numerical reference;
// the production cross action now uses the cached complete kernel.
mechanics.ApplyGravityGradientJacobianAction(gradientInput, mechanicalReference);
physical.GetDomainDeformation().applyJacobianTranspose(
physical.GetSurfaceDeformationParameters(), mechanicalReference, legacySurface
);
physical.GetHydrostaticOperator().ApplyGravityPotentialJacobianAction(potentialInput, legacyEnthalpy);
physical.GetSurfaceConstraintOperator().ApplyJacobianRows(zeroEnthalpy, legacyEnthalpy);
legacySurface.SyncAliasMemory(legacy);
legacyEnthalpy.SyncAliasMemory(legacy);
cross.ApplyGravityToMaterial(gravityInput, actual);
const mfem::Vector actualSurface(
actual.GetData() + materialOffsets[1], materialOffsets[2] - materialOffsets[1]
);
const mfem::Vector actualEnthalpy(
actual.GetData() + materialOffsets[2], materialOffsets[3] - materialOffsets[2]
);
INFO("legacy/cached surface error = " << relativeError(actualSurface, legacySurface)
<< ", cached norm = " << actualSurface.Norml2() << ", legacy norm = " << legacySurface.Norml2());
CHECK(relativeError(actualSurface, legacySurface) <= 2.0e-12);
CHECK(relativeError(actualEnthalpy, legacyEnthalpy) <= 2.0e-12);
CHECK(relativeError(actual, legacy) <= 2.0e-12);
rootInput = 0.0;
const auto inputView = physical.GetRootManifest().stateView(rootInput);
mfem::Vector rootGradient = inputView.block(blocks::gravity_field.gradient_term);
mfem::Vector rootPotential = inputView.block(blocks::gravity_field.poisson_term);
rootGradient = gradientInput;
rootPotential = potentialInput;
rootGradient.SyncAliasMemory(rootInput);
rootPotential.SyncAliasMemory(rootInput);
physical.Mult(rootInput, rootAction);
const auto actionView = physical.GetRootManifest().residualView(rootAction);
CHECK(relativeError(actualSurface,
actionView.block(blocks::surface_deformation_field.shape_equilibrium_term)) <= 2.0e-12);
CHECK(relativeError(actualEnthalpy, actionView.block(blocks::enthalpy_field.specific_term)) <= 2.0e-12);
cross.ApplyGravityToMaterial(gravityInput, repeated);
CHECK(relativeError(actual, repeated) <= 2.0e-14);
}
CHECK(mechanics.GetGravityOperator().GetResidualPreparationCount() == preparationCount);
};
checkPreparedGravityColumn(1);
auto materialBlock = preconditioning::materialSurfaceBlock(problem);
auto gravityBlock = preconditioning::GravityFieldBlock(
backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}}, preconditioning::GravityApproximateLDU{}
@@ -440,4 +504,19 @@ TEST_CASE(
const auto unchanged = prepared.Refresh();
CHECK_FALSE(unchanged.DidAnyWork());
CHECK(prepared.IsCurrent());
mfem::Vector changedState(projected.values);
const auto changedView = problem.GetManifest().stateView(changedState);
mfem::Vector changedDensity = changedView.block(blocks::density_field.mass_term);
mfem::Vector changedSurface = changedView.block(blocks::surface_deformation_field.parameters_term);
changedDensity *= 1.001;
for (int index = 0; index < changedSurface.Size(); ++index) {
changedSurface(index) += 1.0e-7 * std::sin(0.31 * (index + 1));
}
changedDensity.SyncAliasMemory(changedState);
changedSurface.SyncAliasMemory(changedState);
problem.Prepare(changedState, makeDependencies(2), zeroRotation());
// The cross operator borrows the owning physical operator, whose prepared
// geometry and density are replaced in place. It must not retain old data.
checkPreparedGravityColumn(2);
}