#include "fesa/analysis/linear_frequency_procedure.h" #include #include #include #include #include #include #include #include #include #include #include #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" #include "fesa/solvers/eigen/mkl_feast_eigen_solver.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::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 ExecuteCandidate(const Domain& domain, const EigenSolver& solver) { const FrequencyStepDefinition* frequency = domain.Frequency(); if (frequency == nullptr || domain.Steps().Size() != 1U) { return Result::Failure( Failure("invalid-frequency-procedure", "step", "The domain has no sole frequency step.")); } if (frequency->requested_mode_count <= 0) { return Result::Failure( Failure("invalid-frequency-request", "requested-mode-count", "Frequency requested mode count must be positive.")); } Result model = AnalysisModel::Create(domain); if (!model.HasValue()) return Result::Failure(model.GetStatus()); const ElementFactory factory; std::vector> owned; std::vector> references; 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::Failure(element.GetStatus()); auto reference = factory.BindShellReference(*element.Value()); if (reference == nullptr) { return Result::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())); references.push_back(std::move(reference)); } DofManager dofs; const Status dof_status = dofs.Build(model.Value(), view); if (!dof_status.IsOk()) return Result::Failure(dof_status); const SerialParallelFor serial; auto stiffness = SparseAssembler::Assemble(view, dofs, serial); if (!stiffness.HasValue()) return Result::Failure(stiffness.GetStatus()); std::vector 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::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::Failure( Status::Failure(FailureCategory::kInput, {{Severity::kError, "unsupported-eigen-boundary", frequency->frequency_location, "BOUNDARY", {}, "Frequency requires homogeneous constraints."}})); } } std::vector nodal_mass(domain.Nodes().size(), Matrix(6U, 6U)); std::vector 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::Failure( Failure("missing-frequency-density", std::to_string(definition.MaterialIndex()), "Every active shell material requires a positive density.")); } const auto& element = references[shell]; auto mass = element->Mass(density[definition.MaterialIndex()]); auto shell_stiffness = element->Stiffness(); if (!mass.HasValue()) return Result::Failure(mass.GetStatus()); if (!shell_stiffness.HasValue()) return Result::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 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::Failure(reduction.GetStatus()); const std::size_t request = static_cast(frequency->requested_mode_count); if (request > reduction.Value().whitened_operator.Rows()) { return Result::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::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( &domain.Materials()[material_id]); if (material == nullptr) return Result::Failure( Failure("invalid-eigenvalue-result", "material", "Unsupported frequency material.")); zero_scale = (std::min)(zero_scale, (material->YoungsModulus() / density[material_id]) / length / length); } auto backend = solver.Solve( reduction.Value().whitened_operator, request, zero_scale); if (!backend.HasValue()) return Result::Failure(backend.GetStatus()); EigenAnalysisState state(std::move(backend.Value())); for (const auto& reference : references) { if (state.drilling_mode && *state.drilling_mode != reference->DrillingMode()) return Result::Failure(Failure( "invalid-shell-drilling-evidence", "mode", "All frequency kernels must retain one actual drilling mode.")); state.drilling_mode = reference->DrillingMode(); } state.full_dof_count = dofs.FullDofCount(); state.free_dof_count = reduction.Value().free_count; state.mass_null_dimension = reduction.Value().null_count; 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 all_modes; std::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::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::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::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::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>{ {"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::Failure(MetricFailure( metric.first, metric.second, state.backend.column_ids[column])); } if (!std::isfinite(rayleigh)) return Result::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 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::Failure( Failure("invalid-eigenvalue-result", std::to_string(column), "Cycle frequency conversion failed.")); } state.modes.push_back( {column + 1U, lambda, static_cast(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(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::Failure( MetricFailure("eta_gram", state.eta_gram, 0U)); } state.backend.eigenvectors = Matrix(0U, 0U); return Result::Success(std::move(state)); } } // namespace Result LinearFrequencyProcedureExecutor::Execute( const Domain& domain) const { try { const MklFeastEigenSolver default_solver; auto result = ExecuteCandidate(domain, solver_ ? *solver_ : default_solver); 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::Failure(Status::Failure( result.GetStatus().Category().value_or(FailureCategory::kSolver), std::move(diagnostics))); } return result; } catch (const std::invalid_argument&) { return Result::Failure( Failure("invalid-eigenvalue-result", "modal-workspace", "Modal product inputs must be finite and shape compatible.")); } catch (const std::bad_alloc&) { return Result::Failure( Failure("eigen-allocation-failed", "modal-workspace", "Allocation failed after dimension checks; requested modes " "were not reduced.")); } catch (const std::length_error&) { return Result::Failure( Failure("eigen-index-overflow", "modal-workspace", "Requested workspace exceeds container capacity.")); } catch (const std::runtime_error&) { return Result::Failure( Failure("eigen-multiplication-failed", "modal-workspace", "Modal sparse multiplication failed.")); } } } // namespace fesa