feat: add eigen2 frequency pipeline and fail-closed validation

Implement MITC4 mass, constrained whitening, MKL FEAST, modal HDF5, and deterministic Value comparison. MSVC Debug builds; 394 of 395 tests pass. Original-model all-c residual and the documented S3 static contract conflict remain unresolved; this is an implementation checkpoint, not feature acceptance.
This commit is contained in:
KOKO\Mimi
2026-09-09 03:14:30 +09:00
parent e74ac247db
commit 22c9d4435c
39 changed files with 7240 additions and 541 deletions
@@ -0,0 +1,78 @@
#ifndef FESA_ANALYSIS_EIGEN_ANALYSIS_STATE_H_
#define FESA_ANALYSIS_EIGEN_ANALYSIS_STATE_H_
#include <cstddef>
#include <cstdint>
#include <optional>
#include <utility>
#include <vector>
#include "fesa/math/matrix.h"
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
namespace fesa {
/// @brief Identifies one requested mode without conflating equal eigenvalues.
struct EigenMode {
std::size_t mode_id;
double eigenvalue;
std::uint8_t classification;
std::size_t cluster_id;
std::optional<double> frequency;
};
/// @brief Records the analytic mass split at one source-ordered node.
struct EigenNodalReductionRecord {
std::uint8_t free_count;
std::uint8_t null_count;
std::uint8_t positive_count;
bool rcond_available;
double rcond_inf;
};
/// @brief Stores independently evaluated evidence for every computed pair.
struct EigenpairCheck {
std::size_t sorted_rank;
std::size_t solver_column;
double eigenvalue;
double modal_mass;
double rayleigh_quotient;
double eta_r;
double eta_w;
double rho_b;
double eta_norm;
double eta_energy;
bool backend_res_available;
double backend_res;
};
/// @brief Owns a validated modal candidate independently of static state.
/// @note Vectors are requested-mode by full source DOF; they are not response
/// displacements. Backend work vectors are released after all-c validation.
struct EigenAnalysisState {
explicit EigenAnalysisState(FeastEigenResult backend_result)
: backend{std::move(backend_result)} {}
std::size_t full_dof_count{0U};
std::size_t free_dof_count{0U};
std::size_t mass_null_dimension{0U};
std::size_t finite_dimension{0U};
double characteristic_length{0.0};
double eta_gram{0.0};
double max_eta_orth{0.0};
bool null_checks_applicable{false};
double eta_mz{0.0};
double eta_kz{0.0};
double eta_pz{0.0};
std::vector<std::uint8_t> constraint_mask;
std::vector<double> prescribed_displacement;
std::vector<EigenNodalReductionRecord> nodal_reduction;
std::vector<EigenMode> modes;
Matrix vectors{0U, 0U};
std::vector<EigenpairCheck> eigenpair_checks;
FeastEigenResult backend;
};
} // namespace fesa
#endif // FESA_ANALYSIS_EIGEN_ANALYSIS_STATE_H_
@@ -0,0 +1,20 @@
#ifndef FESA_ANALYSIS_LINEAR_FREQUENCY_PROCEDURE_H_
#define FESA_ANALYSIS_LINEAR_FREQUENCY_PROCEDURE_H_
#include "fesa/analysis/eigen_analysis_state.h"
namespace fesa {
class Domain;
/// @brief Executes the supported linear MITC4 frequency procedure.
class LinearFrequencyProcedureExecutor final {
public:
/// @brief Builds, solves, and validates one frequency modal candidate.
/// @return Requested physical modes plus all-c validation evidence.
Result<EigenAnalysisState> Execute(const Domain& domain) const;
};
} // namespace fesa
#endif // FESA_ANALYSIS_LINEAR_FREQUENCY_PROCEDURE_H_
+7 -2
View File
@@ -6,6 +6,7 @@
#include <vector>
#include "fesa/analysis/analysis_state.h"
#include "fesa/analysis/eigen_analysis_state.h"
#include "fesa/analysis/nonlinear_analysis_state.h"
#include "fesa/core/diagnostic.h"
#include "fesa/core/status.h"
@@ -14,6 +15,8 @@
namespace fesa {
class LinearStaticProcedureExecutor;
class LinearFrequencyProcedureExecutor;
class EigenResultsWriter;
class NewtonIterationLogWriter;
class NonlinearResultsWriter;
class NonlinearStaticProcedureExecutor;
@@ -47,7 +50,8 @@ struct NonlinearStaticExecutionResult {
};
using PreparedAnalysisOutcome =
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult>;
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
EigenAnalysisState>;
/// @brief Selects the approved procedure from a prepared Domain.
/// @pre The executors and log outlive this call.
@@ -55,6 +59,7 @@ using PreparedAnalysisOutcome =
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
NonlinearStaticProcedureExecutor& nonlinear_executor,
const LinearFrequencyProcedureExecutor& frequency_executor,
NewtonIterationLogWriter& log);
/// @brief Writes an already computed outcome to its authoritative result file.
@@ -64,7 +69,7 @@ Status FinalizePreparedAnalysisResult(
const DerivedOutputPaths& paths, const Domain& domain,
const PreparedAnalysisOutcome& outcome,
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
NonlinearResultsWriter& nonlinear_writer);
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer);
} // namespace fesa
+13
View File
@@ -98,6 +98,14 @@ struct Mitc4Stiffness {
double drilling_stiffness;
};
/// @brief Stores consistent scalar moments and their derived MITC4 masses.
struct Mitc4Mass {
std::array<Matrix, 3> consistent_moments;
Matrix consistent_global24;
Matrix lumped_global24;
std::array<std::array<double, 3>, 4> nodal_moments;
};
/// @brief Stores physical shell recovery at one midsurface Gauss location.
struct Mitc4PhysicalRecoveryPoint {
std::array<double, 2> natural_coordinates;
@@ -187,6 +195,11 @@ class Mitc4Shell final : public Element {
/// @return Finite stiffness matrices or a structured model failure.
[[nodiscard]] Result<Mitc4Stiffness> Stiffness() const;
/// @brief Computes consistent and scalar-row-sum lumped MITC4 mass.
/// @param density Constant material density in mass per volume.
/// @return Finite mass matrices or a structured model failure.
[[nodiscard]] Result<Mitc4Mass> Mass(double density) const;
/// @brief Recovers physical shell quantities without drilling results.
/// @param global_element_displacement24 Global element DOFs in node order.
/// @return Physical recovery rows or a structured model failure.
@@ -0,0 +1,24 @@
#ifndef FESA_IO_HDF5_EIGEN_HDF5_RESULTS_WRITER_H_
#define FESA_IO_HDF5_EIGEN_HDF5_RESULTS_WRITER_H_
#include <filesystem>
#include <vector>
#include "fesa/results/eigen_results_writer.h"
namespace fesa {
/// @brief Atomically writes validated linear-frequency HDF5 results.
class EigenHdf5ResultsWriter final : public EigenResultsWriter {
public:
/// @brief Serializes an accepted modal state after closed-file self-check.
/// @return An output failure when the candidate cannot be validated or
/// finalized; an existing final file is then preserved.
Status Write(const std::filesystem::path& output_path, const Domain& domain,
const EigenAnalysisState& state,
const std::vector<Diagnostic>& diagnostics) override;
};
} // namespace fesa
#endif // FESA_IO_HDF5_EIGEN_HDF5_RESULTS_WRITER_H_
+19 -2
View File
@@ -4,6 +4,7 @@
#include <cstddef>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@@ -47,7 +48,7 @@ class DomainCollectionView {
std::vector<const T*> entries_;
};
/// @brief Owns one immutable static-step semantic definition.
/// @brief Owns one immutable static or frequency-step semantic definition.
/// @note Loads retain source order and are owned polymorphically by unique
/// pointers.
class StepDefinition {
@@ -84,6 +85,12 @@ class StepDefinition {
/// @brief Reports whether this step uses the nonlinear static procedure.
bool IsNonlinearStatic() const noexcept;
/// @brief Reports whether this step defines the frequency procedure.
bool IsFrequency() const noexcept;
/// @brief Returns frequency source semantics when this is a frequency step.
const FrequencyStepDefinition* Frequency() const noexcept;
/// @brief Reports the source NLGEOM setting for this static step.
bool UsesNonlinearGeometry() const noexcept;
@@ -108,6 +115,9 @@ class StepDefinition {
/// @brief Converts one parsed static-step record to owned semantic objects.
explicit StepDefinition(StaticStepDefinition definition);
/// @brief Converts one parsed frequency-step record to owned semantics.
explicit StepDefinition(FrequencyStepDefinition definition);
std::string name_;
std::vector<std::unique_ptr<BoundaryCondition>> boundary_conditions_;
BoundaryConditionView boundary_conditions_view_;
@@ -124,6 +134,7 @@ class StepDefinition {
double minimum_increment_;
double maximum_increment_;
SourceLocation location_;
std::optional<FrequencyStepDefinition> frequency_;
};
/// @brief Owns the complete immutable semantic model definition.
@@ -184,9 +195,15 @@ class Domain {
/// @brief Returns assembly element surfaces in stable declaration order.
const std::vector<ElementSurfaceDefinition>& ElementSurfaces() const noexcept;
/// @brief Returns static steps in stable declaration order.
/// @brief Returns procedure steps in stable declaration order.
const DomainCollectionView<StepDefinition>& Steps() const noexcept;
/// @brief Returns resolved density records in stable material-index order.
const std::vector<MaterialDensityDefinition>& Densities() const noexcept;
/// @brief Returns the sole frequency source definition when present.
const FrequencyStepDefinition* Frequency() const noexcept;
/// @brief Returns sorted nonfatal mapping diagnostics.
const std::vector<Diagnostic>& Warnings() const noexcept;
+60 -34
View File
@@ -117,18 +117,15 @@ struct StaticStepDefinition {
std::vector<PrescribedDisplacementDefinition> step_boundaries,
std::vector<NodalLoad> step_loads,
std::vector<SurfacePressureLoadDefinition> step_surface_pressure_loads,
StaticProcedureKind step_procedure_kind,
bool step_nlgeom,
double step_initial_increment,
double step_time_period,
double step_minimum_increment,
double step_maximum_increment,
StaticProcedureKind step_procedure_kind, bool step_nlgeom,
double step_initial_increment, double step_time_period,
double step_minimum_increment, double step_maximum_increment,
SourceLocation step_location)
: name{std::move(step_name)},
boundaries{std::move(step_boundaries)},
loads{std::move(step_loads)},
surface_pressure_loads{std::move(step_surface_pressure_loads)},
procedure_kind{step_procedure_kind},
: name{std::move(step_name)},
boundaries{std::move(step_boundaries)},
loads{std::move(step_loads)},
surface_pressure_loads{std::move(step_surface_pressure_loads)},
procedure_kind{step_procedure_kind},
nlgeom{step_nlgeom},
initial_increment{step_initial_increment},
time_period{step_time_period},
@@ -141,35 +138,30 @@ struct StaticStepDefinition {
std::string step_name,
std::vector<PrescribedDisplacementDefinition> step_boundaries,
std::vector<NodalLoad> step_loads,
StaticProcedureKind step_procedure_kind,
bool step_nlgeom,
double step_initial_increment,
double step_time_period,
double step_minimum_increment,
double step_maximum_increment,
StaticProcedureKind step_procedure_kind, bool step_nlgeom,
double step_initial_increment, double step_time_period,
double step_minimum_increment, double step_maximum_increment,
SourceLocation step_location)
: StaticStepDefinition(
std::move(step_name), std::move(step_boundaries),
std::move(step_loads), {}, step_procedure_kind, step_nlgeom,
step_initial_increment, step_time_period, step_minimum_increment,
step_maximum_increment, std::move(step_location)) {}
: StaticStepDefinition(std::move(step_name), std::move(step_boundaries),
std::move(step_loads), {}, step_procedure_kind,
step_nlgeom, step_initial_increment,
step_time_period, step_minimum_increment,
step_maximum_increment, std::move(step_location)) {
}
/// @brief Creates a legacy linear static step definition.
StaticStepDefinition(
std::string step_name,
std::vector<PrescribedDisplacementDefinition> step_boundaries,
std::vector<NodalLoad> step_loads,
double step_initial_increment,
double step_time_period,
double step_minimum_increment,
double step_maximum_increment,
SourceLocation step_location)
: StaticStepDefinition(
std::move(step_name), std::move(step_boundaries),
std::move(step_loads),
StaticProcedureKind::kLinearSmallDisplacement, false,
step_initial_increment, step_time_period, step_minimum_increment,
step_maximum_increment, std::move(step_location)) {}
std::vector<NodalLoad> step_loads, double step_initial_increment,
double step_time_period, double step_minimum_increment,
double step_maximum_increment, SourceLocation step_location)
: StaticStepDefinition(std::move(step_name), std::move(step_boundaries),
std::move(step_loads),
StaticProcedureKind::kLinearSmallDisplacement,
false, step_initial_increment, step_time_period,
step_minimum_increment, step_maximum_increment,
std::move(step_location)) {}
std::string name;
std::vector<PrescribedDisplacementDefinition> boundaries;
@@ -184,6 +176,38 @@ struct StaticStepDefinition {
SourceLocation location;
};
/// @brief Identifies the accepted source eigensolver selection.
enum class FrequencyEigensolver { kLanczos };
/// @brief Identifies the accepted modal normalization selection.
enum class FrequencyNormalization { kMass };
/// @brief Stores one resolved constant material mass density.
struct MaterialDensityDefinition {
EntityIndex material_index;
double rho;
SourceLocation location;
};
/// @brief Stores the sole supported frequency-step source semantics.
struct FrequencyStepDefinition {
std::string source_step_name_raw;
std::string source_step_name_canonical;
SourceLocation step_location;
SourceLocation frequency_location;
SourceLocation data_location;
std::int64_t requested_mode_count;
bool nlgeom_explicit;
bool perturbation_explicit;
bool eigensolver_explicit;
bool sim_explicit;
bool acoustic_coupling_explicit;
bool normalization_explicit;
FrequencyEigensolver eigensolver;
FrequencyNormalization normalization;
std::vector<PrescribedDisplacementDefinition> boundaries;
};
/// @brief Stores a stable resolved node-set membership list.
struct NodeSet {
std::string name;
@@ -245,6 +269,8 @@ struct ModelDefinition {
std::vector<InstanceDefinition> instances;
std::vector<ElementSurfaceDefinition> element_surfaces;
std::vector<StaticStepDefinition> steps;
std::vector<MaterialDensityDefinition> densities;
std::optional<FrequencyStepDefinition> frequency_step;
std::vector<Diagnostic> warnings;
};
@@ -0,0 +1,29 @@
#ifndef FESA_RESULTS_EIGEN_RESULTS_WRITER_H_
#define FESA_RESULTS_EIGEN_RESULTS_WRITER_H_
#include <filesystem>
#include <vector>
#include "fesa/analysis/eigen_analysis_state.h"
#include "fesa/core/diagnostic.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
/// @brief Isolates authoritative modal storage from procedure execution.
class EigenResultsWriter {
public:
virtual ~EigenResultsWriter() = default;
/// @brief Validates and atomically publishes the complete modal candidate.
/// @return Success only after closed-file validation and final replacement.
virtual Status Write(const std::filesystem::path& output_path,
const Domain& domain, const EigenAnalysisState& state,
const std::vector<Diagnostic>& diagnostics) = 0;
};
} // namespace fesa
#endif // FESA_RESULTS_EIGEN_RESULTS_WRITER_H_
@@ -0,0 +1,83 @@
#ifndef FESA_SOLVERS_EIGEN_MKL_FEAST_EIGEN_SOLVER_H_
#define FESA_SOLVERS_EIGEN_MKL_FEAST_EIGEN_SOLVER_H_
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "fesa/core/status.h"
#include "fesa/math/matrix.h"
namespace fesa {
class SparseMatrix;
/// @brief Records one deterministic private-backend invocation.
struct FeastBackendCall {
std::size_t ordinal;
std::string kind;
std::string purpose;
double lower;
double upper;
std::size_t allocated_m0;
int info;
bool count_available;
std::size_t count;
bool loop_available;
int loop;
bool epsout_available;
double epsout;
std::string outcome;
std::string decision_reason;
};
/// @brief Owns the complete validated FEAST interval result before public N
/// selection.
struct FeastEigenResult {
std::size_t requested_modes;
std::vector<double> eigenvalues;
Matrix eigenvectors;
double operator_scale;
std::vector<std::size_t> column_ids;
std::vector<bool> backend_res_available;
std::vector<double> backend_res;
std::vector<double> normalized_eigenvalues;
std::vector<double> whitened_residuals;
double gram_error;
std::vector<FeastBackendCall> backend_calls;
std::vector<std::int64_t> base_fpm;
std::string runtime_version;
std::string integer_abi;
std::size_t thread_count;
std::uint64_t seed;
double enclosure_lower;
double enclosure_upper;
double solve_lower;
double solve_upper;
std::string upper_search_termination;
};
/// @brief Solves a whitened real-symmetric eigenproblem with oneMKL FEAST.
class MklFeastEigenSolver final {
public:
/// @brief Computes all validated interval pairs for a requested lowest-mode
/// prefix.
/// @param whitened_operator Symmetric physical-lambda operator after
/// whitening.
/// @param requested_modes Number of lowest modes eventually exposed by the
/// caller.
/// @param zero_operator_scale Positive physical scale used only when A is
/// exactly zero.
/// @return Sorted all-c eigenpairs in physical lambda units or a solver
/// failure.
/// @note The implementation fixes oneMKL local thread count to one during
/// every call.
Result<FeastEigenResult> Solve(const SparseMatrix& whitened_operator,
std::size_t requested_modes,
double zero_operator_scale) const;
};
} // namespace fesa
#endif // FESA_SOLVERS_EIGEN_MKL_FEAST_EIGEN_SOLVER_H_