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,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(
|
||||
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);
|
||||
if (!model.HasValue()) {
|
||||
return Result<LinearStaticExecutionResult>::Failure(model.GetStatus());
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/analysis/linear_frequency_procedure.h"
|
||||
#include "fesa/analysis/linear_static_procedure.h"
|
||||
#include "fesa/analysis/newton_iteration_log.h"
|
||||
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||
#include "fesa/app/output_paths.h"
|
||||
#include "fesa/io/abaqus/domain_mapper.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/results_writer.h"
|
||||
|
||||
@@ -34,12 +36,26 @@ Result<PreparedAnalysisInput> PrepareAnalysisInput(
|
||||
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
||||
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
||||
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
||||
const LinearFrequencyProcedureExecutor& frequency_executor,
|
||||
NewtonIterationLogWriter& log) {
|
||||
if (domain.Steps().Size() != 1U) {
|
||||
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
|
||||
FailureCategory::kInput,
|
||||
{{Severity::kError, "invalid-analysis-step-count", {}, "*STEP", {},
|
||||
"Prepared analysis requires exactly one static step."}}));
|
||||
{{Severity::kError,
|
||||
"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()) {
|
||||
@@ -64,7 +80,13 @@ Status FinalizePreparedAnalysisResult(
|
||||
const DerivedOutputPaths& paths, const Domain& domain,
|
||||
const PreparedAnalysisOutcome& outcome,
|
||||
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
||||
NonlinearResultsWriter& nonlinear_writer) {
|
||||
NonlinearResultsWriter& nonlinear_writer,
|
||||
EigenResultsWriter& eigen_writer) {
|
||||
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)) {
|
||||
return linear_writer.Write(
|
||||
paths.results_path, domain,
|
||||
|
||||
Reference in New Issue
Block a user