feat(newton): first newton solver implementation

This commit is contained in:
2026-09-08 06:36:39 -04:00
parent 76818f2f82
commit b3c04d507a
98 changed files with 20397 additions and 11040 deletions

View File

@@ -3,10 +3,14 @@ module;
#include <array>
#include <cmath>
#include <cstdint>
#include <expected>
#include <limits>
#include <optional>
#include <stdexcept>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
module mean_field;
@@ -20,6 +24,99 @@ namespace {
using PressureDomain = mean_field::field::FieldDomainT<mean_field::field::Enthalpy>;
using Rejection = mean_field::operators::PressureForcePreparationRejection;
using Reason = mean_field::operators::PressureForcePreparationRejectionReason;
[[nodiscard]] Rejection equation_of_state_rejection(const mean_field::eos::EvaluationErrorCode code) noexcept {
return {.reason = Reason::equation_of_state, .equationOfStateCode = code};
}
[[nodiscard]] Rejection mapping_rejection(const mean_field::mapping::MappingStatus status) {
MFEM_VERIFY(
status != mean_field::mapping::MappingStatus::invalid_dimension,
"Prepared pressure-force mapping reported an invariant dimension mismatch."
);
return {.reason = Reason::invalid_mapping, .mappingStatus = status};
}
[[nodiscard]] Rejection non_finite_rejection() noexcept {
return {.reason = Reason::non_finite_arithmetic};
}
[[nodiscard]] int encode_rejection(const std::optional<Rejection> &rejection) noexcept {
if (!rejection.has_value()) {
return 0;
}
switch (rejection->reason) {
case Reason::equation_of_state:
return static_cast<int>(rejection->equationOfStateCode) + 1;
case Reason::invalid_mapping:
return 128 + static_cast<int>(rejection->mappingStatus);
case Reason::non_finite_arithmetic:
default:
return 256;
}
}
[[nodiscard]] Rejection decode_rejection(const int encoded) {
if (encoded >= 256) {
return non_finite_rejection();
}
if (encoded >= 128) {
return mapping_rejection(static_cast<mean_field::mapping::MappingStatus>(encoded - 128));
}
return equation_of_state_rejection(static_cast<mean_field::eos::EvaluationErrorCode>(encoded - 1));
}
[[nodiscard]] std::optional<Rejection> synchronize_rejection(
const std::optional<Rejection> &localRejection,
const MPI_Comm communicator
) {
const int localEncoded = encode_rejection(localRejection);
int globalEncoded = 0;
if (MPI_Allreduce(&localEncoded, &globalEncoded, 1, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("PreparedPressureForceOperator could not synchronize candidate validity.");
}
if (globalEncoded == 0) {
return std::nullopt;
}
return decode_rejection(globalEncoded);
}
[[nodiscard]] bool vector_is_finite(const mfem::Vector &vector) noexcept {
for (int index = 0; index < vector.Size(); ++index) {
if (!std::isfinite(vector(index))) {
return false;
}
}
return true;
}
[[nodiscard]] bool matrix_is_finite(const mfem::DenseMatrix &matrix) noexcept {
for (int row = 0; row < matrix.Height(); ++row) {
for (int column = 0; column < matrix.Width(); ++column) {
if (!std::isfinite(matrix(row, column))) {
return false;
}
}
}
return true;
}
[[noreturn]] void throw_rejection(const Rejection &rejection) {
switch (rejection.reason) {
case Reason::equation_of_state:
throw mean_field::eos::EvaluationError(
rejection.equationOfStateCode, "PreparedPressureForceOperator encountered invalid thermodynamic data."
);
case Reason::invalid_mapping:
throw std::domain_error("PreparedPressureForceOperator encountered an invalid mapped domain.");
case Reason::non_finite_arithmetic:
default:
throw std::domain_error("PreparedPressureForceOperator produced non-finite arithmetic.");
}
}
void verify_required_spaces(const mean_field::fem::FEM &f) {
MFEM_VERIFY(f.mesh != nullptr, "PreparedPressureForceOperator requires a mesh.");
@@ -296,11 +393,26 @@ namespace mean_field::operators {
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
) {
auto result = TryPrepare(state, dependencies);
if (!result.has_value()) {
throw_rejection(result.error());
}
return std::move(result).value();
}
std::expected<
PreparedPressureForceReport,
PressureForcePreparationRejection>
PreparedPressureForceOperator::TryPrepare(
const context::pressure_force::PressureForceStateView &state,
const context::pressure_force::PressureForceDependencies &dependencies
) {
const bool wasPrepared = m_isPrepared;
PreparedPressureForceReport report;
report.contextReport = m_context.Prepare(state, dependencies);
if (!report.contextReport.DidAnyWork() && m_isPrepared) {
if (!report.contextReport.DidAnyWork() && wasPrepared) {
return report;
}
@@ -317,20 +429,38 @@ namespace mean_field::operators {
m_isPrepared = false;
if (report.contextReport.preparedStaticDependencies) {
if (report.contextReport.preparedStaticDependencies || !wasPrepared) {
PrepareStaticPlan();
}
if (report.contextReport.preparedGeometryState) {
PrepareGeometry();
if (report.contextReport.preparedGeometryState || !wasPrepared) {
const auto globalGeometryFailure = synchronize_rejection(PrepareGeometry(), m_fem.enthalpyFes->GetComm());
if (globalGeometryFailure.has_value()) {
return std::unexpected(*globalGeometryFailure);
}
}
if (report.contextReport.preparedMaterialState) {
PrepareMaterialState();
std::optional<PressureForcePreparationRejection> localMaterialFailure;
if (report.contextReport.preparedMaterialState || !wasPrepared) {
localMaterialFailure = PrepareMaterialState();
}
const auto globalMaterialFailure = synchronize_rejection(localMaterialFailure, m_fem.enthalpyFes->GetComm());
if (globalMaterialFailure.has_value()) {
return std::unexpected(*globalMaterialFailure);
}
if (report.contextReport.preparedMaterialState || !wasPrepared) {
FinalizeDisplacementJacobianPreparation();
AssembleCachedResidual();
const auto globalAssemblyFailure =
synchronize_rejection(AssembleCachedResidual(), m_fem.enthalpyFes->GetComm());
if (globalAssemblyFailure.has_value()) {
return std::unexpected(*globalAssemblyFailure);
}
++m_enthalpyJacobianStatistics.preparations;
++m_displacementJacobianStatistics.preparations;
++m_residualPreparationCount;
@@ -455,7 +585,7 @@ namespace mean_field::operators {
}
}
void PreparedPressureForceOperator::PrepareGeometry() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareGeometry() {
mfem::Vector displacementLocal;
true_to_local(*m_fem.displacementFes, m_baseDisplacementTrue, displacementLocal);
@@ -526,21 +656,18 @@ namespace mean_field::operators {
mappingData, *transformation, integrationPoint, workspace, mappingContext
);
MFEM_VERIFY(
mappingStatus == mapping::MappingStatus::valid,
"Stateless mapping failed while preparing "
"pressure-force geometry. Element: "
<< data.elementId << ", attribute: " << transformation->Attribute
<< ", quadrature point: " << quadraturePoint << ", status: " << static_cast<int>(mappingStatus)
);
if (mappingStatus != mapping::MappingStatus::valid) {
return mapping_rejection(mappingStatus);
}
const double quadratureWeight = mappingContext.quadrature.weight;
MFEM_VERIFY(
std::isfinite(quadratureWeight) && quadratureWeight > 0.0,
"Prepared pressure-force geometry encountered an "
"invalid quadrature weight."
);
if (!std::isfinite(quadratureWeight)) {
return mapping_rejection(mapping::MappingStatus::non_finite_result);
}
if (quadratureWeight <= 0.0) {
return mapping_rejection(mapping::MappingStatus::non_positive_determinant);
}
data.quadratureWeights(quadraturePoint) = quadratureWeight;
@@ -559,11 +686,15 @@ namespace mean_field::operators {
physicalTestGradient.SetSize(referenceTestGradient.Height(), mappingContext.quadrature.J_inv.Width());
mfem::Mult(referenceTestGradient, mappingContext.quadrature.J_inv, physicalTestGradient);
if (!matrix_is_finite(physicalTestGradient)) {
return non_finite_rejection();
}
}
}
return std::nullopt;
}
void PreparedPressureForceOperator::PrepareMaterialState() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::PrepareMaterialState() {
mfem::Vector enthalpyLocal;
true_to_local(*m_fem.enthalpyFes, m_baseEnthalpyTrue, enthalpyLocal);
@@ -574,6 +705,7 @@ namespace mean_field::operators {
const int dimension = m_fem.mesh->Dimension();
const mfem::Ordering::Type displacementOrdering = m_fem.displacementFes->GetOrdering();
std::optional<PressureForcePreparationRejection> materialFailure;
for (ElementPAData &data : m_elements) {
enthalpyLocal.GetSubVector(data.enthalpyDofs, elementEnthalpy);
@@ -619,6 +751,19 @@ namespace mean_field::operators {
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const double enthalpy = quadratureEnthalpy(quadraturePoint);
if (!std::isfinite(enthalpy)) {
materialFailure = equation_of_state_rejection(eos::EvaluationErrorCode::nonfinite_input);
data.pressure(quadraturePoint) = 0.0;
data.pressureDerivative(quadraturePoint) = 0.0;
continue;
}
if (enthalpy < 0.0) {
materialFailure = equation_of_state_rejection(eos::EvaluationErrorCode::outside_domain);
data.pressure(quadraturePoint) = 0.0;
data.pressureDerivative(quadraturePoint) = 0.0;
continue;
}
const dimensions::SpecificEnthalpyValue specificEnthalpy{enthalpy};
const double pressure =
eos::evaluate<eos::quantity::Pressure>(m_equationOfState, specificEnthalpy).value();
@@ -631,11 +776,12 @@ namespace mean_field::operators {
const double quadratureWeight = data.quadratureWeights(quadraturePoint);
MFEM_VERIFY(
std::isfinite(pressure) && std::isfinite(pressureDerivative),
"Prepared pressure-force material state encountered "
"a non-finite EOS value."
);
if (!std::isfinite(pressure) || !std::isfinite(pressureDerivative)) {
materialFailure = equation_of_state_rejection(eos::EvaluationErrorCode::nonfinite_result);
data.pressure(quadraturePoint) = 0.0;
data.pressureDerivative(quadraturePoint) = 0.0;
continue;
}
data.pressure(quadraturePoint) = pressure;
@@ -659,19 +805,32 @@ namespace mean_field::operators {
const double weightedTestGradient =
quadratureWeight * physicalTestGradient(scalarDof, component);
data.elementResidual(vectorDof) -= pressure * weightedTestGradient;
const double residualContribution = pressure * weightedTestGradient;
if (!std::isfinite(weightedTestGradient) || !std::isfinite(residualContribution)) {
materialFailure = non_finite_rejection();
continue;
}
data.elementResidual(vectorDof) -= residualContribution;
for (int enthalpyDof = 0; enthalpyDof < enthalpyDofCount; ++enthalpyDof) {
data.enthalpyJacobian(vectorDof, enthalpyDof) -=
pressureDerivative * weightedTestGradient *
data.enthalpyBasis(quadraturePoint, enthalpyDof);
const double jacobianContribution = pressureDerivative * weightedTestGradient *
data.enthalpyBasis(quadraturePoint, enthalpyDof);
if (!std::isfinite(jacobianContribution)) {
materialFailure = non_finite_rejection();
continue;
}
data.enthalpyJacobian(vectorDof, enthalpyDof) -= jacobianContribution;
}
}
}
}
}
++m_enthalpyJacobianStatistics.preparations;
if (!vector_is_finite(data.elementResidual) || !matrix_is_finite(data.enthalpyJacobian)) {
materialFailure = non_finite_rejection();
}
}
return materialFailure;
}
void PreparedPressureForceOperator::FinalizeDisplacementJacobianPreparation() {
@@ -700,11 +859,9 @@ namespace mean_field::operators {
);
}
}
++m_displacementJacobianStatistics.preparations;
}
void PreparedPressureForceOperator::AssembleCachedResidual() {
std::optional<PressureForcePreparationRejection> PreparedPressureForceOperator::AssembleCachedResidual() {
mfem::Vector localResidual(m_fem.displacementFes->GetVSize());
localResidual = 0.0;
@@ -729,6 +886,10 @@ namespace mean_field::operators {
* FieldDofMap::gather does not resize its destination.
*/
m_displacementMap.gather(m_fullDisplacementAction, m_cachedResidual);
if (!vector_is_finite(m_fullDisplacementAction) || !vector_is_finite(m_cachedResidual)) {
return non_finite_rejection();
}
return std::nullopt;
}
void PreparedPressureForceOperator::BuildResidual(mfem::Vector &residual) const {
@@ -816,15 +977,12 @@ namespace mean_field::operators {
mfem::Vector elementDisplacementVariation;
mfem::Vector elementAction;
mfem::DenseMatrix referenceDisplacementDShape;
mfem::DenseMatrix referenceDisplacementJacobian;
mfem::DenseMatrix inverseElementJacobianVariation;
mfem::DenseMatrix matrixTemporary;
mfem::DenseMatrix physicalTestGradientVariation;
const int dimension = m_fem.mesh->Dimension();
const mfem::Ordering::Type displacementOrdering = m_fem.displacementFes->GetOrdering();
const int dimension = m_fem.mesh->Dimension();
for (const ElementPAData &data : m_elements) {
MFEM_VERIFY(
@@ -849,14 +1007,13 @@ namespace mean_field::operators {
const mfem::FiniteElement &displacementElement = *m_fem.displacementFes->GetFE(data.elementId);
const mapping::ElementDisplacementData directionData =
mapping::ElementDisplacementDataFromElementVDofs(displacementElement, elementDisplacementVariation);
const int quadraturePointCount = data.integrationRule->GetNPoints();
const int quadraturePointCount = data.integrationRule->GetNPoints();
const int scalarDisplacementDofCount = displacementElement.GetDof();
const int scalarDisplacementDofCount = displacementElement.GetDof();
const mfem::DenseMatrix &directionDofs = directionData.GetDofMatrix();
const mfem::DenseMatrix directionDofs(
elementDisplacementVariation.GetData(), scalarDisplacementDofCount, dimension
);
MFEM_VERIFY(
static_cast<int>(data.baseMappingContexts.size()) == quadraturePointCount &&
@@ -869,17 +1026,15 @@ namespace mean_field::operators {
elementAction = 0.0;
referenceDisplacementDShape.SetSize(scalarDisplacementDofCount, dimension);
referenceDisplacementJacobian.SetSize(dimension, dimension);
inverseElementJacobianVariation.SetSize(dimension, dimension);
matrixTemporary.SetSize(dimension, dimension);
physicalTestGradientVariation.SetSize(scalarDisplacementDofCount, dimension);
for (int quadraturePoint = 0; quadraturePoint < quadraturePointCount; ++quadraturePoint) {
const mfem::IntegrationPoint &integrationPoint = data.integrationRule->IntPoint(quadraturePoint);
const mfem::DenseMatrix &referenceTestGradient = data.referenceTestGradients[quadraturePoint];
displacementElement.CalcDShape(integrationPoint, referenceDisplacementDShape);
mfem::MultAtB(directionDofs, referenceDisplacementDShape, referenceDisplacementJacobian);
mfem::MultAtB(directionDofs, referenceTestGradient, referenceDisplacementJacobian);
const mfem::DenseMatrix &inverseElementJacobian =
data.baseMappingContexts[quadraturePoint].quadrature.J_inv;
@@ -893,39 +1048,35 @@ namespace mean_field::operators {
mfem::Mult(matrixTemporary, inverseElementJacobian, inverseElementJacobianVariation);
inverseElementJacobianVariation *= -1.0;
mfem::Mult(
data.referenceTestGradients[quadraturePoint], inverseElementJacobianVariation,
physicalTestGradientVariation
);
mfem::Mult(referenceTestGradient, inverseElementJacobianVariation, physicalTestGradientVariation);
const mfem::DenseMatrix &physicalTestGradient = data.physicalTestGradients[quadraturePoint];
const double quadratureWeight = data.quadratureWeights(quadraturePoint);
const double pressure = data.pressure(quadraturePoint);
for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) {
for (int component = 0; component < dimension; ++component) {
const int vectorDof = vector_dof_index(
displacementOrdering, scalarDof, component, scalarDisplacementDofCount, dimension
);
for (int component = 0; component < dimension; ++component) {
const double *variationColumn =
physicalTestGradientVariation.GetData() + component * scalarDisplacementDofCount;
const double *physicalColumn =
physicalTestGradient.GetData() + component * scalarDisplacementDofCount;
double *actionColumn = elementAction.GetData() + component * scalarDisplacementDofCount;
const double gradientWeightVariation = data.quadratureWeights(quadraturePoint) *
physicalTestGradientVariation(scalarDof, component) +
data.quadratureWeights(quadraturePoint) *
logarithmicJacobianVariation *
physicalTestGradient(scalarDof, component);
for (int scalarDof = 0; scalarDof < scalarDisplacementDofCount; ++scalarDof) {
const double gradientWeightVariation =
quadratureWeight * variationColumn[scalarDof] +
quadratureWeight * logarithmicJacobianVariation * physicalColumn[scalarDof];
const double contribution = pressure * gradientWeightVariation;
const double contribution = data.pressure(quadraturePoint) * gradientWeightVariation;
MFEM_VERIFY(
std::isfinite(gradientWeightVariation) && std::isfinite(contribution),
"Prepared pressure-force displacement "
"Jacobian encountered a non-finite "
"contribution."
);
elementAction(vectorDof) -= contribution;
actionColumn[scalarDof] -= contribution;
}
}
}
MFEM_VERIFY(
vector_is_finite(elementAction),
"Prepared pressure-force displacement Jacobian encountered a non-finite element action."
);
if (data.displacementDofTransformation != nullptr) {
data.displacementDofTransformation->TransformDual(elementAction);
}