feat(cpp-object-oriented-modular-refactoring): step 5 - solver-workflow-google-style
This commit is contained in:
@@ -1,96 +1,90 @@
|
||||
#include "fesa/analysis/analysis_model.hpp"
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
Result<AnalysisModel> AnalysisModel::create(const Domain& domain) {
|
||||
if (domain.Steps().empty()) {
|
||||
return Result<AnalysisModel>::Failure(Status::Failure(
|
||||
FailureCategory::kInput,
|
||||
{{Severity::kError,
|
||||
"invalid-model-cardinality",
|
||||
{domain.SourcePath(), 0U},
|
||||
"STEP",
|
||||
"0",
|
||||
"AnalysisModel requires exactly one static step."}}));
|
||||
}
|
||||
if (domain.Steps().size() > 1U) {
|
||||
const auto& secondStep = domain.Steps()[1];
|
||||
return Result<AnalysisModel>::Failure(Status::Failure(
|
||||
FailureCategory::kInput,
|
||||
{{Severity::kError,
|
||||
"unsupported-multiple-step",
|
||||
secondStep.location,
|
||||
"STEP",
|
||||
secondStep.name,
|
||||
"AnalysisModel does not support multiple steps."}}));
|
||||
}
|
||||
return Result<AnalysisModel>::Success(AnalysisModel{domain});
|
||||
Result<AnalysisModel> AnalysisModel::Create(const Domain& domain) {
|
||||
if (domain.Steps().empty()) {
|
||||
return Result<AnalysisModel>::Failure(
|
||||
Status::Failure(FailureCategory::kInput,
|
||||
{{Severity::kError,
|
||||
"invalid-model-cardinality",
|
||||
{domain.SourcePath(), 0U},
|
||||
"STEP",
|
||||
"0",
|
||||
"AnalysisModel requires exactly one static step."}}));
|
||||
}
|
||||
if (domain.Steps().size() > 1U) {
|
||||
const auto& second_step = domain.Steps()[1];
|
||||
return Result<AnalysisModel>::Failure(
|
||||
Status::Failure(FailureCategory::kInput,
|
||||
{{Severity::kError, "unsupported-multiple-step",
|
||||
second_step.location, "STEP", second_step.name,
|
||||
"AnalysisModel does not support multiple steps."}}));
|
||||
}
|
||||
return Result<AnalysisModel>::Success(AnalysisModel{domain});
|
||||
}
|
||||
|
||||
const Domain& AnalysisModel::domain() const noexcept {
|
||||
return *domain_;
|
||||
const Domain& AnalysisModel::GetDomain() const noexcept { return *domain_; }
|
||||
|
||||
const StaticStepDefinition& AnalysisModel::Step() const noexcept {
|
||||
return domain_->Steps().front();
|
||||
}
|
||||
|
||||
const StaticStepDefinition& AnalysisModel::step() const noexcept {
|
||||
return domain_->Steps().front();
|
||||
const std::vector<EntityIndex>& AnalysisModel::ActiveElements() const noexcept {
|
||||
return active_elements_;
|
||||
}
|
||||
|
||||
const std::vector<EntityIndex>& AnalysisModel::activeElements() const noexcept {
|
||||
return activeElements_;
|
||||
const std::vector<EntityIndex>& AnalysisModel::ActiveMaterials()
|
||||
const noexcept {
|
||||
return active_materials_;
|
||||
}
|
||||
|
||||
const std::vector<EntityIndex>& AnalysisModel::activeMaterials() const noexcept {
|
||||
return activeMaterials_;
|
||||
const std::vector<EntityIndex>& AnalysisModel::ActiveSections() const noexcept {
|
||||
return active_sections_;
|
||||
}
|
||||
|
||||
const std::vector<EntityIndex>& AnalysisModel::activeSections() const noexcept {
|
||||
return activeSections_;
|
||||
const std::vector<EntityIndex>& AnalysisModel::ActiveBoundaryConditions()
|
||||
const noexcept {
|
||||
return active_boundary_conditions_;
|
||||
}
|
||||
|
||||
const std::vector<EntityIndex>&
|
||||
AnalysisModel::activeBoundaryConditions() const noexcept {
|
||||
return activeBoundaryConditions_;
|
||||
}
|
||||
|
||||
const std::vector<EntityIndex>& AnalysisModel::activeLoads() const noexcept {
|
||||
return activeLoads_;
|
||||
const std::vector<EntityIndex>& AnalysisModel::ActiveLoads() const noexcept {
|
||||
return active_loads_;
|
||||
}
|
||||
|
||||
AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} {
|
||||
std::vector<bool> reachableMaterials(domain.Materials().size(), false);
|
||||
std::vector<bool> reachableSections(domain.Sections().size(), false);
|
||||
std::vector<bool> reachable_materials(domain.Materials().size(), false);
|
||||
std::vector<bool> reachable_sections(domain.Sections().size(), false);
|
||||
|
||||
for (std::size_t index = 0U; index < domain.Elements().size(); ++index) {
|
||||
const auto& element = domain.Elements()[index];
|
||||
activeElements_.push_back(static_cast<EntityIndex>(index));
|
||||
reachableMaterials[element.material_index] = true;
|
||||
reachableSections[element.section_index] = true;
|
||||
}
|
||||
for (std::size_t index = 0U; index < domain.Elements().size(); ++index) {
|
||||
const auto& element = domain.Elements()[index];
|
||||
active_elements_.push_back(static_cast<EntityIndex>(index));
|
||||
reachable_materials[element.material_index] = true;
|
||||
reachable_sections[element.section_index] = true;
|
||||
}
|
||||
|
||||
// Ascending vector positions are the stable internal order, independent
|
||||
// of first reachability or duplicate element assignments.
|
||||
for (std::size_t index = 0U; index < reachableMaterials.size(); ++index) {
|
||||
if (reachableMaterials[index]) {
|
||||
activeMaterials_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
// Ascending vector positions are the stable internal order, independent
|
||||
// of first reachability or duplicate element assignments.
|
||||
for (std::size_t index = 0U; index < reachable_materials.size(); ++index) {
|
||||
if (reachable_materials[index]) {
|
||||
active_materials_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
for (std::size_t index = 0U; index < reachableSections.size(); ++index) {
|
||||
if (reachableSections[index]) {
|
||||
activeSections_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
}
|
||||
for (std::size_t index = 0U; index < reachable_sections.size(); ++index) {
|
||||
if (reachable_sections[index]) {
|
||||
active_sections_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0U;
|
||||
index < step().boundaries.size();
|
||||
++index) {
|
||||
activeBoundaryConditions_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
for (std::size_t index = 0U; index < step().loads.size(); ++index) {
|
||||
activeLoads_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
for (std::size_t index = 0U; index < Step().boundaries.size(); ++index) {
|
||||
active_boundary_conditions_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
for (std::size_t index = 0U; index < Step().loads.size(); ++index) {
|
||||
active_loads_.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "fesa/analysis/analysis_state.hpp"
|
||||
#include "fesa/analysis/analysis_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -12,244 +12,212 @@ namespace {
|
||||
|
||||
constexpr std::size_t kShellLocationsPerElement = 4U;
|
||||
|
||||
Status shellCandidateFailure(
|
||||
const std::string& code,
|
||||
const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kModel,
|
||||
{{Severity::kError,
|
||||
code,
|
||||
{},
|
||||
"ANALYSIS_STATE",
|
||||
identity,
|
||||
message}});
|
||||
/// @brief Creates a structured failure without mutating existing state.
|
||||
Status ShellCandidateFailure(const std::string& code,
|
||||
const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kModel,
|
||||
{{Severity::kError, code, {}, "ANALYSIS_STATE", identity, message}});
|
||||
}
|
||||
|
||||
template<std::size_t Size>
|
||||
bool finite(const std::array<double, Size>& values) {
|
||||
return std::all_of(
|
||||
values.begin(), values.end(),
|
||||
[](const double value) { return std::isfinite(value); });
|
||||
template <std::size_t Size>
|
||||
/// @brief Tests one fixed-size candidate component array for finite values.
|
||||
bool IsFinite(const std::array<double, Size>& values) {
|
||||
return std::all_of(values.begin(), values.end(),
|
||||
[](const double value) { return std::isfinite(value); });
|
||||
}
|
||||
|
||||
bool finite(const ShellResultRow& row) {
|
||||
if (!finite(row.naturalCoordinates) ||
|
||||
!finite(row.generalizedStrain) ||
|
||||
!finite(row.sectionResultant)) {
|
||||
return false;
|
||||
/// @brief Tests every physical shell row component for finite values.
|
||||
bool IsFinite(const ShellResultRow& row) {
|
||||
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
|
||||
!IsFinite(row.section_resultant)) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& axis : row.local_frame) {
|
||||
if (!IsFinite(axis)) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& axis : row.localFrame) {
|
||||
if (!finite(axis)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return std::all_of(
|
||||
row.stress.begin(), row.stress.end(),
|
||||
[](const ShellSectionStressRow& stress) {
|
||||
return std::isfinite(stress.zeta) && finite(stress.components);
|
||||
});
|
||||
}
|
||||
return std::all_of(row.stress.begin(), row.stress.end(),
|
||||
[](const ShellSectionStressRow& stress) {
|
||||
return std::isfinite(stress.zeta) &&
|
||||
IsFinite(stress.components);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
AnalysisState AnalysisState::create(
|
||||
const DofManager& dofs, StepFrameIdentity identity) {
|
||||
return AnalysisState{dofs.fullDofCount(), std::move(identity)};
|
||||
AnalysisState AnalysisState::Create(const DofManager& dofs,
|
||||
StepFrameIdentity identity) {
|
||||
return AnalysisState{dofs.FullDofCount(), std::move(identity)};
|
||||
}
|
||||
|
||||
Vector& AnalysisState::displacement() noexcept {
|
||||
return displacement_;
|
||||
Vector& AnalysisState::Displacement() noexcept { return displacement_; }
|
||||
|
||||
const Vector& AnalysisState::Displacement() const noexcept {
|
||||
return displacement_;
|
||||
}
|
||||
|
||||
const Vector& AnalysisState::displacement() const noexcept {
|
||||
return displacement_;
|
||||
Vector& AnalysisState::ExternalForce() noexcept { return external_force_; }
|
||||
|
||||
const Vector& AnalysisState::ExternalForce() const noexcept {
|
||||
return external_force_;
|
||||
}
|
||||
|
||||
Vector& AnalysisState::externalForce() noexcept {
|
||||
return externalForce_;
|
||||
Vector& AnalysisState::InternalForce() noexcept { return internal_force_; }
|
||||
|
||||
const Vector& AnalysisState::InternalForce() const noexcept {
|
||||
return internal_force_;
|
||||
}
|
||||
|
||||
const Vector& AnalysisState::externalForce() const noexcept {
|
||||
return externalForce_;
|
||||
Vector& AnalysisState::Residual() noexcept { return residual_; }
|
||||
|
||||
const Vector& AnalysisState::Residual() const noexcept { return residual_; }
|
||||
|
||||
Vector& AnalysisState::Reaction() noexcept { return reaction_; }
|
||||
|
||||
const Vector& AnalysisState::Reaction() const noexcept { return reaction_; }
|
||||
|
||||
const StepFrameIdentity& AnalysisState::Identity() const noexcept {
|
||||
return identity_;
|
||||
}
|
||||
|
||||
Vector& AnalysisState::internalForce() noexcept {
|
||||
return internalForce_;
|
||||
std::vector<EndpointResultRow>& AnalysisState::EndpointResults() noexcept {
|
||||
return endpoint_results_;
|
||||
}
|
||||
|
||||
const Vector& AnalysisState::internalForce() const noexcept {
|
||||
return internalForce_;
|
||||
const std::vector<EndpointResultRow>& AnalysisState::EndpointResults()
|
||||
const noexcept {
|
||||
return endpoint_results_;
|
||||
}
|
||||
|
||||
Vector& AnalysisState::residual() noexcept {
|
||||
return residual_;
|
||||
std::vector<GaussResultRow>& AnalysisState::GaussResults() noexcept {
|
||||
return gauss_results_;
|
||||
}
|
||||
|
||||
const Vector& AnalysisState::residual() const noexcept {
|
||||
return residual_;
|
||||
const std::vector<GaussResultRow>& AnalysisState::GaussResults()
|
||||
const noexcept {
|
||||
return gauss_results_;
|
||||
}
|
||||
|
||||
Vector& AnalysisState::reaction() noexcept {
|
||||
return reaction_;
|
||||
std::vector<StressS11Row>& AnalysisState::StressResults() noexcept {
|
||||
return stress_results_;
|
||||
}
|
||||
|
||||
const Vector& AnalysisState::reaction() const noexcept {
|
||||
return reaction_;
|
||||
const std::vector<StressS11Row>& AnalysisState::StressResults() const noexcept {
|
||||
return stress_results_;
|
||||
}
|
||||
|
||||
const StepFrameIdentity& AnalysisState::identity() const noexcept {
|
||||
return identity_;
|
||||
}
|
||||
|
||||
std::vector<EndpointResultRow>& AnalysisState::endpointResults() noexcept {
|
||||
return endpointResults_;
|
||||
}
|
||||
|
||||
const std::vector<EndpointResultRow>& AnalysisState::endpointResults() const noexcept {
|
||||
return endpointResults_;
|
||||
}
|
||||
|
||||
std::vector<GaussResultRow>& AnalysisState::gaussResults() noexcept {
|
||||
return gaussResults_;
|
||||
}
|
||||
|
||||
const std::vector<GaussResultRow>& AnalysisState::gaussResults() const noexcept {
|
||||
return gaussResults_;
|
||||
}
|
||||
|
||||
std::vector<StressS11Row>& AnalysisState::stressResults() noexcept {
|
||||
return stressResults_;
|
||||
}
|
||||
|
||||
const std::vector<StressS11Row>& AnalysisState::stressResults() const noexcept {
|
||||
return stressResults_;
|
||||
}
|
||||
|
||||
Status AnalysisState::commitShellResults(
|
||||
const std::vector<EntityIndex>& expectedElementOrder,
|
||||
Status AnalysisState::CommitShellResults(
|
||||
const std::vector<EntityIndex>& expected_element_order,
|
||||
ShellStateCandidate candidate) {
|
||||
if (expectedElementOrder.size() >
|
||||
(std::numeric_limits<std::size_t>::max)() /
|
||||
kShellLocationsPerElement) {
|
||||
return shellCandidateFailure(
|
||||
"invalid-shell-state-inventory",
|
||||
identity_.stepName,
|
||||
"The expected shell result inventory is too large.");
|
||||
}
|
||||
const std::size_t expectedRowCount =
|
||||
expectedElementOrder.size() * kShellLocationsPerElement;
|
||||
if (candidate.rows.size() != expectedRowCount) {
|
||||
return shellCandidateFailure(
|
||||
"invalid-shell-state-inventory",
|
||||
identity_.stepName,
|
||||
"Shell results require exactly four rows per expected element.");
|
||||
}
|
||||
if (std::adjacent_find(
|
||||
expectedElementOrder.begin(), expectedElementOrder.end(),
|
||||
[](const EntityIndex left, const EntityIndex right) {
|
||||
return left >= right;
|
||||
}) != expectedElementOrder.end()) {
|
||||
return shellCandidateFailure(
|
||||
"invalid-shell-state-inventory",
|
||||
identity_.stepName,
|
||||
"Expected shell elements must be unique and in stable index order.");
|
||||
}
|
||||
if (expected_element_order.size() >
|
||||
(std::numeric_limits<std::size_t>::max)() / kShellLocationsPerElement) {
|
||||
return ShellCandidateFailure(
|
||||
"invalid-shell-state-inventory", identity_.step_name,
|
||||
"The expected shell result inventory is too large.");
|
||||
}
|
||||
const std::size_t expected_row_count =
|
||||
expected_element_order.size() * kShellLocationsPerElement;
|
||||
if (candidate.rows.size() != expected_row_count) {
|
||||
return ShellCandidateFailure(
|
||||
"invalid-shell-state-inventory", identity_.step_name,
|
||||
"Shell results require exactly four rows per expected element.");
|
||||
}
|
||||
if (std::adjacent_find(expected_element_order.begin(),
|
||||
expected_element_order.end(),
|
||||
[](const EntityIndex left, const EntityIndex right) {
|
||||
return left >= right;
|
||||
}) != expected_element_order.end()) {
|
||||
return ShellCandidateFailure(
|
||||
"invalid-shell-state-inventory", identity_.step_name,
|
||||
"Expected shell elements must be unique and in stable index order.");
|
||||
}
|
||||
|
||||
const std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
|
||||
expectedLocations{
|
||||
ShellMidsurfaceLocation::gp1,
|
||||
ShellMidsurfaceLocation::gp2,
|
||||
ShellMidsurfaceLocation::gp3,
|
||||
ShellMidsurfaceLocation::gp4};
|
||||
const double gauss = 1.0 / std::sqrt(3.0);
|
||||
const std::array<std::array<double, 2>, kShellLocationsPerElement>
|
||||
expectedCoordinates{
|
||||
std::array<double, 2>{-gauss, -gauss},
|
||||
std::array<double, 2>{gauss, -gauss},
|
||||
std::array<double, 2>{gauss, gauss},
|
||||
std::array<double, 2>{-gauss, gauss}};
|
||||
const std::array<ShellSectionPosition, 3> expectedPositions{
|
||||
ShellSectionPosition::bottom,
|
||||
ShellSectionPosition::middle,
|
||||
ShellSectionPosition::top};
|
||||
constexpr std::array<double, 3> expectedZeta{-1.0, 0.0, 1.0};
|
||||
for (std::size_t elementOrder = 0U;
|
||||
elementOrder < expectedElementOrder.size();
|
||||
++elementOrder) {
|
||||
for (std::size_t point = 0U;
|
||||
point < kShellLocationsPerElement;
|
||||
++point) {
|
||||
const auto& row = candidate.rows[
|
||||
elementOrder * kShellLocationsPerElement + point];
|
||||
if (row.element != expectedElementOrder[elementOrder] ||
|
||||
row.location != expectedLocations[point] ||
|
||||
row.naturalCoordinates != expectedCoordinates[point]) {
|
||||
return shellCandidateFailure(
|
||||
"invalid-shell-state-inventory",
|
||||
std::to_string(row.element),
|
||||
"Shell rows must preserve element and GP1 through GP4 identity.");
|
||||
}
|
||||
for (std::size_t position = 0U;
|
||||
position < expectedPositions.size();
|
||||
++position) {
|
||||
if (row.stress[position].position !=
|
||||
expectedPositions[position] ||
|
||||
row.stress[position].zeta != expectedZeta[position]) {
|
||||
return shellCandidateFailure(
|
||||
"invalid-shell-state-inventory",
|
||||
std::to_string(row.element),
|
||||
"Shell stress rows require BOTTOM, MIDDLE, TOP identity.");
|
||||
}
|
||||
}
|
||||
if (!finite(row)) {
|
||||
return shellCandidateFailure(
|
||||
"nonfinite-shell-state-value",
|
||||
std::to_string(row.element),
|
||||
"Shell result rows must contain only finite values.");
|
||||
}
|
||||
const std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
|
||||
expected_locations{
|
||||
ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2,
|
||||
ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4};
|
||||
const double gauss = 1.0 / std::sqrt(3.0);
|
||||
const std::array<std::array<double, 2>, kShellLocationsPerElement>
|
||||
expected_coordinates{std::array<double, 2>{-gauss, -gauss},
|
||||
std::array<double, 2>{gauss, -gauss},
|
||||
std::array<double, 2>{gauss, gauss},
|
||||
std::array<double, 2>{-gauss, gauss}};
|
||||
const std::array<ShellSectionPosition, 3> expected_positions{
|
||||
ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle,
|
||||
ShellSectionPosition::kTop};
|
||||
constexpr std::array<double, 3> expected_zeta{-1.0, 0.0, 1.0};
|
||||
for (std::size_t element_order = 0U;
|
||||
element_order < expected_element_order.size(); ++element_order) {
|
||||
for (std::size_t point = 0U; point < kShellLocationsPerElement; ++point) {
|
||||
const auto& row =
|
||||
candidate.rows[element_order * kShellLocationsPerElement + point];
|
||||
if (row.element != expected_element_order[element_order] ||
|
||||
row.location != expected_locations[point] ||
|
||||
row.natural_coordinates != expected_coordinates[point]) {
|
||||
return ShellCandidateFailure(
|
||||
"invalid-shell-state-inventory", std::to_string(row.element),
|
||||
"Shell rows must preserve element and GP1 through GP4 identity.");
|
||||
}
|
||||
for (std::size_t position = 0U; position < expected_positions.size();
|
||||
++position) {
|
||||
if (row.stress[position].position != expected_positions[position] ||
|
||||
row.stress[position].zeta != expected_zeta[position]) {
|
||||
return ShellCandidateFailure(
|
||||
"invalid-shell-state-inventory", std::to_string(row.element),
|
||||
"Shell stress rows require BOTTOM, MIDDLE, TOP identity.");
|
||||
}
|
||||
}
|
||||
if (!IsFinite(row)) {
|
||||
return ShellCandidateFailure(
|
||||
"nonfinite-shell-state-value", std::to_string(row.element),
|
||||
"Shell result rows must contain only finite values.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::isfinite(candidate.physicalStrainEnergy) ||
|
||||
!finite(candidate.equilibrium) ||
|
||||
!finite(candidate.verificationMetrics)) {
|
||||
return shellCandidateFailure(
|
||||
"nonfinite-shell-state-value",
|
||||
identity_.stepName,
|
||||
"Shell energy, equilibrium, and normalized metrics must be finite.");
|
||||
}
|
||||
if (!std::isfinite(candidate.physical_strain_energy) ||
|
||||
!IsFinite(candidate.equilibrium) ||
|
||||
!IsFinite(candidate.verification_metrics)) {
|
||||
return ShellCandidateFailure(
|
||||
"nonfinite-shell-state-value", identity_.step_name,
|
||||
"Shell energy, equilibrium, and normalized metrics must be finite.");
|
||||
}
|
||||
|
||||
shellResults_ = std::move(candidate.rows);
|
||||
physicalStrainEnergy_ = candidate.physicalStrainEnergy;
|
||||
equilibrium_ = candidate.equilibrium;
|
||||
verificationMetrics_ = candidate.verificationMetrics;
|
||||
return Status::Ok();
|
||||
shell_results_ = std::move(candidate.rows);
|
||||
physical_strain_energy_ = candidate.physical_strain_energy;
|
||||
equilibrium_ = candidate.equilibrium;
|
||||
verification_metrics_ = candidate.verification_metrics;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
const std::vector<ShellResultRow>& AnalysisState::shellResults() const noexcept {
|
||||
return shellResults_;
|
||||
const std::vector<ShellResultRow>& AnalysisState::ShellResults()
|
||||
const noexcept {
|
||||
return shell_results_;
|
||||
}
|
||||
|
||||
double AnalysisState::physicalStrainEnergy() const noexcept {
|
||||
return physicalStrainEnergy_;
|
||||
double AnalysisState::PhysicalStrainEnergy() const noexcept {
|
||||
return physical_strain_energy_;
|
||||
}
|
||||
|
||||
const std::array<double, 6>& AnalysisState::equilibrium() const noexcept {
|
||||
return equilibrium_;
|
||||
const std::array<double, 6>& AnalysisState::Equilibrium() const noexcept {
|
||||
return equilibrium_;
|
||||
}
|
||||
|
||||
const std::array<double, 3>& AnalysisState::verificationMetrics() const noexcept {
|
||||
return verificationMetrics_;
|
||||
const std::array<double, 3>& AnalysisState::VerificationMetrics()
|
||||
const noexcept {
|
||||
return verification_metrics_;
|
||||
}
|
||||
|
||||
AnalysisState::AnalysisState(
|
||||
std::size_t fullDofCount, StepFrameIdentity identity)
|
||||
AnalysisState::AnalysisState(std::size_t full_dof_count,
|
||||
StepFrameIdentity identity)
|
||||
: identity_{std::move(identity)},
|
||||
displacement_{fullDofCount},
|
||||
externalForce_{fullDofCount},
|
||||
internalForce_{fullDofCount},
|
||||
residual_{fullDofCount},
|
||||
reaction_{fullDofCount} {}
|
||||
displacement_{full_dof_count},
|
||||
external_force_{full_dof_count},
|
||||
internal_force_{full_dof_count},
|
||||
residual_{full_dof_count},
|
||||
reaction_{full_dof_count} {}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
@@ -1,170 +1,166 @@
|
||||
#include "fesa/analysis/linear_static_analysis.hpp"
|
||||
|
||||
#include "fesa/assembly/load_assembler.hpp"
|
||||
#include "fesa/assembly/parallel_for.hpp"
|
||||
#include "fesa/assembly/sparse_assembler.hpp"
|
||||
#include "fesa/io/abaqus/domain_mapper.hpp"
|
||||
#include "fesa/io/abaqus/input_reader.hpp"
|
||||
#include "fesa/results/result_recovery.hpp"
|
||||
#include "fesa/results/results_writer.hpp"
|
||||
#include "fesa/solvers/linear/linear_solver.h"
|
||||
#include "fesa/analysis/linear_static_analysis.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/assembly/load_assembler.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/assembly/sparse_assembler.h"
|
||||
#include "fesa/io/abaqus/domain_mapper.hpp"
|
||||
#include "fesa/io/abaqus/input_reader.hpp"
|
||||
#include "fesa/results/result_recovery.h"
|
||||
#include "fesa/results/results_writer.h"
|
||||
#include "fesa/solvers/linear/linear_solver.h"
|
||||
|
||||
namespace fesa {
|
||||
|
||||
Status Analysis::run(const AnalysisRequest& request) {
|
||||
Status status = initialize(request);
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = buildAnalysisModel();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = buildDofMapAndSparsePattern();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = assembleAndPartitionStiffness();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = factorize();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = assembleLoadsAndEffectiveRhs();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = substituteAndReconstruct();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
return recoverAndWriteResults();
|
||||
Status Analysis::Run(const AnalysisRequest& request) {
|
||||
Status status = Initialize(request);
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = BuildAnalysisModel();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = BuildDofMapAndSparsePattern();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = AssembleAndPartitionStiffness();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = Factorize();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = AssembleLoadsAndEffectiveRhs();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
status = SubstituteAndReconstruct();
|
||||
if (!status.IsOk()) {
|
||||
return status;
|
||||
}
|
||||
return RecoverAndWriteResults();
|
||||
}
|
||||
|
||||
LinearStaticAnalysis::LinearStaticAnalysis(
|
||||
const ParallelFor& parallelFor,
|
||||
LinearSolver& linearSolver,
|
||||
ResultsWriter& resultsWriter)
|
||||
: parallelFor_{parallelFor},
|
||||
linearSolver_{linearSolver},
|
||||
resultsWriter_{resultsWriter} {}
|
||||
LinearStaticAnalysis::LinearStaticAnalysis(const ParallelFor& parallel_for,
|
||||
LinearSolver& linear_solver,
|
||||
ResultsWriter& results_writer)
|
||||
: parallel_for_{parallel_for},
|
||||
linear_solver_{linear_solver},
|
||||
results_writer_{results_writer} {}
|
||||
|
||||
Status LinearStaticAnalysis::initialize(const AnalysisRequest& request) {
|
||||
// Clear dependent objects in reverse ownership order so a reused analysis
|
||||
// never exposes a view into a Domain from an earlier run.
|
||||
effectiveRhs_.reset();
|
||||
partitionedStiffness_.reset();
|
||||
fullStiffness_.reset();
|
||||
state_.reset();
|
||||
dofs_.reset();
|
||||
model_.reset();
|
||||
domain_.reset();
|
||||
diagnostics_.clear();
|
||||
request_ = request;
|
||||
Status LinearStaticAnalysis::Initialize(const AnalysisRequest& request) {
|
||||
// Clear dependent objects in reverse ownership order so a reused analysis
|
||||
// never exposes a view into a Domain from an earlier run.
|
||||
effective_rhs_.reset();
|
||||
partitioned_stiffness_.reset();
|
||||
full_stiffness_.reset();
|
||||
state_.reset();
|
||||
dofs_.reset();
|
||||
model_.reset();
|
||||
domain_.reset();
|
||||
diagnostics_.clear();
|
||||
request_ = request;
|
||||
|
||||
const auto parsed = AbaqusInputReader{}.read(request_.inputPath);
|
||||
if (!parsed.HasValue()) {
|
||||
return parsed.GetStatus();
|
||||
}
|
||||
auto domain = AbaqusDomainMapper{}.map(parsed.Value());
|
||||
if (!domain.HasValue()) {
|
||||
return domain.GetStatus();
|
||||
}
|
||||
const auto parsed = AbaqusInputReader{}.read(request_.input_path);
|
||||
if (!parsed.HasValue()) {
|
||||
return parsed.GetStatus();
|
||||
}
|
||||
auto domain = AbaqusDomainMapper{}.map(parsed.Value());
|
||||
if (!domain.HasValue()) {
|
||||
return domain.GetStatus();
|
||||
}
|
||||
|
||||
domain_ = std::make_unique<Domain>(std::move(domain.Value()));
|
||||
diagnostics_ = domain_->Warnings();
|
||||
SortDiagnostics(diagnostics_);
|
||||
return Status::Ok();
|
||||
domain_ = std::make_unique<Domain>(std::move(domain.Value()));
|
||||
diagnostics_ = domain_->Warnings();
|
||||
SortDiagnostics(diagnostics_);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status LinearStaticAnalysis::buildAnalysisModel() {
|
||||
auto model = AnalysisModel::create(*domain_);
|
||||
if (!model.HasValue()) {
|
||||
return model.GetStatus();
|
||||
}
|
||||
model_ = std::make_unique<AnalysisModel>(std::move(model.Value()));
|
||||
return Status::Ok();
|
||||
Status LinearStaticAnalysis::BuildAnalysisModel() {
|
||||
auto model = AnalysisModel::Create(*domain_);
|
||||
if (!model.HasValue()) {
|
||||
return model.GetStatus();
|
||||
}
|
||||
model_ = std::make_unique<AnalysisModel>(std::move(model.Value()));
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status LinearStaticAnalysis::buildDofMapAndSparsePattern() {
|
||||
auto dofs = DofManager::create(*model_);
|
||||
if (!dofs.HasValue()) {
|
||||
return dofs.GetStatus();
|
||||
}
|
||||
dofs_ = std::make_unique<DofManager>(std::move(dofs.Value()));
|
||||
state_ = std::make_unique<AnalysisState>(
|
||||
AnalysisState::create(*dofs_, {"Step-1", 0U}));
|
||||
return Status::Ok();
|
||||
Status LinearStaticAnalysis::BuildDofMapAndSparsePattern() {
|
||||
auto dofs = DofManager::Create(*model_);
|
||||
if (!dofs.HasValue()) {
|
||||
return dofs.GetStatus();
|
||||
}
|
||||
dofs_ = std::make_unique<DofManager>(std::move(dofs.Value()));
|
||||
state_ = std::make_unique<AnalysisState>(
|
||||
AnalysisState::Create(*dofs_, {"Step-1", 0U}));
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status LinearStaticAnalysis::assembleAndPartitionStiffness() {
|
||||
auto stiffness = SparseAssembler::assembleStiffness(
|
||||
*model_, *dofs_, parallelFor_);
|
||||
if (!stiffness.HasValue()) {
|
||||
return stiffness.GetStatus();
|
||||
}
|
||||
fullStiffness_ =
|
||||
std::make_unique<SparseMatrix>(std::move(stiffness.Value()));
|
||||
Status LinearStaticAnalysis::AssembleAndPartitionStiffness() {
|
||||
auto stiffness =
|
||||
SparseAssembler::AssembleStiffness(*model_, *dofs_, parallel_for_);
|
||||
if (!stiffness.HasValue()) {
|
||||
return stiffness.GetStatus();
|
||||
}
|
||||
full_stiffness_ =
|
||||
std::make_unique<SparseMatrix>(std::move(stiffness.Value()));
|
||||
|
||||
auto partitioned = EssentialConstraints::partition(
|
||||
*fullStiffness_, *dofs_);
|
||||
if (!partitioned.HasValue()) {
|
||||
return partitioned.GetStatus();
|
||||
}
|
||||
partitionedStiffness_ = std::make_unique<PartitionedStiffness>(
|
||||
std::move(partitioned.Value()));
|
||||
return Status::Ok();
|
||||
auto partitioned = EssentialConstraints::Partition(*full_stiffness_, *dofs_);
|
||||
if (!partitioned.HasValue()) {
|
||||
return partitioned.GetStatus();
|
||||
}
|
||||
partitioned_stiffness_ =
|
||||
std::make_unique<PartitionedStiffness>(std::move(partitioned.Value()));
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status LinearStaticAnalysis::factorize() {
|
||||
// This call intentionally precedes all load assembly in Analysis::run.
|
||||
return linearSolver_.Factorize(partitionedStiffness_->kff);
|
||||
Status LinearStaticAnalysis::Factorize() {
|
||||
// This call intentionally precedes all load assembly in Analysis::Run.
|
||||
return linear_solver_.Factorize(partitioned_stiffness_->kff);
|
||||
}
|
||||
|
||||
Status LinearStaticAnalysis::assembleLoadsAndEffectiveRhs() {
|
||||
auto fullLoad = LoadAssembler::assembleFullNodalLoad(*model_, *dofs_);
|
||||
if (!fullLoad.HasValue()) {
|
||||
return fullLoad.GetStatus();
|
||||
}
|
||||
state_->externalForce() = std::move(fullLoad.Value());
|
||||
Status LinearStaticAnalysis::AssembleLoadsAndEffectiveRhs() {
|
||||
auto full_load = LoadAssembler::AssembleFullNodalLoad(*model_, *dofs_);
|
||||
if (!full_load.HasValue()) {
|
||||
return full_load.GetStatus();
|
||||
}
|
||||
state_->ExternalForce() = std::move(full_load.Value());
|
||||
|
||||
auto rhs = LoadAssembler::effectiveFreeRhs(
|
||||
state_->externalForce(),
|
||||
partitionedStiffness_->kfc,
|
||||
dofs_->prescribedValues(),
|
||||
*dofs_);
|
||||
if (!rhs.HasValue()) {
|
||||
return rhs.GetStatus();
|
||||
}
|
||||
effectiveRhs_ = std::make_unique<Vector>(std::move(rhs.Value()));
|
||||
return Status::Ok();
|
||||
auto rhs = LoadAssembler::EffectiveFreeRhs(state_->ExternalForce(),
|
||||
partitioned_stiffness_->kfc,
|
||||
dofs_->PrescribedValues(), *dofs_);
|
||||
if (!rhs.HasValue()) {
|
||||
return rhs.GetStatus();
|
||||
}
|
||||
effective_rhs_ = std::make_unique<Vector>(std::move(rhs.Value()));
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status LinearStaticAnalysis::substituteAndReconstruct() {
|
||||
Vector freeDisplacement{dofs_->freeDofCount()};
|
||||
const Status solveStatus =
|
||||
linearSolver_.Solve(*effectiveRhs_, freeDisplacement);
|
||||
if (!solveStatus.IsOk()) {
|
||||
return solveStatus;
|
||||
}
|
||||
Status LinearStaticAnalysis::SubstituteAndReconstruct() {
|
||||
Vector free_displacement{dofs_->FreeDofCount()};
|
||||
const Status solve_status =
|
||||
linear_solver_.Solve(*effective_rhs_, free_displacement);
|
||||
if (!solve_status.IsOk()) {
|
||||
return solve_status;
|
||||
}
|
||||
|
||||
state_->displacement() = EssentialConstraints::reconstructFull(
|
||||
freeDisplacement, dofs_->prescribedValues(), *dofs_);
|
||||
return Status::Ok();
|
||||
state_->Displacement() = EssentialConstraints::ReconstructFull(
|
||||
free_displacement, dofs_->PrescribedValues(), *dofs_);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status LinearStaticAnalysis::recoverAndWriteResults() {
|
||||
const Status recoveryStatus = ResultRecovery::recover(
|
||||
*model_, *dofs_, *fullStiffness_, *state_);
|
||||
if (!recoveryStatus.IsOk()) {
|
||||
return recoveryStatus;
|
||||
}
|
||||
return resultsWriter_.write(
|
||||
request_.outputPath, *domain_, *state_, diagnostics_);
|
||||
Status LinearStaticAnalysis::RecoverAndWriteResults() {
|
||||
const Status recovery_status =
|
||||
ResultRecovery::Recover(*model_, *dofs_, *full_stiffness_, *state_);
|
||||
if (!recovery_status.IsOk()) {
|
||||
return recovery_status;
|
||||
}
|
||||
return results_writer_.Write(request_.output_path, *domain_, *state_,
|
||||
diagnostics_);
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "fesa/app/fesa_application.hpp"
|
||||
|
||||
#include "fesa/analysis/linear_static_analysis.hpp"
|
||||
#include "fesa/assembly/parallel_for.hpp"
|
||||
#include "fesa/analysis/linear_static_analysis.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/core/diagnostic.h"
|
||||
#include "fesa/io/hdf5/hdf5_results_writer.hpp"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
@@ -91,8 +91,8 @@ int FesaApplication::run(const std::vector<std::string>& arguments) {
|
||||
}
|
||||
|
||||
AnalysisRequest request;
|
||||
request.inputPath = arguments[0U];
|
||||
request.outputPath = explicitOutputForm
|
||||
request.input_path = arguments[0U];
|
||||
request.output_path = explicitOutputForm
|
||||
? std::filesystem::path{arguments[2U]}
|
||||
: std::filesystem::current_path() / "results.h5";
|
||||
|
||||
@@ -101,7 +101,7 @@ int FesaApplication::run(const std::vector<std::string>& arguments) {
|
||||
Hdf5ResultsWriter resultsWriter;
|
||||
LinearStaticAnalysis analysis{
|
||||
parallelFor, linearSolver, resultsWriter};
|
||||
const Status status = analysis.run(request);
|
||||
const Status status = analysis.Run(request);
|
||||
if (status.IsOk()) {
|
||||
return kSuccessExitCode;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#include "fesa/assembly/load_assembler.hpp"
|
||||
|
||||
#include "fesa/constraints/essential_constraints.hpp"
|
||||
#include "fesa/assembly/load_assembler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
@@ -13,475 +11,400 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/constraints/essential_constraints.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t dofsPerNode = 6U;
|
||||
constexpr double shellMomentProjectionTolerance = 1.0e-12;
|
||||
constexpr std::size_t kDofsPerNode = 6U;
|
||||
constexpr double kShellMomentProjectionTolerance = 1.0e-12;
|
||||
|
||||
Status loadFailure(
|
||||
const std::string& code,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword,
|
||||
const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kModel,
|
||||
{{Severity::kError, code, location, keyword, identity, message}});
|
||||
Status LoadFailure(const std::string& code, const SourceLocation& location,
|
||||
const std::string& keyword, const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kModel,
|
||||
{{Severity::kError, code, location, keyword, identity, message}});
|
||||
}
|
||||
|
||||
char asciiLower(const char value) {
|
||||
if (value >= 'A' && value <= 'Z') {
|
||||
return static_cast<char>(value + ('a' - 'A'));
|
||||
}
|
||||
return value;
|
||||
char AsciiLower(const char value) {
|
||||
if (value >= 'A' && value <= 'Z') {
|
||||
return static_cast<char>(value + ('a' - 'A'));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool equalName(const std::string& left, const std::string& right) {
|
||||
return left.size() == right.size() &&
|
||||
std::equal(
|
||||
left.begin(),
|
||||
left.end(),
|
||||
right.begin(),
|
||||
[](const char leftValue, const char rightValue) {
|
||||
return asciiLower(leftValue) == asciiLower(rightValue);
|
||||
});
|
||||
bool EqualName(const std::string& left, const std::string& right) {
|
||||
return left.size() == right.size() &&
|
||||
std::equal(left.begin(), left.end(), right.begin(),
|
||||
[](const char left_value, const char right_value) {
|
||||
return AsciiLower(left_value) == AsciiLower(right_value);
|
||||
});
|
||||
}
|
||||
|
||||
bool tryPositiveInteger(const std::string& text, std::int64_t& value) {
|
||||
const char* const first = text.data();
|
||||
const char* const last = first + text.size();
|
||||
const auto parsed = std::from_chars(first, last, value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
|
||||
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
|
||||
const char* const first = text.data();
|
||||
const char* const last = first + text.size();
|
||||
const auto parsed = std::from_chars(first, last, value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
|
||||
}
|
||||
|
||||
bool isStrictlyIncreasing(const std::vector<std::size_t>& values) {
|
||||
return std::adjacent_find(
|
||||
values.begin(),
|
||||
values.end(),
|
||||
[](const std::size_t left, const std::size_t right) {
|
||||
return left >= right;
|
||||
}) == values.end();
|
||||
/// @brief Checks that equation-space indices preserve stable full-DOF order.
|
||||
bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) {
|
||||
return std::adjacent_find(
|
||||
values.begin(), values.end(),
|
||||
[](const std::size_t left, const std::size_t right) {
|
||||
return left >= right;
|
||||
}) == values.end();
|
||||
}
|
||||
|
||||
Status validateDofOrder(
|
||||
const DofManager& dofs,
|
||||
const std::size_t expectedFullCount,
|
||||
const SourceLocation& location) {
|
||||
const std::size_t fullCount = dofs.fullDofCount();
|
||||
const auto& freeDofs = dofs.freeDofs();
|
||||
const auto& constrainedDofs = dofs.constrainedDofs();
|
||||
if (fullCount != expectedFullCount ||
|
||||
freeDofs.size() != dofs.freeDofCount() ||
|
||||
constrainedDofs.size() != dofs.constrainedDofCount() ||
|
||||
dofs.prescribedValues().Size() != constrainedDofs.size() ||
|
||||
constrainedDofs.size() > fullCount ||
|
||||
freeDofs.size() != fullCount - constrainedDofs.size()) {
|
||||
return loadFailure(
|
||||
"invalid-load-dimensions",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(fullCount),
|
||||
"Full, free, constrained, prescribed, and model dimensions must agree.");
|
||||
}
|
||||
if (!isStrictlyIncreasing(freeDofs) ||
|
||||
!isStrictlyIncreasing(constrainedDofs)) {
|
||||
return loadFailure(
|
||||
"invalid-load-order",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(fullCount),
|
||||
"Free and constrained DOFs must use stable increasing full-DOF order.");
|
||||
}
|
||||
/// @brief Validates the full/free/constrained partition used by load assembly.
|
||||
Status ValidateDofOrder(const DofManager& dofs,
|
||||
const std::size_t expected_full_count,
|
||||
const SourceLocation& location) {
|
||||
const std::size_t full_count = dofs.FullDofCount();
|
||||
const auto& free_dofs = dofs.FreeDofs();
|
||||
const auto& constrained_dofs = dofs.ConstrainedDofs();
|
||||
if (full_count != expected_full_count ||
|
||||
free_dofs.size() != dofs.FreeDofCount() ||
|
||||
constrained_dofs.size() != dofs.ConstrainedDofCount() ||
|
||||
dofs.PrescribedValues().Size() != constrained_dofs.size() ||
|
||||
constrained_dofs.size() > full_count ||
|
||||
free_dofs.size() != full_count - constrained_dofs.size()) {
|
||||
return LoadFailure("invalid-load-dimensions", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(full_count),
|
||||
"Full, free, constrained, prescribed, and model "
|
||||
"dimensions must agree.");
|
||||
}
|
||||
if (!IsStrictlyIncreasing(free_dofs) ||
|
||||
!IsStrictlyIncreasing(constrained_dofs)) {
|
||||
return LoadFailure(
|
||||
"invalid-load-order", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(full_count),
|
||||
"Free and constrained DOFs must use stable increasing full-DOF order.");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> ownership(fullCount, 0U);
|
||||
try {
|
||||
for (std::size_t equation = 0U;
|
||||
equation < freeDofs.size();
|
||||
++equation) {
|
||||
const std::size_t fullDof = freeDofs[equation];
|
||||
if (fullDof >= fullCount || ownership[fullDof] != 0U ||
|
||||
dofs.freeEquation(fullDof) != equation) {
|
||||
return loadFailure(
|
||||
"invalid-load-order",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(fullDof),
|
||||
"Free equation numbering must match stable full-DOF order.");
|
||||
}
|
||||
ownership[fullDof] = 1U;
|
||||
}
|
||||
for (const std::size_t fullDof : constrainedDofs) {
|
||||
if (fullDof >= fullCount || ownership[fullDof] != 0U ||
|
||||
dofs.freeEquation(fullDof).has_value()) {
|
||||
return loadFailure(
|
||||
"invalid-load-order",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(fullDof),
|
||||
"Constrained DOFs must be unique and absent from free equations.");
|
||||
}
|
||||
ownership[fullDof] = 2U;
|
||||
}
|
||||
} catch (const std::out_of_range&) {
|
||||
return loadFailure(
|
||||
"invalid-load-dimensions",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(fullCount),
|
||||
"DofManager equation storage must cover every full DOF.");
|
||||
std::vector<unsigned char> ownership(full_count, 0U);
|
||||
try {
|
||||
for (std::size_t equation = 0U; equation < free_dofs.size(); ++equation) {
|
||||
const std::size_t full_dof = free_dofs[equation];
|
||||
if (full_dof >= full_count || ownership[full_dof] != 0U ||
|
||||
dofs.FreeEquation(full_dof) != equation) {
|
||||
return LoadFailure(
|
||||
"invalid-load-order", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(full_dof),
|
||||
"Free equation numbering must match stable full-DOF order.");
|
||||
}
|
||||
ownership[full_dof] = 1U;
|
||||
}
|
||||
if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) {
|
||||
return loadFailure(
|
||||
"invalid-load-order",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(fullCount),
|
||||
"Free and constrained DOFs must partition the full range.");
|
||||
for (const std::size_t full_dof : constrained_dofs) {
|
||||
if (full_dof >= full_count || ownership[full_dof] != 0U ||
|
||||
dofs.FreeEquation(full_dof).has_value()) {
|
||||
return LoadFailure(
|
||||
"invalid-load-order", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(full_dof),
|
||||
"Constrained DOFs must be unique and absent from free equations.");
|
||||
}
|
||||
ownership[full_dof] = 2U;
|
||||
}
|
||||
} catch (const std::out_of_range&) {
|
||||
return LoadFailure(
|
||||
"invalid-load-dimensions", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(full_count),
|
||||
"DofManager equation storage must cover every full DOF.");
|
||||
}
|
||||
if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) {
|
||||
return LoadFailure(
|
||||
"invalid-load-order", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(full_count),
|
||||
"Free and constrained DOFs must partition the full range.");
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<std::vector<EntityIndex>> ResolveTarget(const Domain& domain,
|
||||
const NodalLoad& load) {
|
||||
std::vector<const NodeSet*> matching_sets;
|
||||
for (const auto& set : domain.NodeSets()) {
|
||||
if (EqualName(set.name, load.target)) {
|
||||
matching_sets.push_back(&set);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EntityIndex> matching_nodes;
|
||||
std::int64_t label = 0;
|
||||
if (TryPositiveInteger(load.target, label)) {
|
||||
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
|
||||
if (domain.Nodes()[index].source_id.source_label == label) {
|
||||
matching_nodes.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matching_sets.size() > 1U || matching_nodes.size() > 1U ||
|
||||
(!matching_sets.empty() && !matching_nodes.empty())) {
|
||||
return Result<std::vector<EntityIndex>>::Failure(
|
||||
LoadFailure("invalid-load-target", load.location, "CLOAD", load.target,
|
||||
"The load target must resolve unambiguously to one node or "
|
||||
"one expanded node set."));
|
||||
}
|
||||
if (!matching_sets.empty()) {
|
||||
const auto& nodes = matching_sets.front()->node_indices;
|
||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||
for (const EntityIndex node : nodes) {
|
||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
|
||||
"invalid-load-target", load.location, "CLOAD", load.target,
|
||||
"The expanded node set must contain unique in-range stable node "
|
||||
"identities."));
|
||||
}
|
||||
seen[node] = 1U;
|
||||
}
|
||||
return Result<std::vector<EntityIndex>>::Success(nodes);
|
||||
}
|
||||
if (!matching_nodes.empty()) {
|
||||
return Result<std::vector<EntityIndex>>::Success(std::move(matching_nodes));
|
||||
}
|
||||
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
|
||||
"invalid-load-target", load.location, "CLOAD", load.target,
|
||||
"The load target must resolve to one semantic node or node set."));
|
||||
}
|
||||
|
||||
Status ValidateFiniteVector(const Vector& values,
|
||||
const SourceLocation& location,
|
||||
const std::string& identity) {
|
||||
for (std::size_t index = 0U; index < values.Size(); ++index) {
|
||||
if (!std::isfinite(values[index])) {
|
||||
return LoadFailure("nonfinite-load-value", location, "LOAD_ASSEMBLER",
|
||||
identity + ":" + std::to_string(index),
|
||||
"Load and prescribed displacement vectors must "
|
||||
"contain finite values.");
|
||||
}
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status ValidateShellMoments(const Domain& domain, const Vector& full_load) {
|
||||
if (domain.ShellElements().empty()) {
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
std::vector<const ShellNodeInitialFrame*> frame_by_node(domain.Nodes().size(),
|
||||
nullptr);
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
if (frame.node_index >= frame_by_node.size() ||
|
||||
frame_by_node[frame.node_index] != nullptr) {
|
||||
return LoadFailure(
|
||||
"invalid-shell-director", {domain.SourcePath(), 0U}, "NODE",
|
||||
std::to_string(frame.node_index),
|
||||
"Shell nodal directors must have unique in-range node identities.");
|
||||
}
|
||||
frame_by_node[frame.node_index] = &frame;
|
||||
}
|
||||
|
||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||
const double moment_x = full_load[node * kDofsPerNode + 3U];
|
||||
const double moment_y = full_load[node * kDofsPerNode + 4U];
|
||||
const double moment_z = full_load[node * kDofsPerNode + 5U];
|
||||
if (moment_x == 0.0 && moment_y == 0.0 && moment_z == 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* const frame = frame_by_node[node];
|
||||
if (frame == nullptr) {
|
||||
return LoadFailure(
|
||||
"invalid-shell-director", domain.Nodes()[node].location, "NODE",
|
||||
domain.Nodes()[node].source_id.source_label_text,
|
||||
"A loaded shell node must have an approved initial director.");
|
||||
}
|
||||
|
||||
const double moment_scale = std::max(
|
||||
std::abs(moment_x), std::max(std::abs(moment_y), std::abs(moment_z)));
|
||||
const double scaled_x = moment_x / moment_scale;
|
||||
const double scaled_y = moment_y / moment_scale;
|
||||
const double scaled_z = moment_z / moment_scale;
|
||||
const double scaled_norm = std::hypot(scaled_x, scaled_y, scaled_z);
|
||||
const double scaled_dot = frame->director[0U] * scaled_x +
|
||||
frame->director[1U] * scaled_y +
|
||||
frame->director[2U] * scaled_z;
|
||||
const double projection_ratio = std::abs(scaled_dot) / scaled_norm;
|
||||
if (!(projection_ratio <= kShellMomentProjectionTolerance)) {
|
||||
return LoadFailure("unsupported-drilling-load",
|
||||
domain.Nodes()[node].location, "CLOAD",
|
||||
domain.Nodes()[node].source_id.source_label_text,
|
||||
"The aggregate nodal moment has an unsupported "
|
||||
"director-parallel component.");
|
||||
}
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<std::vector<EntityIndex>> resolveTarget(
|
||||
const Domain& domain,
|
||||
const NodalLoad& load) {
|
||||
std::vector<const NodeSet*> matchingSets;
|
||||
for (const auto& set : domain.NodeSets()) {
|
||||
if (equalName(set.name, load.target)) {
|
||||
matchingSets.push_back(&set);
|
||||
} // namespace
|
||||
|
||||
Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
|
||||
const DofManager& dofs) {
|
||||
const Domain& domain = model.GetDomain();
|
||||
if (domain.Nodes().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-dimensions", {domain.SourcePath(), 0U}, "LOAD_ASSEMBLER",
|
||||
domain.SourceContentIdentity(),
|
||||
"The semantic node count cannot be represented in full-DOF order."));
|
||||
}
|
||||
const std::size_t expected_full_count = domain.Nodes().size() * kDofsPerNode;
|
||||
const Status dof_status =
|
||||
ValidateDofOrder(dofs, expected_full_count, {domain.SourcePath(), 0U});
|
||||
if (!dof_status.IsOk()) {
|
||||
return Result<Vector>::Failure(dof_status);
|
||||
}
|
||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||
for (std::size_t component = 0U; component < kDofsPerNode; ++component) {
|
||||
try {
|
||||
if (dofs.FullDof(static_cast<EntityIndex>(node),
|
||||
static_cast<DofComponent>(component)) !=
|
||||
node * kDofsPerNode + component) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-order", domain.Nodes()[node].location,
|
||||
"LOAD_ASSEMBLER",
|
||||
domain.Nodes()[node].source_id.source_label_text,
|
||||
"DofManager node/component identity must match full-DOF order."));
|
||||
}
|
||||
} catch (const std::out_of_range&) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-dimensions", domain.Nodes()[node].location,
|
||||
"LOAD_ASSEMBLER", domain.Nodes()[node].source_id.source_label_text,
|
||||
"DofManager must provide all six DOFs for every semantic node."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto& active_loads = model.ActiveLoads();
|
||||
const auto& loads = model.Step().loads;
|
||||
if (active_loads.size() != loads.size()) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-order", model.Step().location, "CLOAD", model.Step().name,
|
||||
"The active load view must include every sole-step load once."));
|
||||
}
|
||||
|
||||
Vector full_load{expected_full_count};
|
||||
// Active load indices are required to be the original source order; this
|
||||
// loop is therefore also the fixed floating-point accumulation order.
|
||||
for (std::size_t source_order = 0U; source_order < active_loads.size();
|
||||
++source_order) {
|
||||
const EntityIndex load_index = active_loads[source_order];
|
||||
if (static_cast<std::size_t>(load_index) != source_order ||
|
||||
load_index >= loads.size()) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-order", model.Step().location, "CLOAD",
|
||||
std::to_string(source_order),
|
||||
"Active loads must retain complete stable source order."));
|
||||
}
|
||||
const auto& load = loads[load_index];
|
||||
if (load.dof < 1 || load.dof > static_cast<int>(kDofsPerNode)) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-dof", load.location, "CLOAD", load.target,
|
||||
"A nodal load component must be in the range 1 through 6."));
|
||||
}
|
||||
if (!std::isfinite(load.magnitude)) {
|
||||
return Result<Vector>::Failure(
|
||||
LoadFailure("nonfinite-load-value", load.location, "CLOAD",
|
||||
load.target, "A nodal load magnitude must be finite."));
|
||||
}
|
||||
|
||||
std::vector<EntityIndex> matchingNodes;
|
||||
std::int64_t label = 0;
|
||||
if (tryPositiveInteger(load.target, label)) {
|
||||
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
|
||||
if (domain.Nodes()[index].source_id.source_label == label) {
|
||||
matchingNodes.push_back(static_cast<EntityIndex>(index));
|
||||
}
|
||||
}
|
||||
auto target = ResolveTarget(domain, load);
|
||||
if (!target.HasValue()) {
|
||||
return Result<Vector>::Failure(target.GetStatus());
|
||||
}
|
||||
|
||||
if (matchingSets.size() > 1U || matchingNodes.size() > 1U ||
|
||||
(!matchingSets.empty() && !matchingNodes.empty())) {
|
||||
return Result<std::vector<EntityIndex>>::Failure(loadFailure(
|
||||
"invalid-load-target",
|
||||
load.location,
|
||||
"CLOAD",
|
||||
load.target,
|
||||
"The load target must resolve unambiguously to one node or one expanded node set."));
|
||||
const auto component = static_cast<DofComponent>(load.dof - 1);
|
||||
for (const EntityIndex node : target.Value()) {
|
||||
const std::size_t full_dof = dofs.FullDof(node, component);
|
||||
const double accumulated = full_load[full_dof] + load.magnitude;
|
||||
if (!std::isfinite(accumulated)) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"nonfinite-load-accumulation", load.location, "CLOAD", load.target,
|
||||
"Source-order load accumulation produced a nonfinite value."));
|
||||
}
|
||||
full_load[full_dof] = accumulated;
|
||||
}
|
||||
if (!matchingSets.empty()) {
|
||||
const auto& nodes = matchingSets.front()->node_indices;
|
||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||
for (const EntityIndex node : nodes) {
|
||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||
return Result<std::vector<EntityIndex>>::Failure(loadFailure(
|
||||
"invalid-load-target",
|
||||
load.location,
|
||||
"CLOAD",
|
||||
load.target,
|
||||
"The expanded node set must contain unique in-range stable node identities."));
|
||||
}
|
||||
seen[node] = 1U;
|
||||
}
|
||||
return Result<std::vector<EntityIndex>>::Success(nodes);
|
||||
}
|
||||
if (!matchingNodes.empty()) {
|
||||
return Result<std::vector<EntityIndex>>::Success(
|
||||
std::move(matchingNodes));
|
||||
}
|
||||
return Result<std::vector<EntityIndex>>::Failure(loadFailure(
|
||||
"invalid-load-target",
|
||||
load.location,
|
||||
"CLOAD",
|
||||
load.target,
|
||||
"The load target must resolve to one semantic node or node set."));
|
||||
}
|
||||
const Status shell_moment_status = ValidateShellMoments(domain, full_load);
|
||||
if (!shell_moment_status.IsOk()) {
|
||||
return Result<Vector>::Failure(shell_moment_status);
|
||||
}
|
||||
return Result<Vector>::Success(std::move(full_load));
|
||||
}
|
||||
|
||||
Status validateFiniteVector(
|
||||
const Vector& values,
|
||||
const SourceLocation& location,
|
||||
const std::string& identity) {
|
||||
for (std::size_t index = 0U; index < values.Size(); ++index) {
|
||||
if (!std::isfinite(values[index])) {
|
||||
return loadFailure(
|
||||
"nonfinite-load-value",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
identity + ":" + std::to_string(index),
|
||||
"Load and prescribed displacement vectors must contain finite values.");
|
||||
}
|
||||
Result<Vector> LoadAssembler::EffectiveFreeRhs(const Vector& full_load,
|
||||
const SparseMatrix& kfc,
|
||||
const Vector& prescribed_values,
|
||||
const DofManager& dofs) {
|
||||
const SourceLocation location{{}, 0U};
|
||||
const Status dof_status = ValidateDofOrder(dofs, full_load.Size(), location);
|
||||
if (!dof_status.IsOk()) {
|
||||
return Result<Vector>::Failure(dof_status);
|
||||
}
|
||||
if (kfc.Rows() != dofs.FreeDofCount() ||
|
||||
kfc.Columns() != dofs.ConstrainedDofCount() ||
|
||||
prescribed_values.Size() != dofs.ConstrainedDofCount()) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-dimensions", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(kfc.Rows()) + "x" + std::to_string(kfc.Columns()),
|
||||
"Kfc rows/columns and prescribed values must match free/constrained "
|
||||
"order."));
|
||||
}
|
||||
const Status matrix_status = kfc.Validate();
|
||||
if (!matrix_status.IsOk()) {
|
||||
return Result<Vector>::Failure(matrix_status);
|
||||
}
|
||||
const Status load_status =
|
||||
ValidateFiniteVector(full_load, location, "full-load");
|
||||
if (!load_status.IsOk()) {
|
||||
return Result<Vector>::Failure(load_status);
|
||||
}
|
||||
const Status prescribed_status =
|
||||
ValidateFiniteVector(prescribed_values, location, "prescribed-values");
|
||||
if (!prescribed_status.IsOk()) {
|
||||
return Result<Vector>::Failure(prescribed_status);
|
||||
}
|
||||
|
||||
Vector correction{kfc.Rows()};
|
||||
for (std::size_t row = 0U; row < kfc.Rows(); ++row) {
|
||||
double sum = 0.0;
|
||||
for (std::size_t position = kfc.RowOffsets()[row];
|
||||
position < kfc.RowOffsets()[row + 1U]; ++position) {
|
||||
const double product = kfc.Values()[position] *
|
||||
prescribed_values[kfc.ColumnIndices()[position]];
|
||||
if (!std::isfinite(product)) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"nonfinite-load-accumulation", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(row),
|
||||
"Kfc times prescribed displacement produced a nonfinite product."));
|
||||
}
|
||||
sum += product;
|
||||
if (!std::isfinite(sum)) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"nonfinite-load-accumulation", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(row),
|
||||
"Kfc times prescribed displacement produced a nonfinite row sum."));
|
||||
}
|
||||
}
|
||||
return Status::Ok();
|
||||
correction[row] = sum;
|
||||
}
|
||||
|
||||
Vector rhs = EssentialConstraints::GatherFree(full_load, dofs);
|
||||
// The constrained vector is already in DofManager order, so this is the
|
||||
// approved elimination equation rhs = Ff - Kfc*dc without reordering dc.
|
||||
for (std::size_t row = 0U; row < rhs.Size(); ++row) {
|
||||
const double value = rhs[row] - correction[row];
|
||||
if (!std::isfinite(value)) {
|
||||
return Result<Vector>::Failure(
|
||||
LoadFailure("nonfinite-load-accumulation", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(row),
|
||||
"Effective RHS subtraction produced a nonfinite value."));
|
||||
}
|
||||
rhs[row] = value;
|
||||
}
|
||||
return Result<Vector>::Success(std::move(rhs));
|
||||
}
|
||||
|
||||
Status validateShellMoments(
|
||||
const Domain& domain,
|
||||
const Vector& fullLoad) {
|
||||
if (domain.ShellElements().empty()) {
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
std::vector<const ShellNodeInitialFrame*> frameByNode(
|
||||
domain.Nodes().size(), nullptr);
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
if (frame.node_index >= frameByNode.size() ||
|
||||
frameByNode[frame.node_index] != nullptr) {
|
||||
return loadFailure(
|
||||
"invalid-shell-director",
|
||||
{domain.SourcePath(), 0U},
|
||||
"NODE",
|
||||
std::to_string(frame.node_index),
|
||||
"Shell nodal directors must have unique in-range node identities.");
|
||||
}
|
||||
frameByNode[frame.node_index] = &frame;
|
||||
}
|
||||
|
||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||
const double momentX = fullLoad[node * dofsPerNode + 3U];
|
||||
const double momentY = fullLoad[node * dofsPerNode + 4U];
|
||||
const double momentZ = fullLoad[node * dofsPerNode + 5U];
|
||||
if (momentX == 0.0 && momentY == 0.0 && momentZ == 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* const frame = frameByNode[node];
|
||||
if (frame == nullptr) {
|
||||
return loadFailure(
|
||||
"invalid-shell-director",
|
||||
domain.Nodes()[node].location,
|
||||
"NODE",
|
||||
domain.Nodes()[node].source_id.source_label_text,
|
||||
"A loaded shell node must have an approved initial director.");
|
||||
}
|
||||
|
||||
const double momentScale = std::max(
|
||||
std::abs(momentX),
|
||||
std::max(std::abs(momentY), std::abs(momentZ)));
|
||||
const double scaledX = momentX / momentScale;
|
||||
const double scaledY = momentY / momentScale;
|
||||
const double scaledZ = momentZ / momentScale;
|
||||
const double scaledNorm = std::hypot(scaledX, scaledY, scaledZ);
|
||||
const double scaledDot =
|
||||
frame->director[0U] * scaledX +
|
||||
frame->director[1U] * scaledY +
|
||||
frame->director[2U] * scaledZ;
|
||||
const double projectionRatio = std::abs(scaledDot) / scaledNorm;
|
||||
if (!(projectionRatio <= shellMomentProjectionTolerance)) {
|
||||
return loadFailure(
|
||||
"unsupported-drilling-load",
|
||||
domain.Nodes()[node].location,
|
||||
"CLOAD",
|
||||
domain.Nodes()[node].source_id.source_label_text,
|
||||
"The aggregate nodal moment has an unsupported director-parallel component.");
|
||||
}
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<Vector> LoadAssembler::assembleFullNodalLoad(
|
||||
const AnalysisModel& model,
|
||||
const DofManager& dofs) {
|
||||
const Domain& domain = model.domain();
|
||||
if (domain.Nodes().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() / dofsPerNode) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"invalid-load-dimensions",
|
||||
{domain.SourcePath(), 0U},
|
||||
"LOAD_ASSEMBLER",
|
||||
domain.SourceContentIdentity(),
|
||||
"The semantic node count cannot be represented in full-DOF order."));
|
||||
}
|
||||
const std::size_t expectedFullCount =
|
||||
domain.Nodes().size() * dofsPerNode;
|
||||
const Status dofStatus = validateDofOrder(
|
||||
dofs, expectedFullCount, {domain.SourcePath(), 0U});
|
||||
if (!dofStatus.IsOk()) {
|
||||
return Result<Vector>::Failure(dofStatus);
|
||||
}
|
||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||
for (std::size_t component = 0U;
|
||||
component < dofsPerNode;
|
||||
++component) {
|
||||
try {
|
||||
if (dofs.fullDof(
|
||||
static_cast<EntityIndex>(node),
|
||||
static_cast<DofComponent>(component)) !=
|
||||
node * dofsPerNode + component) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"invalid-load-order",
|
||||
domain.Nodes()[node].location,
|
||||
"LOAD_ASSEMBLER",
|
||||
domain.Nodes()[node].source_id.source_label_text,
|
||||
"DofManager node/component identity must match full-DOF order."));
|
||||
}
|
||||
} catch (const std::out_of_range&) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"invalid-load-dimensions",
|
||||
domain.Nodes()[node].location,
|
||||
"LOAD_ASSEMBLER",
|
||||
domain.Nodes()[node].source_id.source_label_text,
|
||||
"DofManager must provide all six DOFs for every semantic node."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto& activeLoads = model.activeLoads();
|
||||
const auto& loads = model.step().loads;
|
||||
if (activeLoads.size() != loads.size()) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"invalid-load-order",
|
||||
model.step().location,
|
||||
"CLOAD",
|
||||
model.step().name,
|
||||
"The active load view must include every sole-step load once."));
|
||||
}
|
||||
|
||||
Vector fullLoad{expectedFullCount};
|
||||
// Active load indices are required to be the original source order; this
|
||||
// loop is therefore also the fixed floating-point accumulation order.
|
||||
for (std::size_t sourceOrder = 0U;
|
||||
sourceOrder < activeLoads.size();
|
||||
++sourceOrder) {
|
||||
const EntityIndex loadIndex = activeLoads[sourceOrder];
|
||||
if (static_cast<std::size_t>(loadIndex) != sourceOrder ||
|
||||
loadIndex >= loads.size()) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"invalid-load-order",
|
||||
model.step().location,
|
||||
"CLOAD",
|
||||
std::to_string(sourceOrder),
|
||||
"Active loads must retain complete stable source order."));
|
||||
}
|
||||
const auto& load = loads[loadIndex];
|
||||
if (load.dof < 1 || load.dof > static_cast<int>(dofsPerNode)) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"invalid-load-dof",
|
||||
load.location,
|
||||
"CLOAD",
|
||||
load.target,
|
||||
"A nodal load component must be in the range 1 through 6."));
|
||||
}
|
||||
if (!std::isfinite(load.magnitude)) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"nonfinite-load-value",
|
||||
load.location,
|
||||
"CLOAD",
|
||||
load.target,
|
||||
"A nodal load magnitude must be finite."));
|
||||
}
|
||||
|
||||
auto target = resolveTarget(domain, load);
|
||||
if (!target.HasValue()) {
|
||||
return Result<Vector>::Failure(target.GetStatus());
|
||||
}
|
||||
const auto component = static_cast<DofComponent>(load.dof - 1);
|
||||
for (const EntityIndex node : target.Value()) {
|
||||
const std::size_t fullDof = dofs.fullDof(node, component);
|
||||
const double accumulated = fullLoad[fullDof] + load.magnitude;
|
||||
if (!std::isfinite(accumulated)) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"nonfinite-load-accumulation",
|
||||
load.location,
|
||||
"CLOAD",
|
||||
load.target,
|
||||
"Source-order load accumulation produced a nonfinite value."));
|
||||
}
|
||||
fullLoad[fullDof] = accumulated;
|
||||
}
|
||||
}
|
||||
const Status shellMomentStatus = validateShellMoments(domain, fullLoad);
|
||||
if (!shellMomentStatus.IsOk()) {
|
||||
return Result<Vector>::Failure(shellMomentStatus);
|
||||
}
|
||||
return Result<Vector>::Success(std::move(fullLoad));
|
||||
}
|
||||
|
||||
Result<Vector> LoadAssembler::effectiveFreeRhs(
|
||||
const Vector& fullLoad,
|
||||
const SparseMatrix& kfc,
|
||||
const Vector& prescribedValues,
|
||||
const DofManager& dofs) {
|
||||
const SourceLocation location{{}, 0U};
|
||||
const Status dofStatus =
|
||||
validateDofOrder(dofs, fullLoad.Size(), location);
|
||||
if (!dofStatus.IsOk()) {
|
||||
return Result<Vector>::Failure(dofStatus);
|
||||
}
|
||||
if (kfc.Rows() != dofs.freeDofCount() ||
|
||||
kfc.Columns() != dofs.constrainedDofCount() ||
|
||||
prescribedValues.Size() != dofs.constrainedDofCount()) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"invalid-load-dimensions",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(kfc.Rows()) + "x" +
|
||||
std::to_string(kfc.Columns()),
|
||||
"Kfc rows/columns and prescribed values must match free/constrained order."));
|
||||
}
|
||||
const Status matrixStatus = kfc.Validate();
|
||||
if (!matrixStatus.IsOk()) {
|
||||
return Result<Vector>::Failure(matrixStatus);
|
||||
}
|
||||
const Status loadStatus =
|
||||
validateFiniteVector(fullLoad, location, "full-load");
|
||||
if (!loadStatus.IsOk()) {
|
||||
return Result<Vector>::Failure(loadStatus);
|
||||
}
|
||||
const Status prescribedStatus = validateFiniteVector(
|
||||
prescribedValues, location, "prescribed-values");
|
||||
if (!prescribedStatus.IsOk()) {
|
||||
return Result<Vector>::Failure(prescribedStatus);
|
||||
}
|
||||
|
||||
Vector correction{kfc.Rows()};
|
||||
for (std::size_t row = 0U; row < kfc.Rows(); ++row) {
|
||||
double sum = 0.0;
|
||||
for (std::size_t position = kfc.RowOffsets()[row];
|
||||
position < kfc.RowOffsets()[row + 1U];
|
||||
++position) {
|
||||
const double product = kfc.Values()[position] *
|
||||
prescribedValues[kfc.ColumnIndices()[position]];
|
||||
if (!std::isfinite(product)) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"nonfinite-load-accumulation",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(row),
|
||||
"Kfc times prescribed displacement produced a nonfinite product."));
|
||||
}
|
||||
sum += product;
|
||||
if (!std::isfinite(sum)) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"nonfinite-load-accumulation",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(row),
|
||||
"Kfc times prescribed displacement produced a nonfinite row sum."));
|
||||
}
|
||||
}
|
||||
correction[row] = sum;
|
||||
}
|
||||
|
||||
Vector rhs = EssentialConstraints::gatherFree(fullLoad, dofs);
|
||||
// The constrained vector is already in DofManager order, so this is the
|
||||
// approved elimination equation rhs = Ff - Kfc*dc without reordering dc.
|
||||
for (std::size_t row = 0U; row < rhs.Size(); ++row) {
|
||||
const double value = rhs[row] - correction[row];
|
||||
if (!std::isfinite(value)) {
|
||||
return Result<Vector>::Failure(loadFailure(
|
||||
"nonfinite-load-accumulation",
|
||||
location,
|
||||
"LOAD_ASSEMBLER",
|
||||
std::to_string(row),
|
||||
"Effective RHS subtraction produced a nonfinite value."));
|
||||
}
|
||||
rhs[row] = value;
|
||||
}
|
||||
return Result<Vector>::Success(std::move(rhs));
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
#include "fesa/assembly/parallel_for.hpp"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
|
||||
#include <oneapi/tbb/parallel_for.h>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
void SerialParallelFor::execute(
|
||||
std::size_t count,
|
||||
const std::function<void(std::size_t)>& body) const {
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
body(index);
|
||||
}
|
||||
void SerialParallelFor::Execute(
|
||||
std::size_t count, const std::function<void(std::size_t)>& body) const {
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
body(index);
|
||||
}
|
||||
}
|
||||
|
||||
void TbbParallelFor::execute(
|
||||
std::size_t count,
|
||||
const std::function<void(std::size_t)>& body) const {
|
||||
if (count == 0U) {
|
||||
return;
|
||||
}
|
||||
void TbbParallelFor::Execute(
|
||||
std::size_t count, const std::function<void(std::size_t)>& body) const {
|
||||
if (count == 0U) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use oneTBB's caller-scoped scheduler policy. This adapter does not set
|
||||
// process-wide concurrency or override the later MKL/TBB oversubscription
|
||||
// policy. A body exception cancels sibling tasks and is rethrown; work
|
||||
// already running during cancellation may still finish its indexed slot.
|
||||
oneapi::tbb::parallel_for(
|
||||
std::size_t{0}, count, [&body](std::size_t index) { body(index); });
|
||||
// Use oneTBB's caller-scoped scheduler policy. This adapter does not set
|
||||
// process-wide concurrency or override the later MKL/TBB oversubscription
|
||||
// policy. A body exception cancels sibling tasks and is rethrown; work
|
||||
// already running during cancellation may still finish its indexed slot.
|
||||
oneapi::tbb::parallel_for(std::size_t{0}, count,
|
||||
[&body](std::size_t index) { body(index); });
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
#include "fesa/assembly/sparse_assembler.hpp"
|
||||
|
||||
#include "fesa/analysis/analysis_model.hpp"
|
||||
#include "fesa/assembly/parallel_for.hpp"
|
||||
#include "fesa/elements/euler_beam_3d.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/fem/dof_manager.hpp"
|
||||
#include "fesa/assembly/sparse_assembler.h"
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
@@ -14,6 +8,12 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/elements/euler_beam_3d.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
@@ -25,336 +25,277 @@ constexpr std::size_t kShellElementDofCount = 24U;
|
||||
constexpr std::size_t kShellContributionCount =
|
||||
kShellElementDofCount * kShellElementDofCount;
|
||||
|
||||
using BeamElementBuffer =
|
||||
std::array<CooContribution, kBeamContributionCount>;
|
||||
using ShellElementBuffer =
|
||||
std::array<CooContribution, kShellContributionCount>;
|
||||
using BeamElementBuffer = std::array<CooContribution, kBeamContributionCount>;
|
||||
using ShellElementBuffer = std::array<CooContribution, kShellContributionCount>;
|
||||
|
||||
Result<SparseMatrix> assemblyFailure(
|
||||
const std::string& code,
|
||||
const SourceLocation& location,
|
||||
const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Result<SparseMatrix>::Failure(Status::Failure(
|
||||
FailureCategory::kModel,
|
||||
{{Severity::kError,
|
||||
code,
|
||||
location,
|
||||
"*ELEMENT",
|
||||
identity,
|
||||
message}}));
|
||||
Result<SparseMatrix> AssemblyFailure(const std::string& code,
|
||||
const SourceLocation& location,
|
||||
const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Result<SparseMatrix>::Failure(Status::Failure(
|
||||
FailureCategory::kModel,
|
||||
{{Severity::kError, code, location, "*ELEMENT", identity, message}}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
Result<SparseMatrix> SparseAssembler::assembleStiffness(
|
||||
const AnalysisModel& model,
|
||||
const DofManager& dofs,
|
||||
const ParallelFor& parallelFor) {
|
||||
const Domain& domain = model.domain();
|
||||
if (domain.Nodes().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
|
||||
dofs.fullDofCount() != domain.Nodes().size() * kDofsPerNode) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-dimensions",
|
||||
{domain.SourcePath(), 0U},
|
||||
std::to_string(dofs.fullDofCount()),
|
||||
"DofManager dimensions do not match the active model nodes.");
|
||||
}
|
||||
if (!model.activeElements().empty() && !domain.ShellElements().empty()) {
|
||||
return assemblyFailure(
|
||||
"unsupported-mixed-element-model",
|
||||
{domain.SourcePath(), 0U},
|
||||
"B33:FESA-MITC4",
|
||||
"Sparse assembly does not support mixed beam and shell models.");
|
||||
Result<SparseMatrix> SparseAssembler::AssembleStiffness(
|
||||
const AnalysisModel& model, const DofManager& dofs,
|
||||
const ParallelFor& parallel_for) {
|
||||
const Domain& domain = model.GetDomain();
|
||||
if (domain.Nodes().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
|
||||
dofs.FullDofCount() != domain.Nodes().size() * kDofsPerNode) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-dimensions", {domain.SourcePath(), 0U},
|
||||
std::to_string(dofs.FullDofCount()),
|
||||
"DofManager dimensions do not match the active model nodes.");
|
||||
}
|
||||
if (!model.ActiveElements().empty() && !domain.ShellElements().empty()) {
|
||||
return AssemblyFailure(
|
||||
"unsupported-mixed-element-model", {domain.SourcePath(), 0U},
|
||||
"B33:FESA-MITC4",
|
||||
"Sparse assembly does not support mixed beam and shell models.");
|
||||
}
|
||||
|
||||
if (!domain.ShellElements().empty()) {
|
||||
if (domain.ShellElements().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() / kShellContributionCount) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-dimensions", {domain.SourcePath(), 0U},
|
||||
std::to_string(domain.ShellElements().size()),
|
||||
"Shell contribution storage exceeds the addressable range.");
|
||||
}
|
||||
|
||||
if (!domain.ShellElements().empty()) {
|
||||
if (domain.ShellElements().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() /
|
||||
kShellContributionCount) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-dimensions",
|
||||
{domain.SourcePath(), 0U},
|
||||
std::to_string(domain.ShellElements().size()),
|
||||
"Shell contribution storage exceeds the addressable range.");
|
||||
}
|
||||
|
||||
std::vector<std::optional<std::array<double, 3>>> directorsByNode(
|
||||
domain.Nodes().size());
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
if (frame.node_index >= directorsByNode.size() ||
|
||||
directorsByNode[frame.node_index]) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-element",
|
||||
{domain.SourcePath(), 0U},
|
||||
std::to_string(frame.node_index),
|
||||
"Shell initial frames must map uniquely to model nodes.");
|
||||
}
|
||||
directorsByNode[frame.node_index] = frame.director;
|
||||
}
|
||||
|
||||
struct ShellInput {
|
||||
std::array<const Node*, 4> nodes;
|
||||
std::array<std::array<double, 3>, 4> directors;
|
||||
const ShellSection* section;
|
||||
const LinearElasticMaterial* material;
|
||||
std::array<std::size_t, kShellElementDofCount> scatter;
|
||||
};
|
||||
std::vector<ShellInput> inputs;
|
||||
inputs.reserve(domain.ShellElements().size());
|
||||
for (std::size_t elementOrder = 0U;
|
||||
elementOrder < domain.ShellElements().size();
|
||||
++elementOrder) {
|
||||
const auto& element = domain.ShellElements()[elementOrder];
|
||||
if (element.material_index >= domain.Materials().size() ||
|
||||
element.section_index >= domain.ShellSections().size()) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-element",
|
||||
element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Shell element references an entity outside the Domain.");
|
||||
}
|
||||
|
||||
ShellInput input{};
|
||||
input.section = &domain.ShellSections()[element.section_index];
|
||||
input.material = &domain.Materials()[element.material_index];
|
||||
try {
|
||||
input.scatter = dofs.shellElementScatter(
|
||||
static_cast<EntityIndex>(elementOrder));
|
||||
} catch (const std::out_of_range&) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-scatter",
|
||||
element.location,
|
||||
element.source_id.source_label_text,
|
||||
"DofManager does not contain the active shell scatter.");
|
||||
}
|
||||
for (std::size_t nodePosition = 0U;
|
||||
nodePosition < element.node_indices.size();
|
||||
++nodePosition) {
|
||||
const EntityIndex nodeIndex = element.node_indices[nodePosition];
|
||||
if (nodeIndex >= domain.Nodes().size() ||
|
||||
!directorsByNode[nodeIndex]) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-element",
|
||||
element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Shell element requires a valid node and initial director.");
|
||||
}
|
||||
input.nodes[nodePosition] = &domain.Nodes()[nodeIndex];
|
||||
input.directors[nodePosition] = *directorsByNode[nodeIndex];
|
||||
for (std::size_t component = 0U;
|
||||
component < kDofsPerNode;
|
||||
++component) {
|
||||
const std::size_t local =
|
||||
nodePosition * kDofsPerNode + component;
|
||||
const std::size_t expected =
|
||||
static_cast<std::size_t>(nodeIndex) * kDofsPerNode +
|
||||
component;
|
||||
if (input.scatter[local] != expected ||
|
||||
input.scatter[local] >= dofs.fullDofCount()) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-scatter",
|
||||
element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Shell scatter does not match the active model topology.");
|
||||
}
|
||||
}
|
||||
}
|
||||
inputs.push_back(input);
|
||||
}
|
||||
|
||||
std::vector<ShellElementBuffer> localBuffers(inputs.size());
|
||||
std::vector<std::optional<Status>> localFailures(inputs.size());
|
||||
parallelFor.execute(
|
||||
inputs.size(),
|
||||
[&](const std::size_t elementOrder) {
|
||||
const auto& input = inputs[elementOrder];
|
||||
const auto shell = Mitc4Shell::Create(
|
||||
input.nodes,
|
||||
input.directors,
|
||||
*input.section,
|
||||
*input.material);
|
||||
if (!shell.HasValue()) {
|
||||
localFailures[elementOrder] = shell.GetStatus();
|
||||
return;
|
||||
}
|
||||
const auto stiffness = shell.Value().Stiffness();
|
||||
if (!stiffness.HasValue()) {
|
||||
localFailures[elementOrder] = stiffness.GetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
auto& buffer = localBuffers[elementOrder];
|
||||
for (std::size_t localRow = 0U;
|
||||
localRow < kShellElementDofCount;
|
||||
++localRow) {
|
||||
for (std::size_t localColumn = 0U;
|
||||
localColumn < kShellElementDofCount;
|
||||
++localColumn) {
|
||||
const std::size_t localOrder =
|
||||
localRow * kShellElementDofCount + localColumn;
|
||||
buffer[localOrder] = {
|
||||
input.scatter[localRow],
|
||||
input.scatter[localColumn],
|
||||
stiffness.Value().stabilized_global24(
|
||||
localRow, localColumn),
|
||||
elementOrder,
|
||||
localOrder};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (std::size_t elementOrder = 0U;
|
||||
elementOrder < localFailures.size();
|
||||
++elementOrder) {
|
||||
if (localFailures[elementOrder]) {
|
||||
return Result<SparseMatrix>::Failure(
|
||||
*localFailures[elementOrder]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CooContribution> contributions;
|
||||
contributions.reserve(
|
||||
localBuffers.size() * kShellContributionCount);
|
||||
// Flatten in source-element order after workers complete. The canonical
|
||||
// COO reduction remains the sole writer of global CSR values.
|
||||
for (const auto& buffer : localBuffers) {
|
||||
contributions.insert(
|
||||
contributions.end(), buffer.begin(), buffer.end());
|
||||
}
|
||||
return SparseMatrix::FromCoo(
|
||||
dofs.fullDofCount(),
|
||||
dofs.fullDofCount(),
|
||||
std::move(contributions),
|
||||
dofs.sparsePattern());
|
||||
std::vector<std::optional<std::array<double, 3>>> directors_by_node(
|
||||
domain.Nodes().size());
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
if (frame.node_index >= directors_by_node.size() ||
|
||||
directors_by_node[frame.node_index]) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-element", {domain.SourcePath(), 0U},
|
||||
std::to_string(frame.node_index),
|
||||
"Shell initial frames must map uniquely to model nodes.");
|
||||
}
|
||||
directors_by_node[frame.node_index] = frame.director;
|
||||
}
|
||||
|
||||
if (model.activeElements().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() /
|
||||
kBeamContributionCount) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-dimensions",
|
||||
{domain.SourcePath(), 0U},
|
||||
std::to_string(model.activeElements().size()),
|
||||
"Element contribution storage exceeds the addressable range.");
|
||||
}
|
||||
struct ShellInput {
|
||||
std::array<const Node*, 4> nodes;
|
||||
std::array<std::array<double, 3>, 4> directors;
|
||||
const ShellSection* section;
|
||||
const LinearElasticMaterial* material;
|
||||
std::array<std::size_t, kShellElementDofCount> scatter;
|
||||
};
|
||||
std::vector<ShellInput> inputs;
|
||||
inputs.reserve(domain.ShellElements().size());
|
||||
for (std::size_t element_order = 0U;
|
||||
element_order < domain.ShellElements().size(); ++element_order) {
|
||||
const auto& element = domain.ShellElements()[element_order];
|
||||
if (element.material_index >= domain.Materials().size() ||
|
||||
element.section_index >= domain.ShellSections().size()) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-element", element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Shell element references an entity outside the Domain.");
|
||||
}
|
||||
|
||||
std::vector<std::array<std::size_t, kBeamElementDofCount>> scatters;
|
||||
scatters.reserve(model.activeElements().size());
|
||||
for (const EntityIndex elementIndex : model.activeElements()) {
|
||||
if (elementIndex >= domain.Elements().size()) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-element",
|
||||
{domain.SourcePath(), 0U},
|
||||
std::to_string(elementIndex),
|
||||
"Active element index is outside the Domain.");
|
||||
ShellInput input{};
|
||||
input.section = &domain.ShellSections()[element.section_index];
|
||||
input.material = &domain.Materials()[element.material_index];
|
||||
try {
|
||||
input.scatter =
|
||||
dofs.ShellElementScatter(static_cast<EntityIndex>(element_order));
|
||||
} catch (const std::out_of_range&) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-scatter", element.location,
|
||||
element.source_id.source_label_text,
|
||||
"DofManager does not contain the active shell scatter.");
|
||||
}
|
||||
for (std::size_t node_position = 0U;
|
||||
node_position < element.node_indices.size(); ++node_position) {
|
||||
const EntityIndex node_index = element.node_indices[node_position];
|
||||
if (node_index >= domain.Nodes().size() ||
|
||||
!directors_by_node[node_index]) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-element", element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Shell element requires a valid node and initial director.");
|
||||
}
|
||||
const auto& element = domain.Elements()[elementIndex];
|
||||
if (element.node_indices[0U] >= domain.Nodes().size() ||
|
||||
element.node_indices[1U] >= domain.Nodes().size() ||
|
||||
element.material_index >= domain.Materials().size() ||
|
||||
element.section_index >= domain.Sections().size()) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-element",
|
||||
element.location,
|
||||
input.nodes[node_position] = &domain.Nodes()[node_index];
|
||||
input.directors[node_position] = *directors_by_node[node_index];
|
||||
for (std::size_t component = 0U; component < kDofsPerNode;
|
||||
++component) {
|
||||
const std::size_t local = node_position * kDofsPerNode + component;
|
||||
const std::size_t expected =
|
||||
static_cast<std::size_t>(node_index) * kDofsPerNode + component;
|
||||
if (input.scatter[local] != expected ||
|
||||
input.scatter[local] >= dofs.FullDofCount()) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-scatter", element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Element references an entity outside the Domain.");
|
||||
"Shell scatter does not match the active model topology.");
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::size_t, kBeamElementDofCount> scatter{};
|
||||
try {
|
||||
scatter = dofs.elementScatter(elementIndex);
|
||||
} catch (const std::out_of_range&) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-scatter",
|
||||
element.location,
|
||||
element.source_id.source_label_text,
|
||||
"DofManager does not contain the active element scatter.");
|
||||
}
|
||||
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
|
||||
for (std::size_t component = 0U;
|
||||
component < kDofsPerNode;
|
||||
++component) {
|
||||
const std::size_t local = endpoint * kDofsPerNode + component;
|
||||
const std::size_t expected =
|
||||
static_cast<std::size_t>(element.node_indices[endpoint]) *
|
||||
kDofsPerNode +
|
||||
component;
|
||||
if (scatter[local] != expected ||
|
||||
scatter[local] >= dofs.fullDofCount()) {
|
||||
return assemblyFailure(
|
||||
"invalid-assembly-scatter",
|
||||
element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Element scatter does not match the active model topology.");
|
||||
}
|
||||
}
|
||||
}
|
||||
scatters.push_back(scatter);
|
||||
}
|
||||
inputs.push_back(input);
|
||||
}
|
||||
|
||||
std::vector<BeamElementBuffer> localBuffers(model.activeElements().size());
|
||||
std::vector<std::optional<Status>> localFailures(
|
||||
model.activeElements().size());
|
||||
parallelFor.execute(
|
||||
model.activeElements().size(),
|
||||
[&](const std::size_t elementOrder) {
|
||||
const EntityIndex elementIndex = model.activeElements()[elementOrder];
|
||||
const auto& definition = domain.Elements()[elementIndex];
|
||||
const auto beam = EulerBeam3D::Create(
|
||||
domain.Nodes()[definition.node_indices[0U]],
|
||||
domain.Nodes()[definition.node_indices[1U]],
|
||||
domain.Sections()[definition.section_index],
|
||||
domain.Materials()[definition.material_index]);
|
||||
if (!beam.HasValue()) {
|
||||
localFailures[elementOrder] = beam.GetStatus();
|
||||
return;
|
||||
}
|
||||
std::vector<ShellElementBuffer> local_buffers(inputs.size());
|
||||
std::vector<std::optional<Status>> local_failures(inputs.size());
|
||||
parallel_for.Execute(inputs.size(), [&](const std::size_t element_order) {
|
||||
const auto& input = inputs[element_order];
|
||||
const auto shell = Mitc4Shell::Create(input.nodes, input.directors,
|
||||
*input.section, *input.material);
|
||||
if (!shell.HasValue()) {
|
||||
local_failures[element_order] = shell.GetStatus();
|
||||
return;
|
||||
}
|
||||
const auto stiffness = shell.Value().Stiffness();
|
||||
if (!stiffness.HasValue()) {
|
||||
local_failures[element_order] = stiffness.GetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const Matrix stiffness = beam.Value().GlobalStiffness();
|
||||
auto& buffer = localBuffers[elementOrder];
|
||||
const auto& scatter = scatters[elementOrder];
|
||||
for (std::size_t localRow = 0U;
|
||||
localRow < kBeamElementDofCount;
|
||||
++localRow) {
|
||||
for (std::size_t localColumn = 0U;
|
||||
localColumn < kBeamElementDofCount;
|
||||
++localColumn) {
|
||||
const std::size_t localOrder =
|
||||
localRow * kBeamElementDofCount + localColumn;
|
||||
buffer[localOrder] = {
|
||||
scatter[localRow],
|
||||
scatter[localColumn],
|
||||
stiffness(localRow, localColumn),
|
||||
elementOrder,
|
||||
localOrder};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (std::size_t elementOrder = 0U;
|
||||
elementOrder < localFailures.size();
|
||||
++elementOrder) {
|
||||
if (localFailures[elementOrder]) {
|
||||
return Result<SparseMatrix>::Failure(
|
||||
*localFailures[elementOrder]);
|
||||
auto& buffer = local_buffers[element_order];
|
||||
for (std::size_t local_row = 0U; local_row < kShellElementDofCount;
|
||||
++local_row) {
|
||||
for (std::size_t local_column = 0U;
|
||||
local_column < kShellElementDofCount; ++local_column) {
|
||||
const std::size_t local_order =
|
||||
local_row * kShellElementDofCount + local_column;
|
||||
buffer[local_order] = {
|
||||
input.scatter[local_row], input.scatter[local_column],
|
||||
stiffness.Value().stabilized_global24(local_row, local_column),
|
||||
element_order, local_order};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (std::size_t element_order = 0U; element_order < local_failures.size();
|
||||
++element_order) {
|
||||
if (local_failures[element_order]) {
|
||||
return Result<SparseMatrix>::Failure(*local_failures[element_order]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CooContribution> contributions;
|
||||
contributions.reserve(
|
||||
localBuffers.size() * kBeamContributionCount);
|
||||
// Flatten only after all workers complete; workers never share CSR state.
|
||||
for (const auto& buffer : localBuffers) {
|
||||
contributions.insert(
|
||||
contributions.end(), buffer.begin(), buffer.end());
|
||||
contributions.reserve(local_buffers.size() * kShellContributionCount);
|
||||
// Flatten in source-element order after workers complete. The canonical
|
||||
// COO reduction remains the sole writer of global CSR values.
|
||||
for (const auto& buffer : local_buffers) {
|
||||
contributions.insert(contributions.end(), buffer.begin(), buffer.end());
|
||||
}
|
||||
return SparseMatrix::FromCoo(
|
||||
dofs.fullDofCount(),
|
||||
dofs.fullDofCount(),
|
||||
std::move(contributions),
|
||||
dofs.sparsePattern());
|
||||
return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
|
||||
std::move(contributions),
|
||||
dofs.GetSparsePattern());
|
||||
}
|
||||
|
||||
if (model.ActiveElements().size() >
|
||||
(std::numeric_limits<std::size_t>::max)() / kBeamContributionCount) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-dimensions", {domain.SourcePath(), 0U},
|
||||
std::to_string(model.ActiveElements().size()),
|
||||
"Element contribution storage exceeds the addressable range.");
|
||||
}
|
||||
|
||||
std::vector<std::array<std::size_t, kBeamElementDofCount>> scatters;
|
||||
scatters.reserve(model.ActiveElements().size());
|
||||
for (const EntityIndex element_index : model.ActiveElements()) {
|
||||
if (element_index >= domain.Elements().size()) {
|
||||
return AssemblyFailure("invalid-assembly-element",
|
||||
{domain.SourcePath(), 0U},
|
||||
std::to_string(element_index),
|
||||
"Active element index is outside the Domain.");
|
||||
}
|
||||
const auto& element = domain.Elements()[element_index];
|
||||
if (element.node_indices[0U] >= domain.Nodes().size() ||
|
||||
element.node_indices[1U] >= domain.Nodes().size() ||
|
||||
element.material_index >= domain.Materials().size() ||
|
||||
element.section_index >= domain.Sections().size()) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-element", element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Element references an entity outside the Domain.");
|
||||
}
|
||||
|
||||
std::array<std::size_t, kBeamElementDofCount> scatter{};
|
||||
try {
|
||||
scatter = dofs.ElementScatter(element_index);
|
||||
} catch (const std::out_of_range&) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-scatter", element.location,
|
||||
element.source_id.source_label_text,
|
||||
"DofManager does not contain the active element scatter.");
|
||||
}
|
||||
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
|
||||
for (std::size_t component = 0U; component < kDofsPerNode; ++component) {
|
||||
const std::size_t local = endpoint * kDofsPerNode + component;
|
||||
const std::size_t expected =
|
||||
static_cast<std::size_t>(element.node_indices[endpoint]) *
|
||||
kDofsPerNode +
|
||||
component;
|
||||
if (scatter[local] != expected ||
|
||||
scatter[local] >= dofs.FullDofCount()) {
|
||||
return AssemblyFailure(
|
||||
"invalid-assembly-scatter", element.location,
|
||||
element.source_id.source_label_text,
|
||||
"Element scatter does not match the active model topology.");
|
||||
}
|
||||
}
|
||||
}
|
||||
scatters.push_back(scatter);
|
||||
}
|
||||
|
||||
std::vector<BeamElementBuffer> local_buffers(model.ActiveElements().size());
|
||||
std::vector<std::optional<Status>> local_failures(
|
||||
model.ActiveElements().size());
|
||||
parallel_for.Execute(
|
||||
model.ActiveElements().size(), [&](const std::size_t element_order) {
|
||||
const EntityIndex element_index = model.ActiveElements()[element_order];
|
||||
const auto& definition = domain.Elements()[element_index];
|
||||
const auto beam =
|
||||
EulerBeam3D::Create(domain.Nodes()[definition.node_indices[0U]],
|
||||
domain.Nodes()[definition.node_indices[1U]],
|
||||
domain.Sections()[definition.section_index],
|
||||
domain.Materials()[definition.material_index]);
|
||||
if (!beam.HasValue()) {
|
||||
local_failures[element_order] = beam.GetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const Matrix stiffness = beam.Value().GlobalStiffness();
|
||||
auto& buffer = local_buffers[element_order];
|
||||
const auto& scatter = scatters[element_order];
|
||||
for (std::size_t local_row = 0U; local_row < kBeamElementDofCount;
|
||||
++local_row) {
|
||||
for (std::size_t local_column = 0U;
|
||||
local_column < kBeamElementDofCount; ++local_column) {
|
||||
const std::size_t local_order =
|
||||
local_row * kBeamElementDofCount + local_column;
|
||||
buffer[local_order] = {scatter[local_row], scatter[local_column],
|
||||
stiffness(local_row, local_column),
|
||||
element_order, local_order};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (std::size_t element_order = 0U; element_order < local_failures.size();
|
||||
++element_order) {
|
||||
if (local_failures[element_order]) {
|
||||
return Result<SparseMatrix>::Failure(*local_failures[element_order]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CooContribution> contributions;
|
||||
contributions.reserve(local_buffers.size() * kBeamContributionCount);
|
||||
// Flatten only after all workers complete; workers never share CSR state.
|
||||
for (const auto& buffer : local_buffers) {
|
||||
contributions.insert(contributions.end(), buffer.begin(), buffer.end());
|
||||
}
|
||||
return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
|
||||
std::move(contributions),
|
||||
dofs.GetSparsePattern());
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#include "fesa/constraints/essential_constraints.hpp"
|
||||
|
||||
#include "fesa/fem/dof_manager.hpp"
|
||||
#include "fesa/constraints/essential_constraints.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
@@ -9,253 +7,220 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
Status constraintFailure(
|
||||
const std::string& code,
|
||||
const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kModel,
|
||||
{{Severity::kError,
|
||||
code,
|
||||
{{}, 0U},
|
||||
"ESSENTIAL_CONSTRAINTS",
|
||||
identity,
|
||||
message}});
|
||||
Status ConstraintFailure(const std::string& code, const std::string& identity,
|
||||
const std::string& message) {
|
||||
return Status::Failure(FailureCategory::kModel, {{Severity::kError,
|
||||
code,
|
||||
{{}, 0U},
|
||||
"ESSENTIAL_CONSTRAINTS",
|
||||
identity,
|
||||
message}});
|
||||
}
|
||||
|
||||
bool isStrictlyIncreasing(const std::vector<std::size_t>& values) {
|
||||
return std::adjacent_find(
|
||||
values.begin(),
|
||||
values.end(),
|
||||
[](const std::size_t left, const std::size_t right) {
|
||||
return left >= right;
|
||||
}) == values.end();
|
||||
bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) {
|
||||
return std::adjacent_find(
|
||||
values.begin(), values.end(),
|
||||
[](const std::size_t left, const std::size_t right) {
|
||||
return left >= right;
|
||||
}) == values.end();
|
||||
}
|
||||
|
||||
Status validateDofOrder(const DofManager& dofs) {
|
||||
const std::size_t fullCount = dofs.fullDofCount();
|
||||
const auto& freeDofs = dofs.freeDofs();
|
||||
const auto& constrainedDofs = dofs.constrainedDofs();
|
||||
if (freeDofs.size() != dofs.freeDofCount() ||
|
||||
constrainedDofs.size() != dofs.constrainedDofCount() ||
|
||||
dofs.prescribedValues().Size() != constrainedDofs.size() ||
|
||||
constrainedDofs.size() > fullCount ||
|
||||
freeDofs.size() != fullCount - constrainedDofs.size()) {
|
||||
return constraintFailure(
|
||||
"invalid-constraint-dimensions",
|
||||
std::to_string(fullCount),
|
||||
"DofManager full, free, constrained, and prescribed dimensions must agree.");
|
||||
}
|
||||
if (!isStrictlyIncreasing(freeDofs) ||
|
||||
!isStrictlyIncreasing(constrainedDofs)) {
|
||||
return constraintFailure(
|
||||
"invalid-constraint-order",
|
||||
std::to_string(fullCount),
|
||||
"Free and constrained DOFs must use stable increasing full-DOF order.");
|
||||
}
|
||||
/// @brief Validates the stable full/free/constrained numbering invariant.
|
||||
Status ValidateDofOrder(const DofManager& dofs) {
|
||||
const std::size_t full_count = dofs.FullDofCount();
|
||||
const auto& free_dofs = dofs.FreeDofs();
|
||||
const auto& constrained_dofs = dofs.ConstrainedDofs();
|
||||
if (free_dofs.size() != dofs.FreeDofCount() ||
|
||||
constrained_dofs.size() != dofs.ConstrainedDofCount() ||
|
||||
dofs.PrescribedValues().Size() != constrained_dofs.size() ||
|
||||
constrained_dofs.size() > full_count ||
|
||||
free_dofs.size() != full_count - constrained_dofs.size()) {
|
||||
return ConstraintFailure("invalid-constraint-dimensions",
|
||||
std::to_string(full_count),
|
||||
"DofManager full, free, constrained, and "
|
||||
"prescribed dimensions must agree.");
|
||||
}
|
||||
if (!IsStrictlyIncreasing(free_dofs) ||
|
||||
!IsStrictlyIncreasing(constrained_dofs)) {
|
||||
return ConstraintFailure(
|
||||
"invalid-constraint-order", std::to_string(full_count),
|
||||
"Free and constrained DOFs must use stable increasing full-DOF order.");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> ownership(fullCount, 0U);
|
||||
try {
|
||||
for (std::size_t equation = 0U;
|
||||
equation < freeDofs.size();
|
||||
++equation) {
|
||||
const std::size_t fullDof = freeDofs[equation];
|
||||
if (fullDof >= fullCount || ownership[fullDof] != 0U ||
|
||||
dofs.freeEquation(fullDof) != equation) {
|
||||
return constraintFailure(
|
||||
"invalid-constraint-order",
|
||||
std::to_string(fullDof),
|
||||
"Free equation numbering must match the stable free-DOF order.");
|
||||
}
|
||||
ownership[fullDof] = 1U;
|
||||
}
|
||||
for (const std::size_t fullDof : constrainedDofs) {
|
||||
if (fullDof >= fullCount || ownership[fullDof] != 0U ||
|
||||
dofs.freeEquation(fullDof).has_value()) {
|
||||
return constraintFailure(
|
||||
"invalid-constraint-order",
|
||||
std::to_string(fullDof),
|
||||
"Constrained DOFs must be unique and absent from free equations.");
|
||||
}
|
||||
ownership[fullDof] = 2U;
|
||||
}
|
||||
} catch (const std::out_of_range&) {
|
||||
return constraintFailure(
|
||||
"invalid-constraint-dimensions",
|
||||
std::to_string(fullCount),
|
||||
"DofManager equation storage does not cover every full DOF.");
|
||||
std::vector<unsigned char> ownership(full_count, 0U);
|
||||
try {
|
||||
for (std::size_t equation = 0U; equation < free_dofs.size(); ++equation) {
|
||||
const std::size_t full_dof = free_dofs[equation];
|
||||
if (full_dof >= full_count || ownership[full_dof] != 0U ||
|
||||
dofs.FreeEquation(full_dof) != equation) {
|
||||
return ConstraintFailure(
|
||||
"invalid-constraint-order", std::to_string(full_dof),
|
||||
"Free equation numbering must match the stable free-DOF order.");
|
||||
}
|
||||
ownership[full_dof] = 1U;
|
||||
}
|
||||
if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) {
|
||||
return constraintFailure(
|
||||
"invalid-constraint-order",
|
||||
std::to_string(fullCount),
|
||||
"Free and constrained DOFs must partition the complete full-DOF range.");
|
||||
for (const std::size_t full_dof : constrained_dofs) {
|
||||
if (full_dof >= full_count || ownership[full_dof] != 0U ||
|
||||
dofs.FreeEquation(full_dof).has_value()) {
|
||||
return ConstraintFailure(
|
||||
"invalid-constraint-order", std::to_string(full_dof),
|
||||
"Constrained DOFs must be unique and absent from free equations.");
|
||||
}
|
||||
ownership[full_dof] = 2U;
|
||||
}
|
||||
return Status::Ok();
|
||||
} catch (const std::out_of_range&) {
|
||||
return ConstraintFailure(
|
||||
"invalid-constraint-dimensions", std::to_string(full_count),
|
||||
"DofManager equation storage does not cover every full DOF.");
|
||||
}
|
||||
if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) {
|
||||
return ConstraintFailure("invalid-constraint-order",
|
||||
std::to_string(full_count),
|
||||
"Free and constrained DOFs must partition the "
|
||||
"complete full-DOF range.");
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<SparseMatrix> extractBlock(
|
||||
const SparseMatrix& full,
|
||||
const std::vector<std::size_t>& rowDofs,
|
||||
const std::vector<std::size_t>& columnDofs) {
|
||||
const std::size_t absent = (std::numeric_limits<std::size_t>::max)();
|
||||
std::vector<std::size_t> localColumn(full.Columns(), absent);
|
||||
for (std::size_t column = 0U; column < columnDofs.size(); ++column) {
|
||||
localColumn[columnDofs[column]] = column;
|
||||
}
|
||||
/// @brief Extracts one partition without changing the supplied DOF order.
|
||||
Result<SparseMatrix> ExtractBlock(const SparseMatrix& full,
|
||||
const std::vector<std::size_t>& row_dofs,
|
||||
const std::vector<std::size_t>& column_dofs) {
|
||||
const std::size_t absent = (std::numeric_limits<std::size_t>::max)();
|
||||
std::vector<std::size_t> local_column(full.Columns(), absent);
|
||||
for (std::size_t column = 0U; column < column_dofs.size(); ++column) {
|
||||
local_column[column_dofs[column]] = column;
|
||||
}
|
||||
|
||||
SparsePattern pattern;
|
||||
pattern.rowOffsets.reserve(rowDofs.size() + 1U);
|
||||
pattern.rowOffsets.push_back(0U);
|
||||
std::vector<CooContribution> contributions;
|
||||
contributions.reserve(full.Values().size());
|
||||
for (std::size_t localRow = 0U;
|
||||
localRow < rowDofs.size();
|
||||
++localRow) {
|
||||
const std::size_t fullRow = rowDofs[localRow];
|
||||
for (std::size_t position = full.RowOffsets()[fullRow];
|
||||
position < full.RowOffsets()[fullRow + 1U];
|
||||
++position) {
|
||||
const std::size_t column =
|
||||
localColumn[full.ColumnIndices()[position]];
|
||||
if (column == absent) {
|
||||
continue;
|
||||
}
|
||||
pattern.columnIndices.push_back(column);
|
||||
// One source CSR entry maps to one block slot, so exact numeric
|
||||
// values and structural zeros survive without a new reduction.
|
||||
contributions.push_back({
|
||||
localRow,
|
||||
column,
|
||||
full.Values()[position],
|
||||
localRow,
|
||||
position});
|
||||
}
|
||||
pattern.rowOffsets.push_back(pattern.columnIndices.size());
|
||||
SparsePattern pattern;
|
||||
pattern.row_offsets.reserve(row_dofs.size() + 1U);
|
||||
pattern.row_offsets.push_back(0U);
|
||||
std::vector<CooContribution> contributions;
|
||||
contributions.reserve(full.Values().size());
|
||||
for (std::size_t local_row = 0U; local_row < row_dofs.size(); ++local_row) {
|
||||
const std::size_t full_row = row_dofs[local_row];
|
||||
for (std::size_t position = full.RowOffsets()[full_row];
|
||||
position < full.RowOffsets()[full_row + 1U]; ++position) {
|
||||
const std::size_t column = local_column[full.ColumnIndices()[position]];
|
||||
if (column == absent) {
|
||||
continue;
|
||||
}
|
||||
pattern.column_indices.push_back(column);
|
||||
// One source CSR entry maps to one block slot, so exact numeric
|
||||
// values and structural zeros survive without a new reduction.
|
||||
contributions.push_back(
|
||||
{local_row, column, full.Values()[position], local_row, position});
|
||||
}
|
||||
return SparseMatrix::FromCoo(
|
||||
rowDofs.size(),
|
||||
columnDofs.size(),
|
||||
std::move(contributions),
|
||||
pattern);
|
||||
pattern.row_offsets.push_back(pattern.column_indices.size());
|
||||
}
|
||||
return SparseMatrix::FromCoo(row_dofs.size(), column_dofs.size(),
|
||||
std::move(contributions), pattern);
|
||||
}
|
||||
|
||||
void requireDofOrder(const DofManager& dofs) {
|
||||
if (!validateDofOrder(dofs).IsOk()) {
|
||||
throw std::invalid_argument{
|
||||
"DofManager constraint dimensions or order are invalid."};
|
||||
}
|
||||
void RequireDofOrder(const DofManager& dofs) {
|
||||
if (!ValidateDofOrder(dofs).IsOk()) {
|
||||
throw std::invalid_argument{
|
||||
"DofManager constraint dimensions or order are invalid."};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
Result<PartitionedStiffness> EssentialConstraints::partition(
|
||||
const SparseMatrix& full,
|
||||
const DofManager& dofs) {
|
||||
const Status matrixStatus = full.Validate();
|
||||
if (!matrixStatus.IsOk()) {
|
||||
return Result<PartitionedStiffness>::Failure(matrixStatus);
|
||||
}
|
||||
if (full.Rows() != full.Columns() ||
|
||||
full.Rows() != dofs.fullDofCount()) {
|
||||
return Result<PartitionedStiffness>::Failure(constraintFailure(
|
||||
"invalid-constraint-dimensions",
|
||||
std::to_string(full.Rows()) + "x" +
|
||||
std::to_string(full.Columns()),
|
||||
"Full stiffness must be square and match the DofManager full dimension."));
|
||||
}
|
||||
const Status dofStatus = validateDofOrder(dofs);
|
||||
if (!dofStatus.IsOk()) {
|
||||
return Result<PartitionedStiffness>::Failure(dofStatus);
|
||||
}
|
||||
Result<PartitionedStiffness> EssentialConstraints::Partition(
|
||||
const SparseMatrix& full, const DofManager& dofs) {
|
||||
const Status matrix_status = full.Validate();
|
||||
if (!matrix_status.IsOk()) {
|
||||
return Result<PartitionedStiffness>::Failure(matrix_status);
|
||||
}
|
||||
if (full.Rows() != full.Columns() || full.Rows() != dofs.FullDofCount()) {
|
||||
return Result<PartitionedStiffness>::Failure(ConstraintFailure(
|
||||
"invalid-constraint-dimensions",
|
||||
std::to_string(full.Rows()) + "x" + std::to_string(full.Columns()),
|
||||
"Full stiffness must be square and match the DofManager full "
|
||||
"dimension."));
|
||||
}
|
||||
const Status dof_status = ValidateDofOrder(dofs);
|
||||
if (!dof_status.IsOk()) {
|
||||
return Result<PartitionedStiffness>::Failure(dof_status);
|
||||
}
|
||||
|
||||
auto kff = extractBlock(full, dofs.freeDofs(), dofs.freeDofs());
|
||||
if (!kff.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kff.GetStatus());
|
||||
}
|
||||
auto kfc = extractBlock(full, dofs.freeDofs(), dofs.constrainedDofs());
|
||||
if (!kfc.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kfc.GetStatus());
|
||||
}
|
||||
auto kcf = extractBlock(full, dofs.constrainedDofs(), dofs.freeDofs());
|
||||
if (!kcf.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kcf.GetStatus());
|
||||
}
|
||||
auto kcc = extractBlock(
|
||||
full, dofs.constrainedDofs(), dofs.constrainedDofs());
|
||||
if (!kcc.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kcc.GetStatus());
|
||||
}
|
||||
auto kff = ExtractBlock(full, dofs.FreeDofs(), dofs.FreeDofs());
|
||||
if (!kff.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kff.GetStatus());
|
||||
}
|
||||
auto kfc = ExtractBlock(full, dofs.FreeDofs(), dofs.ConstrainedDofs());
|
||||
if (!kfc.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kfc.GetStatus());
|
||||
}
|
||||
auto kcf = ExtractBlock(full, dofs.ConstrainedDofs(), dofs.FreeDofs());
|
||||
if (!kcf.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kcf.GetStatus());
|
||||
}
|
||||
auto kcc = ExtractBlock(full, dofs.ConstrainedDofs(), dofs.ConstrainedDofs());
|
||||
if (!kcc.HasValue()) {
|
||||
return Result<PartitionedStiffness>::Failure(kcc.GetStatus());
|
||||
}
|
||||
|
||||
return Result<PartitionedStiffness>::Success({
|
||||
std::move(kff.Value()),
|
||||
std::move(kfc.Value()),
|
||||
std::move(kcf.Value()),
|
||||
std::move(kcc.Value())});
|
||||
return Result<PartitionedStiffness>::Success(
|
||||
{std::move(kff.Value()), std::move(kfc.Value()), std::move(kcf.Value()),
|
||||
std::move(kcc.Value())});
|
||||
}
|
||||
|
||||
Vector EssentialConstraints::gatherFree(
|
||||
const Vector& full,
|
||||
const DofManager& dofs) {
|
||||
requireDofOrder(dofs);
|
||||
if (full.Size() != dofs.fullDofCount()) {
|
||||
throw std::invalid_argument{
|
||||
"Full vector size must match the DofManager full dimension."};
|
||||
}
|
||||
Vector reduced{dofs.freeDofCount()};
|
||||
for (std::size_t equation = 0U;
|
||||
equation < dofs.freeDofs().size();
|
||||
++equation) {
|
||||
reduced[equation] = full[dofs.freeDofs()[equation]];
|
||||
}
|
||||
return reduced;
|
||||
Vector EssentialConstraints::GatherFree(const Vector& full,
|
||||
const DofManager& dofs) {
|
||||
RequireDofOrder(dofs);
|
||||
if (full.Size() != dofs.FullDofCount()) {
|
||||
throw std::invalid_argument{
|
||||
"Full vector size must match the DofManager full dimension."};
|
||||
}
|
||||
Vector reduced{dofs.FreeDofCount()};
|
||||
for (std::size_t equation = 0U; equation < dofs.FreeDofs().size();
|
||||
++equation) {
|
||||
reduced[equation] = full[dofs.FreeDofs()[equation]];
|
||||
}
|
||||
return reduced;
|
||||
}
|
||||
|
||||
Vector EssentialConstraints::gatherConstrained(
|
||||
const Vector& full,
|
||||
const DofManager& dofs) {
|
||||
requireDofOrder(dofs);
|
||||
if (full.Size() != dofs.fullDofCount()) {
|
||||
throw std::invalid_argument{
|
||||
"Full vector size must match the DofManager full dimension."};
|
||||
}
|
||||
Vector reduced{dofs.constrainedDofCount()};
|
||||
for (std::size_t index = 0U;
|
||||
index < dofs.constrainedDofs().size();
|
||||
++index) {
|
||||
reduced[index] = full[dofs.constrainedDofs()[index]];
|
||||
}
|
||||
return reduced;
|
||||
Vector EssentialConstraints::GatherConstrained(const Vector& full,
|
||||
const DofManager& dofs) {
|
||||
RequireDofOrder(dofs);
|
||||
if (full.Size() != dofs.FullDofCount()) {
|
||||
throw std::invalid_argument{
|
||||
"Full vector size must match the DofManager full dimension."};
|
||||
}
|
||||
Vector reduced{dofs.ConstrainedDofCount()};
|
||||
for (std::size_t index = 0U; index < dofs.ConstrainedDofs().size(); ++index) {
|
||||
reduced[index] = full[dofs.ConstrainedDofs()[index]];
|
||||
}
|
||||
return reduced;
|
||||
}
|
||||
|
||||
Vector EssentialConstraints::reconstructFull(
|
||||
const Vector& freeValues,
|
||||
const Vector& constrainedValues,
|
||||
const DofManager& dofs) {
|
||||
requireDofOrder(dofs);
|
||||
if (freeValues.Size() != dofs.freeDofCount() ||
|
||||
constrainedValues.Size() != dofs.constrainedDofCount()) {
|
||||
throw std::invalid_argument{
|
||||
"Reduced vector sizes must match the DofManager order."};
|
||||
}
|
||||
Vector EssentialConstraints::ReconstructFull(const Vector& free_values,
|
||||
const Vector& constrained_values,
|
||||
const DofManager& dofs) {
|
||||
RequireDofOrder(dofs);
|
||||
if (free_values.Size() != dofs.FreeDofCount() ||
|
||||
constrained_values.Size() != dofs.ConstrainedDofCount()) {
|
||||
throw std::invalid_argument{
|
||||
"Reduced vector sizes must match the DofManager order."};
|
||||
}
|
||||
|
||||
Vector full{dofs.fullDofCount()};
|
||||
for (std::size_t equation = 0U;
|
||||
equation < dofs.freeDofs().size();
|
||||
++equation) {
|
||||
full[dofs.freeDofs()[equation]] = freeValues[equation];
|
||||
}
|
||||
// Preserve caller-supplied dc exactly; nonzero prescribed displacement is
|
||||
// never replaced with an implicit homogeneous constraint.
|
||||
for (std::size_t index = 0U;
|
||||
index < dofs.constrainedDofs().size();
|
||||
++index) {
|
||||
full[dofs.constrainedDofs()[index]] = constrainedValues[index];
|
||||
}
|
||||
return full;
|
||||
Vector full{dofs.FullDofCount()};
|
||||
for (std::size_t equation = 0U; equation < dofs.FreeDofs().size();
|
||||
++equation) {
|
||||
full[dofs.FreeDofs()[equation]] = free_values[equation];
|
||||
}
|
||||
// Preserve caller-supplied dc exactly; nonzero prescribed displacement is
|
||||
// never replaced with an implicit homogeneous constraint.
|
||||
for (std::size_t index = 0U; index < dofs.ConstrainedDofs().size(); ++index) {
|
||||
full[dofs.ConstrainedDofs()[index]] = constrained_values[index];
|
||||
}
|
||||
return full;
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
+206
-221
@@ -1,4 +1,4 @@
|
||||
#include "fesa/fem/dof_manager.hpp"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
@@ -10,263 +10,248 @@
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t dofsPerNode = 6U;
|
||||
constexpr std::size_t kDofsPerNode = 6U;
|
||||
|
||||
char asciiLower(char value) {
|
||||
if (value >= 'A' && value <= 'Z') {
|
||||
return static_cast<char>(value + ('a' - 'A'));
|
||||
}
|
||||
return value;
|
||||
char AsciiLower(char value) {
|
||||
if (value >= 'A' && value <= 'Z') {
|
||||
return static_cast<char>(value + ('a' - 'A'));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool equalName(const std::string& left, const std::string& right) {
|
||||
return left.size() == right.size() &&
|
||||
std::equal(
|
||||
left.begin(), left.end(), right.begin(),
|
||||
[](char leftValue, char rightValue) {
|
||||
return asciiLower(leftValue) == asciiLower(rightValue);
|
||||
});
|
||||
bool EqualName(const std::string& left, const std::string& right) {
|
||||
return left.size() == right.size() &&
|
||||
std::equal(left.begin(), left.end(), right.begin(),
|
||||
[](char left_value, char right_value) {
|
||||
return AsciiLower(left_value) == AsciiLower(right_value);
|
||||
});
|
||||
}
|
||||
|
||||
bool tryPositiveInteger(const std::string& text, std::int64_t& value) {
|
||||
const char* const first = text.data();
|
||||
const char* const last = first + text.size();
|
||||
const auto parsed = std::from_chars(first, last, value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
|
||||
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
|
||||
const char* const first = text.data();
|
||||
const char* const last = first + text.size();
|
||||
const auto parsed = std::from_chars(first, last, value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
|
||||
}
|
||||
|
||||
std::vector<EntityIndex> expandBoundaryTarget(
|
||||
std::vector<EntityIndex> ExpandBoundaryTarget(
|
||||
const Domain& domain, const BoundaryCondition& boundary) {
|
||||
for (const auto& set : domain.NodeSets()) {
|
||||
if (equalName(set.name, boundary.target)) {
|
||||
return set.node_indices;
|
||||
for (const auto& set : domain.NodeSets()) {
|
||||
if (EqualName(set.name, boundary.target)) {
|
||||
return set.node_indices;
|
||||
}
|
||||
}
|
||||
|
||||
std::int64_t source_label = 0;
|
||||
if (TryPositiveInteger(boundary.target, source_label)) {
|
||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||
if (domain.Nodes()[node].source_id.source_label == source_label) {
|
||||
return {static_cast<EntityIndex>(node)};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
template <std::size_t scatter_size>
|
||||
void AppendScatter(std::vector<std::vector<std::size_t>>& columns_by_row,
|
||||
const std::array<std::size_t, scatter_size>& scatter) {
|
||||
for (const std::size_t row : scatter) {
|
||||
auto& columns = columns_by_row[row];
|
||||
columns.insert(columns.end(), scatter.begin(), scatter.end());
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Builds sorted unique CSR columns by deterministic scatter traversal.
|
||||
SparsePattern BuildSparsePattern(
|
||||
std::size_t full_dof_count, const std::vector<EntityIndex>& active_elements,
|
||||
const std::vector<std::array<std::size_t, 12>>& element_scatters,
|
||||
const std::vector<std::array<std::size_t, 24>>& shell_element_scatters) {
|
||||
std::vector<std::vector<std::size_t>> columns_by_row(full_dof_count);
|
||||
for (const EntityIndex element : active_elements) {
|
||||
AppendScatter(columns_by_row, element_scatters.at(element));
|
||||
}
|
||||
// Every shell in the approved single-step shell subset is active.
|
||||
for (const auto& scatter : shell_element_scatters) {
|
||||
AppendScatter(columns_by_row, scatter);
|
||||
}
|
||||
|
||||
SparsePattern pattern;
|
||||
pattern.row_offsets.reserve(full_dof_count + 1U);
|
||||
pattern.row_offsets.push_back(0U);
|
||||
for (auto& columns : columns_by_row) {
|
||||
// Stable CSR structure is independent of element traversal duplicates.
|
||||
std::sort(columns.begin(), columns.end());
|
||||
columns.erase(std::unique(columns.begin(), columns.end()), columns.end());
|
||||
pattern.column_indices.insert(pattern.column_indices.end(), columns.begin(),
|
||||
columns.end());
|
||||
pattern.row_offsets.push_back(pattern.column_indices.size());
|
||||
}
|
||||
return pattern;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<DofManager> DofManager::Create(const AnalysisModel& model) {
|
||||
const Domain& domain = model.GetDomain();
|
||||
const std::size_t full_count = domain.Nodes().size() * kDofsPerNode;
|
||||
|
||||
std::vector<std::optional<double>> prescribed_by_full_dof(full_count);
|
||||
for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) {
|
||||
const auto& boundary = model.Step().boundaries.at(boundary_index);
|
||||
const auto target = ExpandBoundaryTarget(domain, boundary);
|
||||
for (const EntityIndex node : target) {
|
||||
for (int component = boundary.first_dof; component <= boundary.last_dof;
|
||||
++component) {
|
||||
const std::size_t full_dof =
|
||||
static_cast<std::size_t>(node) * kDofsPerNode +
|
||||
static_cast<std::size_t>(component - 1);
|
||||
auto& prescribed = prescribed_by_full_dof[full_dof];
|
||||
if (prescribed && *prescribed != boundary.value) {
|
||||
return Result<DofManager>::Failure(Status::Failure(
|
||||
FailureCategory::kInput,
|
||||
{{Severity::kError, "conflicting-boundary-condition",
|
||||
boundary.location, "BOUNDARY", boundary.target,
|
||||
"Expanded boundary rows prescribe different values to one "
|
||||
"node/DOF."}}));
|
||||
}
|
||||
prescribed = boundary.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::int64_t sourceLabel = 0;
|
||||
if (tryPositiveInteger(boundary.target, sourceLabel)) {
|
||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||
if (domain.Nodes()[node].source_id.source_label == sourceLabel) {
|
||||
return {static_cast<EntityIndex>(node)};
|
||||
}
|
||||
}
|
||||
std::vector<std::size_t> free_dofs;
|
||||
std::vector<std::size_t> constrained_dofs;
|
||||
std::vector<double> constrained_values;
|
||||
std::vector<std::optional<std::size_t>> free_equations(full_count);
|
||||
free_dofs.reserve(full_count);
|
||||
constrained_dofs.reserve(full_count);
|
||||
constrained_values.reserve(full_count);
|
||||
// A full-DOF scan fixes free equations, constrained DOFs, and dc in the
|
||||
// same stable order regardless of boundary declaration overlap.
|
||||
for (std::size_t full_dof = 0U; full_dof < full_count; ++full_dof) {
|
||||
if (prescribed_by_full_dof[full_dof]) {
|
||||
constrained_dofs.push_back(full_dof);
|
||||
constrained_values.push_back(*prescribed_by_full_dof[full_dof]);
|
||||
} else {
|
||||
free_equations[full_dof] = free_dofs.size();
|
||||
free_dofs.push_back(full_dof);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Vector prescribed_values{constrained_values.size()};
|
||||
for (std::size_t index = 0U; index < constrained_values.size(); ++index) {
|
||||
prescribed_values[index] = constrained_values[index];
|
||||
}
|
||||
|
||||
std::vector<std::array<std::size_t, 12>> element_scatters(
|
||||
domain.Elements().size());
|
||||
for (const EntityIndex element_index : model.ActiveElements()) {
|
||||
const auto& element = domain.Elements().at(element_index);
|
||||
auto& scatter = element_scatters.at(element_index);
|
||||
for (std::size_t endpoint = 0U; endpoint < element.node_indices.size();
|
||||
++endpoint) {
|
||||
const std::size_t node = element.node_indices[endpoint];
|
||||
for (std::size_t component = 0U; component < kDofsPerNode; ++component) {
|
||||
scatter[endpoint * kDofsPerNode + component] =
|
||||
node * kDofsPerNode + component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::array<std::size_t, 24>> shell_element_scatters(
|
||||
domain.ShellElements().size());
|
||||
for (std::size_t element_index = 0U;
|
||||
element_index < domain.ShellElements().size(); ++element_index) {
|
||||
const auto& element = domain.ShellElements()[element_index];
|
||||
auto& scatter = shell_element_scatters[element_index];
|
||||
for (std::size_t node_position = 0U;
|
||||
node_position < element.node_indices.size(); ++node_position) {
|
||||
const std::size_t node = element.node_indices[node_position];
|
||||
for (std::size_t component = 0U; component < kDofsPerNode; ++component) {
|
||||
scatter[node_position * kDofsPerNode + component] =
|
||||
node * kDofsPerNode + component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pattern = BuildSparsePattern(full_count, model.ActiveElements(),
|
||||
element_scatters, shell_element_scatters);
|
||||
return Result<DofManager>::Success(
|
||||
DofManager{full_count, std::move(free_equations),
|
||||
std::move(element_scatters), std::move(shell_element_scatters),
|
||||
std::move(free_dofs), std::move(constrained_dofs),
|
||||
std::move(prescribed_values), std::move(pattern)});
|
||||
}
|
||||
|
||||
template <std::size_t scatterSize>
|
||||
void appendScatter(
|
||||
std::vector<std::vector<std::size_t>>& columnsByRow,
|
||||
const std::array<std::size_t, scatterSize>& scatter) {
|
||||
for (const std::size_t row : scatter) {
|
||||
auto& columns = columnsByRow[row];
|
||||
columns.insert(columns.end(), scatter.begin(), scatter.end());
|
||||
}
|
||||
std::size_t DofManager::FullDofCount() const noexcept {
|
||||
return full_dof_count_;
|
||||
}
|
||||
|
||||
SparsePattern buildSparsePattern(
|
||||
std::size_t fullDofCount,
|
||||
const std::vector<EntityIndex>& activeElements,
|
||||
const std::vector<std::array<std::size_t, 12>>& elementScatters,
|
||||
const std::vector<std::array<std::size_t, 24>>& shellElementScatters) {
|
||||
std::vector<std::vector<std::size_t>> columnsByRow(fullDofCount);
|
||||
for (const EntityIndex element : activeElements) {
|
||||
appendScatter(columnsByRow, elementScatters.at(element));
|
||||
}
|
||||
// Every shell in the approved single-step shell subset is active.
|
||||
for (const auto& scatter : shellElementScatters) {
|
||||
appendScatter(columnsByRow, scatter);
|
||||
}
|
||||
|
||||
SparsePattern pattern;
|
||||
pattern.rowOffsets.reserve(fullDofCount + 1U);
|
||||
pattern.rowOffsets.push_back(0U);
|
||||
for (auto& columns : columnsByRow) {
|
||||
// Stable CSR structure is independent of element traversal duplicates.
|
||||
std::sort(columns.begin(), columns.end());
|
||||
columns.erase(std::unique(columns.begin(), columns.end()), columns.end());
|
||||
pattern.columnIndices.insert(
|
||||
pattern.columnIndices.end(), columns.begin(), columns.end());
|
||||
pattern.rowOffsets.push_back(pattern.columnIndices.size());
|
||||
}
|
||||
return pattern;
|
||||
std::size_t DofManager::FreeDofCount() const noexcept {
|
||||
return free_dofs_.size();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<DofManager> DofManager::create(const AnalysisModel& model) {
|
||||
const Domain& domain = model.domain();
|
||||
const std::size_t fullCount = domain.Nodes().size() * dofsPerNode;
|
||||
|
||||
std::vector<std::optional<double>> prescribedByFullDof(fullCount);
|
||||
for (const EntityIndex boundaryIndex : model.activeBoundaryConditions()) {
|
||||
const auto& boundary = model.step().boundaries.at(boundaryIndex);
|
||||
const auto target = expandBoundaryTarget(domain, boundary);
|
||||
for (const EntityIndex node : target) {
|
||||
for (int component = boundary.first_dof;
|
||||
component <= boundary.last_dof;
|
||||
++component) {
|
||||
const std::size_t fullDof =
|
||||
static_cast<std::size_t>(node) * dofsPerNode +
|
||||
static_cast<std::size_t>(component - 1);
|
||||
auto& prescribed = prescribedByFullDof[fullDof];
|
||||
if (prescribed && *prescribed != boundary.value) {
|
||||
return Result<DofManager>::Failure(Status::Failure(
|
||||
FailureCategory::kInput,
|
||||
{{Severity::kError,
|
||||
"conflicting-boundary-condition",
|
||||
boundary.location,
|
||||
"BOUNDARY",
|
||||
boundary.target,
|
||||
"Expanded boundary rows prescribe different values to one node/DOF."}}));
|
||||
}
|
||||
prescribed = boundary.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::size_t> freeDofs;
|
||||
std::vector<std::size_t> constrainedDofs;
|
||||
std::vector<double> constrainedValues;
|
||||
std::vector<std::optional<std::size_t>> freeEquations(fullCount);
|
||||
freeDofs.reserve(fullCount);
|
||||
constrainedDofs.reserve(fullCount);
|
||||
constrainedValues.reserve(fullCount);
|
||||
// A full-DOF scan fixes free equations, constrained DOFs, and dc in the
|
||||
// same stable order regardless of boundary declaration overlap.
|
||||
for (std::size_t fullDof = 0U; fullDof < fullCount; ++fullDof) {
|
||||
if (prescribedByFullDof[fullDof]) {
|
||||
constrainedDofs.push_back(fullDof);
|
||||
constrainedValues.push_back(*prescribedByFullDof[fullDof]);
|
||||
} else {
|
||||
freeEquations[fullDof] = freeDofs.size();
|
||||
freeDofs.push_back(fullDof);
|
||||
}
|
||||
}
|
||||
|
||||
Vector prescribedValues{constrainedValues.size()};
|
||||
for (std::size_t index = 0U; index < constrainedValues.size(); ++index) {
|
||||
prescribedValues[index] = constrainedValues[index];
|
||||
}
|
||||
|
||||
std::vector<std::array<std::size_t, 12>> elementScatters(
|
||||
domain.Elements().size());
|
||||
for (const EntityIndex elementIndex : model.activeElements()) {
|
||||
const auto& element = domain.Elements().at(elementIndex);
|
||||
auto& scatter = elementScatters.at(elementIndex);
|
||||
for (std::size_t endpoint = 0U; endpoint < element.node_indices.size(); ++endpoint) {
|
||||
const std::size_t node = element.node_indices[endpoint];
|
||||
for (std::size_t component = 0U; component < dofsPerNode; ++component) {
|
||||
scatter[endpoint * dofsPerNode + component] =
|
||||
node * dofsPerNode + component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::array<std::size_t, 24>> shellElementScatters(
|
||||
domain.ShellElements().size());
|
||||
for (std::size_t elementIndex = 0U;
|
||||
elementIndex < domain.ShellElements().size();
|
||||
++elementIndex) {
|
||||
const auto& element = domain.ShellElements()[elementIndex];
|
||||
auto& scatter = shellElementScatters[elementIndex];
|
||||
for (std::size_t nodePosition = 0U;
|
||||
nodePosition < element.node_indices.size();
|
||||
++nodePosition) {
|
||||
const std::size_t node = element.node_indices[nodePosition];
|
||||
for (std::size_t component = 0U;
|
||||
component < dofsPerNode;
|
||||
++component) {
|
||||
scatter[nodePosition * dofsPerNode + component] =
|
||||
node * dofsPerNode + component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pattern = buildSparsePattern(
|
||||
fullCount,
|
||||
model.activeElements(),
|
||||
elementScatters,
|
||||
shellElementScatters);
|
||||
return Result<DofManager>::Success(DofManager{
|
||||
fullCount,
|
||||
std::move(freeEquations),
|
||||
std::move(elementScatters),
|
||||
std::move(shellElementScatters),
|
||||
std::move(freeDofs),
|
||||
std::move(constrainedDofs),
|
||||
std::move(prescribedValues),
|
||||
std::move(pattern)});
|
||||
std::size_t DofManager::ConstrainedDofCount() const noexcept {
|
||||
return constrained_dofs_.size();
|
||||
}
|
||||
|
||||
std::size_t DofManager::fullDofCount() const noexcept {
|
||||
return fullDofCount_;
|
||||
std::size_t DofManager::FullDof(EntityIndex node,
|
||||
DofComponent component) const {
|
||||
const std::size_t component_index = static_cast<std::size_t>(component);
|
||||
if (node >= full_dof_count_ / kDofsPerNode ||
|
||||
component_index >= kDofsPerNode) {
|
||||
throw std::out_of_range{"Node or DOF component is out of range."};
|
||||
}
|
||||
return static_cast<std::size_t>(node) * kDofsPerNode + component_index;
|
||||
}
|
||||
|
||||
std::size_t DofManager::freeDofCount() const noexcept {
|
||||
return freeDofs_.size();
|
||||
std::optional<std::size_t> DofManager::FreeEquation(
|
||||
std::size_t full_dof) const {
|
||||
return free_equations_.at(full_dof);
|
||||
}
|
||||
|
||||
std::size_t DofManager::constrainedDofCount() const noexcept {
|
||||
return constrainedDofs_.size();
|
||||
}
|
||||
|
||||
std::size_t DofManager::fullDof(
|
||||
EntityIndex node, DofComponent component) const {
|
||||
const std::size_t componentIndex = static_cast<std::size_t>(component);
|
||||
if (node >= fullDofCount_ / dofsPerNode || componentIndex >= dofsPerNode) {
|
||||
throw std::out_of_range{"Node or DOF component is out of range."};
|
||||
}
|
||||
return static_cast<std::size_t>(node) * dofsPerNode + componentIndex;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> DofManager::freeEquation(
|
||||
std::size_t fullDof) const {
|
||||
return freeEquations_.at(fullDof);
|
||||
}
|
||||
|
||||
const std::array<std::size_t, 12>& DofManager::elementScatter(
|
||||
const std::array<std::size_t, 12>& DofManager::ElementScatter(
|
||||
EntityIndex element) const {
|
||||
return elementScatters_.at(element);
|
||||
return element_scatters_.at(element);
|
||||
}
|
||||
|
||||
const std::array<std::size_t, 24>& DofManager::shellElementScatter(
|
||||
const std::array<std::size_t, 24>& DofManager::ShellElementScatter(
|
||||
EntityIndex element) const {
|
||||
return shellElementScatters_.at(element);
|
||||
return shell_element_scatters_.at(element);
|
||||
}
|
||||
|
||||
const std::vector<std::size_t>& DofManager::freeDofs() const noexcept {
|
||||
return freeDofs_;
|
||||
const std::vector<std::size_t>& DofManager::FreeDofs() const noexcept {
|
||||
return free_dofs_;
|
||||
}
|
||||
|
||||
const std::vector<std::size_t>& DofManager::constrainedDofs() const noexcept {
|
||||
return constrainedDofs_;
|
||||
const std::vector<std::size_t>& DofManager::ConstrainedDofs() const noexcept {
|
||||
return constrained_dofs_;
|
||||
}
|
||||
|
||||
const Vector& DofManager::prescribedValues() const noexcept {
|
||||
return prescribedValues_;
|
||||
const Vector& DofManager::PrescribedValues() const noexcept {
|
||||
return prescribed_values_;
|
||||
}
|
||||
|
||||
const SparsePattern& DofManager::sparsePattern() const noexcept {
|
||||
return sparsePattern_;
|
||||
const SparsePattern& DofManager::GetSparsePattern() const noexcept {
|
||||
return sparse_pattern_;
|
||||
}
|
||||
|
||||
DofManager::DofManager(
|
||||
std::size_t fullDofCount,
|
||||
std::vector<std::optional<std::size_t>> freeEquations,
|
||||
std::vector<std::array<std::size_t, 12>> elementScatters,
|
||||
std::vector<std::array<std::size_t, 24>> shellElementScatters,
|
||||
std::vector<std::size_t> freeDofs,
|
||||
std::vector<std::size_t> constrainedDofs,
|
||||
Vector prescribedValues,
|
||||
SparsePattern sparsePattern)
|
||||
: fullDofCount_{fullDofCount},
|
||||
freeEquations_{std::move(freeEquations)},
|
||||
elementScatters_{std::move(elementScatters)},
|
||||
shellElementScatters_{std::move(shellElementScatters)},
|
||||
freeDofs_{std::move(freeDofs)},
|
||||
constrainedDofs_{std::move(constrainedDofs)},
|
||||
prescribedValues_{std::move(prescribedValues)},
|
||||
sparsePattern_{std::move(sparsePattern)} {}
|
||||
std::size_t full_dof_count,
|
||||
std::vector<std::optional<std::size_t>> free_equations,
|
||||
std::vector<std::array<std::size_t, 12>> element_scatters,
|
||||
std::vector<std::array<std::size_t, 24>> shell_element_scatters,
|
||||
std::vector<std::size_t> free_dofs,
|
||||
std::vector<std::size_t> constrained_dofs, Vector prescribed_values,
|
||||
SparsePattern sparse_pattern)
|
||||
: full_dof_count_{full_dof_count},
|
||||
free_equations_{std::move(free_equations)},
|
||||
element_scatters_{std::move(element_scatters)},
|
||||
shell_element_scatters_{std::move(shell_element_scatters)},
|
||||
free_dofs_{std::move(free_dofs)},
|
||||
constrained_dofs_{std::move(constrained_dofs)},
|
||||
prescribed_values_{std::move(prescribed_values)},
|
||||
sparse_pattern_{std::move(sparse_pattern)} {}
|
||||
|
||||
} // namespace fesa
|
||||
} // namespace fesa
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
#include "fesa/io/hdf5/hdf5_results_writer.hpp"
|
||||
|
||||
#include "fesa/analysis/analysis_model.hpp"
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/build_info.h"
|
||||
#include "fesa/fem/dof_manager.hpp"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
|
||||
#include <hdf5.h>
|
||||
|
||||
@@ -389,38 +389,38 @@ Status validateShellWriterInput(
|
||||
std::size_t expectedRows = 0U;
|
||||
if (!sizeProductFits(
|
||||
domain.ShellElements().size(), kShellLocationCount, expectedRows) ||
|
||||
state.shellResults().size() != expectedRows) {
|
||||
state.ShellResults().size() != expectedRows) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Shell output requires exactly GP1 through GP4 for every shell element.");
|
||||
}
|
||||
const double gauss = 1.0 / std::sqrt(3.0);
|
||||
const std::array<ShellMidsurfaceLocation, kShellLocationCount> locations{
|
||||
ShellMidsurfaceLocation::gp1,
|
||||
ShellMidsurfaceLocation::gp2,
|
||||
ShellMidsurfaceLocation::gp3,
|
||||
ShellMidsurfaceLocation::gp4};
|
||||
ShellMidsurfaceLocation::kGp1,
|
||||
ShellMidsurfaceLocation::kGp2,
|
||||
ShellMidsurfaceLocation::kGp3,
|
||||
ShellMidsurfaceLocation::kGp4};
|
||||
const std::array<std::array<double, 2>, kShellLocationCount> coordinates{{
|
||||
{-gauss, -gauss},
|
||||
{gauss, -gauss},
|
||||
{gauss, gauss},
|
||||
{-gauss, gauss}}};
|
||||
const std::array<ShellSectionPosition, kShellSectionPositionCount> positions{
|
||||
ShellSectionPosition::bottom,
|
||||
ShellSectionPosition::middle,
|
||||
ShellSectionPosition::top};
|
||||
ShellSectionPosition::kBottom,
|
||||
ShellSectionPosition::kMiddle,
|
||||
ShellSectionPosition::kTop};
|
||||
constexpr std::array<double, kShellSectionPositionCount> zeta{-1.0, 0.0, 1.0};
|
||||
for (std::size_t rowIndex = 0U;
|
||||
rowIndex < state.shellResults().size();
|
||||
rowIndex < state.ShellResults().size();
|
||||
++rowIndex) {
|
||||
const auto& row = state.shellResults()[rowIndex];
|
||||
const auto& row = state.ShellResults()[rowIndex];
|
||||
const std::size_t element = rowIndex / kShellLocationCount;
|
||||
const std::size_t location = rowIndex % kShellLocationCount;
|
||||
if (row.element != element || row.location != locations[location] ||
|
||||
row.naturalCoordinates != coordinates[location] ||
|
||||
!isOrthonormalRightHanded(row.localFrame) ||
|
||||
!isFinite(row.generalizedStrain) ||
|
||||
!isFinite(row.sectionResultant)) {
|
||||
row.natural_coordinates != coordinates[location] ||
|
||||
!isOrthonormalRightHanded(row.local_frame) ||
|
||||
!isFinite(row.generalized_strain) ||
|
||||
!isFinite(row.section_resultant)) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Shell result rows must be finite and preserve element/GP/frame identity.");
|
||||
@@ -437,9 +437,9 @@ Status validateShellWriterInput(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!std::isfinite(state.physicalStrainEnergy()) ||
|
||||
!isFinite(state.equilibrium()) ||
|
||||
!isFinite(state.verificationMetrics())) {
|
||||
if (!std::isfinite(state.PhysicalStrainEnergy()) ||
|
||||
!isFinite(state.Equilibrium()) ||
|
||||
!isFinite(state.VerificationMetrics())) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Shell energy, equilibrium, and verification metrics must be finite.");
|
||||
@@ -457,8 +457,8 @@ Status validateWriterInput(
|
||||
return outputFailure(
|
||||
"invalid-output-path", "The HDF5 output path must name a file.");
|
||||
}
|
||||
if (state.identity().stepName != kStepName ||
|
||||
state.identity().frameIndex != kFrameIndex) {
|
||||
if (state.Identity().step_name != kStepName ||
|
||||
state.Identity().frame_index != kFrameIndex) {
|
||||
return outputFailure(
|
||||
"invalid-result-state",
|
||||
"Schema v0 requires literal Step-1 and frame index 0.");
|
||||
@@ -470,7 +470,7 @@ Status validateWriterInput(
|
||||
if (!shellValidation.IsOk()) {
|
||||
return shellValidation;
|
||||
}
|
||||
} else if (!state.shellResults().empty()) {
|
||||
} else if (!state.ShellResults().empty()) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Beam output cannot contain shell recovery rows.");
|
||||
@@ -482,11 +482,11 @@ Status validateWriterInput(
|
||||
"invalid-result-state", "The nodal result shape overflows size_t.");
|
||||
}
|
||||
const std::array<const Vector*, 5> vectors = {
|
||||
&state.displacement(),
|
||||
&state.externalForce(),
|
||||
&state.internalForce(),
|
||||
&state.residual(),
|
||||
&state.reaction()};
|
||||
&state.Displacement(),
|
||||
&state.ExternalForce(),
|
||||
&state.InternalForce(),
|
||||
&state.Residual(),
|
||||
&state.Reaction()};
|
||||
for (const Vector* vector : vectors) {
|
||||
if (vector->Size() != fullDofCount) {
|
||||
return outputFailure(
|
||||
@@ -542,42 +542,42 @@ Status validateWriterInput(
|
||||
std::size_t gaussCount = 0U;
|
||||
if (!sizeProductFits(domain.Elements().size(), kEndpointCount, endpointCount) ||
|
||||
!sizeProductFits(domain.Elements().size(), kGaussPointCount, gaussCount) ||
|
||||
state.endpointResults().size() != endpointCount ||
|
||||
state.gaussResults().size() != gaussCount) {
|
||||
state.EndpointResults().size() != endpointCount ||
|
||||
state.GaussResults().size() != gaussCount) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Endpoint and Gauss row counts must match every element and location.");
|
||||
}
|
||||
for (std::size_t rowIndex = 0U;
|
||||
rowIndex < state.endpointResults().size();
|
||||
rowIndex < state.EndpointResults().size();
|
||||
++rowIndex) {
|
||||
const EntityIndex expectedElement =
|
||||
static_cast<EntityIndex>(rowIndex / kEndpointCount);
|
||||
const int expectedEndpoint = static_cast<int>(rowIndex % kEndpointCount);
|
||||
const EndpointResultRow& row = state.endpointResults()[rowIndex];
|
||||
const EndpointResultRow& row = state.EndpointResults()[rowIndex];
|
||||
const auto& element = domain.Elements()[expectedElement];
|
||||
const auto& expectedNode =
|
||||
domain.Nodes()[element.node_indices[static_cast<std::size_t>(expectedEndpoint)]];
|
||||
if (row.element != expectedElement || row.endpoint != expectedEndpoint ||
|
||||
!sameIdentity(row.node, expectedNode.source_id) ||
|
||||
!isFinite(row.endAction) || !isFinite(row.sectionResultant)) {
|
||||
!isFinite(row.end_action) || !isFinite(row.section_resultant)) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Endpoint result rows must follow element/endpoint order and identity.");
|
||||
}
|
||||
}
|
||||
for (std::size_t rowIndex = 0U;
|
||||
rowIndex < state.gaussResults().size();
|
||||
rowIndex < state.GaussResults().size();
|
||||
++rowIndex) {
|
||||
const EntityIndex expectedElement =
|
||||
static_cast<EntityIndex>(rowIndex / kGaussPointCount);
|
||||
const int expectedGaussPoint =
|
||||
static_cast<int>(rowIndex % kGaussPointCount) + 1;
|
||||
const GaussResultRow& row = state.gaussResults()[rowIndex];
|
||||
const GaussResultRow& row = state.GaussResults()[rowIndex];
|
||||
if (row.element != expectedElement ||
|
||||
row.gaussPoint != expectedGaussPoint ||
|
||||
!isFinite(row.generalizedStrain) ||
|
||||
!isFinite(row.generalizedResultant)) {
|
||||
row.gauss_point != expectedGaussPoint ||
|
||||
!isFinite(row.generalized_strain) ||
|
||||
!isFinite(row.generalized_resultant)) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Gauss result rows must follow element/Gauss order and identity.");
|
||||
@@ -594,19 +594,19 @@ Status validateWriterInput(
|
||||
for (std::size_t gauss = 0U; gauss < kGaussPointCount; ++gauss) {
|
||||
const std::size_t count = sectionPoints.empty() ? 1U : sectionPoints.size();
|
||||
for (std::size_t point = 0U; point < count; ++point) {
|
||||
if (stressIndex >= state.stressResults().size()) {
|
||||
if (stressIndex >= state.StressResults().size()) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows",
|
||||
"Axial stress rows are missing required element/Gauss/section locations.");
|
||||
}
|
||||
const StressS11Row& row = state.stressResults()[stressIndex++];
|
||||
const StressS11Row& row = state.StressResults()[stressIndex++];
|
||||
const std::size_t expectedPoint = sectionPoints.empty() ? 0U : point + 1U;
|
||||
const double expectedX1 = sectionPoints.empty() ? 0.0 : sectionPoints[point][0U];
|
||||
const double expectedX2 = sectionPoints.empty() ? 0.0 : sectionPoints[point][1U];
|
||||
const char* expectedSource = sectionPoints.empty() ? "fesa-default" : "input";
|
||||
if (row.element != static_cast<EntityIndex>(elementIndex) ||
|
||||
row.gaussPoint != static_cast<int>(gauss + 1U) ||
|
||||
row.sectionPoint != expectedPoint ||
|
||||
row.gauss_point != static_cast<int>(gauss + 1U) ||
|
||||
row.section_point != expectedPoint ||
|
||||
row.x1 != expectedX1 || row.x2 != expectedX2 ||
|
||||
row.source != expectedSource || !isValidUtf8(row.source) ||
|
||||
!std::isfinite(row.x1) || !std::isfinite(row.x2) ||
|
||||
@@ -618,7 +618,7 @@ Status validateWriterInput(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stressIndex != state.stressResults().size()) {
|
||||
if (stressIndex != state.StressResults().size()) {
|
||||
return outputFailure(
|
||||
"invalid-result-rows", "Axial stress output contains extra rows.");
|
||||
}
|
||||
@@ -635,7 +635,7 @@ Status validateWriterInput(
|
||||
}
|
||||
|
||||
|
||||
auto analysisModelResult = AnalysisModel::create(domain);
|
||||
auto analysisModelResult = AnalysisModel::Create(domain);
|
||||
if (!analysisModelResult.HasValue()) {
|
||||
return outputFailure(
|
||||
"invalid-result-state",
|
||||
@@ -643,7 +643,7 @@ Status validateWriterInput(
|
||||
}
|
||||
const AnalysisModel analysisModel =
|
||||
std::move(analysisModelResult.Value());
|
||||
auto dofResult = DofManager::create(analysisModel);
|
||||
auto dofResult = DofManager::Create(analysisModel);
|
||||
if (!dofResult.HasValue()) {
|
||||
return outputFailure(
|
||||
"invalid-result-state",
|
||||
@@ -652,16 +652,16 @@ Status validateWriterInput(
|
||||
const DofManager dofs = std::move(dofResult.Value());
|
||||
modelData.constraintMask.assign(fullDofCount, 0U);
|
||||
modelData.prescribedDisplacement.assign(fullDofCount, 0.0);
|
||||
if (dofs.constrainedDofs().size() != dofs.prescribedValues().Size()) {
|
||||
if (dofs.ConstrainedDofs().size() != dofs.PrescribedValues().Size()) {
|
||||
return outputFailure(
|
||||
"invalid-result-state",
|
||||
"Constraint identities and prescribed values have inconsistent sizes.");
|
||||
}
|
||||
for (std::size_t index = 0U;
|
||||
index < dofs.constrainedDofs().size();
|
||||
index < dofs.ConstrainedDofs().size();
|
||||
++index) {
|
||||
const std::size_t fullDof = dofs.constrainedDofs()[index];
|
||||
const double prescribed = dofs.prescribedValues()[index];
|
||||
const std::size_t fullDof = dofs.ConstrainedDofs()[index];
|
||||
const double prescribed = dofs.PrescribedValues()[index];
|
||||
if (fullDof >= fullDofCount || !std::isfinite(prescribed)) {
|
||||
return outputFailure(
|
||||
"invalid-result-state",
|
||||
@@ -1422,9 +1422,9 @@ std::vector<double> flattenEndpointValues(
|
||||
for (const auto& row : rows) {
|
||||
if (sectionResultants) {
|
||||
values.insert(
|
||||
values.end(), row.sectionResultant.begin(), row.sectionResultant.end());
|
||||
values.end(), row.section_resultant.begin(), row.section_resultant.end());
|
||||
} else {
|
||||
values.insert(values.end(), row.endAction.begin(), row.endAction.end());
|
||||
values.insert(values.end(), row.end_action.begin(), row.end_action.end());
|
||||
}
|
||||
}
|
||||
return values;
|
||||
@@ -1437,7 +1437,7 @@ std::vector<double> flattenGaussValues(
|
||||
values.reserve(rows.size() * kGeneralizedComponentCount);
|
||||
for (const auto& row : rows) {
|
||||
const auto& rowValues =
|
||||
resultants ? row.generalizedResultant : row.generalizedStrain;
|
||||
resultants ? row.generalized_resultant : row.generalized_strain;
|
||||
values.insert(values.end(), rowValues.begin(), rowValues.end());
|
||||
}
|
||||
return values;
|
||||
@@ -1445,12 +1445,12 @@ std::vector<double> flattenGaussValues(
|
||||
|
||||
void writeStress(const hid_t file, const AnalysisState& state) {
|
||||
std::vector<StressWriteRow> rows;
|
||||
rows.reserve(state.stressResults().size());
|
||||
for (const auto& row : state.stressResults()) {
|
||||
rows.reserve(state.StressResults().size());
|
||||
for (const auto& row : state.StressResults()) {
|
||||
rows.push_back({
|
||||
static_cast<std::uint64_t>(row.element),
|
||||
static_cast<std::uint64_t>(row.gaussPoint),
|
||||
static_cast<std::uint64_t>(row.sectionPoint),
|
||||
static_cast<std::uint64_t>(row.gauss_point),
|
||||
static_cast<std::uint64_t>(row.section_point),
|
||||
row.x1,
|
||||
row.x2,
|
||||
row.source.c_str(),
|
||||
@@ -1544,26 +1544,26 @@ void writeShellResultDatasets(
|
||||
std::vector<double> generalizedStrains;
|
||||
std::vector<double> sectionResultants;
|
||||
std::vector<double> stresses;
|
||||
localFrames.reserve(state.shellResults().size() * 9U);
|
||||
localFrames.reserve(state.ShellResults().size() * 9U);
|
||||
generalizedStrains.reserve(
|
||||
state.shellResults().size() * kShellGeneralizedComponentCount);
|
||||
state.ShellResults().size() * kShellGeneralizedComponentCount);
|
||||
sectionResultants.reserve(
|
||||
state.shellResults().size() * kShellGeneralizedComponentCount);
|
||||
state.ShellResults().size() * kShellGeneralizedComponentCount);
|
||||
stresses.reserve(
|
||||
state.shellResults().size() * kShellSectionPositionCount *
|
||||
state.ShellResults().size() * kShellSectionPositionCount *
|
||||
kShellStressComponentCount);
|
||||
for (const auto& row : state.shellResults()) {
|
||||
for (const auto& axis : row.localFrame) {
|
||||
for (const auto& row : state.ShellResults()) {
|
||||
for (const auto& axis : row.local_frame) {
|
||||
localFrames.insert(localFrames.end(), axis.begin(), axis.end());
|
||||
}
|
||||
generalizedStrains.insert(
|
||||
generalizedStrains.end(),
|
||||
row.generalizedStrain.begin(),
|
||||
row.generalizedStrain.end());
|
||||
row.generalized_strain.begin(),
|
||||
row.generalized_strain.end());
|
||||
sectionResultants.insert(
|
||||
sectionResultants.end(),
|
||||
row.sectionResultant.begin(),
|
||||
row.sectionResultant.end());
|
||||
row.section_resultant.begin(),
|
||||
row.section_resultant.end());
|
||||
for (const auto& position : row.stress) {
|
||||
stresses.insert(
|
||||
stresses.end(),
|
||||
@@ -1624,22 +1624,22 @@ void writeShellResultDatasets(
|
||||
"shell-local", "section-position");
|
||||
writeShellResultIdentity(file, stressPath, true);
|
||||
|
||||
const double energy = state.physicalStrainEnergy();
|
||||
const double energy = state.PhysicalStrainEnergy();
|
||||
writeDoubleDataset(
|
||||
file, std::string{kStepRoot} + "/global/energy", {1U},
|
||||
&energy, 1U, "PHYSICAL_STRAIN_ENERGY", "force*length",
|
||||
"global", "global");
|
||||
writeDoubleDataset(
|
||||
file, std::string{kStepRoot} + "/global/equilibrium", {6U},
|
||||
state.equilibrium().data(), state.equilibrium().size(),
|
||||
state.Equilibrium().data(), state.Equilibrium().size(),
|
||||
"FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3",
|
||||
"force,force,force,force*length,force*length,force*length",
|
||||
"global-cartesian", "global-origin");
|
||||
const std::string metricsPath =
|
||||
std::string{kStepRoot} + "/global/verification_metrics";
|
||||
writeDoubleDataset(
|
||||
file, metricsPath, {3U}, state.verificationMetrics().data(),
|
||||
state.verificationMetrics().size(),
|
||||
file, metricsPath, {3U}, state.VerificationMetrics().data(),
|
||||
state.VerificationMetrics().size(),
|
||||
"FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_NORMALIZED",
|
||||
"1,1,1", "global", "verification");
|
||||
{
|
||||
@@ -1735,8 +1735,8 @@ void writeResultDatasets(
|
||||
file,
|
||||
std::string{kStepRoot} + "/nodal/displacement",
|
||||
nodalDimensions,
|
||||
state.displacement().Data(),
|
||||
state.displacement().Size(),
|
||||
state.Displacement().Data(),
|
||||
state.Displacement().Size(),
|
||||
"UX,UY,UZ,URX,URY,URZ",
|
||||
"length,length,length,radian,radian,radian",
|
||||
"global-cartesian",
|
||||
@@ -1745,8 +1745,8 @@ void writeResultDatasets(
|
||||
file,
|
||||
std::string{kStepRoot} + "/nodal/reaction",
|
||||
nodalDimensions,
|
||||
state.reaction().Data(),
|
||||
state.reaction().Size(),
|
||||
state.Reaction().Data(),
|
||||
state.Reaction().Size(),
|
||||
"RF1,RF2,RF3,RM1,RM2,RM3",
|
||||
"force,force,force,force*length,force*length,force*length",
|
||||
"global-cartesian",
|
||||
@@ -1765,7 +1765,7 @@ void writeResultDatasets(
|
||||
static_cast<hsize_t>(domain.Elements().size()),
|
||||
kEndpointCount,
|
||||
kGeneralizedComponentCount};
|
||||
const auto endActions = flattenEndpointValues(state.endpointResults(), false);
|
||||
const auto endActions = flattenEndpointValues(state.EndpointResults(), false);
|
||||
writeDoubleDataset(
|
||||
file,
|
||||
std::string{kStepRoot} + "/element/end_force_local",
|
||||
@@ -1777,7 +1777,7 @@ void writeResultDatasets(
|
||||
"beam-local",
|
||||
"endpoint-outward-action");
|
||||
const auto sectionResultants =
|
||||
flattenEndpointValues(state.endpointResults(), true);
|
||||
flattenEndpointValues(state.EndpointResults(), true);
|
||||
writeDoubleDataset(
|
||||
file,
|
||||
std::string{kStepRoot} + "/element/section_resultant",
|
||||
@@ -1789,7 +1789,7 @@ void writeResultDatasets(
|
||||
"beam-local",
|
||||
"endpoint-positive-local-x-section-cut");
|
||||
const auto generalizedStrains =
|
||||
flattenGaussValues(state.gaussResults(), false);
|
||||
flattenGaussValues(state.GaussResults(), false);
|
||||
writeDoubleDataset(
|
||||
file,
|
||||
std::string{kStepRoot} + "/element/generalized_strain",
|
||||
@@ -1801,7 +1801,7 @@ void writeResultDatasets(
|
||||
"beam-local",
|
||||
"integration-point");
|
||||
const auto generalizedResultants =
|
||||
flattenGaussValues(state.gaussResults(), true);
|
||||
flattenGaussValues(state.GaussResults(), true);
|
||||
writeDoubleDataset(
|
||||
file,
|
||||
std::string{kStepRoot} + "/element/generalized_resultant",
|
||||
@@ -2494,7 +2494,7 @@ void selfCheckFile(
|
||||
"beam-local", "integration-point");
|
||||
requireCompoundDataset(
|
||||
file.get(), "/steps/Step-1/frames/0/element/stress_s11",
|
||||
static_cast<hsize_t>(state.stressResults().size()),
|
||||
static_cast<hsize_t>(state.StressResults().size()),
|
||||
{"internal_element_id", "gauss_point_index", "section_point_index",
|
||||
"x1", "x2", "source", "S11"});
|
||||
auto stress = openDatasetForCheck(
|
||||
@@ -2563,7 +2563,7 @@ bool finalizeFile(
|
||||
|
||||
} // namespace
|
||||
|
||||
Status Hdf5ResultsWriter::write(
|
||||
Status Hdf5ResultsWriter::Write(
|
||||
const std::filesystem::path& outputPath,
|
||||
const Domain& domain,
|
||||
const AnalysisState& state,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/fem/dof_manager.hpp"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
@@ -79,8 +79,8 @@ Result<SparseMatrix> SparseMatrix::FromCoo(
|
||||
std::vector<CooContribution> contributions,
|
||||
const SparsePattern& expected_pattern) {
|
||||
const Status pattern_status =
|
||||
ValidateCsr(rows, columns, expected_pattern.rowOffsets,
|
||||
expected_pattern.columnIndices, nullptr);
|
||||
ValidateCsr(rows, columns, expected_pattern.row_offsets,
|
||||
expected_pattern.column_indices, nullptr);
|
||||
if (!pattern_status.IsOk()) {
|
||||
return Result<SparseMatrix>::Failure(pattern_status);
|
||||
}
|
||||
@@ -113,12 +113,12 @@ Result<SparseMatrix> SparseMatrix::FromCoo(
|
||||
right.local_order);
|
||||
});
|
||||
|
||||
std::vector<double> values(expected_pattern.columnIndices.size(), 0.0);
|
||||
std::vector<double> values(expected_pattern.column_indices.size(), 0.0);
|
||||
for (const auto& contribution : contributions) {
|
||||
const std::size_t begin = expected_pattern.rowOffsets[contribution.row];
|
||||
const std::size_t end = expected_pattern.rowOffsets[contribution.row + 1U];
|
||||
const auto first = expected_pattern.columnIndices.begin() + begin;
|
||||
const auto last = expected_pattern.columnIndices.begin() + end;
|
||||
const std::size_t begin = expected_pattern.row_offsets[contribution.row];
|
||||
const std::size_t end = expected_pattern.row_offsets[contribution.row + 1U];
|
||||
const auto first = expected_pattern.column_indices.begin() + begin;
|
||||
const auto last = expected_pattern.column_indices.begin() + end;
|
||||
const auto found = std::lower_bound(first, last, contribution.column);
|
||||
if (found == last || *found != contribution.column) {
|
||||
return Result<SparseMatrix>::Failure(SparseFailure(
|
||||
@@ -129,7 +129,7 @@ Result<SparseMatrix> SparseMatrix::FromCoo(
|
||||
}
|
||||
|
||||
const std::size_t position = static_cast<std::size_t>(
|
||||
std::distance(expected_pattern.columnIndices.begin(), found));
|
||||
std::distance(expected_pattern.column_indices.begin(), found));
|
||||
values[position] += contribution.value;
|
||||
if (!std::isfinite(values[position])) {
|
||||
return Result<SparseMatrix>::Failure(SparseFailure(
|
||||
@@ -140,8 +140,8 @@ Result<SparseMatrix> SparseMatrix::FromCoo(
|
||||
}
|
||||
}
|
||||
|
||||
SparseMatrix matrix{rows, columns, expected_pattern.rowOffsets,
|
||||
expected_pattern.columnIndices, std::move(values)};
|
||||
SparseMatrix matrix{rows, columns, expected_pattern.row_offsets,
|
||||
expected_pattern.column_indices, std::move(values)};
|
||||
const Status status = matrix.Validate();
|
||||
if (!status.IsOk()) {
|
||||
return Result<SparseMatrix>::Failure(status);
|
||||
|
||||
+914
-1036
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user