diff --git a/include/fesa/core/source_identity.h b/include/fesa/core/source_identity.h index c592dfc..3890d79 100644 --- a/include/fesa/core/source_identity.h +++ b/include/fesa/core/source_identity.h @@ -8,6 +8,9 @@ namespace fesa { +/// @brief Identifies a semantic entity by its stable collection position. +using EntityIndex = std::uint32_t; + /// @brief Identifies the input location that produced an item or diagnostic. struct SourceLocation { std::filesystem::path file; diff --git a/include/fesa/materials/isotropic_linear_elastic_material.h b/include/fesa/materials/isotropic_linear_elastic_material.h new file mode 100644 index 0000000..cc1fd21 --- /dev/null +++ b/include/fesa/materials/isotropic_linear_elastic_material.h @@ -0,0 +1,68 @@ +#ifndef FESA_MATERIALS_ISOTROPIC_LINEAR_ELASTIC_MATERIAL_H_ +#define FESA_MATERIALS_ISOTROPIC_LINEAR_ELASTIC_MATERIAL_H_ + +#include + +#include "fesa/core/status.h" +#include "fesa/materials/material.h" + +namespace fesa { + +/// @brief Stores homogeneous isotropic linear-elastic material data. +/// @note Poisson ratios above 0.5 remain valid for the approved beam subset; +/// shell compatibility is checked by the shell kernel. +class IsotropicLinearElasticMaterial final : public Material { + public: + /// @brief Creates a material after validating the current elastic fields. + /// @param source_id Stable semantic identity supplied by the mapper. + /// @param name Source material name. + /// @param youngs_modulus Young's modulus in the active consistent unit + /// system. + /// @param poissons_ratio Dimensionless Poisson ratio. + /// @param location Source MATERIAL keyword location. + /// @return A material or a structured model failure. + static Result Create(SourceEntityId source_id, + std::string name, + double youngs_modulus, + double poissons_ratio, + SourceLocation location); + + /// @brief Constructs an already validated parser-owned material record. + /// @note This compatibility seam preserves existing semantic mapping until + /// Domain polymorphic ownership is migrated. + IsotropicLinearElasticMaterial(std::string name, double youngs_modulus, + double poissons_ratio, + SourceLocation location); + + MaterialKind Kind() const noexcept override; + const SourceEntityId& SourceId() const noexcept override; + const SourceLocation& Location() const noexcept override; + + /// @brief Returns the source material name. + const std::string& Name() const noexcept; + + /// @brief Returns Young's modulus in the active consistent unit system. + double YoungsModulus() const noexcept; + + /// @brief Returns the dimensionless Poisson ratio. + double PoissonsRatio() const noexcept; + + // Public storage preserves the current semantic-record API until Domain + // ownership migrates in the next approved Step. + std::string name; + double youngs_modulus; + double poisson_ratio; + SourceLocation location; + + private: + /// @brief Constructs a candidate whose fields have already been checked. + IsotropicLinearElasticMaterial(SourceEntityId source_id, std::string name, + double youngs_modulus, double poissons_ratio, + SourceLocation location); + + SourceEntityId source_id_; +}; + +} // namespace fesa + +#endif // FESA_MATERIALS_ISOTROPIC_LINEAR_ELASTIC_MATERIAL_H_ diff --git a/include/fesa/materials/material.h b/include/fesa/materials/material.h new file mode 100644 index 0000000..5643b3d --- /dev/null +++ b/include/fesa/materials/material.h @@ -0,0 +1,28 @@ +#ifndef FESA_MATERIALS_MATERIAL_H_ +#define FESA_MATERIALS_MATERIAL_H_ + +#include "fesa/core/source_identity.h" + +namespace fesa { + +/// @brief Identifies the supported concrete material semantics. +enum class MaterialKind { kIsotropicLinearElastic }; + +/// @brief Provides stable identity for a Domain-owned material definition. +class Material { + public: + virtual ~Material() = default; + + /// @brief Returns the concrete material kind. + virtual MaterialKind Kind() const noexcept = 0; + + /// @brief Returns the stable source identity preserved for diagnostics. + virtual const SourceEntityId& SourceId() const noexcept = 0; + + /// @brief Returns the input location that defined the material. + virtual const SourceLocation& Location() const noexcept = 0; +}; + +} // namespace fesa + +#endif // FESA_MATERIALS_MATERIAL_H_ diff --git a/include/fesa/model/model_types.h b/include/fesa/model/model_types.h index ad9e2e4..76e7370 100644 --- a/include/fesa/model/model_types.h +++ b/include/fesa/model/model_types.h @@ -11,12 +11,12 @@ #include "fesa/core/diagnostic.h" #include "fesa/core/source_identity.h" +#include "fesa/materials/isotropic_linear_elastic_material.h" +#include "fesa/properties/general_beam_section.h" +#include "fesa/properties/shell_section.h" namespace fesa { -/// @brief Identifies a semantic entity by its stable collection position. -using EntityIndex = std::uint32_t; - /// @brief Stores one source node and its global coordinates. struct Node { SourceEntityId source_id; @@ -24,26 +24,8 @@ struct Node { SourceLocation location; }; -/// @brief Stores the supported homogeneous isotropic elastic properties. -struct LinearElasticMaterial { - std::string name; - double youngs_modulus; - double poisson_ratio; - SourceLocation location; -}; - -/// @brief Stores the supported general Euler beam section properties. -struct GeneralBeamSection { - std::string name; - double area; - double i11; - double i12; - double i22; - double torsional_constant; - std::array first_axis; - std::vector> section_points; - 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. @@ -52,14 +34,6 @@ 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 Stores a centered constant-thickness shell section assignment. -struct ShellSection { - std::string name; - double thickness; - EntityIndex material_index; - SourceLocation location; -}; - /// @brief Defines one four-node shell with stable semantic references. struct Mitc4ShellDefinition { SourceEntityId source_id; diff --git a/include/fesa/properties/element_property.h b/include/fesa/properties/element_property.h new file mode 100644 index 0000000..ce889aa --- /dev/null +++ b/include/fesa/properties/element_property.h @@ -0,0 +1,28 @@ +#ifndef FESA_PROPERTIES_ELEMENT_PROPERTY_H_ +#define FESA_PROPERTIES_ELEMENT_PROPERTY_H_ + +#include "fesa/core/source_identity.h" + +namespace fesa { + +/// @brief Identifies the supported concrete element-property semantics. +enum class ElementPropertyKind { kGeneralBeamSection, kShellSection }; + +/// @brief Provides stable identity for a Domain-owned element property. +class ElementProperty { + public: + virtual ~ElementProperty() = default; + + /// @brief Returns the concrete property kind. + virtual ElementPropertyKind Kind() const noexcept = 0; + + /// @brief Returns the stable source identity preserved for diagnostics. + virtual const SourceEntityId& SourceId() const noexcept = 0; + + /// @brief Returns the input location that defined the property. + virtual const SourceLocation& Location() const noexcept = 0; +}; + +} // namespace fesa + +#endif // FESA_PROPERTIES_ELEMENT_PROPERTY_H_ diff --git a/include/fesa/properties/general_beam_section.h b/include/fesa/properties/general_beam_section.h new file mode 100644 index 0000000..7fdeeb4 --- /dev/null +++ b/include/fesa/properties/general_beam_section.h @@ -0,0 +1,83 @@ +#ifndef FESA_PROPERTIES_GENERAL_BEAM_SECTION_H_ +#define FESA_PROPERTIES_GENERAL_BEAM_SECTION_H_ + +#include +#include +#include + +#include "fesa/core/status.h" +#include "fesa/properties/element_property.h" + +namespace fesa { + +/// @brief Stores the approved general Euler-beam section properties. +class GeneralBeamSection final : public ElementProperty { + public: + /// @brief Creates a validated general beam section. + /// @param first_axis Abaqus first section axis in global coordinates. + /// @param section_points Optional section-point coordinates `(x1,x2)`. + /// @return A section or a structured model failure. + static Result Create( + SourceEntityId source_id, std::string name, double area, double i11, + double i12, double i22, double torsional_constant, + std::array first_axis, + std::vector> section_points, + SourceLocation location); + + /// @brief Constructs an already validated parser-owned section record. + /// @note This compatibility seam preserves current consumers until Domain + /// polymorphic ownership is migrated. + GeneralBeamSection(std::string name, double area, double i11, double i12, + double i22, double torsional_constant, + std::array first_axis, + std::vector> section_points, + SourceLocation location); + + ElementPropertyKind Kind() const noexcept override; + const SourceEntityId& SourceId() const noexcept override; + const SourceLocation& Location() const noexcept override; + + /// @brief Returns the source section name. + const std::string& Name() const noexcept; + /// @brief Returns cross-sectional area. + double Area() const noexcept; + /// @brief Returns the first principal section inertia input. + double I11() const noexcept; + /// @brief Returns the cross-bending inertia, which is exactly zero in V0. + double I12() const noexcept; + /// @brief Returns the second principal section inertia input. + double I22() const noexcept; + /// @brief Returns the Saint-Venant torsional constant. + double TorsionalConstant() const noexcept; + /// @brief Returns the Abaqus first section axis in global coordinates. + const std::array& FirstAxis() const noexcept; + /// @brief Returns optional section points in source order. + const std::vector>& SectionPoints() const noexcept; + + // Public storage preserves the current semantic-record API until Domain + // ownership migrates in the next approved Step. + std::string name; + double area; + double i11; + double i12; + double i22; + double torsional_constant; + std::array first_axis; + std::vector> section_points; + SourceLocation location; + + private: + /// @brief Constructs a candidate whose fields have already been checked. + GeneralBeamSection(SourceEntityId source_id, std::string name, double area, + double i11, double i12, double i22, + double torsional_constant, + std::array first_axis, + std::vector> section_points, + SourceLocation location); + + SourceEntityId source_id_; +}; + +} // namespace fesa + +#endif // FESA_PROPERTIES_GENERAL_BEAM_SECTION_H_ diff --git a/include/fesa/properties/shell_section.h b/include/fesa/properties/shell_section.h new file mode 100644 index 0000000..890fd11 --- /dev/null +++ b/include/fesa/properties/shell_section.h @@ -0,0 +1,57 @@ +#ifndef FESA_PROPERTIES_SHELL_SECTION_H_ +#define FESA_PROPERTIES_SHELL_SECTION_H_ + +#include + +#include "fesa/core/status.h" +#include "fesa/properties/element_property.h" + +namespace fesa { + +/// @brief Stores a centered constant-thickness shell section assignment. +class ShellSection final : public ElementProperty { + public: + /// @brief Creates a validated shell section. + /// @param thickness Positive thickness in the active consistent unit system. + /// @param material_index Stable Domain material collection position. + /// @return A section or a structured model failure. + static Result Create(SourceEntityId source_id, std::string name, + double thickness, + EntityIndex material_index, + SourceLocation location); + + /// @brief Constructs an already validated parser-owned shell record. + /// @note This compatibility seam preserves current consumers until Domain + /// polymorphic ownership is migrated. + ShellSection(std::string name, double thickness, EntityIndex material_index, + SourceLocation location); + + ElementPropertyKind Kind() const noexcept override; + const SourceEntityId& SourceId() const noexcept override; + const SourceLocation& Location() const noexcept override; + + /// @brief Returns the source shell-section name. + const std::string& Name() const noexcept; + /// @brief Returns centered shell thickness. + double Thickness() const noexcept; + /// @brief Returns the stable Domain material collection position. + EntityIndex MaterialIndex() const noexcept; + + // Public storage preserves the current semantic-record API until Domain + // ownership migrates in the next approved Step. + std::string name; + double thickness; + EntityIndex material_index; + SourceLocation location; + + private: + /// @brief Constructs a candidate whose fields have already been checked. + ShellSection(SourceEntityId source_id, std::string name, double thickness, + EntityIndex material_index, SourceLocation location); + + SourceEntityId source_id_; +}; + +} // namespace fesa + +#endif // FESA_PROPERTIES_SHELL_SECTION_H_ diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index af5178d..847f6c3 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -19,6 +19,7 @@ add_library( io/abaqus/domain_mapper.cpp io/abaqus/input_reader.cpp io/hdf5/hdf5_results_writer.cpp + materials/isotropic_linear_elastic_material.cpp math/dense_blas_internal.cpp math/matrix.cpp math/sparse_matrix.cpp @@ -26,6 +27,8 @@ add_library( model/domain.cpp model/shell_geometry.cpp model/source_target_resolver.cpp + properties/general_beam_section.cpp + properties/shell_section.cpp results/result_recovery.cpp solvers/linear/mkl_pardiso_solver.cpp ) diff --git a/src/fesa/materials/isotropic_linear_elastic_material.cpp b/src/fesa/materials/isotropic_linear_elastic_material.cpp new file mode 100644 index 0000000..5d44847 --- /dev/null +++ b/src/fesa/materials/isotropic_linear_elastic_material.cpp @@ -0,0 +1,89 @@ +#include "fesa/materials/isotropic_linear_elastic_material.h" + +#include +#include +#include + +namespace fesa { +namespace { + +Status InvalidMaterial(const SourceEntityId& source_id, + const SourceLocation& location, + const std::string& message) { + return Status::Failure(FailureCategory::kModel, + {{Severity::kError, "invalid-material", location, + "*MATERIAL", source_id.source_label_text, message}}); +} + +} // namespace + +Result IsotropicLinearElasticMaterial::Create( + SourceEntityId source_id, std::string name, double youngs_modulus, + double poissons_ratio, SourceLocation location) { + if (name.empty() || source_id.source_label_text.empty()) { + return Result::Failure(InvalidMaterial( + source_id, location, "Material source identity must not be empty.")); + } + + const double denominator = 2.0 * (1.0 + poissons_ratio); + const double shear_modulus = youngs_modulus / denominator; + if (!std::isfinite(youngs_modulus) || !(youngs_modulus > 0.0) || + !std::isfinite(poissons_ratio) || !std::isfinite(shear_modulus) || + !(shear_modulus > 0.0)) { + return Result::Failure(InvalidMaterial( + source_id, location, + "E and the derived G=E/(2*(1+nu)) must be finite and positive.")); + } + + return Result::Success( + IsotropicLinearElasticMaterial{std::move(source_id), std::move(name), + youngs_modulus, poissons_ratio, + std::move(location)}); +} + +IsotropicLinearElasticMaterial::IsotropicLinearElasticMaterial( + std::string name_value, double youngs_modulus_value, + double poissons_ratio_value, SourceLocation location_value) + : name{std::move(name_value)}, + youngs_modulus{youngs_modulus_value}, + poisson_ratio{poissons_ratio_value}, + location{std::move(location_value)}, + source_id_{"", 0, name} {} + +MaterialKind IsotropicLinearElasticMaterial::Kind() const noexcept { + return MaterialKind::kIsotropicLinearElastic; +} + +const SourceEntityId& IsotropicLinearElasticMaterial::SourceId() + const noexcept { + return source_id_; +} + +const SourceLocation& IsotropicLinearElasticMaterial::Location() + const noexcept { + return location; +} + +const std::string& IsotropicLinearElasticMaterial::Name() const noexcept { + return name; +} + +double IsotropicLinearElasticMaterial::YoungsModulus() const noexcept { + return youngs_modulus; +} + +double IsotropicLinearElasticMaterial::PoissonsRatio() const noexcept { + return poisson_ratio; +} + +IsotropicLinearElasticMaterial::IsotropicLinearElasticMaterial( + SourceEntityId source_id, std::string name_value, + double youngs_modulus_value, double poissons_ratio_value, + SourceLocation location_value) + : name{std::move(name_value)}, + youngs_modulus{youngs_modulus_value}, + poisson_ratio{poissons_ratio_value}, + location{std::move(location_value)}, + source_id_{std::move(source_id)} {} + +} // namespace fesa diff --git a/src/fesa/properties/general_beam_section.cpp b/src/fesa/properties/general_beam_section.cpp new file mode 100644 index 0000000..ddc3959 --- /dev/null +++ b/src/fesa/properties/general_beam_section.cpp @@ -0,0 +1,140 @@ +#include "fesa/properties/general_beam_section.h" + +#include +#include +#include +#include + +namespace fesa { +namespace { + +Status InvalidBeamSection(const std::string& code, + const SourceEntityId& source_id, + const SourceLocation& location, + const std::string& message) { + return Status::Failure( + FailureCategory::kModel, + {{Severity::kError, code, location, "*BEAM GENERAL SECTION", + source_id.source_label_text, message}}); +} + +bool IsFinitePoint(const std::array& point) { + return std::isfinite(point[0]) && std::isfinite(point[1]); +} + +} // namespace + +Result GeneralBeamSection::Create( + SourceEntityId source_id, std::string name, double area, double i11, + double i12, double i22, double torsional_constant, + std::array first_axis, + std::vector> section_points, + SourceLocation location) { + if (name.empty() || source_id.source_label_text.empty()) { + return Result::Failure( + InvalidBeamSection("invalid-beam-property", source_id, location, + "Beam section source identity must not be empty.")); + } + if (!std::isfinite(i12) || i12 != 0.0) { + return Result::Failure(InvalidBeamSection( + std::isfinite(i12) ? "unsupported-coupled-section" + : "invalid-beam-property", + source_id, location, + "The general beam section requires finite exact I12=0.")); + } + const std::array positive_properties = {area, i11, i22, + torsional_constant}; + if (std::any_of(positive_properties.begin(), positive_properties.end(), + [](double property) { + return !std::isfinite(property) || !(property > 0.0); + }) || + std::any_of(first_axis.begin(), first_axis.end(), + [](double component) { return !std::isfinite(component); })) { + return Result::Failure(InvalidBeamSection( + "invalid-beam-property", source_id, location, + "A, I11, I22, J, and first-axis components must be finite; section " + "magnitudes must be positive.")); + } + if (std::any_of(section_points.begin(), section_points.end(), + [](const auto& point) { return !IsFinitePoint(point); })) { + return Result::Failure( + InvalidBeamSection("invalid-section-point", source_id, location, + "Section-point coordinates must be finite.")); + } + + return Result::Success( + GeneralBeamSection{std::move(source_id), std::move(name), area, i11, i12, + i22, torsional_constant, first_axis, + std::move(section_points), std::move(location)}); +} + +GeneralBeamSection::GeneralBeamSection( + std::string name_value, double area_value, double i11_value, + double i12_value, double i22_value, double torsional_constant_value, + std::array first_axis_value, + std::vector> section_points_value, + SourceLocation location_value) + : name{std::move(name_value)}, + area{area_value}, + i11{i11_value}, + i12{i12_value}, + i22{i22_value}, + torsional_constant{torsional_constant_value}, + first_axis{first_axis_value}, + section_points{std::move(section_points_value)}, + location{std::move(location_value)}, + source_id_{"", 0, name} {} + +ElementPropertyKind GeneralBeamSection::Kind() const noexcept { + return ElementPropertyKind::kGeneralBeamSection; +} + +const SourceEntityId& GeneralBeamSection::SourceId() const noexcept { + return source_id_; +} + +const SourceLocation& GeneralBeamSection::Location() const noexcept { + return location; +} + +const std::string& GeneralBeamSection::Name() const noexcept { return name; } + +double GeneralBeamSection::Area() const noexcept { return area; } + +double GeneralBeamSection::I11() const noexcept { return i11; } + +double GeneralBeamSection::I12() const noexcept { return i12; } + +double GeneralBeamSection::I22() const noexcept { return i22; } + +double GeneralBeamSection::TorsionalConstant() const noexcept { + return torsional_constant; +} + +const std::array& GeneralBeamSection::FirstAxis() const noexcept { + return first_axis; +} + +const std::vector>& GeneralBeamSection::SectionPoints() + const noexcept { + return section_points; +} + +GeneralBeamSection::GeneralBeamSection( + SourceEntityId source_id, std::string name_value, double area_value, + double i11_value, double i12_value, double i22_value, + double torsional_constant_value, std::array first_axis_value, + std::vector> section_points_value, + SourceLocation location_value) + : name{std::move(name_value)}, + area{area_value}, + i11{i11_value}, + i12{i12_value}, + i22{i22_value}, + torsional_constant{torsional_constant_value}, + first_axis{first_axis_value}, + section_points{std::move(section_points_value)}, + location{std::move(location_value)}, + source_id_{std::move(source_id)} {} + +} // namespace fesa diff --git a/src/fesa/properties/shell_section.cpp b/src/fesa/properties/shell_section.cpp new file mode 100644 index 0000000..01654fd --- /dev/null +++ b/src/fesa/properties/shell_section.cpp @@ -0,0 +1,78 @@ +#include "fesa/properties/shell_section.h" + +#include +#include +#include + +namespace fesa { +namespace { + +Status InvalidShellSection(const SourceEntityId& source_id, + const SourceLocation& location, + const std::string& message) { + return Status::Failure( + FailureCategory::kModel, + {{Severity::kError, "invalid-shell-thickness", location, "*SHELL SECTION", + source_id.source_label_text, message}}); +} + +} // namespace + +Result ShellSection::Create(SourceEntityId source_id, + std::string name, double thickness, + EntityIndex material_index, + SourceLocation location) { + if (name.empty() || source_id.source_label_text.empty()) { + return Result::Failure(InvalidShellSection( + source_id, location, + "Shell section source identity must not be empty.")); + } + if (!std::isfinite(thickness) || !(thickness > 0.0)) { + return Result::Failure(InvalidShellSection( + source_id, location, "Shell thickness must be finite and positive.")); + } + return Result::Success( + ShellSection{std::move(source_id), std::move(name), thickness, + material_index, std::move(location)}); +} + +ShellSection::ShellSection(std::string name_value, double thickness_value, + EntityIndex material_index_value, + SourceLocation location_value) + : name{std::move(name_value)}, + thickness{thickness_value}, + material_index{material_index_value}, + location{std::move(location_value)}, + source_id_{"", 0, name} {} + +ElementPropertyKind ShellSection::Kind() const noexcept { + return ElementPropertyKind::kShellSection; +} + +const SourceEntityId& ShellSection::SourceId() const noexcept { + return source_id_; +} + +const SourceLocation& ShellSection::Location() const noexcept { + return location; +} + +const std::string& ShellSection::Name() const noexcept { return name; } + +double ShellSection::Thickness() const noexcept { return thickness; } + +EntityIndex ShellSection::MaterialIndex() const noexcept { + return material_index; +} + +ShellSection::ShellSection(SourceEntityId source_id, std::string name_value, + double thickness_value, + EntityIndex material_index_value, + SourceLocation location_value) + : name{std::move(name_value)}, + thickness{thickness_value}, + material_index{material_index_value}, + location{std::move(location_value)}, + source_id_{std::move(source_id)} {} + +} // namespace fesa diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e9df759..5ade579 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,6 +29,8 @@ add_executable( unit/model/model_types_test.cpp unit/model/shell_geometry_test.cpp unit/model/source_target_resolver_test.cpp + unit/materials/material_test.cpp + unit/properties/element_property_test.cpp unit/results/result_records_test.cpp unit/results/result_recovery_test.cpp unit/results/results_writer_test.cpp diff --git a/tests/unit/materials/material_test.cpp b/tests/unit/materials/material_test.cpp new file mode 100644 index 0000000..a06b029 --- /dev/null +++ b/tests/unit/materials/material_test.cpp @@ -0,0 +1,63 @@ +#include + +#include +#include +#include +#include +#include + +#include "fesa/materials/isotropic_linear_elastic_material.h" + +namespace { + +fesa::SourceEntityId MaterialSourceId() { return {"", 7, "Steel"}; } + +fesa::SourceLocation MaterialLocation() { return {"models/material.inp", 30U}; } + +} // namespace + +TEST(Material, OwnsIsotropicElasticityThroughVirtualBase) { + static_assert(std::has_virtual_destructor_v); + + auto candidate = fesa::IsotropicLinearElasticMaterial::Create( + MaterialSourceId(), "Steel", 210.0e9, 0.3, MaterialLocation()); + ASSERT_TRUE(candidate.HasValue()); + + std::unique_ptr material = + std::make_unique( + std::move(candidate.Value())); + EXPECT_EQ(material->Kind(), fesa::MaterialKind::kIsotropicLinearElastic); + EXPECT_EQ(material->SourceId().source_label, 7); + EXPECT_EQ(material->SourceId().source_label_text, "Steel"); + + const auto& isotropic = + static_cast(*material); + EXPECT_EQ(isotropic.Name(), "Steel"); + EXPECT_DOUBLE_EQ(isotropic.YoungsModulus(), 210.0e9); + EXPECT_DOUBLE_EQ(isotropic.PoissonsRatio(), 0.3); + EXPECT_EQ(isotropic.Location().file, "models/material.inp"); + EXPECT_EQ(isotropic.Location().line, 30U); +} + +TEST(Material, RejectsInvalidCurrentElasticFields) { + const auto expect_rejected = [](double youngs_modulus, + double poissons_ratio) { + const auto result = fesa::IsotropicLinearElasticMaterial::Create( + MaterialSourceId(), "Steel", youngs_modulus, poissons_ratio, + MaterialLocation()); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(result.GetStatus().Diagnostics()[0].severity, + fesa::Severity::kError); + }; + + expect_rejected(0.0, 0.3); + expect_rejected(std::numeric_limits::infinity(), 0.3); + expect_rejected(210.0e9, -1.0); + expect_rejected(210.0e9, std::numeric_limits::quiet_NaN()); + + const auto beam_compatible = fesa::IsotropicLinearElasticMaterial::Create( + MaterialSourceId(), "Steel", 210.0e9, 0.75, MaterialLocation()); + EXPECT_TRUE(beam_compatible.HasValue()); +} diff --git a/tests/unit/properties/element_property_test.cpp b/tests/unit/properties/element_property_test.cpp new file mode 100644 index 0000000..b406bbe --- /dev/null +++ b/tests/unit/properties/element_property_test.cpp @@ -0,0 +1,99 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "fesa/properties/general_beam_section.h" +#include "fesa/properties/shell_section.h" + +namespace { + +fesa::SourceLocation PropertyLocation() { return {"models/property.inp", 40U}; } + +} // namespace + +TEST(ElementProperty, OwnsGeneralBeamSectionThroughVirtualBase) { + static_assert(std::has_virtual_destructor_v); + + const fesa::SourceEntityId source_id{"", 4, "BeamSection"}; + auto candidate = fesa::GeneralBeamSection::Create( + source_id, "BeamSection", 0.02, 1.0e-5, 0.0, 2.0e-5, 5.0e-6, + {0.0, 1.0, 0.0}, {{-0.1, 0.0}, {0.1, 0.0}}, PropertyLocation()); + ASSERT_TRUE(candidate.HasValue()); + + std::unique_ptr property = + std::make_unique(std::move(candidate.Value())); + EXPECT_EQ(property->Kind(), fesa::ElementPropertyKind::kGeneralBeamSection); + EXPECT_EQ(property->SourceId().source_label, 4); + EXPECT_EQ(property->SourceId().source_label_text, "BeamSection"); + + const auto& section = static_cast(*property); + EXPECT_EQ(section.Name(), "BeamSection"); + EXPECT_DOUBLE_EQ(section.Area(), 0.02); + EXPECT_DOUBLE_EQ(section.I11(), 1.0e-5); + EXPECT_DOUBLE_EQ(section.I12(), 0.0); + EXPECT_DOUBLE_EQ(section.I22(), 2.0e-5); + EXPECT_DOUBLE_EQ(section.TorsionalConstant(), 5.0e-6); + EXPECT_EQ(section.FirstAxis(), (std::array{0.0, 1.0, 0.0})); + EXPECT_EQ(section.SectionPoints(), + (std::vector>{{-0.1, 0.0}, {0.1, 0.0}})); + EXPECT_EQ(section.Location().line, 40U); +} + +TEST(ElementProperty, OwnsShellSectionThroughVirtualBase) { + const fesa::SourceEntityId source_id{"", 8, "ShellSection"}; + auto candidate = + fesa::ShellSection::Create(source_id, "ShellSection", 0.01, + fesa::EntityIndex{3}, PropertyLocation()); + ASSERT_TRUE(candidate.HasValue()); + + std::unique_ptr property = + std::make_unique(std::move(candidate.Value())); + EXPECT_EQ(property->Kind(), fesa::ElementPropertyKind::kShellSection); + EXPECT_EQ(property->SourceId().source_label, 8); + + const auto& section = static_cast(*property); + EXPECT_EQ(section.Name(), "ShellSection"); + EXPECT_DOUBLE_EQ(section.Thickness(), 0.01); + EXPECT_EQ(section.MaterialIndex(), fesa::EntityIndex{3}); + EXPECT_EQ(section.Location().file, "models/property.inp"); +} + +TEST(ElementProperty, PreservesEmptyOptionalSectionPointInventory) { + const auto section = fesa::GeneralBeamSection::Create( + {"", 4, "BeamSection"}, "BeamSection", 0.02, 1.0e-5, 0.0, 2.0e-5, 5.0e-6, + {0.0, 1.0, 0.0}, {}, PropertyLocation()); + ASSERT_TRUE(section.HasValue()); + EXPECT_TRUE(section.Value().SectionPoints().empty()); +} + +TEST(ElementProperty, RejectsInvalidCurrentSectionFields) { + const auto invalid_beam = fesa::GeneralBeamSection::Create( + {"", 4, "BeamSection"}, "BeamSection", 0.0, 1.0e-5, 0.0, 2.0e-5, 5.0e-6, + {0.0, 1.0, 0.0}, {}, PropertyLocation()); + ASSERT_FALSE(invalid_beam.HasValue()); + EXPECT_EQ(invalid_beam.GetStatus().Category(), fesa::FailureCategory::kModel); + + const auto coupled_beam = fesa::GeneralBeamSection::Create( + {"", 4, "BeamSection"}, "BeamSection", 0.02, 1.0e-5, 1.0e-8, 2.0e-5, + 5.0e-6, {0.0, 1.0, 0.0}, {}, PropertyLocation()); + EXPECT_FALSE(coupled_beam.HasValue()); + + const auto nonfinite_point = fesa::GeneralBeamSection::Create( + {"", 4, "BeamSection"}, "BeamSection", 0.02, 1.0e-5, 0.0, 2.0e-5, 5.0e-6, + {0.0, 1.0, 0.0}, {{std::numeric_limits::quiet_NaN(), 0.0}}, + PropertyLocation()); + EXPECT_FALSE(nonfinite_point.HasValue()); + + const auto invalid_shell = + fesa::ShellSection::Create({"", 8, "ShellSection"}, "ShellSection", 0.0, + fesa::EntityIndex{3}, PropertyLocation()); + ASSERT_FALSE(invalid_shell.HasValue()); + EXPECT_EQ(invalid_shell.GetStatus().Category(), + fesa::FailureCategory::kModel); +}