441 lines
19 KiB
C++
441 lines
19 KiB
C++
#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) {
|
|
return left.Dot(right);
|
|
}
|
|
|
|
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::isfinite(physical[dof]))
|
|
return Result<EigenAnalysisState>::Failure(
|
|
MetricFailure("initial_modal_mass", physical[dof],
|
|
state.backend.column_ids[column]));
|
|
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;
|
|
if (!std::isfinite(x[row]))
|
|
return Result<EigenAnalysisState>::Failure(
|
|
Failure("invalid-eigenvalue-result", std::to_string(column),
|
|
"Recovered mode is nonfinite or violates constraints."));
|
|
}
|
|
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::invalid_argument&) {
|
|
return Result<EigenAnalysisState>::Failure(
|
|
Failure("invalid-eigenvalue-result", "modal-workspace",
|
|
"Modal product inputs must be finite and shape compatible."));
|
|
} 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."));
|
|
} catch (const std::runtime_error&) {
|
|
return Result<EigenAnalysisState>::Failure(
|
|
Failure("eigen-multiplication-failed", "modal-workspace",
|
|
"Modal sparse multiplication failed."));
|
|
}
|
|
}
|
|
|
|
} // namespace fesa
|