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:
@@ -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_
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "fesa/analysis/analysis_state.h"
|
#include "fesa/analysis/analysis_state.h"
|
||||||
|
#include "fesa/analysis/eigen_analysis_state.h"
|
||||||
#include "fesa/analysis/nonlinear_analysis_state.h"
|
#include "fesa/analysis/nonlinear_analysis_state.h"
|
||||||
#include "fesa/core/diagnostic.h"
|
#include "fesa/core/diagnostic.h"
|
||||||
#include "fesa/core/status.h"
|
#include "fesa/core/status.h"
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
class LinearStaticProcedureExecutor;
|
class LinearStaticProcedureExecutor;
|
||||||
|
class LinearFrequencyProcedureExecutor;
|
||||||
|
class EigenResultsWriter;
|
||||||
class NewtonIterationLogWriter;
|
class NewtonIterationLogWriter;
|
||||||
class NonlinearResultsWriter;
|
class NonlinearResultsWriter;
|
||||||
class NonlinearStaticProcedureExecutor;
|
class NonlinearStaticProcedureExecutor;
|
||||||
@@ -47,7 +50,8 @@ struct NonlinearStaticExecutionResult {
|
|||||||
};
|
};
|
||||||
|
|
||||||
using PreparedAnalysisOutcome =
|
using PreparedAnalysisOutcome =
|
||||||
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult>;
|
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
|
||||||
|
EigenAnalysisState>;
|
||||||
|
|
||||||
/// @brief Selects the approved procedure from a prepared Domain.
|
/// @brief Selects the approved procedure from a prepared Domain.
|
||||||
/// @pre The executors and log outlive this call.
|
/// @pre The executors and log outlive this call.
|
||||||
@@ -55,6 +59,7 @@ using PreparedAnalysisOutcome =
|
|||||||
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
||||||
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
||||||
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
||||||
|
const LinearFrequencyProcedureExecutor& frequency_executor,
|
||||||
NewtonIterationLogWriter& log);
|
NewtonIterationLogWriter& log);
|
||||||
|
|
||||||
/// @brief Writes an already computed outcome to its authoritative result file.
|
/// @brief Writes an already computed outcome to its authoritative result file.
|
||||||
@@ -64,7 +69,7 @@ Status FinalizePreparedAnalysisResult(
|
|||||||
const DerivedOutputPaths& paths, const Domain& domain,
|
const DerivedOutputPaths& paths, const Domain& domain,
|
||||||
const PreparedAnalysisOutcome& outcome,
|
const PreparedAnalysisOutcome& outcome,
|
||||||
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
||||||
NonlinearResultsWriter& nonlinear_writer);
|
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer);
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ struct Mitc4Stiffness {
|
|||||||
double drilling_stiffness;
|
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.
|
/// @brief Stores physical shell recovery at one midsurface Gauss location.
|
||||||
struct Mitc4PhysicalRecoveryPoint {
|
struct Mitc4PhysicalRecoveryPoint {
|
||||||
std::array<double, 2> natural_coordinates;
|
std::array<double, 2> natural_coordinates;
|
||||||
@@ -187,6 +195,11 @@ class Mitc4Shell final : public Element {
|
|||||||
/// @return Finite stiffness matrices or a structured model failure.
|
/// @return Finite stiffness matrices or a structured model failure.
|
||||||
[[nodiscard]] Result<Mitc4Stiffness> Stiffness() const;
|
[[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.
|
/// @brief Recovers physical shell quantities without drilling results.
|
||||||
/// @param global_element_displacement24 Global element DOFs in node order.
|
/// @param global_element_displacement24 Global element DOFs in node order.
|
||||||
/// @return Physical recovery rows or a structured model failure.
|
/// @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_
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ class DomainCollectionView {
|
|||||||
std::vector<const T*> entries_;
|
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
|
/// @note Loads retain source order and are owned polymorphically by unique
|
||||||
/// pointers.
|
/// pointers.
|
||||||
class StepDefinition {
|
class StepDefinition {
|
||||||
@@ -84,6 +85,12 @@ class StepDefinition {
|
|||||||
/// @brief Reports whether this step uses the nonlinear static procedure.
|
/// @brief Reports whether this step uses the nonlinear static procedure.
|
||||||
bool IsNonlinearStatic() const noexcept;
|
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.
|
/// @brief Reports the source NLGEOM setting for this static step.
|
||||||
bool UsesNonlinearGeometry() const noexcept;
|
bool UsesNonlinearGeometry() const noexcept;
|
||||||
|
|
||||||
@@ -108,6 +115,9 @@ class StepDefinition {
|
|||||||
/// @brief Converts one parsed static-step record to owned semantic objects.
|
/// @brief Converts one parsed static-step record to owned semantic objects.
|
||||||
explicit StepDefinition(StaticStepDefinition definition);
|
explicit StepDefinition(StaticStepDefinition definition);
|
||||||
|
|
||||||
|
/// @brief Converts one parsed frequency-step record to owned semantics.
|
||||||
|
explicit StepDefinition(FrequencyStepDefinition definition);
|
||||||
|
|
||||||
std::string name_;
|
std::string name_;
|
||||||
std::vector<std::unique_ptr<BoundaryCondition>> boundary_conditions_;
|
std::vector<std::unique_ptr<BoundaryCondition>> boundary_conditions_;
|
||||||
BoundaryConditionView boundary_conditions_view_;
|
BoundaryConditionView boundary_conditions_view_;
|
||||||
@@ -124,6 +134,7 @@ class StepDefinition {
|
|||||||
double minimum_increment_;
|
double minimum_increment_;
|
||||||
double maximum_increment_;
|
double maximum_increment_;
|
||||||
SourceLocation location_;
|
SourceLocation location_;
|
||||||
|
std::optional<FrequencyStepDefinition> frequency_;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @brief Owns the complete immutable semantic model definition.
|
/// @brief Owns the complete immutable semantic model definition.
|
||||||
@@ -184,9 +195,15 @@ class Domain {
|
|||||||
/// @brief Returns assembly element surfaces in stable declaration order.
|
/// @brief Returns assembly element surfaces in stable declaration order.
|
||||||
const std::vector<ElementSurfaceDefinition>& ElementSurfaces() const noexcept;
|
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;
|
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.
|
/// @brief Returns sorted nonfatal mapping diagnostics.
|
||||||
const std::vector<Diagnostic>& Warnings() const noexcept;
|
const std::vector<Diagnostic>& Warnings() const noexcept;
|
||||||
|
|
||||||
|
|||||||
@@ -117,12 +117,9 @@ struct StaticStepDefinition {
|
|||||||
std::vector<PrescribedDisplacementDefinition> step_boundaries,
|
std::vector<PrescribedDisplacementDefinition> step_boundaries,
|
||||||
std::vector<NodalLoad> step_loads,
|
std::vector<NodalLoad> step_loads,
|
||||||
std::vector<SurfacePressureLoadDefinition> step_surface_pressure_loads,
|
std::vector<SurfacePressureLoadDefinition> step_surface_pressure_loads,
|
||||||
StaticProcedureKind step_procedure_kind,
|
StaticProcedureKind step_procedure_kind, bool step_nlgeom,
|
||||||
bool step_nlgeom,
|
double step_initial_increment, double step_time_period,
|
||||||
double step_initial_increment,
|
double step_minimum_increment, double step_maximum_increment,
|
||||||
double step_time_period,
|
|
||||||
double step_minimum_increment,
|
|
||||||
double step_maximum_increment,
|
|
||||||
SourceLocation step_location)
|
SourceLocation step_location)
|
||||||
: name{std::move(step_name)},
|
: name{std::move(step_name)},
|
||||||
boundaries{std::move(step_boundaries)},
|
boundaries{std::move(step_boundaries)},
|
||||||
@@ -141,35 +138,30 @@ struct StaticStepDefinition {
|
|||||||
std::string step_name,
|
std::string step_name,
|
||||||
std::vector<PrescribedDisplacementDefinition> step_boundaries,
|
std::vector<PrescribedDisplacementDefinition> step_boundaries,
|
||||||
std::vector<NodalLoad> step_loads,
|
std::vector<NodalLoad> step_loads,
|
||||||
StaticProcedureKind step_procedure_kind,
|
StaticProcedureKind step_procedure_kind, bool step_nlgeom,
|
||||||
bool step_nlgeom,
|
double step_initial_increment, double step_time_period,
|
||||||
double step_initial_increment,
|
double step_minimum_increment, double step_maximum_increment,
|
||||||
double step_time_period,
|
|
||||||
double step_minimum_increment,
|
|
||||||
double step_maximum_increment,
|
|
||||||
SourceLocation step_location)
|
SourceLocation step_location)
|
||||||
: StaticStepDefinition(
|
: StaticStepDefinition(std::move(step_name), std::move(step_boundaries),
|
||||||
std::move(step_name), std::move(step_boundaries),
|
std::move(step_loads), {}, step_procedure_kind,
|
||||||
std::move(step_loads), {}, step_procedure_kind, step_nlgeom,
|
step_nlgeom, step_initial_increment,
|
||||||
step_initial_increment, step_time_period, step_minimum_increment,
|
step_time_period, step_minimum_increment,
|
||||||
step_maximum_increment, std::move(step_location)) {}
|
step_maximum_increment, std::move(step_location)) {
|
||||||
|
}
|
||||||
|
|
||||||
/// @brief Creates a legacy linear static step definition.
|
/// @brief Creates a legacy linear static step definition.
|
||||||
StaticStepDefinition(
|
StaticStepDefinition(
|
||||||
std::string step_name,
|
std::string step_name,
|
||||||
std::vector<PrescribedDisplacementDefinition> step_boundaries,
|
std::vector<PrescribedDisplacementDefinition> step_boundaries,
|
||||||
std::vector<NodalLoad> step_loads,
|
std::vector<NodalLoad> step_loads, double step_initial_increment,
|
||||||
double step_initial_increment,
|
double step_time_period, double step_minimum_increment,
|
||||||
double step_time_period,
|
double step_maximum_increment, SourceLocation step_location)
|
||||||
double step_minimum_increment,
|
: StaticStepDefinition(std::move(step_name), std::move(step_boundaries),
|
||||||
double step_maximum_increment,
|
|
||||||
SourceLocation step_location)
|
|
||||||
: StaticStepDefinition(
|
|
||||||
std::move(step_name), std::move(step_boundaries),
|
|
||||||
std::move(step_loads),
|
std::move(step_loads),
|
||||||
StaticProcedureKind::kLinearSmallDisplacement, false,
|
StaticProcedureKind::kLinearSmallDisplacement,
|
||||||
step_initial_increment, step_time_period, step_minimum_increment,
|
false, step_initial_increment, step_time_period,
|
||||||
step_maximum_increment, std::move(step_location)) {}
|
step_minimum_increment, step_maximum_increment,
|
||||||
|
std::move(step_location)) {}
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
std::vector<PrescribedDisplacementDefinition> boundaries;
|
std::vector<PrescribedDisplacementDefinition> boundaries;
|
||||||
@@ -184,6 +176,38 @@ struct StaticStepDefinition {
|
|||||||
SourceLocation location;
|
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.
|
/// @brief Stores a stable resolved node-set membership list.
|
||||||
struct NodeSet {
|
struct NodeSet {
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -245,6 +269,8 @@ struct ModelDefinition {
|
|||||||
std::vector<InstanceDefinition> instances;
|
std::vector<InstanceDefinition> instances;
|
||||||
std::vector<ElementSurfaceDefinition> element_surfaces;
|
std::vector<ElementSurfaceDefinition> element_surfaces;
|
||||||
std::vector<StaticStepDefinition> steps;
|
std::vector<StaticStepDefinition> steps;
|
||||||
|
std::vector<MaterialDensityDefinition> densities;
|
||||||
|
std::optional<FrequencyStepDefinition> frequency_step;
|
||||||
std::vector<Diagnostic> warnings;
|
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_
|
||||||
@@ -3,6 +3,8 @@ add_library(
|
|||||||
STATIC
|
STATIC
|
||||||
analysis/analysis_model.cpp
|
analysis/analysis_model.cpp
|
||||||
analysis/analysis_state.cpp
|
analysis/analysis_state.cpp
|
||||||
|
analysis/eigen_reduction.cpp
|
||||||
|
analysis/linear_frequency_procedure.cpp
|
||||||
analysis/linear_static_analysis.cpp
|
analysis/linear_static_analysis.cpp
|
||||||
analysis/linear_static_procedure.cpp
|
analysis/linear_static_procedure.cpp
|
||||||
analysis/newton_iteration_log.cpp
|
analysis/newton_iteration_log.cpp
|
||||||
@@ -42,6 +44,8 @@ add_library(
|
|||||||
io/hdf5/hdf5_primitives.cpp
|
io/hdf5/hdf5_primitives.cpp
|
||||||
io/hdf5/hdf5_result_writer.cpp
|
io/hdf5/hdf5_result_writer.cpp
|
||||||
io/hdf5/hdf5_results_writer.cpp
|
io/hdf5/hdf5_results_writer.cpp
|
||||||
|
io/hdf5/eigen_hdf5_results_writer.cpp
|
||||||
|
io/hdf5/eigen_hdf5_self_check.cpp
|
||||||
io/hdf5/hdf5_self_check.cpp
|
io/hdf5/hdf5_self_check.cpp
|
||||||
loads/concentrated_nodal_load.cpp
|
loads/concentrated_nodal_load.cpp
|
||||||
loads/shell_surface_pressure.cpp
|
loads/shell_surface_pressure.cpp
|
||||||
@@ -61,6 +65,7 @@ add_library(
|
|||||||
results/recovery_candidate.cpp
|
results/recovery_candidate.cpp
|
||||||
results/result_recovery.cpp
|
results/result_recovery.cpp
|
||||||
results/shell_result_recovery.cpp
|
results/shell_result_recovery.cpp
|
||||||
|
solvers/eigen/mkl_feast_eigen_solver.cpp
|
||||||
solvers/linear/mkl_pardiso_solver.cpp
|
solvers/linear/mkl_pardiso_solver.cpp
|
||||||
solvers/linear/mkl_pardiso_tangent_solver.cpp
|
solvers/linear/mkl_pardiso_tangent_solver.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
#include "analysis/eigen_reduction.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
|
||||||
|
namespace fesa::eigen_internal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr double kAlgebraLimit = 1e-12;
|
||||||
|
constexpr double kConditionLimit = 0x1p-32;
|
||||||
|
|
||||||
|
Status Failure(const std::string& code, const std::size_t node,
|
||||||
|
const std::string& message) {
|
||||||
|
return Status::Failure(FailureCategory::kSolver, {{Severity::kError,
|
||||||
|
code,
|
||||||
|
{},
|
||||||
|
"FREQUENCY",
|
||||||
|
std::to_string(node),
|
||||||
|
message}});
|
||||||
|
}
|
||||||
|
|
||||||
|
double Norm(const Matrix& matrix) {
|
||||||
|
double norm = 0.0;
|
||||||
|
for (std::size_t i = 0; i < matrix.Rows(); ++i) {
|
||||||
|
for (std::size_t j = 0; j < matrix.Columns(); ++j) {
|
||||||
|
norm = std::hypot(norm, matrix(i, j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Ratio(const double numerator, const double denominator) {
|
||||||
|
if (denominator > 0.0 && std::isfinite(denominator)) {
|
||||||
|
return numerator / denominator;
|
||||||
|
}
|
||||||
|
return numerator == 0.0 && denominator == 0.0
|
||||||
|
? 0.0
|
||||||
|
: (std::numeric_limits<double>::infinity)();
|
||||||
|
}
|
||||||
|
|
||||||
|
double Scale(const std::size_t component, const double length) {
|
||||||
|
return component < 3U ? length : 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix ScaledMass(const Matrix& mass, const double length) {
|
||||||
|
Matrix scaled(6U, 6U);
|
||||||
|
for (std::size_t i = 0; i < 6U; ++i) {
|
||||||
|
for (std::size_t j = 0; j < 6U; ++j) {
|
||||||
|
scaled(i, j) = Scale(i, length) * mass(i, j) * Scale(j, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<NodalLift> MakeLift(const Matrix& mass,
|
||||||
|
const ShellNodeInitialFrame& frame,
|
||||||
|
const std::vector<std::uint8_t>& mask,
|
||||||
|
const double length, const std::size_t offset) {
|
||||||
|
const std::size_t node = frame.node_index;
|
||||||
|
std::vector<std::size_t> translations;
|
||||||
|
std::vector<std::size_t> rotations;
|
||||||
|
bool has_null = true;
|
||||||
|
for (std::size_t c = 0; c < 6U; ++c) {
|
||||||
|
if (mask[6U * node + c] == 0U) {
|
||||||
|
(c < 3U ? translations : rotations).push_back(c);
|
||||||
|
} else if (c >= 3U && frame.director[c - 3U] != 0.0) {
|
||||||
|
has_null = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
has_null = has_null && !rotations.empty();
|
||||||
|
const std::size_t free = translations.size() + rotations.size();
|
||||||
|
const std::size_t positive = free - static_cast<std::size_t>(has_null);
|
||||||
|
NodalLift result;
|
||||||
|
result.offset = offset;
|
||||||
|
result.record = {static_cast<std::uint8_t>(free),
|
||||||
|
static_cast<std::uint8_t>(has_null),
|
||||||
|
static_cast<std::uint8_t>(positive), positive != 0U, 0.0};
|
||||||
|
result.positive_basis = Matrix(6U, positive);
|
||||||
|
auto& basis = result.positive_basis;
|
||||||
|
std::size_t column = 0U;
|
||||||
|
for (const auto c : translations) basis(c, column++) = 1.0;
|
||||||
|
if (!has_null) {
|
||||||
|
for (const auto c : rotations) basis(c, column++) = 1.0;
|
||||||
|
} else {
|
||||||
|
for (std::size_t c = 0; c < 3U; ++c) {
|
||||||
|
result.null_vector[c + 3U] = frame.director[c];
|
||||||
|
}
|
||||||
|
if (rotations.size() == 3U) {
|
||||||
|
for (std::size_t c = 0; c < 3U; ++c) {
|
||||||
|
basis(c + 3U, column) = frame.tangent_a[c];
|
||||||
|
basis(c + 3U, column + 1U) = frame.tangent_b[c];
|
||||||
|
}
|
||||||
|
} else if (rotations.size() == 2U) {
|
||||||
|
const auto a = rotations[0];
|
||||||
|
const auto b = rotations[1];
|
||||||
|
const double norm =
|
||||||
|
std::hypot(frame.director[a - 3U], frame.director[b - 3U]);
|
||||||
|
basis(a, column) = -frame.director[b - 3U] / norm;
|
||||||
|
basis(b, column) = frame.director[a - 3U] / norm;
|
||||||
|
const auto pivot =
|
||||||
|
std::abs(basis(a, column)) >= std::abs(basis(b, column)) ? a : b;
|
||||||
|
if (basis(pivot, column) < 0.0) {
|
||||||
|
basis(a, column) = -basis(a, column);
|
||||||
|
basis(b, column) = -basis(b, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.scaled_lift = Matrix(6U, positive);
|
||||||
|
if (positive == 0U) return Result<NodalLift>::Success(std::move(result));
|
||||||
|
|
||||||
|
const Matrix scaled = ScaledMass(mass, length);
|
||||||
|
Matrix mp(positive, positive);
|
||||||
|
double scale = 0.0;
|
||||||
|
double basis_error = 0.0;
|
||||||
|
for (std::size_t i = 0; i < positive; ++i) {
|
||||||
|
for (std::size_t j = 0; j < positive; ++j) {
|
||||||
|
double gram = 0.0;
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a) {
|
||||||
|
gram += basis(a, i) * basis(a, j);
|
||||||
|
for (std::size_t b = 0; b < 6U; ++b) {
|
||||||
|
mp(i, j) += basis(a, i) * scaled(a, b) * basis(b, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
basis_error = std::hypot(basis_error, gram - (i == j ? 1.0 : 0.0));
|
||||||
|
scale = (std::max)(scale, std::abs(mp(i, j)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(basis_error <= kAlgebraLimit) || !(scale > 0.0) ||
|
||||||
|
!std::isfinite(scale)) {
|
||||||
|
return Result<NodalLift>::Failure(Failure("invalid-eigen-mass-space", node,
|
||||||
|
"Invalid positive mass basis."));
|
||||||
|
}
|
||||||
|
Matrix lower(positive, positive);
|
||||||
|
for (std::size_t i = 0; i < positive; ++i) {
|
||||||
|
for (std::size_t j = 0; j <= i; ++j) {
|
||||||
|
double value = mp(i, j) / scale;
|
||||||
|
for (std::size_t k = 0; k < j; ++k) value -= lower(i, k) * lower(j, k);
|
||||||
|
if (i == j) {
|
||||||
|
if (!(value > 0.0) || !std::isfinite(value)) {
|
||||||
|
return Result<NodalLift>::Failure(
|
||||||
|
Failure("eigen-mass-conditioning", node,
|
||||||
|
"A positive-space mass pivot is not resolvable."));
|
||||||
|
}
|
||||||
|
lower(i, j) = std::sqrt(value);
|
||||||
|
} else {
|
||||||
|
lower(i, j) = value / lower(j, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Matrix inverse_lower(positive, positive);
|
||||||
|
for (std::size_t j = 0; j < positive; ++j) {
|
||||||
|
for (std::size_t i = 0; i < positive; ++i) {
|
||||||
|
double value = i == j ? 1.0 : 0.0;
|
||||||
|
for (std::size_t k = 0; k < i; ++k)
|
||||||
|
value -= lower(i, k) * inverse_lower(k, j);
|
||||||
|
inverse_lower(i, j) = value / lower(i, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double norm_mp = 0.0;
|
||||||
|
double norm_inverse = 0.0;
|
||||||
|
double reconstruction = 0.0;
|
||||||
|
double scaled_mass_norm = 0.0;
|
||||||
|
double rebuilt_mass_norm = 0.0;
|
||||||
|
for (std::size_t i = 0; i < positive; ++i) {
|
||||||
|
double row_mp = 0.0;
|
||||||
|
double row_inverse = 0.0;
|
||||||
|
for (std::size_t j = 0; j < positive; ++j) {
|
||||||
|
double inverse = 0.0;
|
||||||
|
double rebuilt = 0.0;
|
||||||
|
for (std::size_t k = 0; k < positive; ++k) {
|
||||||
|
inverse += inverse_lower(k, i) * inverse_lower(k, j);
|
||||||
|
rebuilt += lower(i, k) * lower(j, k);
|
||||||
|
}
|
||||||
|
row_inverse += std::abs(inverse);
|
||||||
|
row_mp += std::abs(mp(i, j) / scale);
|
||||||
|
reconstruction = std::hypot(reconstruction, rebuilt - mp(i, j) / scale);
|
||||||
|
scaled_mass_norm = std::hypot(scaled_mass_norm, mp(i, j) / scale);
|
||||||
|
rebuilt_mass_norm = std::hypot(rebuilt_mass_norm, rebuilt);
|
||||||
|
}
|
||||||
|
norm_mp = (std::max)(norm_mp, row_mp);
|
||||||
|
norm_inverse = (std::max)(norm_inverse, row_inverse);
|
||||||
|
}
|
||||||
|
result.record.rcond_inf = (1.0 / norm_mp) / norm_inverse;
|
||||||
|
if (!(result.record.rcond_inf >= kConditionLimit) ||
|
||||||
|
!(Ratio(reconstruction, scaled_mass_norm + rebuilt_mass_norm) <=
|
||||||
|
kAlgebraLimit)) {
|
||||||
|
return Result<NodalLift>::Failure(Failure(
|
||||||
|
"eigen-mass-conditioning", node,
|
||||||
|
"Nodal mass fails rcond_inf >= 2^-32 or Cholesky reconstruction."));
|
||||||
|
}
|
||||||
|
const double root_scale = std::sqrt(scale);
|
||||||
|
for (std::size_t c = 0; c < 6U; ++c) {
|
||||||
|
for (std::size_t j = 0; j < positive; ++j) {
|
||||||
|
for (std::size_t k = 0; k < positive; ++k) {
|
||||||
|
result.scaled_lift(c, j) +=
|
||||||
|
basis(c, k) * inverse_lower(j, k) / root_scale;
|
||||||
|
}
|
||||||
|
if (!std::isfinite(result.scaled_lift(c, j))) {
|
||||||
|
return Result<NodalLift>::Failure(Failure(
|
||||||
|
"eigen-mass-conditioning", node, "Nodal whitening is nonfinite."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result<NodalLift>::Success(std::move(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result<EigenReduction> BuildEigenReduction(
|
||||||
|
const SparseMatrix& stiffness, const std::vector<Matrix>& mass,
|
||||||
|
const std::vector<ShellNodeInitialFrame>& frames,
|
||||||
|
const std::vector<std::uint8_t>& mask, const std::vector<double>& drilling,
|
||||||
|
const double length) {
|
||||||
|
const std::size_t count = mass.size();
|
||||||
|
const Status valid = stiffness.Validate();
|
||||||
|
if (!valid.IsOk()) return Result<EigenReduction>::Failure(valid);
|
||||||
|
if (count > (std::numeric_limits<std::size_t>::max)() / 6U ||
|
||||||
|
stiffness.Rows() != 6U * count || stiffness.Columns() != 6U * count ||
|
||||||
|
frames.size() != count || drilling.size() != count ||
|
||||||
|
mask.size() != 6U * count || !(length > 0.0) || !std::isfinite(length)) {
|
||||||
|
return Result<EigenReduction>::Failure(Failure(
|
||||||
|
"invalid-eigen-mass-space", 0U, "Incompatible mass-space inventory."));
|
||||||
|
}
|
||||||
|
std::vector<NodalLift> lifts;
|
||||||
|
std::size_t dimension = 0U;
|
||||||
|
std::size_t free_count = 0U;
|
||||||
|
std::size_t null_count = 0U;
|
||||||
|
double mass_norm = 0.0;
|
||||||
|
double mass_null_error = 0.0;
|
||||||
|
double null_norm = 0.0;
|
||||||
|
double expected_kz_norm = 0.0;
|
||||||
|
double pz_norm = 0.0;
|
||||||
|
for (std::size_t node = 0; node < count; ++node) {
|
||||||
|
if (mass[node].Rows() != 6U || mass[node].Columns() != 6U ||
|
||||||
|
frames[node].node_index != node || !(drilling[node] > 0.0) ||
|
||||||
|
!std::isfinite(drilling[node])) {
|
||||||
|
return Result<EigenReduction>::Failure(
|
||||||
|
Failure("invalid-eigen-mass-space", node,
|
||||||
|
"Invalid nodal mass or drill inventory."));
|
||||||
|
}
|
||||||
|
const auto scaled = ScaledMass(mass[node], length);
|
||||||
|
double symmetry = 0.0;
|
||||||
|
double director_norm = 0.0;
|
||||||
|
for (const auto value : frames[node].director)
|
||||||
|
director_norm = std::hypot(director_norm, value);
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a) {
|
||||||
|
if (mask[6U * node + a] > 1U) {
|
||||||
|
return Result<EigenReduction>::Failure(
|
||||||
|
Failure("invalid-eigen-mass-space", node,
|
||||||
|
"Constraint mask must be binary."));
|
||||||
|
}
|
||||||
|
for (std::size_t b = 0; b < 6U; ++b) {
|
||||||
|
if (!std::isfinite(scaled(a, b))) {
|
||||||
|
return Result<EigenReduction>::Failure(
|
||||||
|
Failure("invalid-eigen-mass-space", node, "Mass is nonfinite."));
|
||||||
|
}
|
||||||
|
symmetry = std::hypot(symmetry, scaled(a, b) - scaled(b, a));
|
||||||
|
if (mask[6U * node + a] == 0U && mask[6U * node + b] == 0U) {
|
||||||
|
mass_norm = std::hypot(mass_norm, scaled(a, b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(std::abs(director_norm - 1.0) <= kAlgebraLimit) ||
|
||||||
|
!(Ratio(symmetry, Norm(scaled)) <= kAlgebraLimit)) {
|
||||||
|
return Result<EigenReduction>::Failure(
|
||||||
|
Failure("invalid-eigen-mass-space", node,
|
||||||
|
"Invalid director or mass symmetry."));
|
||||||
|
}
|
||||||
|
auto lift = MakeLift(mass[node], frames[node], mask, length, dimension);
|
||||||
|
if (!lift.HasValue())
|
||||||
|
return Result<EigenReduction>::Failure(lift.GetStatus());
|
||||||
|
dimension += lift.Value().record.positive_count;
|
||||||
|
free_count += lift.Value().record.free_count;
|
||||||
|
null_count += lift.Value().record.null_count;
|
||||||
|
const auto& z = lift.Value().null_vector;
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a) {
|
||||||
|
null_norm = std::hypot(null_norm, z[a]);
|
||||||
|
expected_kz_norm = std::hypot(expected_kz_norm, drilling[node] * z[a]);
|
||||||
|
if (mask[6U * node + a] != 0U) continue;
|
||||||
|
double action = 0.0;
|
||||||
|
for (std::size_t b = 0; b < 6U; ++b) action += scaled(a, b) * z[b];
|
||||||
|
mass_null_error = std::hypot(mass_null_error, action);
|
||||||
|
}
|
||||||
|
for (std::size_t j = 0; j < lift.Value().positive_basis.Columns(); ++j) {
|
||||||
|
double dot = 0.0;
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a)
|
||||||
|
dot += lift.Value().positive_basis(a, j) * z[a];
|
||||||
|
pz_norm = std::hypot(pz_norm, dot);
|
||||||
|
}
|
||||||
|
lifts.push_back(std::move(lift.Value()));
|
||||||
|
}
|
||||||
|
if (dimension == 0U) {
|
||||||
|
return Result<EigenReduction>::Failure(Status::Failure(
|
||||||
|
FailureCategory::kModel, {{Severity::kError,
|
||||||
|
"empty-eigenproblem",
|
||||||
|
{},
|
||||||
|
"FREQUENCY",
|
||||||
|
{},
|
||||||
|
"No finite mass coordinates remain."}}));
|
||||||
|
}
|
||||||
|
double stiffness_norm = 0.0;
|
||||||
|
double kz_error = 0.0;
|
||||||
|
const auto& offsets = stiffness.RowOffsets();
|
||||||
|
const auto& columns = stiffness.ColumnIndices();
|
||||||
|
const auto& values = stiffness.Values();
|
||||||
|
SparsePattern pattern;
|
||||||
|
pattern.row_offsets.push_back(0U);
|
||||||
|
std::vector<CooContribution> contributions;
|
||||||
|
for (std::size_t node = 0; node < count; ++node) {
|
||||||
|
std::map<std::size_t, Matrix> blocks;
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a) {
|
||||||
|
const auto row = node * 6U + a;
|
||||||
|
if (mask[row] != 0U) continue;
|
||||||
|
std::map<std::size_t, double> null_action;
|
||||||
|
if (lifts[node].record.null_count != 0U) {
|
||||||
|
null_action[node] = -drilling[node] * lifts[node].null_vector[a];
|
||||||
|
}
|
||||||
|
for (std::size_t pos = offsets[row]; pos < offsets[row + 1U]; ++pos) {
|
||||||
|
const auto col = columns[pos];
|
||||||
|
if (mask[col] != 0U) continue;
|
||||||
|
const auto other = col / 6U;
|
||||||
|
const auto b = col % 6U;
|
||||||
|
const double value = Scale(a, length) * values[pos] * Scale(b, length);
|
||||||
|
if (!std::isfinite(value)) {
|
||||||
|
return Result<EigenReduction>::Failure(
|
||||||
|
Failure("invalid-eigen-mass-space", node,
|
||||||
|
"Scaled stiffness is nonfinite."));
|
||||||
|
}
|
||||||
|
stiffness_norm = std::hypot(stiffness_norm, value);
|
||||||
|
null_action[other] += value * lifts[other].null_vector[b];
|
||||||
|
auto block = blocks.try_emplace(other, 6U, 6U).first;
|
||||||
|
block->second(a, b) = value;
|
||||||
|
}
|
||||||
|
for (const auto& item : null_action)
|
||||||
|
kz_error = std::hypot(kz_error, item.second);
|
||||||
|
}
|
||||||
|
const auto& left = lifts[node];
|
||||||
|
for (std::size_t i = 0; i < left.scaled_lift.Columns(); ++i) {
|
||||||
|
for (const auto& entry : blocks) {
|
||||||
|
const auto& right = lifts[entry.first];
|
||||||
|
for (std::size_t j = 0; j < right.scaled_lift.Columns(); ++j) {
|
||||||
|
double value = 0.0;
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a) {
|
||||||
|
for (std::size_t b = 0; b < 6U; ++b) {
|
||||||
|
value += left.scaled_lift(a, i) * entry.second(a, b) *
|
||||||
|
right.scaled_lift(b, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pattern.column_indices.push_back(right.offset + j);
|
||||||
|
contributions.push_back({left.offset + i, right.offset + j, value,
|
||||||
|
node, pattern.column_indices.size() - 1U});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pattern.row_offsets.push_back(pattern.column_indices.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double eta_mz = Ratio(mass_null_error, mass_norm * null_norm);
|
||||||
|
const double eta_kz =
|
||||||
|
Ratio(kz_error, stiffness_norm * null_norm + expected_kz_norm);
|
||||||
|
if (!(eta_mz <= kAlgebraLimit) || !(eta_kz <= kAlgebraLimit) ||
|
||||||
|
!(pz_norm <= kAlgebraLimit)) {
|
||||||
|
return Result<EigenReduction>::Failure(Failure(
|
||||||
|
"invalid-eigen-mass-space", 0U,
|
||||||
|
"Original constrained MZ, KZ or PZ violates the analytic null split."));
|
||||||
|
}
|
||||||
|
auto reduced = SparseMatrix::FromCoo(dimension, dimension,
|
||||||
|
std::move(contributions), pattern);
|
||||||
|
if (!reduced.HasValue())
|
||||||
|
return Result<EigenReduction>::Failure(reduced.GetStatus());
|
||||||
|
return Result<EigenReduction>::Success(
|
||||||
|
{std::move(reduced.Value()), std::move(lifts), free_count, null_count,
|
||||||
|
length, eta_mz, eta_kz, pz_norm});
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector RestoreScaledMode(const EigenReduction& reduction, const Matrix& vectors,
|
||||||
|
const std::size_t column) {
|
||||||
|
Vector result(reduction.nodes.size() * 6U);
|
||||||
|
for (std::size_t node = 0; node < reduction.nodes.size(); ++node) {
|
||||||
|
const auto& lift = reduction.nodes[node];
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a) {
|
||||||
|
for (std::size_t j = 0; j < lift.scaled_lift.Columns(); ++j) {
|
||||||
|
result[6U * node + a] +=
|
||||||
|
lift.scaled_lift(a, j) * vectors(lift.offset + j, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector MultiplyNodalMass(const std::vector<Matrix>& mass,
|
||||||
|
const Vector& vector) {
|
||||||
|
Vector result(vector.Size());
|
||||||
|
for (std::size_t node = 0; node < mass.size(); ++node) {
|
||||||
|
for (std::size_t a = 0; a < 6U; ++a) {
|
||||||
|
for (std::size_t b = 0; b < 6U; ++b) {
|
||||||
|
result[6U * node + a] += mass[node](a, b) * vector[6U * node + b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::eigen_internal
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#ifndef FESA_ANALYSIS_EIGEN_REDUCTION_H_
|
||||||
|
#define FESA_ANALYSIS_EIGEN_REDUCTION_H_
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/eigen_analysis_state.h"
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "fesa/math/matrix.h"
|
||||||
|
#include "fesa/math/sparse_matrix.h"
|
||||||
|
#include "fesa/math/vector.h"
|
||||||
|
#include "fesa/model/model_types.h"
|
||||||
|
|
||||||
|
namespace fesa::eigen_internal {
|
||||||
|
|
||||||
|
/// @brief Stores one constraint-first nodal lift from whitened coordinates.
|
||||||
|
struct NodalLift {
|
||||||
|
Matrix positive_basis{6U, 0U};
|
||||||
|
Matrix scaled_lift{6U, 0U};
|
||||||
|
std::array<double, 6> null_vector{};
|
||||||
|
std::size_t offset{0U};
|
||||||
|
EigenNodalReductionRecord record{};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Preserves the original sparse pencil and its checked nodal reduction.
|
||||||
|
struct EigenReduction {
|
||||||
|
SparseMatrix whitened_operator;
|
||||||
|
std::vector<NodalLift> nodes;
|
||||||
|
std::size_t free_count;
|
||||||
|
std::size_t null_count;
|
||||||
|
double characteristic_length;
|
||||||
|
double eta_mz;
|
||||||
|
double eta_kz;
|
||||||
|
double eta_pz;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Applies actual component constraints before analytic null removal.
|
||||||
|
Result<EigenReduction> BuildEigenReduction(
|
||||||
|
const SparseMatrix& full_stiffness, const std::vector<Matrix>& nodal_mass,
|
||||||
|
const std::vector<ShellNodeInitialFrame>& frames,
|
||||||
|
const std::vector<std::uint8_t>& constraint_mask,
|
||||||
|
const std::vector<double>& nodal_drilling_stiffness,
|
||||||
|
double characteristic_length);
|
||||||
|
|
||||||
|
/// @brief Restores scaled global coordinates with constrained entries zero.
|
||||||
|
Vector RestoreScaledMode(const EigenReduction& reduction,
|
||||||
|
const Matrix& whitened_vectors, std::size_t column);
|
||||||
|
|
||||||
|
/// @brief Applies the assembled nodal mass in physical global coordinates.
|
||||||
|
Vector MultiplyNodalMass(const std::vector<Matrix>& nodal_mass,
|
||||||
|
const Vector& vector);
|
||||||
|
|
||||||
|
} // namespace fesa::eigen_internal
|
||||||
|
|
||||||
|
#endif // FESA_ANALYSIS_EIGEN_REDUCTION_H_
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <new>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "analysis/eigen_reduction.h"
|
||||||
|
#include "fesa/analysis/analysis_model.h"
|
||||||
|
#include "fesa/assembly/parallel_for.h"
|
||||||
|
#include "fesa/assembly/sparse_assembler.h"
|
||||||
|
#include "fesa/elements/element_factory.h"
|
||||||
|
#include "fesa/elements/mitc4_shell.h"
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/math/sparse_matrix.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Status Failure(const std::string& code, const std::string& identity,
|
||||||
|
const std::string& message) {
|
||||||
|
return Status::Failure(
|
||||||
|
FailureCategory::kSolver,
|
||||||
|
{{Severity::kError, code, {}, "FREQUENCY", identity, message}});
|
||||||
|
}
|
||||||
|
|
||||||
|
double Dot(const Vector& left, const Vector& right) {
|
||||||
|
double result = 0.0;
|
||||||
|
for (std::size_t index = 0U; index < left.Size(); ++index) {
|
||||||
|
result += left[index] * right[index];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Norm(const Vector& vector) {
|
||||||
|
double result = 0.0;
|
||||||
|
for (std::size_t i = 0; i < vector.Size(); ++i)
|
||||||
|
result = std::hypot(result, vector[i]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Ratio(double numerator, double denominator) {
|
||||||
|
if (std::isfinite(numerator) && std::isfinite(denominator) &&
|
||||||
|
denominator > 0.0)
|
||||||
|
return numerator / denominator;
|
||||||
|
return numerator == 0.0 && denominator == 0.0
|
||||||
|
? 0.0
|
||||||
|
: (std::numeric_limits<double>::infinity)();
|
||||||
|
}
|
||||||
|
|
||||||
|
double D(std::size_t dof, double length) {
|
||||||
|
return dof % 6U < 3U ? length : 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status MetricFailure(const char* metric, double value, std::size_t column) {
|
||||||
|
std::ostringstream message;
|
||||||
|
message << std::setprecision(17) << metric << "=" << value << "; limit=1e-10";
|
||||||
|
return Failure("eigenpair-validation-failed",
|
||||||
|
"solver-column=" + std::to_string(column), message.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
double FrobeniusNorm(const SparseMatrix& matrix) {
|
||||||
|
double norm = 0.0;
|
||||||
|
for (const double value : matrix.Values()) norm = std::hypot(norm, value);
|
||||||
|
return norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
double MaximumEdgeLength(const Domain& domain) {
|
||||||
|
double maximum = 0.0;
|
||||||
|
for (std::size_t shell = 0U; shell < domain.ShellElements().Size(); ++shell) {
|
||||||
|
const auto& nodes = domain.ShellElements()[shell].NodeIndices();
|
||||||
|
for (std::size_t edge = 0U; edge < 4U; ++edge) {
|
||||||
|
const auto& first = domain.Nodes()[nodes[edge]].coordinates;
|
||||||
|
const auto& second = domain.Nodes()[nodes[(edge + 1U) % 4U]].coordinates;
|
||||||
|
maximum =
|
||||||
|
(std::max)(maximum, std::hypot(std::hypot(first[0U] - second[0U],
|
||||||
|
first[1U] - second[1U]),
|
||||||
|
first[2U] - second[2U]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain) {
|
||||||
|
const FrequencyStepDefinition* frequency = domain.Frequency();
|
||||||
|
if (frequency == nullptr || domain.Steps().Size() != 1U) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("invalid-frequency-procedure", "step",
|
||||||
|
"The domain has no sole frequency step."));
|
||||||
|
}
|
||||||
|
if (frequency->requested_mode_count <= 0) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("invalid-frequency-request", "requested-mode-count",
|
||||||
|
"Frequency requested mode count must be positive."));
|
||||||
|
}
|
||||||
|
Result<AnalysisModel> model = AnalysisModel::Create(domain);
|
||||||
|
if (!model.HasValue())
|
||||||
|
return Result<EigenAnalysisState>::Failure(model.GetStatus());
|
||||||
|
const ElementFactory factory;
|
||||||
|
std::vector<std::unique_ptr<Element>> owned;
|
||||||
|
ElementView view;
|
||||||
|
owned.reserve(model.Value().ActiveElements().size());
|
||||||
|
view.reserve(model.Value().ActiveElements().size());
|
||||||
|
for (const EntityIndex index : model.Value().ActiveElements()) {
|
||||||
|
auto element = factory.Create(domain.Elements()[index], domain);
|
||||||
|
if (!element.HasValue())
|
||||||
|
return Result<EigenAnalysisState>::Failure(element.GetStatus());
|
||||||
|
if (dynamic_cast<const Mitc4Shell*>(element.Value().get()) == nullptr) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(Failure(
|
||||||
|
"unsupported-frequency-element", std::to_string(index),
|
||||||
|
"The frequency procedure supports MITC4 shell elements only."));
|
||||||
|
}
|
||||||
|
view.emplace_back(*element.Value());
|
||||||
|
owned.push_back(std::move(element.Value()));
|
||||||
|
}
|
||||||
|
DofManager dofs;
|
||||||
|
const Status dof_status = dofs.Build(model.Value(), view);
|
||||||
|
if (!dof_status.IsOk())
|
||||||
|
return Result<EigenAnalysisState>::Failure(dof_status);
|
||||||
|
const SerialParallelFor serial;
|
||||||
|
auto stiffness = SparseAssembler::Assemble(view, dofs, serial);
|
||||||
|
if (!stiffness.HasValue())
|
||||||
|
return Result<EigenAnalysisState>::Failure(stiffness.GetStatus());
|
||||||
|
|
||||||
|
std::vector<double> density(domain.Materials().Size(), 0.0);
|
||||||
|
for (const auto& item : domain.Densities()) {
|
||||||
|
if (item.material_index >= density.size() ||
|
||||||
|
density[item.material_index] != 0.0 || !(item.rho > 0.0) ||
|
||||||
|
!std::isfinite(item.rho)) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(Failure(
|
||||||
|
"invalid-material-density", std::to_string(item.material_index),
|
||||||
|
"Invalid or duplicate material density."));
|
||||||
|
}
|
||||||
|
density[item.material_index] = item.rho;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < dofs.PrescribedValues().Size(); ++i) {
|
||||||
|
if (dofs.PrescribedValues()[i] != 0.0) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Status::Failure(FailureCategory::kInput,
|
||||||
|
{{Severity::kError,
|
||||||
|
"unsupported-eigen-boundary",
|
||||||
|
frequency->frequency_location,
|
||||||
|
"BOUNDARY",
|
||||||
|
{},
|
||||||
|
"Frequency requires homogeneous constraints."}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<Matrix> nodal_mass(domain.Nodes().size(), Matrix(6U, 6U));
|
||||||
|
std::vector<double> drilling(domain.Nodes().size(), 0.0);
|
||||||
|
for (std::size_t shell = 0U; shell < domain.ShellElements().Size(); ++shell) {
|
||||||
|
const auto& definition = domain.ShellElements()[shell];
|
||||||
|
if (!(density[definition.MaterialIndex()] > 0.0)) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("missing-frequency-density",
|
||||||
|
std::to_string(definition.MaterialIndex()),
|
||||||
|
"Every active shell material requires a positive density."));
|
||||||
|
}
|
||||||
|
const auto* element = dynamic_cast<const Mitc4Shell*>(owned[shell].get());
|
||||||
|
auto mass = element->Mass(density[definition.MaterialIndex()]);
|
||||||
|
auto shell_stiffness = element->Stiffness();
|
||||||
|
if (!mass.HasValue())
|
||||||
|
return Result<EigenAnalysisState>::Failure(mass.GetStatus());
|
||||||
|
if (!shell_stiffness.HasValue())
|
||||||
|
return Result<EigenAnalysisState>::Failure(shell_stiffness.GetStatus());
|
||||||
|
for (std::size_t local_node = 0U; local_node < 4U; ++local_node) {
|
||||||
|
const std::size_t node = definition.NodeIndices()[local_node];
|
||||||
|
drilling[node] += shell_stiffness.Value().drilling_stiffness;
|
||||||
|
for (std::size_t a = 0U; a < 6U; ++a) {
|
||||||
|
for (std::size_t b = 0U; b < 6U; ++b) {
|
||||||
|
nodal_mass[node](a, b) += mass.Value().lumped_global24(
|
||||||
|
6U * local_node + a, 6U * local_node + b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double length = MaximumEdgeLength(domain);
|
||||||
|
std::vector<std::uint8_t> mask(dofs.FullDofCount(), 0U);
|
||||||
|
for (const std::size_t index : dofs.ConstrainedDofs()) mask[index] = 1U;
|
||||||
|
auto reduction = eigen_internal::BuildEigenReduction(
|
||||||
|
stiffness.Value(), nodal_mass, domain.ShellNodeInitialFrames(), mask,
|
||||||
|
drilling, length);
|
||||||
|
if (!reduction.HasValue())
|
||||||
|
return Result<EigenAnalysisState>::Failure(reduction.GetStatus());
|
||||||
|
const std::size_t request =
|
||||||
|
static_cast<std::size_t>(frequency->requested_mode_count);
|
||||||
|
if (request > reduction.Value().whitened_operator.Rows()) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(Status::Failure(
|
||||||
|
FailureCategory::kModel,
|
||||||
|
{{Severity::kError, "insufficient-finite-modes",
|
||||||
|
frequency->frequency_location, "FREQUENCY", std::to_string(request),
|
||||||
|
"Requested modes exceed finite dimension " +
|
||||||
|
std::to_string(reduction.Value().whitened_operator.Rows())}}));
|
||||||
|
}
|
||||||
|
double zero_scale = (std::numeric_limits<double>::infinity)();
|
||||||
|
for (std::size_t e = 0; e < domain.ShellElements().Size(); ++e) {
|
||||||
|
const auto material_id = domain.ShellElements()[e].MaterialIndex();
|
||||||
|
const auto* material = dynamic_cast<const IsotropicLinearElasticMaterial*>(
|
||||||
|
&domain.Materials()[material_id]);
|
||||||
|
if (material == nullptr)
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("invalid-eigenvalue-result", "material",
|
||||||
|
"Unsupported frequency material."));
|
||||||
|
zero_scale = (std::min)(zero_scale,
|
||||||
|
(material->YoungsModulus() / density[material_id]) /
|
||||||
|
length / length);
|
||||||
|
}
|
||||||
|
auto backend = MklFeastEigenSolver{}.Solve(
|
||||||
|
reduction.Value().whitened_operator, request, zero_scale);
|
||||||
|
if (!backend.HasValue())
|
||||||
|
return Result<EigenAnalysisState>::Failure(backend.GetStatus());
|
||||||
|
EigenAnalysisState state(std::move(backend.Value()));
|
||||||
|
state.full_dof_count = dofs.FullDofCount();
|
||||||
|
state.free_dof_count = reduction.Value().free_count;
|
||||||
|
state.mass_null_dimension = reduction.Value().null_count;
|
||||||
|
state.finite_dimension = reduction.Value().whitened_operator.Rows();
|
||||||
|
state.characteristic_length = length;
|
||||||
|
state.eta_mz = reduction.Value().eta_mz;
|
||||||
|
state.eta_kz = reduction.Value().eta_kz;
|
||||||
|
state.eta_pz = reduction.Value().eta_pz;
|
||||||
|
state.null_checks_applicable = state.mass_null_dimension != 0U;
|
||||||
|
state.constraint_mask = mask;
|
||||||
|
state.prescribed_displacement.assign(state.full_dof_count, 0.0);
|
||||||
|
for (const auto& node : reduction.Value().nodes)
|
||||||
|
state.nodal_reduction.push_back(node.record);
|
||||||
|
|
||||||
|
// The controls use the actual constrained, length-scaled pencil, including
|
||||||
|
// positive partial-BC drilling stiffness. Full constrained rows are
|
||||||
|
// reactions.
|
||||||
|
const SparseMatrix& k = stiffness.Value();
|
||||||
|
const SparseMatrix& a = reduction.Value().whitened_operator;
|
||||||
|
double stiffness_norm = 0.0;
|
||||||
|
for (const auto row : dofs.FreeDofs()) {
|
||||||
|
for (std::size_t entry = k.RowOffsets()[row];
|
||||||
|
entry < k.RowOffsets()[row + 1U]; ++entry) {
|
||||||
|
const auto col = k.ColumnIndices()[entry];
|
||||||
|
if (!mask[col])
|
||||||
|
stiffness_norm =
|
||||||
|
std::hypot(stiffness_norm,
|
||||||
|
D(row, length) * k.Values()[entry] * D(col, length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double mass_norm = 0.0;
|
||||||
|
for (std::size_t node = 0; node < nodal_mass.size(); ++node) {
|
||||||
|
for (std::size_t row = 0; row < 6U; ++row) {
|
||||||
|
for (std::size_t col = 0; col < 6U; ++col) {
|
||||||
|
if (!mask[6U * node + row] && !mask[6U * node + col]) {
|
||||||
|
mass_norm = std::hypot(
|
||||||
|
mass_norm,
|
||||||
|
D(row, length) * nodal_mass[node](row, col) * D(col, length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double a_norm = FrobeniusNorm(a);
|
||||||
|
state.vectors = Matrix(request, state.full_dof_count);
|
||||||
|
const std::size_t count = state.backend.eigenvalues.size();
|
||||||
|
std::vector<Vector> all_modes;
|
||||||
|
std::vector<Vector> all_mass_times;
|
||||||
|
all_modes.reserve(count);
|
||||||
|
all_mass_times.reserve(count);
|
||||||
|
std::size_t cluster = 1U;
|
||||||
|
for (std::size_t column = 0; column < count; ++column) {
|
||||||
|
const double lambda = state.backend.eigenvalues[column];
|
||||||
|
const double mu = lambda / state.backend.operator_scale;
|
||||||
|
if (column != 0U && mu - state.backend.eigenvalues[column - 1U] /
|
||||||
|
state.backend.operator_scale >
|
||||||
|
1e-8)
|
||||||
|
++cluster;
|
||||||
|
Vector y = eigen_internal::RestoreScaledMode(
|
||||||
|
reduction.Value(), state.backend.eigenvectors, column);
|
||||||
|
Vector physical(y.Size());
|
||||||
|
std::size_t pivot = 0U;
|
||||||
|
for (std::size_t dof = 0; dof < y.Size(); ++dof) {
|
||||||
|
physical[dof] = D(dof, length) * y[dof];
|
||||||
|
if (std::abs(y[dof]) > std::abs(y[pivot])) pivot = dof;
|
||||||
|
}
|
||||||
|
Vector mass_times = eigen_internal::MultiplyNodalMass(nodal_mass, physical);
|
||||||
|
const double initial_mass = Dot(physical, mass_times);
|
||||||
|
if (!(initial_mass > 0.0) || !std::isfinite(initial_mass))
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
MetricFailure("initial_modal_mass", initial_mass,
|
||||||
|
state.backend.column_ids[column]));
|
||||||
|
const double normalization =
|
||||||
|
(y[pivot] < 0.0 ? -1.0 : 1.0) / std::sqrt(initial_mass);
|
||||||
|
for (std::size_t dof = 0; dof < physical.Size(); ++dof) {
|
||||||
|
physical[dof] *= normalization;
|
||||||
|
if (!std::isfinite(physical[dof]) ||
|
||||||
|
(mask[dof] && physical[dof] != 0.0)) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("invalid-eigenvalue-result", std::to_string(column),
|
||||||
|
"Recovered mode is nonfinite or violates constraints."));
|
||||||
|
}
|
||||||
|
y[dof] = physical[dof] / D(dof, length);
|
||||||
|
}
|
||||||
|
// All stored physical metrics are recomputed from the final normalized phi.
|
||||||
|
mass_times = eigen_internal::MultiplyNodalMass(nodal_mass, physical);
|
||||||
|
const double modal_mass = Dot(physical, mass_times);
|
||||||
|
Vector ky = k.Multiply(physical);
|
||||||
|
Vector residual(physical.Size());
|
||||||
|
for (std::size_t dof = 0; dof < physical.Size(); ++dof) {
|
||||||
|
ky[dof] = mask[dof] ? 0.0 : D(dof, length) * ky[dof];
|
||||||
|
residual[dof] =
|
||||||
|
mask[dof] ? 0.0 : ky[dof] - lambda * D(dof, length) * mass_times[dof];
|
||||||
|
}
|
||||||
|
const double energy = Dot(y, ky);
|
||||||
|
const double y_norm = Norm(y);
|
||||||
|
const double pencil_scale = stiffness_norm + std::abs(lambda) * mass_norm;
|
||||||
|
const double eta_r = Ratio(Norm(residual), pencil_scale * y_norm);
|
||||||
|
const double eta_norm = std::abs(modal_mass - 1.0);
|
||||||
|
const double eta_energy = Ratio(std::abs(energy - lambda * modal_mass),
|
||||||
|
(pencil_scale * y_norm) * y_norm);
|
||||||
|
const double rayleigh = energy / modal_mass;
|
||||||
|
Vector x(a.Rows());
|
||||||
|
for (std::size_t row = 0; row < x.Size(); ++row)
|
||||||
|
x[row] = state.backend.eigenvectors(row, column) * normalization;
|
||||||
|
Vector ax = a.Multiply(x);
|
||||||
|
Vector whitened_residual = ax;
|
||||||
|
whitened_residual.Axpy(-lambda, x);
|
||||||
|
const double x_norm = Norm(x);
|
||||||
|
const double eta_w =
|
||||||
|
Ratio(Norm(whitened_residual), (a_norm + std::abs(lambda)) * x_norm);
|
||||||
|
for (std::size_t row = 0; row < ax.Size(); ++row)
|
||||||
|
ax[row] = ax[row] / state.backend.operator_scale - mu * x[row];
|
||||||
|
const double rho_b = Ratio(Norm(ax), x_norm);
|
||||||
|
for (const auto& metric :
|
||||||
|
std::initializer_list<std::pair<const char*, double>>{
|
||||||
|
{"eta_r", eta_r},
|
||||||
|
{"eta_w", eta_w},
|
||||||
|
{"rho_b", rho_b},
|
||||||
|
{"eta_norm", eta_norm},
|
||||||
|
{"eta_energy", eta_energy}}) {
|
||||||
|
if (!std::isfinite(metric.second) || metric.second > 1e-10)
|
||||||
|
return Result<EigenAnalysisState>::Failure(MetricFailure(
|
||||||
|
metric.first, metric.second, state.backend.column_ids[column]));
|
||||||
|
}
|
||||||
|
if (!std::isfinite(rayleigh))
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("invalid-eigenvalue-result", std::to_string(column),
|
||||||
|
"Nonfinite Rayleigh quotient."));
|
||||||
|
state.eigenpair_checks.push_back(
|
||||||
|
{column + 1U, state.backend.column_ids[column], lambda, modal_mass,
|
||||||
|
rayleigh, eta_r, eta_w, rho_b, eta_norm, eta_energy,
|
||||||
|
state.backend.backend_res_available[column],
|
||||||
|
state.backend.backend_res[column]});
|
||||||
|
if (column < request) {
|
||||||
|
std::optional<double> frequency_value;
|
||||||
|
if (lambda >= 0.0) {
|
||||||
|
frequency_value = std::sqrt(lambda) / (2.0 * std::acos(-1.0));
|
||||||
|
if (!std::isfinite(*frequency_value) ||
|
||||||
|
(lambda > 0.0 && *frequency_value == 0.0))
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("invalid-eigenvalue-result", std::to_string(column),
|
||||||
|
"Cycle frequency conversion failed."));
|
||||||
|
}
|
||||||
|
state.modes.push_back(
|
||||||
|
{column + 1U, lambda,
|
||||||
|
static_cast<std::uint8_t>(std::abs(mu) <= 1e-9 ? 0U : 1U), cluster,
|
||||||
|
frequency_value});
|
||||||
|
for (std::size_t dof = 0; dof < physical.Size(); ++dof)
|
||||||
|
state.vectors(column, dof) = physical[dof];
|
||||||
|
}
|
||||||
|
all_modes.push_back(std::move(physical));
|
||||||
|
all_mass_times.push_back(std::move(mass_times));
|
||||||
|
}
|
||||||
|
double gram_norm = 0.0;
|
||||||
|
for (std::size_t left = 0; left < count; ++left) {
|
||||||
|
for (std::size_t right = 0; right < count; ++right) {
|
||||||
|
const double gram = Dot(all_modes[left], all_mass_times[right]);
|
||||||
|
gram_norm = std::hypot(gram_norm, gram - (left == right ? 1.0 : 0.0));
|
||||||
|
if (left != right)
|
||||||
|
state.max_eta_orth =
|
||||||
|
(std::max)(state.max_eta_orth,
|
||||||
|
Ratio(std::abs(gram),
|
||||||
|
std::sqrt(
|
||||||
|
state.eigenpair_checks[left].modal_mass *
|
||||||
|
state.eigenpair_checks[right].modal_mass)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.eta_gram = gram_norm / std::sqrt(static_cast<double>(count));
|
||||||
|
if (!std::isfinite(state.eta_gram) || state.eta_gram > 1e-10 ||
|
||||||
|
!std::isfinite(state.max_eta_orth) || state.max_eta_orth > 1e-10) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
MetricFailure("eta_gram", state.eta_gram, 0U));
|
||||||
|
}
|
||||||
|
state.backend.eigenvectors = Matrix(0U, 0U);
|
||||||
|
return Result<EigenAnalysisState>::Success(std::move(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result<EigenAnalysisState> LinearFrequencyProcedureExecutor::Execute(
|
||||||
|
const Domain& domain) const {
|
||||||
|
try {
|
||||||
|
auto result = ExecuteCandidate(domain);
|
||||||
|
if (!result.HasValue()) {
|
||||||
|
auto diagnostics = result.GetStatus().Diagnostics();
|
||||||
|
for (auto& diagnostic : diagnostics) {
|
||||||
|
if (diagnostic.location.file.empty())
|
||||||
|
diagnostic.location = domain.Frequency() != nullptr
|
||||||
|
? domain.Frequency()->frequency_location
|
||||||
|
: SourceLocation{domain.SourcePath(), 0U};
|
||||||
|
}
|
||||||
|
return Result<EigenAnalysisState>::Failure(Status::Failure(
|
||||||
|
result.GetStatus().Category().value_or(FailureCategory::kSolver),
|
||||||
|
std::move(diagnostics)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (const std::bad_alloc&) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("eigen-allocation-failed", "modal-workspace",
|
||||||
|
"Allocation failed after dimension checks; requested modes "
|
||||||
|
"were not reduced."));
|
||||||
|
} catch (const std::length_error&) {
|
||||||
|
return Result<EigenAnalysisState>::Failure(
|
||||||
|
Failure("eigen-index-overflow", "modal-workspace",
|
||||||
|
"Requested workspace exceeds container capacity."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -26,6 +26,16 @@ LinearStaticProcedureExecutor::LinearStaticProcedureExecutor(
|
|||||||
|
|
||||||
Result<LinearStaticExecutionResult> LinearStaticProcedureExecutor::Execute(
|
Result<LinearStaticExecutionResult> LinearStaticProcedureExecutor::Execute(
|
||||||
const Domain& domain) const {
|
const Domain& domain) const {
|
||||||
|
if (domain.Frequency() != nullptr) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(Status::Failure(
|
||||||
|
FailureCategory::kModel,
|
||||||
|
{{Severity::kError,
|
||||||
|
"invalid-analysis-procedure",
|
||||||
|
domain.Frequency()->frequency_location,
|
||||||
|
"FREQUENCY",
|
||||||
|
{},
|
||||||
|
"Linear-static execution cannot consume a frequency step."}}));
|
||||||
|
}
|
||||||
Result<AnalysisModel> model = AnalysisModel::Create(domain);
|
Result<AnalysisModel> model = AnalysisModel::Create(domain);
|
||||||
if (!model.HasValue()) {
|
if (!model.HasValue()) {
|
||||||
return Result<LinearStaticExecutionResult>::Failure(model.GetStatus());
|
return Result<LinearStaticExecutionResult>::Failure(model.GetStatus());
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||||
#include "fesa/analysis/linear_static_procedure.h"
|
#include "fesa/analysis/linear_static_procedure.h"
|
||||||
#include "fesa/analysis/newton_iteration_log.h"
|
#include "fesa/analysis/newton_iteration_log.h"
|
||||||
#include "fesa/analysis/nonlinear_static_procedure.h"
|
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||||
#include "fesa/app/output_paths.h"
|
#include "fesa/app/output_paths.h"
|
||||||
#include "fesa/io/abaqus/domain_mapper.h"
|
#include "fesa/io/abaqus/domain_mapper.h"
|
||||||
#include "fesa/io/abaqus/input_reader.h"
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
#include "fesa/results/eigen_results_writer.h"
|
||||||
#include "fesa/results/nonlinear_results_writer.h"
|
#include "fesa/results/nonlinear_results_writer.h"
|
||||||
#include "fesa/results/results_writer.h"
|
#include "fesa/results/results_writer.h"
|
||||||
|
|
||||||
@@ -34,12 +36,26 @@ Result<PreparedAnalysisInput> PrepareAnalysisInput(
|
|||||||
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
||||||
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
||||||
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
||||||
|
const LinearFrequencyProcedureExecutor& frequency_executor,
|
||||||
NewtonIterationLogWriter& log) {
|
NewtonIterationLogWriter& log) {
|
||||||
if (domain.Steps().Size() != 1U) {
|
if (domain.Steps().Size() != 1U) {
|
||||||
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
|
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
|
||||||
FailureCategory::kInput,
|
FailureCategory::kInput,
|
||||||
{{Severity::kError, "invalid-analysis-step-count", {}, "*STEP", {},
|
{{Severity::kError,
|
||||||
"Prepared analysis requires exactly one static step."}}));
|
"invalid-analysis-step-count",
|
||||||
|
{},
|
||||||
|
"*STEP",
|
||||||
|
{},
|
||||||
|
"Prepared analysis requires exactly one analysis step."}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.Steps()[0U].IsFrequency()) {
|
||||||
|
auto eigen = frequency_executor.Execute(domain);
|
||||||
|
if (!eigen.HasValue()) {
|
||||||
|
return Result<PreparedAnalysisOutcome>::Failure(eigen.GetStatus());
|
||||||
|
}
|
||||||
|
return Result<PreparedAnalysisOutcome>::Success(
|
||||||
|
PreparedAnalysisOutcome{std::move(eigen.Value())});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain.Steps()[0U].IsNonlinearStatic()) {
|
if (domain.Steps()[0U].IsNonlinearStatic()) {
|
||||||
@@ -64,7 +80,13 @@ Status FinalizePreparedAnalysisResult(
|
|||||||
const DerivedOutputPaths& paths, const Domain& domain,
|
const DerivedOutputPaths& paths, const Domain& domain,
|
||||||
const PreparedAnalysisOutcome& outcome,
|
const PreparedAnalysisOutcome& outcome,
|
||||||
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
||||||
NonlinearResultsWriter& nonlinear_writer) {
|
NonlinearResultsWriter& nonlinear_writer,
|
||||||
|
EigenResultsWriter& eigen_writer) {
|
||||||
|
if (std::holds_alternative<EigenAnalysisState>(outcome)) {
|
||||||
|
return eigen_writer.Write(paths.results_path, domain,
|
||||||
|
std::get<EigenAnalysisState>(outcome),
|
||||||
|
diagnostics);
|
||||||
|
}
|
||||||
if (std::holds_alternative<LinearStaticExecutionResult>(outcome)) {
|
if (std::holds_alternative<LinearStaticExecutionResult>(outcome)) {
|
||||||
return linear_writer.Write(
|
return linear_writer.Write(
|
||||||
paths.results_path, domain,
|
paths.results_path, domain,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||||
#include "fesa/analysis/linear_static_procedure.h"
|
#include "fesa/analysis/linear_static_procedure.h"
|
||||||
#include "fesa/analysis/newton_iteration_log.h"
|
#include "fesa/analysis/newton_iteration_log.h"
|
||||||
#include "fesa/analysis/nonlinear_static_procedure.h"
|
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include "fesa/app/output_paths.h"
|
#include "fesa/app/output_paths.h"
|
||||||
#include "fesa/assembly/parallel_for.h"
|
#include "fesa/assembly/parallel_for.h"
|
||||||
#include "fesa/core/diagnostic.h"
|
#include "fesa/core/diagnostic.h"
|
||||||
|
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
|
||||||
#include "fesa/io/hdf5/hdf5_results_writer.h"
|
#include "fesa/io/hdf5/hdf5_results_writer.h"
|
||||||
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
|
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
|
||||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||||
@@ -63,8 +65,8 @@ int ExitCodeFor(const Status& status) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
||||||
const auto paths = DeriveSingleInputOutputPaths(
|
const auto paths =
|
||||||
arguments, std::filesystem::current_path());
|
DeriveSingleInputOutputPaths(arguments, std::filesystem::current_path());
|
||||||
if (!paths.HasValue()) {
|
if (!paths.HasValue()) {
|
||||||
WriteDiagnostics(paths.GetStatus().Diagnostics());
|
WriteDiagnostics(paths.GetStatus().Diagnostics());
|
||||||
return kUsageExitCode;
|
return kUsageExitCode;
|
||||||
@@ -81,6 +83,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
|||||||
MklPardisoTangentSolver tangent_solver;
|
MklPardisoTangentSolver tangent_solver;
|
||||||
Hdf5ResultsWriter linear_writer;
|
Hdf5ResultsWriter linear_writer;
|
||||||
NonlinearHdf5ResultsWriter nonlinear_writer;
|
NonlinearHdf5ResultsWriter nonlinear_writer;
|
||||||
|
EigenHdf5ResultsWriter eigen_writer;
|
||||||
NewtonIterationLogWriter log;
|
NewtonIterationLogWriter log;
|
||||||
|
|
||||||
Status status = log.Open(paths.Value().log_path);
|
Status status = log.Open(paths.Value().log_path);
|
||||||
@@ -97,8 +100,10 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
|||||||
LinearStaticProcedureExecutor linear_executor(parallel_for, linear_solver);
|
LinearStaticProcedureExecutor linear_executor(parallel_for, linear_solver);
|
||||||
NonlinearStaticProcedureExecutor nonlinear_executor(parallel_for,
|
NonlinearStaticProcedureExecutor nonlinear_executor(parallel_for,
|
||||||
tangent_solver);
|
tangent_solver);
|
||||||
Result<PreparedAnalysisOutcome> outcome = ExecutePreparedAnalysis(
|
const LinearFrequencyProcedureExecutor frequency_executor;
|
||||||
prepared.Value().domain, linear_executor, nonlinear_executor, log);
|
Result<PreparedAnalysisOutcome> outcome =
|
||||||
|
ExecutePreparedAnalysis(prepared.Value().domain, linear_executor,
|
||||||
|
nonlinear_executor, frequency_executor, log);
|
||||||
if (!outcome.HasValue()) {
|
if (!outcome.HasValue()) {
|
||||||
status = outcome.GetStatus();
|
status = outcome.GetStatus();
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +114,8 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
|||||||
}
|
}
|
||||||
status = FinalizePreparedAnalysisResult(
|
status = FinalizePreparedAnalysisResult(
|
||||||
paths.Value(), prepared.Value().domain, outcome.Value(),
|
paths.Value(), prepared.Value().domain, outcome.Value(),
|
||||||
prepared.Value().diagnostics, linear_writer, nonlinear_writer);
|
prepared.Value().diagnostics, linear_writer, nonlinear_writer,
|
||||||
|
eigen_writer);
|
||||||
if (status.IsOk()) {
|
if (status.IsOk()) {
|
||||||
return kSuccessExitCode;
|
return kSuccessExitCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,6 +279,27 @@ Result<Mitc4Stiffness> StiffnessFailure(const SourceLocation& location,
|
|||||||
"*ELEMENT", identity, std::move(message)}}));
|
"*ELEMENT", identity, std::move(message)}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result<Mitc4Mass> MassFailure(const SourceLocation& location,
|
||||||
|
const std::string& identity,
|
||||||
|
std::string message) {
|
||||||
|
return Result<Mitc4Mass>::Failure(
|
||||||
|
Status::Failure(FailureCategory::kModel,
|
||||||
|
{{Severity::kError, "invalid-shell-mass", location,
|
||||||
|
"*ELEMENT", identity, std::move(message)}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief Returns S=-[director]_cross for theta cross director.
|
||||||
|
Matrix DirectorSkew(const Vector3& director) {
|
||||||
|
Matrix result{3U, 3U};
|
||||||
|
result(0U, 1U) = director.Z();
|
||||||
|
result(0U, 2U) = -director.Y();
|
||||||
|
result(1U, 0U) = -director.Z();
|
||||||
|
result(1U, 2U) = director.X();
|
||||||
|
result(2U, 0U) = director.Y();
|
||||||
|
result(2U, 1U) = -director.X();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
Result<Mitc4PhysicalRecovery> RecoveryFailure(const SourceLocation& location,
|
Result<Mitc4PhysicalRecovery> RecoveryFailure(const SourceLocation& location,
|
||||||
const std::string& identity,
|
const std::string& identity,
|
||||||
std::string message) {
|
std::string message) {
|
||||||
@@ -625,24 +646,29 @@ Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double drilling_reference = (std::numeric_limits<double>::max)();
|
double maximum_rotary_diagonal = 0.0;
|
||||||
bool has_drilling_reference = false;
|
std::array<double, 8> rotary_diagonals{};
|
||||||
|
std::size_t rotary_index = 0U;
|
||||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
const std::size_t offset = node * kPhysicalDofsPerNode;
|
const std::size_t offset = node * kPhysicalDofsPerNode;
|
||||||
for (std::size_t rotation = 3U; rotation < 5U; ++rotation) {
|
for (std::size_t rotation = 3U; rotation < 5U; ++rotation) {
|
||||||
const double diagonal =
|
const double diagonal =
|
||||||
physical_local(offset + rotation, offset + rotation);
|
physical_local(offset + rotation, offset + rotation);
|
||||||
if (std::isfinite(diagonal) && diagonal > 0.0) {
|
if (!std::isfinite(diagonal) || diagonal < 0.0) {
|
||||||
drilling_reference = (std::min)(drilling_reference, diagonal);
|
|
||||||
has_drilling_reference = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!has_drilling_reference) {
|
|
||||||
return StiffnessFailure(
|
return StiffnessFailure(
|
||||||
source_location_, identity_,
|
source_location_, identity_,
|
||||||
"MITC4 drilling stabilization requires a finite positive physical "
|
"MITC4 drilling stabilization requires finite nonnegative "
|
||||||
"tangent-rotation diagonal.");
|
"physical tangent-rotation diagonals.");
|
||||||
|
}
|
||||||
|
rotary_diagonals[rotary_index++] = diagonal;
|
||||||
|
maximum_rotary_diagonal = (std::max)(maximum_rotary_diagonal, diagonal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(maximum_rotary_diagonal > 0.0)) {
|
||||||
|
return StiffnessFailure(
|
||||||
|
source_location_, identity_,
|
||||||
|
"MITC4 drilling stabilization requires a positive physical "
|
||||||
|
"tangent-rotation diagonal inventory.");
|
||||||
}
|
}
|
||||||
if (!IsFinite(physical_local)) {
|
if (!IsFinite(physical_local)) {
|
||||||
return StiffnessFailure(
|
return StiffnessFailure(
|
||||||
@@ -650,7 +676,12 @@ Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const {
|
|||||||
"MITC4 physical stiffness must contain only finite values.");
|
"MITC4 physical stiffness must contain only finite values.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const double drilling_stiffness = 1.0e-3 * drilling_reference;
|
double normalized_diagonal_sum = 0.0;
|
||||||
|
for (const double diagonal : rotary_diagonals) {
|
||||||
|
normalized_diagonal_sum += diagonal / maximum_rotary_diagonal;
|
||||||
|
}
|
||||||
|
const double drilling_stiffness =
|
||||||
|
1.0e-3 * maximum_rotary_diagonal * (normalized_diagonal_sum / 8.0);
|
||||||
if (!std::isfinite(drilling_stiffness) || !(drilling_stiffness > 0.0)) {
|
if (!std::isfinite(drilling_stiffness) || !(drilling_stiffness > 0.0)) {
|
||||||
return StiffnessFailure(
|
return StiffnessFailure(
|
||||||
source_location_, identity_,
|
source_location_, identity_,
|
||||||
@@ -685,6 +716,144 @@ Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const {
|
|||||||
drilling_stiffness});
|
drilling_stiffness});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result<Mitc4Mass> Mitc4Shell::Mass(const double density) const {
|
||||||
|
if (!std::isfinite(density) || !(density > 0.0)) {
|
||||||
|
return MassFailure(source_location_, identity_,
|
||||||
|
"MITC4 mass requires finite density > 0.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<Matrix, 3> moments{Matrix{kNodeCount, kNodeCount},
|
||||||
|
Matrix{kNodeCount, kNodeCount},
|
||||||
|
Matrix{kNodeCount, kNodeCount}};
|
||||||
|
constexpr double kGaussCoordinate = 0.77459666924148337704;
|
||||||
|
constexpr std::array<double, 3> kGaussPoints{-kGaussCoordinate, 0.0,
|
||||||
|
kGaussCoordinate};
|
||||||
|
constexpr std::array<double, 3> kGaussWeights{5.0 / 9.0, 8.0 / 9.0,
|
||||||
|
5.0 / 9.0};
|
||||||
|
for (std::size_t xi_index = 0U; xi_index < kGaussPoints.size(); ++xi_index) {
|
||||||
|
for (std::size_t eta_index = 0U; eta_index < kGaussPoints.size();
|
||||||
|
++eta_index) {
|
||||||
|
const Mitc4ShapeFunctions shape =
|
||||||
|
ShapeFunctions(kGaussPoints[xi_index], kGaussPoints[eta_index]);
|
||||||
|
for (std::size_t zeta_index = 0U; zeta_index < kGaussPoints.size();
|
||||||
|
++zeta_index) {
|
||||||
|
GeometryData geometry{};
|
||||||
|
const double zeta = kGaussPoints[zeta_index];
|
||||||
|
if (!EvaluateGeometry(kGaussPoints[xi_index], kGaussPoints[eta_index],
|
||||||
|
zeta, geometry)) {
|
||||||
|
return MassFailure(source_location_, identity_,
|
||||||
|
"MITC4 mass quadrature geometry is invalid.");
|
||||||
|
}
|
||||||
|
const double z = 0.5 * thickness_ * zeta;
|
||||||
|
const double weight =
|
||||||
|
density * geometry.jacobian * kGaussWeights[xi_index] *
|
||||||
|
kGaussWeights[eta_index] * kGaussWeights[zeta_index];
|
||||||
|
if (!std::isfinite(z) || !std::isfinite(weight)) {
|
||||||
|
return MassFailure(
|
||||||
|
source_location_, identity_,
|
||||||
|
"MITC4 mass quadrature contribution is nonfinite.");
|
||||||
|
}
|
||||||
|
const std::array<double, 3> z_powers{1.0, z, z * z};
|
||||||
|
for (std::size_t row = 0U; row < kNodeCount; ++row) {
|
||||||
|
for (std::size_t column = row; column < kNodeCount; ++column) {
|
||||||
|
const double shape_product =
|
||||||
|
shape.values[row] * shape.values[column];
|
||||||
|
for (std::size_t moment = 0U; moment < moments.size(); ++moment) {
|
||||||
|
const double contribution =
|
||||||
|
weight * shape_product * z_powers[moment];
|
||||||
|
moments[moment](row, column) += contribution;
|
||||||
|
if (row != column) {
|
||||||
|
moments[moment](column, row) += contribution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const Matrix& moment : moments) {
|
||||||
|
if (!IsFinite(moment)) {
|
||||||
|
return MassFailure(source_location_, identity_,
|
||||||
|
"MITC4 consistent moments must be finite.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix consistent{kGlobalDofCount, kGlobalDofCount};
|
||||||
|
for (std::size_t row_node = 0U; row_node < kNodeCount; ++row_node) {
|
||||||
|
const Matrix row_skew = DirectorSkew(directors_[row_node]);
|
||||||
|
for (std::size_t column_node = 0U; column_node < kNodeCount;
|
||||||
|
++column_node) {
|
||||||
|
const Matrix column_skew = DirectorSkew(directors_[column_node]);
|
||||||
|
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||||
|
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||||
|
const std::size_t global_row = kGlobalDofsPerNode * row_node + row;
|
||||||
|
const std::size_t global_column =
|
||||||
|
kGlobalDofsPerNode * column_node + column;
|
||||||
|
consistent(global_row, global_column) =
|
||||||
|
moments[0U](row_node, column_node) * (row == column ? 1.0 : 0.0);
|
||||||
|
consistent(global_row, global_column + 3U) =
|
||||||
|
moments[1U](row_node, column_node) * column_skew(row, column);
|
||||||
|
consistent(global_row + 3U, global_column) =
|
||||||
|
moments[1U](row_node, column_node) * row_skew(column, row);
|
||||||
|
double rotary = 0.0;
|
||||||
|
for (std::size_t axis = 0U; axis < 3U; ++axis) {
|
||||||
|
rotary += row_skew(axis, row) * column_skew(axis, column);
|
||||||
|
}
|
||||||
|
consistent(global_row + 3U, global_column + 3U) =
|
||||||
|
moments[2U](row_node, column_node) * rotary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix lumped{kGlobalDofCount, kGlobalDofCount};
|
||||||
|
std::array<std::array<double, 3>, kNodeCount> nodal_moments{};
|
||||||
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
|
double mass = 0.0;
|
||||||
|
double first_moment = 0.0;
|
||||||
|
double second_moment = 0.0;
|
||||||
|
for (std::size_t column = 0U; column < kNodeCount; ++column) {
|
||||||
|
mass += moments[0U](node, column);
|
||||||
|
first_moment += moments[1U](node, column);
|
||||||
|
second_moment += moments[2U](node, column);
|
||||||
|
}
|
||||||
|
const double centered_inertia =
|
||||||
|
second_moment - first_moment * (first_moment / mass);
|
||||||
|
if (!std::isfinite(mass) || !std::isfinite(first_moment) ||
|
||||||
|
!std::isfinite(second_moment) || !std::isfinite(centered_inertia) ||
|
||||||
|
!(mass > 0.0) || !(second_moment > 0.0) || !(centered_inertia > 0.0)) {
|
||||||
|
return MassFailure(
|
||||||
|
source_location_, identity_,
|
||||||
|
"MITC4 nodal mass moments must be finite and positive.");
|
||||||
|
}
|
||||||
|
nodal_moments[node] = {mass, first_moment, second_moment};
|
||||||
|
const Matrix skew = DirectorSkew(directors_[node]);
|
||||||
|
const std::size_t offset = kGlobalDofsPerNode * node;
|
||||||
|
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||||
|
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||||
|
lumped(offset + row, offset + column) =
|
||||||
|
mass * (row == column ? 1.0 : 0.0);
|
||||||
|
lumped(offset + row, offset + column + 3U) =
|
||||||
|
first_moment * skew(row, column);
|
||||||
|
lumped(offset + row + 3U, offset + column) =
|
||||||
|
first_moment * skew(column, row);
|
||||||
|
double projector = row == column ? 1.0 : 0.0;
|
||||||
|
projector -= directors_[node][row] * directors_[node][column];
|
||||||
|
lumped(offset + row + 3U, offset + column + 3U) =
|
||||||
|
second_moment * projector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!IsFinite(consistent) || !IsFinite(lumped)) {
|
||||||
|
return MassFailure(
|
||||||
|
source_location_, identity_,
|
||||||
|
"MITC4 transformed mass must contain only finite values.");
|
||||||
|
}
|
||||||
|
return Result<Mitc4Mass>::Success(
|
||||||
|
Mitc4Mass{std::move(moments), std::move(consistent), std::move(lumped),
|
||||||
|
std::move(nodal_moments)});
|
||||||
|
}
|
||||||
|
|
||||||
Result<Mitc4PhysicalRecovery> Mitc4Shell::RecoverPhysical(
|
Result<Mitc4PhysicalRecovery> Mitc4Shell::RecoverPhysical(
|
||||||
const Vector& global_element_displacement24) const {
|
const Vector& global_element_displacement24) const {
|
||||||
if (global_element_displacement24.Size() != kGlobalDofCount) {
|
if (global_element_displacement24.Size() != kGlobalDofCount) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,974 @@
|
|||||||
|
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <limits>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/analysis_model.h"
|
||||||
|
#include "fesa/build_info.h"
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
#include "io/hdf5/eigen_hdf5_self_check.h"
|
||||||
|
#include "io/hdf5/hdf5_atomic_file.h"
|
||||||
|
#include "io/hdf5/hdf5_model_writer.h"
|
||||||
|
#include "io/hdf5/hdf5_primitives.h"
|
||||||
|
#include "io/hdf5/hdf5_raii.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char* kEigenRoot = "/steps/Step-1/eigen";
|
||||||
|
|
||||||
|
void WriteQuantityAttributes(hid_t dataset, const std::string& components,
|
||||||
|
const std::string& units,
|
||||||
|
const std::string& coordinates,
|
||||||
|
const std::string& location) {
|
||||||
|
hdf5_internal::WriteStringAttribute(dataset, "component_names", components);
|
||||||
|
hdf5_internal::WriteStringAttribute(dataset, "component_unit_dimensions",
|
||||||
|
units);
|
||||||
|
hdf5_internal::WriteStringAttribute(dataset, "coordinate_system",
|
||||||
|
coordinates);
|
||||||
|
hdf5_internal::WriteStringAttribute(dataset, "location", location);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteModalAttributes(hid_t dataset, const std::string& source_step) {
|
||||||
|
hdf5_internal::WriteUint64Attribute(dataset, "eigen_schema_version", 1U);
|
||||||
|
hdf5_internal::WriteStringAttribute(dataset, "source_step_name", source_step);
|
||||||
|
hdf5_internal::WriteStringAttribute(dataset, "domain", "mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsFinite(const Matrix& matrix) {
|
||||||
|
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
|
||||||
|
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
|
||||||
|
if (!std::isfinite(matrix(row, column))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Validate(const Domain& domain, const EigenAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) {
|
||||||
|
const FrequencyStepDefinition* frequency = domain.Frequency();
|
||||||
|
if (frequency == nullptr || !hdf5_internal::IsShellDomain(domain)) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Eigen HDF5 output requires one shell frequency domain.");
|
||||||
|
}
|
||||||
|
std::set<EntityIndex> density_materials;
|
||||||
|
EntityIndex previous_density_material = 0U;
|
||||||
|
bool first_density = true;
|
||||||
|
for (const MaterialDensityDefinition& density : domain.Densities()) {
|
||||||
|
if (density.material_index >= domain.Materials().Size() ||
|
||||||
|
!std::isfinite(density.rho) || !(density.rho > 0.0) ||
|
||||||
|
(!first_density &&
|
||||||
|
density.material_index <= previous_density_material)) {
|
||||||
|
return hdf5_internal::OutputFailure("invalid-eigen-result-state",
|
||||||
|
"Eigen material density inventory "
|
||||||
|
"must be finite and source ordered.");
|
||||||
|
}
|
||||||
|
density_materials.insert(density.material_index);
|
||||||
|
previous_density_material = density.material_index;
|
||||||
|
first_density = false;
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) {
|
||||||
|
if (density_materials.count(
|
||||||
|
domain.ShellElements()[index].MaterialIndex()) == 0U) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Every active shell material requires one positive density.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const std::size_t node_count = domain.Nodes().size();
|
||||||
|
if (node_count > (std::numeric_limits<std::size_t>::max)() / 6U ||
|
||||||
|
state.full_dof_count != 6U * node_count ||
|
||||||
|
state.free_dof_count !=
|
||||||
|
state.mass_null_dimension + state.finite_dimension ||
|
||||||
|
state.modes.size() !=
|
||||||
|
static_cast<std::size_t>(frequency->requested_mode_count) ||
|
||||||
|
state.vectors.Rows() != state.modes.size() ||
|
||||||
|
state.vectors.Columns() != 6U * node_count ||
|
||||||
|
state.nodal_reduction.size() != node_count ||
|
||||||
|
state.constraint_mask.size() != 6U * node_count ||
|
||||||
|
state.prescribed_displacement.size() != 6U * node_count ||
|
||||||
|
state.eigenpair_checks.size() < state.modes.size() ||
|
||||||
|
state.eigenpair_checks.size() > state.finite_dimension ||
|
||||||
|
!IsFinite(state.vectors) || !std::isfinite(state.characteristic_length) ||
|
||||||
|
!(state.characteristic_length > 0.0) ||
|
||||||
|
!std::isfinite(state.backend.operator_scale) ||
|
||||||
|
!(state.backend.operator_scale > 0.0)) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Eigen state does not satisfy the durable result inventory.");
|
||||||
|
}
|
||||||
|
const auto metric_ok = [](double value, double limit) {
|
||||||
|
return std::isfinite(value) && value >= 0.0 && value <= limit;
|
||||||
|
};
|
||||||
|
const auto& backend = state.backend;
|
||||||
|
if (backend.base_fpm.size() != 128U || backend.backend_calls.empty() ||
|
||||||
|
backend.backend_calls.size() > 512U || backend.thread_count != 1U ||
|
||||||
|
backend.seed != 0x4D495443344D4645ULL ||
|
||||||
|
backend.runtime_version.empty() ||
|
||||||
|
(backend.integer_abi != "LP64" && backend.integer_abi != "ILP64") ||
|
||||||
|
backend.requested_modes != state.modes.size() ||
|
||||||
|
!metric_ok(backend.gram_error, 1e-10) ||
|
||||||
|
!std::isfinite(backend.enclosure_lower) ||
|
||||||
|
!std::isfinite(backend.enclosure_upper) ||
|
||||||
|
!std::isfinite(backend.solve_lower) ||
|
||||||
|
!std::isfinite(backend.solve_upper) ||
|
||||||
|
!(backend.enclosure_lower < backend.enclosure_upper) ||
|
||||||
|
backend.solve_lower != backend.enclosure_lower ||
|
||||||
|
!(backend.solve_lower < backend.solve_upper) ||
|
||||||
|
backend.solve_upper > backend.enclosure_upper) {
|
||||||
|
return hdf5_internal::OutputFailure("invalid-eigen-result-state",
|
||||||
|
"Backend inventory is invalid.");
|
||||||
|
}
|
||||||
|
for (const auto& control :
|
||||||
|
std::array<std::pair<std::size_t, std::int64_t>, 11>{{{0U, 0},
|
||||||
|
{1U, 16},
|
||||||
|
{2U, 12},
|
||||||
|
{3U, 20},
|
||||||
|
{4U, 1},
|
||||||
|
{5U, 1},
|
||||||
|
{13U, 0},
|
||||||
|
{26U, 1},
|
||||||
|
{27U, 1},
|
||||||
|
{49U, 0},
|
||||||
|
{63U, 0}}}) {
|
||||||
|
if (backend.base_fpm[control.first] != control.second)
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Backend controls differ from the numerical contract.");
|
||||||
|
}
|
||||||
|
std::size_t solve_calls = 0U;
|
||||||
|
bool enclosed = false;
|
||||||
|
for (std::size_t i = 0U; i < backend.backend_calls.size(); ++i) {
|
||||||
|
const auto& call = backend.backend_calls[i];
|
||||||
|
const bool count = call.kind == "count", solve = call.kind == "solve";
|
||||||
|
if (solve) ++solve_calls;
|
||||||
|
if (call.ordinal != i || (!count && !solve) || !std::isfinite(call.lower) ||
|
||||||
|
!std::isfinite(call.upper) || !(call.lower < call.upper) ||
|
||||||
|
call.allocated_m0 == 0U || call.allocated_m0 > state.finite_dimension ||
|
||||||
|
(count && call.allocated_m0 != state.finite_dimension) ||
|
||||||
|
(call.count_available && call.count > call.allocated_m0) ||
|
||||||
|
(!call.count_available && call.count != 0U) ||
|
||||||
|
(call.loop_available && call.loop < 0) ||
|
||||||
|
(!call.loop_available && call.loop != 0) ||
|
||||||
|
(call.epsout_available && !std::isfinite(call.epsout)) ||
|
||||||
|
(!call.epsout_available && call.epsout != 0.0) ||
|
||||||
|
(call.outcome != "accepted" && call.outcome != "retry" &&
|
||||||
|
call.outcome != "optional-abandoned") ||
|
||||||
|
(call.outcome == "accepted" &&
|
||||||
|
(call.info != 0 || !call.count_available)) ||
|
||||||
|
call.decision_reason.empty() ||
|
||||||
|
(enclosed && call.lower != backend.enclosure_lower)) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Backend call history is inconsistent.");
|
||||||
|
}
|
||||||
|
if (call.purpose == "total-enclosure" && call.outcome == "accepted" &&
|
||||||
|
call.count == state.finite_dimension)
|
||||||
|
enclosed = true;
|
||||||
|
}
|
||||||
|
const auto& last_call = backend.backend_calls.back();
|
||||||
|
if (!enclosed || solve_calls == 0U || solve_calls > 3U ||
|
||||||
|
last_call.kind != "solve" || last_call.outcome != "accepted" ||
|
||||||
|
last_call.count != state.eigenpair_checks.size() ||
|
||||||
|
!last_call.loop_available || !last_call.epsout_available ||
|
||||||
|
last_call.lower != backend.solve_lower ||
|
||||||
|
last_call.upper != backend.solve_upper) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state", "Final all-c solve evidence is invalid.");
|
||||||
|
}
|
||||||
|
if (!metric_ok(state.eta_gram, 1e-10) ||
|
||||||
|
!metric_ok(state.max_eta_orth, 1e-10) ||
|
||||||
|
!metric_ok(state.eta_mz, 1e-12) || !metric_ok(state.eta_kz, 1e-12) ||
|
||||||
|
!metric_ok(state.eta_pz, 1e-12) ||
|
||||||
|
state.null_checks_applicable != (state.mass_null_dimension != 0U) ||
|
||||||
|
(!state.null_checks_applicable &&
|
||||||
|
(state.eta_mz != 0.0 || state.eta_kz != 0.0 || state.eta_pz != 0.0))) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state", "Global modal verification is invalid.");
|
||||||
|
}
|
||||||
|
const auto model = AnalysisModel::Create(domain);
|
||||||
|
if (!model.HasValue()) return model.GetStatus();
|
||||||
|
const auto dofs = DofManager::Create(model.Value());
|
||||||
|
if (!dofs.HasValue()) return dofs.GetStatus();
|
||||||
|
for (std::size_t i = 0U; i < state.full_dof_count; ++i) {
|
||||||
|
const auto constrained =
|
||||||
|
static_cast<std::uint8_t>(!dofs.Value().FreeEquation(i).has_value());
|
||||||
|
if (state.constraint_mask[i] != constrained)
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state", "Constraint mask differs from Domain.");
|
||||||
|
if (constrained != 0U) {
|
||||||
|
for (std::size_t mode = 0U; mode < state.vectors.Rows(); ++mode)
|
||||||
|
if (state.vectors(mode, i) != 0.0)
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Constrained modal component is nonzero.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::size_t cluster = 1U;
|
||||||
|
for (std::size_t index = 0U; index < state.modes.size(); ++index) {
|
||||||
|
const EigenMode& mode = state.modes[index];
|
||||||
|
const double mu = mode.eigenvalue / state.backend.operator_scale;
|
||||||
|
if (index > 0U &&
|
||||||
|
mu - state.modes[index - 1U].eigenvalue / state.backend.operator_scale >
|
||||||
|
1e-8)
|
||||||
|
++cluster;
|
||||||
|
if (mode.mode_id != index + 1U || !std::isfinite(mode.eigenvalue) ||
|
||||||
|
!std::isfinite(mu) || mu < -1e-9 || mode.cluster_id != cluster ||
|
||||||
|
mode.classification != static_cast<std::uint8_t>(std::abs(mu) > 1e-9) ||
|
||||||
|
(index > 0U && mode.eigenvalue < state.modes[index - 1U].eigenvalue) ||
|
||||||
|
(mode.eigenvalue < 0.0 && mode.frequency.has_value()) ||
|
||||||
|
(mode.eigenvalue >= 0.0 && !mode.frequency.has_value()) ||
|
||||||
|
(mode.frequency &&
|
||||||
|
(!std::isfinite(*mode.frequency) || *mode.frequency < 0.0 ||
|
||||||
|
(mode.eigenvalue > 0.0 && *mode.frequency == 0.0) ||
|
||||||
|
*mode.frequency !=
|
||||||
|
(mode.eigenvalue == 0.0
|
||||||
|
? 0.0
|
||||||
|
: std::sqrt(mode.eigenvalue) / (2.0 * std::acos(-1.0)))))) {
|
||||||
|
return hdf5_internal::OutputFailure("invalid-eigen-result-state",
|
||||||
|
"Eigen mode inventory is invalid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::size_t free_count = 0U;
|
||||||
|
std::size_t null_count = 0U;
|
||||||
|
std::size_t positive_count = 0U;
|
||||||
|
for (const EigenNodalReductionRecord& record : state.nodal_reduction) {
|
||||||
|
if (record.free_count != record.null_count + record.positive_count ||
|
||||||
|
record.free_count > 6U || record.null_count > 1U ||
|
||||||
|
record.rcond_available != (record.positive_count != 0U) ||
|
||||||
|
(record.rcond_available &&
|
||||||
|
(!std::isfinite(record.rcond_inf) ||
|
||||||
|
record.rcond_inf < std::ldexp(1.0, -32) || record.rcond_inf > 1.0)) ||
|
||||||
|
(!record.rcond_available && record.rcond_inf != 0.0)) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Nodal mass-reduction evidence is invalid.");
|
||||||
|
}
|
||||||
|
free_count += record.free_count;
|
||||||
|
null_count += record.null_count;
|
||||||
|
positive_count += record.positive_count;
|
||||||
|
}
|
||||||
|
if (free_count != state.free_dof_count ||
|
||||||
|
null_count != state.mass_null_dimension ||
|
||||||
|
positive_count != state.finite_dimension) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Nodal mass-reduction counts do not match state dimensions.");
|
||||||
|
}
|
||||||
|
std::set<std::size_t> columns;
|
||||||
|
for (std::size_t index = 0U; index < state.eigenpair_checks.size(); ++index) {
|
||||||
|
const EigenpairCheck& check = state.eigenpair_checks[index];
|
||||||
|
if (check.sorted_rank != index + 1U || !std::isfinite(check.eigenvalue) ||
|
||||||
|
check.solver_column >= state.eigenpair_checks.size() ||
|
||||||
|
!columns.insert(check.solver_column).second ||
|
||||||
|
(index < state.modes.size() &&
|
||||||
|
check.eigenvalue != state.modes[index].eigenvalue) ||
|
||||||
|
(index > 0U &&
|
||||||
|
check.eigenvalue < state.eigenpair_checks[index - 1U].eigenvalue) ||
|
||||||
|
!std::isfinite(check.modal_mass) ||
|
||||||
|
std::abs(check.modal_mass - 1.0) > 1e-10 ||
|
||||||
|
!std::isfinite(check.rayleigh_quotient) ||
|
||||||
|
!std::isfinite(check.eta_r) || !std::isfinite(check.eta_w) ||
|
||||||
|
!std::isfinite(check.rho_b) || !std::isfinite(check.eta_norm) ||
|
||||||
|
!metric_ok(check.eta_r, 1e-10) || !metric_ok(check.eta_w, 1e-10) ||
|
||||||
|
!metric_ok(check.rho_b, 1e-10) || !metric_ok(check.eta_norm, 1e-10) ||
|
||||||
|
!metric_ok(check.eta_energy, 1e-10) ||
|
||||||
|
(check.backend_res_available && !std::isfinite(check.backend_res)) ||
|
||||||
|
(!check.backend_res_available && check.backend_res != 0.0)) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Eigenpair verification evidence is invalid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::uint8_t value : state.constraint_mask) {
|
||||||
|
if (value > 1U) {
|
||||||
|
return hdf5_internal::OutputFailure("invalid-eigen-result-state",
|
||||||
|
"Constraint mask must be binary.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (double value : state.prescribed_displacement) {
|
||||||
|
if (value != 0.0) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-result-state",
|
||||||
|
"Eigen prescribed displacements must be exact zero.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const Diagnostic& diagnostic : diagnostics) {
|
||||||
|
if (diagnostic.severity != Severity::kWarning) {
|
||||||
|
return hdf5_internal::OutputFailure(
|
||||||
|
"invalid-eigen-diagnostics",
|
||||||
|
"Successful eigen output stores warnings only.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Status::Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplaceStringAttribute(hid_t object, const char* name,
|
||||||
|
const std::string& value) {
|
||||||
|
if (H5Aexists(object, name) > 0) {
|
||||||
|
hdf5_internal::RequireHdf5(H5Adelete(object, name),
|
||||||
|
"Unable to replace HDF5 metadata attribute.");
|
||||||
|
}
|
||||||
|
hdf5_internal::WriteStringAttribute(object, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteEigenMetadata(hid_t file) {
|
||||||
|
hdf5_internal::Hdf5Handle metadata{
|
||||||
|
hdf5_internal::RequireHdf5Id(H5Gopen2(file, "/metadata", H5P_DEFAULT),
|
||||||
|
"Unable to open metadata."),
|
||||||
|
H5Gclose};
|
||||||
|
ReplaceStringAttribute(metadata.Get(), "feature_id",
|
||||||
|
"mitc4-shell-feast-eigen2");
|
||||||
|
ReplaceStringAttribute(metadata.Get(), "analysis_procedure",
|
||||||
|
"linear-frequency");
|
||||||
|
ReplaceStringAttribute(metadata.Get(), "coordinate_convention",
|
||||||
|
"global-cartesian; shell-local=initial-reference; "
|
||||||
|
"positive-thickness=+zeta");
|
||||||
|
hdf5_internal::WriteUint64Attribute(metadata.Get(), "eigen_schema_version",
|
||||||
|
1U);
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "mass_integration_rule",
|
||||||
|
"3x3x3-gauss");
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "stabilization_rule",
|
||||||
|
"initial-physical-rotary-trace-mean");
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "mass_variant",
|
||||||
|
"derived-lumped");
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "mass_origin",
|
||||||
|
"consistent-moment-row-sum");
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "normalization", "mass");
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "eigensolver_backend",
|
||||||
|
"MKL FEAST");
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "eigensolver_routine",
|
||||||
|
"dfeast_scsrgv");
|
||||||
|
hdf5_internal::WriteStringAttribute(
|
||||||
|
metadata.Get(), "backend_problem",
|
||||||
|
"scaled-whitened-lumped-with-identity-mass");
|
||||||
|
hdf5_internal::WriteStringAttribute(metadata.Get(), "numerical_contract",
|
||||||
|
"mitc4-shell-feast-eigen2:r1");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteStep(hid_t file, const Domain& domain,
|
||||||
|
const EigenAnalysisState& state) {
|
||||||
|
const FrequencyStepDefinition& source = *domain.Frequency();
|
||||||
|
auto step = hdf5_internal::CreateGroup(file, "/steps/Step-1");
|
||||||
|
hdf5_internal::WriteStringAttribute(step.Get(), "source_step_name",
|
||||||
|
source.source_step_name_raw);
|
||||||
|
hdf5_internal::WriteStringAttribute(step.Get(), "source_step_name_canonical",
|
||||||
|
source.source_step_name_canonical);
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "step_source_line",
|
||||||
|
source.step_location.line);
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "frequency_source_line",
|
||||||
|
source.frequency_location.line);
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "frequency_data_line",
|
||||||
|
source.data_location.line);
|
||||||
|
hdf5_internal::WriteUint8Attribute(step.Get(), "source_nlgeom", 0U);
|
||||||
|
hdf5_internal::WriteUint8Attribute(
|
||||||
|
step.Get(), "source_perturbation_present",
|
||||||
|
static_cast<std::uint8_t>(source.perturbation_explicit));
|
||||||
|
const std::array<std::uint8_t, 4> presence{
|
||||||
|
static_cast<std::uint8_t>(source.eigensolver_explicit),
|
||||||
|
static_cast<std::uint8_t>(source.sim_explicit),
|
||||||
|
static_cast<std::uint8_t>(source.acoustic_coupling_explicit),
|
||||||
|
static_cast<std::uint8_t>(source.normalization_explicit)};
|
||||||
|
auto presence_space = hdf5_internal::CreateDatasetSpace({4U});
|
||||||
|
hdf5_internal::Hdf5Handle presence_attr{
|
||||||
|
hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Acreate2(step.Get(), "source_option_presence", H5T_STD_U8LE,
|
||||||
|
presence_space.Get(), H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
"Unable to create source option presence."),
|
||||||
|
H5Aclose};
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Awrite(presence_attr.Get(), H5T_NATIVE_UINT8, presence.data()),
|
||||||
|
"Unable to write source option presence.");
|
||||||
|
hdf5_internal::WriteStringAttribute(step.Get(), "source_eigensolver",
|
||||||
|
"LANCZOS");
|
||||||
|
hdf5_internal::WriteStringAttribute(step.Get(), "source_sim", "YES");
|
||||||
|
hdf5_internal::WriteStringAttribute(step.Get(), "source_acoustic_coupling",
|
||||||
|
"ON");
|
||||||
|
hdf5_internal::WriteStringAttribute(step.Get(), "source_normalization",
|
||||||
|
"MASS");
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "requested_mode_count",
|
||||||
|
state.modes.size());
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "returned_mode_count",
|
||||||
|
state.modes.size());
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "full_dof_count",
|
||||||
|
state.full_dof_count);
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "free_dof_count",
|
||||||
|
state.free_dof_count);
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "mass_null_dimension",
|
||||||
|
state.mass_null_dimension);
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "finite_dimension",
|
||||||
|
state.finite_dimension);
|
||||||
|
hdf5_internal::WriteUint64Attribute(step.Get(), "validated_eigenpair_count",
|
||||||
|
state.eigenpair_checks.size());
|
||||||
|
hdf5_internal::WriteStringAttribute(step.Get(), "frame_indexing",
|
||||||
|
"mode-rank-one-based");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModeRow {
|
||||||
|
std::uint64_t mode_id;
|
||||||
|
double eigenvalue;
|
||||||
|
std::uint8_t classification;
|
||||||
|
std::uint64_t cluster_id;
|
||||||
|
};
|
||||||
|
struct FrequencyRow {
|
||||||
|
std::uint64_t mode_id;
|
||||||
|
double frequency;
|
||||||
|
};
|
||||||
|
struct BackendCallRow {
|
||||||
|
std::uint64_t ordinal;
|
||||||
|
const char* kind;
|
||||||
|
const char* purpose;
|
||||||
|
double lower;
|
||||||
|
double upper;
|
||||||
|
std::uint64_t allocated_m0;
|
||||||
|
std::int64_t info;
|
||||||
|
std::uint8_t count_available;
|
||||||
|
std::uint64_t count;
|
||||||
|
std::uint8_t loop_available;
|
||||||
|
std::uint64_t loop;
|
||||||
|
std::uint8_t epsout_available;
|
||||||
|
double epsout;
|
||||||
|
const char* outcome;
|
||||||
|
const char* decision_reason;
|
||||||
|
};
|
||||||
|
struct DensityRow {
|
||||||
|
std::uint64_t material_internal_id;
|
||||||
|
double rho;
|
||||||
|
const char* source_file;
|
||||||
|
std::uint64_t source_line;
|
||||||
|
};
|
||||||
|
struct NodalReductionRow {
|
||||||
|
std::uint8_t free_count;
|
||||||
|
std::uint8_t null_count;
|
||||||
|
std::uint8_t positive_count;
|
||||||
|
std::uint8_t rcond_available;
|
||||||
|
double rcond_inf;
|
||||||
|
};
|
||||||
|
struct EigenpairCheckRow {
|
||||||
|
std::uint64_t sorted_rank;
|
||||||
|
std::uint64_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;
|
||||||
|
std::uint8_t backend_res_available;
|
||||||
|
double backend_res;
|
||||||
|
};
|
||||||
|
struct DiagnosticRow {
|
||||||
|
const char* severity;
|
||||||
|
const char* code;
|
||||||
|
const char* file;
|
||||||
|
std::uint64_t line;
|
||||||
|
const char* keyword;
|
||||||
|
const char* entity_identity;
|
||||||
|
const char* message;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Row>
|
||||||
|
hdf5_internal::Hdf5Handle MakeCompoundType() {
|
||||||
|
return hdf5_internal::Hdf5Handle{
|
||||||
|
hdf5_internal::RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(Row)),
|
||||||
|
"Unable to create compound type."),
|
||||||
|
H5Tclose};
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteModes(hid_t file, const EigenAnalysisState& state,
|
||||||
|
const std::string& source_step) {
|
||||||
|
std::vector<ModeRow> modes;
|
||||||
|
std::vector<FrequencyRow> frequencies;
|
||||||
|
modes.reserve(state.modes.size());
|
||||||
|
for (const EigenMode& mode : state.modes) {
|
||||||
|
modes.push_back(
|
||||||
|
{mode.mode_id, mode.eigenvalue, mode.classification, mode.cluster_id});
|
||||||
|
if (mode.frequency) frequencies.push_back({mode.mode_id, *mode.frequency});
|
||||||
|
}
|
||||||
|
auto mode_file = MakeCompoundType<ModeRow>();
|
||||||
|
auto mode_memory = MakeCompoundType<ModeRow>();
|
||||||
|
for (hid_t type : {mode_file.Get(), mode_memory.Get()}) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "mode_id", HOFFSET(ModeRow, mode_id), H5T_STD_U64LE),
|
||||||
|
"Unable to define mode ID.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "eigenvalue", HOFFSET(ModeRow, eigenvalue),
|
||||||
|
H5T_IEEE_F64LE),
|
||||||
|
"Unable to define eigenvalue.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "classification", HOFFSET(ModeRow, classification),
|
||||||
|
H5T_STD_U8LE),
|
||||||
|
"Unable to define classification.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "cluster_id", HOFFSET(ModeRow, cluster_id),
|
||||||
|
H5T_STD_U64LE),
|
||||||
|
"Unable to define cluster ID.");
|
||||||
|
}
|
||||||
|
auto dataset = hdf5_internal::WriteCompoundDataset(
|
||||||
|
file, "/steps/Step-1/eigen/modes", modes.size(), mode_file.Get(),
|
||||||
|
mode_memory.Get(), modes.data());
|
||||||
|
WriteQuantityAttributes(dataset.Get(), "Value", "time^-2", "scalar",
|
||||||
|
"modal-frame");
|
||||||
|
WriteModalAttributes(dataset.Get(), source_step);
|
||||||
|
hdf5_internal::WriteStringAttribute(dataset.Get(), "quantity", "eigenvalue");
|
||||||
|
auto frequency_file = MakeCompoundType<FrequencyRow>();
|
||||||
|
auto frequency_memory = MakeCompoundType<FrequencyRow>();
|
||||||
|
for (hid_t type : {frequency_file.Get(), frequency_memory.Get()}) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "mode_id", HOFFSET(FrequencyRow, mode_id),
|
||||||
|
H5T_STD_U64LE),
|
||||||
|
"Unable to define frequency mode ID.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "frequency", HOFFSET(FrequencyRow, frequency),
|
||||||
|
H5T_IEEE_F64LE),
|
||||||
|
"Unable to define frequency.");
|
||||||
|
}
|
||||||
|
auto frequency_dataset = hdf5_internal::WriteCompoundDataset(
|
||||||
|
file, "/steps/Step-1/eigen/frequencies", frequencies.size(),
|
||||||
|
frequency_file.Get(), frequency_memory.Get(), frequencies.data());
|
||||||
|
WriteQuantityAttributes(frequency_dataset.Get(), "Freq", "time^-1", "scalar",
|
||||||
|
"modal-frame");
|
||||||
|
WriteModalAttributes(frequency_dataset.Get(), source_step);
|
||||||
|
hdf5_internal::WriteStringAttribute(frequency_dataset.Get(), "quantity",
|
||||||
|
"cycle-frequency");
|
||||||
|
hdf5_internal::WriteStringAttribute(
|
||||||
|
frequency_dataset.Get(), "unavailable_rule", "negative-raw-eigenvalue");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteBaseFpm(hid_t file, const std::vector<std::int64_t>& values) {
|
||||||
|
const std::vector<hsize_t> dimensions{static_cast<hsize_t>(values.size())};
|
||||||
|
auto space = hdf5_internal::CreateDatasetSpace(dimensions);
|
||||||
|
hdf5_internal::Hdf5Handle dataset{
|
||||||
|
hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Dcreate2(file, "/steps/Step-1/eigen/backend/base_fpm",
|
||||||
|
H5T_STD_I64LE, space.Get(), H5P_DEFAULT, H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT),
|
||||||
|
"Unable to create eigen backend control dataset."),
|
||||||
|
H5Dclose};
|
||||||
|
if (!values.empty()) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Dwrite(dataset.Get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||||
|
values.data()),
|
||||||
|
"Unable to write eigen backend control dataset.");
|
||||||
|
}
|
||||||
|
WriteQuantityAttributes(dataset.Get(), "fpm", "1", "none",
|
||||||
|
"backend-parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteBackendCalls(hid_t file, const std::vector<FeastBackendCall>& calls) {
|
||||||
|
std::vector<BackendCallRow> rows;
|
||||||
|
rows.reserve(calls.size());
|
||||||
|
for (const FeastBackendCall& call : calls) {
|
||||||
|
rows.push_back({call.ordinal, call.kind.c_str(), call.purpose.c_str(),
|
||||||
|
call.lower, call.upper, call.allocated_m0, call.info,
|
||||||
|
static_cast<std::uint8_t>(call.count_available), call.count,
|
||||||
|
static_cast<std::uint8_t>(call.loop_available),
|
||||||
|
static_cast<std::uint64_t>(call.loop),
|
||||||
|
static_cast<std::uint8_t>(call.epsout_available),
|
||||||
|
call.epsout, call.outcome.c_str(),
|
||||||
|
call.decision_reason.c_str()});
|
||||||
|
}
|
||||||
|
auto string_type = hdf5_internal::MakeUtf8StringType();
|
||||||
|
auto file_type = MakeCompoundType<BackendCallRow>();
|
||||||
|
auto memory_type = MakeCompoundType<BackendCallRow>();
|
||||||
|
const auto add_fields = [&](hid_t type, hid_t u64, hid_t i64, hid_t f64) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "ordinal", HOFFSET(BackendCallRow, ordinal), u64),
|
||||||
|
"Unable to define backend ordinal.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "kind", HOFFSET(BackendCallRow, kind),
|
||||||
|
string_type.Get()),
|
||||||
|
"Unable to define backend kind.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "purpose", HOFFSET(BackendCallRow, purpose),
|
||||||
|
string_type.Get()),
|
||||||
|
"Unable to define backend purpose.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "lower", HOFFSET(BackendCallRow, lower), f64),
|
||||||
|
"Unable to define backend lower.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "upper", HOFFSET(BackendCallRow, upper), f64),
|
||||||
|
"Unable to define backend upper.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "allocated_m0", HOFFSET(BackendCallRow, allocated_m0),
|
||||||
|
u64),
|
||||||
|
"Unable to define backend capacity.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "info", HOFFSET(BackendCallRow, info), i64),
|
||||||
|
"Unable to define backend info.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "count_available",
|
||||||
|
HOFFSET(BackendCallRow, count_available), H5T_STD_U8LE),
|
||||||
|
"Unable to define count flag.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "count", HOFFSET(BackendCallRow, count), u64),
|
||||||
|
"Unable to define count.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "loop_available",
|
||||||
|
HOFFSET(BackendCallRow, loop_available), H5T_STD_U8LE),
|
||||||
|
"Unable to define loop flag.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "loop", HOFFSET(BackendCallRow, loop), u64),
|
||||||
|
"Unable to define loop.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "epsout_available",
|
||||||
|
HOFFSET(BackendCallRow, epsout_available), H5T_STD_U8LE),
|
||||||
|
"Unable to define epsout flag.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "epsout", HOFFSET(BackendCallRow, epsout), f64),
|
||||||
|
"Unable to define epsout.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "outcome", HOFFSET(BackendCallRow, outcome),
|
||||||
|
string_type.Get()),
|
||||||
|
"Unable to define outcome.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "decision_reason",
|
||||||
|
HOFFSET(BackendCallRow, decision_reason), string_type.Get()),
|
||||||
|
"Unable to define decision reason.");
|
||||||
|
};
|
||||||
|
add_fields(file_type.Get(), H5T_STD_U64LE, H5T_STD_I64LE, H5T_IEEE_F64LE);
|
||||||
|
add_fields(memory_type.Get(), H5T_NATIVE_UINT64, H5T_NATIVE_INT64,
|
||||||
|
H5T_NATIVE_DOUBLE);
|
||||||
|
auto dataset = hdf5_internal::WriteCompoundDataset(
|
||||||
|
file, "/steps/Step-1/eigen/backend/calls", rows.size(), file_type.Get(),
|
||||||
|
memory_type.Get(), rows.data());
|
||||||
|
WriteQuantityAttributes(dataset.Get(), "lower,upper,epsout", "1,1,1",
|
||||||
|
"scalar", "backend-call");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteBackendAttributes(hid_t file, const EigenAnalysisState& state) {
|
||||||
|
hdf5_internal::Hdf5Handle backend{
|
||||||
|
hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Gopen2(file, "/steps/Step-1/eigen/backend", H5P_DEFAULT),
|
||||||
|
"Unable to open eigen backend group."),
|
||||||
|
H5Gclose};
|
||||||
|
const FeastEigenResult& result = state.backend;
|
||||||
|
hdf5_internal::WriteStringAttribute(backend.Get(), "runtime_version",
|
||||||
|
result.runtime_version);
|
||||||
|
hdf5_internal::WriteStringAttribute(backend.Get(), "integer_abi",
|
||||||
|
result.integer_abi);
|
||||||
|
hdf5_internal::WriteUint64Attribute(backend.Get(), "thread_count",
|
||||||
|
result.thread_count);
|
||||||
|
hdf5_internal::WriteUint64Attribute(backend.Get(), "seed", result.seed);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(backend.Get(), "enclosure_lower",
|
||||||
|
result.enclosure_lower);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(backend.Get(), "enclosure_upper",
|
||||||
|
result.enclosure_upper);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(backend.Get(), "solve_lower",
|
||||||
|
result.solve_lower);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(backend.Get(), "solve_upper",
|
||||||
|
result.solve_upper);
|
||||||
|
std::size_t count_calls = 0U;
|
||||||
|
std::size_t solve_calls = 0U;
|
||||||
|
std::size_t final_solve_ordinal = 0U;
|
||||||
|
for (const FeastBackendCall& call : result.backend_calls) {
|
||||||
|
if (call.kind == "count") ++count_calls;
|
||||||
|
if (call.kind == "solve") {
|
||||||
|
++solve_calls;
|
||||||
|
final_solve_ordinal = call.ordinal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hdf5_internal::WriteUint64Attribute(backend.Get(), "count_calls",
|
||||||
|
count_calls);
|
||||||
|
hdf5_internal::WriteUint64Attribute(backend.Get(), "solve_calls",
|
||||||
|
solve_calls);
|
||||||
|
hdf5_internal::WriteUint64Attribute(backend.Get(), "final_solve_ordinal",
|
||||||
|
final_solve_ordinal);
|
||||||
|
hdf5_internal::WriteUint64Attribute(backend.Get(), "final_count",
|
||||||
|
state.eigenpair_checks.size());
|
||||||
|
hdf5_internal::WriteStringAttribute(backend.Get(), "upper_search_termination",
|
||||||
|
result.upper_search_termination);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteDensities(hid_t file, const Domain& domain) {
|
||||||
|
std::vector<DensityRow> rows;
|
||||||
|
std::vector<std::string> source_paths;
|
||||||
|
rows.reserve(domain.Densities().size());
|
||||||
|
source_paths.reserve(domain.Densities().size());
|
||||||
|
for (const MaterialDensityDefinition& density : domain.Densities()) {
|
||||||
|
source_paths.push_back(
|
||||||
|
hdf5_internal::NormalizedPathString(density.location.file));
|
||||||
|
rows.push_back({density.material_index, density.rho,
|
||||||
|
source_paths.back().c_str(), density.location.line});
|
||||||
|
}
|
||||||
|
auto string_type = hdf5_internal::MakeUtf8StringType();
|
||||||
|
auto file_type = MakeCompoundType<DensityRow>();
|
||||||
|
auto memory_type = MakeCompoundType<DensityRow>();
|
||||||
|
const auto add_fields = [&](hid_t type, hid_t u64, hid_t f64) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "material_internal_id",
|
||||||
|
HOFFSET(DensityRow, material_internal_id), u64),
|
||||||
|
"Unable to define density material ID.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "rho", HOFFSET(DensityRow, rho), f64),
|
||||||
|
"Unable to define density value.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "source_file", HOFFSET(DensityRow, source_file),
|
||||||
|
string_type.Get()),
|
||||||
|
"Unable to define density source file.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "source_line", HOFFSET(DensityRow, source_line), u64),
|
||||||
|
"Unable to define density source line.");
|
||||||
|
};
|
||||||
|
add_fields(file_type.Get(), H5T_STD_U64LE, H5T_IEEE_F64LE);
|
||||||
|
add_fields(memory_type.Get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE);
|
||||||
|
auto dataset = hdf5_internal::WriteCompoundDataset(
|
||||||
|
file, "/model/shell/densities", rows.size(), file_type.Get(),
|
||||||
|
memory_type.Get(), rows.data());
|
||||||
|
WriteQuantityAttributes(dataset.Get(), "rho", "mass/length^3", "scalar",
|
||||||
|
"model-material");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteVerification(hid_t file, const EigenAnalysisState& state) {
|
||||||
|
auto verification =
|
||||||
|
hdf5_internal::CreateGroup(file, "/steps/Step-1/eigen/verification");
|
||||||
|
hdf5_internal::WriteDoubleAttribute(
|
||||||
|
verification.Get(), "characteristic_length", state.characteristic_length);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(verification.Get(), "operator_scale",
|
||||||
|
state.backend.operator_scale);
|
||||||
|
hdf5_internal::WriteStringAttribute(verification.Get(), "validation_scope",
|
||||||
|
"all-computed-c-columns");
|
||||||
|
hdf5_internal::WriteDoubleAttribute(verification.Get(), "eta_gram",
|
||||||
|
state.eta_gram);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(verification.Get(), "max_eta_orth",
|
||||||
|
state.max_eta_orth);
|
||||||
|
hdf5_internal::WriteUint8Attribute(
|
||||||
|
verification.Get(), "null_checks_applicable",
|
||||||
|
static_cast<std::uint8_t>(state.null_checks_applicable));
|
||||||
|
hdf5_internal::WriteDoubleAttribute(verification.Get(), "eta_mz",
|
||||||
|
state.eta_mz);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(verification.Get(), "eta_kz",
|
||||||
|
state.eta_kz);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(verification.Get(), "eta_pz",
|
||||||
|
state.eta_pz);
|
||||||
|
|
||||||
|
std::vector<NodalReductionRow> reductions;
|
||||||
|
reductions.reserve(state.nodal_reduction.size());
|
||||||
|
for (const EigenNodalReductionRecord& record : state.nodal_reduction) {
|
||||||
|
reductions.push_back(
|
||||||
|
{record.free_count, record.null_count, record.positive_count,
|
||||||
|
static_cast<std::uint8_t>(record.rcond_available), record.rcond_inf});
|
||||||
|
}
|
||||||
|
auto reduction_file = MakeCompoundType<NodalReductionRow>();
|
||||||
|
auto reduction_memory = MakeCompoundType<NodalReductionRow>();
|
||||||
|
const auto add_reduction = [](hid_t type, hid_t f64) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "free_count", HOFFSET(NodalReductionRow, free_count),
|
||||||
|
H5T_STD_U8LE),
|
||||||
|
"Unable to define free count.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "null_count", HOFFSET(NodalReductionRow, null_count),
|
||||||
|
H5T_STD_U8LE),
|
||||||
|
"Unable to define null count.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "positive_count",
|
||||||
|
HOFFSET(NodalReductionRow, positive_count), H5T_STD_U8LE),
|
||||||
|
"Unable to define positive count.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "rcond_available",
|
||||||
|
HOFFSET(NodalReductionRow, rcond_available), H5T_STD_U8LE),
|
||||||
|
"Unable to define rcond availability.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "rcond_inf", HOFFSET(NodalReductionRow, rcond_inf),
|
||||||
|
f64),
|
||||||
|
"Unable to define rcond.");
|
||||||
|
};
|
||||||
|
add_reduction(reduction_file.Get(), H5T_IEEE_F64LE);
|
||||||
|
add_reduction(reduction_memory.Get(), H5T_NATIVE_DOUBLE);
|
||||||
|
auto reduction_dataset = hdf5_internal::WriteCompoundDataset(
|
||||||
|
file, "/steps/Step-1/eigen/verification/nodal_reduction",
|
||||||
|
reductions.size(), reduction_file.Get(), reduction_memory.Get(),
|
||||||
|
reductions.data());
|
||||||
|
WriteQuantityAttributes(reduction_dataset.Get(), "rcond_inf", "1", "scalar",
|
||||||
|
"nodal-mass-reduction");
|
||||||
|
|
||||||
|
std::vector<EigenpairCheckRow> checks;
|
||||||
|
checks.reserve(state.eigenpair_checks.size());
|
||||||
|
for (const EigenpairCheck& check : state.eigenpair_checks) {
|
||||||
|
checks.push_back({check.sorted_rank, check.solver_column, check.eigenvalue,
|
||||||
|
check.modal_mass, check.rayleigh_quotient, check.eta_r,
|
||||||
|
check.eta_w, check.rho_b, check.eta_norm,
|
||||||
|
check.eta_energy,
|
||||||
|
static_cast<std::uint8_t>(check.backend_res_available),
|
||||||
|
check.backend_res});
|
||||||
|
}
|
||||||
|
auto check_file = MakeCompoundType<EigenpairCheckRow>();
|
||||||
|
auto check_memory = MakeCompoundType<EigenpairCheckRow>();
|
||||||
|
const auto add_check = [](hid_t type, hid_t u64, hid_t f64) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "sorted_rank", HOFFSET(EigenpairCheckRow, sorted_rank),
|
||||||
|
u64),
|
||||||
|
"Unable to define sorted rank.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "solver_column",
|
||||||
|
HOFFSET(EigenpairCheckRow, solver_column), u64),
|
||||||
|
"Unable to define solver column.");
|
||||||
|
for (const auto& field : std::array<std::pair<const char*, std::size_t>, 8>{
|
||||||
|
{{"eigenvalue", HOFFSET(EigenpairCheckRow, eigenvalue)},
|
||||||
|
{"modal_mass", HOFFSET(EigenpairCheckRow, modal_mass)},
|
||||||
|
{"rayleigh_quotient",
|
||||||
|
HOFFSET(EigenpairCheckRow, rayleigh_quotient)},
|
||||||
|
{"eta_r", HOFFSET(EigenpairCheckRow, eta_r)},
|
||||||
|
{"eta_w", HOFFSET(EigenpairCheckRow, eta_w)},
|
||||||
|
{"rho_b", HOFFSET(EigenpairCheckRow, rho_b)},
|
||||||
|
{"eta_norm", HOFFSET(EigenpairCheckRow, eta_norm)},
|
||||||
|
{"eta_energy", HOFFSET(EigenpairCheckRow, eta_energy)}}}) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, field.first, field.second, f64),
|
||||||
|
"Unable to define eigenpair metric.");
|
||||||
|
}
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "backend_res_available",
|
||||||
|
HOFFSET(EigenpairCheckRow, backend_res_available),
|
||||||
|
H5T_STD_U8LE),
|
||||||
|
"Unable to define backend residual flag.");
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "backend_res", HOFFSET(EigenpairCheckRow, backend_res),
|
||||||
|
f64),
|
||||||
|
"Unable to define backend residual.");
|
||||||
|
};
|
||||||
|
add_check(check_file.Get(), H5T_STD_U64LE, H5T_IEEE_F64LE);
|
||||||
|
add_check(check_memory.Get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE);
|
||||||
|
auto check_dataset = hdf5_internal::WriteCompoundDataset(
|
||||||
|
file, "/steps/Step-1/eigen/verification/eigenpair_checks", checks.size(),
|
||||||
|
check_file.Get(), check_memory.Get(), checks.data());
|
||||||
|
WriteQuantityAttributes(check_dataset.Get(),
|
||||||
|
"eigenvalue,modal_mass,rayleigh_quotient,eta_r,"
|
||||||
|
"eta_w,rho_b,eta_norm,eta_energy,backend_res",
|
||||||
|
"time^-2,1,time^-2,1,1,1,1,1,1", "scalar",
|
||||||
|
"eigenpair-verification");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteDiagnostics(hid_t file, const std::vector<Diagnostic>& diagnostics) {
|
||||||
|
std::vector<std::string> files;
|
||||||
|
files.reserve(diagnostics.size());
|
||||||
|
std::vector<DiagnosticRow> rows;
|
||||||
|
rows.reserve(diagnostics.size());
|
||||||
|
for (const Diagnostic& diagnostic : diagnostics) {
|
||||||
|
files.push_back(
|
||||||
|
hdf5_internal::NormalizedPathString(diagnostic.location.file));
|
||||||
|
rows.push_back({"warning", diagnostic.code.c_str(), files.back().c_str(),
|
||||||
|
diagnostic.location.line, diagnostic.keyword.c_str(),
|
||||||
|
diagnostic.entity_identity.c_str(),
|
||||||
|
diagnostic.message.c_str()});
|
||||||
|
}
|
||||||
|
auto string_type = hdf5_internal::MakeUtf8StringType();
|
||||||
|
auto file_type = MakeCompoundType<DiagnosticRow>();
|
||||||
|
auto memory_type = MakeCompoundType<DiagnosticRow>();
|
||||||
|
const auto add_fields = [&](hid_t type, hid_t u64) {
|
||||||
|
for (const auto& field : std::array<std::pair<const char*, std::size_t>, 6>{
|
||||||
|
{{"severity", HOFFSET(DiagnosticRow, severity)},
|
||||||
|
{"code", HOFFSET(DiagnosticRow, code)},
|
||||||
|
{"file", HOFFSET(DiagnosticRow, file)},
|
||||||
|
{"keyword", HOFFSET(DiagnosticRow, keyword)},
|
||||||
|
{"entity_identity", HOFFSET(DiagnosticRow, entity_identity)},
|
||||||
|
{"message", HOFFSET(DiagnosticRow, message)}}}) {
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, field.first, field.second, string_type.Get()),
|
||||||
|
"Unable to define diagnostic text field.");
|
||||||
|
}
|
||||||
|
hdf5_internal::RequireHdf5(
|
||||||
|
H5Tinsert(type, "line", HOFFSET(DiagnosticRow, line), u64),
|
||||||
|
"Unable to define diagnostic line.");
|
||||||
|
};
|
||||||
|
add_fields(file_type.Get(), H5T_STD_U64LE);
|
||||||
|
add_fields(memory_type.Get(), H5T_NATIVE_UINT64);
|
||||||
|
(void)hdf5_internal::WriteCompoundDataset(file, "/diagnostics", rows.size(),
|
||||||
|
file_type.Get(), memory_type.Get(),
|
||||||
|
rows.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Status EigenHdf5ResultsWriter::Write(
|
||||||
|
const std::filesystem::path& output_path, const Domain& domain,
|
||||||
|
const EigenAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) {
|
||||||
|
const Status valid = Validate(domain, state, diagnostics);
|
||||||
|
if (!valid.IsOk()) return valid;
|
||||||
|
try {
|
||||||
|
hdf5_internal::Hdf5ErrorSilencer silence;
|
||||||
|
const auto candidate_path = hdf5_internal::MakeCandidatePath(output_path);
|
||||||
|
hdf5_internal::CandidateFileGuard cleanup{candidate_path};
|
||||||
|
hdf5_internal::Hdf5Handle file{
|
||||||
|
hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Fcreate(candidate_path.string().c_str(), H5F_ACC_EXCL,
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
"Unable to create eigen candidate."),
|
||||||
|
H5Fclose};
|
||||||
|
hdf5_internal::WriterModelData model{
|
||||||
|
{}, state.constraint_mask, state.prescribed_displacement};
|
||||||
|
(void)hdf5_internal::CreateGroup(file.Get(), "/model");
|
||||||
|
(void)hdf5_internal::CreateGroup(file.Get(), "/model/shell");
|
||||||
|
hdf5_internal::WriteModel(file.Get(), domain, model);
|
||||||
|
WriteDensities(file.Get(), domain);
|
||||||
|
WriteDiagnostics(file.Get(), diagnostics);
|
||||||
|
WriteEigenMetadata(file.Get());
|
||||||
|
WriteStep(file.Get(), domain, state);
|
||||||
|
(void)hdf5_internal::CreateGroup(file.Get(), kEigenRoot);
|
||||||
|
WriteModes(file.Get(), state, domain.Frequency()->source_step_name_raw);
|
||||||
|
std::vector<double> vector_values;
|
||||||
|
vector_values.reserve(state.vectors.Rows() * state.vectors.Columns());
|
||||||
|
for (std::size_t row = 0U; row < state.vectors.Rows(); ++row)
|
||||||
|
for (std::size_t column = 0U; column < state.vectors.Columns(); ++column)
|
||||||
|
vector_values.push_back(state.vectors(row, column));
|
||||||
|
hdf5_internal::WriteModelDoubleDataset(
|
||||||
|
file.Get(), "/steps/Step-1/eigen/vectors",
|
||||||
|
{static_cast<hsize_t>(state.modes.size()),
|
||||||
|
static_cast<hsize_t>(domain.Nodes().size()), 6U},
|
||||||
|
vector_values.data(), vector_values.size(), "UX,UY,UZ,URX,URY,URZ",
|
||||||
|
"mass^-1/2,mass^-1/2,mass^-1/2,mass^-1/2*length^-1,mass^-1/"
|
||||||
|
"2*length^-1,mass^-1/2*length^-1",
|
||||||
|
"global-cartesian", "nodal-modal-vector");
|
||||||
|
{
|
||||||
|
hdf5_internal::Hdf5Handle vectors{
|
||||||
|
hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Dopen2(file.Get(), "/steps/Step-1/eigen/vectors", H5P_DEFAULT),
|
||||||
|
"Unable to open modal vectors."),
|
||||||
|
H5Dclose};
|
||||||
|
WriteModalAttributes(vectors.Get(),
|
||||||
|
domain.Frequency()->source_step_name_raw);
|
||||||
|
hdf5_internal::WriteStringAttribute(vectors.Get(), "normalization",
|
||||||
|
"mass");
|
||||||
|
hdf5_internal::WriteStringAttribute(vectors.Get(), "mass_variant",
|
||||||
|
"derived-lumped");
|
||||||
|
hdf5_internal::WriteStringAttribute(
|
||||||
|
vectors.Get(), "interpretation",
|
||||||
|
"normalized-mode-not-response-displacement");
|
||||||
|
}
|
||||||
|
WriteVerification(file.Get(), state);
|
||||||
|
(void)hdf5_internal::CreateGroup(file.Get(), "/steps/Step-1/eigen/backend");
|
||||||
|
WriteBackendAttributes(file.Get(), state);
|
||||||
|
WriteBaseFpm(file.Get(), state.backend.base_fpm);
|
||||||
|
WriteBackendCalls(file.Get(), state.backend.backend_calls);
|
||||||
|
hdf5_internal::RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL),
|
||||||
|
"Unable to flush eigen candidate.");
|
||||||
|
hdf5_internal::RequireHdf5(file.CloseChecked(),
|
||||||
|
"Unable to close eigen candidate.");
|
||||||
|
const Status checked = hdf5_internal::SelfCheckEigenFile(
|
||||||
|
candidate_path, domain, state, diagnostics);
|
||||||
|
const Status result = hdf5_internal::AtomicFinalizeValidatedCandidate(
|
||||||
|
candidate_path, output_path, checked);
|
||||||
|
if (result.IsOk()) cleanup.Release();
|
||||||
|
return result;
|
||||||
|
} catch (const hdf5_internal::Hdf5Failure& failure) {
|
||||||
|
return hdf5_internal::OutputFailure("hdf5-write-failure", failure.what());
|
||||||
|
} catch (const std::exception& failure) {
|
||||||
|
return hdf5_internal::OutputFailure("hdf5-write-failure", failure.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,827 @@
|
|||||||
|
#include "io/hdf5/eigen_hdf5_self_check.h"
|
||||||
|
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <numeric>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/build_info.h"
|
||||||
|
#include "io/hdf5/hdf5_primitives.h"
|
||||||
|
#include "io/hdf5/hdf5_raii.h"
|
||||||
|
|
||||||
|
namespace fesa::hdf5_internal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Status Failure(const std::string& message) {
|
||||||
|
return OutputFailure("invalid-eigen-hdf5-candidate", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasPath(hid_t file, const char* path) {
|
||||||
|
return H5Lexists(file, path, H5P_DEFAULT) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasNoStaticResults(hid_t file) {
|
||||||
|
for (const char* path : {"/steps/Step-1/frames", "/steps/Step-1/global",
|
||||||
|
"/steps/Step-1/element"}) {
|
||||||
|
if (HasPath(file, path)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadUint64Attribute(hid_t object, const char* name,
|
||||||
|
std::uint64_t expected) {
|
||||||
|
if (H5Aexists(object, name) <= 0) return false;
|
||||||
|
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
|
||||||
|
if (attribute.Get() < 0) return false;
|
||||||
|
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
|
||||||
|
Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose};
|
||||||
|
if (type.Get() < 0 || H5Tequal(type.Get(), H5T_STD_U64LE) <= 0 ||
|
||||||
|
space.Get() < 0 || H5Sget_simple_extent_type(space.Get()) != H5S_SCALAR)
|
||||||
|
return false;
|
||||||
|
std::uint64_t value = 0U;
|
||||||
|
return H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value) >= 0 &&
|
||||||
|
value == expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadStringAttribute(hid_t object, const char* name, const char* expected) {
|
||||||
|
if (H5Aexists(object, name) <= 0) return false;
|
||||||
|
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
|
||||||
|
if (attribute.Get() < 0) return false;
|
||||||
|
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
|
||||||
|
Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose};
|
||||||
|
if (type.Get() < 0 || H5Tget_class(type.Get()) != H5T_STRING ||
|
||||||
|
H5Tis_variable_str(type.Get()) <= 0 ||
|
||||||
|
H5Tget_cset(type.Get()) != H5T_CSET_UTF8 || space.Get() < 0 ||
|
||||||
|
H5Sget_simple_extent_type(space.Get()) != H5S_SCALAR)
|
||||||
|
return false;
|
||||||
|
char* value = nullptr;
|
||||||
|
if (H5Aread(attribute.Get(), type.Get(), &value) < 0 || value == nullptr)
|
||||||
|
return false;
|
||||||
|
const bool matches = std::string{value} == expected;
|
||||||
|
H5free_memory(value);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatasetShape(hid_t file, const char* path,
|
||||||
|
const std::vector<hsize_t>& expected_dimensions) {
|
||||||
|
Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
|
||||||
|
if (dataset.Get() < 0) return false;
|
||||||
|
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
|
||||||
|
if (space.Get() < 0 || H5Sget_simple_extent_ndims(space.Get()) !=
|
||||||
|
static_cast<int>(expected_dimensions.size()))
|
||||||
|
return false;
|
||||||
|
std::vector<hsize_t> dimensions(expected_dimensions.size());
|
||||||
|
if (H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0)
|
||||||
|
return false;
|
||||||
|
return dimensions == expected_dimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VerifyVectors(hid_t file, const Domain& domain,
|
||||||
|
const EigenAnalysisState& state) {
|
||||||
|
const std::size_t count = state.vectors.Rows() * state.vectors.Columns();
|
||||||
|
if (!DatasetShape(file, "/steps/Step-1/eigen/vectors",
|
||||||
|
{static_cast<hsize_t>(state.modes.size()),
|
||||||
|
static_cast<hsize_t>(domain.Nodes().size()), 6U}))
|
||||||
|
return false;
|
||||||
|
std::vector<double> values(count);
|
||||||
|
Hdf5Handle dataset{H5Dopen2(file, "/steps/Step-1/eigen/vectors", H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
|
||||||
|
if (dataset.Get() < 0 || H5Tequal(type.Get(), H5T_IEEE_F64LE) <= 0 ||
|
||||||
|
(count != 0U && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL,
|
||||||
|
H5S_ALL, H5P_DEFAULT, values.data()) < 0))
|
||||||
|
return false;
|
||||||
|
for (std::size_t row = 0U; row < state.vectors.Rows(); ++row) {
|
||||||
|
for (std::size_t column = 0U; column < state.vectors.Columns(); ++column) {
|
||||||
|
const double value = values[row * state.vectors.Columns() + column];
|
||||||
|
if (!std::isfinite(value) || value != state.vectors(row, column))
|
||||||
|
return false;
|
||||||
|
if (state.constraint_mask[column] != 0U && value != 0.0) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T, class Rows, class Getter>
|
||||||
|
bool CheckMember(hid_t file, const char* path, const char* name,
|
||||||
|
hid_t file_type, hid_t memory_type, const Rows& rows,
|
||||||
|
Getter get) {
|
||||||
|
if (!DatasetShape(file, path, {static_cast<hsize_t>(rows.size())}))
|
||||||
|
return false;
|
||||||
|
Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
|
||||||
|
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
|
||||||
|
if (type.Get() < 0 || H5Tget_class(type.Get()) != H5T_COMPOUND) return false;
|
||||||
|
const int member = H5Tget_member_index(type.Get(), name);
|
||||||
|
if (member < 0) return false;
|
||||||
|
Hdf5Handle actual_type{
|
||||||
|
H5Tget_member_type(type.Get(), static_cast<unsigned>(member)), H5Tclose};
|
||||||
|
if (H5Tequal(actual_type.Get(), file_type) <= 0) return false;
|
||||||
|
Hdf5Handle memory{H5Tcreate(H5T_COMPOUND, sizeof(T)), H5Tclose};
|
||||||
|
if (memory.Get() < 0 || H5Tinsert(memory.Get(), name, 0U, memory_type) < 0)
|
||||||
|
return false;
|
||||||
|
std::vector<T> values(rows.size());
|
||||||
|
if (!values.empty() && H5Dread(dataset.Get(), memory.Get(), H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, values.data()) < 0)
|
||||||
|
return false;
|
||||||
|
for (std::size_t i = 0; i < values.size(); ++i)
|
||||||
|
if (values[i] != static_cast<T>(get(rows[i]))) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Rows, class Getter>
|
||||||
|
bool CheckTextMember(hid_t file, const char* path, const char* name,
|
||||||
|
const Rows& rows, Getter get) {
|
||||||
|
if (!DatasetShape(file, path, {static_cast<hsize_t>(rows.size())}))
|
||||||
|
return false;
|
||||||
|
Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
|
||||||
|
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
|
||||||
|
if (type.Get() < 0 || H5Tget_class(type.Get()) != H5T_COMPOUND) return false;
|
||||||
|
const int member = H5Tget_member_index(type.Get(), name);
|
||||||
|
if (member < 0) return false;
|
||||||
|
Hdf5Handle member_type{
|
||||||
|
H5Tget_member_type(type.Get(), static_cast<unsigned>(member)), H5Tclose};
|
||||||
|
if (H5Tget_class(member_type.Get()) != H5T_STRING ||
|
||||||
|
H5Tis_variable_str(member_type.Get()) <= 0 ||
|
||||||
|
H5Tget_cset(member_type.Get()) != H5T_CSET_UTF8)
|
||||||
|
return false;
|
||||||
|
Hdf5Handle memory{H5Tcreate(H5T_COMPOUND, sizeof(char*)), H5Tclose};
|
||||||
|
if (H5Tinsert(memory.Get(), name, 0U, member_type.Get()) < 0) return false;
|
||||||
|
std::vector<char*> values(rows.size(), nullptr);
|
||||||
|
if (!values.empty() && H5Dread(dataset.Get(), memory.Get(), H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, values.data()) < 0)
|
||||||
|
return false;
|
||||||
|
bool matches = true;
|
||||||
|
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||||
|
if (values[i] == nullptr || std::string(values[i]) != get(rows[i]))
|
||||||
|
matches = false;
|
||||||
|
if (values[i] != nullptr && H5free_memory(values[i]) < 0) matches = false;
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
bool CheckScalar(hid_t object, const char* name, hid_t file_type,
|
||||||
|
hid_t memory_type, T expected) {
|
||||||
|
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
|
||||||
|
if (attribute.Get() < 0) return false;
|
||||||
|
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
|
||||||
|
Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose};
|
||||||
|
if (H5Tequal(type.Get(), file_type) <= 0 ||
|
||||||
|
H5Sget_simple_extent_type(space.Get()) != H5S_SCALAR)
|
||||||
|
return false;
|
||||||
|
T value{};
|
||||||
|
return H5Aread(attribute.Get(), memory_type, &value) >= 0 &&
|
||||||
|
value == expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T, class Values>
|
||||||
|
bool CheckArray(hid_t file, const char* path, hid_t file_type,
|
||||||
|
hid_t memory_type, const std::vector<hsize_t>& shape,
|
||||||
|
const Values& expected) {
|
||||||
|
if (!DatasetShape(file, path, shape)) return false;
|
||||||
|
Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
|
||||||
|
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
|
||||||
|
if (H5Tequal(type.Get(), file_type) <= 0) return false;
|
||||||
|
std::vector<T> actual(expected.size());
|
||||||
|
if (!actual.empty() && H5Dread(dataset.Get(), memory_type, H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, actual.data()) < 0)
|
||||||
|
return false;
|
||||||
|
for (std::size_t i = 0U; i < actual.size(); ++i)
|
||||||
|
if (actual[i] != expected[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckTextAttributes(
|
||||||
|
hid_t object,
|
||||||
|
std::initializer_list<std::pair<const char*, std::string>> attributes) {
|
||||||
|
for (const auto& attribute : attributes)
|
||||||
|
if (!ReadStringAttribute(object, attribute.first, attribute.second.c_str()))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckAttributes(hid_t file, const Domain& domain,
|
||||||
|
const EigenAnalysisState& state) {
|
||||||
|
Hdf5Handle metadata{H5Gopen2(file, "/metadata", H5P_DEFAULT), H5Gclose};
|
||||||
|
if (!CheckTextAttributes(
|
||||||
|
metadata.Get(),
|
||||||
|
{{"solver_version", std::string{SolverVersion()}},
|
||||||
|
{"source_input_identity", SourceInputIdentity(domain)},
|
||||||
|
{"unit_system_label", "user-consistent-unspecified"},
|
||||||
|
{"coordinate_convention",
|
||||||
|
"global-cartesian; shell-local=initial-reference; "
|
||||||
|
"positive-thickness=+zeta"},
|
||||||
|
{"internal_formulation", "FESA-MITC4"},
|
||||||
|
{"integration_rule", "2x2x2-gauss; mitc4-edge-midpoint-shear"},
|
||||||
|
{"mass_integration_rule", "3x3x3-gauss"},
|
||||||
|
{"stabilization_rule", "initial-physical-rotary-trace-mean"},
|
||||||
|
{"mass_variant", "derived-lumped"},
|
||||||
|
{"mass_origin", "consistent-moment-row-sum"},
|
||||||
|
{"normalization", "mass"},
|
||||||
|
{"eigensolver_backend", "MKL FEAST"},
|
||||||
|
{"eigensolver_routine", "dfeast_scsrgv"},
|
||||||
|
{"backend_problem", "scaled-whitened-lumped-with-identity-mass"},
|
||||||
|
{"numerical_contract", "mitc4-shell-feast-eigen2:r1"}}))
|
||||||
|
return false;
|
||||||
|
const auto& source = *domain.Frequency();
|
||||||
|
Hdf5Handle step{H5Gopen2(file, "/steps/Step-1", H5P_DEFAULT), H5Gclose};
|
||||||
|
if (!CheckTextAttributes(
|
||||||
|
step.Get(),
|
||||||
|
{{"source_step_name", source.source_step_name_raw},
|
||||||
|
{"source_step_name_canonical", source.source_step_name_canonical},
|
||||||
|
{"source_eigensolver", "LANCZOS"},
|
||||||
|
{"source_sim", "YES"},
|
||||||
|
{"source_acoustic_coupling", "ON"},
|
||||||
|
{"source_normalization", "MASS"},
|
||||||
|
{"frame_indexing", "mode-rank-one-based"}}) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "step_source_line",
|
||||||
|
source.step_location.line) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "frequency_source_line",
|
||||||
|
source.frequency_location.line) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "frequency_data_line",
|
||||||
|
source.data_location.line) ||
|
||||||
|
!CheckScalar<std::uint8_t>(step.Get(), "source_nlgeom", H5T_STD_U8LE,
|
||||||
|
H5T_NATIVE_UINT8, 0U) ||
|
||||||
|
!CheckScalar<std::uint8_t>(
|
||||||
|
step.Get(), "source_perturbation_present", H5T_STD_U8LE,
|
||||||
|
H5T_NATIVE_UINT8,
|
||||||
|
static_cast<std::uint8_t>(source.perturbation_explicit)))
|
||||||
|
return false;
|
||||||
|
Hdf5Handle presence{
|
||||||
|
H5Aopen(step.Get(), "source_option_presence", H5P_DEFAULT), H5Aclose};
|
||||||
|
Hdf5Handle presence_type{H5Aget_type(presence.Get()), H5Tclose};
|
||||||
|
Hdf5Handle presence_space{H5Aget_space(presence.Get()), H5Sclose};
|
||||||
|
hsize_t extent = 0U;
|
||||||
|
std::array<std::uint8_t, 4> actual_presence{};
|
||||||
|
const std::array<std::uint8_t, 4> expected_presence{
|
||||||
|
static_cast<std::uint8_t>(source.eigensolver_explicit),
|
||||||
|
static_cast<std::uint8_t>(source.sim_explicit),
|
||||||
|
static_cast<std::uint8_t>(source.acoustic_coupling_explicit),
|
||||||
|
static_cast<std::uint8_t>(source.normalization_explicit)};
|
||||||
|
if (presence.Get() < 0 || H5Tequal(presence_type.Get(), H5T_STD_U8LE) <= 0 ||
|
||||||
|
H5Sget_simple_extent_ndims(presence_space.Get()) != 1 ||
|
||||||
|
H5Sget_simple_extent_dims(presence_space.Get(), &extent, nullptr) < 0 ||
|
||||||
|
extent != 4U ||
|
||||||
|
H5Aread(presence.Get(), H5T_NATIVE_UINT8, actual_presence.data()) < 0 ||
|
||||||
|
actual_presence != expected_presence)
|
||||||
|
return false;
|
||||||
|
for (const char* leaf : {"modes", "frequencies", "vectors"}) {
|
||||||
|
const std::string path = std::string{"/steps/Step-1/eigen/"} + leaf;
|
||||||
|
Hdf5Handle dataset{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
|
||||||
|
if (!ReadUint64Attribute(dataset.Get(), "eigen_schema_version", 1U) ||
|
||||||
|
!CheckTextAttributes(dataset.Get(),
|
||||||
|
{{"source_step_name", source.source_step_name_raw},
|
||||||
|
{"domain", "mode"}}))
|
||||||
|
return false;
|
||||||
|
if (std::string{leaf} == "vectors") {
|
||||||
|
if (!CheckTextAttributes(
|
||||||
|
dataset.Get(),
|
||||||
|
{{"component_names", "UX,UY,UZ,URX,URY,URZ"},
|
||||||
|
{"component_unit_dimensions",
|
||||||
|
"mass^-1/2,mass^-1/2,mass^-1/2,mass^-1/2*length^-1,mass^-1/"
|
||||||
|
"2*length^-1,mass^-1/2*length^-1"},
|
||||||
|
{"coordinate_system", "global-cartesian"},
|
||||||
|
{"location", "nodal-modal-vector"},
|
||||||
|
{"normalization", "mass"},
|
||||||
|
{"mass_variant", "derived-lumped"},
|
||||||
|
{"interpretation",
|
||||||
|
"normalized-mode-not-response-displacement"}}))
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
const bool modes = std::string{leaf} == "modes";
|
||||||
|
if (!CheckTextAttributes(
|
||||||
|
dataset.Get(),
|
||||||
|
{{"quantity", modes ? "eigenvalue" : "cycle-frequency"},
|
||||||
|
{"component_names", modes ? "Value" : "Freq"},
|
||||||
|
{"component_unit_dimensions", modes ? "time^-2" : "time^-1"},
|
||||||
|
{"coordinate_system", "scalar"},
|
||||||
|
{"location", "modal-frame"}}))
|
||||||
|
return false;
|
||||||
|
if (!modes && !ReadStringAttribute(dataset.Get(), "unavailable_rule",
|
||||||
|
"negative-raw-eigenvalue"))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Hdf5Handle verification{
|
||||||
|
H5Gopen2(file, "/steps/Step-1/eigen/verification", H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
for (const auto& metric :
|
||||||
|
std::initializer_list<std::pair<const char*, double>>{
|
||||||
|
{"characteristic_length", state.characteristic_length},
|
||||||
|
{"operator_scale", state.backend.operator_scale},
|
||||||
|
{"eta_gram", state.eta_gram},
|
||||||
|
{"max_eta_orth", state.max_eta_orth},
|
||||||
|
{"eta_mz", state.eta_mz},
|
||||||
|
{"eta_kz", state.eta_kz},
|
||||||
|
{"eta_pz", state.eta_pz}})
|
||||||
|
if (!CheckScalar<double>(verification.Get(), metric.first, H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, metric.second))
|
||||||
|
return false;
|
||||||
|
if (!ReadStringAttribute(verification.Get(), "validation_scope",
|
||||||
|
"all-computed-c-columns") ||
|
||||||
|
!CheckScalar<std::uint8_t>(
|
||||||
|
verification.Get(), "null_checks_applicable", H5T_STD_U8LE,
|
||||||
|
H5T_NATIVE_UINT8,
|
||||||
|
static_cast<std::uint8_t>(state.null_checks_applicable)))
|
||||||
|
return false;
|
||||||
|
Hdf5Handle backend{H5Gopen2(file, "/steps/Step-1/eigen/backend", H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
const auto& b = state.backend;
|
||||||
|
if (!CheckTextAttributes(
|
||||||
|
backend.Get(),
|
||||||
|
{{"runtime_version", b.runtime_version},
|
||||||
|
{"integer_abi", b.integer_abi},
|
||||||
|
{"upper_search_termination", b.upper_search_termination}}) ||
|
||||||
|
!ReadUint64Attribute(backend.Get(), "thread_count", b.thread_count) ||
|
||||||
|
!ReadUint64Attribute(backend.Get(), "seed", b.seed) ||
|
||||||
|
!ReadUint64Attribute(backend.Get(), "final_count",
|
||||||
|
state.eigenpair_checks.size()))
|
||||||
|
return false;
|
||||||
|
for (const auto& bound :
|
||||||
|
std::initializer_list<std::pair<const char*, double>>{
|
||||||
|
{"enclosure_lower", b.enclosure_lower},
|
||||||
|
{"enclosure_upper", b.enclosure_upper},
|
||||||
|
{"solve_lower", b.solve_lower},
|
||||||
|
{"solve_upper", b.solve_upper}})
|
||||||
|
if (!CheckScalar<double>(backend.Get(), bound.first, H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, bound.second))
|
||||||
|
return false;
|
||||||
|
std::size_t counts = 0U, solves = 0U, final_ordinal = 0U;
|
||||||
|
for (const auto& call : b.backend_calls) {
|
||||||
|
if (call.kind == "count") ++counts;
|
||||||
|
if (call.kind == "solve") {
|
||||||
|
++solves;
|
||||||
|
final_ordinal = call.ordinal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReadUint64Attribute(backend.Get(), "count_calls", counts) &&
|
||||||
|
ReadUint64Attribute(backend.Get(), "solve_calls", solves) &&
|
||||||
|
ReadUint64Attribute(backend.Get(), "final_solve_ordinal",
|
||||||
|
final_ordinal) &&
|
||||||
|
CheckArray<std::int64_t>(file, "/steps/Step-1/eigen/backend/base_fpm",
|
||||||
|
H5T_STD_I64LE, H5T_NATIVE_INT64, {128U},
|
||||||
|
b.base_fpm) &&
|
||||||
|
CheckArray<std::uint8_t>(
|
||||||
|
file, "/model/nodal_constraint_mask", H5T_STD_U8LE,
|
||||||
|
H5T_NATIVE_UINT8,
|
||||||
|
{static_cast<hsize_t>(domain.Nodes().size()), 6U},
|
||||||
|
state.constraint_mask) &&
|
||||||
|
CheckArray<double>(file, "/model/prescribed_displacement",
|
||||||
|
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE,
|
||||||
|
{static_cast<hsize_t>(domain.Nodes().size()), 6U},
|
||||||
|
state.prescribed_displacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckModelRows(hid_t file, const Domain& domain) {
|
||||||
|
std::vector<std::size_t> indices(domain.Nodes().size());
|
||||||
|
std::iota(indices.begin(), indices.end(), 0U);
|
||||||
|
const char* path = "/model/nodes";
|
||||||
|
const hsize_t three = 3U, four = 4U;
|
||||||
|
Hdf5Handle coordinates_file{H5Tarray_create2(H5T_IEEE_F64LE, 1, &three),
|
||||||
|
H5Tclose};
|
||||||
|
Hdf5Handle coordinates_memory{H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, &three),
|
||||||
|
H5Tclose};
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "internal_node_id", H5T_STD_U64LE,
|
||||||
|
H5T_NATIVE_UINT64, indices,
|
||||||
|
[](auto i) { return i; }) ||
|
||||||
|
!CheckTextMember(
|
||||||
|
file, path, "instance_name", domain.Nodes(),
|
||||||
|
[](const auto& row) { return row.source_id.instance_name; }) ||
|
||||||
|
!CheckTextMember(
|
||||||
|
file, path, "source_label", domain.Nodes(),
|
||||||
|
[](const auto& row) { return row.source_id.source_label_text; }) ||
|
||||||
|
!CheckMember<std::array<double, 3>>(
|
||||||
|
file, path, "coordinates", coordinates_file.Get(),
|
||||||
|
coordinates_memory.Get(), domain.Nodes(),
|
||||||
|
[](const auto& row) { return row.coordinates; }))
|
||||||
|
return false;
|
||||||
|
indices.resize(domain.ShellElements().Size());
|
||||||
|
std::iota(indices.begin(), indices.end(), 0U);
|
||||||
|
Hdf5Handle nodes_file{H5Tarray_create2(H5T_STD_U64LE, 1, &four), H5Tclose};
|
||||||
|
Hdf5Handle nodes_memory{H5Tarray_create2(H5T_NATIVE_UINT64, 1, &four),
|
||||||
|
H5Tclose};
|
||||||
|
path = "/model/elements";
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "internal_element_id",
|
||||||
|
H5T_STD_U64LE, H5T_NATIVE_UINT64, indices,
|
||||||
|
[](auto i) { return i; }) ||
|
||||||
|
!CheckTextMember(
|
||||||
|
file, path, "instance_name", indices,
|
||||||
|
[&](auto i) {
|
||||||
|
return domain.ShellElements()[i].source_id.instance_name;
|
||||||
|
}) ||
|
||||||
|
!CheckTextMember(
|
||||||
|
file, path, "source_label", indices,
|
||||||
|
[&](auto i) {
|
||||||
|
return domain.ShellElements()[i].source_id.source_label_text;
|
||||||
|
}) ||
|
||||||
|
!CheckTextMember(file, path, "source_element_type", indices,
|
||||||
|
[&](auto i) {
|
||||||
|
return ShellSourceTypeName(
|
||||||
|
domain.ShellElements()[i].source_type);
|
||||||
|
}) ||
|
||||||
|
!CheckTextMember(file, path, "internal_formulation", indices,
|
||||||
|
[](auto) { return std::string{"FESA-MITC4"}; }) ||
|
||||||
|
!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "material_internal_id", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
indices,
|
||||||
|
[&](auto i) { return domain.ShellElements()[i].material_index; }) ||
|
||||||
|
!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "shell_section_internal_id", H5T_STD_U64LE,
|
||||||
|
H5T_NATIVE_UINT64, indices,
|
||||||
|
[&](auto i) { return domain.ShellElements()[i].section_index; }) ||
|
||||||
|
!CheckMember<std::array<std::uint64_t, 4>>(
|
||||||
|
file, path, "node_internal_ids", nodes_file.Get(), nodes_memory.Get(),
|
||||||
|
indices, [&](auto i) {
|
||||||
|
const auto& nodes = domain.ShellElements()[i].node_indices;
|
||||||
|
return std::array<std::uint64_t, 4>{nodes[0], nodes[1], nodes[2],
|
||||||
|
nodes[3]};
|
||||||
|
}))
|
||||||
|
return false;
|
||||||
|
indices.resize(domain.LinearElasticMaterials().Size());
|
||||||
|
std::iota(indices.begin(), indices.end(), 0U);
|
||||||
|
path = "/model/shell/materials";
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "internal_material_id",
|
||||||
|
H5T_STD_U64LE, H5T_NATIVE_UINT64, indices,
|
||||||
|
[](auto i) { return i; }) ||
|
||||||
|
!CheckTextMember(
|
||||||
|
file, path, "name", indices,
|
||||||
|
[&](auto i) { return domain.LinearElasticMaterials()[i].name; }) ||
|
||||||
|
!CheckMember<double>(
|
||||||
|
file, path, "E", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, indices,
|
||||||
|
[&](auto i) {
|
||||||
|
return domain.LinearElasticMaterials()[i].youngs_modulus;
|
||||||
|
}) ||
|
||||||
|
!CheckMember<double>(
|
||||||
|
file, path, "nu", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, indices,
|
||||||
|
[&](auto i) {
|
||||||
|
return domain.LinearElasticMaterials()[i].poisson_ratio;
|
||||||
|
}))
|
||||||
|
return false;
|
||||||
|
indices.resize(domain.ShellSections().Size());
|
||||||
|
std::iota(indices.begin(), indices.end(), 0U);
|
||||||
|
path = "/model/shell/sections";
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "internal_section_id",
|
||||||
|
H5T_STD_U64LE, H5T_NATIVE_UINT64, indices,
|
||||||
|
[](auto i) { return i; }) ||
|
||||||
|
!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "material_internal_id", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
indices,
|
||||||
|
[&](auto i) { return domain.ShellSections()[i].material_index; }) ||
|
||||||
|
!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "source_line", H5T_STD_U64LE, H5T_NATIVE_UINT64, indices,
|
||||||
|
[&](auto i) { return domain.ShellSections()[i].location.line; }) ||
|
||||||
|
!CheckTextMember(file, path, "source_file", indices,
|
||||||
|
[&](auto i) {
|
||||||
|
return NormalizedPathString(
|
||||||
|
domain.ShellSections()[i].location.file);
|
||||||
|
}) ||
|
||||||
|
!CheckTextMember(
|
||||||
|
file, path, "source_elset", indices,
|
||||||
|
[&](auto i) { return domain.ShellSections()[i].name; }) ||
|
||||||
|
!CheckMember<double>(
|
||||||
|
file, path, "thickness", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, indices,
|
||||||
|
[&](auto i) { return domain.ShellSections()[i].thickness; }))
|
||||||
|
return false;
|
||||||
|
std::vector<double> directors, frames;
|
||||||
|
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||||
|
directors.insert(directors.end(), frame.director.begin(),
|
||||||
|
frame.director.end());
|
||||||
|
frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end());
|
||||||
|
frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end());
|
||||||
|
frames.insert(frames.end(), frame.director.begin(), frame.director.end());
|
||||||
|
}
|
||||||
|
return CheckArray<double>(file, "/model/shell/nodal_director", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE,
|
||||||
|
{static_cast<hsize_t>(domain.Nodes().size()), 3U},
|
||||||
|
directors) &&
|
||||||
|
CheckArray<double>(
|
||||||
|
file, "/model/shell/nodal_frame", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE,
|
||||||
|
{static_cast<hsize_t>(domain.Nodes().size()), 3U, 3U}, frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckModalRows(hid_t file, const Domain& domain,
|
||||||
|
const EigenAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) {
|
||||||
|
const char* path = "/steps/Step-1/eigen/modes";
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "mode_id", H5T_STD_U64LE,
|
||||||
|
H5T_NATIVE_UINT64, state.modes,
|
||||||
|
[](const auto& row) { return row.mode_id; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "eigenvalue", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.modes,
|
||||||
|
[](const auto& row) { return row.eigenvalue; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "classification", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.modes, [](const auto& row) { return row.classification; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "cluster_id", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
state.modes, [](const auto& row) { return row.cluster_id; }))
|
||||||
|
return false;
|
||||||
|
std::vector<EigenMode> frequencies;
|
||||||
|
for (const auto& mode : state.modes)
|
||||||
|
if (mode.frequency) frequencies.push_back(mode);
|
||||||
|
path = "/steps/Step-1/eigen/frequencies";
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "mode_id", H5T_STD_U64LE,
|
||||||
|
H5T_NATIVE_UINT64, frequencies,
|
||||||
|
[](const auto& row) { return row.mode_id; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "frequency", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, frequencies,
|
||||||
|
[](const auto& row) { return *row.frequency; }))
|
||||||
|
return false;
|
||||||
|
path = "/steps/Step-1/eigen/verification/nodal_reduction";
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "free_count", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.nodal_reduction,
|
||||||
|
[](const auto& row) { return row.free_count; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "null_count", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.nodal_reduction,
|
||||||
|
[](const auto& row) { return row.null_count; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "positive_count", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.nodal_reduction,
|
||||||
|
[](const auto& row) { return row.positive_count; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "rcond_available", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.nodal_reduction,
|
||||||
|
[](const auto& row) { return row.rcond_available; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "rcond_inf", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.nodal_reduction,
|
||||||
|
[](const auto& row) { return row.rcond_inf; }))
|
||||||
|
return false;
|
||||||
|
path = "/steps/Step-1/eigen/verification/eigenpair_checks";
|
||||||
|
if (!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "sorted_rank", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.sorted_rank; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "solver_column", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.solver_column; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "eigenvalue", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.eigenvalue; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "modal_mass", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.modal_mass; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(
|
||||||
|
file, path, "rayleigh_quotient", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE,
|
||||||
|
state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.rayleigh_quotient; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "eta_r", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.eta_r; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "eta_w", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.eta_w; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "rho_b", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.rho_b; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "eta_norm", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.eta_norm; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "eta_energy", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.eta_energy; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "backend_res_available", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.backend_res_available; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "backend_res", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.eigenpair_checks,
|
||||||
|
[](const auto& row) { return row.backend_res; }))
|
||||||
|
return false;
|
||||||
|
path = "/steps/Step-1/eigen/backend/calls";
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "ordinal", H5T_STD_U64LE,
|
||||||
|
H5T_NATIVE_UINT64,
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.ordinal; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "lower", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.lower; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "upper", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.upper; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "allocated_m0", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.allocated_m0; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::int64_t>(file, path, "info", H5T_STD_I64LE,
|
||||||
|
H5T_NATIVE_INT64, state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.info; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "count_available", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.count_available; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "count", H5T_STD_U64LE,
|
||||||
|
H5T_NATIVE_UINT64,
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.count; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "loop_available", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.loop_available; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint64_t>(file, path, "loop", H5T_STD_U64LE,
|
||||||
|
H5T_NATIVE_UINT64,
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.loop; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint8_t>(
|
||||||
|
file, path, "epsout_available", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.epsout_available; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "epsout", H5T_IEEE_F64LE,
|
||||||
|
H5T_NATIVE_DOUBLE, state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.epsout; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "kind", state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.kind; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "purpose", state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.purpose; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "outcome", state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.outcome; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "decision_reason",
|
||||||
|
state.backend.backend_calls,
|
||||||
|
[](const auto& row) { return row.decision_reason; }))
|
||||||
|
return false;
|
||||||
|
path = "/model/shell/densities";
|
||||||
|
if (!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "material_internal_id", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
domain.Densities(),
|
||||||
|
[](const auto& row) { return row.material_index; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<double>(file, path, "rho", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE,
|
||||||
|
domain.Densities(),
|
||||||
|
[](const auto& row) { return row.rho; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "source_line", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||||
|
domain.Densities(),
|
||||||
|
[](const auto& row) { return row.location.line; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "source_file", domain.Densities(),
|
||||||
|
[](const auto& row) {
|
||||||
|
return NormalizedPathString(row.location.file);
|
||||||
|
}))
|
||||||
|
return false;
|
||||||
|
path = "/diagnostics";
|
||||||
|
if (!CheckMember<std::uint64_t>(
|
||||||
|
file, path, "line", H5T_STD_U64LE, H5T_NATIVE_UINT64, diagnostics,
|
||||||
|
[](const auto& row) { return row.location.line; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "severity", diagnostics,
|
||||||
|
[](const auto&) { return std::string{"warning"}; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "file", diagnostics, [](const auto& row) {
|
||||||
|
return NormalizedPathString(row.location.file);
|
||||||
|
}))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "code", diagnostics,
|
||||||
|
[](const auto& row) { return row.code; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "keyword", diagnostics,
|
||||||
|
[](const auto& row) { return row.keyword; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "entity_identity", diagnostics,
|
||||||
|
[](const auto& row) { return row.entity_identity; }))
|
||||||
|
return false;
|
||||||
|
if (!CheckTextMember(file, path, "message", diagnostics,
|
||||||
|
[](const auto& row) { return row.message; }))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Status SelfCheckEigenFile(const std::filesystem::path& path,
|
||||||
|
const Domain& domain, const EigenAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) {
|
||||||
|
try {
|
||||||
|
Hdf5ErrorSilencer silence;
|
||||||
|
Hdf5Handle file{RequireHdf5Id(H5Fopen(path.string().c_str(), H5F_ACC_RDONLY,
|
||||||
|
H5P_DEFAULT),
|
||||||
|
"Unable to reopen eigen candidate."),
|
||||||
|
H5Fclose};
|
||||||
|
for (const char* required :
|
||||||
|
{"/metadata", "/model/nodes", "/model/elements",
|
||||||
|
"/model/shell/densities", "/diagnostics", "/steps/Step-1/eigen/modes",
|
||||||
|
"/steps/Step-1/eigen/frequencies", "/steps/Step-1/eigen/vectors",
|
||||||
|
"/steps/Step-1/eigen/verification/nodal_reduction",
|
||||||
|
"/steps/Step-1/eigen/verification/eigenpair_checks",
|
||||||
|
"/steps/Step-1/eigen/backend/base_fpm",
|
||||||
|
"/steps/Step-1/eigen/backend/calls"}) {
|
||||||
|
if (!HasPath(file.Get(), required))
|
||||||
|
return Failure("Mandatory eigen path is missing.");
|
||||||
|
}
|
||||||
|
if (!HasNoStaticResults(file.Get()))
|
||||||
|
return Failure("Eigen output contains static result paths.");
|
||||||
|
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
Hdf5Handle step{H5Gopen2(file.Get(), "/steps/Step-1", H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
if (metadata.Get() < 0 || step.Get() < 0 ||
|
||||||
|
!ReadUint64Attribute(metadata.Get(), "schema_version", 0U) ||
|
||||||
|
!ReadUint64Attribute(metadata.Get(), "eigen_schema_version", 1U) ||
|
||||||
|
!ReadStringAttribute(metadata.Get(), "feature_id",
|
||||||
|
"mitc4-shell-feast-eigen2") ||
|
||||||
|
!ReadStringAttribute(metadata.Get(), "analysis_procedure",
|
||||||
|
"linear-frequency") ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "requested_mode_count",
|
||||||
|
state.modes.size()) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "returned_mode_count",
|
||||||
|
state.modes.size()) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "full_dof_count",
|
||||||
|
state.full_dof_count) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "free_dof_count",
|
||||||
|
state.free_dof_count) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "mass_null_dimension",
|
||||||
|
state.mass_null_dimension) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "finite_dimension",
|
||||||
|
state.finite_dimension) ||
|
||||||
|
!ReadUint64Attribute(step.Get(), "validated_eigenpair_count",
|
||||||
|
state.eigenpair_checks.size())) {
|
||||||
|
return Failure("Eigen metadata or step inventory differs from state.");
|
||||||
|
}
|
||||||
|
if (!DatasetShape(file.Get(), "/steps/Step-1/eigen/modes",
|
||||||
|
{static_cast<hsize_t>(state.modes.size())}) ||
|
||||||
|
!DatasetShape(file.Get(), "/steps/Step-1/eigen/frequencies",
|
||||||
|
{static_cast<hsize_t>([&state] {
|
||||||
|
std::size_t count = 0U;
|
||||||
|
for (const auto& mode : state.modes)
|
||||||
|
if (mode.frequency) ++count;
|
||||||
|
return count;
|
||||||
|
}())}) ||
|
||||||
|
!DatasetShape(file.Get(),
|
||||||
|
"/steps/Step-1/eigen/verification/nodal_reduction",
|
||||||
|
{static_cast<hsize_t>(domain.Nodes().size())}) ||
|
||||||
|
!DatasetShape(file.Get(),
|
||||||
|
"/steps/Step-1/eigen/verification/eigenpair_checks",
|
||||||
|
{static_cast<hsize_t>(state.eigenpair_checks.size())}) ||
|
||||||
|
!DatasetShape(file.Get(), "/steps/Step-1/eigen/backend/base_fpm",
|
||||||
|
{128U}) ||
|
||||||
|
!DatasetShape(
|
||||||
|
file.Get(), "/steps/Step-1/eigen/backend/calls",
|
||||||
|
{static_cast<hsize_t>(state.backend.backend_calls.size())}) ||
|
||||||
|
!VerifyVectors(file.Get(), domain, state)) {
|
||||||
|
return Failure("Eigen dataset shape or vector data differs from state.");
|
||||||
|
}
|
||||||
|
if (!CheckModalRows(file.Get(), domain, state, diagnostics))
|
||||||
|
return Failure(
|
||||||
|
"Compound type, source identity or value differs from candidate "
|
||||||
|
"state.");
|
||||||
|
if (!CheckAttributes(file.Get(), domain, state))
|
||||||
|
return Failure(
|
||||||
|
"Mandatory attributes, controls or boundary data differ from "
|
||||||
|
"candidate state.");
|
||||||
|
if (!CheckModelRows(file.Get(), domain))
|
||||||
|
return Failure(
|
||||||
|
"Model source identities, connectivity or physical data differ from "
|
||||||
|
"Domain.");
|
||||||
|
if (metadata.CloseChecked() < 0 || step.CloseChecked() < 0)
|
||||||
|
return Failure("Unable to close candidate groups.");
|
||||||
|
if (file.CloseChecked() < 0)
|
||||||
|
return Failure("Unable to close checked eigen candidate.");
|
||||||
|
return Status::Ok();
|
||||||
|
} catch (const Hdf5Failure& failure) {
|
||||||
|
return Failure(failure.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::hdf5_internal
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#ifndef FESA_IO_HDF5_EIGEN_HDF5_SELF_CHECK_H_
|
||||||
|
#define FESA_IO_HDF5_EIGEN_HDF5_SELF_CHECK_H_
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/eigen_analysis_state.h"
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
struct Diagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace fesa::hdf5_internal {
|
||||||
|
|
||||||
|
/// @brief Reopens and validates a closed authoritative eigen HDF5 candidate.
|
||||||
|
Status SelfCheckEigenFile(const std::filesystem::path& path,
|
||||||
|
const Domain& domain, const EigenAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics);
|
||||||
|
|
||||||
|
} // namespace fesa::hdf5_internal
|
||||||
|
|
||||||
|
#endif // FESA_IO_HDF5_EIGEN_HDF5_SELF_CHECK_H_
|
||||||
@@ -44,6 +44,31 @@ StepDefinition::StepDefinition(StaticStepDefinition definition)
|
|||||||
surface_pressure_loads_ = std::move(definition.surface_pressure_loads);
|
surface_pressure_loads_ = std::move(definition.surface_pressure_loads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StepDefinition::StepDefinition(FrequencyStepDefinition definition)
|
||||||
|
: name_{definition.source_step_name_raw},
|
||||||
|
procedure_kind_{StaticProcedureKind::kLinearSmallDisplacement},
|
||||||
|
nlgeom_{false},
|
||||||
|
initial_increment_{0.0},
|
||||||
|
time_period_{0.0},
|
||||||
|
minimum_increment_{0.0},
|
||||||
|
maximum_increment_{0.0},
|
||||||
|
location_{definition.step_location},
|
||||||
|
frequency_{std::move(definition)} {
|
||||||
|
for (const auto& boundary : frequency_->boundaries) {
|
||||||
|
for (int source_dof = boundary.first_dof; source_dof <= boundary.last_dof;
|
||||||
|
++source_dof) {
|
||||||
|
const std::size_t source_order = boundary_conditions_.size();
|
||||||
|
auto owned = std::make_unique<PrescribedDisplacementBoundaryCondition>(
|
||||||
|
SourceTargetQuery{SourceEntityKind::kNode, "", boundary.target},
|
||||||
|
static_cast<DofComponent>(source_dof - 1), boundary.value,
|
||||||
|
source_order, boundary.location);
|
||||||
|
boundary_conditions_view_.push_back(std::cref(*owned));
|
||||||
|
prescribed_displacements_view_.Add(*owned);
|
||||||
|
boundary_conditions_.push_back(std::move(owned));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const std::string& StepDefinition::Name() const noexcept { return name_; }
|
const std::string& StepDefinition::Name() const noexcept { return name_; }
|
||||||
|
|
||||||
const BoundaryConditionView& StepDefinition::BoundaryConditions()
|
const BoundaryConditionView& StepDefinition::BoundaryConditions()
|
||||||
@@ -76,6 +101,14 @@ bool StepDefinition::IsNonlinearStatic() const noexcept {
|
|||||||
return procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton;
|
return procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool StepDefinition::IsFrequency() const noexcept {
|
||||||
|
return frequency_.has_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
const FrequencyStepDefinition* StepDefinition::Frequency() const noexcept {
|
||||||
|
return frequency_ ? &*frequency_ : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
bool StepDefinition::UsesNonlinearGeometry() const noexcept { return nlgeom_; }
|
bool StepDefinition::UsesNonlinearGeometry() const noexcept { return nlgeom_; }
|
||||||
|
|
||||||
double StepDefinition::InitialIncrement() const noexcept {
|
double StepDefinition::InitialIncrement() const noexcept {
|
||||||
@@ -165,6 +198,15 @@ const DomainCollectionView<StepDefinition>& Domain::Steps() const noexcept {
|
|||||||
return steps_view_;
|
return steps_view_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::vector<MaterialDensityDefinition>& Domain::Densities()
|
||||||
|
const noexcept {
|
||||||
|
return definition_.densities;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FrequencyStepDefinition* Domain::Frequency() const noexcept {
|
||||||
|
return steps_view_.Size() == 1U ? steps_view_[0U].Frequency() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const std::vector<Diagnostic>& Domain::Warnings() const noexcept {
|
const std::vector<Diagnostic>& Domain::Warnings() const noexcept {
|
||||||
return definition_.warnings;
|
return definition_.warnings;
|
||||||
}
|
}
|
||||||
@@ -227,7 +269,8 @@ Domain::Domain(ModelDefinition definition)
|
|||||||
}
|
}
|
||||||
definition_.shell_elements.clear();
|
definition_.shell_elements.clear();
|
||||||
|
|
||||||
step_definitions_.reserve(definition_.steps.size());
|
step_definitions_.reserve(definition_.steps.size() +
|
||||||
|
(definition_.frequency_step ? 1U : 0U));
|
||||||
for (auto& step : definition_.steps) {
|
for (auto& step : definition_.steps) {
|
||||||
auto owned =
|
auto owned =
|
||||||
std::unique_ptr<StepDefinition>(new StepDefinition(std::move(step)));
|
std::unique_ptr<StepDefinition>(new StepDefinition(std::move(step)));
|
||||||
@@ -235,6 +278,12 @@ Domain::Domain(ModelDefinition definition)
|
|||||||
step_definitions_.push_back(std::move(owned));
|
step_definitions_.push_back(std::move(owned));
|
||||||
}
|
}
|
||||||
definition_.steps.clear();
|
definition_.steps.clear();
|
||||||
|
if (definition_.frequency_step) {
|
||||||
|
auto owned = std::unique_ptr<StepDefinition>(
|
||||||
|
new StepDefinition(std::move(*definition_.frequency_step)));
|
||||||
|
steps_view_.Add(*owned);
|
||||||
|
step_definitions_.push_back(std::move(owned));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ add_executable(
|
|||||||
unit/analysis/analysis_test.cpp
|
unit/analysis/analysis_test.cpp
|
||||||
unit/analysis/analysis_model_test.cpp
|
unit/analysis/analysis_model_test.cpp
|
||||||
unit/analysis/analysis_state_test.cpp
|
unit/analysis/analysis_state_test.cpp
|
||||||
|
unit/analysis/eigen_reduction_test.cpp
|
||||||
unit/analysis/nonlinear_analysis_state_test.cpp
|
unit/analysis/nonlinear_analysis_state_test.cpp
|
||||||
unit/analysis/nonlinear_static_control_test.cpp
|
unit/analysis/nonlinear_static_control_test.cpp
|
||||||
unit/analysis/newton_iteration_log_test.cpp
|
unit/analysis/newton_iteration_log_test.cpp
|
||||||
@@ -24,6 +25,7 @@ add_executable(
|
|||||||
unit/elements/euler_beam_3d_test.cpp
|
unit/elements/euler_beam_3d_test.cpp
|
||||||
unit/elements/finite_rotation_test.cpp
|
unit/elements/finite_rotation_test.cpp
|
||||||
unit/elements/mitc4_shell_test.cpp
|
unit/elements/mitc4_shell_test.cpp
|
||||||
|
unit/elements/mitc4_shell_mass_contract_test.cpp
|
||||||
unit/elements/nonlinear_mitc4_shell_test.cpp
|
unit/elements/nonlinear_mitc4_shell_test.cpp
|
||||||
unit/elements/nonlinear_mitc4_shell_tangent_test.cpp
|
unit/elements/nonlinear_mitc4_shell_tangent_test.cpp
|
||||||
unit/fem/dof_manager_test.cpp
|
unit/fem/dof_manager_test.cpp
|
||||||
@@ -34,10 +36,12 @@ add_executable(
|
|||||||
unit/math/vector_test.cpp
|
unit/math/vector_test.cpp
|
||||||
unit/io/abaqus/domain_mapping_components_test.cpp
|
unit/io/abaqus/domain_mapping_components_test.cpp
|
||||||
unit/io/abaqus/domain_mapper_test.cpp
|
unit/io/abaqus/domain_mapper_test.cpp
|
||||||
|
unit/io/abaqus/eigen_domain_test.cpp
|
||||||
unit/io/abaqus/input_reader_test.cpp
|
unit/io/abaqus/input_reader_test.cpp
|
||||||
unit/io/abaqus/input_syntax_test.cpp
|
unit/io/abaqus/input_syntax_test.cpp
|
||||||
unit/io/abaqus/shell_surface_pressure_domain_test.cpp
|
unit/io/abaqus/shell_surface_pressure_domain_test.cpp
|
||||||
unit/io/hdf5/hdf5_results_writer_test.cpp
|
unit/io/hdf5/hdf5_results_writer_test.cpp
|
||||||
|
unit/io/hdf5/eigen_hdf5_results_writer_test.cpp
|
||||||
unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp
|
unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp
|
||||||
unit/io/hdf5/hdf5_writer_components_test.cpp
|
unit/io/hdf5/hdf5_writer_components_test.cpp
|
||||||
unit/loads/load_test.cpp
|
unit/loads/load_test.cpp
|
||||||
@@ -52,6 +56,7 @@ add_executable(
|
|||||||
unit/results/result_recovery_components_test.cpp
|
unit/results/result_recovery_components_test.cpp
|
||||||
unit/results/result_recovery_test.cpp
|
unit/results/result_recovery_test.cpp
|
||||||
unit/results/results_writer_test.cpp
|
unit/results/results_writer_test.cpp
|
||||||
|
unit/solvers/eigen/mkl_feast_eigen_solver_test.cpp
|
||||||
unit/solvers/linear/linear_solver_test.cpp
|
unit/solvers/linear/linear_solver_test.cpp
|
||||||
unit/solvers/linear/mkl_pardiso_solver_test.cpp
|
unit/solvers/linear/mkl_pardiso_solver_test.cpp
|
||||||
unit/solvers/linear/tangent_solver_test.cpp
|
unit/solvers/linear/tangent_solver_test.cpp
|
||||||
@@ -75,10 +80,12 @@ target_link_libraries(
|
|||||||
add_executable(
|
add_executable(
|
||||||
fesa_integration_tests
|
fesa_integration_tests
|
||||||
integration/analysis/linear_static_analysis_test.cpp
|
integration/analysis/linear_static_analysis_test.cpp
|
||||||
|
integration/analysis/linear_frequency_procedure_test.cpp
|
||||||
integration/analysis/shell_surface_pressure_linear_test.cpp
|
integration/analysis/shell_surface_pressure_linear_test.cpp
|
||||||
integration/analysis/shell_surface_pressure_nonlinear_test.cpp
|
integration/analysis/shell_surface_pressure_nonlinear_test.cpp
|
||||||
integration/analysis/nonlinear_static_procedure_test.cpp
|
integration/analysis/nonlinear_static_procedure_test.cpp
|
||||||
integration/app/fesa_application_test.cpp
|
integration/app/fesa_application_test.cpp
|
||||||
|
integration/app/eigen_application_test.cpp
|
||||||
integration/app/shell_surface_pressure_application_test.cpp
|
integration/app/shell_surface_pressure_application_test.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,6 +100,9 @@ target_link_libraries(
|
|||||||
add_executable(
|
add_executable(
|
||||||
fesa_reference_tests
|
fesa_reference_tests
|
||||||
reference/reference_tolerance_policy.cpp
|
reference/reference_tolerance_policy.cpp
|
||||||
|
reference/eigen_reference_comparison.cpp
|
||||||
|
reference/eigen_reference_comparison_test.cpp
|
||||||
|
reference/eigen_reference_cases_test.cpp
|
||||||
reference/reference_tolerance_policy_test.cpp
|
reference/reference_tolerance_policy_test.cpp
|
||||||
reference/reference_comparison.cpp
|
reference/reference_comparison.cpp
|
||||||
reference/reference_comparison_test.cpp
|
reference/reference_comparison_test.cpp
|
||||||
@@ -178,18 +188,21 @@ endif()
|
|||||||
|
|
||||||
gtest_discover_tests(
|
gtest_discover_tests(
|
||||||
fesa_unit_tests
|
fesa_unit_tests
|
||||||
|
DISCOVERY_MODE PRE_TEST
|
||||||
PROPERTIES
|
PROPERTIES
|
||||||
LABELS "linear-static-3d-euler-beam;unit"
|
LABELS "linear-static-3d-euler-beam;unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
gtest_discover_tests(
|
gtest_discover_tests(
|
||||||
fesa_integration_tests
|
fesa_integration_tests
|
||||||
|
DISCOVERY_MODE PRE_TEST
|
||||||
PROPERTIES
|
PROPERTIES
|
||||||
LABELS "linear-static-3d-euler-beam;integration"
|
LABELS "linear-static-3d-euler-beam;integration"
|
||||||
)
|
)
|
||||||
|
|
||||||
gtest_discover_tests(
|
gtest_discover_tests(
|
||||||
fesa_reference_tests
|
fesa_reference_tests
|
||||||
|
DISCOVERY_MODE PRE_TEST
|
||||||
PROPERTIES
|
PROPERTIES
|
||||||
LABELS "linear-static-3d-euler-beam;reference"
|
LABELS "linear-static-3d-euler-beam;reference"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "fesa/analysis/analysis_model.h"
|
||||||
|
#include "fesa/analysis/linear_static_procedure.h"
|
||||||
|
#include "fesa/analysis/prepared_analysis.h"
|
||||||
|
#include "fesa/assembly/parallel_for.h"
|
||||||
|
#include "fesa/assembly/sparse_assembler.h"
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
#include "fesa/model/model_types.h"
|
||||||
|
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||||
|
|
||||||
|
TEST(LinearFrequencyProcedureExecutor, RejectsDomainWithoutFrequencyStep) {
|
||||||
|
fesa::ModelDefinition definition;
|
||||||
|
definition.source_path = "frequency-missing.inp";
|
||||||
|
definition.source_content_identity = "frequency-missing";
|
||||||
|
auto domain = fesa::Domain::Create(std::move(definition));
|
||||||
|
ASSERT_TRUE(domain.HasValue());
|
||||||
|
|
||||||
|
const fesa::LinearFrequencyProcedureExecutor executor;
|
||||||
|
const auto result = executor.Execute(domain.Value());
|
||||||
|
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kSolver);
|
||||||
|
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
|
||||||
|
"invalid-frequency-procedure");
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string StatusText(const fesa::Status& status) {
|
||||||
|
std::string text;
|
||||||
|
for (const auto& d : status.Diagnostics())
|
||||||
|
text += d.code + " " + d.message + "\n";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Result<fesa::Domain> PanelDomain(bool rotated, bool free_shell,
|
||||||
|
bool statics = false,
|
||||||
|
std::size_t requested = 1U,
|
||||||
|
double length_scale = 1.0,
|
||||||
|
double density = 1.0,
|
||||||
|
double modulus = 12.0) {
|
||||||
|
const std::array<std::array<double, 3>, 4> points{{{{0.8, -2.0, -0.6}},
|
||||||
|
{{0.8, 2.0, -0.6}},
|
||||||
|
{{-0.8, 2.0, 0.6}},
|
||||||
|
{{-0.8, -2.0, 0.6}}}};
|
||||||
|
std::ostringstream deck;
|
||||||
|
deck << std::setprecision(17) << "*Part,name=Panel\n*Node\n";
|
||||||
|
const double c = std::sqrt(0.5);
|
||||||
|
for (std::size_t i = 0; i < points.size(); ++i) {
|
||||||
|
const auto& p = points[i];
|
||||||
|
deck << 11U * (i + 1U) << ','
|
||||||
|
<< length_scale * (rotated ? c * (p[0] - p[1]) : p[0]) << ','
|
||||||
|
<< length_scale * (rotated ? c * (p[0] + p[1]) : p[1]) << ','
|
||||||
|
<< length_scale * p[2] << '\n';
|
||||||
|
}
|
||||||
|
deck << R"inp(*Element,type=S4
|
||||||
|
71,11,22,33,44
|
||||||
|
*Elset,elset=Shell
|
||||||
|
71
|
||||||
|
*Shell Section,elset=Shell,material=Steel
|
||||||
|
)inp" << length_scale
|
||||||
|
<< R"inp(
|
||||||
|
*End Part
|
||||||
|
*Assembly,name=Assembly
|
||||||
|
*Instance,name=Panel-1,part=Panel
|
||||||
|
*End Instance
|
||||||
|
*Nset,nset=Fixed,instance=Panel-1
|
||||||
|
22,33,44
|
||||||
|
*Nset,nset=Tip,instance=Panel-1
|
||||||
|
11
|
||||||
|
*End Assembly
|
||||||
|
*Material,name=Steel
|
||||||
|
*Elastic
|
||||||
|
)inp" << modulus
|
||||||
|
<< ",0.\n";
|
||||||
|
if (!statics) deck << "*Density\n" << density << '\n';
|
||||||
|
if (!free_shell) deck << "*Boundary\nFixed,1,6\nTip,1,5\n";
|
||||||
|
if (statics) {
|
||||||
|
deck << "*Step,name=Static,nlgeom=NO\n*Static\n0.1,1.,0.01,1.\n*Cload\nTip,"
|
||||||
|
"4,"
|
||||||
|
<< (-4.0 / 3.0) * (rotated ? c : 1.0) << "\nTip,5,"
|
||||||
|
<< (rotated ? (-4.0 / 3.0) * c : 0.0) << "\nTip,6,1.\n";
|
||||||
|
} else
|
||||||
|
deck << "*Step,name=Step-2,nlgeom=NO,perturbation\n*Frequency\n"
|
||||||
|
<< requested << '\n';
|
||||||
|
deck << "*End Step\n";
|
||||||
|
static std::atomic<unsigned long long> sequence{0U};
|
||||||
|
const auto path =
|
||||||
|
std::filesystem::temp_directory_path() /
|
||||||
|
("fesa-eigen-procedure-" +
|
||||||
|
std::to_string(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count()) +
|
||||||
|
"-" + std::to_string(sequence.fetch_add(1U)) + ".inp");
|
||||||
|
{
|
||||||
|
std::ofstream stream{path};
|
||||||
|
stream << deck.str();
|
||||||
|
}
|
||||||
|
auto prepared = fesa::PrepareAnalysisInput(path);
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(path, ignored);
|
||||||
|
if (!prepared.HasValue())
|
||||||
|
return fesa::Result<fesa::Domain>::Failure(prepared.GetStatus());
|
||||||
|
return fesa::Result<fesa::Domain>::Success(
|
||||||
|
std::move(prepared.Value().domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(LinearFrequencyProcedureExecutor,
|
||||||
|
MatchesPartialBoundaryPencilAndFullResidualCovariance) {
|
||||||
|
std::array<double, 24> original_residual{};
|
||||||
|
for (const bool rotated : {false, true}) {
|
||||||
|
auto domain = PanelDomain(rotated, false);
|
||||||
|
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||||
|
const auto result =
|
||||||
|
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||||
|
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||||
|
const auto& state = result.Value();
|
||||||
|
ASSERT_EQ(state.modes.size(), 1U);
|
||||||
|
EXPECT_EQ(state.mass_null_dimension, 0U);
|
||||||
|
EXPECT_EQ(state.finite_dimension, 1U);
|
||||||
|
EXPECT_NEAR(state.modes[0].eigenvalue, 10369.0 / 450.0, 1e-10);
|
||||||
|
EXPECT_EQ(state.modes[0].classification, 1U);
|
||||||
|
EXPECT_EQ(state.vectors.Rows(), 1U);
|
||||||
|
EXPECT_EQ(state.vectors.Columns(), 24U);
|
||||||
|
EXPECT_NEAR(state.vectors(0U, 5U), std::sqrt(50.0 / 3.0), 1e-12);
|
||||||
|
for (std::size_t dof = 0; dof < 24U; ++dof)
|
||||||
|
if (dof != 5U) EXPECT_EQ(state.vectors(0U, dof), 0.0);
|
||||||
|
ASSERT_EQ(state.eigenpair_checks.size(), 1U);
|
||||||
|
EXPECT_NEAR(state.eigenpair_checks[0].modal_mass, 1.0, 1e-12);
|
||||||
|
EXPECT_LE(state.eigenpair_checks[0].eta_r, 1e-10);
|
||||||
|
EXPECT_LE(state.eigenpair_checks[0].eta_energy, 1e-10);
|
||||||
|
EXPECT_LE(state.eigenpair_checks[0].rho_b, 1e-10);
|
||||||
|
EXPECT_EQ(state.backend.eigenvectors.Rows(), 0U);
|
||||||
|
auto static_domain = PanelDomain(rotated, false, true);
|
||||||
|
ASSERT_TRUE(static_domain.HasValue())
|
||||||
|
<< StatusText(static_domain.GetStatus());
|
||||||
|
const fesa::SerialParallelFor serial;
|
||||||
|
const auto model = fesa::AnalysisModel::Create(static_domain.Value());
|
||||||
|
ASSERT_TRUE(model.HasValue());
|
||||||
|
const auto dofs = fesa::DofManager::Create(model.Value());
|
||||||
|
ASSERT_TRUE(dofs.HasValue());
|
||||||
|
const auto stiffness = fesa::SparseAssembler::AssembleStiffness(
|
||||||
|
model.Value(), dofs.Value(), serial);
|
||||||
|
ASSERT_TRUE(stiffness.HasValue());
|
||||||
|
fesa::Vector unit{24U};
|
||||||
|
unit[5U] = 1.0;
|
||||||
|
const double kff = stiffness.Value().Multiply(unit)[5U];
|
||||||
|
EXPECT_NEAR(kff, 10369.0 / 7500.0, 1e-12);
|
||||||
|
fesa::Vector displacement{24U};
|
||||||
|
displacement[5U] = 1.0 / kff;
|
||||||
|
EXPECT_NEAR(displacement[5U], 7500.0 / 10369.0, 1e-12);
|
||||||
|
auto residual = stiffness.Value().Multiply(displacement);
|
||||||
|
const double c = std::sqrt(0.5);
|
||||||
|
residual[3U] -= (-4.0 / 3.0) * (rotated ? c : 1.0);
|
||||||
|
residual[4U] -= rotated ? (-4.0 / 3.0) * c : 0.0;
|
||||||
|
residual[5U] -= 1.0;
|
||||||
|
EXPECT_NEAR(residual[5U], 0.0, 1e-12);
|
||||||
|
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||||
|
if (!rotated)
|
||||||
|
original_residual[i] = residual[i];
|
||||||
|
else {
|
||||||
|
const std::size_t base = (i / 3U) * 3U;
|
||||||
|
const double expected =
|
||||||
|
i % 3U == 0U
|
||||||
|
? c * (original_residual[base] - original_residual[base + 1U])
|
||||||
|
: (i % 3U == 1U ? c * (original_residual[base] +
|
||||||
|
original_residual[base + 1U])
|
||||||
|
: original_residual[i]);
|
||||||
|
EXPECT_NEAR(residual[i], expected, 1e-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E2-C09 is a pencil oracle. The public static contract conflict is recorded
|
||||||
|
// separately; direct covariance does not certify spatial moment equilibrium.
|
||||||
|
TEST(LinearFrequencyProcedureExecutor,
|
||||||
|
PartialBoundaryStaticRejectsUnbalancedDrillingMoment) {
|
||||||
|
const auto domain = PanelDomain(false, false, true);
|
||||||
|
ASSERT_TRUE(domain.HasValue());
|
||||||
|
const fesa::SerialParallelFor serial;
|
||||||
|
fesa::MklPardisoSolver solver;
|
||||||
|
const auto result =
|
||||||
|
fesa::LinearStaticProcedureExecutor{serial, solver}.Execute(
|
||||||
|
domain.Value());
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_NE(StatusText(result.GetStatus())
|
||||||
|
.find("global-equilibrium-tolerance-failure"),
|
||||||
|
std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LinearFrequencyProcedureExecutor,
|
||||||
|
FreePanelRetainsSixRigidModesAndAllComputedEvidence) {
|
||||||
|
auto domain = PanelDomain(false, true, false, 6U);
|
||||||
|
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||||
|
const auto result =
|
||||||
|
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||||
|
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||||
|
const auto& state = result.Value();
|
||||||
|
EXPECT_EQ(state.mass_null_dimension, 4U);
|
||||||
|
EXPECT_EQ(state.finite_dimension, 20U);
|
||||||
|
ASSERT_EQ(state.modes.size(), 6U);
|
||||||
|
EXPECT_GE(state.eigenpair_checks.size(), 6U);
|
||||||
|
for (const auto& mode : state.modes) {
|
||||||
|
EXPECT_EQ(mode.classification, 0U);
|
||||||
|
EXPECT_EQ(mode.cluster_id, 1U);
|
||||||
|
}
|
||||||
|
EXPECT_LE(state.eta_gram, 1e-10);
|
||||||
|
EXPECT_LE(state.max_eta_orth, 1e-10);
|
||||||
|
EXPECT_LE(state.eta_mz, 1e-12);
|
||||||
|
EXPECT_LE(state.eta_kz, 1e-12);
|
||||||
|
for (const auto& check : state.eigenpair_checks) {
|
||||||
|
EXPECT_LE(check.eta_r, 1e-10);
|
||||||
|
EXPECT_LE(check.eta_norm, 1e-10);
|
||||||
|
EXPECT_LE(check.eta_energy, 1e-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LinearFrequencyProcedureExecutor,
|
||||||
|
ExcessRequestedModesFailsWithSourceContext) {
|
||||||
|
auto domain = PanelDomain(false, false, false, 2U);
|
||||||
|
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||||
|
const auto result =
|
||||||
|
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kModel);
|
||||||
|
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
|
||||||
|
"insufficient-finite-modes");
|
||||||
|
EXPECT_GT(result.GetStatus().Diagnostics()[0].location.line, 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LinearFrequencyProcedureExecutor, PreservesDensityAndPhysicalUnitScaling) {
|
||||||
|
const auto baseline_domain = PanelDomain(false, false);
|
||||||
|
const auto dense_domain = PanelDomain(false, false, false, 1U, 1.0, 4.0);
|
||||||
|
constexpr double length = 1024.0;
|
||||||
|
const auto units_domain =
|
||||||
|
PanelDomain(false, false, false, 1U, length,
|
||||||
|
1.0 / (length * length * length), 12.0 / length);
|
||||||
|
ASSERT_TRUE(baseline_domain.HasValue());
|
||||||
|
ASSERT_TRUE(dense_domain.HasValue());
|
||||||
|
ASSERT_TRUE(units_domain.HasValue());
|
||||||
|
const fesa::LinearFrequencyProcedureExecutor executor;
|
||||||
|
const auto baseline = executor.Execute(baseline_domain.Value());
|
||||||
|
const auto dense = executor.Execute(dense_domain.Value());
|
||||||
|
const auto units = executor.Execute(units_domain.Value());
|
||||||
|
ASSERT_TRUE(baseline.HasValue()) << StatusText(baseline.GetStatus());
|
||||||
|
ASSERT_TRUE(dense.HasValue()) << StatusText(dense.GetStatus());
|
||||||
|
ASSERT_TRUE(units.HasValue()) << StatusText(units.GetStatus());
|
||||||
|
EXPECT_NEAR(
|
||||||
|
dense.Value().modes[0].eigenvalue / baseline.Value().modes[0].eigenvalue,
|
||||||
|
0.25, 1e-12);
|
||||||
|
EXPECT_NEAR(dense.Value().vectors(0U, 5U) / baseline.Value().vectors(0U, 5U),
|
||||||
|
0.5, 1e-12);
|
||||||
|
EXPECT_NEAR(
|
||||||
|
units.Value().modes[0].eigenvalue / baseline.Value().modes[0].eigenvalue,
|
||||||
|
1.0, 1e-12);
|
||||||
|
EXPECT_NEAR(
|
||||||
|
units.Value().vectors(0U, 5U) * length / baseline.Value().vectors(0U, 5U),
|
||||||
|
1.0, 1e-12);
|
||||||
|
EXPECT_LE(units.Value().eigenpair_checks[0].eta_r, 1e-10);
|
||||||
|
EXPECT_LE(units.Value().eigenpair_checks[0].eta_energy, 1e-10);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "fesa/app/fesa_application.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class EigenApplicationTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
static std::atomic<unsigned long long> sequence{0U};
|
||||||
|
folder_ =
|
||||||
|
std::filesystem::temp_directory_path() /
|
||||||
|
("fesa-eigen-app-" +
|
||||||
|
std::to_string(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count()) +
|
||||||
|
"-" + std::to_string(sequence.fetch_add(1U)));
|
||||||
|
ASSERT_TRUE(std::filesystem::create_directory(folder_));
|
||||||
|
input_ = folder_ / "Panel.Modes.inp";
|
||||||
|
output_ = folder_ / "Panel.Modes_results.h5";
|
||||||
|
log_ = folder_ / "Panel.Modes_log.txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove_all(folder_, ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteDeck(const bool empty = false) {
|
||||||
|
std::ofstream file{input_};
|
||||||
|
file << R"inp(*Part, name=Panel
|
||||||
|
*Node
|
||||||
|
11,0,0,0
|
||||||
|
22,1,0,0
|
||||||
|
33,1,1,0
|
||||||
|
44,0,1,0
|
||||||
|
*Element,type=S4
|
||||||
|
71,11,22,33,44
|
||||||
|
*Elset,elset=Shell
|
||||||
|
71
|
||||||
|
*Shell Section,elset=Shell,material=Steel
|
||||||
|
0.5
|
||||||
|
*End Part
|
||||||
|
*Assembly,name=Assembly
|
||||||
|
*Instance,name=Panel-1,part=Panel
|
||||||
|
*End Instance
|
||||||
|
*Nset,nset=Fixed,instance=Panel-1
|
||||||
|
11,22,33
|
||||||
|
*Nset,nset=Tip,instance=Panel-1
|
||||||
|
44
|
||||||
|
*End Assembly
|
||||||
|
*Material,name=Steel
|
||||||
|
*Density
|
||||||
|
1.
|
||||||
|
*Elastic
|
||||||
|
12.,0.
|
||||||
|
*Boundary
|
||||||
|
Fixed,1,6
|
||||||
|
Tip,1,2
|
||||||
|
Tip,4,6
|
||||||
|
)inp";
|
||||||
|
if (empty) file << "Tip,3,3\n";
|
||||||
|
file << R"inp(*Step,name=Step-2,nlgeom=NO,perturbation
|
||||||
|
*Frequency,eigensolver=Lanczos,normalization=mass
|
||||||
|
1
|
||||||
|
*End Step
|
||||||
|
)inp";
|
||||||
|
ASSERT_TRUE(file.good());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path folder_, input_, output_, log_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(EigenApplicationTest, DispatchesFrequencyAndWritesModalSibling) {
|
||||||
|
WriteDeck();
|
||||||
|
ASSERT_EQ(fesa::FesaApplication{}.Run({input_.string()}), 0);
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(output_));
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(log_));
|
||||||
|
EXPECT_EQ(std::filesystem::file_size(log_), 0U);
|
||||||
|
const hid_t file =
|
||||||
|
H5Fopen(output_.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||||
|
ASSERT_GE(file, 0);
|
||||||
|
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/modes", H5P_DEFAULT), 0);
|
||||||
|
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/vectors", H5P_DEFAULT), 0);
|
||||||
|
EXPECT_EQ(H5Lexists(file, "/steps/Step-1/frames", H5P_DEFAULT), 0);
|
||||||
|
EXPECT_GE(H5Fclose(file), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EigenApplicationTest, EmptyPencilPreservesPreviousFinalAndEmptyLog) {
|
||||||
|
WriteDeck(true);
|
||||||
|
const std::string original = "previous authoritative file";
|
||||||
|
{
|
||||||
|
std::ofstream previous{output_, std::ios::binary};
|
||||||
|
previous << original;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(fesa::FesaApplication{}.Run({input_.string()}), 4);
|
||||||
|
std::ifstream previous{output_, std::ios::binary};
|
||||||
|
EXPECT_TRUE(std::string(std::istreambuf_iterator<char>{previous}, {}) ==
|
||||||
|
original);
|
||||||
|
EXPECT_EQ(std::filesystem::file_size(log_), 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "eigen_reference_comparison.h"
|
||||||
|
#include "fesa/app/fesa_application.h"
|
||||||
|
|
||||||
|
#ifndef FESA_TEST_SOURCE_DIR
|
||||||
|
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef FESA_TEST_BINARY_DIR
|
||||||
|
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string ReadBytes(const std::filesystem::path& path) {
|
||||||
|
std::ifstream stream{path, std::ios::binary};
|
||||||
|
if (!stream) {
|
||||||
|
throw std::runtime_error{"Unable to read declared eigen artifact."};
|
||||||
|
}
|
||||||
|
return {std::istreambuf_iterator<char>{stream},
|
||||||
|
std::istreambuf_iterator<char>{}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// E2-C25.
|
||||||
|
TEST(EigenReference, RunsOriginalInputAndComparesAllModalFrameIdentities) {
|
||||||
|
const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR};
|
||||||
|
const std::filesystem::path reference_directory =
|
||||||
|
source_root / "reference" / "shellNL-eigen-meshfine";
|
||||||
|
const std::filesystem::path input =
|
||||||
|
reference_directory / "shellNL-eigen-meshfine.inp";
|
||||||
|
const std::filesystem::path text =
|
||||||
|
reference_directory / "shellNL-eigen-meshfine eigenvalues.txt";
|
||||||
|
const std::string input_before = ReadBytes(input);
|
||||||
|
const std::string text_before = ReadBytes(text);
|
||||||
|
|
||||||
|
fesa::FesaApplication application;
|
||||||
|
const int exit_code = application.Run({input.string()});
|
||||||
|
EXPECT_EQ(ReadBytes(input), input_before);
|
||||||
|
EXPECT_EQ(ReadBytes(text), text_before);
|
||||||
|
ASSERT_EQ(exit_code, 0);
|
||||||
|
const std::filesystem::path results =
|
||||||
|
reference_directory / "shellNL-eigen-meshfine_results.h5";
|
||||||
|
ASSERT_TRUE(std::filesystem::is_regular_file(results));
|
||||||
|
const auto comparison = fesa::test::EigenReferenceComparison::Compare(
|
||||||
|
{"shellnl-eigen-meshfine", input, text, results});
|
||||||
|
ASSERT_TRUE(comparison.HasValue());
|
||||||
|
EXPECT_TRUE(comparison.Value().passed);
|
||||||
|
EXPECT_EQ(comparison.Value().source_step_name, "Step-2");
|
||||||
|
EXPECT_EQ(comparison.Value().requested_mode_count, 10U);
|
||||||
|
ASSERT_EQ(comparison.Value().rows.size(), 10U);
|
||||||
|
ASSERT_EQ(comparison.Value().families.size(), 10U);
|
||||||
|
for (std::size_t index = 0U; index < comparison.Value().rows.size();
|
||||||
|
++index) {
|
||||||
|
EXPECT_EQ(comparison.Value().rows[index].mode_id, index + 1U);
|
||||||
|
EXPECT_EQ(comparison.Value().rows[index].component, "Value");
|
||||||
|
EXPECT_EQ(comparison.Value().families[index].row_count, 1U);
|
||||||
|
EXPECT_TRUE(comparison.Value().families[index].passed);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(ReadBytes(input), input_before);
|
||||||
|
EXPECT_EQ(ReadBytes(text), text_before);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
#include "eigen_reference_comparison.h"
|
||||||
|
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <regex>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/io/abaqus/domain_mapper.h"
|
||||||
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
|
||||||
|
namespace fesa::test {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr char kModesPath[] = "/steps/Step-1/eigen/modes";
|
||||||
|
|
||||||
|
class ComparisonFailure final : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
ComparisonFailure(std::string code, std::string message)
|
||||||
|
: std::runtime_error{std::move(message)}, code_{std::move(code)} {}
|
||||||
|
|
||||||
|
const std::string& Code() const noexcept { return code_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string code_;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[noreturn]] void Fail(const std::string& code, const std::string& message) {
|
||||||
|
throw ComparisonFailure{code, message};
|
||||||
|
}
|
||||||
|
|
||||||
|
Status FailureStatus(const EigenReferenceCase& reference_case,
|
||||||
|
const std::string& code, const std::string& message) {
|
||||||
|
return Status::Failure(FailureCategory::kModel, {{Severity::kError,
|
||||||
|
code,
|
||||||
|
{},
|
||||||
|
"FREQUENCY",
|
||||||
|
reference_case.case_id,
|
||||||
|
message}});
|
||||||
|
}
|
||||||
|
|
||||||
|
class Hdf5Handle {
|
||||||
|
public:
|
||||||
|
using Closer = herr_t (*)(hid_t);
|
||||||
|
|
||||||
|
Hdf5Handle() = default;
|
||||||
|
Hdf5Handle(hid_t value, Closer closer) : value_{value}, closer_{closer} {}
|
||||||
|
Hdf5Handle(const Hdf5Handle&) = delete;
|
||||||
|
Hdf5Handle& operator=(const Hdf5Handle&) = delete;
|
||||||
|
Hdf5Handle(Hdf5Handle&& other) noexcept
|
||||||
|
: value_{other.value_}, closer_{other.closer_} {
|
||||||
|
other.value_ = -1;
|
||||||
|
}
|
||||||
|
~Hdf5Handle() {
|
||||||
|
if (value_ >= 0 && closer_ != nullptr) {
|
||||||
|
(void)closer_(value_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hid_t Get() const noexcept { return value_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
hid_t value_{-1};
|
||||||
|
Closer closer_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
hid_t RequireId(hid_t value, const char* message) {
|
||||||
|
if (value < 0) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema", message);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RequireHdf(herr_t value, const char* message) {
|
||||||
|
if (value < 0) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ReadUtf8Attribute(hid_t object, const char* name) {
|
||||||
|
Hdf5Handle attribute{RequireId(H5Aopen(object, name, H5P_DEFAULT),
|
||||||
|
"A required HDF5 attribute is missing."),
|
||||||
|
H5Aclose};
|
||||||
|
Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()),
|
||||||
|
"Unable to inspect an HDF5 attribute type."),
|
||||||
|
H5Tclose};
|
||||||
|
if (H5Tget_class(type.Get()) != H5T_STRING ||
|
||||||
|
H5Tget_cset(type.Get()) != H5T_CSET_UTF8 ||
|
||||||
|
H5Tis_variable_str(type.Get()) <= 0) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema",
|
||||||
|
"A required HDF5 string attribute has the wrong type.");
|
||||||
|
}
|
||||||
|
Hdf5Handle space{RequireId(H5Aget_space(attribute.Get()),
|
||||||
|
"Unable to inspect an HDF5 attribute shape."),
|
||||||
|
H5Sclose};
|
||||||
|
if (H5Sget_simple_extent_ndims(space.Get()) != 0) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema",
|
||||||
|
"A required HDF5 string attribute is not scalar.");
|
||||||
|
}
|
||||||
|
char* value = nullptr;
|
||||||
|
RequireHdf(H5Aread(attribute.Get(), type.Get(), &value),
|
||||||
|
"Unable to read an HDF5 string attribute.");
|
||||||
|
if (value == nullptr) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema", "An HDF5 string attribute is null.");
|
||||||
|
}
|
||||||
|
const std::string result{value};
|
||||||
|
RequireHdf(H5free_memory(value), "Unable to release HDF5 string memory.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t ReadUint64Attribute(hid_t object, const char* name) {
|
||||||
|
Hdf5Handle attribute{RequireId(H5Aopen(object, name, H5P_DEFAULT),
|
||||||
|
"A required HDF5 attribute is missing."),
|
||||||
|
H5Aclose};
|
||||||
|
Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()),
|
||||||
|
"Unable to inspect an HDF5 attribute type."),
|
||||||
|
H5Tclose};
|
||||||
|
if (H5Tget_class(type.Get()) != H5T_INTEGER ||
|
||||||
|
H5Tget_size(type.Get()) != sizeof(std::uint64_t) ||
|
||||||
|
H5Tget_sign(type.Get()) != H5T_SGN_NONE) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema",
|
||||||
|
"A required HDF5 integer attribute has the wrong type.");
|
||||||
|
}
|
||||||
|
Hdf5Handle space{RequireId(H5Aget_space(attribute.Get()),
|
||||||
|
"Unable to inspect an HDF5 attribute shape."),
|
||||||
|
H5Sclose};
|
||||||
|
if (H5Sget_simple_extent_ndims(space.Get()) != 0) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema",
|
||||||
|
"A required HDF5 integer attribute is not scalar.");
|
||||||
|
}
|
||||||
|
std::uint64_t value = 0U;
|
||||||
|
RequireHdf(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value),
|
||||||
|
"Unable to read an HDF5 integer attribute.");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Hdf5Mode {
|
||||||
|
std::uint64_t mode_id;
|
||||||
|
double eigenvalue;
|
||||||
|
std::uint8_t classification;
|
||||||
|
std::uint64_t cluster_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Hdf5Mode> ReadModes(hid_t file, std::uint64_t expected_count,
|
||||||
|
const std::string& source_step_name) {
|
||||||
|
Hdf5Handle dataset{RequireId(H5Dopen2(file, kModesPath, H5P_DEFAULT),
|
||||||
|
"The eigen modes dataset is missing."),
|
||||||
|
H5Dclose};
|
||||||
|
Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()),
|
||||||
|
"Unable to inspect the modes dataspace."),
|
||||||
|
H5Sclose};
|
||||||
|
if (H5Sget_simple_extent_ndims(space.Get()) != 1) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema", "The modes dataset must be rank one.");
|
||||||
|
}
|
||||||
|
hsize_t count = 0U;
|
||||||
|
RequireHdf(H5Sget_simple_extent_dims(space.Get(), &count, nullptr),
|
||||||
|
"Unable to inspect the modes dataset extent.");
|
||||||
|
if (count != expected_count) {
|
||||||
|
Fail("eigen-mode-identity-mismatch",
|
||||||
|
"The modes dataset count does not equal requested mode count.");
|
||||||
|
}
|
||||||
|
Hdf5Handle type{RequireId(H5Dget_type(dataset.Get()),
|
||||||
|
"Unable to inspect the modes dataset type."),
|
||||||
|
H5Tclose};
|
||||||
|
if (H5Tget_class(type.Get()) != H5T_COMPOUND ||
|
||||||
|
H5Tget_nmembers(type.Get()) != 4 ||
|
||||||
|
H5Tget_member_index(type.Get(), "mode_id") < 0 ||
|
||||||
|
H5Tget_member_index(type.Get(), "eigenvalue") < 0 ||
|
||||||
|
H5Tget_member_index(type.Get(), "classification") < 0 ||
|
||||||
|
H5Tget_member_index(type.Get(), "cluster_id") < 0) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema", "The modes compound schema is invalid.");
|
||||||
|
}
|
||||||
|
const std::array<hid_t, 4> expected_types{H5T_STD_U64LE, H5T_IEEE_F64LE,
|
||||||
|
H5T_STD_U8LE, H5T_STD_U64LE};
|
||||||
|
const std::array<const char*, 4> names{"mode_id", "eigenvalue",
|
||||||
|
"classification", "cluster_id"};
|
||||||
|
for (std::size_t index = 0U; index < names.size(); ++index) {
|
||||||
|
Hdf5Handle member{
|
||||||
|
RequireId(H5Tget_member_type(type.Get(), static_cast<unsigned>(index)),
|
||||||
|
"Unable to inspect a modes compound member."),
|
||||||
|
H5Tclose};
|
||||||
|
char* raw_name =
|
||||||
|
H5Tget_member_name(type.Get(), static_cast<unsigned>(index));
|
||||||
|
if (raw_name == nullptr) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema",
|
||||||
|
"Unable to inspect a modes member name.");
|
||||||
|
}
|
||||||
|
const std::string member_name{raw_name};
|
||||||
|
RequireHdf(H5free_memory(raw_name),
|
||||||
|
"Unable to release a modes member name.");
|
||||||
|
if (member_name != names[index] ||
|
||||||
|
H5Tequal(member.Get(), expected_types[index]) <= 0) {
|
||||||
|
Fail("invalid-eigen-hdf5-schema",
|
||||||
|
"The modes compound member type or order is invalid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Hdf5Handle memory_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(Hdf5Mode)),
|
||||||
|
"Unable to create a modes memory type."),
|
||||||
|
H5Tclose};
|
||||||
|
RequireHdf(H5Tinsert(memory_type.Get(), "mode_id", HOFFSET(Hdf5Mode, mode_id),
|
||||||
|
H5T_NATIVE_UINT64),
|
||||||
|
"Unable to define the mode identifier field.");
|
||||||
|
RequireHdf(H5Tinsert(memory_type.Get(), "eigenvalue",
|
||||||
|
HOFFSET(Hdf5Mode, eigenvalue), H5T_NATIVE_DOUBLE),
|
||||||
|
"Unable to define the eigenvalue field.");
|
||||||
|
RequireHdf(H5Tinsert(memory_type.Get(), "classification",
|
||||||
|
HOFFSET(Hdf5Mode, classification), H5T_NATIVE_UINT8),
|
||||||
|
"Unable to define the classification field.");
|
||||||
|
RequireHdf(H5Tinsert(memory_type.Get(), "cluster_id",
|
||||||
|
HOFFSET(Hdf5Mode, cluster_id), H5T_NATIVE_UINT64),
|
||||||
|
"Unable to define the cluster field.");
|
||||||
|
std::vector<Hdf5Mode> modes(static_cast<std::size_t>(count));
|
||||||
|
RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, modes.data()),
|
||||||
|
"Unable to read the modes dataset.");
|
||||||
|
for (std::size_t index = 0U; index < modes.size(); ++index) {
|
||||||
|
if (modes[index].mode_id != index + 1U ||
|
||||||
|
modes[index].mode_id > static_cast<std::uint64_t>((
|
||||||
|
std::numeric_limits<std::int64_t>::max)()) ||
|
||||||
|
!std::isfinite(modes[index].eigenvalue)) {
|
||||||
|
Fail("eigen-mode-identity-mismatch",
|
||||||
|
"Mode IDs must be ordered one through requested mode count.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ReadUtf8Attribute(dataset.Get(), "quantity") != "eigenvalue" ||
|
||||||
|
ReadUint64Attribute(dataset.Get(), "eigen_schema_version") != 1U ||
|
||||||
|
ReadUtf8Attribute(dataset.Get(), "source_step_name") !=
|
||||||
|
source_step_name ||
|
||||||
|
ReadUtf8Attribute(dataset.Get(), "domain") != "mode" ||
|
||||||
|
ReadUtf8Attribute(dataset.Get(), "component_names") != "Value" ||
|
||||||
|
ReadUtf8Attribute(dataset.Get(), "component_unit_dimensions") !=
|
||||||
|
"time^-2" ||
|
||||||
|
ReadUtf8Attribute(dataset.Get(), "coordinate_system") != "scalar" ||
|
||||||
|
ReadUtf8Attribute(dataset.Get(), "location") != "modal-frame") {
|
||||||
|
Fail("invalid-eigen-hdf5-schema", "Modes dataset metadata is invalid.");
|
||||||
|
}
|
||||||
|
return modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ParseFiniteNumber(const std::string& token) {
|
||||||
|
static const std::regex number{
|
||||||
|
R"([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)"};
|
||||||
|
if (!std::regex_match(token, number)) {
|
||||||
|
Fail("invalid-eigen-reference-text", "A TXT numeric token is malformed.");
|
||||||
|
}
|
||||||
|
double value = 0.0;
|
||||||
|
const char* first = token.data() + (token.front() == '+' ? 1U : 0U);
|
||||||
|
const auto conversion =
|
||||||
|
std::from_chars(first, token.data() + token.size(), value);
|
||||||
|
if (conversion.ec != std::errc{} ||
|
||||||
|
conversion.ptr != token.data() + token.size() || !std::isfinite(value)) {
|
||||||
|
Fail("invalid-eigen-reference-text", "A TXT Value is nonfinite.");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RequireNumberSyntax(const std::string& token) {
|
||||||
|
static const std::regex number{
|
||||||
|
R"([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)"};
|
||||||
|
if (!std::regex_match(token, number)) {
|
||||||
|
Fail("invalid-eigen-reference-text", "A TXT numeric token is malformed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::uint64_t, double> ReadReferenceText(
|
||||||
|
const std::filesystem::path& path) {
|
||||||
|
std::ifstream stream{path, std::ios::binary};
|
||||||
|
if (!stream) {
|
||||||
|
Fail("needs-reference-artifacts",
|
||||||
|
"The eigenvalue TXT artifact is missing.");
|
||||||
|
}
|
||||||
|
const std::regex row{
|
||||||
|
R"(^[ \t]*Frame:[ \t]+Mode[ \t]+([0-9]+):[ \t]+Value[ \t]*=[ \t]*([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)[ \t]+Freq[ \t]*=[ \t]*([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)[ \t]+\(cycles/time\)[ \t]*$)"};
|
||||||
|
std::map<std::uint64_t, double> rows;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(stream, line)) {
|
||||||
|
if (!line.empty() && line.back() == '\r') {
|
||||||
|
line.pop_back();
|
||||||
|
}
|
||||||
|
if (line.find_first_not_of(" \t") == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::smatch match;
|
||||||
|
if (!std::regex_match(line, match, row)) {
|
||||||
|
Fail("invalid-eigen-reference-text",
|
||||||
|
"A TXT row is not exact Frame/Mode syntax.");
|
||||||
|
}
|
||||||
|
std::uint64_t mode_id = 0U;
|
||||||
|
const std::string mode_text = match[1].str();
|
||||||
|
const auto parsed = std::from_chars(
|
||||||
|
mode_text.data(), mode_text.data() + mode_text.size(), mode_id);
|
||||||
|
if (parsed.ec != std::errc{} ||
|
||||||
|
parsed.ptr != mode_text.data() + mode_text.size() || mode_id == 0U ||
|
||||||
|
mode_id > static_cast<std::uint64_t>(
|
||||||
|
(std::numeric_limits<std::int64_t>::max)()) ||
|
||||||
|
!rows.emplace(mode_id, ParseFiniteNumber(match[2].str())).second) {
|
||||||
|
Fail("eigen-mode-identity-mismatch",
|
||||||
|
"TXT mode IDs must be unique positive integers.");
|
||||||
|
}
|
||||||
|
RequireNumberSyntax(match[3].str());
|
||||||
|
}
|
||||||
|
if (rows.empty()) {
|
||||||
|
Fail("invalid-eigen-reference-text",
|
||||||
|
"The eigenvalue TXT artifact is empty.");
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result<EigenReferenceComparisonReport> EigenReferenceComparison::Compare(
|
||||||
|
const EigenReferenceCase& reference_case) {
|
||||||
|
try {
|
||||||
|
auto parsed = AbaqusInputReader{}.Read(reference_case.input_path);
|
||||||
|
if (!parsed.HasValue()) {
|
||||||
|
Fail("needs-reference-artifacts", "The eigen input cannot be parsed.");
|
||||||
|
}
|
||||||
|
auto domain = AbaqusDomainMapper{}.Map(parsed.Value());
|
||||||
|
if (!domain.HasValue() || domain.Value().Frequency() == nullptr) {
|
||||||
|
Fail("needs-reference-artifacts",
|
||||||
|
"The input has no sole frequency step.");
|
||||||
|
}
|
||||||
|
const FrequencyStepDefinition& frequency = *domain.Value().Frequency();
|
||||||
|
const std::uint64_t requested =
|
||||||
|
static_cast<std::uint64_t>(frequency.requested_mode_count);
|
||||||
|
const auto reference =
|
||||||
|
ReadReferenceText(reference_case.eigenvalues_text_path);
|
||||||
|
Hdf5Handle file{
|
||||||
|
RequireId(H5Fopen(reference_case.results_hdf5_path.string().c_str(),
|
||||||
|
H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||||
|
"The eigen HDF5 result is missing."),
|
||||||
|
H5Fclose};
|
||||||
|
Hdf5Handle metadata{
|
||||||
|
RequireId(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT),
|
||||||
|
"The metadata group is missing."),
|
||||||
|
H5Gclose};
|
||||||
|
if (ReadUint64Attribute(metadata.Get(), "schema_version") != 0U ||
|
||||||
|
ReadUint64Attribute(metadata.Get(), "eigen_schema_version") != 1U ||
|
||||||
|
ReadUtf8Attribute(metadata.Get(), "feature_id") !=
|
||||||
|
"mitc4-shell-feast-eigen2" ||
|
||||||
|
ReadUtf8Attribute(metadata.Get(), "analysis_procedure") !=
|
||||||
|
"linear-frequency" ||
|
||||||
|
ReadUtf8Attribute(metadata.Get(), "mass_variant") != "derived-lumped" ||
|
||||||
|
ReadUtf8Attribute(metadata.Get(), "normalization") != "mass" ||
|
||||||
|
ReadUtf8Attribute(metadata.Get(), "eigensolver_backend") !=
|
||||||
|
"MKL FEAST") {
|
||||||
|
Fail("invalid-eigen-hdf5-schema",
|
||||||
|
"The eigen result discriminator is invalid.");
|
||||||
|
}
|
||||||
|
const std::string expected_identity =
|
||||||
|
"path=" + parsed.Value().source_path.generic_string() +
|
||||||
|
";content_identity=" + parsed.Value().source_content_identity;
|
||||||
|
if (ReadUtf8Attribute(metadata.Get(), "source_input_identity") !=
|
||||||
|
expected_identity) {
|
||||||
|
Fail("eigen-source-identity-mismatch",
|
||||||
|
"The HDF5 source input identity differs from the executed input.");
|
||||||
|
}
|
||||||
|
Hdf5Handle step{
|
||||||
|
RequireId(H5Gopen2(file.Get(), "/steps/Step-1", H5P_DEFAULT),
|
||||||
|
"The eigen storage step is missing."),
|
||||||
|
H5Gclose};
|
||||||
|
if (ReadUtf8Attribute(step.Get(), "source_step_name") !=
|
||||||
|
frequency.source_step_name_raw ||
|
||||||
|
ReadUint64Attribute(step.Get(), "requested_mode_count") != requested ||
|
||||||
|
ReadUint64Attribute(step.Get(), "returned_mode_count") != requested) {
|
||||||
|
Fail("eigen-mode-identity-mismatch",
|
||||||
|
"The HDF5 source step or requested mode count is inconsistent.");
|
||||||
|
}
|
||||||
|
const auto modes =
|
||||||
|
ReadModes(file.Get(), requested, frequency.source_step_name_raw);
|
||||||
|
if (reference.size() != modes.size()) {
|
||||||
|
Fail("eigen-mode-identity-mismatch",
|
||||||
|
"TXT and HDF5 mode inventories have different sizes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
EigenReferenceComparisonReport report{};
|
||||||
|
report.case_id = reference_case.case_id;
|
||||||
|
report.source_step_name = frequency.source_step_name_raw;
|
||||||
|
report.requested_mode_count = requested;
|
||||||
|
std::vector<ReferenceToleranceFamily> families;
|
||||||
|
families.reserve(modes.size());
|
||||||
|
for (const auto& mode : modes) {
|
||||||
|
const auto row = reference.find(mode.mode_id);
|
||||||
|
if (row == reference.end()) {
|
||||||
|
Fail("eigen-mode-identity-mismatch",
|
||||||
|
"A HDF5 mode is missing from TXT.");
|
||||||
|
}
|
||||||
|
families.push_back({"case=" + reference_case.case_id +
|
||||||
|
";source_step=" + frequency.source_step_name_raw +
|
||||||
|
";mode_id=" + std::to_string(mode.mode_id) +
|
||||||
|
";component=Value",
|
||||||
|
{"Value"},
|
||||||
|
true,
|
||||||
|
{{mode.eigenvalue, row->second}}});
|
||||||
|
}
|
||||||
|
const auto evaluation = ReferenceTolerancePolicy::Evaluate(families);
|
||||||
|
if (!evaluation.valid || evaluation.families.size() != families.size()) {
|
||||||
|
Fail("nonfinite-comparison-value",
|
||||||
|
"Tolerance evaluation rejected modal values.");
|
||||||
|
}
|
||||||
|
report.families = evaluation.families;
|
||||||
|
report.passed = evaluation.passed;
|
||||||
|
for (std::size_t index = 0U; index < modes.size(); ++index) {
|
||||||
|
const auto& policy = report.families[index];
|
||||||
|
report.rows.push_back(
|
||||||
|
{frequency.source_step_name_raw, modes[index].mode_id, "Value",
|
||||||
|
modes[index].eigenvalue, reference.at(modes[index].mode_id),
|
||||||
|
policy.rows[0U]});
|
||||||
|
}
|
||||||
|
return Result<EigenReferenceComparisonReport>::Success(std::move(report));
|
||||||
|
} catch (const ComparisonFailure& exception) {
|
||||||
|
return Result<EigenReferenceComparisonReport>::Failure(
|
||||||
|
FailureStatus(reference_case, exception.Code(), exception.what()));
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
return Result<EigenReferenceComparisonReport>::Failure(
|
||||||
|
FailureStatus(reference_case, "eigen-reference-comparison-failure",
|
||||||
|
exception.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::test
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#ifndef FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
|
||||||
|
#define FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "reference_tolerance_policy.h"
|
||||||
|
|
||||||
|
namespace fesa::test {
|
||||||
|
|
||||||
|
struct EigenReferenceCase {
|
||||||
|
std::string case_id;
|
||||||
|
std::filesystem::path input_path;
|
||||||
|
std::filesystem::path eigenvalues_text_path;
|
||||||
|
std::filesystem::path results_hdf5_path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EigenReferenceRowDecision {
|
||||||
|
std::string source_step_name;
|
||||||
|
std::uint64_t mode_id;
|
||||||
|
std::string component;
|
||||||
|
double fesa_value;
|
||||||
|
double reference_value;
|
||||||
|
ReferenceToleranceRowDecision tolerance;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EigenReferenceComparisonReport {
|
||||||
|
std::string case_id;
|
||||||
|
std::string source_step_name;
|
||||||
|
std::uint64_t requested_mode_count;
|
||||||
|
std::vector<EigenReferenceRowDecision> rows;
|
||||||
|
std::vector<ReferenceToleranceFamilyDecision> families;
|
||||||
|
bool passed;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Compares modal eigenvalues without changing the approved input or
|
||||||
|
/// TXT.
|
||||||
|
class EigenReferenceComparison {
|
||||||
|
public:
|
||||||
|
static Result<EigenReferenceComparisonReport> Compare(
|
||||||
|
const EigenReferenceCase& reference_case);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::test
|
||||||
|
|
||||||
|
#endif // FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#include "eigen_reference_comparison.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
|
||||||
|
// The direct fixture is intentionally kept in this test file: it exercises the
|
||||||
|
// comparator contract without depending on the application or HDF5 writer.
|
||||||
|
namespace {
|
||||||
|
std::filesystem::path UniqueFixtureDirectory() {
|
||||||
|
static std::atomic<std::uint64_t> serial{0U};
|
||||||
|
const auto ticks =
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||||
|
return std::filesystem::temp_directory_path() /
|
||||||
|
("fesa-eigen-direct-" + std::to_string(ticks) + "-" +
|
||||||
|
std::to_string(serial.fetch_add(1U)));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Mode {
|
||||||
|
std::uint64_t mode_id;
|
||||||
|
double eigenvalue;
|
||||||
|
std::uint8_t classification;
|
||||||
|
std::uint64_t cluster_id;
|
||||||
|
};
|
||||||
|
void Str(hid_t o, const char* n, const std::string& v) {
|
||||||
|
hid_t t = H5Tcopy(H5T_C_S1);
|
||||||
|
H5Tset_size(t, H5T_VARIABLE);
|
||||||
|
H5Tset_cset(t, H5T_CSET_UTF8);
|
||||||
|
hid_t s = H5Screate(H5S_SCALAR),
|
||||||
|
a = H5Acreate2(o, n, t, s, H5P_DEFAULT, H5P_DEFAULT);
|
||||||
|
const char* p = v.c_str();
|
||||||
|
H5Awrite(a, t, &p);
|
||||||
|
H5Aclose(a);
|
||||||
|
H5Sclose(s);
|
||||||
|
H5Tclose(t);
|
||||||
|
}
|
||||||
|
void U(hid_t o, const char* n, std::uint64_t v) {
|
||||||
|
hid_t s = H5Screate(H5S_SCALAR),
|
||||||
|
a = H5Acreate2(o, n, H5T_STD_U64LE, s, H5P_DEFAULT, H5P_DEFAULT);
|
||||||
|
H5Awrite(a, H5T_NATIVE_UINT64, &v);
|
||||||
|
H5Aclose(a);
|
||||||
|
H5Sclose(s);
|
||||||
|
}
|
||||||
|
TEST(EigenReferenceComparison,
|
||||||
|
DirectSchemaFixtureAcceptsRepeatedReorderedModes) {
|
||||||
|
const auto d = UniqueFixtureDirectory();
|
||||||
|
std::error_code e;
|
||||||
|
std::filesystem::create_directories(d, e);
|
||||||
|
const auto inp = d / "x.inp", txt = d / "x.txt", h5 = d / "x.h5";
|
||||||
|
std::ofstream(inp) << "*Part,name=P\n*Node\n1,0,0,0\n2,1,0,0\n3,1,1,0\n4,0,1,"
|
||||||
|
"0\n*Element,type=S4\n1,1,2,3,4\n*Elset,elset=E\n1\n*"
|
||||||
|
"Shell Section,elset=E,material=M\n1.\n*End "
|
||||||
|
"Part\n*Assembly,name=A\n*Instance,name=I,part=P\n*End "
|
||||||
|
"Instance\n*Nset,nset=R,instance=I\n1\n*End "
|
||||||
|
"Assembly\n*Material,name=M\n*Density\n1.\n*Elastic\n1."
|
||||||
|
",0.3\n*Boundary\nR,1,6\n*Step,name=Step-2,nlgeom=NO\n*"
|
||||||
|
"Frequency\n2\n*End Step\n";
|
||||||
|
std::ofstream(txt)
|
||||||
|
<< "Frame: Mode 2: Value = 100 Freq = 1e999999 (cycles/time)\nFrame: "
|
||||||
|
"Mode 1: Value = 100 Freq = 1 (cycles/time)\n";
|
||||||
|
auto p = fesa::AbaqusInputReader{}.Read(inp);
|
||||||
|
ASSERT_TRUE(p.HasValue());
|
||||||
|
hid_t f = H5Fcreate(h5.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT),
|
||||||
|
m = H5Gcreate2(f, "/metadata", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||||
|
U(m, "schema_version", 0);
|
||||||
|
U(m, "eigen_schema_version", 1);
|
||||||
|
Str(m, "feature_id", "mitc4-shell-feast-eigen2");
|
||||||
|
Str(m, "analysis_procedure", "linear-frequency");
|
||||||
|
Str(m, "mass_variant", "derived-lumped");
|
||||||
|
Str(m, "normalization", "mass");
|
||||||
|
Str(m, "eigensolver_backend", "MKL FEAST");
|
||||||
|
Str(m, "source_input_identity",
|
||||||
|
"path=" + p.Value().source_path.generic_string() +
|
||||||
|
";content_identity=" + p.Value().source_content_identity);
|
||||||
|
H5Gclose(m);
|
||||||
|
hid_t ss = H5Gcreate2(f, "/steps", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
s = H5Gcreate2(ss, "Step-1", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||||
|
Str(s, "source_step_name", "Step-2");
|
||||||
|
U(s, "requested_mode_count", 2);
|
||||||
|
U(s, "returned_mode_count", 2);
|
||||||
|
hid_t g = H5Gcreate2(s, "eigen", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||||
|
hsize_t n = 2;
|
||||||
|
hid_t sp = H5Screate_simple(1, &n, nullptr),
|
||||||
|
t = H5Tcreate(H5T_COMPOUND, sizeof(Mode));
|
||||||
|
H5Tinsert(t, "mode_id", HOFFSET(Mode, mode_id), H5T_STD_U64LE);
|
||||||
|
H5Tinsert(t, "eigenvalue", HOFFSET(Mode, eigenvalue), H5T_IEEE_F64LE);
|
||||||
|
H5Tinsert(t, "classification", HOFFSET(Mode, classification), H5T_STD_U8LE);
|
||||||
|
H5Tinsert(t, "cluster_id", HOFFSET(Mode, cluster_id), H5T_STD_U64LE);
|
||||||
|
hid_t ds =
|
||||||
|
H5Dcreate2(g, "modes", t, sp, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
||||||
|
Mode v[] = {{1, 100, 1, 1}, {2, 100, 1, 1}};
|
||||||
|
H5Dwrite(ds, t, H5S_ALL, H5S_ALL, H5P_DEFAULT, v);
|
||||||
|
U(ds, "eigen_schema_version", 1);
|
||||||
|
Str(ds, "source_step_name", "Step-2");
|
||||||
|
Str(ds, "domain", "mode");
|
||||||
|
Str(ds, "quantity", "eigenvalue");
|
||||||
|
Str(ds, "component_names", "Value");
|
||||||
|
Str(ds, "component_unit_dimensions", "time^-2");
|
||||||
|
Str(ds, "coordinate_system", "scalar");
|
||||||
|
Str(ds, "location", "modal-frame");
|
||||||
|
H5Dclose(ds);
|
||||||
|
H5Tclose(t);
|
||||||
|
H5Sclose(sp);
|
||||||
|
H5Gclose(g);
|
||||||
|
H5Gclose(s);
|
||||||
|
H5Gclose(ss);
|
||||||
|
H5Fclose(f);
|
||||||
|
auto reference_case = fesa::test::EigenReferenceCase{"x", inp, txt, h5};
|
||||||
|
auto ok = fesa::test::EigenReferenceComparison::Compare(reference_case);
|
||||||
|
ASSERT_TRUE(ok.HasValue());
|
||||||
|
EXPECT_EQ(ok.Value().families.size(), 2U);
|
||||||
|
struct TextMutation {
|
||||||
|
std::string text;
|
||||||
|
std::string expected_code;
|
||||||
|
};
|
||||||
|
const std::vector<TextMutation> invalid_texts{
|
||||||
|
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
|
||||||
|
"eigen-mode-identity-mismatch"},
|
||||||
|
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n"
|
||||||
|
"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
|
||||||
|
"eigen-mode-identity-mismatch"},
|
||||||
|
{"Frame: Mode 1: Value = nan Freq = 1 (cycles/time)\n"
|
||||||
|
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||||
|
"invalid-eigen-reference-text"},
|
||||||
|
{"Frame: Mode 184467440737095516160: Value = 100 Freq = 1 (cycles/time)\n"
|
||||||
|
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||||
|
"eigen-mode-identity-mismatch"},
|
||||||
|
{"Frame: Mode -1: Value = 100 Freq = 1 (cycles/time)\n"
|
||||||
|
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||||
|
"invalid-eigen-reference-text"},
|
||||||
|
{"Frame: Mode 3: Value = 100 Freq = 1 (cycles/time)\n"
|
||||||
|
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
|
||||||
|
"eigen-mode-identity-mismatch"}};
|
||||||
|
for (const TextMutation& invalid_text : invalid_texts) {
|
||||||
|
std::ofstream(txt, std::ios::trunc) << invalid_text.text;
|
||||||
|
const auto comparison =
|
||||||
|
fesa::test::EigenReferenceComparison::Compare(reference_case);
|
||||||
|
ASSERT_FALSE(comparison.HasValue());
|
||||||
|
ASSERT_EQ(comparison.GetStatus().Diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(comparison.GetStatus().Diagnostics()[0U].code,
|
||||||
|
invalid_text.expected_code);
|
||||||
|
}
|
||||||
|
std::ofstream(txt, std::ios::trunc)
|
||||||
|
<< "Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\nFrame: Mode 2: "
|
||||||
|
"Value = 100 Freq = 1 (cycles/time)\n";
|
||||||
|
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||||
|
m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
|
||||||
|
H5Adelete(m, "source_input_identity");
|
||||||
|
Str(m, "source_input_identity", "wrong");
|
||||||
|
H5Gclose(m);
|
||||||
|
H5Fclose(f);
|
||||||
|
EXPECT_FALSE(
|
||||||
|
fesa::test::EigenReferenceComparison::Compare(reference_case).HasValue());
|
||||||
|
std::filesystem::remove_all(d, e);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
#include "nonlinear_static_reference_comparison.h"
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -13,6 +11,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "nonlinear_static_reference_comparison.h"
|
||||||
|
|
||||||
#ifndef FESA_TEST_SOURCE_DIR
|
#ifndef FESA_TEST_SOURCE_DIR
|
||||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||||
#endif
|
#endif
|
||||||
@@ -34,13 +34,10 @@ std::string ReadBytes(const std::filesystem::path& path) {
|
|||||||
|
|
||||||
fesa::test::NonlinearReferenceCase MakeCase(const std::string& case_id) {
|
fesa::test::NonlinearReferenceCase MakeCase(const std::string& case_id) {
|
||||||
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
||||||
const bool fine = case_id == "shellnl-meshfine-s4-final";
|
const bool nonlinear = case_id == "shellnl-s4-final";
|
||||||
const bool nonlinear = case_id == "shellnl-s4-final" || fine;
|
|
||||||
const std::filesystem::path directory =
|
const std::filesystem::path directory =
|
||||||
root / "reference" / (fine ? "shellNL-meshfine"
|
root / "reference" / (nonlinear ? "shellNL" : "shell");
|
||||||
: (nonlinear ? "shellNL" : "shell"));
|
const std::string stem = nonlinear ? "shellNL" : "shell";
|
||||||
const std::string stem = fine ? "shellNL-meshfine"
|
|
||||||
: (nonlinear ? "shellNL" : "shell");
|
|
||||||
return {case_id,
|
return {case_id,
|
||||||
directory / (stem + ".inp"),
|
directory / (stem + ".inp"),
|
||||||
directory / (stem + " displacements.csv"),
|
directory / (stem + " displacements.csv"),
|
||||||
@@ -105,8 +102,7 @@ void ExpectInternalFamiliesPassed(
|
|||||||
<< family.family_identity << " rows=" << family.row_count
|
<< family.family_identity << " rows=" << family.row_count
|
||||||
<< " numerator=" << family.numerator << " scale=" << family.scale
|
<< " numerator=" << family.numerator << " scale=" << family.scale
|
||||||
<< " metric="
|
<< " metric="
|
||||||
<< (family.normalized_metric.has_value()
|
<< (family.normalized_metric.has_value() ? *family.normalized_metric
|
||||||
? *family.normalized_metric
|
|
||||||
: -1.0)
|
: -1.0)
|
||||||
<< " diagnostic=" << family.diagnostic_code;
|
<< " diagnostic=" << family.diagnostic_code;
|
||||||
}
|
}
|
||||||
@@ -159,14 +155,14 @@ TEST(NonlinearStaticReference,
|
|||||||
EXPECT_EQ(generated.Value().copied_input_path, copied.Value());
|
EXPECT_EQ(generated.Value().copied_input_path, copied.Value());
|
||||||
EXPECT_TRUE(std::filesystem::is_regular_file(
|
EXPECT_TRUE(std::filesystem::is_regular_file(
|
||||||
generated.Value().generated_results_hdf5));
|
generated.Value().generated_results_hdf5));
|
||||||
EXPECT_TRUE(std::filesystem::is_regular_file(
|
EXPECT_TRUE(
|
||||||
generated.Value().generated_log_path));
|
std::filesystem::is_regular_file(generated.Value().generated_log_path));
|
||||||
EXPECT_EQ(generated.Value().generated_results_hdf5.filename(),
|
EXPECT_EQ(generated.Value().generated_results_hdf5.filename(),
|
||||||
case_id == "shellnl-s4-final" ? "shellNL_results.h5"
|
case_id == "shellnl-s4-final" ? "shellNL_results.h5"
|
||||||
: "shell_results.h5");
|
: "shell_results.h5");
|
||||||
EXPECT_EQ(generated.Value().generated_log_path.filename(),
|
EXPECT_EQ(
|
||||||
case_id == "shellnl-s4-final" ? "shellNL_log.txt"
|
generated.Value().generated_log_path.filename(),
|
||||||
: "shell_log.txt");
|
case_id == "shellnl-s4-final" ? "shellNL_log.txt" : "shell_log.txt");
|
||||||
|
|
||||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
||||||
reference_case, generated.Value().generated_results_hdf5);
|
reference_case, generated.Value().generated_results_hdf5);
|
||||||
@@ -184,11 +180,10 @@ TEST(NonlinearStaticReference,
|
|||||||
case_id == "shellnl-s4-final" ? "coarse.translation"
|
case_id == "shellnl-s4-final" ? "coarse.translation"
|
||||||
: "linear.translation",
|
: "linear.translation",
|
||||||
147U, fesa::test::NonlinearReferenceDisposition::kBlocking);
|
147U, fesa::test::NonlinearReferenceDisposition::kBlocking);
|
||||||
ExpectFamily(comparison.Value(),
|
ExpectFamily(
|
||||||
case_id == "shellnl-s4-final" ? "coarse.rotation"
|
comparison.Value(),
|
||||||
: "linear.rotation",
|
case_id == "shellnl-s4-final" ? "coarse.rotation" : "linear.rotation",
|
||||||
147U,
|
147U, fesa::test::NonlinearReferenceDisposition::kWarningOnly);
|
||||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
|
|
||||||
if (case_id == "shellnl-s4-final") {
|
if (case_id == "shellnl-s4-final") {
|
||||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force",
|
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force",
|
||||||
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||||
@@ -219,8 +214,8 @@ TEST(NonlinearStaticReference,
|
|||||||
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
|
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
|
||||||
stream << "not an HDF5 file";
|
stream << "not an HDF5 file";
|
||||||
stream.close();
|
stream.close();
|
||||||
EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case,
|
EXPECT_FALSE(
|
||||||
invalid_hdf5)
|
fesa::test::CompareHdf5ToReferenceCsv(reference_case, invalid_hdf5)
|
||||||
.HasValue());
|
.HasValue());
|
||||||
|
|
||||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||||
@@ -230,16 +225,28 @@ TEST(NonlinearStaticReference,
|
|||||||
{"UR1", "UR2", "UR3"},
|
{"UR1", "UR2", "UR3"},
|
||||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||||
147U,
|
147U,
|
||||||
{"nonlinear.rotation", {"UR1", "UR2", "UR3"}, false, 1.0, 0.01,
|
{"nonlinear.rotation",
|
||||||
0U, 0.02, 0.02, 0U, false, false, "relative-rms-exceeded", {}, 147U}});
|
{"UR1", "UR2", "UR3"},
|
||||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
|
false,
|
||||||
|
1.0,
|
||||||
|
0.01,
|
||||||
|
0U,
|
||||||
|
0.02,
|
||||||
|
0.02,
|
||||||
|
0U,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"relative-rms-exceeded",
|
||||||
|
{},
|
||||||
|
147U}});
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::test::ClassifyReferenceComparison(report),
|
||||||
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NSNR-REF-020.
|
// NSNR-REF-020.
|
||||||
TEST(NonlinearStaticReference, AuditsFinePrimaryAndCoarseCompanionArtifacts) {
|
TEST(NonlinearStaticReference, AuditsRetainedStaticArtifacts) {
|
||||||
const std::array<std::string, 3> case_ids{
|
const std::array<std::string, 2> case_ids{"shellnl-s4-final",
|
||||||
"shellnl-meshfine-s4-final", "shellnl-s4-final",
|
|
||||||
"shell-s4-linear-limit"};
|
"shell-s4-linear-limit"};
|
||||||
for (const auto& case_id : case_ids) {
|
for (const auto& case_id : case_ids) {
|
||||||
const auto reference_case = MakeCase(case_id);
|
const auto reference_case = MakeCase(case_id);
|
||||||
@@ -281,16 +288,14 @@ TEST(NonlinearStaticReference,
|
|||||||
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
|
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
|
||||||
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment",
|
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment", 48U,
|
||||||
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||||
EXPECT_TRUE(comparison.Value().external_blocking_passed);
|
EXPECT_TRUE(comparison.Value().external_blocking_passed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NSNR-REF-022.
|
// NSNR-REF-022.
|
||||||
TEST(NonlinearStaticReference,
|
TEST(NonlinearStaticReference, BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
|
||||||
BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
|
for (const std::string case_id : {"shellnl-s4-final"}) {
|
||||||
for (const std::string case_id :
|
|
||||||
{"shellnl-meshfine-s4-final", "shellnl-s4-final"}) {
|
|
||||||
const auto reference_case = MakeCase(case_id);
|
const auto reference_case = MakeCase(case_id);
|
||||||
const auto copied =
|
const auto copied =
|
||||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||||
@@ -302,9 +307,7 @@ TEST(NonlinearStaticReference,
|
|||||||
reference_case, generated.Value().generated_results_hdf5);
|
reference_case, generated.Value().generated_results_hdf5);
|
||||||
ASSERT_TRUE(comparison.HasValue()) << case_id;
|
ASSERT_TRUE(comparison.HasValue()) << case_id;
|
||||||
|
|
||||||
const std::string prefix = case_id == "shellnl-meshfine-s4-final"
|
const std::string prefix = "coarse.";
|
||||||
? "fine."
|
|
||||||
: "coarse.";
|
|
||||||
const std::array<std::string, 4> identities{
|
const std::array<std::string, 4> identities{
|
||||||
prefix + "global_force_balance", prefix + "global_moment_balance",
|
prefix + "global_force_balance", prefix + "global_moment_balance",
|
||||||
prefix + "accepted_free_force_residual",
|
prefix + "accepted_free_force_residual",
|
||||||
@@ -313,7 +316,8 @@ TEST(NonlinearStaticReference,
|
|||||||
const auto found = std::find_if(
|
const auto found = std::find_if(
|
||||||
comparison.Value().internal_equilibrium.begin(),
|
comparison.Value().internal_equilibrium.begin(),
|
||||||
comparison.Value().internal_equilibrium.end(),
|
comparison.Value().internal_equilibrium.end(),
|
||||||
[&identity](const fesa::test::NonlinearInternalEquilibriumReport& row) {
|
[&identity](
|
||||||
|
const fesa::test::NonlinearInternalEquilibriumReport& row) {
|
||||||
return row.family_identity == identity;
|
return row.family_identity == identity;
|
||||||
});
|
});
|
||||||
ASSERT_NE(found, comparison.Value().internal_equilibrium.end())
|
ASSERT_NE(found, comparison.Value().internal_equilibrium.end())
|
||||||
@@ -321,8 +325,7 @@ TEST(NonlinearStaticReference,
|
|||||||
EXPECT_TRUE(found->passed)
|
EXPECT_TRUE(found->passed)
|
||||||
<< identity << " numerator=" << found->numerator
|
<< identity << " numerator=" << found->numerator
|
||||||
<< " scale=" << found->scale << " metric="
|
<< " scale=" << found->scale << " metric="
|
||||||
<< (found->normalized_metric.has_value()
|
<< (found->normalized_metric.has_value() ? *found->normalized_metric
|
||||||
? *found->normalized_metric
|
|
||||||
: -1.0)
|
: -1.0)
|
||||||
<< " diagnostic=" << found->diagnostic_code;
|
<< " diagnostic=" << found->diagnostic_code;
|
||||||
}
|
}
|
||||||
@@ -334,19 +337,26 @@ TEST(NonlinearStaticReference,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NSNR-REF-023.
|
// NSNR-REF-023.
|
||||||
TEST(NonlinearStaticReference,
|
TEST(
|
||||||
|
NonlinearStaticReference,
|
||||||
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
|
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
|
||||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||||
report.blocking_passed = true;
|
report.blocking_passed = true;
|
||||||
report.external_blocking_passed = true;
|
report.external_blocking_passed = true;
|
||||||
report.internal_blocking_passed = true;
|
report.internal_blocking_passed = true;
|
||||||
report.precheck_findings = {
|
report.precheck_findings = {
|
||||||
{"blocking", fesa::test::NonlinearReferenceDisposition::kBlocking,
|
{"blocking",
|
||||||
"missing-row", {}},
|
fesa::test::NonlinearReferenceDisposition::kBlocking,
|
||||||
{"warning", fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
"missing-row",
|
||||||
"duplicate-row", {}},
|
{}},
|
||||||
{"review", fesa::test::NonlinearReferenceDisposition::kReviewOnly,
|
{"warning",
|
||||||
"nonfinite-row", {}}};
|
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||||
|
"duplicate-row",
|
||||||
|
{}},
|
||||||
|
{"review",
|
||||||
|
fesa::test::NonlinearReferenceDisposition::kReviewOnly,
|
||||||
|
"nonfinite-row",
|
||||||
|
{}}};
|
||||||
|
|
||||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
|
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
|
||||||
fesa::test::NonlinearReferenceClassification::kFailed);
|
fesa::test::NonlinearReferenceClassification::kFailed);
|
||||||
@@ -359,22 +369,22 @@ TEST(NonlinearStaticReference,
|
|||||||
GlobalForceAndMomentBalanceUseApprovedInclusiveOneEighthGate) {
|
GlobalForceAndMomentBalanceUseApprovedInclusiveOneEighthGate) {
|
||||||
for (const std::string& identity :
|
for (const std::string& identity :
|
||||||
{"fine.global_force_balance", "fine.global_moment_balance"}) {
|
{"fine.global_force_balance", "fine.global_moment_balance"}) {
|
||||||
const auto below = fesa::test::EvaluateGlobalBalance(
|
const auto below =
|
||||||
identity, 5.0e-9, 1.0, 0.0);
|
fesa::test::EvaluateGlobalBalance(identity, 5.0e-9, 1.0, 0.0);
|
||||||
EXPECT_TRUE(below.passed) << identity;
|
EXPECT_TRUE(below.passed) << identity;
|
||||||
ASSERT_TRUE(below.normalized_metric.has_value()) << identity;
|
ASSERT_TRUE(below.normalized_metric.has_value()) << identity;
|
||||||
EXPECT_EQ(*below.normalized_metric, 5.0e-9) << identity;
|
EXPECT_EQ(*below.normalized_metric, 5.0e-9) << identity;
|
||||||
|
|
||||||
const auto boundary = fesa::test::EvaluateGlobalBalance(
|
const auto boundary =
|
||||||
identity, 1.0e-8, 1.0, 0.0);
|
fesa::test::EvaluateGlobalBalance(identity, 1.0e-8, 1.0, 0.0);
|
||||||
EXPECT_TRUE(boundary.passed) << identity;
|
EXPECT_TRUE(boundary.passed) << identity;
|
||||||
ASSERT_TRUE(boundary.normalized_metric.has_value()) << identity;
|
ASSERT_TRUE(boundary.normalized_metric.has_value()) << identity;
|
||||||
EXPECT_EQ(*boundary.normalized_metric, 1.0e-8) << identity;
|
EXPECT_EQ(*boundary.normalized_metric, 1.0e-8) << identity;
|
||||||
|
|
||||||
const auto above = fesa::test::EvaluateGlobalBalance(
|
const auto above = fesa::test::EvaluateGlobalBalance(
|
||||||
identity, std::nextafter(1.0e-8,
|
identity,
|
||||||
std::numeric_limits<double>::infinity()),
|
std::nextafter(1.0e-8, std::numeric_limits<double>::infinity()), 1.0,
|
||||||
1.0, 0.0);
|
0.0);
|
||||||
EXPECT_FALSE(above.passed) << identity;
|
EXPECT_FALSE(above.passed) << identity;
|
||||||
ASSERT_TRUE(above.normalized_metric.has_value()) << identity;
|
ASSERT_TRUE(above.normalized_metric.has_value()) << identity;
|
||||||
EXPECT_GT(*above.normalized_metric, 1.0e-8) << identity;
|
EXPECT_GT(*above.normalized_metric, 1.0e-8) << identity;
|
||||||
|
|||||||
@@ -1,198 +1,15 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <optional>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "nonlinear_static_reference_comparison.h"
|
#include "nonlinear_static_reference_comparison.h"
|
||||||
|
|
||||||
#ifndef FESA_TEST_SOURCE_DIR
|
|
||||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef FESA_TEST_BINARY_DIR
|
|
||||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
fesa::test::NonlinearReferenceCase MakePressureCase() {
|
|
||||||
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
|
||||||
const std::filesystem::path directory =
|
|
||||||
root / "reference" / "shellNL-meshfine-pressure";
|
|
||||||
return {"shellnl-meshfine-pressure-s4-follower",
|
|
||||||
directory / "shellNL-meshfine-pressure.inp",
|
|
||||||
directory / "shellNL-meshfine-pressure displacements.csv",
|
|
||||||
directory / "shellNL-meshfine-pressure reactions.csv",
|
|
||||||
directory / "shellNL-meshfine-pressure stresses.csv",
|
|
||||||
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" /
|
|
||||||
"shell-surface-pressure-load",
|
|
||||||
fesa::test::NonlinearReferenceArtifactDisposition::kRequired,
|
|
||||||
fesa::test::NonlinearReferenceArtifactDisposition::kRequired,
|
|
||||||
fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly,
|
|
||||||
fesa::test::NonlinearReferenceArtifactDisposition::kExcluded};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<std::filesystem::path> FindCachedResults(
|
|
||||||
const fesa::test::NonlinearReferenceCase& reference_case) {
|
|
||||||
const std::filesystem::path solver_library =
|
|
||||||
std::filesystem::path{FESA_TEST_BINARY_DIR} / "src" / "fesa" / "Debug" /
|
|
||||||
"fesa_solver.lib";
|
|
||||||
std::error_code error;
|
|
||||||
const auto solver_time =
|
|
||||||
std::filesystem::last_write_time(solver_library, error);
|
|
||||||
if (error ||
|
|
||||||
!std::filesystem::is_directory(reference_case.execution_directory)) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
std::vector<std::filesystem::path> candidates;
|
|
||||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(
|
|
||||||
reference_case.execution_directory, error)) {
|
|
||||||
if (error) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
if (entry.path().filename() == "shellNL-meshfine-pressure_results.h5" &&
|
|
||||||
std::filesystem::is_regular_file(entry.path()) &&
|
|
||||||
std::filesystem::last_write_time(entry.path(), error) >= solver_time &&
|
|
||||||
!error) {
|
|
||||||
candidates.push_back(entry.path());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::sort(candidates.begin(), candidates.end());
|
|
||||||
return candidates.empty()
|
|
||||||
? std::nullopt
|
|
||||||
: std::optional<std::filesystem::path>{candidates.back()};
|
|
||||||
}
|
|
||||||
|
|
||||||
const fesa::test::NonlinearReferenceComparisonReport& RunPressureComparison() {
|
|
||||||
static const auto report = [] {
|
|
||||||
const auto reference_case = MakePressureCase();
|
|
||||||
const auto cached = FindCachedResults(reference_case);
|
|
||||||
if (cached.has_value()) {
|
|
||||||
const auto comparison =
|
|
||||||
fesa::test::CompareHdf5ToReferenceCsv(reference_case, *cached);
|
|
||||||
if (comparison.HasValue()) {
|
|
||||||
return comparison.Value();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const auto copied =
|
|
||||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
|
||||||
if (!copied.HasValue()) {
|
|
||||||
throw std::runtime_error{"Unable to copy pressure reference input."};
|
|
||||||
}
|
|
||||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
|
|
||||||
reference_case, copied.Value());
|
|
||||||
if (!generated.HasValue()) {
|
|
||||||
throw std::runtime_error{"Unable to run pressure reference input."};
|
|
||||||
}
|
|
||||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
|
||||||
reference_case, generated.Value().generated_results_hdf5);
|
|
||||||
if (!comparison.HasValue()) {
|
|
||||||
throw std::runtime_error{"Unable to compare pressure reference output."};
|
|
||||||
}
|
|
||||||
return comparison.Value();
|
|
||||||
}();
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fesa::test::NonlinearReferenceFamilyReport& FindFamily(
|
|
||||||
const fesa::test::NonlinearReferenceComparisonReport& report,
|
|
||||||
const std::string& identity) {
|
|
||||||
const auto found = std::find_if(
|
|
||||||
report.families.begin(), report.families.end(),
|
|
||||||
[&identity](const fesa::test::NonlinearReferenceFamilyReport& family) {
|
|
||||||
return family.family_identity == identity;
|
|
||||||
});
|
|
||||||
if (found == report.families.end()) {
|
|
||||||
throw std::runtime_error{"Required comparison family is absent."};
|
|
||||||
}
|
|
||||||
return *found;
|
|
||||||
}
|
|
||||||
|
|
||||||
fesa::test::ReferenceToleranceFamilyDecision MakeTolerance(const bool passed) {
|
fesa::test::ReferenceToleranceFamilyDecision MakeTolerance(const bool passed) {
|
||||||
fesa::test::ReferenceToleranceFamilyDecision result{};
|
fesa::test::ReferenceToleranceFamilyDecision result{};
|
||||||
result.passed = passed;
|
result.passed = passed;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference,
|
|
||||||
AuditsExactRequiredArtifactsAndIgnoresOptionalReviewArtifacts) {
|
|
||||||
const auto artifacts = fesa::test::ArtifactCheck(MakePressureCase());
|
|
||||||
|
|
||||||
ASSERT_TRUE(artifacts.HasValue());
|
|
||||||
EXPECT_EQ(MakePressureCase().reaction_artifact_disposition,
|
|
||||||
fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly);
|
|
||||||
EXPECT_EQ(MakePressureCase().stress_artifact_disposition,
|
|
||||||
fesa::test::NonlinearReferenceArtifactDisposition::kExcluded);
|
|
||||||
ASSERT_EQ(artifacts.Value().size(), 2U);
|
|
||||||
EXPECT_EQ(artifacts.Value()[0U].byte_size, 56665U);
|
|
||||||
EXPECT_EQ(artifacts.Value()[0U].sha256,
|
|
||||||
"53718CCC9B41C23FCC1B509A51D66F1347E87ACA43A74F1D6CEC4D2085F4B90E");
|
|
||||||
EXPECT_EQ(artifacts.Value()[1U].byte_size, 111182U);
|
|
||||||
EXPECT_EQ(artifacts.Value()[1U].sha256,
|
|
||||||
"2FE53F98EAC9F04779100721C3EDF8049EB1DBFE733EBCBD819AEEED03B81198");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference,
|
|
||||||
SelectsUniqueSchemaOneFactorOneAcceptedFrame) {
|
|
||||||
const auto& report = RunPressureComparison();
|
|
||||||
|
|
||||||
EXPECT_TRUE(report.blocking_passed);
|
|
||||||
EXPECT_EQ(report.case_id, "shellnl-meshfine-pressure-s4-follower");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference,
|
|
||||||
Projects729WideRowsTo2187LogicalRowsPerFamily) {
|
|
||||||
const auto& report = RunPressureComparison();
|
|
||||||
|
|
||||||
EXPECT_EQ(FindFamily(report, "pressure_nodal_translation_blocking").row_count,
|
|
||||||
2187U);
|
|
||||||
EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").row_count,
|
|
||||||
2187U);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference,
|
|
||||||
MatchesByInstanceNodeLabelAndComponentNotRowOrder) {
|
|
||||||
const auto& report = RunPressureComparison();
|
|
||||||
|
|
||||||
const auto& translation =
|
|
||||||
FindFamily(report, "pressure_nodal_translation_blocking");
|
|
||||||
EXPECT_EQ(translation.components,
|
|
||||||
(std::vector<std::string>{"U1", "U2", "U3"}));
|
|
||||||
EXPECT_EQ(translation.tolerance.rows.size(), 2187U);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(
|
|
||||||
ShellSurfacePressureReference,
|
|
||||||
RejectsSchemaIdentityCardinalityMissingExtraDuplicateAndNonfiniteBeforeTolerance) {
|
|
||||||
const auto& report = RunPressureComparison();
|
|
||||||
const std::filesystem::path invalid_hdf5 =
|
|
||||||
report.generated_results_hdf5.parent_path() / "invalid-pressure.h5";
|
|
||||||
{
|
|
||||||
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
|
|
||||||
stream << "not an HDF5 file";
|
|
||||||
}
|
|
||||||
EXPECT_FALSE(
|
|
||||||
fesa::test::CompareHdf5ToReferenceCsv(MakePressureCase(), invalid_hdf5)
|
|
||||||
.HasValue());
|
|
||||||
|
|
||||||
fesa::test::NonlinearReferenceComparisonReport precheck_report{};
|
|
||||||
precheck_report.blocking_passed = true;
|
|
||||||
precheck_report.precheck_findings = {
|
|
||||||
{"pressure_nodal_translation_blocking",
|
|
||||||
fesa::test::NonlinearReferenceDisposition::kBlocking,
|
|
||||||
"missing-extra-duplicate-or-nonfinite",
|
|
||||||
{}}};
|
|
||||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(precheck_report),
|
|
||||||
fesa::test::NonlinearReferenceClassification::kFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference,
|
TEST(ShellSurfacePressureReference,
|
||||||
ClassifiesTranslationBlockingAndRotationWarningOnly) {
|
ClassifiesTranslationBlockingAndRotationWarningOnly) {
|
||||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||||
@@ -207,55 +24,9 @@ TEST(ShellSurfacePressureReference,
|
|||||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||||
2187U,
|
2187U,
|
||||||
MakeTolerance(false)}};
|
MakeTolerance(false)}};
|
||||||
|
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
fesa::test::ClassifyReferenceComparison(report),
|
fesa::test::ClassifyReferenceComparison(report),
|
||||||
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference,
|
} // namespace
|
||||||
ExcludesReactionAndStressFamiliesFromVerdict) {
|
|
||||||
const auto& report = RunPressureComparison();
|
|
||||||
|
|
||||||
EXPECT_EQ(report.families.size(), 2U);
|
|
||||||
EXPECT_EQ(
|
|
||||||
FindFamily(report, "pressure_nodal_translation_blocking").disposition,
|
|
||||||
fesa::test::NonlinearReferenceDisposition::kBlocking);
|
|
||||||
EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").disposition,
|
|
||||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference,
|
|
||||||
UsesUnchangedCommonScaleNearZeroRelativeAndRmsPolicy) {
|
|
||||||
const auto& translation = FindFamily(RunPressureComparison(),
|
|
||||||
"pressure_nodal_translation_blocking");
|
|
||||||
|
|
||||||
EXPECT_EQ(translation.tolerance.near_zero_band,
|
|
||||||
fesa::test::ReferenceTolerancePolicy::kNearZeroRatio *
|
|
||||||
translation.tolerance.reference_scale);
|
|
||||||
EXPECT_LE(translation.tolerance.relative_rms,
|
|
||||||
fesa::test::ReferenceTolerancePolicy::kRelativeRmsTolerance);
|
|
||||||
EXPECT_TRUE(translation.tolerance.rms_passed);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(ShellSurfacePressureReference, WritesDeterministicComparisonReport) {
|
|
||||||
const auto& first = RunPressureComparison();
|
|
||||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
|
||||||
MakePressureCase(), first.generated_results_hdf5);
|
|
||||||
|
|
||||||
ASSERT_TRUE(comparison.HasValue());
|
|
||||||
ASSERT_EQ(first.families.size(), comparison.Value().families.size());
|
|
||||||
for (std::size_t index = 0U; index < first.families.size(); ++index) {
|
|
||||||
const auto& expected = first.families[index];
|
|
||||||
const auto& actual = comparison.Value().families[index];
|
|
||||||
EXPECT_EQ(actual.family_identity, expected.family_identity);
|
|
||||||
EXPECT_EQ(actual.components, expected.components);
|
|
||||||
EXPECT_EQ(actual.disposition, expected.disposition);
|
|
||||||
EXPECT_EQ(actual.row_count, expected.row_count);
|
|
||||||
EXPECT_DOUBLE_EQ(actual.tolerance.reference_scale,
|
|
||||||
expected.tolerance.reference_scale);
|
|
||||||
EXPECT_DOUBLE_EQ(actual.tolerance.relative_rms,
|
|
||||||
expected.tolerance.relative_rms);
|
|
||||||
EXPECT_EQ(actual.tolerance.worst_row, expected.tolerance.worst_row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
#include "analysis/eigen_reduction.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::SparseMatrix Sparse(const fesa::Matrix& dense) {
|
||||||
|
fesa::SparsePattern pattern;
|
||||||
|
std::vector<fesa::CooContribution> entries;
|
||||||
|
pattern.row_offsets.push_back(0U);
|
||||||
|
for (std::size_t i = 0; i < dense.Rows(); ++i) {
|
||||||
|
for (std::size_t j = 0; j < dense.Columns(); ++j) {
|
||||||
|
pattern.column_indices.push_back(j);
|
||||||
|
entries.push_back({i, j, dense(i, j), 0U, i * dense.Columns() + j});
|
||||||
|
}
|
||||||
|
pattern.row_offsets.push_back(pattern.column_indices.size());
|
||||||
|
}
|
||||||
|
auto result = fesa::SparseMatrix::FromCoo(dense.Rows(), dense.Columns(),
|
||||||
|
std::move(entries), pattern);
|
||||||
|
EXPECT_TRUE(result.HasValue());
|
||||||
|
return std::move(result.Value());
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Matrix NodalMass(const std::array<double, 3>& d) {
|
||||||
|
fesa::Matrix mass(6U, 6U);
|
||||||
|
for (std::size_t i = 0; i < 3U; ++i) {
|
||||||
|
mass(i, i) = 2.0;
|
||||||
|
for (std::size_t j = 0; j < 3U; ++j) {
|
||||||
|
mass(i + 3U, j + 3U) = (static_cast<double>(i == j) - d[i] * d[j]) / 6.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mass;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::ShellNodeInitialFrame TiltedFrame() {
|
||||||
|
return {0U, {0.6, 0.0, 0.8}, {0.0, 1.0, 0.0}, {-0.8, 0.0, 0.6}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(EigenReduction, KeepsPositivePartialGlobalRotationAndOriginalPencil) {
|
||||||
|
const auto frame = TiltedFrame();
|
||||||
|
fesa::Matrix stiffness(6U, 6U);
|
||||||
|
stiffness(5U, 5U) = 10369.0 / 7500.0;
|
||||||
|
std::vector<std::uint8_t> mask(6U, 1U);
|
||||||
|
mask[5U] = 0U;
|
||||||
|
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {NodalMass(frame.director)}, {frame}, mask,
|
||||||
|
{19.0 / 4800.0}, 4.0);
|
||||||
|
ASSERT_TRUE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.Value().null_count, 0U);
|
||||||
|
EXPECT_EQ(result.Value().whitened_operator.Rows(), 1U);
|
||||||
|
EXPECT_NEAR(result.Value().whitened_operator.Values()[0], 10369.0 / 450.0,
|
||||||
|
1e-12);
|
||||||
|
fesa::Matrix x(1U, 1U, 1.0);
|
||||||
|
const auto y = fesa::eigen_internal::RestoreScaledMode(result.Value(), x, 0U);
|
||||||
|
for (std::size_t i = 0; i < 5U; ++i) EXPECT_DOUBLE_EQ(y[i], 0.0);
|
||||||
|
EXPECT_NEAR(y[5U] * y[5U] * (3.0 / 50.0), 1.0, 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenReduction, RemovesOnlyActualFreeDirectorNull) {
|
||||||
|
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
|
||||||
|
auto mass = NodalMass(frame.director);
|
||||||
|
fesa::Matrix stiffness(6U, 6U);
|
||||||
|
for (std::size_t i = 0; i < 5U; ++i) stiffness(i, i) = mass(i, i) * 7.0;
|
||||||
|
stiffness(5U, 5U) = 0.25;
|
||||||
|
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {mass}, {frame}, std::vector<std::uint8_t>(6U, 0U),
|
||||||
|
{0.25}, 2.0);
|
||||||
|
ASSERT_TRUE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.Value().free_count, 6U);
|
||||||
|
EXPECT_EQ(result.Value().null_count, 1U);
|
||||||
|
EXPECT_EQ(result.Value().whitened_operator.Rows(), 5U);
|
||||||
|
fesa::Vector ones(5U, 1.0);
|
||||||
|
const auto action = result.Value().whitened_operator.Multiply(ones);
|
||||||
|
for (std::size_t i = 0; i < 5U; ++i) EXPECT_NEAR(action[i], 7.0, 1e-12);
|
||||||
|
EXPECT_LE(result.Value().eta_kz, 1e-12);
|
||||||
|
stiffness(0U, 5U) = stiffness(5U, 0U) = 0.1;
|
||||||
|
EXPECT_FALSE(fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {mass}, {frame},
|
||||||
|
std::vector<std::uint8_t>(6U, 0U), {0.25}, 2.0)
|
||||||
|
.HasValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenReduction, RejectsPositiveButUnresolvableMassWithoutDeletingDofs) {
|
||||||
|
const double p = 1e-8;
|
||||||
|
const std::array<double, 3> d{2 * p / (1 + p * p), 0,
|
||||||
|
(1 - p * p) / (1 + p * p)};
|
||||||
|
const fesa::ShellNodeInitialFrame frame{0U, d, {0, 1, 0}, {-d[2], 0, d[0]}};
|
||||||
|
fesa::Matrix stiffness(6U, 6U);
|
||||||
|
for (std::size_t i = 0; i < 6U; ++i) stiffness(i, i) = 1.0;
|
||||||
|
std::vector<std::uint8_t> mask(6U, 0U);
|
||||||
|
mask[3U] = 1U;
|
||||||
|
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {NodalMass(d)}, {frame}, mask, {0.25}, 1.0);
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
|
||||||
|
"eigen-mass-conditioning");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenReduction, WhitensOffDiagonalMassAndRestoresOriginalPencil) {
|
||||||
|
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
|
||||||
|
auto mass = NodalMass(frame.director);
|
||||||
|
mass(0, 1) = mass(1, 0) = 1.0;
|
||||||
|
fesa::Matrix stiffness(6U, 6U);
|
||||||
|
stiffness(0, 0) = stiffness(1, 1) = 6.0;
|
||||||
|
stiffness(0, 1) = stiffness(1, 0) = 4.0;
|
||||||
|
std::vector<std::uint8_t> mask{0U, 0U, 1U, 1U, 1U, 1U};
|
||||||
|
const auto reduction = fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 3.0);
|
||||||
|
ASSERT_TRUE(reduction.HasValue());
|
||||||
|
const auto eigen = fesa::MklFeastEigenSolver{}.Solve(
|
||||||
|
reduction.Value().whitened_operator, 2U, 1.0);
|
||||||
|
ASSERT_TRUE(eigen.HasValue());
|
||||||
|
ASSERT_EQ(eigen.Value().eigenvalues.size(), 2U);
|
||||||
|
EXPECT_NEAR(eigen.Value().eigenvalues[0], 2.0, 1e-12);
|
||||||
|
EXPECT_NEAR(eigen.Value().eigenvalues[1], 10.0 / 3.0, 1e-12);
|
||||||
|
for (std::size_t mode = 0; mode < 2U; ++mode) {
|
||||||
|
auto y = fesa::eigen_internal::RestoreScaledMode(
|
||||||
|
reduction.Value(), eigen.Value().eigenvectors, mode);
|
||||||
|
fesa::Vector q = y;
|
||||||
|
for (std::size_t i = 0; i < 3U; ++i) q[i] *= 3.0;
|
||||||
|
const auto mq = mass.Multiply(q);
|
||||||
|
EXPECT_NEAR(q.Dot(mq), 1.0, 1e-12);
|
||||||
|
auto residual = stiffness.Multiply(q);
|
||||||
|
residual.Axpy(-eigen.Value().eigenvalues[mode], mq);
|
||||||
|
EXPECT_LE(residual.Norm(), 1e-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenReduction, PreservesSmallResolvablePositivePartialRotationMass) {
|
||||||
|
const double p = 1.0 / 2000.0;
|
||||||
|
const std::array<double, 3> d{2 * p / (1 + p * p), 0,
|
||||||
|
(1 - p * p) / (1 + p * p)};
|
||||||
|
const fesa::ShellNodeInitialFrame frame{0U, d, {0, 1, 0}, {-d[2], 0, d[0]}};
|
||||||
|
fesa::Matrix stiffness(6U, 6U);
|
||||||
|
for (std::size_t i = 0; i < 6U; ++i) stiffness(i, i) = 1.0;
|
||||||
|
std::vector<std::uint8_t> mask(6U, 0U);
|
||||||
|
mask[3U] = 1U;
|
||||||
|
const auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {NodalMass(d)}, {frame}, mask, {0.25}, 1.0);
|
||||||
|
ASSERT_TRUE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.Value().null_count, 0U);
|
||||||
|
EXPECT_EQ(result.Value().whitened_operator.Rows(), 5U);
|
||||||
|
EXPECT_GE(result.Value().nodes[0].record.rcond_inf, 0x1p-32);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenReduction, HandlesTwoFreeRotationsAndRejectsEmptyFiniteSpace) {
|
||||||
|
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
|
||||||
|
auto mass = NodalMass(frame.director);
|
||||||
|
fesa::Matrix stiffness(6U, 6U);
|
||||||
|
stiffness(3U, 3U) = 1.0;
|
||||||
|
stiffness(5U, 5U) = 0.25;
|
||||||
|
std::vector<std::uint8_t> mask{1U, 1U, 1U, 0U, 1U, 0U};
|
||||||
|
auto result = fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 1.0);
|
||||||
|
ASSERT_TRUE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.Value().null_count, 1U);
|
||||||
|
EXPECT_EQ(result.Value().whitened_operator.Rows(), 1U);
|
||||||
|
mass(5U, 5U) = 0.1;
|
||||||
|
EXPECT_FALSE(fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 1.0)
|
||||||
|
.HasValue());
|
||||||
|
mass(5U, 5U) = 0.0;
|
||||||
|
for (const bool drill_free : {false, true}) {
|
||||||
|
mask.assign(6U, 1U);
|
||||||
|
mask[5U] = drill_free ? 0U : 1U;
|
||||||
|
const auto empty = fesa::eigen_internal::BuildEigenReduction(
|
||||||
|
Sparse(stiffness), {mass}, {frame}, mask, {0.25}, 1.0);
|
||||||
|
ASSERT_FALSE(empty.HasValue());
|
||||||
|
EXPECT_EQ(empty.GetStatus().Category(), fesa::FailureCategory::kModel);
|
||||||
|
EXPECT_EQ(empty.GetStatus().Diagnostics()[0].code, "empty-eigenproblem");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "fesa/elements/mitc4_shell.h"
|
||||||
|
#include "fesa/model/model_types.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using Coordinates = std::array<double, 3>;
|
||||||
|
|
||||||
|
fesa::Node Node(std::int64_t label, Coordinates coordinates) {
|
||||||
|
return {{"I", label, std::to_string(label)}, coordinates, {"mass.inp", 1U}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<const fesa::Node*, 4> Pointers(
|
||||||
|
const std::array<fesa::Node, 4>& nodes) {
|
||||||
|
return {&nodes[0], &nodes[1], &nodes[2], &nodes[3]};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Mitc4Shell WarpedShell() {
|
||||||
|
const std::array<fesa::Node, 4> nodes{
|
||||||
|
Node(1, {-1.0, -1.0, 0.1}), Node(2, {1.0, -1.0, -0.1}),
|
||||||
|
Node(3, {1.0, 1.0, 0.2}), Node(4, {-1.0, 1.0, -0.1})};
|
||||||
|
const std::array<Coordinates, 4> directors{
|
||||||
|
{{{0.0, 0.0, 1.0}},
|
||||||
|
{{0.6, 0.0, 0.8}},
|
||||||
|
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}},
|
||||||
|
{{0.0, 0.6, 0.8}}}};
|
||||||
|
const fesa::ShellSection section{"S", 0.5, 0U, {"mass.inp", 2U}};
|
||||||
|
const fesa::LinearElasticMaterial material{"M", 12.0, 0.0, {"mass.inp", 3U}};
|
||||||
|
const auto result =
|
||||||
|
fesa::Mitc4Shell::Create(Pointers(nodes), directors, section, material);
|
||||||
|
EXPECT_TRUE(result.HasValue());
|
||||||
|
return result.Value();
|
||||||
|
}
|
||||||
|
|
||||||
|
double Quadratic(const fesa::Matrix& matrix, const fesa::Vector& vector) {
|
||||||
|
return vector.Dot(matrix.Multiply(vector));
|
||||||
|
}
|
||||||
|
|
||||||
|
// E2-C02: values derive independently in numerical-review-evidence.md.
|
||||||
|
TEST(Mitc4ShellMassContract, PreservesWarpedMomentsRowsAndFirstSpatialMoment) {
|
||||||
|
const auto mass = WarpedShell().Mass(3.0);
|
||||||
|
ASSERT_TRUE(mass.HasValue());
|
||||||
|
const auto& value = mass.Value();
|
||||||
|
EXPECT_NEAR(value.consistent_moments[0U](0U, 0U), 17609.0 / 28800.0, 1e-12);
|
||||||
|
EXPECT_NEAR(value.consistent_moments[1U](0U, 0U), 11003.0 / 1440000.0, 1e-12);
|
||||||
|
EXPECT_NEAR(value.consistent_moments[2U](0U, 0U), 88177.0 / 6912000.0, 1e-12);
|
||||||
|
double total_m = 0.0, total_h = 0.0, total_j = 0.0;
|
||||||
|
for (const auto& row : value.nodal_moments) {
|
||||||
|
total_m += row[0U];
|
||||||
|
total_h += row[1U];
|
||||||
|
total_j += row[2U];
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(total_m, 7743.0 / 1600.0, 1e-12);
|
||||||
|
EXPECT_NEAR(total_h, 739.0 / 12000.0, 1e-12);
|
||||||
|
EXPECT_NEAR(total_j, 517.0 / 5120.0, 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// E2-C03: scalar row-sum lumping retains uniform translation energy and is
|
||||||
|
// never below the consistent kinetic energy.
|
||||||
|
TEST(Mitc4ShellMassContract, SatisfiesUniformAndJensenEnergyContracts) {
|
||||||
|
const auto mass = WarpedShell().Mass(3.0);
|
||||||
|
ASSERT_TRUE(mass.HasValue());
|
||||||
|
fesa::Vector translation{24U};
|
||||||
|
fesa::Vector field{24U};
|
||||||
|
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||||
|
translation[6U * node] = 1.0;
|
||||||
|
field[6U * node] = static_cast<double>(node + 1U);
|
||||||
|
field[6U * node + 1U] = -0.5 * static_cast<double>(node);
|
||||||
|
field[6U * node + 3U] = 0.25;
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(Quadratic(mass.Value().consistent_global24, translation),
|
||||||
|
Quadratic(mass.Value().lumped_global24, translation), 1e-12);
|
||||||
|
EXPECT_GE(Quadratic(mass.Value().lumped_global24, field) + 1e-12,
|
||||||
|
Quadratic(mass.Value().consistent_global24, field));
|
||||||
|
}
|
||||||
|
|
||||||
|
// E2-C04: assemble the physical velocity work over successively finer meshes.
|
||||||
|
TEST(Mitc4ShellMassContract, ReproducesAffineSquareVelocityWork) {
|
||||||
|
const std::array<Coordinates, 4> directors{{{{0.0, 0.0, 1.0}},
|
||||||
|
{{0.0, 0.0, 1.0}},
|
||||||
|
{{0.0, 0.0, 1.0}},
|
||||||
|
{{0.0, 0.0, 1.0}}}};
|
||||||
|
const fesa::ShellSection section{"S", 1.0, 0U, {"mass.inp", 2U}};
|
||||||
|
const fesa::LinearElasticMaterial material{"M", 12.0, 0.0, {"mass.inp", 3U}};
|
||||||
|
for (const int mesh : {1, 2, 4}) {
|
||||||
|
double consistent_work = 0.0, lumped_work = 0.0;
|
||||||
|
const double edge = 2.0 / mesh;
|
||||||
|
for (int row = 0; row < mesh; ++row) {
|
||||||
|
for (int column = 0; column < mesh; ++column) {
|
||||||
|
const double x = -1.0 + edge * column, y = -1.0 + edge * row;
|
||||||
|
const std::array<fesa::Node, 4> nodes{
|
||||||
|
Node(1, {x, y, 0.0}), Node(2, {x + edge, y, 0.0}),
|
||||||
|
Node(3, {x + edge, y + edge, 0.0}), Node(4, {x, y + edge, 0.0})};
|
||||||
|
const auto shell = fesa::Mitc4Shell::Create(Pointers(nodes), directors,
|
||||||
|
section, material);
|
||||||
|
ASSERT_TRUE(shell.HasValue());
|
||||||
|
const auto mass = shell.Value().Mass(1.0);
|
||||||
|
ASSERT_TRUE(mass.HasValue());
|
||||||
|
fesa::Vector velocity{24U};
|
||||||
|
for (std::size_t node = 0U; node < 4U; ++node)
|
||||||
|
velocity[6U * node] = nodes[node].coordinates[0U];
|
||||||
|
consistent_work +=
|
||||||
|
Quadratic(mass.Value().consistent_global24, velocity);
|
||||||
|
lumped_work += Quadratic(mass.Value().lumped_global24, velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(consistent_work, 4.0 / 3.0, 1e-12) << mesh;
|
||||||
|
EXPECT_NEAR(lumped_work - consistent_work, 8.0 / (3.0 * mesh * mesh), 1e-12)
|
||||||
|
<< mesh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -53,6 +53,12 @@ Vector3 Cross(const Vector3& left, const Vector3& right) {
|
|||||||
left[0] * right[1] - left[1] * right[0]};
|
left[0] * right[1] - left[1] * right[0]};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vector3 RotateAboutGlobalZ(const Vector3& value) {
|
||||||
|
constexpr double kInverseSqrtTwo = 0.70710678118654752440;
|
||||||
|
return {kInverseSqrtTwo * (value[0U] - value[1U]),
|
||||||
|
kInverseSqrtTwo * (value[0U] + value[1U]), value[2U]};
|
||||||
|
}
|
||||||
|
|
||||||
double Norm(const Vector3& value) { return std::sqrt(Dot(value, value)); }
|
double Norm(const Vector3& value) { return std::sqrt(Dot(value, value)); }
|
||||||
|
|
||||||
void ExpectVectorNear(const Vector3& actual, const Vector3& expected,
|
void ExpectVectorNear(const Vector3& actual, const Vector3& expected,
|
||||||
@@ -636,7 +642,6 @@ TEST(Mitc4ShellKernel,
|
|||||||
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 0U), 0x1.d555555555554p+6);
|
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 0U), 0x1.d555555555554p+6);
|
||||||
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 4U), 0.0);
|
EXPECT_DOUBLE_EQ(stiffness.physical_local20(0U, 4U), 0.0);
|
||||||
EXPECT_DOUBLE_EQ(stiffness.physical_global24(2U, 2U), 0x1.aaaaaaaaaaaadp+5);
|
EXPECT_DOUBLE_EQ(stiffness.physical_global24(2U, 2U), 0x1.aaaaaaaaaaaadp+5);
|
||||||
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 0x1.0d6cffc5beeb5p-4);
|
|
||||||
EXPECT_DOUBLE_EQ(recovery.strain_energy, 0x1.20a3d70a3d70bp+6);
|
EXPECT_DOUBLE_EQ(recovery.strain_energy, 0x1.20a3d70a3d70bp+6);
|
||||||
constexpr std::array<double, 8> kExpectedStrain{
|
constexpr std::array<double, 8> kExpectedStrain{
|
||||||
0x1.999999999999ap-4, -0x1.999999999999bp-5, 0x1.9999999999998p-3,
|
0x1.999999999999ap-4, -0x1.999999999999bp-5, 0x1.9999999999998p-3,
|
||||||
@@ -791,8 +796,7 @@ TEST(Mitc4ShellPatch, ReproducesIndependentMembraneBendingShearAndTwistFields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MITC4-KERNEL-005
|
// MITC4-KERNEL-005
|
||||||
TEST(Mitc4ShellDrilling,
|
TEST(Mitc4ShellDrilling, UsesAllPhysicalRotationDiagonalsAndFixedFactor) {
|
||||||
UsesOnlyEightPositivePhysicalRotationDiagonalsAndFixedFactor) {
|
|
||||||
const std::array<fesa::Node, 4> nodes{
|
const std::array<fesa::Node, 4> nodes{
|
||||||
Node(1, {-50.0, -50.0, 0.0}), Node(2, {50.0, -50.0, 0.0}),
|
Node(1, {-50.0, -50.0, 0.0}), Node(2, {50.0, -50.0, 0.0}),
|
||||||
Node(3, {50.0, 50.0, 0.0}), Node(4, {-50.0, 50.0, 0.0})};
|
Node(3, {50.0, 50.0, 0.0}), Node(4, {-50.0, 50.0, 0.0})};
|
||||||
@@ -803,27 +807,222 @@ TEST(Mitc4ShellDrilling,
|
|||||||
ASSERT_TRUE(stiffness_candidate.HasValue());
|
ASSERT_TRUE(stiffness_candidate.HasValue());
|
||||||
const auto& stiffness = stiffness_candidate.Value();
|
const auto& stiffness = stiffness_candidate.Value();
|
||||||
|
|
||||||
double expected_reference = (std::numeric_limits<double>::max)();
|
double expected_sum = 0.0;
|
||||||
double all_diagonal_minimum = (std::numeric_limits<double>::max)();
|
|
||||||
for (std::size_t node_index = 0U; node_index < 4U; ++node_index) {
|
for (std::size_t node_index = 0U; node_index < 4U; ++node_index) {
|
||||||
for (std::size_t rotation = 3U; rotation < 5U; ++rotation) {
|
for (std::size_t rotation = 3U; rotation < 5U; ++rotation) {
|
||||||
const std::size_t index = 5U * node_index + rotation;
|
const std::size_t index = 5U * node_index + rotation;
|
||||||
const double diagonal = stiffness.physical_local20(index, index);
|
const double diagonal = stiffness.physical_local20(index, index);
|
||||||
ASSERT_TRUE(std::isfinite(diagonal));
|
ASSERT_TRUE(std::isfinite(diagonal));
|
||||||
ASSERT_GT(diagonal, 0.0);
|
ASSERT_GE(diagonal, 0.0);
|
||||||
expected_reference = (std::min)(expected_reference, diagonal);
|
expected_sum += diagonal;
|
||||||
}
|
}
|
||||||
for (std::size_t component = 0U; component < 5U; ++component) {
|
}
|
||||||
const std::size_t index = 5U * node_index + component;
|
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 1.0e-3 * expected_sum / 8.0);
|
||||||
const double diagonal = stiffness.physical_local20(index, index);
|
}
|
||||||
if (std::isfinite(diagonal) && diagonal > 0.0) {
|
|
||||||
all_diagonal_minimum = (std::min)(all_diagonal_minimum, diagonal);
|
TEST(Mitc4ShellMass, IntegratesFlatConsistentAndScalarRowSumLumpedMass) {
|
||||||
|
const auto nodes = PlanarNodes();
|
||||||
|
const auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||||
|
NodePointers(nodes), Directors(), Section(), Material());
|
||||||
|
ASSERT_TRUE(shell_candidate.HasValue());
|
||||||
|
|
||||||
|
const auto mass_candidate = shell_candidate.Value().Mass(3.0);
|
||||||
|
ASSERT_TRUE(mass_candidate.HasValue());
|
||||||
|
const auto& mass = mass_candidate.Value();
|
||||||
|
const std::array<std::array<double, 4>, 4> pattern{{{{4.0, 2.0, 1.0, 2.0}},
|
||||||
|
{{2.0, 4.0, 2.0, 1.0}},
|
||||||
|
{{1.0, 2.0, 4.0, 2.0}},
|
||||||
|
{{2.0, 1.0, 2.0, 4.0}}}};
|
||||||
|
for (std::size_t row = 0U; row < 4U; ++row) {
|
||||||
|
EXPECT_NEAR(mass.nodal_moments[row][0U], 6.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(mass.nodal_moments[row][1U], 0.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(mass.nodal_moments[row][2U], 2.0, 1.0e-12);
|
||||||
|
for (std::size_t column = 0U; column < 4U; ++column) {
|
||||||
|
EXPECT_NEAR(mass.consistent_moments[0U](row, column),
|
||||||
|
pattern[row][column] * (2.0 / 3.0), 1.0e-12);
|
||||||
|
EXPECT_NEAR(mass.consistent_moments[1U](row, column), 0.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(mass.consistent_moments[2U](row, column),
|
||||||
|
pattern[row][column] * (2.0 / 9.0), 1.0e-12);
|
||||||
|
}
|
||||||
|
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||||
|
EXPECT_NEAR(
|
||||||
|
mass.lumped_global24(6U * row + component, 6U * row + component), 6.0,
|
||||||
|
1.0e-12);
|
||||||
|
EXPECT_NEAR(mass.lumped_global24(6U * row + 3U + component,
|
||||||
|
6U * row + 3U + component),
|
||||||
|
component == 2U ? 0.0 : 2.0, 1.0e-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExpectSymmetric(mass.consistent_global24);
|
||||||
|
ExpectSymmetric(mass.lumped_global24);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Mitc4ShellMass, RejectsNonpositiveAndNonfiniteDensity) {
|
||||||
|
const auto nodes = PlanarNodes();
|
||||||
|
const auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||||
|
NodePointers(nodes), Directors(), Section(), Material());
|
||||||
|
ASSERT_TRUE(shell_candidate.HasValue());
|
||||||
|
for (const double density :
|
||||||
|
{0.0, -1.0, std::numeric_limits<double>::infinity()}) {
|
||||||
|
const auto mass = shell_candidate.Value().Mass(density);
|
||||||
|
ASSERT_FALSE(mass.HasValue());
|
||||||
|
ASSERT_EQ(mass.GetStatus().Diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(mass.GetStatus().Diagnostics()[0U].code, "invalid-shell-mass");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Mitc4ShellMass, PreservesFiniteNodalVarianceAcrossDensityScale) {
|
||||||
|
const auto nodes = PlanarNodes();
|
||||||
|
const auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||||
|
NodePointers(nodes), Directors(), Section(), Material());
|
||||||
|
ASSERT_TRUE(shell_candidate.HasValue());
|
||||||
|
for (const double density : {1.0e-180, 1.0e180}) {
|
||||||
|
const auto mass_candidate = shell_candidate.Value().Mass(density);
|
||||||
|
ASSERT_TRUE(mass_candidate.HasValue());
|
||||||
|
for (const auto& moments : mass_candidate.Value().nodal_moments) {
|
||||||
|
EXPECT_TRUE(std::isfinite(moments[0U]));
|
||||||
|
EXPECT_TRUE(std::isfinite(moments[1U]));
|
||||||
|
EXPECT_TRUE(std::isfinite(moments[2U]));
|
||||||
|
EXPECT_GT(moments[2U] - moments[1U] * (moments[1U] / moments[0U]), 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EXPECT_DOUBLE_EQ(stiffness.drilling_stiffness, 1.0e-3 * expected_reference);
|
|
||||||
EXPECT_LT(all_diagonal_minimum, expected_reference);
|
void ExpectGlobalZCovariance(const fesa::Matrix& original,
|
||||||
EXPECT_NE(stiffness.drilling_stiffness, 1.0e-3 * all_diagonal_minimum);
|
const fesa::Matrix& rotated) {
|
||||||
|
ASSERT_EQ(original.Rows(), 24U);
|
||||||
|
ASSERT_EQ(original.Columns(), 24U);
|
||||||
|
ASSERT_EQ(rotated.Rows(), 24U);
|
||||||
|
ASSERT_EQ(rotated.Columns(), 24U);
|
||||||
|
constexpr double kInverseSqrtTwo = 0.70710678118654752440;
|
||||||
|
const std::array<std::array<double, 3>, 3> transform{
|
||||||
|
{{{kInverseSqrtTwo, -kInverseSqrtTwo, 0.0}},
|
||||||
|
{{kInverseSqrtTwo, kInverseSqrtTwo, 0.0}},
|
||||||
|
{{0.0, 0.0, 1.0}}}};
|
||||||
|
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||||
|
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||||
|
double expected = 0.0;
|
||||||
|
const std::size_t row_offset =
|
||||||
|
6U * (row / 6U) + (row % 6U < 3U ? 0U : 3U);
|
||||||
|
const std::size_t column_offset =
|
||||||
|
6U * (column / 6U) + (column % 6U < 3U ? 0U : 3U);
|
||||||
|
const std::size_t row_component = row % 3U;
|
||||||
|
const std::size_t column_component = column % 3U;
|
||||||
|
for (std::size_t first = 0U; first < 3U; ++first) {
|
||||||
|
for (std::size_t second = 0U; second < 3U; ++second) {
|
||||||
|
expected += transform[row_component][first] *
|
||||||
|
original(row_offset + first, column_offset + second) *
|
||||||
|
transform[column_component][second];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(rotated(row, column), expected, 1.0e-12)
|
||||||
|
<< "at (" << row << ", " << column << ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Mitc4ShellMass, PreservesIndependentWarpedMomentOracle) {
|
||||||
|
const std::array<fesa::Node, 4> nodes{
|
||||||
|
Node(1, {-1.0, -1.0, 0.1}), Node(2, {1.0, -1.0, -0.1}),
|
||||||
|
Node(3, {1.0, 1.0, 0.2}), Node(4, {-1.0, 1.0, -0.1})};
|
||||||
|
const std::array<Vector3, 4> directors{{{{0.0, 0.0, 1.0}},
|
||||||
|
{{0.6, 0.0, 0.8}},
|
||||||
|
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}},
|
||||||
|
{{0.0, 0.6, 0.8}}}};
|
||||||
|
const auto shell_candidate = fesa::Mitc4Shell::Create(
|
||||||
|
NodePointers(nodes), directors, Section(0.5), Material());
|
||||||
|
ASSERT_TRUE(shell_candidate.HasValue());
|
||||||
|
const auto mass_candidate = shell_candidate.Value().Mass(3.0);
|
||||||
|
ASSERT_TRUE(mass_candidate.HasValue());
|
||||||
|
const auto& mass = mass_candidate.Value();
|
||||||
|
|
||||||
|
EXPECT_NEAR(mass.consistent_moments[0U](0U, 0U), 17609.0 / 28800.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(mass.consistent_moments[1U](0U, 0U), 11003.0 / 1440000.0,
|
||||||
|
1.0e-12);
|
||||||
|
EXPECT_NEAR(mass.consistent_moments[2U](0U, 0U), 88177.0 / 6912000.0,
|
||||||
|
1.0e-12);
|
||||||
|
double total_mass = 0.0;
|
||||||
|
double total_first_moment = 0.0;
|
||||||
|
double total_second_moment = 0.0;
|
||||||
|
for (const auto& moments : mass.nodal_moments) {
|
||||||
|
total_mass += moments[0U];
|
||||||
|
total_first_moment += moments[1U];
|
||||||
|
total_second_moment += moments[2U];
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(total_mass, 7743.0 / 1600.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(total_first_moment, 739.0 / 12000.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(total_second_moment, 517.0 / 5120.0, 1.0e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Mitc4ShellMass, MeetsPartialBoundaryCompleteMatrixTargets) {
|
||||||
|
const Vector3 tangent_a{0.0, 1.0, 0.0};
|
||||||
|
const Vector3 tangent_b{-0.8, 0.0, 0.6};
|
||||||
|
const std::array<fesa::Node, 4> nodes{
|
||||||
|
Node(1, {-2.0 * tangent_a[0U] - tangent_b[0U],
|
||||||
|
-2.0 * tangent_a[1U] - tangent_b[1U],
|
||||||
|
-2.0 * tangent_a[2U] - tangent_b[2U]}),
|
||||||
|
Node(2, {2.0 * tangent_a[0U] - tangent_b[0U],
|
||||||
|
2.0 * tangent_a[1U] - tangent_b[1U],
|
||||||
|
2.0 * tangent_a[2U] - tangent_b[2U]}),
|
||||||
|
Node(3, {2.0 * tangent_a[0U] + tangent_b[0U],
|
||||||
|
2.0 * tangent_a[1U] + tangent_b[1U],
|
||||||
|
2.0 * tangent_a[2U] + tangent_b[2U]}),
|
||||||
|
Node(4, {-2.0 * tangent_a[0U] + tangent_b[0U],
|
||||||
|
-2.0 * tangent_a[1U] + tangent_b[1U],
|
||||||
|
-2.0 * tangent_a[2U] + tangent_b[2U]})};
|
||||||
|
const auto shell_candidate =
|
||||||
|
fesa::Mitc4Shell::Create(NodePointers(nodes), Directors({0.6, 0.0, 0.8}),
|
||||||
|
Section(1.0), Material(12.0, 0.0));
|
||||||
|
ASSERT_TRUE(shell_candidate.HasValue());
|
||||||
|
const auto stiffness_candidate = shell_candidate.Value().Stiffness();
|
||||||
|
const auto mass_candidate = shell_candidate.Value().Mass(1.0);
|
||||||
|
ASSERT_TRUE(stiffness_candidate.HasValue());
|
||||||
|
ASSERT_TRUE(mass_candidate.HasValue());
|
||||||
|
const double complete_stiffness =
|
||||||
|
stiffness_candidate.Value().stabilized_global24(5U, 5U);
|
||||||
|
const double lumped_mass = mass_candidate.Value().lumped_global24(5U, 5U);
|
||||||
|
EXPECT_NEAR(stiffness_candidate.Value().drilling_stiffness, 19.0 / 4800.0,
|
||||||
|
1.0e-12);
|
||||||
|
EXPECT_NEAR(complete_stiffness, 10369.0 / 7500.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(lumped_mass, 3.0 / 50.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(complete_stiffness / lumped_mass, 10369.0 / 450.0, 1.0e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Mitc4ShellMass, RecomputesCompleteMassAndStiffnessCovariantly) {
|
||||||
|
const std::array<fesa::Node, 4> nodes{
|
||||||
|
Node(1, {-1.0, -1.0, 0.1}), Node(2, {1.0, -1.0, -0.1}),
|
||||||
|
Node(3, {1.0, 1.0, 0.2}), Node(4, {-1.0, 1.0, -0.1})};
|
||||||
|
const std::array<Vector3, 4> directors{{{{0.0, 0.0, 1.0}},
|
||||||
|
{{0.6, 0.0, 0.8}},
|
||||||
|
{{2.0 / 3.0, 1.0 / 3.0, 2.0 / 3.0}},
|
||||||
|
{{0.0, 0.6, 0.8}}}};
|
||||||
|
std::array<fesa::Node, 4> rotated_nodes = nodes;
|
||||||
|
std::array<Vector3, 4> rotated_directors{};
|
||||||
|
for (std::size_t node = 0U; node < nodes.size(); ++node) {
|
||||||
|
rotated_nodes[node].coordinates =
|
||||||
|
RotateAboutGlobalZ(nodes[node].coordinates);
|
||||||
|
rotated_directors[node] = RotateAboutGlobalZ(directors[node]);
|
||||||
|
}
|
||||||
|
const auto original = fesa::Mitc4Shell::Create(NodePointers(nodes), directors,
|
||||||
|
Section(0.5), Material());
|
||||||
|
const auto rotated = fesa::Mitc4Shell::Create(
|
||||||
|
NodePointers(rotated_nodes), rotated_directors, Section(0.5), Material());
|
||||||
|
ASSERT_TRUE(original.HasValue());
|
||||||
|
ASSERT_TRUE(rotated.HasValue());
|
||||||
|
const auto original_stiffness = original.Value().Stiffness();
|
||||||
|
const auto rotated_stiffness = rotated.Value().Stiffness();
|
||||||
|
const auto original_mass = original.Value().Mass(3.0);
|
||||||
|
const auto rotated_mass = rotated.Value().Mass(3.0);
|
||||||
|
ASSERT_TRUE(original_stiffness.HasValue());
|
||||||
|
ASSERT_TRUE(rotated_stiffness.HasValue());
|
||||||
|
ASSERT_TRUE(original_mass.HasValue());
|
||||||
|
ASSERT_TRUE(rotated_mass.HasValue());
|
||||||
|
ExpectGlobalZCovariance(original_stiffness.Value().stabilized_global24,
|
||||||
|
rotated_stiffness.Value().stabilized_global24);
|
||||||
|
ExpectGlobalZCovariance(original_mass.Value().consistent_global24,
|
||||||
|
rotated_mass.Value().consistent_global24);
|
||||||
|
ExpectGlobalZCovariance(original_mass.Value().lumped_global24,
|
||||||
|
rotated_mass.Value().lumped_global24);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MITC4-KERNEL-006
|
// MITC4-KERNEL-006
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ TEST(DomainMappingComponents,
|
|||||||
EXPECT_EQ(domain.Steps()[0].Loads().size(), 1U);
|
EXPECT_EQ(domain.Steps()[0].Loads().size(), 1U);
|
||||||
ASSERT_EQ(domain.Warnings().size(), 3U);
|
ASSERT_EQ(domain.Warnings().size(), 3U);
|
||||||
EXPECT_EQ(domain.Warnings()[0].keyword, "PREPRINT");
|
EXPECT_EQ(domain.Warnings()[0].keyword, "PREPRINT");
|
||||||
|
EXPECT_EQ(
|
||||||
|
domain.Warnings()[0].message,
|
||||||
|
"The allowlisted Abaqus keyword is ignored without semantic effect.");
|
||||||
EXPECT_EQ(domain.Warnings()[1].keyword, "OUTPUT");
|
EXPECT_EQ(domain.Warnings()[1].keyword, "OUTPUT");
|
||||||
EXPECT_EQ(domain.Warnings()[2].keyword, "NODE OUTPUT");
|
EXPECT_EQ(domain.Warnings()[2].keyword, "NODE OUTPUT");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "fesa/io/abaqus/domain_mapper.h"
|
||||||
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TemporaryInputFile {
|
||||||
|
public:
|
||||||
|
TemporaryInputFile(const std::string& stem, const std::string& content)
|
||||||
|
: path_{std::filesystem::temp_directory_path() /
|
||||||
|
("fesa-eigen-domain-" + stem + ".inp")} {
|
||||||
|
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
|
||||||
|
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
|
||||||
|
if (!stream) {
|
||||||
|
throw std::runtime_error{"Unable to create eigen domain fixture."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~TemporaryInputFile() {
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::remove(path_, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path& Path() const noexcept { return path_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
fesa::Result<fesa::Domain> MapText(const std::string& stem,
|
||||||
|
const std::string& content) {
|
||||||
|
const TemporaryInputFile input{stem, content};
|
||||||
|
auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
|
||||||
|
if (!parsed.HasValue()) {
|
||||||
|
return fesa::Result<fesa::Domain>::Failure(parsed.GetStatus());
|
||||||
|
}
|
||||||
|
return fesa::AbaqusDomainMapper{}.Map(parsed.Value());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string EigenDeck(const std::string& density = "2700.,",
|
||||||
|
const std::string& frequency = "10, , , , ,") {
|
||||||
|
return R"inp(*Part, name=ShellPart
|
||||||
|
*Node
|
||||||
|
1, 0., 0., 0.
|
||||||
|
2, 1., 0., 0.
|
||||||
|
3, 1., 1., 0.
|
||||||
|
4, 0., 1., 0.
|
||||||
|
*Element, type=S4R
|
||||||
|
1, 1, 2, 3, 4
|
||||||
|
*Elset, elset=ShellSet
|
||||||
|
1
|
||||||
|
*Shell Section, elset=ShellSet, material=Steel
|
||||||
|
0.5, 5
|
||||||
|
*End Part
|
||||||
|
*Assembly, name=Assembly
|
||||||
|
*Instance, name=Shell-1, part=ShellPart
|
||||||
|
*End Instance
|
||||||
|
*Nset, nset=Root, instance=Shell-1
|
||||||
|
1
|
||||||
|
*End Assembly
|
||||||
|
*Material, name=Steel
|
||||||
|
*Density
|
||||||
|
)inp" + density +
|
||||||
|
R"inp(
|
||||||
|
*Elastic
|
||||||
|
2.1e11, 0.3
|
||||||
|
*Boundary
|
||||||
|
Root, 1, 6
|
||||||
|
*Step, name=Step-2, nlgeom=NO, perturbation
|
||||||
|
*Frequency, eigensolver=Lanczos, sim, acoustic coupling=on, normalization=mass
|
||||||
|
)inp" + frequency +
|
||||||
|
R"inp(
|
||||||
|
*Restart, write, frequency=0
|
||||||
|
*Output, field
|
||||||
|
*Node Output, variable=PRESELECT
|
||||||
|
*Output, history
|
||||||
|
*Energy Output, variable=ALL
|
||||||
|
*End Step
|
||||||
|
)inp";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status,
|
||||||
|
const std::string& code) {
|
||||||
|
for (const auto& diagnostic : status.Diagnostics()) {
|
||||||
|
if (diagnostic.code == code) {
|
||||||
|
return &diagnostic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenDomainMapping, PreservesFrequencyDensityAndSourceStepIdentity) {
|
||||||
|
auto result = MapText("accepted", EigenDeck());
|
||||||
|
ASSERT_TRUE(result.HasValue());
|
||||||
|
const fesa::Domain& domain = result.Value();
|
||||||
|
|
||||||
|
ASSERT_EQ(domain.Densities().size(), 1U);
|
||||||
|
EXPECT_EQ(domain.Densities()[0].material_index, fesa::EntityIndex{0U});
|
||||||
|
EXPECT_DOUBLE_EQ(domain.Densities()[0].rho, 2700.0);
|
||||||
|
ASSERT_NE(domain.Frequency(), nullptr);
|
||||||
|
EXPECT_EQ(domain.Frequency()->source_step_name_raw, "Step-2");
|
||||||
|
EXPECT_EQ(domain.Frequency()->source_step_name_canonical, "STEP-2");
|
||||||
|
EXPECT_EQ(domain.Frequency()->requested_mode_count, 10);
|
||||||
|
EXPECT_TRUE(domain.Frequency()->perturbation_explicit);
|
||||||
|
EXPECT_TRUE(domain.Frequency()->sim_explicit);
|
||||||
|
ASSERT_EQ(domain.Steps().Size(), 1U);
|
||||||
|
EXPECT_TRUE(domain.Steps()[0U].IsFrequency());
|
||||||
|
EXPECT_EQ(domain.Steps()[0U].Frequency(), domain.Frequency());
|
||||||
|
EXPECT_EQ(domain.Steps()[0U].Name(), "Step-2");
|
||||||
|
ASSERT_EQ(domain.Steps()[0U].BoundaryConditions().size(), 6U);
|
||||||
|
EXPECT_TRUE(domain.Steps()[0U].Loads().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenDomainMapping, RejectsInvalidDensityAndFrequencySlots) {
|
||||||
|
auto density = MapText("bad-density", EigenDeck("2700., 1."));
|
||||||
|
ASSERT_FALSE(density.HasValue());
|
||||||
|
EXPECT_NE(FindDiagnostic(density.GetStatus(), "invalid-density-data"),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
auto slots = MapText("bad-slots", EigenDeck("2700.,", "10, 0"));
|
||||||
|
ASSERT_FALSE(slots.HasValue());
|
||||||
|
EXPECT_NE(FindDiagnostic(slots.GetStatus(), "invalid-frequency-data"),
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenDomainMapping, RejectsMissingActiveDensityAndNonzeroBoundary) {
|
||||||
|
std::string missing_deck = EigenDeck();
|
||||||
|
const std::string density_block = "*Density\n2700.,\n";
|
||||||
|
const std::size_t density_location = missing_deck.find(density_block);
|
||||||
|
ASSERT_NE(density_location, std::string::npos);
|
||||||
|
missing_deck.erase(density_location, density_block.size());
|
||||||
|
auto missing = MapText("missing-density", missing_deck);
|
||||||
|
ASSERT_FALSE(missing.HasValue());
|
||||||
|
EXPECT_NE(FindDiagnostic(missing.GetStatus(), "missing-material-density"),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
std::string nonzero = EigenDeck();
|
||||||
|
const std::string source = "Root, 1, 6";
|
||||||
|
const std::size_t location = nonzero.rfind(source);
|
||||||
|
ASSERT_NE(location, std::string::npos);
|
||||||
|
nonzero.replace(location, source.size(), "Root, 1, 6, 1.");
|
||||||
|
auto boundary = MapText("nonzero-boundary", nonzero);
|
||||||
|
ASSERT_FALSE(boundary.HasValue());
|
||||||
|
EXPECT_NE(FindDiagnostic(boundary.GetStatus(), "unsupported-eigen-boundary"),
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenDomainMapping, RejectsMixedProceduresInvalidOutputAndSurface) {
|
||||||
|
std::string mixed = EigenDeck();
|
||||||
|
mixed.insert(mixed.find("*Restart"), "*Static\n0.1, 1., 0.01, 1.\n");
|
||||||
|
auto mixed_result = MapText("mixed-procedures", mixed);
|
||||||
|
ASSERT_FALSE(mixed_result.HasValue());
|
||||||
|
EXPECT_NE(FindDiagnostic(mixed_result.GetStatus(),
|
||||||
|
"unsupported-analysis-procedure"),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
auto wrong_output = MapText(
|
||||||
|
"wrong-output",
|
||||||
|
EigenDeck().replace(EigenDeck().find("*Output, field\n*Node Output"),
|
||||||
|
std::string{"*Output, field\n*Node Output"}.size(),
|
||||||
|
"*Output, history\n*Node Output"));
|
||||||
|
ASSERT_FALSE(wrong_output.HasValue());
|
||||||
|
EXPECT_NE(FindDiagnostic(wrong_output.GetStatus(), "unsupported-keyword"),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
std::string surface = EigenDeck();
|
||||||
|
surface.insert(surface.find("*Material"),
|
||||||
|
"*Surface, name=Face, type=ELEMENT\n");
|
||||||
|
auto surface_result = MapText("surface", surface);
|
||||||
|
ASSERT_FALSE(surface_result.HasValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenDomainMapping, RejectsInvalidPreprintAndAcceptsStrictNoOp) {
|
||||||
|
const std::string preprint =
|
||||||
|
"*Preprint, echo=NO, model=NO, history=NO, contact=NO\n";
|
||||||
|
auto accepted = MapText("preprint", preprint + EigenDeck());
|
||||||
|
ASSERT_TRUE(accepted.HasValue());
|
||||||
|
const auto warning = std::find_if(accepted.Value().Warnings().begin(),
|
||||||
|
accepted.Value().Warnings().end(),
|
||||||
|
[](const fesa::Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.keyword == "PREPRINT";
|
||||||
|
});
|
||||||
|
ASSERT_NE(warning, accepted.Value().Warnings().end());
|
||||||
|
EXPECT_EQ(warning->message, "mandatory-eigen-output-unchanged");
|
||||||
|
|
||||||
|
auto rejected =
|
||||||
|
MapText("bad-preprint", "*Preprint, echo=YES\n" + EigenDeck());
|
||||||
|
ASSERT_FALSE(rejected.HasValue());
|
||||||
|
EXPECT_NE(FindDiagnostic(rejected.GetStatus(), "unsupported-keyword"),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
auto missing_parameters =
|
||||||
|
MapText("bare-preprint", "*Preprint\n" + EigenDeck());
|
||||||
|
ASSERT_FALSE(missing_parameters.HasValue());
|
||||||
|
EXPECT_NE(
|
||||||
|
FindDiagnostic(missing_parameters.GetStatus(), "unsupported-keyword"),
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
#include "io/hdf5/eigen_hdf5_self_check.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::ModelDefinition Definition(const std::filesystem::path& source) {
|
||||||
|
fesa::ModelDefinition definition{};
|
||||||
|
definition.source_path = source;
|
||||||
|
definition.source_content_identity = "fnv1a64:0123456789abcdef";
|
||||||
|
definition.nodes = {{{"I", 1, "1"}, {-1.0, -1.0, 0.0}, {source, 1U}},
|
||||||
|
{{"I", 2, "2"}, {1.0, -1.0, 0.0}, {source, 2U}},
|
||||||
|
{{"I", 3, "3"}, {1.0, 1.0, 0.0}, {source, 3U}},
|
||||||
|
{{"I", 4, "4"}, {-1.0, 1.0, 0.0}, {source, 4U}}};
|
||||||
|
definition.materials = {{"Steel", 12.0, 0.0, {source, 5U}}};
|
||||||
|
definition.shell_sections = {{"S", 1.0, 0U, {source, 6U}}};
|
||||||
|
definition.shell_elements = {{{"I", 1, "1"},
|
||||||
|
fesa::ShellSourceElementType::kS4,
|
||||||
|
{0U, 1U, 2U, 3U},
|
||||||
|
0U,
|
||||||
|
0U,
|
||||||
|
{source, 7U}}};
|
||||||
|
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||||
|
definition.shell_node_initial_frames.push_back(
|
||||||
|
{static_cast<fesa::EntityIndex>(node),
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0}});
|
||||||
|
}
|
||||||
|
definition.densities = {{0U, 1.0, {source, 8U}}};
|
||||||
|
definition.frequency_step =
|
||||||
|
fesa::FrequencyStepDefinition{"Step-2",
|
||||||
|
"STEP-2",
|
||||||
|
{source, 9U},
|
||||||
|
{source, 10U},
|
||||||
|
{source, 11U},
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
fesa::FrequencyEigensolver::kLanczos,
|
||||||
|
fesa::FrequencyNormalization::kMass,
|
||||||
|
{}};
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
|
||||||
|
const auto path =
|
||||||
|
std::filesystem::temp_directory_path() / "fesa-eigen-writer-test.h5";
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(path, ignored);
|
||||||
|
const auto source = path.parent_path() / "fesa-eigen-writer-test.inp";
|
||||||
|
auto domain_result = fesa::Domain::Create(Definition(source));
|
||||||
|
ASSERT_TRUE(domain_result.HasValue());
|
||||||
|
fesa::Domain domain{std::move(domain_result.Value())};
|
||||||
|
fesa::EigenHdf5ResultsWriter writer;
|
||||||
|
auto execution = fesa::LinearFrequencyProcedureExecutor{}.Execute(domain);
|
||||||
|
ASSERT_TRUE(execution.HasValue());
|
||||||
|
fesa::EigenAnalysisState state = std::move(execution.Value());
|
||||||
|
ASSERT_TRUE(writer.Write(path, domain, state, {}).IsOk());
|
||||||
|
const hid_t file =
|
||||||
|
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||||
|
ASSERT_GE(file, 0);
|
||||||
|
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/modes", H5P_DEFAULT), 0);
|
||||||
|
EXPECT_GE(H5Fclose(file), 0);
|
||||||
|
std::ifstream original_file{path, std::ios::binary};
|
||||||
|
const std::string original{std::istreambuf_iterator<char>{original_file}, {}};
|
||||||
|
original_file.close();
|
||||||
|
const std::vector<std::function<void(fesa::EigenAnalysisState&)>> mutations{
|
||||||
|
[](auto& candidate) {
|
||||||
|
candidate.vectors(0U, 0U) = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
},
|
||||||
|
[](auto& candidate) { candidate.eigenpair_checks[0].rho_b = 1e-5; },
|
||||||
|
[](auto& candidate) { candidate.eigenpair_checks[0].eigenvalue += 1.0; },
|
||||||
|
[](auto& candidate) { candidate.modes[0].cluster_id = 7U; },
|
||||||
|
[](auto& candidate) { candidate.constraint_mask[0] = 1U; },
|
||||||
|
[](auto& candidate) { candidate.nodal_reduction[0].rcond_inf = 1e-15; },
|
||||||
|
[](auto& candidate) { candidate.eta_gram = 1e-5; }};
|
||||||
|
for (const auto& mutate : mutations) {
|
||||||
|
auto candidate = state;
|
||||||
|
mutate(candidate);
|
||||||
|
EXPECT_FALSE(writer.Write(path, domain, candidate, {}).IsOk());
|
||||||
|
}
|
||||||
|
std::ifstream retained_file{path, std::ios::binary};
|
||||||
|
const std::string retained{std::istreambuf_iterator<char>{retained_file}, {}};
|
||||||
|
EXPECT_TRUE(retained == original);
|
||||||
|
retained_file.close();
|
||||||
|
ASSERT_TRUE(
|
||||||
|
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
|
||||||
|
const hid_t corrupt_file =
|
||||||
|
H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||||
|
ASSERT_GE(corrupt_file, 0);
|
||||||
|
const hid_t nodes = H5Dopen2(corrupt_file, "/model/nodes", H5P_DEFAULT);
|
||||||
|
ASSERT_GE(nodes, 0);
|
||||||
|
const hid_t id_type = H5Tcreate(H5T_COMPOUND, sizeof(std::uint64_t));
|
||||||
|
ASSERT_GE(H5Tinsert(id_type, "internal_node_id", 0U, H5T_NATIVE_UINT64), 0);
|
||||||
|
const std::array<std::uint64_t, 4> duplicate_ids{0U, 0U, 2U, 3U};
|
||||||
|
ASSERT_GE(H5Dwrite(nodes, id_type, H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||||
|
duplicate_ids.data()),
|
||||||
|
0);
|
||||||
|
EXPECT_GE(H5Tclose(id_type), 0);
|
||||||
|
EXPECT_GE(H5Dclose(nodes), 0);
|
||||||
|
EXPECT_GE(H5Fclose(corrupt_file), 0);
|
||||||
|
EXPECT_FALSE(
|
||||||
|
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
|
||||||
|
std::filesystem::remove(path, ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <mkl.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/math/sparse_matrix.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::SparseMatrix MakeDenseCsr(const std::size_t size,
|
||||||
|
const std::vector<double>& values) {
|
||||||
|
EXPECT_EQ(values.size(), size * size);
|
||||||
|
fesa::SparsePattern pattern;
|
||||||
|
std::vector<fesa::CooContribution> contributions;
|
||||||
|
pattern.row_offsets.push_back(0U);
|
||||||
|
for (std::size_t row = 0U; row < size; ++row) {
|
||||||
|
for (std::size_t column = 0U; column < size; ++column) {
|
||||||
|
pattern.column_indices.push_back(column);
|
||||||
|
contributions.push_back(
|
||||||
|
{row, column, values[row * size + column], row, column});
|
||||||
|
}
|
||||||
|
pattern.row_offsets.push_back(pattern.column_indices.size());
|
||||||
|
}
|
||||||
|
auto matrix = fesa::SparseMatrix::FromCoo(size, size,
|
||||||
|
std::move(contributions), pattern);
|
||||||
|
EXPECT_TRUE(matrix.HasValue());
|
||||||
|
return std::move(matrix.Value());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectUnitGram(const fesa::Matrix& vectors) {
|
||||||
|
for (std::size_t left = 0U; left < vectors.Columns(); ++left) {
|
||||||
|
for (std::size_t right = 0U; right < vectors.Columns(); ++right) {
|
||||||
|
double dot = 0.0;
|
||||||
|
for (std::size_t row = 0U; row < vectors.Rows(); ++row) {
|
||||||
|
dot += vectors(row, left) * vectors(row, right);
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(dot, left == right ? 1.0 : 0.0, 1.0e-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RequireSuccess(const fesa::Result<fesa::FeastEigenResult>& result) {
|
||||||
|
if (result.HasValue()) return;
|
||||||
|
const auto& diagnostics = result.GetStatus().Diagnostics();
|
||||||
|
ASSERT_FALSE(diagnostics.empty());
|
||||||
|
ADD_FAILURE() << diagnostics.front().code << " "
|
||||||
|
<< diagnostics.front().entity_identity << " "
|
||||||
|
<< diagnostics.front().message;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, SolvesN1AndRetainsBackendEvidence) {
|
||||||
|
const auto matrix = MakeDenseCsr(1U, {4.0});
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto result = solver.Solve(matrix, 1U, 1.0);
|
||||||
|
|
||||||
|
RequireSuccess(result);
|
||||||
|
if (!result.HasValue()) return;
|
||||||
|
EXPECT_EQ(result.Value().requested_modes, 1U);
|
||||||
|
ASSERT_EQ(result.Value().eigenvalues.size(), 1U);
|
||||||
|
EXPECT_NEAR(result.Value().eigenvalues[0U], 4.0, 1.0e-10);
|
||||||
|
EXPECT_DOUBLE_EQ(result.Value().operator_scale, 4.0);
|
||||||
|
EXPECT_EQ(result.Value().eigenvectors.Rows(), 1U);
|
||||||
|
EXPECT_EQ(result.Value().eigenvectors.Columns(), 1U);
|
||||||
|
EXPECT_EQ(result.Value().thread_count, 1U);
|
||||||
|
ASSERT_FALSE(result.Value().backend_calls.empty());
|
||||||
|
EXPECT_EQ(result.Value().backend_calls.front().purpose, "total-enclosure");
|
||||||
|
EXPECT_EQ(result.Value().backend_calls.back().kind, "solve");
|
||||||
|
EXPECT_TRUE(result.Value().backend_calls.back().count_available);
|
||||||
|
EXPECT_EQ(result.Value().backend_calls.back().count,
|
||||||
|
result.Value().eigenvalues.size());
|
||||||
|
EXPECT_TRUE(result.Value().backend_calls.back().loop_available);
|
||||||
|
EXPECT_TRUE(result.Value().backend_calls.back().epsout_available);
|
||||||
|
EXPECT_EQ(result.Value().backend_calls.back().outcome, "accepted");
|
||||||
|
EXPECT_EQ(result.Value().backend_calls.back().decision_reason, "accepted");
|
||||||
|
EXPECT_FALSE(result.Value().upper_search_termination.empty());
|
||||||
|
ExpectUnitGram(result.Value().eigenvectors);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, SolvesAllZeroWithExplicitPhysicalScale) {
|
||||||
|
const auto matrix = MakeDenseCsr(2U, {0.0, 0.0, 0.0, 0.0});
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto result = solver.Solve(matrix, 2U, 7.0);
|
||||||
|
|
||||||
|
RequireSuccess(result);
|
||||||
|
if (!result.HasValue()) return;
|
||||||
|
ASSERT_EQ(result.Value().eigenvalues.size(), 2U);
|
||||||
|
EXPECT_DOUBLE_EQ(result.Value().operator_scale, 7.0);
|
||||||
|
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[0U], 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(result.Value().eigenvalues[1U], 0.0);
|
||||||
|
ExpectUnitGram(result.Value().eigenvectors);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, PreservesRepeatedMultiplicityAndStableOrder) {
|
||||||
|
const auto matrix =
|
||||||
|
MakeDenseCsr(3U, {2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 5.0});
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto first = solver.Solve(matrix, 3U, 1.0);
|
||||||
|
const auto second = solver.Solve(matrix, 3U, 1.0);
|
||||||
|
|
||||||
|
RequireSuccess(first);
|
||||||
|
RequireSuccess(second);
|
||||||
|
if (!first.HasValue() || !second.HasValue()) return;
|
||||||
|
ASSERT_EQ(first.Value().eigenvalues.size(), 3U);
|
||||||
|
EXPECT_NEAR(first.Value().eigenvalues[0U], 2.0, 1.0e-10);
|
||||||
|
EXPECT_NEAR(first.Value().eigenvalues[1U], 2.0, 1.0e-10);
|
||||||
|
EXPECT_NEAR(first.Value().eigenvalues[2U], 5.0, 1.0e-10);
|
||||||
|
EXPECT_EQ(first.Value().column_ids, second.Value().column_ids);
|
||||||
|
ExpectUnitGram(first.Value().eigenvectors);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, UsesGuardedPrefixWithoutDroppingRepeatedCluster) {
|
||||||
|
constexpr std::size_t kSize = 64U;
|
||||||
|
std::vector<double> values(kSize * kSize, 0.0);
|
||||||
|
for (std::size_t index = 0U; index < kSize; ++index) {
|
||||||
|
values[index * kSize + index] = index < 40U ? 1.0 : index - 38U;
|
||||||
|
}
|
||||||
|
const auto matrix = MakeDenseCsr(kSize, values);
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto one_mode = solver.Solve(matrix, 1U, 1.0);
|
||||||
|
const auto forty_first = solver.Solve(matrix, 41U, 1.0);
|
||||||
|
|
||||||
|
RequireSuccess(one_mode);
|
||||||
|
RequireSuccess(forty_first);
|
||||||
|
if (!one_mode.HasValue() || !forty_first.HasValue()) return;
|
||||||
|
EXPECT_GE(one_mode.Value().eigenvalues.size(), 40U);
|
||||||
|
EXPECT_GE(forty_first.Value().eigenvalues.size(), 41U);
|
||||||
|
for (std::size_t index = 0U; index < 40U; ++index) {
|
||||||
|
EXPECT_NEAR(one_mode.Value().eigenvalues[index], 1.0, 1.0e-10);
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(forty_first.Value().eigenvalues[40U], 2.0, 1.0e-10);
|
||||||
|
EXPECT_EQ(one_mode.Value().backend_calls.back().kind, "solve");
|
||||||
|
EXPECT_EQ(forty_first.Value().backend_calls.back().kind, "solve");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, RejectsSignificantNegativeSpectrum) {
|
||||||
|
const auto matrix = MakeDenseCsr(1U, {-2.0});
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto result = solver.Solve(matrix, 1U, 1.0);
|
||||||
|
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kSolver);
|
||||||
|
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
|
||||||
|
"negative-eigenvalue-spectrum");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, KeepsCloseDistinctModesSeparated) {
|
||||||
|
const auto matrix = MakeDenseCsr(2U, {1.0, 0.0, 0.0, 1.001});
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto result = solver.Solve(matrix, 2U, 1.0);
|
||||||
|
|
||||||
|
RequireSuccess(result);
|
||||||
|
if (!result.HasValue()) return;
|
||||||
|
ASSERT_EQ(result.Value().eigenvalues.size(), 2U);
|
||||||
|
EXPECT_NEAR(result.Value().eigenvalues[0U], 1.0, 1.0e-10);
|
||||||
|
EXPECT_NEAR(result.Value().eigenvalues[1U], 1.001, 1.0e-10);
|
||||||
|
EXPECT_NEAR(std::abs(result.Value().eigenvectors(0U, 0U)), 1.0, 1.0e-10);
|
||||||
|
EXPECT_NEAR(std::abs(result.Value().eigenvectors(1U, 1U)), 1.0, 1.0e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, RestoresCallingThreadLocalSetting) {
|
||||||
|
const auto matrix = MakeDenseCsr(1U, {4.0});
|
||||||
|
const int prior = mkl_set_num_threads_local(2);
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto result = solver.Solve(matrix, 1U, 1.0);
|
||||||
|
const int restored = mkl_set_num_threads_local(2);
|
||||||
|
static_cast<void>(mkl_set_num_threads_local(prior));
|
||||||
|
|
||||||
|
RequireSuccess(result);
|
||||||
|
if (!result.HasValue()) return;
|
||||||
|
EXPECT_EQ(restored, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, RejectsInvalidRequestAndZeroScale) {
|
||||||
|
const auto matrix = MakeDenseCsr(1U, {0.0});
|
||||||
|
fesa::MklFeastEigenSolver solver;
|
||||||
|
|
||||||
|
const auto zero_request = solver.Solve(matrix, 0U, 1.0);
|
||||||
|
ASSERT_FALSE(zero_request.HasValue());
|
||||||
|
EXPECT_EQ(zero_request.GetStatus().Diagnostics()[0U].code,
|
||||||
|
"feast-invalid-dimension");
|
||||||
|
const auto invalid_scale = solver.Solve(matrix, 1U, 0.0);
|
||||||
|
ASSERT_FALSE(invalid_scale.HasValue());
|
||||||
|
EXPECT_EQ(invalid_scale.GetStatus().Diagnostics()[0U].code,
|
||||||
|
"feast-invalid-zero-scale");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, PreservesLowestPrefixForZeroAndPositiveSpectra) {
|
||||||
|
const std::vector<std::vector<double>> spectra{{2.0},
|
||||||
|
{0.0, 0.0, 0.0, 2.0, 5.0, 9.0},
|
||||||
|
{1.0, 2.0, 4.0, 8.0},
|
||||||
|
{0.0, 0.0, 0.0, 0.0}};
|
||||||
|
const std::vector<std::vector<std::size_t>> requests{
|
||||||
|
{1U}, {1U, 4U, 6U}, {1U, 2U, 4U}, {1U, 4U}};
|
||||||
|
const fesa::MklFeastEigenSolver solver;
|
||||||
|
for (std::size_t case_index = 0U; case_index < spectra.size(); ++case_index) {
|
||||||
|
const auto& spectrum = spectra[case_index];
|
||||||
|
const auto n = spectrum.size();
|
||||||
|
std::vector<double> values(n * n, 0.0);
|
||||||
|
for (std::size_t i = 0U; i < n; ++i) values[i * n + i] = spectrum[i];
|
||||||
|
const auto matrix = MakeDenseCsr(n, values);
|
||||||
|
for (const auto requested : requests[case_index]) {
|
||||||
|
const auto result = solver.Solve(matrix, requested, 1.0);
|
||||||
|
RequireSuccess(result);
|
||||||
|
ASSERT_TRUE(result.HasValue());
|
||||||
|
ASSERT_GE(result.Value().eigenvalues.size(), requested);
|
||||||
|
for (std::size_t i = 0U; i < requested; ++i)
|
||||||
|
EXPECT_NEAR(result.Value().eigenvalues[i], spectrum[i], 1e-10);
|
||||||
|
ExpectUnitGram(result.Value().eigenvectors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MklFeastEigenSolver, ResolvesCloseDistinctSpectrumAfterGivensCongruence) {
|
||||||
|
const std::array<double, 4> spectrum{1.0, 1.0 + std::ldexp(1.0, -28),
|
||||||
|
1.0 + std::ldexp(1.0, -27), 8.0};
|
||||||
|
const std::array<std::array<double, 4>, 4> q{{{{0.6, 0.0, 0.0, -0.8}},
|
||||||
|
{{0.0, 0.6, -0.8, 0.0}},
|
||||||
|
{{0.0, 0.8, 0.6, 0.0}},
|
||||||
|
{{0.8, 0.0, 0.0, 0.6}}}};
|
||||||
|
std::vector<double> values(16U, 0.0);
|
||||||
|
for (std::size_t i = 0U; i < 4U; ++i)
|
||||||
|
for (std::size_t j = 0U; j < 4U; ++j)
|
||||||
|
for (std::size_t k = 0U; k < 4U; ++k)
|
||||||
|
values[4U * i + j] += q[i][k] * spectrum[k] * q[j][k];
|
||||||
|
const auto result =
|
||||||
|
fesa::MklFeastEigenSolver{}.Solve(MakeDenseCsr(4U, values), 2U, 1.0);
|
||||||
|
RequireSuccess(result);
|
||||||
|
ASSERT_TRUE(result.HasValue());
|
||||||
|
ASSERT_EQ(result.Value().eigenvalues.size(), 4U);
|
||||||
|
for (std::size_t i = 0U; i < 4U; ++i)
|
||||||
|
EXPECT_NEAR(result.Value().eigenvalues[i], spectrum[i], 1e-12);
|
||||||
|
EXPECT_GT(result.Value().eigenvalues[1U] - result.Value().eigenvalues[0U],
|
||||||
|
1e-9);
|
||||||
|
ExpectUnitGram(result.Value().eigenvectors);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user