Connect MITC4 drilling evidence across analysis procedures
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
@@ -72,6 +73,8 @@ class AnalysisState {
|
||||
double PhysicalStrainEnergy() const noexcept;
|
||||
/// @brief Returns global force and moment equilibrium components.
|
||||
const std::array<double, 6>& Equilibrium() const noexcept;
|
||||
/// @brief Returns actual MITC4 mode and artificial spatial moment evidence.
|
||||
const std::optional<Mitc4DrillingEvidence>& Mitc4Drilling() const noexcept;
|
||||
/// @brief Returns normalized shell verification metrics.
|
||||
const std::array<double, 3>& VerificationMetrics() const noexcept;
|
||||
|
||||
@@ -96,6 +99,7 @@ class AnalysisState {
|
||||
double physical_strain_energy_{0.0};
|
||||
std::array<double, 6> equilibrium_{};
|
||||
std::array<double, 3> verification_metrics_{};
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling_;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
@@ -54,6 +55,7 @@ struct DynamicAcceptedFrameState {
|
||||
double external_work = 0.0;
|
||||
double energy_defect = 0.0;
|
||||
std::array<double, 6> global_equilibrium{};
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
std::array<double, 3> linear_momentum{};
|
||||
std::array<double, 3> angular_momentum{};
|
||||
std::array<double, 3> linear_momentum_rate{};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/math/matrix.h"
|
||||
#include "fesa/elements/mitc4_drilling_mode.h"
|
||||
#include "fesa/solvers/eigen/eigen_solver.h"
|
||||
|
||||
namespace fesa {
|
||||
@@ -53,6 +54,7 @@ struct EigenAnalysisState {
|
||||
explicit EigenAnalysisState(FeastEigenResult backend_result)
|
||||
: backend{std::move(backend_result)} {}
|
||||
|
||||
std::optional<Mitc4DrillingMode> drilling_mode;
|
||||
std::size_t full_dof_count{0U};
|
||||
std::size_t free_dof_count{0U};
|
||||
std::size_t mass_null_dimension{0U};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
@@ -46,6 +47,7 @@ struct NonlinearTrialState {
|
||||
std::vector<ShellResultRow> shell_results;
|
||||
double physical_strain_energy;
|
||||
std::array<double, 6> global_equilibrium;
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
};
|
||||
|
||||
/// @brief Stores one authoritative nonlinear increment frame.
|
||||
@@ -69,6 +71,7 @@ struct AcceptedFrameState {
|
||||
std::vector<ShellResultRow> shell_results;
|
||||
double physical_strain_energy;
|
||||
std::array<double, 6> global_equilibrium;
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
};
|
||||
|
||||
/// @brief Owns committed nonlinear state and validated accepted frames.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#ifndef FESA_ELEMENTS_ELEMENT_H_
|
||||
#define FESA_ELEMENTS_ELEMENT_H_
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "fesa/elements/element_dof_layout.h"
|
||||
#include "fesa/elements/mitc4_drilling_mode.h"
|
||||
#include "fesa/math/matrix.h"
|
||||
#include "fesa/math/vector.h"
|
||||
#include "fesa/results/result_records.h"
|
||||
@@ -30,6 +33,8 @@ struct BeamElementResultRows {
|
||||
struct ShellElementResultRows {
|
||||
std::vector<ShellResultRow> rows;
|
||||
double physical_strain_energy{0.0};
|
||||
std::optional<Mitc4DrillingMode> drilling_mode;
|
||||
std::array<double, 3> drilling_moment{};
|
||||
};
|
||||
|
||||
/// @brief Selects the physical recovery shape of one runtime element.
|
||||
|
||||
@@ -178,6 +178,8 @@ class ShellReferenceElement : public Element {
|
||||
virtual Result<Mitc4Stiffness> Stiffness() const = 0;
|
||||
/// @copydoc Mitc4Shell::Mass
|
||||
virtual Result<Mitc4Mass> Mass(double density) const = 0;
|
||||
/// @brief Returns the actual immutable reference-kernel drilling formulation.
|
||||
virtual Mitc4DrillingMode DrillingMode() const noexcept = 0;
|
||||
};
|
||||
|
||||
/// @brief Borrows the same linear MITC4 kernel for stiffness and recovery.
|
||||
@@ -190,6 +192,7 @@ class Mitc4ReferenceAdapter final : public ShellReferenceElement {
|
||||
Result<ElementResultBundle> Recover(const Vector& u) const override;
|
||||
Result<Mitc4Stiffness> Stiffness() const override;
|
||||
Result<Mitc4Mass> Mass(double density) const override;
|
||||
Mitc4DrillingMode DrillingMode() const noexcept override;
|
||||
|
||||
private:
|
||||
const Mitc4Shell& kernel_;
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
#include "fesa/elements/mitc4_drilling_mode.h"
|
||||
|
||||
namespace fesa {
|
||||
|
||||
@@ -70,6 +72,15 @@ struct ShellResultRow {
|
||||
std::array<ShellSectionStressRow, 3> stress;
|
||||
};
|
||||
|
||||
/// @brief Separates artificial internal spatial moment from raw equilibrium.
|
||||
/// @note The actual immutable kernel mode accompanies every production frame.
|
||||
struct Mitc4DrillingEvidence {
|
||||
Mitc4DrillingMode mode;
|
||||
std::array<double, 3> drilling_moment{};
|
||||
// Raw force balance and raw moment balance minus positive internal drilling.
|
||||
std::array<double, 6> equilibrium_with_drilling{};
|
||||
};
|
||||
|
||||
/// @brief Carries a complete shell result candidate for atomic validation.
|
||||
struct ShellStateCandidate {
|
||||
std::vector<ShellResultRow> rows;
|
||||
@@ -79,6 +90,7 @@ struct ShellStateCandidate {
|
||||
// [FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,
|
||||
// MOMENT_BALANCE_NORMALIZED].
|
||||
std::array<double, 3> verification_metrics{};
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "fesa/analysis/analysis_state.h"
|
||||
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -167,9 +169,18 @@ Status AnalysisState::CommitShellResults(
|
||||
"Shell energy, equilibrium, and normalized metrics must be finite.");
|
||||
}
|
||||
|
||||
if (!results_internal::ValidDrillingEvidence(candidate.mitc4_drilling,
|
||||
candidate.equilibrium) ||
|
||||
(mitc4_drilling_ && !results_internal::SameDrillingInventory(
|
||||
mitc4_drilling_, candidate.mitc4_drilling)))
|
||||
return ShellCandidateFailure("invalid-shell-drilling-evidence",
|
||||
identity_.step_name,
|
||||
"Drilling evidence must be finite, consistent, and retain its mode.");
|
||||
|
||||
shell_results_ = std::move(candidate.rows);
|
||||
physical_strain_energy_ = candidate.physical_strain_energy;
|
||||
equilibrium_ = candidate.equilibrium;
|
||||
mitc4_drilling_ = candidate.mitc4_drilling;
|
||||
verification_metrics_ = candidate.verification_metrics;
|
||||
return Status::Ok();
|
||||
}
|
||||
@@ -187,6 +198,11 @@ const std::array<double, 6>& AnalysisState::Equilibrium() const noexcept {
|
||||
return equilibrium_;
|
||||
}
|
||||
|
||||
const std::optional<Mitc4DrillingEvidence>& AnalysisState::Mitc4Drilling()
|
||||
const noexcept {
|
||||
return mitc4_drilling_;
|
||||
}
|
||||
|
||||
const std::array<double, 3>& AnalysisState::VerificationMetrics()
|
||||
const noexcept {
|
||||
return verification_metrics_;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "fesa/analysis/dynamic_analysis_state.h"
|
||||
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
@@ -273,6 +275,12 @@ Status DynamicAnalysisState::CommitAcceptedFrame(
|
||||
return Failure(FailureCategory::kSolver,
|
||||
"Frame physical evidence is nonfinite or has invalid size.");
|
||||
}
|
||||
if (!results_internal::ValidDrillingEvidence(frame.mitc4_drilling,
|
||||
frame.global_equilibrium) ||
|
||||
(!accepted_frames_.empty() && !results_internal::SameDrillingInventory(
|
||||
frame.mitc4_drilling, accepted_frames_.back().mitc4_drilling)))
|
||||
return Failure(FailureCategory::kSolver,
|
||||
"Accepted drilling evidence must be finite, consistent, and retain its mode.");
|
||||
for (const std::size_t dof : constrained_dofs_) {
|
||||
if (frame.generalized_coordinates[dof] != 0.0 ||
|
||||
frame.velocity[dof] != 0.0 || frame.acceleration[dof] != 0.0 ||
|
||||
|
||||
@@ -64,6 +64,16 @@ bool Within(const double value, const double scale, const double tolerance) {
|
||||
Status Validate(const Domain& domain, const DofManager& dofs,
|
||||
const DynamicAssembly& assembly, const double length) {
|
||||
const std::size_t count = dofs.FullDofCount();
|
||||
if (assembly.frame.mitc4_drilling &&
|
||||
assembly.frame.mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring) {
|
||||
if (assembly.hidden_constraint.Size() != assembly.rotations.size())
|
||||
return Failure(domain, "dynamic-invalid-algebra-state",
|
||||
"Canonical hidden values must cover every node.");
|
||||
for (std::size_t i = 0U; i < assembly.hidden_constraint.Size(); ++i)
|
||||
if (!std::isfinite(assembly.hidden_constraint[i]))
|
||||
return Failure(domain, "dynamic-invalid-algebra-state",
|
||||
"Canonical hidden values must remain finite.");
|
||||
}
|
||||
if (!std::isfinite(length) || !(length > 0.0) || count % 6U != 0U ||
|
||||
assembly.rotations.size() != count / 6U) {
|
||||
return Failure(domain, "dynamic-invalid-algebra-state",
|
||||
@@ -547,6 +557,8 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
|
||||
const SparseMatrix& gradient,
|
||||
DynamicAcceptedFrameState* frame,
|
||||
const bool require_acceleration) {
|
||||
const bool direct = frame->mitc4_drilling &&
|
||||
frame->mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring;
|
||||
Vector psi{nodes.size()};
|
||||
Vector psi_scale{nodes.size()};
|
||||
Vector velocity_defect{nodes.size()};
|
||||
@@ -575,9 +587,10 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
|
||||
const double g =
|
||||
frame->internal_force[full] - frame->external_force[full];
|
||||
const double n = rotation.drill_null[c];
|
||||
psi[row] += n * g;
|
||||
if (!direct) psi[row] += n * g;
|
||||
psi_scale[row] += std::abs(n) * frame->residual_scale[full];
|
||||
}
|
||||
if (direct) psi[row] = assembly.hidden_constraint[node];
|
||||
velocity_defect[row] += velocity_correction;
|
||||
AddCompensated(assembly.hidden_second[node], &acceleration_defect[row],
|
||||
&acceleration_correction);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "fesa/loads/load.h"
|
||||
#include "fesa/model/source_target_resolver.h"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa::hht_internal {
|
||||
|
||||
@@ -498,6 +499,10 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
internal[e] = std::move(f.Value());
|
||||
inertia[e] = std::move(m.Value());
|
||||
});
|
||||
if (shells.empty())
|
||||
return Result<DynamicAssembly>::Failure(Failure("HHT requires a shell kernel."));
|
||||
frame.mitc4_drilling =
|
||||
Mitc4DrillingEvidence{shells.front().internal_kernel->DrillingMode()};
|
||||
frame.inertia_force = Vector(count);
|
||||
frame.internal_force = Vector(count);
|
||||
frame.residual = Vector(count);
|
||||
@@ -515,6 +520,7 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
std::vector<std::map<std::size_t, double>>
|
||||
constraint_gradients(domain.Nodes().size());
|
||||
std::vector<bool> external_done(domain.Nodes().size(), false);
|
||||
Vector hidden_constraint(directional_third ? domain.Nodes().size() : 0U);
|
||||
Vector third(count);
|
||||
Vector third_scale(count);
|
||||
std::array<std::vector<CooContribution>, 6> entries;
|
||||
@@ -532,9 +538,31 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
return Result<DynamicAssembly>::Failure(*failures[e]);
|
||||
const auto& f = *internal[e];
|
||||
const auto& m = *inertia[e];
|
||||
if (shells[e].internal_kernel->DrillingMode() != frame.mitc4_drilling->mode ||
|
||||
f.drilling_force.Size() != 24U)
|
||||
return Result<DynamicAssembly>::Failure(Failure("Invalid actual drilling mode or force."));
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto local = 6U * node;
|
||||
const auto global = shells[e].scatter[local];
|
||||
const Vector3 force = Triple(f.drilling_force, local);
|
||||
const auto spatial = SpatialMomentSO3(
|
||||
Triple(frame.generalized_coordinates, global + 3U),
|
||||
Triple(f.drilling_force, local + 3U));
|
||||
if (!spatial.HasValue())
|
||||
return Result<DynamicAssembly>::Failure(spatial.GetStatus());
|
||||
const Vector3 position = Vector3(domain.Nodes()[global / 6U].coordinates) +
|
||||
Triple(frame.generalized_coordinates, global);
|
||||
const Vector3 moment = position.Cross(force) + spatial.Value();
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
frame.mitc4_drilling->drilling_moment[c] += moment[c];
|
||||
}
|
||||
for (const double value : frame.mitc4_drilling->drilling_moment)
|
||||
if (!std::isfinite(value))
|
||||
return Result<DynamicAssembly>::Failure(Failure("Drilling spatial moment overflowed."));
|
||||
if (directional_third) {
|
||||
for (std::size_t local = 0U; local < 4U; ++local) {
|
||||
const auto node = shells[e].scatter[6U * local] / 6U;
|
||||
hidden_constraint[node] += f.drilling_constraint[local];
|
||||
auto& gradient = constraint_gradients[node];
|
||||
auto& hessian = constraint_hessians[node];
|
||||
for (std::size_t i = 0U; i < 24U; ++i)
|
||||
@@ -555,6 +583,7 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
Triple(frame.external_force, first), {}, {});
|
||||
if (!external.HasValue())
|
||||
return Result<DynamicAssembly>::Failure(external.GetStatus());
|
||||
hidden_constraint[node] += external.Value().value;
|
||||
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)
|
||||
@@ -740,7 +769,8 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
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_gradient),
|
||||
std::move(trial_constraints), std::move(field_flux)});
|
||||
std::move(trial_constraints), std::move(field_flux),
|
||||
std::move(hidden_constraint)});
|
||||
}
|
||||
|
||||
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
|
||||
@@ -776,10 +806,20 @@ Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
|
||||
frame->global_equilibrium[c] = force_error[c];
|
||||
frame->global_equilibrium[3U + c] = moment_error[c];
|
||||
}
|
||||
Vector3 acceptance_moment = moment_error;
|
||||
if (frame->mitc4_drilling) {
|
||||
results_internal::CompleteDrillingEvidence(frame->global_equilibrium,
|
||||
&*frame->mitc4_drilling);
|
||||
if (!results_internal::ValidDrillingEvidence(frame->mitc4_drilling,
|
||||
frame->global_equilibrium))
|
||||
return Failure("Invalid dynamic drilling equilibrium evidence.");
|
||||
if (frame->mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring)
|
||||
acceptance_moment = moment_error - Vector3(frame->mitc4_drilling->drilling_moment);
|
||||
}
|
||||
if (!force_error.IsFinite() || !moment_error.IsFinite() ||
|
||||
!std::isfinite(force_scale) || !std::isfinite(moment_scale) ||
|
||||
Ratio(force_error.Norm(), force_scale) > 1e-8 ||
|
||||
Ratio(moment_error.Norm(), moment_scale) > 1e-8)
|
||||
Ratio(acceptance_moment.Norm(), moment_scale) > 1e-8)
|
||||
return Failure("Dynamic physical force or moment balance exceeds 1e-8.");
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
@@ -97,6 +97,9 @@ struct DynamicAssembly {
|
||||
std::optional<SparseMatrix> hidden_gradient;
|
||||
std::optional<TrialConstraintData> trial_constraints;
|
||||
std::array<std::vector<hht_field_convergence::FluxOccurrence>, 2> field_flux;
|
||||
// Source-ordered scalar primitive, including the external term exactly once.
|
||||
// Empty when directional primitives were not evaluated; never a zero fallback.
|
||||
Vector hidden_constraint{0U};
|
||||
};
|
||||
|
||||
/// @brief Builds checked shell evaluators in stable source order.
|
||||
|
||||
@@ -82,7 +82,8 @@ const hht_internal::DirectionalShellConstraintData* MatchingConstraints(
|
||||
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(*shell.internal_kernel);
|
||||
if (record.configuration.source.file != configuration.source.file ||
|
||||
record.configuration.source.line != configuration.source.line ||
|
||||
record.configuration.identity != configuration.identity)
|
||||
record.configuration.identity != configuration.identity ||
|
||||
record.configuration.drilling_mode != configuration.drilling_mode)
|
||||
return nullptr;
|
||||
for (std::size_t i = 0U; i < configuration.values.size(); ++i)
|
||||
if (!std::isfinite(record.configuration.values[i]) ||
|
||||
|
||||
@@ -217,6 +217,13 @@ Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain,
|
||||
if (!backend.HasValue())
|
||||
return Result<EigenAnalysisState>::Failure(backend.GetStatus());
|
||||
EigenAnalysisState state(std::move(backend.Value()));
|
||||
for (const auto& reference : references) {
|
||||
if (state.drilling_mode && *state.drilling_mode != reference->DrillingMode())
|
||||
return Result<EigenAnalysisState>::Failure(Failure(
|
||||
"invalid-shell-drilling-evidence", "mode",
|
||||
"All frequency kernels must retain one actual drilling mode."));
|
||||
state.drilling_mode = reference->DrillingMode();
|
||||
}
|
||||
state.full_dof_count = dofs.FullDofCount();
|
||||
state.free_dof_count = reduction.Value().free_count;
|
||||
state.mass_null_dimension = reduction.Value().null_count;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "fesa/elements/element_definition.h"
|
||||
#include "math/finite_values.h"
|
||||
#include "results/shell_result_recovery.h"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
#include "analysis/shell_state_inventory.h"
|
||||
|
||||
namespace fesa {
|
||||
@@ -110,6 +111,13 @@ Status ValidateAcceptedFrame(const AcceptedFrameState& frame,
|
||||
"Accepted frame physical result inventory is invalid.");
|
||||
}
|
||||
|
||||
if (!results_internal::ValidDrillingEvidence(frame.mitc4_drilling,
|
||||
frame.global_equilibrium) ||
|
||||
(accepted_frame_count != 0U && !results_internal::SameDrillingInventory(
|
||||
frame.mitc4_drilling, committed.mitc4_drilling)))
|
||||
return StateFailure(FailureCategory::kSolver, "invalid-shell-drilling-evidence", {},
|
||||
"Accepted drilling evidence must be finite and retain its mode.");
|
||||
|
||||
const double gauss = 1.0 / std::sqrt(3.0);
|
||||
const std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
|
||||
expected_locations{ShellMidsurfaceLocation::kGp1,
|
||||
@@ -243,7 +251,7 @@ Status NonlinearAnalysisState::CommitAcceptedFrame(
|
||||
frame.reaction,
|
||||
frame.shell_results,
|
||||
frame.physical_strain_energy,
|
||||
frame.global_equilibrium};
|
||||
frame.global_equilibrium, frame.mitc4_drilling};
|
||||
std::vector<AcceptedFrameState> accepted_candidate = accepted_frames_;
|
||||
accepted_candidate.push_back(frame);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "assembly/ordered_contribution_internal.h"
|
||||
#include "math/finite_values.h"
|
||||
#include "elements/shell_kernel_values.h"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
@@ -57,6 +58,7 @@ struct InternalAssembly {
|
||||
SparseMatrix tangent;
|
||||
std::vector<ShellResultRow> shell_rows;
|
||||
double physical_strain_energy;
|
||||
Mitc4DrillingEvidence mitc4_drilling;
|
||||
};
|
||||
|
||||
Status ProcedureFailure(const FailureCategory category, const std::string& code,
|
||||
@@ -175,6 +177,10 @@ Result<NonlinearStateBaselineInput> BuildBaseline(const Domain& domain) {
|
||||
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
|
||||
const DofManager& dofs) {
|
||||
std::vector<RuntimeShell> shells;
|
||||
if (domain.Elements().Empty())
|
||||
return Result<std::vector<RuntimeShell>>::Failure(ProcedureFailure(
|
||||
FailureCategory::kModel, "unsupported-nonlinear-element", {}, "ELEMENT",
|
||||
{}, "The nonlinear static procedure requires a MITC4 shell element."));
|
||||
shells.reserve(domain.Elements().Size());
|
||||
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
|
||||
const auto* definition =
|
||||
@@ -320,7 +326,7 @@ NonlinearMitc4TrialDofs ExtractElementTrial(
|
||||
|
||||
Result<InternalAssembly> AssembleInternal(
|
||||
const std::vector<RuntimeShell>& shells, const DofManager& dofs,
|
||||
const Vector& coordinates, const ParallelFor& parallel_for) {
|
||||
const Domain& domain, const Vector& coordinates, const ParallelFor& parallel_for) {
|
||||
std::vector<std::optional<NonlinearMitc4InternalEquilibrium>> local(
|
||||
shells.size());
|
||||
std::vector<std::optional<Status>> failures(shells.size());
|
||||
@@ -341,6 +347,7 @@ Result<InternalAssembly> AssembleInternal(
|
||||
std::vector<ShellResultRow> rows;
|
||||
rows.reserve(shells.size() * 4U);
|
||||
double energy = 0.0;
|
||||
Mitc4DrillingEvidence drilling{shells.front().kernel->DrillingMode()};
|
||||
for (std::size_t index = 0U; index < shells.size(); ++index) {
|
||||
if (failures[index].has_value()) {
|
||||
return Result<InternalAssembly>::Failure(*failures[index]);
|
||||
@@ -354,6 +361,28 @@ Result<InternalAssembly> AssembleInternal(
|
||||
"NONLINEAR_STATIC_PROCEDURE", std::to_string(index),
|
||||
"A nonlinear shell internal residual or tangent is nonfinite."));
|
||||
}
|
||||
if (shells[index].kernel->DrillingMode() != drilling.mode ||
|
||||
local[index]->drilling_force.Size() != kShellDofCount ||
|
||||
!IsFinite(local[index]->drilling_force))
|
||||
return Result<InternalAssembly>::Failure(ProcedureFailure(
|
||||
FailureCategory::kSolver, "invalid-shell-drilling-evidence", {},
|
||||
"NONLINEAR_STATIC_PROCEDURE", {}, "Drilling mode or force is invalid."));
|
||||
const auto element_trial = ExtractElementTrial(shells[index], coordinates);
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto first = 6U * node;
|
||||
const auto global_node = shells[index].scatter[first] / 6U;
|
||||
const auto& values = local[index]->drilling_force;
|
||||
const Vector3 drilling_force{values[first], values[first + 1U], values[first + 2U]};
|
||||
const auto spatial = SpatialMomentSO3(element_trial.rotation_vector[node],
|
||||
{values[first + 3U], values[first + 4U], values[first + 5U]});
|
||||
if (!spatial.HasValue())
|
||||
return Result<InternalAssembly>::Failure(spatial.GetStatus());
|
||||
const Vector3 position = Vector3(domain.Nodes()[global_node].coordinates) +
|
||||
element_trial.displacement[node];
|
||||
const Vector3 moment = position.Cross(drilling_force) + spatial.Value();
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
drilling.drilling_moment[c] += moment[c];
|
||||
}
|
||||
for (std::size_t row = 0U; row < kShellDofCount; ++row) {
|
||||
force[shells[index].scatter[row]] += local[index]->internal_residual[row];
|
||||
assembly_internal::AppendLocalMatrixRow(
|
||||
@@ -373,7 +402,8 @@ Result<InternalAssembly> AssembleInternal(
|
||||
}
|
||||
energy += local[index]->physical_strain_energy;
|
||||
}
|
||||
if (!IsFinite(force) || !std::isfinite(energy)) {
|
||||
if (!IsFinite(force) || !std::isfinite(energy) ||
|
||||
!IsFinite(drilling.drilling_moment)) {
|
||||
return Result<InternalAssembly>::Failure(ProcedureFailure(
|
||||
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||
"NONLINEAR_STATIC_PROCEDURE", {},
|
||||
@@ -387,7 +417,7 @@ Result<InternalAssembly> AssembleInternal(
|
||||
return Result<InternalAssembly>::Failure(tangent.GetStatus());
|
||||
}
|
||||
return Result<InternalAssembly>::Success(
|
||||
{std::move(force), std::move(tangent.Value()), std::move(rows), energy});
|
||||
{std::move(force), std::move(tangent.Value()), std::move(rows), energy, drilling});
|
||||
}
|
||||
|
||||
Result<Vector> Difference(const Vector& left, const Vector& right) {
|
||||
@@ -699,7 +729,7 @@ AcceptedFrameState AcceptedFrame(const std::uint32_t frame_index,
|
||||
trial.reaction,
|
||||
trial.shell_results,
|
||||
trial.physical_strain_energy,
|
||||
trial.global_equilibrium};
|
||||
trial.global_equilibrium, trial.mitc4_drilling};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -807,7 +837,7 @@ Result<NonlinearStaticExecutionResult> ExecuteNonlinearStaticCore(
|
||||
NewtonIterationStage::kKinematicsEvaluated;
|
||||
|
||||
Result<InternalAssembly> internal = AssembleInternal(
|
||||
shells.Value(), dofs.Value(),
|
||||
shells.Value(), dofs.Value(), domain,
|
||||
trial.Value().generalized_coordinates, parallel_for);
|
||||
if (!internal.HasValue()) {
|
||||
return Result<std::optional<ShellStepCandidate>>::Failure(
|
||||
@@ -879,6 +909,9 @@ Result<NonlinearStaticExecutionResult> ExecuteNonlinearStaticCore(
|
||||
NewtonIterationStage::kExternalAssembled));
|
||||
}
|
||||
trial.Value().global_equilibrium = global_equilibrium.Value();
|
||||
trial.Value().mitc4_drilling = internal.Value().mitc4_drilling;
|
||||
results_internal::CompleteDrillingEvidence(
|
||||
trial.Value().global_equilibrium, &*trial.Value().mitc4_drilling);
|
||||
|
||||
Result<PartitionedStiffness> partition =
|
||||
constraints.Partition(tangent.Value(), dofs.Value());
|
||||
|
||||
@@ -95,6 +95,10 @@ Result<ThermalShellRecovery> Ds4ThermalAdapter::Recover(
|
||||
|
||||
Mitc4ReferenceAdapter::Mitc4ReferenceAdapter(const Mitc4Shell& kernel)
|
||||
: kernel_{kernel} {}
|
||||
Mitc4DrillingMode Mitc4ReferenceAdapter::DrillingMode() const noexcept {
|
||||
return kernel_.DrillingMode();
|
||||
}
|
||||
|
||||
const ElementDofLayout& Mitc4ReferenceAdapter::DofLayout() const noexcept {
|
||||
return kernel_.DofLayout();
|
||||
}
|
||||
|
||||
@@ -958,6 +958,22 @@ Result<ElementResultBundle> Mitc4Shell::Recover(
|
||||
ShellElementResultRows rows{};
|
||||
rows.rows.reserve(kNodeCount);
|
||||
rows.physical_strain_energy = recovered.Value().strain_energy;
|
||||
const auto stiffness = Stiffness();
|
||||
if (!stiffness.HasValue())
|
||||
return Result<ElementResultBundle>::Failure(stiffness.GetStatus());
|
||||
const Vector drilling_force =
|
||||
stiffness.Value().drilling_global24.Multiply(element_displacement);
|
||||
rows.drilling_mode = DrillingMode();
|
||||
Vector3 drilling_moment;
|
||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||
const std::size_t first = 6U * node;
|
||||
const Vector3 force{drilling_force[first], drilling_force[first + 1U],
|
||||
drilling_force[first + 2U]};
|
||||
const Vector3 moment{drilling_force[first + 3U], drilling_force[first + 4U],
|
||||
drilling_force[first + 5U]};
|
||||
drilling_moment = drilling_moment + coordinates_[node].Cross(force) + moment;
|
||||
}
|
||||
rows.drilling_moment = drilling_moment.Components();
|
||||
for (std::size_t point = 0U; point < recovered.Value().points.size();
|
||||
++point) {
|
||||
const auto& physical_point = recovered.Value().points[point];
|
||||
|
||||
@@ -48,7 +48,8 @@ Status CommitRecoveryCandidate(RecoveryCandidate candidate,
|
||||
!candidate.shell.rows.empty() ||
|
||||
candidate.shell.physical_strain_energy != 0.0 ||
|
||||
candidate.shell.equilibrium != zero_equilibrium ||
|
||||
candidate.shell.verification_metrics != zero_metrics;
|
||||
candidate.shell.verification_metrics != zero_metrics ||
|
||||
candidate.shell.mitc4_drilling.has_value();
|
||||
const bool has_complete_shell_inventory =
|
||||
!candidate.expected_shell_elements.empty() &&
|
||||
!candidate.shell.rows.empty();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa::results_internal {
|
||||
namespace {
|
||||
@@ -243,11 +244,24 @@ Status RecoverShellGlobalEvidence(const Domain& domain, const DofManager& dofs,
|
||||
candidate.shell.equilibrium[component] = force_balance[component];
|
||||
candidate.shell.equilibrium[3U + component] = moment_balance[component];
|
||||
}
|
||||
Vector3 acceptance_moment = moment_balance;
|
||||
if (candidate.shell.mitc4_drilling) {
|
||||
CompleteDrillingEvidence(candidate.shell.equilibrium,
|
||||
&*candidate.shell.mitc4_drilling);
|
||||
if (!ValidDrillingEvidence(candidate.shell.mitc4_drilling,
|
||||
candidate.shell.equilibrium))
|
||||
return RecoveryFailure("invalid-shell-drilling-evidence",
|
||||
{domain.SourcePath(), 0U}, "global-equilibrium",
|
||||
"Shell drilling diagnostics must be finite and consistent.");
|
||||
if (candidate.shell.mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring)
|
||||
acceptance_moment = moment_balance -
|
||||
Vector3(candidate.shell.mitc4_drilling->drilling_moment);
|
||||
}
|
||||
const double force_metric =
|
||||
NormalizedBalance(force_balance.Components(),
|
||||
(std::max)(applied_force_scale, reaction_force_scale));
|
||||
const double moment_metric = NormalizedBalance(
|
||||
moment_balance.Components(),
|
||||
acceptance_moment.Components(),
|
||||
(std::max)(applied_moment_scale, reaction_moment_scale));
|
||||
candidate.shell.verification_metrics = {global.normalized_free_residual,
|
||||
force_metric, moment_metric};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef FESA_RESULTS_MITC4_DRILLING_EVIDENCE_H_
|
||||
#define FESA_RESULTS_MITC4_DRILLING_EVIDENCE_H_
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
|
||||
#include "fesa/results/result_records.h"
|
||||
|
||||
namespace fesa::results_internal {
|
||||
|
||||
inline bool ValidDrillingMode(const Mitc4DrillingMode mode) {
|
||||
return mode == Mitc4DrillingMode::kDirectSpring ||
|
||||
mode == Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
}
|
||||
|
||||
/// @brief Validates exact raw-minus-internal diagnostics without a tolerance.
|
||||
inline bool ValidDrillingEvidence(
|
||||
const std::optional<Mitc4DrillingEvidence>& evidence,
|
||||
const std::array<double, 6>& raw) {
|
||||
if (!evidence) return true; // Older manually constructed fixtures.
|
||||
if (!ValidDrillingMode(evidence->mode)) return false;
|
||||
for (std::size_t i = 0U; i < 6U; ++i) {
|
||||
const double expected = i < 3U ? raw[i]
|
||||
: raw[i] - evidence->drilling_moment[i - 3U];
|
||||
if (!std::isfinite(raw[i]) || !std::isfinite(expected) ||
|
||||
!std::isfinite(evidence->equilibrium_with_drilling[i]) ||
|
||||
evidence->equilibrium_with_drilling[i] != expected)
|
||||
return false;
|
||||
}
|
||||
for (const double value : evidence->drilling_moment)
|
||||
if (!std::isfinite(value)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool SameDrillingInventory(
|
||||
const std::optional<Mitc4DrillingEvidence>& left,
|
||||
const std::optional<Mitc4DrillingEvidence>& right) {
|
||||
return left.has_value() == right.has_value() &&
|
||||
(!left || left->mode == right->mode);
|
||||
}
|
||||
|
||||
/// @brief Keeps force balance raw and subtracts positive artificial moment once.
|
||||
inline void CompleteDrillingEvidence(
|
||||
const std::array<double, 6>& raw, Mitc4DrillingEvidence* evidence) {
|
||||
evidence->equilibrium_with_drilling = raw;
|
||||
for (std::size_t i = 0U; i < 3U; ++i)
|
||||
evidence->equilibrium_with_drilling[3U + i] -= evidence->drilling_moment[i];
|
||||
}
|
||||
|
||||
} // namespace fesa::results_internal
|
||||
|
||||
#endif // FESA_RESULTS_MITC4_DRILLING_EVIDENCE_H_
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa::results_internal {
|
||||
|
||||
bool IsFiniteShellRow(const ShellResultRow& row) {
|
||||
@@ -29,6 +31,27 @@ Status ShellResultRecovery(const SourceLocation& location,
|
||||
const EntityIndex expected_element,
|
||||
const ShellElementResultRows& rows,
|
||||
RecoveryCandidate& candidate) {
|
||||
if ((rows.drilling_mode && !ValidDrillingMode(*rows.drilling_mode)) ||
|
||||
!IsFiniteArray(rows.drilling_moment) ||
|
||||
(candidate.has_shell_results &&
|
||||
(candidate.shell.mitc4_drilling.has_value() != rows.drilling_mode.has_value() ||
|
||||
(rows.drilling_mode &&
|
||||
candidate.shell.mitc4_drilling->mode != *rows.drilling_mode)))) {
|
||||
return RecoveryFailure("invalid-shell-drilling-evidence", location,
|
||||
source_id.source_label_text,
|
||||
"Shell recovery requires one finite actual drilling mode.");
|
||||
}
|
||||
if (rows.drilling_mode) {
|
||||
if (!candidate.shell.mitc4_drilling)
|
||||
candidate.shell.mitc4_drilling = Mitc4DrillingEvidence{*rows.drilling_mode};
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
candidate.shell.mitc4_drilling->drilling_moment[i] += rows.drilling_moment[i];
|
||||
if (!std::isfinite(candidate.shell.mitc4_drilling->drilling_moment[i]))
|
||||
return RecoveryFailure("nonfinite-recovery-value", location,
|
||||
source_id.source_label_text,
|
||||
"Source-ordered drilling moment overflowed.");
|
||||
}
|
||||
}
|
||||
const double accumulated_energy =
|
||||
candidate.shell.physical_strain_energy + rows.physical_strain_energy;
|
||||
if (!std::isfinite(rows.physical_strain_energy) ||
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
#include "fesa/analysis/prepared_analysis.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/assembly/sparse_assembler.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/results/result_recovery.h"
|
||||
#include "elements/mitc4_drilling_internal.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "fesa/model/model_types.h"
|
||||
@@ -132,6 +135,8 @@ TEST(LinearFrequencyProcedureExecutor,
|
||||
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||
const auto& state = result.Value();
|
||||
ASSERT_TRUE(state.drilling_mode);
|
||||
EXPECT_EQ(*state.drilling_mode, fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
ASSERT_EQ(state.modes.size(), 1U);
|
||||
EXPECT_EQ(state.mass_null_dimension, 0U);
|
||||
EXPECT_EQ(state.finite_dimension, 1U);
|
||||
@@ -189,10 +194,10 @@ TEST(LinearFrequencyProcedureExecutor,
|
||||
}
|
||||
}
|
||||
|
||||
// E2-C09 is a pencil oracle. The public static contract conflict is recorded
|
||||
// separately; direct covariance does not certify spatial moment equilibrium.
|
||||
// E2-C09 keeps raw spatial imbalance visible while the direct-spring gate
|
||||
// subtracts only the separately recovered artificial internal moment.
|
||||
TEST(LinearFrequencyProcedureExecutor,
|
||||
PartialBoundaryStaticRejectsUnbalancedDrillingMoment) {
|
||||
PartialBoundaryStaticSeparatesArtificialMomentAndPreservesLegacyGate) {
|
||||
const auto domain = PanelDomain(false, false, true);
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
const fesa::SerialParallelFor serial;
|
||||
@@ -200,9 +205,32 @@ TEST(LinearFrequencyProcedureExecutor,
|
||||
const auto result =
|
||||
fesa::LinearStaticProcedureExecutor{serial, solver}.Execute(
|
||||
domain.Value());
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
EXPECT_NE(StatusText(result.GetStatus())
|
||||
.find("global-equilibrium-tolerance-failure"),
|
||||
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||
const auto& state = result.Value().state;
|
||||
ASSERT_TRUE(state.Mitc4Drilling());
|
||||
EXPECT_NE(state.Equilibrium()[5U], 0.0);
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
EXPECT_NEAR(state.Equilibrium()[3U + c], state.Mitc4Drilling()->drilling_moment[c], 1e-14);
|
||||
EXPECT_NEAR(state.Mitc4Drilling()->equilibrium_with_drilling[3U + c], 0.0, 1e-14);
|
||||
}
|
||||
auto model = fesa::AnalysisModel::Create(domain.Value());
|
||||
ASSERT_TRUE(model.HasValue());
|
||||
auto dofs = fesa::DofManager::Create(model.Value());
|
||||
ASSERT_TRUE(dofs.HasValue());
|
||||
auto element = fesa::ElementFactory{}.Create(domain.Value().Elements()[0U], domain.Value());
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
auto* shell = dynamic_cast<fesa::Mitc4Shell*>(element.Value().get());
|
||||
ASSERT_NE(shell, nullptr);
|
||||
*shell = fesa::mitc4_drilling_internal::TestAccess::WithMode(
|
||||
*shell, fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
|
||||
fesa::ElementView view{std::cref(*shell)};
|
||||
auto stiffness = fesa::SparseAssembler::Assemble(view, dofs.Value(), serial);
|
||||
ASSERT_TRUE(stiffness.HasValue());
|
||||
auto legacy_state = fesa::AnalysisState::Create(dofs.Value(), {"Step-1", 0U});
|
||||
const auto legacy = fesa::ResultRecovery::Recover(model.Value(), view, dofs.Value(),
|
||||
stiffness.Value(), state.Displacement(), state.ExternalForce(), legacy_state);
|
||||
ASSERT_FALSE(legacy.IsOk());
|
||||
EXPECT_NE(StatusText(legacy).find("global-equilibrium-tolerance-failure"),
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "fesa/analysis/prepared_analysis.h"
|
||||
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||
#include "fesa/analysis/newton_iteration_log.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
|
||||
namespace {
|
||||
@@ -343,3 +350,57 @@ TEST(NonlinearStaticProcedure,
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(Mitc4DrillingEvidence, NonlinearAndPressureFramesRetainSignedTorqueOracle) {
|
||||
for (const bool pressure : {false, true}) {
|
||||
SCOPED_TRACE(pressure);
|
||||
TempDirectory directory{"drilling"};
|
||||
auto deck = AllConstrainedNonlinearShellDeck();
|
||||
const auto boundary = deck.find("All, 1, 6");
|
||||
ASSERT_NE(boundary, std::string::npos);
|
||||
deck.replace(boundary, std::string{"All, 1, 6"}.size(), "All, 1, 5\nAll, 6, 6, 0.125");
|
||||
if (pressure) {
|
||||
const auto assembly_end = deck.find("*End Assembly");
|
||||
deck.insert(assembly_end, "*Surface, TYPE=ELEMENT, NAME=Pressure\nShell-1.1, SPOS\n");
|
||||
deck.insert(deck.find("*End Step"), "*Dsload\nPressure, P, 4.0\n");
|
||||
}
|
||||
const auto path = directory.Path() / "drilling.inp";
|
||||
WriteText(path, deck);
|
||||
auto prepared = fesa::PrepareAnalysisInput(path);
|
||||
ASSERT_TRUE(prepared.HasValue());
|
||||
const auto& domain = prepared.Value().domain;
|
||||
auto element = fesa::ElementFactory{}.Create(domain.Elements()[0U], domain);
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
const auto* shell = dynamic_cast<const fesa::Mitc4Shell*>(element.Value().get());
|
||||
ASSERT_NE(shell, nullptr);
|
||||
const auto stiffness = shell->Stiffness();
|
||||
ASSERT_TRUE(stiffness.HasValue());
|
||||
double trace = 0.0;
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t c = 3U; c < 5U; ++c)
|
||||
trace += stiffness.Value().physical_local20(5U * node + c, 5U * node + c);
|
||||
const double kd = 1e-3 * trace / 8.0;
|
||||
fesa::SerialParallelFor serial;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
fesa::NewtonIterationLogWriter log;
|
||||
ASSERT_TRUE(log.Open(directory.Path() / "drilling.log").IsOk());
|
||||
auto result = fesa::NonlinearStaticProcedureExecutor{serial, tangent}.Execute(domain, log);
|
||||
ASSERT_TRUE(log.Close().IsOk());
|
||||
ASSERT_TRUE(result.HasValue()) << (result.GetStatus().Diagnostics().empty() ? "" : result.GetStatus().Diagnostics()[0U].message);
|
||||
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 10U);
|
||||
for (const auto& frame : result.Value().state.AcceptedFrames()) {
|
||||
ASSERT_TRUE(frame.mitc4_drilling);
|
||||
EXPECT_EQ(frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
const double torque = 4.0 * kd * 0.125 * frame.load_factor;
|
||||
EXPECT_NEAR(frame.global_equilibrium[5U], torque, 1e-13);
|
||||
EXPECT_NEAR(frame.mitc4_drilling->drilling_moment[2U], torque, 1e-13);
|
||||
for (const double value : frame.mitc4_drilling->equilibrium_with_drilling)
|
||||
EXPECT_NEAR(value, 0.0, 1e-13);
|
||||
EXPECT_NEAR(frame.physical_strain_energy, 0.0, 1e-20);
|
||||
if (pressure) EXPECT_NE(frame.external_force[2U], 0.0);
|
||||
}
|
||||
ASSERT_TRUE(result.Value().state.CommittedState().mitc4_drilling);
|
||||
EXPECT_EQ(result.Value().state.CommittedState().mitc4_drilling->drilling_moment,
|
||||
result.Value().state.AcceptedFrames().back().mitc4_drilling->drilling_moment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,3 +320,27 @@ TEST(DynamicAnalysisState, CreateRejectsInvalidScheduleAndNonzeroBoundary) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(DynamicAnalysisState, DrillingEvidenceRejectsPartialCommitAndModeChanges) {
|
||||
auto fixture = MakeFixture();
|
||||
auto initial = MakeFrame(*fixture.domain, 0U);
|
||||
initial.mitc4_drilling = fesa::Mitc4DrillingEvidence{fesa::Mitc4DrillingMode::kDirectSpring};
|
||||
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(initial).IsOk());
|
||||
for (const int kind : {0, 1, 2, 3, 4}) {
|
||||
auto next = MakeFrame(*fixture.domain, 1U);
|
||||
next.mitc4_drilling = initial.mitc4_drilling;
|
||||
if (kind == 0) next.mitc4_drilling.reset();
|
||||
if (kind == 1) next.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
if (kind == 2) next.mitc4_drilling->mode = static_cast<fesa::Mitc4DrillingMode>(99);
|
||||
if (kind == 3) next.mitc4_drilling->equilibrium_with_drilling[3U] = 1.0;
|
||||
if (kind == 4) next.mitc4_drilling->drilling_moment[0U] = std::numeric_limits<double>::quiet_NaN();
|
||||
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(next).IsOk()) << kind;
|
||||
ASSERT_EQ(fixture.state.AcceptedFrames().size(), 1U);
|
||||
EXPECT_EQ(fixture.state.CommittedState().frame_index, 0U);
|
||||
EXPECT_EQ(fixture.state.CommittedState().mitc4_drilling->mode,
|
||||
fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
}
|
||||
auto valid = MakeFrame(*fixture.domain, 1U);
|
||||
valid.mitc4_drilling = initial.mitc4_drilling;
|
||||
EXPECT_TRUE(fixture.state.CommitAcceptedFrame(valid).IsOk());
|
||||
}
|
||||
|
||||
@@ -216,6 +216,12 @@ void ExpectSameDynamicHistory(const RigidTranslationRun& left,
|
||||
EXPECT_DOUBLE_EQ(lhs.drilling_energy, rhs.drilling_energy);
|
||||
EXPECT_DOUBLE_EQ(lhs.external_work, rhs.external_work);
|
||||
EXPECT_DOUBLE_EQ(lhs.energy_defect, rhs.energy_defect);
|
||||
ASSERT_TRUE(lhs.mitc4_drilling);
|
||||
ASSERT_TRUE(rhs.mitc4_drilling);
|
||||
EXPECT_EQ(lhs.mitc4_drilling->mode, rhs.mitc4_drilling->mode);
|
||||
EXPECT_EQ(lhs.mitc4_drilling->drilling_moment, rhs.mitc4_drilling->drilling_moment);
|
||||
EXPECT_EQ(lhs.mitc4_drilling->equilibrium_with_drilling,
|
||||
rhs.mitc4_drilling->equilibrium_with_drilling);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +363,10 @@ TEST(HhtDynamicProcedure, ClampedStepLoadHasPhysicalReactionAndZeroMotion) {
|
||||
EXPECT_EQ(frame.time_integration_formulation,
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
|
||||
EXPECT_EQ(frame.iteration_count, 0U);
|
||||
ASSERT_TRUE(frame.mitc4_drilling);
|
||||
EXPECT_EQ(frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
EXPECT_EQ(frame.mitc4_drilling->drilling_moment, (std::array<double, 3>{}));
|
||||
EXPECT_EQ(frame.mitc4_drilling->equilibrium_with_drilling, frame.global_equilibrium);
|
||||
EXPECT_DOUBLE_EQ(frame.reaction[2U], -4.0);
|
||||
EXPECT_DOUBLE_EQ(frame.external_force[2U], 4.0);
|
||||
EXPECT_DOUBLE_EQ(frame.kinetic_energy, 0.0);
|
||||
|
||||
@@ -22,10 +22,13 @@
|
||||
|
||||
#include "analysis/hht_reduced_internal.h"
|
||||
#include "math/dense_blas_internal.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "elements/mitc4_drilling_internal.h"
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/analysis/hht_time_integration.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -625,6 +628,10 @@ TEST(HhtReducedOperators,
|
||||
[](Assembly& a) {
|
||||
a.trial_constraints->shells[0U].configuration.identity += "x";
|
||||
},
|
||||
[](Assembly& a) {
|
||||
a.trial_constraints->shells[0U].configuration.drilling_mode =
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
},
|
||||
[](Assembly& a) { a.trial_constraints->shells[0U].q[0U] = -0.0; },
|
||||
[](Assembly& a) { a.trial_constraints->shells[0U].v[0U] = -0.0; },
|
||||
[](Assembly& a) {
|
||||
@@ -935,11 +942,15 @@ TEST(HhtReducedOperators, NonfiniteMatchingTimeKeyMissPreservesFreshSuccess) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
|
||||
void CheckShellConstraintScatter(const fesa::Mitc4DrillingMode mode) {
|
||||
const auto fixture = MakeFixture(false);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain,
|
||||
fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
SCOPED_TRACE(static_cast<int>(mode));
|
||||
for (const auto& shell : shells.Value())
|
||||
*shell.internal_kernel = fesa::mitc4_drilling_internal::TestAccess::WithMode(
|
||||
*shell.internal_kernel, mode);
|
||||
fesa::DynamicAcceptedFrameState frame{};
|
||||
for (auto* values : {&frame.generalized_coordinates, &frame.velocity,
|
||||
&frame.acceleration, &frame.algorithmic_acceleration,
|
||||
@@ -980,7 +991,10 @@ TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
|
||||
assembly.Value().frame.residual_scale[6U * node + 3U + component];
|
||||
EXPECT_DOUBLE_EQ(row.value_scale, expected_scale);
|
||||
EXPECT_FALSE(row.gradient.empty());
|
||||
EXPECT_FALSE(row.hessian.empty());
|
||||
if (mode == fesa::Mitc4DrillingMode::kLegacyCorotatedTwist)
|
||||
EXPECT_FALSE(row.hessian.empty());
|
||||
else
|
||||
EXPECT_TRUE(row.hessian.empty());
|
||||
EXPECT_LT(row.gradient.size(), fixture.dofs.FullDofCount());
|
||||
for (const auto& entry : row.hessian) {
|
||||
EXPECT_LT(entry.row, fixture.dofs.FullDofCount());
|
||||
@@ -989,6 +1003,14 @@ TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
|
||||
CheckShellConstraintScatter(fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
|
||||
}
|
||||
|
||||
TEST(HhtReducedOperators, DirectSpringScattersZeroInitialHessianByConstraintRow) {
|
||||
CheckShellConstraintScatter(fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
}
|
||||
|
||||
TEST(HhtReducedEmbedding, RejectsNonfiniteGraphResidualBeforeAcceptance) {
|
||||
const auto fixture = MakeFixture(false);
|
||||
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
|
||||
@@ -1184,11 +1206,15 @@ TEST(HhtReducedEmbedding,
|
||||
EXPECT_DOUBLE_EQ(rates.Value().z_acceleration[0U], 1.0);
|
||||
}
|
||||
|
||||
TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
|
||||
void CheckActualShellRatesWithMixedTimeLoad(const fesa::Mitc4DrillingMode mode) {
|
||||
const auto fixture = MakeFixture(false, true);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain,
|
||||
fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
SCOPED_TRACE(static_cast<int>(mode));
|
||||
for (const auto& shell : shells.Value())
|
||||
*shell.internal_kernel = fesa::mitc4_drilling_internal::TestAccess::WithMode(
|
||||
*shell.internal_kernel, mode);
|
||||
const fesa::Vector zero(fixture.dofs.FullDofCount());
|
||||
const auto initial = Assemble(fixture, shells.Value(), zero, zero, zero,
|
||||
zero, true);
|
||||
@@ -1470,13 +1496,26 @@ TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
|
||||
++row) {
|
||||
auto& omitted = omitted_external_gradient.rows[row].gradient;
|
||||
const auto& no_force = no_force_operators.Value().rows[row].gradient;
|
||||
ASSERT_EQ(omitted.size(), no_force.size());
|
||||
for (std::size_t entry = 0U; entry < omitted.size(); ++entry) {
|
||||
ASSERT_EQ(omitted[entry].dof, no_force[entry].dof);
|
||||
saw_external_position_gradient =
|
||||
saw_external_position_gradient ||
|
||||
omitted[entry].value != no_force[entry].value;
|
||||
omitted[entry].value = no_force[entry].value;
|
||||
if (mode == fesa::Mitc4DrillingMode::kLegacyCorotatedTwist) {
|
||||
ASSERT_EQ(omitted.size(), no_force.size());
|
||||
for (std::size_t entry = 0U; entry < omitted.size(); ++entry) {
|
||||
ASSERT_EQ(omitted[entry].dof, no_force[entry].dof);
|
||||
saw_external_position_gradient =
|
||||
saw_external_position_gradient ||
|
||||
omitted[entry].value != no_force[entry].value;
|
||||
omitted[entry].value = no_force[entry].value;
|
||||
}
|
||||
} else {
|
||||
// Direct internal/external derivatives can cancel exact sparse entries.
|
||||
// Compare the full union before replacing only the external contribution.
|
||||
fesa::Vector with_force(fixture.dofs.FullDofCount());
|
||||
fesa::Vector without_force(fixture.dofs.FullDofCount());
|
||||
for (const auto& entry : omitted) with_force[entry.dof] = entry.value;
|
||||
for (const auto& entry : no_force) without_force[entry.dof] = entry.value;
|
||||
for (std::size_t dof = 0U; dof < with_force.Size(); ++dof)
|
||||
saw_external_position_gradient = saw_external_position_gradient ||
|
||||
with_force[dof] != without_force[dof];
|
||||
omitted = no_force;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(saw_external_position_gradient);
|
||||
@@ -1560,6 +1599,14 @@ TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
|
||||
EXPECT_GT(omitted_second_time_hidden, 1.0e-8);
|
||||
}
|
||||
|
||||
TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
|
||||
CheckActualShellRatesWithMixedTimeLoad(fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
|
||||
}
|
||||
|
||||
TEST(HhtReducedEmbedding, DirectSpringReconstructsActualShellRatesWithMixedTimeLoad) {
|
||||
CheckActualShellRatesWithMixedTimeLoad(fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
}
|
||||
|
||||
TEST(HhtReducedChannelNewton, RequiresEveryTranslationGateAndThaws) {
|
||||
fesa::hht_reduced_internal::FixedChart chart{};
|
||||
chart.full_dof_count = 6U;
|
||||
@@ -1737,3 +1784,158 @@ TEST(HhtReducedOperators, NullMetricKeepsFiniteLeftAssociatedProducts) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(Mitc4DrillingEvidence, HhtActualMomentMatchesClosedFormAndSourceOrder) {
|
||||
const auto fixture = MakeFixture(false, true);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const fesa::Vector zero(fixture.dofs.FullDofCount());
|
||||
auto q = zero;
|
||||
for (std::size_t node = 0U; node < fixture.domain->Nodes().size(); ++node) {
|
||||
q[6U * node + 0U] = 0.03 * static_cast<double>(node);
|
||||
q[6U * node + 3U] = 0.13 + 0.01 * static_cast<double>(node);
|
||||
q[6U * node + 4U] = -0.09;
|
||||
q[6U * node + 5U] = 0.21 - 0.01 * static_cast<double>(node);
|
||||
}
|
||||
const auto serial = Assemble(fixture, shells.Value(), q, zero, zero, zero, true);
|
||||
ASSERT_TRUE(serial.HasValue());
|
||||
ASSERT_TRUE(serial.Value().frame.mitc4_drilling);
|
||||
fesa::Vector3 expected;
|
||||
for (std::size_t element = 0U; element < fixture.domain->ShellElements().Size(); ++element) {
|
||||
const auto& definition = fixture.domain->ShellElements()[element];
|
||||
std::array<const fesa::Node*, 4> nodes{};
|
||||
std::array<std::array<double, 3>, 4> directors{};
|
||||
for (std::size_t i = 0U; i < 4U; ++i) {
|
||||
nodes[i] = &fixture.domain->Nodes()[definition.node_indices[i]];
|
||||
directors[i] = fixture.domain->ShellNodeInitialFrames()[definition.node_indices[i]].director;
|
||||
}
|
||||
const auto* section = dynamic_cast<const fesa::ShellSection*>(
|
||||
&fixture.domain->Properties()[definition.section_index]);
|
||||
const auto* material = dynamic_cast<const fesa::LinearElasticMaterial*>(
|
||||
&fixture.domain->Materials()[definition.material_index]);
|
||||
ASSERT_NE(section, nullptr);
|
||||
ASSERT_NE(material, nullptr);
|
||||
const auto kernel = fesa::Mitc4Shell::Create(nodes, directors, *section, *material);
|
||||
ASSERT_TRUE(kernel.HasValue());
|
||||
const auto stiffness = kernel.Value().Stiffness();
|
||||
ASSERT_TRUE(stiffness.HasValue());
|
||||
double trace = 0.0;
|
||||
for (std::size_t i = 0U; i < 4U; ++i)
|
||||
for (std::size_t c = 3U; c < 5U; ++c)
|
||||
trace += stiffness.Value().physical_local20(5U * i + c, 5U * i + c);
|
||||
const double kd = 1e-3 * trace / 8.0;
|
||||
for (std::size_t i = 0U; i < 4U; ++i) {
|
||||
const auto node = definition.node_indices[i];
|
||||
const fesa::Vector3 phi{q[6U * node + 3U], q[6U * node + 4U], q[6U * node + 5U]};
|
||||
const fesa::Vector3 director{directors[i]};
|
||||
const double angle = phi.Norm();
|
||||
const double coefficient = (1.0 - 0.5 * angle / std::tan(0.5 * angle)) / (angle * angle);
|
||||
// Independent closed inverse-transpose Rodrigues identity; no dense solve.
|
||||
expected = expected + kd * phi.Dot(director) *
|
||||
(director + 0.5 * phi.Cross(director) + coefficient * phi.Cross(phi.Cross(director)));
|
||||
}
|
||||
}
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
EXPECT_NEAR(serial.Value().frame.mitc4_drilling->drilling_moment[c], expected[c], 2e-14);
|
||||
EXPECT_EQ(serial.Value().frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
auto frame = serial.Value().frame;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05);
|
||||
ASSERT_TRUE(parameters.HasValue());
|
||||
fesa::TbbParallelFor parallel;
|
||||
const auto tbb = fesa::hht_internal::EvaluateAssembly(*fixture.domain, fixture.dofs,
|
||||
shells.Value(), frame, zero, 0.1, parameters.Value(), parallel, true);
|
||||
ASSERT_TRUE(tbb.HasValue());
|
||||
ASSERT_TRUE(tbb.Value().frame.mitc4_drilling);
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
ExpectSameBits(tbb.Value().frame.mitc4_drilling->drilling_moment[c],
|
||||
serial.Value().frame.mitc4_drilling->drilling_moment[c]);
|
||||
}
|
||||
|
||||
TEST(Mitc4DrillingEvidence, HhtCorrectedGateRejectsOppositeSignAndRealImbalance) {
|
||||
const auto fixture = MakeFixture(false);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const fesa::Vector zero(fixture.dofs.FullDofCount());
|
||||
auto q = zero;
|
||||
for (std::size_t node = 0U; node < 4U; ++node) q[6U * node + 5U] = 0.125;
|
||||
const auto assembly = Assemble(fixture, shells.Value(), q, zero, zero, zero, false);
|
||||
ASSERT_TRUE(assembly.HasValue());
|
||||
EXPECT_EQ(assembly.Value().hidden_constraint.Size(), 0U);
|
||||
const auto directional = Assemble(fixture, shells.Value(), q, zero, zero, zero, true);
|
||||
ASSERT_TRUE(directional.HasValue());
|
||||
ASSERT_EQ(directional.Value().hidden_constraint.Size(), 4U);
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
EXPECT_GT(directional.Value().hidden_constraint[node], 0.0);
|
||||
fesa::MklPardisoSolver positive;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto unavailable_frame = assembly.Value().frame;
|
||||
const auto unavailable = fesa::hht_internal::CompleteConstraintEvidence(
|
||||
*fixture.domain, fixture.dofs, assembly.Value(), 2.0,
|
||||
fesa::hht_internal::NullNodes(fixture.dofs, assembly.Value().rotations),
|
||||
positive, tangent, &unavailable_frame);
|
||||
EXPECT_FALSE(unavailable.IsOk());
|
||||
EXPECT_EQ(unavailable_frame.hidden_constraint.Size(), 0U);
|
||||
auto frame = assembly.Value().frame;
|
||||
// A support supplying the complete internal force gives exact static balance.
|
||||
frame.reaction = frame.internal_force;
|
||||
ASSERT_TRUE(fesa::hht_internal::CompleteEquilibrium(*fixture.domain, fixture.dofs,
|
||||
assembly.Value(), &frame).IsOk());
|
||||
ASSERT_TRUE(frame.mitc4_drilling);
|
||||
EXPECT_GT(frame.global_equilibrium[5U], 0.0);
|
||||
EXPECT_NEAR(frame.global_equilibrium[5U], frame.mitc4_drilling->drilling_moment[2U], 1e-14);
|
||||
for (const double value : frame.mitc4_drilling->equilibrium_with_drilling)
|
||||
EXPECT_NEAR(value, 0.0, 1e-14);
|
||||
for (const int kind : {0, 1, 2, 3}) {
|
||||
auto invalid = frame;
|
||||
if (kind == 0) invalid.mitc4_drilling->drilling_moment[2U] *= -1.0;
|
||||
if (kind == 1) invalid.reaction[0U] += 1.0;
|
||||
if (kind == 2) invalid.reaction[4U] += 1.0;
|
||||
if (kind == 3) invalid.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
EXPECT_FALSE(fesa::hht_internal::CompleteEquilibrium(*fixture.domain, fixture.dofs,
|
||||
assembly.Value(), &invalid).IsOk()) << kind;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Mitc4DrillingEvidence, DirectHiddenCertificateUsesCanonicalPrimitive) {
|
||||
const auto fixture = MakeFixture(false);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const fesa::Vector zero(fixture.dofs.FullDofCount());
|
||||
auto q = zero;
|
||||
auto external = zero;
|
||||
fesa::NonlinearMitc4TrialDofs trial{};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
q[6U * node] = 0.03 * static_cast<double>(node);
|
||||
trial.displacement[node] = {q[6U * node], 0.0, 0.0};
|
||||
trial.rotation_vector[node] = {0.00005, 0.000035, 0.00002};
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
q[6U * node + 3U + c] = trial.rotation_vector[node][c];
|
||||
}
|
||||
const auto local = shells.Value()[0U].internal->EvaluateTrial(trial, &zero);
|
||||
ASSERT_TRUE(local.HasValue());
|
||||
external = local.Value().drilling_force;
|
||||
const auto assembly = Assemble(fixture, shells.Value(), q, zero, zero, external, true);
|
||||
ASSERT_TRUE(assembly.HasValue());
|
||||
const auto length = fesa::hht_internal::ReferenceLength(*fixture.domain);
|
||||
ASSERT_TRUE(length.HasValue());
|
||||
const auto null_nodes = fesa::hht_internal::NullNodes(fixture.dofs, assembly.Value().rotations);
|
||||
ASSERT_EQ(null_nodes.size(), 4U);
|
||||
fesa::MklPardisoSolver positive;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto frame = assembly.Value().frame;
|
||||
const auto status = fesa::hht_internal::CompleteConstraintEvidence(
|
||||
*fixture.domain, fixture.dofs, assembly.Value(), length.Value(), null_nodes,
|
||||
positive, tangent, &frame);
|
||||
ASSERT_TRUE(status.IsOk()) << (status.Diagnostics().empty() ? "" : status.Diagnostics()[0U].message);
|
||||
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, assembly.Value().rotations);
|
||||
ASSERT_TRUE(chart.HasValue());
|
||||
const auto canonical = fesa::hht_reduced_internal::EvaluateConstraintOperators(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), assembly.Value(), chart.Value(), zero, zero);
|
||||
ASSERT_TRUE(canonical.HasValue());
|
||||
for (std::size_t row = 0U; row < 4U; ++row) {
|
||||
EXPECT_DOUBLE_EQ(frame.hidden_constraint[row], canonical.Value().rows[row].value);
|
||||
EXPECT_NEAR(frame.hidden_constraint[row], 0.0, 1e-17);
|
||||
EXPECT_DOUBLE_EQ(frame.hidden_velocity_defect[row], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.hidden_acceleration_defect[row], 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "fesa/analysis/newton_iteration_log.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "elements/mitc4_drilling_internal.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
|
||||
@@ -120,6 +121,14 @@ struct Fixture {
|
||||
double length;
|
||||
};
|
||||
|
||||
// The finite rigid-orbit and free-spin C08 fixtures prescribe the historical
|
||||
// objective potential and its hidden velocity equation explicitly.
|
||||
void UseLegacyDrilling(const Fixture& fixture) {
|
||||
for (const auto& shell : fixture.shells)
|
||||
*shell.internal_kernel = fesa::mitc4_drilling_internal::TestAccess::WithMode(
|
||||
*shell.internal_kernel, fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
|
||||
}
|
||||
|
||||
Fixture MakeFixture(const std::size_t divisions,
|
||||
const bool lifecycle_translation = false) {
|
||||
fesa::ModelDefinition definition{};
|
||||
@@ -643,6 +652,7 @@ TEST(HhtShellPhysics, ManufacturedHiddenCountsExternalHessianOnce) {
|
||||
TEST(HhtShellPhysics,
|
||||
C08aRigidOrbitMatchesIndependentVolumeForcePowerAndMomentum) {
|
||||
const auto fixture = MakeFixture(1U);
|
||||
UseLegacyDrilling(fixture);
|
||||
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
|
||||
const auto initial =
|
||||
Evaluate(fixture, EmptyFrame(fixture), 1.0 / 16.0, parameters, false);
|
||||
@@ -3078,6 +3088,7 @@ std::array<double, 2> AccelerationSplit(const Frame& basis,
|
||||
|
||||
TEST(HhtShellPhysics, ReducedManufacturedControllerKeepsExactInitialOrbit) {
|
||||
const auto fixture = MakeFixture(1U);
|
||||
UseLegacyDrilling(fixture);
|
||||
const auto grid = RunPlate(fixture, 1.0 / 64.0, true);
|
||||
ASSERT_EQ(grid.history.size(), 65U);
|
||||
const auto exact = RigidOrbit(fixture, 0.0);
|
||||
@@ -3115,6 +3126,7 @@ TEST(HhtShellPhysics, ReducedManufacturedControllerKeepsExactInitialOrbit) {
|
||||
|
||||
TEST(HhtShellPhysics, C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence) {
|
||||
const auto fixture = MakeFixture(1U);
|
||||
UseLegacyDrilling(fixture);
|
||||
std::vector<GridEvidence> grids;
|
||||
std::vector<std::array<double, 6>> errors;
|
||||
for (const unsigned denominator : {16U, 32U, 64U, 128U, 256U, 512U, 1024U}) {
|
||||
@@ -3297,6 +3309,7 @@ TEST(HhtShellPhysics, C08bInitialPhysicalSpectrumProbe) {
|
||||
TEST(HhtShellPhysics,
|
||||
PredictorStartCompletesPreviouslyFailingLumpedFreePlateGrid) {
|
||||
const auto fixture = MakeFixture(2U);
|
||||
UseLegacyDrilling(fixture);
|
||||
const auto evidence = RunPlate(fixture, 1.0 / 256.0, false);
|
||||
ASSERT_EQ(evidence.history.size(), 65U);
|
||||
EXPECT_DOUBLE_EQ(evidence.history.back().time, kEndTime);
|
||||
@@ -3307,6 +3320,7 @@ TEST(HhtShellPhysics,
|
||||
TEST(HhtShellPhysics,
|
||||
C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants) {
|
||||
const auto fixture = MakeFixture(2U);
|
||||
UseLegacyDrilling(fixture);
|
||||
std::vector<GridEvidence> grids;
|
||||
for (const unsigned denominator :
|
||||
{64U, 128U, 256U, 512U, 1024U, 32768U, 65536U, 131072U}) {
|
||||
|
||||
@@ -454,3 +454,28 @@ TEST(NonlinearAnalysisState, StoresNoUnapprovedVerificationMetrics) {
|
||||
EXPECT_FALSE((HasVerificationMetrics<fesa::NonlinearTrialState>::value));
|
||||
EXPECT_FALSE((HasVerificationMetrics<fesa::AcceptedFrameState>::value));
|
||||
}
|
||||
|
||||
TEST(NonlinearAnalysisState, DrillingEvidenceRetainsModeAndRejectsPartialCommit) {
|
||||
const auto fixture = MakeFixture();
|
||||
auto result = fesa::NonlinearAnalysisState::Create(*fixture.domain, *fixture.dofs,
|
||||
MakeBaseline(*fixture.domain));
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
auto& state = result.Value();
|
||||
auto first = MakeFrame(state.CommittedState(), 0U, 1U, 0.5, 1U);
|
||||
first.mitc4_drilling = fesa::Mitc4DrillingEvidence{fesa::Mitc4DrillingMode::kDirectSpring};
|
||||
ASSERT_TRUE(state.CommitAcceptedFrame(first).IsOk());
|
||||
for (const int kind : {0, 1, 2, 3, 4}) {
|
||||
auto next = MakeFrame(state.CommittedState(), 1U, 2U, 1.0, 1U);
|
||||
next.mitc4_drilling = first.mitc4_drilling;
|
||||
if (kind == 0) next.mitc4_drilling.reset();
|
||||
if (kind == 1) next.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
if (kind == 2) next.mitc4_drilling->mode = static_cast<fesa::Mitc4DrillingMode>(99);
|
||||
if (kind == 3) next.mitc4_drilling->equilibrium_with_drilling[3U] = 1.0;
|
||||
if (kind == 4) next.mitc4_drilling->drilling_moment[0U] = std::numeric_limits<double>::infinity();
|
||||
EXPECT_FALSE(state.CommitAcceptedFrame(next).IsOk()) << kind;
|
||||
ASSERT_EQ(state.AcceptedFrames().size(), 1U);
|
||||
EXPECT_DOUBLE_EQ(state.CommittedState().load_factor, 0.5);
|
||||
EXPECT_EQ(state.CommittedState().mitc4_drilling->mode,
|
||||
fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/assembly/sparse_assembler.h"
|
||||
#include "fesa/elements/element.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "fesa/results/result_recovery.h"
|
||||
#include "results/analysis_state_commit.h"
|
||||
#include "results/beam_result_recovery.h"
|
||||
#include "results/global_equilibrium_recovery.h"
|
||||
@@ -627,3 +630,99 @@ TEST(ResultRecoveryComponents,
|
||||
ExpectStatusCode(status, "nonfinite-recovery-value");
|
||||
ExpectAnalysisStateEqual(state, prior);
|
||||
}
|
||||
|
||||
TEST(Mitc4DrillingEvidence, LinearDirectSpringAcceptsKnownArtificialTorque) {
|
||||
auto definition = MakeShellDefinition();
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
definition.steps[0U].boundaries.push_back(
|
||||
{std::to_string(node + 1U), 1, 5, 0.0, {definition.source_path, 51U}});
|
||||
definition.steps[0U].boundaries.push_back(
|
||||
{std::to_string(node + 1U), 6, 6, 0.125, {definition.source_path, 52U}});
|
||||
}
|
||||
const auto fixture = MakeShellFixture(std::move(definition));
|
||||
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
state.Displacement()[6U * node + 5U] = 0.125;
|
||||
const auto status = fesa::ResultRecovery::Recover(
|
||||
*fixture.model, *fixture.dofs, *fixture.stiffness, state);
|
||||
ASSERT_TRUE(status.IsOk()) << (status.Diagnostics().empty() ? "" : status.Diagnostics()[0U].message);
|
||||
}
|
||||
|
||||
TEST(Mitc4DrillingEvidence, LinearTorqueOracleAndSignedGateKeepRealImbalance) {
|
||||
auto definition = MakeShellDefinition();
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
definition.steps[0U].boundaries.push_back(
|
||||
{std::to_string(node + 1U), 1, 5, 0.0, {definition.source_path, 51U}});
|
||||
definition.steps[0U].boundaries.push_back(
|
||||
{std::to_string(node + 1U), 6, 6, 0.125, {definition.source_path, 52U}});
|
||||
}
|
||||
const auto fixture = MakeShellFixture(std::move(definition));
|
||||
auto element = fesa::ElementFactory{}.Create(fixture.domain->Elements()[0U],
|
||||
*fixture.domain);
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
const auto* shell = dynamic_cast<const fesa::Mitc4Shell*>(element.Value().get());
|
||||
ASSERT_NE(shell, nullptr);
|
||||
const auto stiffness = shell->Stiffness();
|
||||
ASSERT_TRUE(stiffness.HasValue());
|
||||
double rotary_trace = 0.0;
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t c = 3U; c < 5U; ++c)
|
||||
rotary_trace += stiffness.Value().physical_local20(5U * node + c, 5U * node + c);
|
||||
const double expected_torque = 4.0 * 0.125 * (1e-3 * rotary_trace / 8.0);
|
||||
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
state.Displacement()[6U * node + 5U] = 0.125;
|
||||
ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs,
|
||||
*fixture.stiffness, state).IsOk());
|
||||
ASSERT_TRUE(state.Mitc4Drilling());
|
||||
EXPECT_EQ(state.Mitc4Drilling()->mode, fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
EXPECT_NEAR(state.Equilibrium()[5U], expected_torque, 1e-14);
|
||||
EXPECT_NEAR(state.Mitc4Drilling()->drilling_moment[2U], expected_torque, 1e-14);
|
||||
EXPECT_DOUBLE_EQ(state.Mitc4Drilling()->drilling_moment[0U], 0.0);
|
||||
EXPECT_DOUBLE_EQ(state.Mitc4Drilling()->drilling_moment[1U], 0.0);
|
||||
for (double value : state.Mitc4Drilling()->equilibrium_with_drilling)
|
||||
EXPECT_NEAR(value, 0.0, 1e-14);
|
||||
EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), 0.0);
|
||||
|
||||
auto global = fesa::results_internal::GlobalEquilibriumRecovery(
|
||||
*fixture.domain, *fixture.dofs, *fixture.stiffness, state.Displacement(),
|
||||
state.ExternalForce());
|
||||
ASSERT_TRUE(global.HasValue());
|
||||
for (const int kind : {0, 1, 2, 3}) {
|
||||
auto changed = global.Value();
|
||||
fesa::results_internal::RecoveryCandidate candidate{24U};
|
||||
candidate.shell.mitc4_drilling = *state.Mitc4Drilling();
|
||||
if (kind == 0) candidate.shell.mitc4_drilling->drilling_moment[2U] *= -1.0;
|
||||
if (kind == 1) changed.residual[0U] += 1.0;
|
||||
if (kind == 2) changed.residual[4U] += 1.0;
|
||||
if (kind == 3) candidate.shell.mitc4_drilling->mode =
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
const auto rejected = fesa::results_internal::RecoverShellGlobalEvidence(
|
||||
*fixture.domain, *fixture.dofs, state.ExternalForce(), changed, candidate);
|
||||
EXPECT_FALSE(rejected.IsOk()) << kind;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Mitc4DrillingEvidence, StaticCommitRejectsInvalidEvidenceAtomically) {
|
||||
const auto fixture = MakeShellFixture(MakeShellDefinition());
|
||||
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
|
||||
fesa::ShellStateCandidate candidate{};
|
||||
candidate.rows = MakeShellRows(0U, 1.0).rows;
|
||||
candidate.mitc4_drilling = fesa::Mitc4DrillingEvidence{
|
||||
fesa::Mitc4DrillingMode::kDirectSpring, {0.0, 0.0, 2.0}, {0, 0, 0, 0, 0, -2}};
|
||||
ASSERT_TRUE(state.CommitShellResults({0U}, candidate).IsOk());
|
||||
for (const int kind : {0, 1, 2, 3, 4}) {
|
||||
auto invalid = candidate;
|
||||
invalid.physical_strain_energy = 77.0;
|
||||
if (kind == 0) invalid.mitc4_drilling->mode = static_cast<fesa::Mitc4DrillingMode>(99);
|
||||
if (kind == 1) invalid.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
if (kind == 2) invalid.mitc4_drilling->drilling_moment[0U] = std::numeric_limits<double>::infinity();
|
||||
if (kind == 3) invalid.mitc4_drilling->equilibrium_with_drilling[5U] = 2.0;
|
||||
if (kind == 4) invalid.mitc4_drilling.reset();
|
||||
EXPECT_FALSE(state.CommitShellResults({0U}, invalid).IsOk()) << kind;
|
||||
EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), 0.0);
|
||||
ASSERT_TRUE(state.Mitc4Drilling());
|
||||
EXPECT_EQ(state.Mitc4Drilling()->mode, candidate.mitc4_drilling->mode);
|
||||
EXPECT_EQ(state.Mitc4Drilling()->drilling_moment, candidate.mitc4_drilling->drilling_moment);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user