From 19ba02a6a4ac51d8457016348a031516c4f5fa2c Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 16 Aug 2026 09:10:38 +0900 Subject: [PATCH] feat(cpp-object-oriented-modular-refactoring): step 13 - element-definition-domain --- include/fesa/analysis/analysis_model.h | 10 +- include/fesa/elements/element_definition.h | 42 ++++++ include/fesa/elements/euler_beam_3d.h | 39 +++++- include/fesa/elements/mitc4_shell.h | 52 +++++++- .../isotropic_linear_elastic_material.h | 3 + include/fesa/model/domain.h | 72 +++++++++- include/fesa/model/model_types.h | 31 +---- src/fesa/analysis/analysis_model.cpp | 30 ++++- src/fesa/assembly/load_assembler.cpp | 2 +- src/fesa/assembly/sparse_assembler.cpp | 56 ++++---- src/fesa/elements/euler_beam_3d.cpp | 43 ++++++ src/fesa/elements/mitc4_shell.cpp | 52 ++++++++ src/fesa/fem/dof_manager.cpp | 12 +- src/fesa/io/hdf5/hdf5_results_writer.cpp | 98 +++++++------- src/fesa/model/domain.cpp | 94 +++++++++++-- src/fesa/model/source_target_resolver.cpp | 18 +-- src/fesa/results/result_recovery.cpp | 67 +++++----- .../analysis/linear_static_analysis_test.cpp | 2 +- tests/reference/reference_comparison.cpp | 4 +- tests/unit/analysis/analysis_model_test.cpp | 50 ++++++- tests/unit/assembly/sparse_assembler_test.cpp | 6 +- tests/unit/elements/euler_beam_3d_test.cpp | 1 + tests/unit/elements/mitc4_shell_test.cpp | 1 + tests/unit/io/abaqus/domain_mapper_test.cpp | 71 ++++++---- tests/unit/model/domain_test.cpp | 124 +++++++++++++++--- tests/unit/results/result_recovery_test.cpp | 2 +- 26 files changed, 740 insertions(+), 242 deletions(-) create mode 100644 include/fesa/elements/element_definition.h diff --git a/include/fesa/analysis/analysis_model.h b/include/fesa/analysis/analysis_model.h index 83c001e..10c543a 100644 --- a/include/fesa/analysis/analysis_model.h +++ b/include/fesa/analysis/analysis_model.h @@ -23,12 +23,18 @@ class AnalysisModel { /// @brief Returns the sole active static step. const StaticStepDefinition& Step() const noexcept; - /// @brief Returns active beam element indices in stable internal order. + /// @brief Returns active element-definition indices in stable Domain order. const std::vector& ActiveElements() const noexcept; + /// @brief Returns active B33 indices in their concrete compatibility view. + const std::vector& ActiveBeamElements() const noexcept; + /// @brief Returns reachable material indices in stable internal order. const std::vector& ActiveMaterials() const noexcept; + /// @brief Returns reachable property indices in stable internal order. + const std::vector& ActiveProperties() const noexcept; + /// @brief Returns reachable beam-section indices in stable internal order. const std::vector& ActiveSections() const noexcept; @@ -44,7 +50,9 @@ class AnalysisModel { const Domain* domain_; std::vector active_elements_; + std::vector active_beam_elements_; std::vector active_materials_; + std::vector active_properties_; std::vector active_sections_; std::vector active_boundary_conditions_; std::vector active_loads_; diff --git a/include/fesa/elements/element_definition.h b/include/fesa/elements/element_definition.h new file mode 100644 index 0000000..b8baca6 --- /dev/null +++ b/include/fesa/elements/element_definition.h @@ -0,0 +1,42 @@ +#ifndef FESA_ELEMENTS_ELEMENT_DEFINITION_H_ +#define FESA_ELEMENTS_ELEMENT_DEFINITION_H_ + +#include +#include + +#include "fesa/core/source_identity.h" + +namespace fesa { + +/// @brief Identifies the supported semantic element-definition kinds. +enum class ElementDefinitionKind { kEulerBeam3D, kMitc4Shell }; + +/// @brief Provides immutable source identity and topology for one element. +/// @note Numerical stiffness, recovery, and equation ids are intentionally +/// excluded from this semantic interface. +class ElementDefinition { + public: + virtual ~ElementDefinition() = default; + + /// @brief Returns the concrete semantic definition kind. + virtual ElementDefinitionKind Kind() const noexcept = 0; + + /// @brief Returns the stable external source identity. + virtual const SourceEntityId& SourceId() const noexcept = 0; + + /// @brief Returns the preserved source element type such as B33 or S4R. + virtual std::string_view SourceElementType() const noexcept = 0; + + /// @brief Returns stable Domain node collection positions. + virtual const std::vector& NodeIndices() const noexcept = 0; + + /// @brief Returns the stable Domain material collection position. + virtual EntityIndex MaterialIndex() const noexcept = 0; + + /// @brief Returns the stable Domain property collection position. + virtual EntityIndex PropertyIndex() const noexcept = 0; +}; + +} // namespace fesa + +#endif // FESA_ELEMENTS_ELEMENT_DEFINITION_H_ diff --git a/include/fesa/elements/euler_beam_3d.h b/include/fesa/elements/euler_beam_3d.h index 5b6bec7..11ee089 100644 --- a/include/fesa/elements/euler_beam_3d.h +++ b/include/fesa/elements/euler_beam_3d.h @@ -4,15 +4,52 @@ #include #include #include +#include #include #include "fesa/core/status.h" +#include "fesa/elements/element_definition.h" +#include "fesa/materials/isotropic_linear_elastic_material.h" #include "fesa/math/matrix.h" #include "fesa/math/vector.h" -#include "fesa/model/model_types.h" +#include "fesa/properties/general_beam_section.h" namespace fesa { +class Domain; +struct Node; + +/// @brief Defines one two-node B33 semantic element. +class EulerBeam3DDefinition final : public ElementDefinition { + public: + /// @brief Constructs a parser-validated semantic definition. + EulerBeam3DDefinition(SourceEntityId source_id, + std::array node_indices, + EntityIndex material_index, EntityIndex section_index, + SourceLocation location); + + ElementDefinitionKind Kind() const noexcept override; + const SourceEntityId& SourceId() const noexcept override; + std::string_view SourceElementType() const noexcept override; + const std::vector& NodeIndices() const noexcept override; + EntityIndex MaterialIndex() const noexcept override; + EntityIndex PropertyIndex() const noexcept override; + + SourceEntityId source_id; + std::array node_indices; + EntityIndex material_index; + EntityIndex section_index; + SourceLocation location; + + private: + friend class Domain; + + /// @brief Synchronizes the base view after parser-candidate construction. + void SynchronizeNodeIndices(); + + std::vector node_indices_view_; +}; + /// @brief Stores constant line-load components in the beam local frame. struct ConstantLocalLineLoad { double px; diff --git a/include/fesa/elements/mitc4_shell.h b/include/fesa/elements/mitc4_shell.h index f0dc0fd..2e63615 100644 --- a/include/fesa/elements/mitc4_shell.h +++ b/include/fesa/elements/mitc4_shell.h @@ -3,15 +3,65 @@ #include #include +#include +#include #include "fesa/core/status.h" +#include "fesa/elements/element_definition.h" +#include "fesa/materials/isotropic_linear_elastic_material.h" #include "fesa/math/matrix.h" #include "fesa/math/vector.h" #include "fesa/math/vector3.h" -#include "fesa/model/model_types.h" +#include "fesa/properties/shell_section.h" namespace fesa { +class Domain; +struct Node; + +/// @brief Preserves the source shell type independently of formulation. +enum class ShellSourceElementType { kS4, kS4r }; + +/// @brief Names the internal shell formulation selected by S4 and S4R. +inline constexpr std::string_view kMitc4InternalFormulation{"FESA-MITC4"}; + +/// @brief Defines one four-node S4/S4R semantic element. +class Mitc4ShellDefinition final : public ElementDefinition { + public: + /// @brief Constructs a parser-validated semantic definition. + Mitc4ShellDefinition(SourceEntityId source_id, + ShellSourceElementType source_type, + std::array node_indices, + EntityIndex material_index, EntityIndex section_index, + SourceLocation location); + + ElementDefinitionKind Kind() const noexcept override; + const SourceEntityId& SourceId() const noexcept override; + std::string_view SourceElementType() const noexcept override; + const std::vector& NodeIndices() const noexcept override; + EntityIndex MaterialIndex() const noexcept override; + EntityIndex PropertyIndex() const noexcept override; + + SourceEntityId source_id; + ShellSourceElementType source_type; + std::array node_indices; + EntityIndex material_index; + EntityIndex section_index; + SourceLocation location; + + private: + friend class Domain; + + /// @brief Rebinds a shell-local section index to the unified property view. + void SetPropertyIndex(EntityIndex property_index) noexcept; + + /// @brief Synchronizes the base view after parser-candidate construction. + void SynchronizeNodeIndices(); + + EntityIndex property_index_; + std::vector node_indices_view_; +}; + /// @brief Stores bilinear shape values and natural-coordinate derivatives. struct Mitc4ShapeFunctions { std::array values; diff --git a/include/fesa/materials/isotropic_linear_elastic_material.h b/include/fesa/materials/isotropic_linear_elastic_material.h index cc1fd21..9a4367b 100644 --- a/include/fesa/materials/isotropic_linear_elastic_material.h +++ b/include/fesa/materials/isotropic_linear_elastic_material.h @@ -63,6 +63,9 @@ class IsotropicLinearElasticMaterial final : public Material { SourceEntityId source_id_; }; +/// @brief Preserves the approved V0 material spelling for current consumers. +using LinearElasticMaterial = IsotropicLinearElasticMaterial; + } // namespace fesa #endif // FESA_MATERIALS_ISOTROPIC_LINEAR_ELASTIC_MATERIAL_H_ diff --git a/include/fesa/model/domain.h b/include/fesa/model/domain.h index bbc20cf..d36ad66 100644 --- a/include/fesa/model/domain.h +++ b/include/fesa/model/domain.h @@ -1,7 +1,9 @@ #ifndef FESA_MODEL_DOMAIN_H_ #define FESA_MODEL_DOMAIN_H_ +#include #include +#include #include #include @@ -10,6 +12,34 @@ namespace fesa { +/// @brief Exposes immutable references without transferring Domain ownership. +/// @tparam T Base or concrete semantic type stored by the Domain. +template +class DomainCollectionView { + public: + /// @brief Returns the number of stable collection positions. + std::size_t Size() const noexcept { return entries_.size(); } + + /// @brief Reports whether the collection has no entries. + bool Empty() const noexcept { return entries_.empty(); } + + /// @brief Returns one immutable entry without bounds checking. + const T& operator[](const std::size_t index) const noexcept { + return *entries_[index]; + } + + /// @brief Returns one immutable entry with bounds checking. + const T& At(const std::size_t index) const { return *entries_.at(index); } + + private: + friend class Domain; + + /// @brief Adds one reference while the owning Domain candidate is built. + void Add(const T& entry) { entries_.push_back(&entry); } + + std::vector entries_; +}; + /// @brief Owns the complete immutable semantic model definition. /// @note Collection positions remain stable internal indices after /// construction. @@ -20,23 +50,40 @@ class Domain { /// @return A successful owning Domain. static Result Create(ModelDefinition definition); + Domain(const Domain&) = delete; + Domain& operator=(const Domain&) = delete; + Domain(Domain&&) noexcept = default; + Domain& operator=(Domain&&) noexcept = default; + /// @brief Returns nodes in stable declaration order. const std::vector& Nodes() const noexcept; - /// @brief Returns Euler beam definitions in stable declaration order. - const std::vector& Elements() const noexcept; + /// @brief Returns all element definitions in stable Domain index order. + const DomainCollectionView& Elements() const noexcept; + + /// @brief Returns B33 definitions in their stable concrete order. + const DomainCollectionView& BeamElements() + const noexcept; /// @brief Returns MITC4 shell definitions in stable declaration order. - const std::vector& ShellElements() const noexcept; + const DomainCollectionView& ShellElements() + const noexcept; - /// @brief Returns materials in stable declaration order. - const std::vector& Materials() const noexcept; + /// @brief Returns all materials in stable Domain index order. + const DomainCollectionView& Materials() const noexcept; + + /// @brief Returns current isotropic materials in stable concrete order. + const DomainCollectionView& LinearElasticMaterials() + const noexcept; + + /// @brief Returns all properties in stable Domain index order. + const DomainCollectionView& Properties() const noexcept; /// @brief Returns beam sections in stable declaration order. - const std::vector& Sections() const noexcept; + const DomainCollectionView& Sections() const noexcept; /// @brief Returns shell sections in stable declaration order. - const std::vector& ShellSections() const noexcept; + const DomainCollectionView& ShellSections() const noexcept; /// @brief Returns preprocessed shell-node frames in stable node order. const std::vector& ShellNodeInitialFrames() @@ -65,6 +112,17 @@ class Domain { explicit Domain(ModelDefinition definition); ModelDefinition definition_; + std::vector> element_definitions_; + std::vector> element_properties_; + std::vector> materials_; + DomainCollectionView elements_view_; + DomainCollectionView beam_elements_view_; + DomainCollectionView shell_elements_view_; + DomainCollectionView properties_view_; + DomainCollectionView sections_view_; + DomainCollectionView shell_sections_view_; + DomainCollectionView materials_view_; + DomainCollectionView linear_materials_view_; }; } // namespace fesa diff --git a/include/fesa/model/model_types.h b/include/fesa/model/model_types.h index 76e7370..d947a0b 100644 --- a/include/fesa/model/model_types.h +++ b/include/fesa/model/model_types.h @@ -11,6 +11,8 @@ #include "fesa/core/diagnostic.h" #include "fesa/core/source_identity.h" +#include "fesa/elements/euler_beam_3d.h" +#include "fesa/elements/mitc4_shell.h" #include "fesa/materials/isotropic_linear_elastic_material.h" #include "fesa/properties/general_beam_section.h" #include "fesa/properties/shell_section.h" @@ -24,26 +26,6 @@ struct Node { SourceLocation location; }; -/// @brief Keeps the current material spelling during the hierarchy migration. -using LinearElasticMaterial = IsotropicLinearElasticMaterial; - -/// @brief Preserves the source shell element label independently of -/// formulation. -enum class ShellSourceElementType { kS4, kS4r }; - -/// @brief Names the single internal shell formulation selected by S4 and S4R. -inline constexpr std::string_view kMitc4InternalFormulation{"FESA-MITC4"}; - -/// @brief Defines one four-node shell with stable semantic references. -struct Mitc4ShellDefinition { - SourceEntityId source_id; - ShellSourceElementType source_type; - std::array node_indices; - EntityIndex material_index; - EntityIndex section_index; - SourceLocation location; -}; - /// @brief Stores the deterministic initial director and tangent frame at a /// node. struct ShellNodeInitialFrame { @@ -53,15 +35,6 @@ struct ShellNodeInitialFrame { std::array tangent_b; }; -/// @brief Defines one two-node Euler beam with stable semantic references. -struct EulerBeam3DDefinition { - SourceEntityId source_id; - std::array node_indices; - EntityIndex material_index; - EntityIndex section_index; - SourceLocation location; -}; - /// @brief Stores one prescribed nodal degree-of-freedom range. struct BoundaryCondition { std::string target; diff --git a/src/fesa/analysis/analysis_model.cpp b/src/fesa/analysis/analysis_model.cpp index 7116181..8e60389 100644 --- a/src/fesa/analysis/analysis_model.cpp +++ b/src/fesa/analysis/analysis_model.cpp @@ -37,11 +37,21 @@ const std::vector& AnalysisModel::ActiveElements() const noexcept { return active_elements_; } +const std::vector& AnalysisModel::ActiveBeamElements() + const noexcept { + return active_beam_elements_; +} + const std::vector& AnalysisModel::ActiveMaterials() const noexcept { return active_materials_; } +const std::vector& AnalysisModel::ActiveProperties() + const noexcept { + return active_properties_; +} + const std::vector& AnalysisModel::ActiveSections() const noexcept { return active_sections_; } @@ -56,13 +66,20 @@ const std::vector& AnalysisModel::ActiveLoads() const noexcept { } AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} { - std::vector reachable_materials(domain.Materials().size(), false); - std::vector reachable_sections(domain.Sections().size(), false); + std::vector reachable_materials(domain.Materials().Size(), false); + std::vector reachable_properties(domain.Properties().Size(), false); + std::vector reachable_sections(domain.Sections().Size(), false); - for (std::size_t index = 0U; index < domain.Elements().size(); ++index) { + for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { const auto& element = domain.Elements()[index]; active_elements_.push_back(static_cast(index)); - reachable_materials[element.material_index] = true; + reachable_materials[element.MaterialIndex()] = true; + reachable_properties[element.PropertyIndex()] = true; + } + + for (std::size_t index = 0U; index < domain.BeamElements().Size(); ++index) { + const auto& element = domain.BeamElements()[index]; + active_beam_elements_.push_back(static_cast(index)); reachable_sections[element.section_index] = true; } @@ -73,6 +90,11 @@ AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} { active_materials_.push_back(static_cast(index)); } } + for (std::size_t index = 0U; index < reachable_properties.size(); ++index) { + if (reachable_properties[index]) { + active_properties_.push_back(static_cast(index)); + } + } for (std::size_t index = 0U; index < reachable_sections.size(); ++index) { if (reachable_sections[index]) { active_sections_.push_back(static_cast(index)); diff --git a/src/fesa/assembly/load_assembler.cpp b/src/fesa/assembly/load_assembler.cpp index 240f5b8..a3f2358 100644 --- a/src/fesa/assembly/load_assembler.cpp +++ b/src/fesa/assembly/load_assembler.cpp @@ -140,7 +140,7 @@ Status ValidateFiniteVector(const Vector& values, } Status ValidateShellMoments(const Domain& domain, const Vector& full_load) { - if (domain.ShellElements().empty()) { + if (domain.ShellElements().Empty()) { return Status::Ok(); } diff --git a/src/fesa/assembly/sparse_assembler.cpp b/src/fesa/assembly/sparse_assembler.cpp index b6ced7a..2a0e176 100644 --- a/src/fesa/assembly/sparse_assembler.cpp +++ b/src/fesa/assembly/sparse_assembler.cpp @@ -51,19 +51,19 @@ Result SparseAssembler::AssembleStiffness( std::to_string(dofs.FullDofCount()), "DofManager dimensions do not match the active model nodes."); } - if (!model.ActiveElements().empty() && !domain.ShellElements().empty()) { + if (!model.ActiveBeamElements().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() > + if (!domain.ShellElements().Empty()) { + if (domain.ShellElements().Size() > (std::numeric_limits::max)() / kShellContributionCount) { return AssemblyFailure( "invalid-assembly-dimensions", {domain.SourcePath(), 0U}, - std::to_string(domain.ShellElements().size()), + std::to_string(domain.ShellElements().Size()), "Shell contribution storage exceeds the addressable range."); } @@ -88,12 +88,12 @@ Result SparseAssembler::AssembleStiffness( std::array scatter; }; std::vector inputs; - inputs.reserve(domain.ShellElements().size()); + inputs.reserve(domain.ShellElements().Size()); for (std::size_t element_order = 0U; - element_order < domain.ShellElements().size(); ++element_order) { + 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()) { + if (element.material_index >= domain.LinearElasticMaterials().Size() || + element.section_index >= domain.ShellSections().Size()) { return AssemblyFailure( "invalid-assembly-element", element.location, element.source_id.source_label_text, @@ -102,7 +102,7 @@ Result SparseAssembler::AssembleStiffness( ShellInput input{}; input.section = &domain.ShellSections()[element.section_index]; - input.material = &domain.Materials()[element.material_index]; + input.material = &domain.LinearElasticMaterials()[element.material_index]; try { input.scatter = dofs.ShellElementScatter(static_cast(element_order)); @@ -191,28 +191,28 @@ Result SparseAssembler::AssembleStiffness( dofs.GetSparsePattern()); } - if (model.ActiveElements().size() > + if (model.ActiveBeamElements().size() > (std::numeric_limits::max)() / kBeamContributionCount) { return AssemblyFailure( "invalid-assembly-dimensions", {domain.SourcePath(), 0U}, - std::to_string(model.ActiveElements().size()), + std::to_string(model.ActiveBeamElements().size()), "Element contribution storage exceeds the addressable range."); } std::vector> scatters; - scatters.reserve(model.ActiveElements().size()); - for (const EntityIndex element_index : model.ActiveElements()) { - if (element_index >= domain.Elements().size()) { + scatters.reserve(model.ActiveBeamElements().size()); + for (const EntityIndex element_index : model.ActiveBeamElements()) { + if (element_index >= domain.BeamElements().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]; + const auto& element = domain.BeamElements()[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()) { + element.material_index >= domain.LinearElasticMaterials().Size() || + element.section_index >= domain.Sections().Size()) { return AssemblyFailure( "invalid-assembly-element", element.location, element.source_id.source_label_text, @@ -247,18 +247,20 @@ Result SparseAssembler::AssembleStiffness( scatters.push_back(scatter); } - std::vector local_buffers(model.ActiveElements().size()); + std::vector local_buffers( + model.ActiveBeamElements().size()); std::vector> local_failures( - model.ActiveElements().size()); + model.ActiveBeamElements().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]); + model.ActiveBeamElements().size(), [&](const std::size_t element_order) { + const EntityIndex element_index = + model.ActiveBeamElements()[element_order]; + const auto& definition = domain.BeamElements()[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.LinearElasticMaterials()[definition.material_index]); if (!beam.HasValue()) { local_failures[element_order] = beam.GetStatus(); return; diff --git a/src/fesa/elements/euler_beam_3d.cpp b/src/fesa/elements/euler_beam_3d.cpp index 51c87dd..a3e94a9 100644 --- a/src/fesa/elements/euler_beam_3d.cpp +++ b/src/fesa/elements/euler_beam_3d.cpp @@ -10,8 +10,51 @@ #include #include "fesa/math/vector3.h" +#include "fesa/model/model_types.h" namespace fesa { + +EulerBeam3DDefinition::EulerBeam3DDefinition( + SourceEntityId source_id_value, + std::array node_indices_value, + const EntityIndex material_index_value, + const EntityIndex section_index_value, SourceLocation location_value) + : source_id{std::move(source_id_value)}, + node_indices{std::move(node_indices_value)}, + material_index{material_index_value}, + section_index{section_index_value}, + location{std::move(location_value)}, + node_indices_view_{node_indices.begin(), node_indices.end()} {} + +ElementDefinitionKind EulerBeam3DDefinition::Kind() const noexcept { + return ElementDefinitionKind::kEulerBeam3D; +} + +const SourceEntityId& EulerBeam3DDefinition::SourceId() const noexcept { + return source_id; +} + +std::string_view EulerBeam3DDefinition::SourceElementType() const noexcept { + return "B33"; +} + +const std::vector& EulerBeam3DDefinition::NodeIndices() + const noexcept { + return node_indices_view_; +} + +EntityIndex EulerBeam3DDefinition::MaterialIndex() const noexcept { + return material_index; +} + +EntityIndex EulerBeam3DDefinition::PropertyIndex() const noexcept { + return section_index; +} + +void EulerBeam3DDefinition::SynchronizeNodeIndices() { + node_indices_view_.assign(node_indices.begin(), node_indices.end()); +} + namespace { constexpr std::size_t kElementDofCount = 12U; diff --git a/src/fesa/elements/mitc4_shell.cpp b/src/fesa/elements/mitc4_shell.cpp index 3b28f8c..e688687 100644 --- a/src/fesa/elements/mitc4_shell.cpp +++ b/src/fesa/elements/mitc4_shell.cpp @@ -7,7 +7,59 @@ #include #include +#include "fesa/model/model_types.h" + namespace fesa { + +Mitc4ShellDefinition::Mitc4ShellDefinition( + SourceEntityId source_id_value, + const ShellSourceElementType source_type_value, + std::array node_indices_value, + const EntityIndex material_index_value, + const EntityIndex section_index_value, SourceLocation location_value) + : source_id{std::move(source_id_value)}, + source_type{source_type_value}, + node_indices{std::move(node_indices_value)}, + material_index{material_index_value}, + section_index{section_index_value}, + location{std::move(location_value)}, + property_index_{section_index_value}, + node_indices_view_{node_indices.begin(), node_indices.end()} {} + +ElementDefinitionKind Mitc4ShellDefinition::Kind() const noexcept { + return ElementDefinitionKind::kMitc4Shell; +} + +const SourceEntityId& Mitc4ShellDefinition::SourceId() const noexcept { + return source_id; +} + +std::string_view Mitc4ShellDefinition::SourceElementType() const noexcept { + return source_type == ShellSourceElementType::kS4 ? "S4" : "S4R"; +} + +const std::vector& Mitc4ShellDefinition::NodeIndices() + const noexcept { + return node_indices_view_; +} + +EntityIndex Mitc4ShellDefinition::MaterialIndex() const noexcept { + return material_index; +} + +EntityIndex Mitc4ShellDefinition::PropertyIndex() const noexcept { + return property_index_; +} + +void Mitc4ShellDefinition::SetPropertyIndex( + const EntityIndex property_index) noexcept { + property_index_ = property_index; +} + +void Mitc4ShellDefinition::SynchronizeNodeIndices() { + node_indices_view_.assign(node_indices.begin(), node_indices.end()); +} + namespace { constexpr std::size_t kNodeCount = 4U; diff --git a/src/fesa/fem/dof_manager.cpp b/src/fesa/fem/dof_manager.cpp index ef441e6..a00eec5 100644 --- a/src/fesa/fem/dof_manager.cpp +++ b/src/fesa/fem/dof_manager.cpp @@ -120,9 +120,9 @@ Result DofManager::Create(const AnalysisModel& model) { } std::vector> element_scatters( - domain.Elements().size()); - for (const EntityIndex element_index : model.ActiveElements()) { - const auto& element = domain.Elements().at(element_index); + domain.BeamElements().Size()); + for (const EntityIndex element_index : model.ActiveBeamElements()) { + const auto& element = domain.BeamElements().At(element_index); auto& scatter = element_scatters.at(element_index); for (std::size_t endpoint = 0U; endpoint < element.node_indices.size(); ++endpoint) { @@ -135,9 +135,9 @@ Result DofManager::Create(const AnalysisModel& model) { } std::vector> shell_element_scatters( - domain.ShellElements().size()); + domain.ShellElements().Size()); for (std::size_t element_index = 0U; - element_index < domain.ShellElements().size(); ++element_index) { + 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; @@ -150,7 +150,7 @@ Result DofManager::Create(const AnalysisModel& model) { } } - auto pattern = BuildSparsePattern(full_count, model.ActiveElements(), + auto pattern = BuildSparsePattern(full_count, model.ActiveBeamElements(), element_scatters, shell_element_scatters); return Result::Success( DofManager{full_count, std::move(free_equations), diff --git a/src/fesa/io/hdf5/hdf5_results_writer.cpp b/src/fesa/io/hdf5/hdf5_results_writer.cpp index e912b2a..f968da6 100644 --- a/src/fesa/io/hdf5/hdf5_results_writer.cpp +++ b/src/fesa/io/hdf5/hdf5_results_writer.cpp @@ -230,7 +230,7 @@ struct WriterModelData { }; bool IsShellDomain(const Domain& domain) noexcept { - return !domain.ShellElements().empty(); + return !domain.ShellElements().Empty(); } const char* ShellSourceTypeName(const ShellSourceElementType type) { @@ -261,7 +261,7 @@ bool ComputeLocalAxes(const Domain& domain, const EulerBeam3DDefinition& element, AxisSet& axes) { if (element.node_indices[0U] >= domain.Nodes().size() || element.node_indices[1U] >= domain.Nodes().size() || - element.section_index >= domain.Sections().size()) { + element.section_index >= domain.Sections().Size()) { return false; } const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; @@ -300,12 +300,14 @@ bool SizeProductFits(const std::size_t left, const std::size_t right, Status ValidateShellWriterInput(const Domain& domain, const AnalysisState& state) { - if (!domain.Elements().empty()) { + if (!domain.BeamElements().Empty()) { return OutputFailure( "invalid-result-identity", "Schema v0 does not combine B33 and FESA-MITC4 element inventories."); } - for (const auto& material : domain.Materials()) { + for (std::size_t index = 0U; index < domain.LinearElasticMaterials().Size(); + ++index) { + const auto& material = domain.LinearElasticMaterials()[index]; if (material.name.empty() || !IsValidUtf8(material.name) || !std::isfinite(material.youngs_modulus) || !std::isfinite(material.poisson_ratio) || @@ -315,32 +317,34 @@ Status ValidateShellWriterInput(const Domain& domain, "finite constitutive data."); } } - for (const auto& section : domain.ShellSections()) { + for (std::size_t index = 0U; index < domain.ShellSections().Size(); ++index) { + const auto& section = domain.ShellSections()[index]; if (section.name.empty() || !IsValidUtf8(section.name) || - section.material_index >= domain.Materials().size() || + section.material_index >= domain.LinearElasticMaterials().Size() || !std::isfinite(section.thickness) || !(section.thickness > 0.0)) { return OutputFailure("invalid-result-identity", "Every shell section requires stable material " "identity and positive finite thickness."); } } - for (const auto& element : domain.ShellElements()) { + for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) { + const auto& element = domain.ShellElements()[index]; if ((element.source_type != ShellSourceElementType::kS4 && element.source_type != ShellSourceElementType::kS4r) || element.source_id.source_label <= 0 || element.source_id.source_label_text.empty() || !IsValidUtf8(element.source_id.instance_name) || !IsValidUtf8(element.source_id.source_label_text) || - element.material_index >= domain.Materials().size() || - element.section_index >= domain.ShellSections().size() || + element.node_indices.size() != kShellNodeCount || + element.material_index >= domain.LinearElasticMaterials().Size() || + element.section_index >= domain.ShellSections().Size() || domain.ShellSections()[element.section_index].material_index != element.material_index) { return OutputFailure("invalid-result-identity", "Every shell element requires stable source, " "material, and section identity."); } - std::array sorted_nodes = - element.node_indices; + auto sorted_nodes = element.node_indices; std::sort(sorted_nodes.begin(), sorted_nodes.end()); if (sorted_nodes.back() >= domain.Nodes().size() || std::adjacent_find(sorted_nodes.begin(), sorted_nodes.end()) != @@ -368,7 +372,7 @@ Status ValidateShellWriterInput(const Domain& domain, } std::size_t expected_rows = 0U; - if (!SizeProductFits(domain.ShellElements().size(), kShellLocationCount, + if (!SizeProductFits(domain.ShellElements().Size(), kShellLocationCount, expected_rows) || state.ShellResults().size() != expected_rows) { return OutputFailure("invalid-result-rows", @@ -487,15 +491,16 @@ Status ValidateWriterInput(const std::filesystem::path& output_path, } model_data.beam_local_axes.clear(); - model_data.beam_local_axes.reserve(domain.Elements().size()); - for (const EulerBeam3DDefinition& element : domain.Elements()) { + model_data.beam_local_axes.reserve(domain.BeamElements().Size()); + for (std::size_t index = 0U; index < domain.BeamElements().Size(); ++index) { + const EulerBeam3DDefinition& element = domain.BeamElements()[index]; AxisSet axes{}; if (element.source_id.source_label <= 0 || element.source_id.source_label_text.empty() || !IsValidUtf8(element.source_id.instance_name) || !IsValidUtf8(element.source_id.source_label_text) || element.node_indices[0U] == element.node_indices[1U] || - element.material_index >= domain.Materials().size() || + element.material_index >= domain.LinearElasticMaterials().Size() || !ComputeLocalAxes(domain, element, axes)) { return OutputFailure("invalid-result-identity", "Every element requires valid source, connectivity, " @@ -506,9 +511,9 @@ Status ValidateWriterInput(const std::filesystem::path& output_path, std::size_t endpoint_count = 0U; std::size_t gauss_count = 0U; - if (!SizeProductFits(domain.Elements().size(), kEndpointCount, + if (!SizeProductFits(domain.BeamElements().Size(), kEndpointCount, endpoint_count) || - !SizeProductFits(domain.Elements().size(), kGaussPointCount, + !SizeProductFits(domain.BeamElements().Size(), kGaussPointCount, gauss_count) || state.EndpointResults().size() != endpoint_count || state.GaussResults().size() != gauss_count) { @@ -522,7 +527,7 @@ Status ValidateWriterInput(const std::filesystem::path& output_path, static_cast(row_index / kEndpointCount); const int expected_endpoint = static_cast(row_index % kEndpointCount); const EndpointResultRow& row = state.EndpointResults()[row_index]; - const auto& element = domain.Elements()[expected_element]; + const auto& element = domain.BeamElements()[expected_element]; const auto& expected_node = domain.Nodes()[element.node_indices[static_cast( expected_endpoint)]]; @@ -552,9 +557,9 @@ Status ValidateWriterInput(const std::filesystem::path& output_path, } std::size_t stress_index = 0U; - for (std::size_t element_index = 0U; element_index < domain.Elements().size(); - ++element_index) { - const auto& element = domain.Elements()[element_index]; + for (std::size_t element_index = 0U; + element_index < domain.BeamElements().Size(); ++element_index) { + const auto& element = domain.BeamElements()[element_index]; const auto& section_points = domain.Sections()[element.section_index].section_points; for (std::size_t gauss = 0U; gauss < kGaussPointCount; ++gauss) { @@ -973,9 +978,9 @@ void WriteNodes(const hid_t file, const Domain& domain) { void WriteBeamElements(const hid_t file, const Domain& domain, const std::vector& axes) { std::vector rows; - rows.reserve(domain.Elements().size()); - for (std::size_t index = 0U; index < domain.Elements().size(); ++index) { - const auto& element = domain.Elements()[index]; + rows.reserve(domain.BeamElements().Size()); + for (std::size_t index = 0U; index < domain.BeamElements().Size(); ++index) { + const auto& element = domain.BeamElements()[index]; ElementWriteRow row{static_cast(index), element.source_id.instance_name.c_str(), element.source_id.source_label_text.c_str(), @@ -1047,8 +1052,8 @@ void WriteBeamElements(const hid_t file, const Domain& domain, void WriteShellElements(const hid_t file, const Domain& domain) { std::vector rows; - rows.reserve(domain.ShellElements().size()); - for (std::size_t index = 0U; index < domain.ShellElements().size(); ++index) { + rows.reserve(domain.ShellElements().Size()); + for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) { const auto& element = domain.ShellElements()[index]; rows.push_back({static_cast(index), element.source_id.instance_name.c_str(), @@ -1127,9 +1132,10 @@ void WriteShellElements(const hid_t file, const Domain& domain) { void WriteShellMaterials(const hid_t file, const Domain& domain) { std::vector rows; - rows.reserve(domain.Materials().size()); - for (std::size_t index = 0U; index < domain.Materials().size(); ++index) { - const auto& material = domain.Materials()[index]; + rows.reserve(domain.LinearElasticMaterials().Size()); + for (std::size_t index = 0U; index < domain.LinearElasticMaterials().Size(); + ++index) { + const auto& material = domain.LinearElasticMaterials()[index]; rows.push_back({static_cast(index), material.name.c_str(), material.youngs_modulus, material.poisson_ratio}); } @@ -1171,13 +1177,14 @@ void WriteShellMaterials(const hid_t file, const Domain& domain) { void WriteShellSections(const hid_t file, const Domain& domain) { std::vector source_files; - source_files.reserve(domain.ShellSections().size()); - for (const auto& section : domain.ShellSections()) { + source_files.reserve(domain.ShellSections().Size()); + for (std::size_t index = 0U; index < domain.ShellSections().Size(); ++index) { + const auto& section = domain.ShellSections()[index]; source_files.push_back(NormalizedPathString(section.location.file)); } std::vector rows; - rows.reserve(domain.ShellSections().size()); - for (std::size_t index = 0U; index < domain.ShellSections().size(); ++index) { + rows.reserve(domain.ShellSections().Size()); + for (std::size_t index = 0U; index < domain.ShellSections().Size(); ++index) { const auto& section = domain.ShellSections()[index]; rows.push_back({static_cast(index), source_files[index].c_str(), @@ -1423,7 +1430,7 @@ void WriteShellResultDatasets(const hid_t file, const Domain& domain, } const hsize_t element_count = - static_cast(domain.ShellElements().size()); + static_cast(domain.ShellElements().Size()); const std::string root = std::string{kStepRoot} + "/element/shell"; const std::string frame_path = root + "/local_frame"; WriteDoubleDataset(file, frame_path, {element_count, 4U, 3U, 3U}, @@ -1578,10 +1585,10 @@ void WriteResultDatasets(const hid_t file, const Domain& domain, } const std::vector endpoint_action_dimensions = { - static_cast(domain.Elements().size()), kEndpointCount, + static_cast(domain.BeamElements().Size()), kEndpointCount, kEndActionComponentCount}; const std::vector generalized_dimensions = { - static_cast(domain.Elements().size()), kEndpointCount, + static_cast(domain.BeamElements().Size()), kEndpointCount, kGeneralizedComponentCount}; const auto end_actions = FlattenEndpointValues(state.EndpointResults(), false); @@ -2052,18 +2059,19 @@ void SelfCheckFile(const std::filesystem::path& path, const Domain& domain, if (IsShellDomain(domain)) { RequireCompoundDataset( file.Get(), "/model/elements", - static_cast(domain.ShellElements().size()), + static_cast(domain.ShellElements().Size()), {"internal_element_id", "instance_name", "source_label", "source_element_type", "internal_formulation", "node_internal_ids", "shell_section_internal_id", "material_internal_id"}); auto elements = OpenDatasetForCheck(file.Get(), "/model/elements"); RequireStringAttribute(elements.Get(), "formulation", "FESA-MITC4"); - RequireCompoundDataset(file.Get(), "/model/shell/materials", - static_cast(domain.Materials().size()), - {"internal_material_id", "name", "E", "nu"}); + RequireCompoundDataset( + file.Get(), "/model/shell/materials", + static_cast(domain.LinearElasticMaterials().Size()), + {"internal_material_id", "name", "E", "nu"}); RequireCompoundDataset( file.Get(), "/model/shell/sections", - static_cast(domain.ShellSections().size()), + static_cast(domain.ShellSections().Size()), {"internal_section_id", "source_file", "source_line", "source_elset", "material_internal_id", "thickness"}); @@ -2114,7 +2122,7 @@ void SelfCheckFile(const std::filesystem::path& path, const Domain& domain, "BOTTOM,MIDDLE,TOP"); const hsize_t element_count = - static_cast(domain.ShellElements().size()); + static_cast(domain.ShellElements().Size()); const std::string shell_root = std::string{kStepRoot} + "/element/shell"; RequireDoubleDataset(file.Get(), (shell_root + "/local_frame").c_str(), {element_count, 4U, 3U, 3U}, "X,Y,Z", "1,1,1", @@ -2181,17 +2189,17 @@ void SelfCheckFile(const std::filesystem::path& path, const Domain& domain, } } else { RequireCompoundDataset(file.Get(), "/model/elements", - static_cast(domain.Elements().size()), + static_cast(domain.BeamElements().Size()), {"internal_element_id", "instance_name", "source_label", "node_internal_ids", "local_axes"}); auto elements = OpenDatasetForCheck(file.Get(), "/model/elements"); RequireStringAttribute(elements.Get(), "formulation", "B33-3D-Euler-Bernoulli"); const std::vector end_dimensions = { - static_cast(domain.Elements().size()), kEndpointCount, + static_cast(domain.BeamElements().Size()), kEndpointCount, kEndActionComponentCount}; const std::vector generalized_dimensions = { - static_cast(domain.Elements().size()), kGaussPointCount, + static_cast(domain.BeamElements().Size()), kGaussPointCount, kGeneralizedComponentCount}; RequireDoubleDataset( file.Get(), "/steps/Step-1/frames/0/element/end_force_local", diff --git a/src/fesa/model/domain.cpp b/src/fesa/model/domain.cpp index ea86b15..f1f8ed6 100644 --- a/src/fesa/model/domain.cpp +++ b/src/fesa/model/domain.cpp @@ -1,5 +1,7 @@ #include "fesa/model/domain.h" +#include +#include #include namespace fesa { @@ -12,25 +14,43 @@ const std::vector& Domain::Nodes() const noexcept { return definition_.nodes; } -const std::vector& Domain::Elements() const noexcept { - return definition_.elements; -} - -const std::vector& Domain::ShellElements() +const DomainCollectionView& Domain::Elements() const noexcept { - return definition_.shell_elements; + return elements_view_; } -const std::vector& Domain::Materials() const noexcept { - return definition_.materials; +const DomainCollectionView& Domain::BeamElements() + const noexcept { + return beam_elements_view_; } -const std::vector& Domain::Sections() const noexcept { - return definition_.sections; +const DomainCollectionView& Domain::ShellElements() + const noexcept { + return shell_elements_view_; } -const std::vector& Domain::ShellSections() const noexcept { - return definition_.shell_sections; +const DomainCollectionView& Domain::Materials() const noexcept { + return materials_view_; +} + +const DomainCollectionView& +Domain::LinearElasticMaterials() const noexcept { + return linear_materials_view_; +} + +const DomainCollectionView& Domain::Properties() + const noexcept { + return properties_view_; +} + +const DomainCollectionView& Domain::Sections() + const noexcept { + return sections_view_; +} + +const DomainCollectionView& Domain::ShellSections() + const noexcept { + return shell_sections_view_; } const std::vector& Domain::ShellNodeInitialFrames() @@ -63,6 +83,54 @@ const std::string& Domain::SourceContentIdentity() const noexcept { } Domain::Domain(ModelDefinition definition) - : definition_{std::move(definition)} {} + : definition_{std::move(definition)} { + materials_.reserve(definition_.materials.size()); + for (auto& material : definition_.materials) { + auto owned = std::make_unique(std::move(material)); + materials_view_.Add(*owned); + linear_materials_view_.Add(*owned); + materials_.push_back(std::move(owned)); + } + definition_.materials.clear(); + + element_properties_.reserve(definition_.sections.size() + + definition_.shell_sections.size()); + for (auto& section : definition_.sections) { + auto owned = std::make_unique(std::move(section)); + properties_view_.Add(*owned); + sections_view_.Add(*owned); + element_properties_.push_back(std::move(owned)); + } + const std::size_t beam_property_count = element_properties_.size(); + definition_.sections.clear(); + for (auto& section : definition_.shell_sections) { + auto owned = std::make_unique(std::move(section)); + properties_view_.Add(*owned); + shell_sections_view_.Add(*owned); + element_properties_.push_back(std::move(owned)); + } + definition_.shell_sections.clear(); + + element_definitions_.reserve(definition_.elements.size() + + definition_.shell_elements.size()); + for (auto& element : definition_.elements) { + element.SynchronizeNodeIndices(); + auto owned = std::make_unique(std::move(element)); + elements_view_.Add(*owned); + beam_elements_view_.Add(*owned); + element_definitions_.push_back(std::move(owned)); + } + definition_.elements.clear(); + for (auto& element : definition_.shell_elements) { + element.SynchronizeNodeIndices(); + element.SetPropertyIndex( + static_cast(beam_property_count + element.section_index)); + auto owned = std::make_unique(std::move(element)); + elements_view_.Add(*owned); + shell_elements_view_.Add(*owned); + element_definitions_.push_back(std::move(owned)); + } + definition_.shell_elements.clear(); +} } // namespace fesa diff --git a/src/fesa/model/source_target_resolver.cpp b/src/fesa/model/source_target_resolver.cpp index 1be572d..fd81c01 100644 --- a/src/fesa/model/source_target_resolver.cpp +++ b/src/fesa/model/source_target_resolver.cpp @@ -96,27 +96,17 @@ SourceTargetIndex SourceTargetIndex::FromDomain(const Domain& domain) { } } - for (std::size_t index = 0U; index < domain.Elements().size(); ++index) { + for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { const auto& element = domain.Elements()[index]; entries.push_back({SourceEntityKind::kElement, - element.source_id.instance_name, "", element.source_id, - static_cast(index), declaration_order++}); - } - for (std::size_t index = 0U; index < domain.ShellElements().size(); ++index) { - const auto& element = domain.ShellElements()[index]; - entries.push_back({SourceEntityKind::kElement, - element.source_id.instance_name, "", element.source_id, + element.SourceId().instance_name, "", element.SourceId(), static_cast(index), declaration_order++}); } for (const auto& set : domain.ElementSets()) { for (const EntityIndex element_index : set.element_indices) { SourceEntityId source_id{set.instance_name.value_or(""), 0, ""}; - if (domain.ShellElements().empty() && - element_index < domain.Elements().size()) { - source_id = domain.Elements()[element_index].source_id; - } else if (domain.Elements().empty() && - element_index < domain.ShellElements().size()) { - source_id = domain.ShellElements()[element_index].source_id; + if (element_index < domain.Elements().Size()) { + source_id = domain.Elements()[element_index].SourceId(); } entries.push_back({SourceEntityKind::kElement, set.instance_name.value_or(source_id.instance_name), diff --git a/src/fesa/results/result_recovery.cpp b/src/fesa/results/result_recovery.cpp index 6e9b67d..7c7000b 100644 --- a/src/fesa/results/result_recovery.cpp +++ b/src/fesa/results/result_recovery.cpp @@ -216,8 +216,8 @@ Status ValidateRecoveryInputs(const AnalysisModel& model, EntityIndex previous_element = 0U; bool first_element = true; - for (const EntityIndex element : model.ActiveElements()) { - if (element >= domain.Elements().size() || + for (const EntityIndex element : model.ActiveBeamElements()) { + if (element >= domain.BeamElements().Size() || (!first_element && element <= previous_element)) { return RecoveryFailure( "invalid-recovery-entity", {domain.SourcePath(), 0U}, @@ -226,11 +226,11 @@ Status ValidateRecoveryInputs(const AnalysisModel& model, } first_element = false; previous_element = element; - const auto& definition = domain.Elements()[element]; + const auto& definition = domain.BeamElements()[element]; if (definition.node_indices[0U] >= domain.Nodes().size() || definition.node_indices[1U] >= domain.Nodes().size() || - definition.material_index >= domain.Materials().size() || - definition.section_index >= domain.Sections().size()) { + definition.material_index >= domain.LinearElasticMaterials().Size() || + definition.section_index >= domain.Sections().Size()) { return RecoveryFailure( "invalid-recovery-entity", definition.location, definition.source_id.source_label_text, @@ -263,13 +263,13 @@ Status ValidateRecoveryInputs(const AnalysisModel& model, } } - if (!model.ActiveElements().empty() && !domain.ShellElements().empty()) { + if (!model.ActiveBeamElements().empty() && !domain.ShellElements().Empty()) { return RecoveryFailure( "unsupported-mixed-element-model", {domain.SourcePath(), 0U}, "B33:FESA-MITC4", "Result recovery does not support mixed beam and shell models."); } - if (domain.ShellElements().size() > + if (domain.ShellElements().Size() > static_cast((std::numeric_limits::max)())) { return RecoveryFailure("invalid-recovery-dimensions", {domain.SourcePath(), 0U}, @@ -278,10 +278,10 @@ Status ValidateRecoveryInputs(const AnalysisModel& model, "stable element identities."); } for (std::size_t element_order = 0U; - element_order < domain.ShellElements().size(); ++element_order) { + element_order < domain.ShellElements().Size(); ++element_order) { const auto& definition = domain.ShellElements()[element_order]; - if (definition.material_index >= domain.Materials().size() || - definition.section_index >= domain.ShellSections().size()) { + if (definition.material_index >= domain.LinearElasticMaterials().Size() || + definition.section_index >= domain.ShellSections().Size()) { return RecoveryFailure("invalid-recovery-entity", definition.location, definition.source_id.source_label_text, "Active shell material and section references " @@ -586,16 +586,16 @@ Status ResultRecovery::Recover(const AnalysisModel& model, std::vector endpoint_rows; std::vector gauss_rows; std::vector stress_rows; - endpoint_rows.reserve(model.ActiveElements().size() * 2U); - gauss_rows.reserve(model.ActiveElements().size() * 2U); + endpoint_rows.reserve(model.ActiveBeamElements().size() * 2U); + gauss_rows.reserve(model.ActiveBeamElements().size() * 2U); const Domain& domain = model.GetDomain(); - for (const EntityIndex element_index : model.ActiveElements()) { - const auto& definition = domain.Elements()[element_index]; - 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]); + for (const EntityIndex element_index : model.ActiveBeamElements()) { + const auto& definition = domain.BeamElements()[element_index]; + auto beam = EulerBeam3D::Create( + domain.Nodes()[definition.node_indices[0U]], + domain.Nodes()[definition.node_indices[1U]], + domain.Sections()[definition.section_index], + domain.LinearElasticMaterials()[definition.material_index]); if (!beam.HasValue()) { return beam.GetStatus(); } @@ -649,8 +649,8 @@ Status ResultRecovery::Recover(const AnalysisModel& model, ShellStateCandidate shell_candidate{}; std::vector expected_shell_elements; - if (!domain.ShellElements().empty()) { - if (domain.ShellElements().size() > + if (!domain.ShellElements().Empty()) { + if (domain.ShellElements().Size() > (std::numeric_limits::max)() / kShellLocationCount) { return RecoveryFailure( "invalid-recovery-dimensions", {domain.SourcePath(), 0U}, @@ -670,9 +670,9 @@ Status ResultRecovery::Recover(const AnalysisModel& model, directors_by_node[frame.node_index] = frame.director; } - shell_candidate.rows.reserve(domain.ShellElements().size() * + shell_candidate.rows.reserve(domain.ShellElements().Size() * kShellLocationCount); - expected_shell_elements.reserve(domain.ShellElements().size()); + expected_shell_elements.reserve(domain.ShellElements().Size()); constexpr std::array locations{ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2, ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4}; @@ -681,7 +681,7 @@ Status ResultRecovery::Recover(const AnalysisModel& model, ShellSectionPosition::kTop}; constexpr std::array zeta{-1.0, 0.0, 1.0}; for (std::size_t element_order = 0U; - element_order < domain.ShellElements().size(); ++element_order) { + element_order < domain.ShellElements().Size(); ++element_order) { const EntityIndex element_index = static_cast(element_order); const auto& definition = domain.ShellElements()[element_order]; std::array nodes{}; @@ -701,7 +701,7 @@ Status ResultRecovery::Recover(const AnalysisModel& model, auto shell = Mitc4Shell::Create( nodes, directors, domain.ShellSections()[definition.section_index], - domain.Materials()[definition.material_index]); + domain.LinearElasticMaterials()[definition.material_index]); if (!shell.HasValue()) { return shell.GetStatus(); } @@ -788,9 +788,9 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations( "Node-station component tolerances must be finite and nonnegative."); } } - if (model.ActiveElements().size() > + if (model.ActiveBeamElements().size() > (std::numeric_limits::max)() / 2U || - endpoint_rows.size() != model.ActiveElements().size() * 2U) { + endpoint_rows.size() != model.ActiveBeamElements().size() * 2U) { return RecoveryResultFailure>( "invalid-node-station-shape", {domain.SourcePath(), 0U}, std::to_string(endpoint_rows.size()), @@ -799,15 +799,16 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations( std::vector> rows_by_node( domain.Nodes().size()); - for (std::size_t order = 0U; order < model.ActiveElements().size(); ++order) { - const EntityIndex element_index = model.ActiveElements()[order]; - if (element_index >= domain.Elements().size()) { + for (std::size_t order = 0U; order < model.ActiveBeamElements().size(); + ++order) { + const EntityIndex element_index = model.ActiveBeamElements()[order]; + if (element_index >= domain.BeamElements().Size()) { return RecoveryResultFailure>( "invalid-node-station-entity", {domain.SourcePath(), 0U}, std::to_string(element_index), "Every active station element must be a valid stable entity."); } - const auto& definition = domain.Elements()[element_index]; + const auto& definition = domain.BeamElements()[element_index]; for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { const auto& row = endpoint_rows[order * 2U + endpoint]; const EntityIndex node_index = definition.node_indices[endpoint]; @@ -885,8 +886,8 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations( "Interior station collapse requires exactly two unloaded endpoints."); } - const auto& first_element = domain.Elements()[incident[0U]->element]; - const auto& second_element = domain.Elements()[incident[1U]->element]; + const auto& first_element = domain.BeamElements()[incident[0U]->element]; + const auto& second_element = domain.BeamElements()[incident[1U]->element]; const bool chain_orientation = incident[0U]->endpoint != incident[1U]->endpoint && ((incident[0U]->endpoint == 1 && incident[1U]->endpoint == 0) || diff --git a/tests/integration/analysis/linear_static_analysis_test.cpp b/tests/integration/analysis/linear_static_analysis_test.cpp index 9cfeda4..f593601 100644 --- a/tests/integration/analysis/linear_static_analysis_test.cpp +++ b/tests/integration/analysis/linear_static_analysis_test.cpp @@ -307,7 +307,7 @@ class CapturingResultsWriter final : public fesa::ResultsWriter { const std::vector& diagnostics) override { output_path_ = output_path; node_count_ = domain.Nodes().size(); - shell_element_count_ = domain.ShellElements().size(); + shell_element_count_ = domain.ShellElements().Size(); state_ = std::make_unique(state); diagnostics_ = diagnostics; return fesa::Status::Ok(); diff --git a/tests/reference/reference_comparison.cpp b/tests/reference/reference_comparison.cpp index e3f27b9..5fd1795 100644 --- a/tests/reference/reference_comparison.cpp +++ b/tests/reference/reference_comparison.cpp @@ -815,7 +815,7 @@ HdfProjection ReadHdfProjection(const std::filesystem::path& results, projection.nodes = ReadNodeRows(file.Get()); projection.elements = ReadElementRows(file.Get()); if (projection.nodes.size() != domain.Nodes().size() || - projection.elements.size() != domain.Elements().size()) { + projection.elements.size() != domain.BeamElements().Size()) { Fail("schema-mismatch", "HDF5 model identity counts do not match the input."); } @@ -834,7 +834,7 @@ HdfProjection ReadHdfProjection(const std::filesystem::path& results, for (std::size_t element = 0U; element < projection.elements.size(); ++element) { const auto& actual = projection.elements[element]; - const auto& expected = domain.Elements()[element]; + const auto& expected = domain.BeamElements()[element]; if (actual.internal_element_id != element || actual.instance_name != expected.source_id.instance_name || actual.source_element_label != expected.source_id.source_label || diff --git a/tests/unit/analysis/analysis_model_test.cpp b/tests/unit/analysis/analysis_model_test.cpp index 7fb8cdd..22a5e18 100644 --- a/tests/unit/analysis/analysis_model_test.cpp +++ b/tests/unit/analysis/analysis_model_test.cpp @@ -77,6 +77,19 @@ fesa::ModelDefinition MakeDefinition() { return definition; } +fesa::ModelDefinition MakeMixedDefinition() { + auto definition = MakeDefinition(); + const auto source = definition.source_path; + definition.shell_sections = {{"ShellSection", 0.01, 1U, {source, 35U}}}; + definition.shell_elements = {{{"Shell-1", 5, "5"}, + fesa::ShellSourceElementType::kS4, + {0U, 1U, 2U, 3U}, + 1U, + 0U, + {source, 45U}}}; + return definition; +} + } // namespace TEST(AnalysisModel, ClassifiesActiveEntitiesInStableOrder) { @@ -102,9 +115,9 @@ TEST(AnalysisModel, ReferencesWithoutCopyingOrMutatingDomain) { auto domain_result = fesa::Domain::Create(MakeDefinition()); ASSERT_TRUE(domain_result.HasValue()); const fesa::Domain& domain = domain_result.Value(); - const auto* const element_address = domain.Elements().data(); - const auto* const material_address = domain.Materials().data(); - const auto* const section_address = domain.Sections().data(); + const auto* const element_address = &domain.Elements()[0U]; + const auto* const material_address = &domain.Materials()[0U]; + const auto* const section_address = &domain.Sections()[0U]; const std::string step_name = domain.Steps()[0].name; const double first_load_magnitude = domain.Steps()[0].loads[0].magnitude; @@ -114,9 +127,9 @@ TEST(AnalysisModel, ReferencesWithoutCopyingOrMutatingDomain) { EXPECT_EQ(&model.GetDomain(), &domain); EXPECT_EQ(&model.Step(), &domain.Steps()[0]); - EXPECT_EQ(model.GetDomain().Elements().data(), element_address); - EXPECT_EQ(model.GetDomain().Materials().data(), material_address); - EXPECT_EQ(model.GetDomain().Sections().data(), section_address); + EXPECT_EQ(&model.GetDomain().Elements()[0U], element_address); + EXPECT_EQ(&model.GetDomain().Materials()[0U], material_address); + EXPECT_EQ(&model.GetDomain().Sections()[0U], section_address); EXPECT_EQ(&model.GetDomain().Elements()[model.ActiveElements()[1]], &domain.Elements()[1]); EXPECT_EQ(&model.GetDomain().Materials()[model.ActiveMaterials()[2]], @@ -127,6 +140,31 @@ TEST(AnalysisModel, ReferencesWithoutCopyingOrMutatingDomain) { EXPECT_DOUBLE_EQ(domain.Steps()[0].loads[0].magnitude, first_load_magnitude); } +// C-MODEL-002 +TEST(AnalysisModel, KeepsNonOwningMixedDefinitionAndPropertyIndices) { + auto domain_result = fesa::Domain::Create(MakeMixedDefinition()); + ASSERT_TRUE(domain_result.HasValue()); + const fesa::Domain& domain = domain_result.Value(); + + auto model_result = fesa::AnalysisModel::Create(domain); + ASSERT_TRUE(model_result.HasValue()); + const fesa::AnalysisModel& model = model_result.Value(); + + EXPECT_EQ(&model.GetDomain(), &domain); + EXPECT_EQ(model.ActiveElements(), + (std::vector{0U, 1U, 2U, 3U, 4U})); + EXPECT_EQ(model.ActiveMaterials(), + (std::vector{0U, 1U, 2U})); + EXPECT_EQ(model.ActiveProperties(), + (std::vector{0U, 1U, 2U, 4U})); + EXPECT_EQ(&model.GetDomain().Elements()[model.ActiveElements()[4U]], + &domain.Elements()[4U]); + EXPECT_EQ(&model.GetDomain().Materials()[model.ActiveMaterials()[1U]], + &domain.Materials()[1U]); + EXPECT_EQ(&model.GetDomain().Properties()[model.ActiveProperties()[3U]], + &domain.Properties()[4U]); +} + TEST(AnalysisModel, RejectsMissingOrMultipleStep) { auto missing_definition = MakeDefinition(); missing_definition.steps.clear(); diff --git a/tests/unit/assembly/sparse_assembler_test.cpp b/tests/unit/assembly/sparse_assembler_test.cpp index dabd125..5e00050 100644 --- a/tests/unit/assembly/sparse_assembler_test.cpp +++ b/tests/unit/assembly/sparse_assembler_test.cpp @@ -103,7 +103,7 @@ fesa::ModelDefinition MakeShellDefinition( fesa::Result DirectShellStiffness( const fesa::Domain& domain, const fesa::EntityIndex element_index) { - const auto& definition = domain.ShellElements().at(element_index); + const auto& definition = domain.ShellElements().At(element_index); std::array nodes{}; std::array, 4> directors{}; for (std::size_t node = 0U; node < definition.node_indices.size(); ++node) { @@ -112,8 +112,8 @@ fesa::Result DirectShellStiffness( directors[node] = domain.ShellNodeInitialFrames().at(node_index).director; } auto shell = fesa::Mitc4Shell::Create( - nodes, directors, domain.ShellSections().at(definition.section_index), - domain.Materials().at(definition.material_index)); + nodes, directors, domain.ShellSections().At(definition.section_index), + domain.LinearElasticMaterials().At(definition.material_index)); if (!shell.HasValue()) { return fesa::Result::Failure(shell.GetStatus()); } diff --git a/tests/unit/elements/euler_beam_3d_test.cpp b/tests/unit/elements/euler_beam_3d_test.cpp index d47d8d6..44f4604 100644 --- a/tests/unit/elements/euler_beam_3d_test.cpp +++ b/tests/unit/elements/euler_beam_3d_test.cpp @@ -13,6 +13,7 @@ #include #include "fesa/math/vector3.h" +#include "fesa/model/model_types.h" namespace fesa { namespace { diff --git a/tests/unit/elements/mitc4_shell_test.cpp b/tests/unit/elements/mitc4_shell_test.cpp index 214500c..30d7ae9 100644 --- a/tests/unit/elements/mitc4_shell_test.cpp +++ b/tests/unit/elements/mitc4_shell_test.cpp @@ -13,6 +13,7 @@ #include #include "fesa/math/vector3.h" +#include "fesa/model/model_types.h" namespace { diff --git a/tests/unit/io/abaqus/domain_mapper_test.cpp b/tests/unit/io/abaqus/domain_mapper_test.cpp index d8863b4..a087ff6 100644 --- a/tests/unit/io/abaqus/domain_mapper_test.cpp +++ b/tests/unit/io/abaqus/domain_mapper_test.cpp @@ -13,6 +13,7 @@ #include #include +#include "fesa/elements/element_definition.h" #include "fesa/io/abaqus/input_reader.h" #include "fesa/math/vector3.h" @@ -242,6 +243,7 @@ RootAssembly, 6, -12.5 } // namespace // C-DUP-002 +// C-MODEL-002 TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) { auto result = MapText("supported-inventory", SupportedInventoryDeck(true)); ASSERT_TRUE(result.HasValue()); @@ -254,17 +256,23 @@ TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) { EXPECT_EQ(domain.Nodes()[1].source_id.source_label_text, "0002"); EXPECT_DOUBLE_EQ(domain.Nodes()[1].coordinates[0], 2.0); - ASSERT_EQ(domain.Elements().size(), 1U); - EXPECT_EQ(domain.Elements()[0].source_id.source_label_text, "0007"); - EXPECT_EQ(domain.Elements()[0].node_indices[0], 0U); - EXPECT_EQ(domain.Elements()[0].node_indices[1], 1U); + ASSERT_EQ(domain.Elements().Size(), 1U); + const fesa::ElementDefinition& element = domain.Elements()[0U]; + EXPECT_EQ(element.Kind(), fesa::ElementDefinitionKind::kEulerBeam3D); + EXPECT_EQ(element.SourceId().source_label_text, "0007"); + EXPECT_EQ(element.SourceElementType(), "B33"); + EXPECT_EQ(element.NodeIndices(), (std::vector{0U, 1U})); + EXPECT_EQ(&element, &domain.BeamElements()[0U]); - ASSERT_EQ(domain.Materials().size(), 1U); - EXPECT_EQ(domain.Materials()[0].name, "Steel"); - EXPECT_DOUBLE_EQ(domain.Materials()[0].youngs_modulus, 210000.0); - EXPECT_DOUBLE_EQ(domain.Materials()[0].poisson_ratio, 0.75); + ASSERT_EQ(domain.Materials().Size(), 1U); + EXPECT_EQ(domain.Materials()[0U].Kind(), + fesa::MaterialKind::kIsotropicLinearElastic); + EXPECT_EQ(domain.LinearElasticMaterials()[0U].name, "Steel"); + EXPECT_DOUBLE_EQ(domain.LinearElasticMaterials()[0U].youngs_modulus, + 210000.0); + EXPECT_DOUBLE_EQ(domain.LinearElasticMaterials()[0U].poisson_ratio, 0.75); - ASSERT_EQ(domain.Sections().size(), 1U); + ASSERT_EQ(domain.Sections().Size(), 1U); const auto& section = domain.Sections()[0]; EXPECT_DOUBLE_EQ(section.area, 2.0); EXPECT_DOUBLE_EQ(section.i11, 3.0); @@ -274,8 +282,8 @@ TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) { EXPECT_EQ(section.first_axis, (std::array{0.0, 1.0, 0.0})); EXPECT_EQ(section.section_points, (std::vector>{{-0.5, 0.25}, {0.5, -0.25}})); - EXPECT_EQ(domain.Elements()[0].material_index, 0U); - EXPECT_EQ(domain.Elements()[0].section_index, 0U); + EXPECT_EQ(domain.BeamElements()[0U].material_index, 0U); + EXPECT_EQ(domain.BeamElements()[0U].section_index, 0U); ASSERT_EQ(domain.Steps().size(), 1U); const auto& step = domain.Steps()[0]; @@ -305,9 +313,9 @@ TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) { auto legacy = fesa::AbaqusDomainMapper{}.Map(parsed_legacy.Value()); ASSERT_TRUE(legacy.HasValue()); EXPECT_EQ(legacy.Value().Nodes().size(), 11U); - EXPECT_EQ(legacy.Value().Elements().size(), 10U); - EXPECT_EQ(legacy.Value().Materials().size(), 1U); - EXPECT_EQ(legacy.Value().Sections().size(), 1U); + EXPECT_EQ(legacy.Value().Elements().Size(), 10U); + EXPECT_EQ(legacy.Value().Materials().Size(), 1U); + EXPECT_EQ(legacy.Value().Sections().Size(), 1U); EXPECT_EQ(legacy.Value().Steps().size(), 1U); EXPECT_EQ(legacy.Value().Warnings().size(), 7U); EXPECT_EQ(ReadExactBytes(legacy_path), bytes_before); @@ -367,12 +375,12 @@ OnlySecond, 2, 5. EXPECT_EQ(domain.Nodes()[3].source_id.instance_name, "Second"); EXPECT_EQ(domain.Nodes()[3].source_id.source_label, 20); - ASSERT_EQ(domain.Elements().size(), 2U); - EXPECT_EQ(domain.Elements()[0].source_id.instance_name, "First"); - EXPECT_EQ(domain.Elements()[0].node_indices, + ASSERT_EQ(domain.BeamElements().Size(), 2U); + EXPECT_EQ(domain.BeamElements()[0].source_id.instance_name, "First"); + EXPECT_EQ(domain.BeamElements()[0].node_indices, (std::array{0U, 1U})); - EXPECT_EQ(domain.Elements()[1].source_id.instance_name, "Second"); - EXPECT_EQ(domain.Elements()[1].node_indices, + EXPECT_EQ(domain.BeamElements()[1].source_id.instance_name, "Second"); + EXPECT_EQ(domain.BeamElements()[1].node_indices, (std::array{2U, 3U})); ASSERT_EQ(domain.NodeSets().size(), 3U); @@ -525,12 +533,12 @@ TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) { } EXPECT_EQ(with_no_ops.Value().Nodes().size(), plain.Value().Nodes().size()); - EXPECT_EQ(with_no_ops.Value().Elements().size(), - plain.Value().Elements().size()); - EXPECT_EQ(with_no_ops.Value().Materials().size(), - plain.Value().Materials().size()); - EXPECT_EQ(with_no_ops.Value().Sections().size(), - plain.Value().Sections().size()); + EXPECT_EQ(with_no_ops.Value().Elements().Size(), + plain.Value().Elements().Size()); + EXPECT_EQ(with_no_ops.Value().Materials().Size(), + plain.Value().Materials().Size()); + EXPECT_EQ(with_no_ops.Value().Sections().Size(), + plain.Value().Sections().Size()); EXPECT_EQ(with_no_ops.Value().NodeSets().size(), plain.Value().NodeSets().size()); EXPECT_EQ(with_no_ops.Value().ElementSets().size(), @@ -543,13 +551,20 @@ TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) { } // MITC4-MAP-001 +// C-MODEL-002 TEST(InpDomainMapping, MapsS4AndS4rThroughOneMitc4Identity) { auto result = MapText("mitc4-map-001", ShellDeck()); ASSERT_TRUE(result.HasValue()); const fesa::Domain& domain = result.Value(); - EXPECT_TRUE(domain.Elements().empty()); - ASSERT_EQ(domain.ShellElements().size(), 4U); + EXPECT_TRUE(domain.BeamElements().Empty()); + ASSERT_EQ(domain.Elements().Size(), 4U); + EXPECT_EQ(domain.Elements()[0U].Kind(), + fesa::ElementDefinitionKind::kMitc4Shell); + EXPECT_EQ(domain.Elements()[0U].SourceElementType(), "S4"); + EXPECT_EQ(domain.Elements()[1U].SourceElementType(), "S4R"); + EXPECT_EQ(&domain.Elements()[0U], &domain.ShellElements()[0U]); + ASSERT_EQ(domain.ShellElements().Size(), 4U); EXPECT_EQ(domain.ShellElements()[0].source_id.instance_name, "First"); EXPECT_EQ(domain.ShellElements()[0].source_id.source_label_text, "0010"); EXPECT_EQ(domain.ShellElements()[0].source_type, @@ -573,7 +588,7 @@ TEST(InpDomainMapping, MapsS4AndS4rThroughOneMitc4Identity) { EXPECT_EQ(domain.ShellElements()[3].source_id.instance_name, "Second"); EXPECT_EQ(fesa::kMitc4InternalFormulation, std::string_view{"FESA-MITC4"}); - ASSERT_EQ(domain.ShellSections().size(), 2U); + ASSERT_EQ(domain.ShellSections().Size(), 2U); EXPECT_EQ(domain.ShellSections()[0].name, "ShellS4"); EXPECT_DOUBLE_EQ(domain.ShellSections()[0].thickness, 0.1); EXPECT_EQ(domain.ShellSections()[0].material_index, 0U); diff --git a/tests/unit/model/domain_test.cpp b/tests/unit/model/domain_test.cpp index 2aa6f1b..47a8b6b 100644 --- a/tests/unit/model/domain_test.cpp +++ b/tests/unit/model/domain_test.cpp @@ -8,6 +8,8 @@ #include #include +#include "fesa/elements/element_definition.h" + namespace { fesa::ModelDefinition MakeOwnedDefinition() { @@ -63,6 +65,90 @@ fesa::ModelDefinition MakeOwnedDefinition() { } // namespace +// C-MODEL-002 +TEST(ElementDefinition, DomainOwnsMixedDefinitionsInStableBaseOrder) { + static_assert(std::has_virtual_destructor_v); + static_assert(!std::is_copy_constructible_v); + static_assert(!std::is_copy_assignable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_move_assignable_v); + + fesa::ModelDefinition definition{}; + definition.source_path = "models/mixed-owned.inp"; + definition.source_content_identity = "fnv1a64:mixed"; + definition.materials = { + {"Steel", 210.0e9, 0.3, {definition.source_path, 30U}}}; + definition.sections = {{"BeamSection", + 0.02, + 1.0e-5, + 0.0, + 2.0e-5, + 5.0e-6, + {0.0, 1.0, 0.0}, + {}, + {definition.source_path, 40U}}}; + definition.shell_sections = { + {"ShellSection", 0.01, 0U, {definition.source_path, 41U}}}; + definition.elements = { + {{"Beam-1", 7, "0007"}, {0U, 1U}, 0U, 0U, {definition.source_path, 20U}}}; + definition.shell_elements = {{{"Shell-1", 9, "0009"}, + fesa::ShellSourceElementType::kS4r, + {2U, 3U, 4U, 5U}, + 0U, + 0U, + {definition.source_path, 21U}}}; + + auto result = fesa::Domain::Create(definition); + ASSERT_TRUE(result.HasValue()); + fesa::Domain domain = std::move(result.Value()); + + static_assert(std::is_same_v< + decltype(std::declval().Elements()[0U]), + const fesa::ElementDefinition&>); + static_assert(std::is_same_v< + decltype(std::declval().Materials()[0U]), + const fesa::Material&>); + static_assert(std::is_same_v< + decltype(std::declval().Properties()[0U]), + const fesa::ElementProperty&>); + + ASSERT_EQ(domain.Elements().Size(), 2U); + const fesa::ElementDefinition& beam = domain.Elements()[0U]; + EXPECT_EQ(beam.Kind(), fesa::ElementDefinitionKind::kEulerBeam3D); + EXPECT_EQ(beam.SourceId().source_label_text, "0007"); + EXPECT_EQ(beam.SourceElementType(), "B33"); + EXPECT_EQ(beam.NodeIndices(), (std::vector{0U, 1U})); + EXPECT_EQ(beam.MaterialIndex(), fesa::EntityIndex{0U}); + EXPECT_EQ(beam.PropertyIndex(), fesa::EntityIndex{0U}); + + const fesa::ElementDefinition& shell = domain.Elements()[1U]; + EXPECT_EQ(shell.Kind(), fesa::ElementDefinitionKind::kMitc4Shell); + EXPECT_EQ(shell.SourceId().source_label_text, "0009"); + EXPECT_EQ(shell.SourceElementType(), "S4R"); + EXPECT_EQ(shell.NodeIndices(), + (std::vector{2U, 3U, 4U, 5U})); + EXPECT_EQ(shell.MaterialIndex(), fesa::EntityIndex{0U}); + EXPECT_EQ(shell.PropertyIndex(), fesa::EntityIndex{1U}); + + ASSERT_EQ(domain.BeamElements().Size(), 1U); + ASSERT_EQ(domain.ShellElements().Size(), 1U); + EXPECT_EQ(&domain.BeamElements()[0U], &beam); + EXPECT_EQ(&domain.ShellElements()[0U], &shell); + + ASSERT_EQ(domain.Materials().Size(), 1U); + EXPECT_EQ(domain.Materials()[0U].Kind(), + fesa::MaterialKind::kIsotropicLinearElastic); + EXPECT_EQ(&domain.LinearElasticMaterials()[0U], &domain.Materials()[0U]); + + ASSERT_EQ(domain.Properties().Size(), 2U); + EXPECT_EQ(domain.Properties()[0U].Kind(), + fesa::ElementPropertyKind::kGeneralBeamSection); + EXPECT_EQ(domain.Properties()[1U].Kind(), + fesa::ElementPropertyKind::kShellSection); + EXPECT_EQ(&domain.Sections()[0U], &domain.Properties()[0U]); + EXPECT_EQ(&domain.ShellSections()[0U], &domain.Properties()[1U]); +} + TEST(DomainModel, ImmutableOwnershipPreservesStableOrder) { auto definition = MakeOwnedDefinition(); auto result = fesa::Domain::Create(definition); @@ -81,9 +167,9 @@ TEST(DomainModel, ImmutableOwnershipPreservesStableOrder) { static_assert( std::is_same_v().Nodes()), const std::vector&>); - static_assert( - std::is_same_v().Elements()), - const std::vector&>); + static_assert(std::is_same_v< + decltype(std::declval().Elements()), + const fesa::DomainCollectionView&>); EXPECT_EQ(domain.SourcePath(), std::filesystem::path{"models/owned.inp"}); EXPECT_EQ(domain.SourceContentIdentity(), "fnv1a64:fedcba9876543210"); @@ -94,11 +180,11 @@ TEST(DomainModel, ImmutableOwnershipPreservesStableOrder) { EXPECT_EQ(domain.Nodes()[1].source_id.source_label, 10); EXPECT_EQ(domain.Nodes().data(), first_node_address); - ASSERT_EQ(domain.Elements().size(), 1U); - EXPECT_EQ(domain.Elements()[0].node_indices[0], 1U); - EXPECT_EQ(domain.Elements()[0].node_indices[1], 0U); - ASSERT_EQ(domain.Materials().size(), 1U); - ASSERT_EQ(domain.Sections().size(), 1U); + ASSERT_EQ(domain.BeamElements().Size(), 1U); + EXPECT_EQ(domain.BeamElements()[0].node_indices[0], 1U); + EXPECT_EQ(domain.BeamElements()[0].node_indices[1], 0U); + ASSERT_EQ(domain.Materials().Size(), 1U); + ASSERT_EQ(domain.Sections().Size(), 1U); EXPECT_DOUBLE_EQ(domain.Sections()[0].section_points[0][0], -0.1); ASSERT_EQ(domain.NodeSets().size(), 1U); EXPECT_EQ(domain.NodeSets()[0].node_indices[0], 0U); @@ -189,13 +275,13 @@ TEST(DomainModel, MultipleIdentityInstancesDoNotMerge) { EXPECT_NE(domain.Nodes()[0].source_id.instance_name, domain.Nodes()[2].source_id.instance_name); - ASSERT_EQ(domain.Elements().size(), 2U); - EXPECT_EQ(domain.Elements()[0].source_id.source_label, 1); - EXPECT_EQ(domain.Elements()[1].source_id.source_label, 1); - EXPECT_EQ(domain.Elements()[0].source_id.instance_name, "Instance-A"); - EXPECT_EQ(domain.Elements()[1].source_id.instance_name, "Instance-B"); - EXPECT_EQ(domain.Elements()[0].node_indices[0], 0U); - EXPECT_EQ(domain.Elements()[1].node_indices[0], 2U); + ASSERT_EQ(domain.BeamElements().Size(), 2U); + EXPECT_EQ(domain.BeamElements()[0].source_id.source_label, 1); + EXPECT_EQ(domain.BeamElements()[1].source_id.source_label, 1); + EXPECT_EQ(domain.BeamElements()[0].source_id.instance_name, "Instance-A"); + EXPECT_EQ(domain.BeamElements()[1].source_id.instance_name, "Instance-B"); + EXPECT_EQ(domain.BeamElements()[0].node_indices[0], 0U); + EXPECT_EQ(domain.BeamElements()[1].node_indices[0], 2U); } // MITC4-MODEL-002 @@ -235,16 +321,16 @@ TEST(DomainModel, ShellOwnershipPreservesResolvedAssignmentsAndOptionalFrames) { const fesa::Domain& domain = result.Value(); static_assert(std::is_same_v< decltype(std::declval().ShellElements()), - const std::vector&>); + const fesa::DomainCollectionView&>); static_assert(std::is_same_v< decltype(std::declval().ShellSections()), - const std::vector&>); + const fesa::DomainCollectionView&>); static_assert( std::is_same_v() .ShellNodeInitialFrames()), const std::vector&>); - ASSERT_EQ(domain.ShellElements().size(), 2U); + ASSERT_EQ(domain.ShellElements().Size(), 2U); EXPECT_EQ(domain.ShellElements()[0].source_id.source_label, 20); EXPECT_EQ(domain.ShellElements()[0].source_id.source_label_text, "0020"); EXPECT_EQ(domain.ShellElements()[1].source_id.source_label, 10); @@ -256,7 +342,7 @@ TEST(DomainModel, ShellOwnershipPreservesResolvedAssignmentsAndOptionalFrames) { EXPECT_EQ(domain.ShellElements()[0].material_index, 0U); EXPECT_EQ(domain.ShellElements()[0].section_index, 0U); - ASSERT_EQ(domain.ShellSections().size(), 2U); + ASSERT_EQ(domain.ShellSections().Size(), 2U); EXPECT_EQ(domain.ShellSections()[0].name, "Section-B"); EXPECT_DOUBLE_EQ(domain.ShellSections()[0].thickness, 0.02); EXPECT_EQ(domain.ShellSections()[0].material_index, 0U); diff --git a/tests/unit/results/result_recovery_test.cpp b/tests/unit/results/result_recovery_test.cpp index 0ba1260..8a6b268 100644 --- a/tests/unit/results/result_recovery_test.cpp +++ b/tests/unit/results/result_recovery_test.cpp @@ -318,7 +318,7 @@ void ExpectScaledNear(const double actual, const double expected, std::vector MakeStationRows( const RecoveryFixture& fixture) { const auto& nodes = fixture.domain->Nodes(); - const auto& elements = fixture.domain->Elements(); + const auto& elements = fixture.domain->BeamElements(); return {{0U, 0, nodes[elements[0U].node_indices[0U]].source_id,