feat: adopt lumped mass and reduced-coordinate shell HHT

Integrate Hex8 mass verification and fixed-null-basis shell dynamics with numerical and physics handoff evidence. Preserve interrupted reference and full-validation status; full integration validation is deferred until after performance optimization.
This commit is contained in:
KOKO\Mimi
2026-10-13 16:57:19 +09:00
parent ede137c6b4
commit eaa5cbeb21
110 changed files with 15274 additions and 971 deletions
+142 -11
View File
@@ -225,6 +225,92 @@ Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs,
return Status::Ok();
}
Result<DynamicAssembly> EvaluateNewtonStart(
const Domain& domain, const DofManager& dofs,
const std::vector<RuntimeShell>& shells, DynamicAcceptedFrameState frame,
const HhtPredictor& predictor, const Vector& old_acceleration,
const Vector& load_scale, const double time_increment,
const HhtParameters& parameters, const ParallelFor& parallel_for) {
const Vector accepted_coordinates = frame.generalized_coordinates;
auto evaluate = [&](const Vector& coordinates) -> Result<DynamicAssembly> {
frame.generalized_coordinates = coordinates;
auto rates = EvaluateHhtRates(coordinates, predictor, old_acceleration,
time_increment, parameters);
if (!rates.HasValue())
return Result<DynamicAssembly>::Failure(rates.GetStatus());
frame.velocity = std::move(rates.Value().velocity);
frame.acceleration = std::move(rates.Value().acceleration);
frame.algorithmic_acceleration =
std::move(rates.Value().algorithmic_acceleration);
frame.iteration_count = 0U;
return EvaluateAssembly(domain, dofs, shells, frame, load_scale,
time_increment, parameters, parallel_for, false);
};
auto start = evaluate(predictor.coordinates);
if (start.HasValue()) {
bool same_coordinates =
accepted_coordinates.Size() == predictor.coordinates.Size();
for (std::size_t i = 0U; same_coordinates && i < accepted_coordinates.Size();
++i)
same_coordinates = accepted_coordinates[i] == predictor.coordinates[i];
if (same_coordinates) return start;
auto accepted_start = evaluate(accepted_coordinates);
if (!accepted_start.HasValue()) return accepted_start;
const auto length = ReferenceLength(domain);
if (!length.HasValue())
return Result<DynamicAssembly>::Failure(length.GetStatus());
double predictor_residual = 0.0;
double accepted_residual = 0.0;
for (const auto full : dofs.FreeDofs()) {
const double scale = full % 6U < 3U ? length.Value() : 1.0;
predictor_residual =
std::max(predictor_residual,
scale * std::abs(start.Value().frame.residual[full]));
accepted_residual =
std::max(accepted_residual,
scale * std::abs(accepted_start.Value().frame.residual[full]));
}
if (!std::isfinite(predictor_residual) || !std::isfinite(accepted_residual))
return Result<DynamicAssembly>::Failure(
Failure("The scaled initial Newton residual must remain finite."));
// A geometrically valid large DIRECT predictor may still be far from the
// endpoint. Compare only these two initial trials, in common energy units.
return predictor_residual <= accepted_residual ? std::move(start)
: std::move(accepted_start);
}
if (start.GetStatus().Diagnostics().empty()) return start;
const auto& code = start.GetStatus().Diagnostics().front().code;
bool invalid_geometry = code == "invalid-nonlinear-shell-geometry" ||
code == "invalid-nonlinear-shell-director" ||
code == "invalid-nonlinear-inertia-geometry" ||
code == "invalid-nonlinear-inertia-director";
if (code == "invalid-dynamic-rotation") {
// This diagnostic also covers rate overflow. Only a finite coordinate
// outside the existing principal margin permits another initial geometry.
const double pi = std::acos(-1.0);
const double margin = std::sqrt(std::numeric_limits<double>::epsilon());
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const std::size_t offset = 6U * node + 3U;
const auto phi = Triple(predictor.coordinates, offset);
const auto rotation = EvaluateDynamicRotation(
phi, Vector3(domain.ShellNodeInitialFrames()[node].director),
Triple(frame.velocity, offset), Triple(frame.acceleration, offset));
if (rotation.HasValue()) continue;
// Match assembly's first failing node; an unrelated later chart error
// must not turn an earlier valid-chart rate failure into a fallback.
const double angle = phi.Norm();
invalid_geometry =
phi.IsFinite() &&
(!std::isfinite(angle) || !(pi - angle > margin));
break;
}
}
if (!invalid_geometry) return start;
// This is initial-iterate selection only: neither a Newton correction nor
// the frozen HHT predictor is changed, and accepted history is never mutated.
return evaluate(accepted_coordinates);
}
Result<DynamicAssembly> EvaluateAssembly(
const Domain& domain, const DofManager& dofs,
const std::vector<RuntimeShell>& shells, DynamicAcceptedFrameState frame,
@@ -317,9 +403,12 @@ Result<DynamicAssembly> EvaluateAssembly(
frame.angular_momentum_rate = {};
std::vector<std::map<std::pair<std::size_t, std::size_t>, double>>
constraint_hessians(domain.Nodes().size());
std::vector<std::map<std::size_t, double>>
constraint_gradients(domain.Nodes().size());
std::vector<bool> external_done(domain.Nodes().size(), false);
Vector third(count);
Vector third_scale(count);
std::array<std::vector<CooContribution>, 4> entries;
std::array<std::vector<CooContribution>, 6> entries;
for (auto& values : entries) values.reserve(shells.size() * 24U * 24U);
double momentum_scale = 0.0;
double angular_scale = 0.0;
@@ -335,12 +424,34 @@ Result<DynamicAssembly> EvaluateAssembly(
const auto& m = *inertia[e];
if (directional_third) {
for (std::size_t local = 0U; local < 4U; ++local) {
auto& hessian = constraint_hessians[shells[e].scatter[6U * local] / 6U];
const auto node = shells[e].scatter[6U * local] / 6U;
auto& gradient = constraint_gradients[node];
auto& hessian = constraint_hessians[node];
for (std::size_t i = 0U; i < 24U; ++i)
gradient[shells[e].scatter[i]] +=
f.drilling_constraint_gradient(local, i);
for (std::size_t i = 0U; i < 24U; ++i)
for (std::size_t j = 0U; j < 24U; ++j) {
hessian[{shells[e].scatter[i], shells[e].scatter[j]}] +=
f.drilling_constraint_hessian[local](i, j);
}
if (!external_done[node]) {
external_done[node] = true;
const auto first = 6U * node + 3U;
const auto external =
shells[e].internal.EvaluateExternalDrillingConstraint(
local, Triple(frame.generalized_coordinates, first),
Triple(frame.velocity, first),
Triple(frame.external_force, first), {}, {});
if (!external.HasValue())
return Result<DynamicAssembly>::Failure(external.GetStatus());
for (std::size_t i = 0U; i < 3U; ++i) {
gradient[first + i] += external.Value().gradient[i];
for (std::size_t j = 0U; j < 3U; ++j)
hessian[{first + i, first + j}] +=
external.Value().hessian(i, j);
}
}
}
}
const auto a = Gather(frame.acceleration, shells[e]);
@@ -356,13 +467,14 @@ Result<DynamicAssembly> EvaluateAssembly(
for (std::size_t column = 0U; column < 24U; ++column) {
frame.residual_scale[global] +=
std::abs(m.mass(row, column) * a[column]);
const std::array<double, 4> values{
m.mass(row, column), f.internal_tangent(row, column),
const std::array<double, 6> values{
m.mass(row, column), m.position_tangent(row, column),
m.velocity_tangent(row, column), f.internal_tangent(row, column),
f.internal_tangent(row, column) + m.position_tangent(row, column) +
v_coefficient * m.velocity_tangent(row, column) +
a_coefficient * m.mass(row, column),
f.drilling_metric(row, column)};
for (std::size_t k = 0U; k < 4U; ++k)
for (std::size_t k = 0U; k < 6U; ++k)
entries[k].push_back({global, shells[e].scatter[column], values[k], e,
row * 24U + column});
}
@@ -414,13 +526,18 @@ Result<DynamicAssembly> EvaluateAssembly(
frame.free_residual_metrics[c] = Ratio(residual_norm[c], scale_norm[c]);
auto mass = SparseMatrix::FromCoo(count, count, std::move(entries[0U]),
dofs.GetSparsePattern());
auto stiffness = SparseMatrix::FromCoo(count, count, std::move(entries[1U]),
auto position = SparseMatrix::FromCoo(count, count, std::move(entries[1U]),
dofs.GetSparsePattern());
auto velocity = SparseMatrix::FromCoo(count, count, std::move(entries[2U]),
dofs.GetSparsePattern());
auto stiffness = SparseMatrix::FromCoo(count, count, std::move(entries[3U]),
dofs.GetSparsePattern());
auto effective = SparseMatrix::FromCoo(count, count, std::move(entries[2U]),
auto effective = SparseMatrix::FromCoo(count, count, std::move(entries[4U]),
dofs.GetSparsePattern());
auto metric = SparseMatrix::FromCoo(count, count, std::move(entries[3U]),
auto metric = SparseMatrix::FromCoo(count, count, std::move(entries[5U]),
dofs.GetSparsePattern());
for (const auto* result : {&mass, &stiffness, &effective, &metric})
for (const auto* result : {&mass, &position, &velocity, &stiffness,
&effective, &metric})
if (!result->HasValue())
return Result<DynamicAssembly>::Failure(result->GetStatus());
Vector hidden_second(domain.Nodes().size());
@@ -433,12 +550,26 @@ Result<DynamicAssembly> EvaluateAssembly(
hidden_second_scale[node] += std::abs(term);
}
}
std::optional<SparseMatrix> hidden_gradient;
if (directional_third) {
std::vector<CooContribution> gradient_entries;
for (std::size_t node = 0U; node < constraint_gradients.size(); ++node)
for (const auto& entry : constraint_gradients[node])
gradient_entries.push_back(
{node, entry.first, entry.second, node, gradient_entries.size()});
auto gradient = MakeSparse(domain.Nodes().size(), count,
std::move(gradient_entries));
if (!gradient.HasValue())
return Result<DynamicAssembly>::Failure(gradient.GetStatus());
hidden_gradient = std::move(gradient.Value());
}
return Result<DynamicAssembly>::Success(
{std::move(frame), std::move(mass.Value()), std::move(stiffness.Value()),
{std::move(frame), std::move(mass.Value()), std::move(position.Value()),
std::move(velocity.Value()), std::move(stiffness.Value()),
std::move(effective.Value()), std::move(metric.Value()),
std::move(third), std::move(third_scale), std::move(rotations),
momentum_scale, angular_scale, std::move(hidden_second),
std::move(hidden_second_scale)});
std::move(hidden_second_scale), std::move(hidden_gradient)});
}
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,