diff --git a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md index 8b249ac..036ac63 100644 --- a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md +++ b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md @@ -144,3 +144,48 @@ `git diff --exit-code -- reference/` exited 0. - scope: the deferred empty-dot review minor was not changed, reference artifacts remain untouched, and phase Step 9 status/timestamps were not modified. + +## Step 10 — domain-model + +- task_id: `TASK-10` +- status: `completed` +- changed_files: `include/fesa/model/model_types.hpp`, + `include/fesa/model/domain.hpp`, `src/fesa/model/domain.cpp`, + `tests/unit/model/model_types_test.cpp`, `tests/unit/model/domain_test.cpp`, + `src/fesa/CMakeLists.txt`, `tests/CMakeLists.txt`, + `docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md`, + `phases/linear-static-3d-euler-beam/index.json`, + `.superpowers/sdd/linear-static-3d-euler-beam/task-10-report.md` +- requirement_ids: `FESA-REQ-LS3DEB-015`, `FESA-REQ-LS3DEB-016`, + `FESA-REQ-LS3DEB-021`, `FESA-REQ-LS3DEB-030`, + `FESA-REQ-LS3DEB-034` +- test_ids: `T10-DOM-001`, `T10-DOM-002`, `T10-DOM-003` + +| stage | exact command | exit_code | expected_or_observed_result | evidence_tail | +| --- | --- | ---: | --- | --- | +| RED | `cmake --build .harness/build --config Debug --target fesa_tests` | 1 | Both planned test translation units are registered before production and fail for the missing model API | MSVC C1083 for `fesa/model/domain.hpp` and `fesa/model/model_types.hpp`; the subsequent `ctest --test-dir .harness/build -C Debug -R DomainModel --output-on-failure` exited 0 with `No tests were found` because the new test executable could not build | +| GREEN-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 0 | Minimal model records, immutable Domain ownership, and both tests compile and link | `domain.cpp`, `domain_test.cpp`, and `model_types_test.cpp` built; `fesa_solver.lib` and `fesa_unit_tests.exe` linked without a FESA warning under `/W4 /WX` | +| GREEN-test | `ctest --test-dir .harness/build -C Debug -R DomainModel --output-on-failure` | 0 | Source/internal identity, stable immutable ownership, and multiple-instance distinction pass | 3/3 exact `DomainModel` tests passed | +| VERIFY-configure | `cmake -S . -B .harness/build -A x64 -DFESA_GTEST_SOURCE_DIR=C:/git/googletest "-DMKL_DIR=C:/Program Files (x86)/Intel/oneAPI/mkl/2026.1/lib/cmake/mkl" "-DTBB_DIR=C:/Program Files (x86)/Intel/oneAPI/tbb/2023.1/lib/cmake/tbb" "-DHDF5_DIR=C:/Program Files/HDF_Group/HDF5/2.1.1/cmake"` | 0 | Approved MSVC x64 build tree regenerates with explicit dependencies | Configure and generate completed; oneMKL 2026.1 and the existing dependency targets resolved | +| VERIFY-build | `cmake --build .harness/build --config Debug` | 0 | Full Debug build passes without a new FESA warning | `fesa_solver.lib` and `fesa_unit_tests.exe` built under `/W4 /WX` | +| VERIFY-targeted | `ctest --test-dir .harness/build -C Debug -R DomainModel --output-on-failure` | 0 | Focused Step 10 suite remains green | 3/3 `DomainModel` tests passed | +| VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | CTest discovers the accumulated suite and all exact DomainModel names | 10 tests discovered with `linear-static-3d-euler-beam` and `unit` labels | +| VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | Full accumulated C++ suite has zero failures | 10/10 tests passed | +| VERIFY-dependency-direction | Backend scan over `include/fesa`; upward-include scan from `include/fesa/model`; core-to-model include scan from `include/fesa/core` using `rg -n` and fail-on-match wrappers | 0 | Public headers expose no MKL/PARDISO/TBB/HDF5 API and preserve `core -> model` dependency direction | backend leaks 0; model upward dependencies 0; core-to-model dependencies 0 | +| VERIFY-diff | `git diff --check` | 0 | Patch has no whitespace errors | Exit 0; only Git LF-to-CRLF working-copy notices were emitted | +| VERIFY-reference | `git diff --exit-code -- reference/` | 0 | Approved reference artifacts remain unchanged | Exit 0 and `git status --short -- reference/` produced no entries | + +- contract_checks: every ledger record preserves `SourceLocation`; the static + step retains all four timing values; `PartDefinition`, `InstanceDefinition`, + and `SourceIndexMapping` retain source-to-internal identity; `ModelDefinition` + retains source path/content identity, heading, warnings, and all semantic + vectors. `Domain` owns the complete definition and returns const references; + declaration-order vector positions remain stable internal indices. Node and + beam definitions contain no equation-numbering state, and same-part identity + instances retain distinct source identities and connectivity. +- generated_evidence: `.harness/build/src/fesa/Debug/fesa_solver.lib`, + `.harness/build/tests/Debug/fesa_unit_tests.exe` +- reference_diff: unchanged; `git diff --exit-code -- reference/` exit 0 +- handoff: backend-free `EntityIndex`, semantic model records, + `ModelDefinition`, and immutable `Domain::create`/const accessors are + available to Step 11 syntax parsing and Step 12 semantic mapping. diff --git a/include/fesa/model/domain.hpp b/include/fesa/model/domain.hpp new file mode 100644 index 0000000..2e5a012 --- /dev/null +++ b/include/fesa/model/domain.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "fesa/core/status.hpp" +#include "fesa/model/model_types.hpp" + +#include +#include +#include + +namespace fesa { + +// Owns the complete semantic definition. Public access remains const so a +// vector position can serve as a stable internal index after construction. +class Domain { +public: + static Result create(ModelDefinition definition); + + const std::vector& nodes() const noexcept; + const std::vector& elements() const noexcept; + const std::vector& materials() const noexcept; + const std::vector& sections() const noexcept; + const std::vector& nodeSets() const noexcept; + const std::vector& elementSets() const noexcept; + const std::vector& steps() const noexcept; + const std::vector& warnings() const noexcept; + const std::filesystem::path& sourcePath() const noexcept; + const std::string& sourceContentIdentity() const noexcept; + +private: + explicit Domain(ModelDefinition definition); + + ModelDefinition definition_; +}; + +} // namespace fesa diff --git a/include/fesa/model/model_types.hpp b/include/fesa/model/model_types.hpp new file mode 100644 index 0000000..09d6c6d --- /dev/null +++ b/include/fesa/model/model_types.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include "fesa/core/diagnostic.hpp" +#include "fesa/core/source_identity.hpp" + +#include +#include +#include +#include +#include +#include + +namespace fesa { + +// Stable internal identities are vector positions assigned in declaration order. +using EntityIndex = std::uint32_t; + +struct Node { + SourceEntityId sourceId; + std::array coordinates; + SourceLocation location; +}; + +struct LinearElasticMaterial { + std::string name; + double youngsModulus; + double poissonRatio; + SourceLocation location; +}; + +struct GeneralBeamSection { + std::string name; + double area; + double i11; + double i12; + double i22; + double torsionalConstant; + std::array firstAxis; + std::vector> sectionPoints; + SourceLocation location; +}; + +struct EulerBeam3DDefinition { + SourceEntityId sourceId; + std::array nodeIndices; + EntityIndex materialIndex; + EntityIndex sectionIndex; + SourceLocation location; +}; + +struct BoundaryCondition { + std::string target; + int firstDof; + int lastDof; + double value; + SourceLocation location; +}; + +struct NodalLoad { + std::string target; + int dof; + double magnitude; + SourceLocation location; +}; + +struct StaticStepDefinition { + std::string name; + std::vector boundaries; + std::vector loads; + double initialIncrement; + double timePeriod; + double minimumIncrement; + double maximumIncrement; + SourceLocation location; +}; + +struct NodeSet { + std::string name; + std::optional instanceName; + std::vector nodeIndices; + SourceLocation location; +}; + +struct ElementSet { + std::string name; + std::optional instanceName; + std::vector elementIndices; + SourceLocation location; +}; + +struct PartDefinition { + std::string name; + std::vector nodeSourceLabels; + std::vector elementSourceLabels; + std::vector nodeSetNames; + std::vector elementSetNames; + SourceLocation location; +}; + +struct SourceIndexMapping { + std::int64_t sourceLabel; + EntityIndex internalIndex; +}; + +struct InstanceDefinition { + std::string name; + std::string partName; + std::vector nodeMappings; + std::vector elementMappings; + SourceLocation location; +}; + +// This construction-boundary value owns every parsed semantic record before +// it is finalized into an immutable Domain. +struct ModelDefinition { + std::filesystem::path sourcePath; + std::string sourceContentIdentity; + std::string heading; + std::vector nodes; + std::vector elements; + std::vector materials; + std::vector sections; + std::vector nodeSets; + std::vector elementSets; + std::vector parts; + std::vector instances; + std::vector steps; + std::vector warnings; +}; + +} // namespace fesa diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index 59b9bec..9568d7e 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -6,6 +6,7 @@ add_library( core/status.cpp math/matrix.cpp math/vector.cpp + model/domain.cpp ) target_include_directories( diff --git a/src/fesa/model/domain.cpp b/src/fesa/model/domain.cpp new file mode 100644 index 0000000..c22c3e5 --- /dev/null +++ b/src/fesa/model/domain.cpp @@ -0,0 +1,54 @@ +#include "fesa/model/domain.hpp" + +#include + +namespace fesa { + +Result Domain::create(ModelDefinition definition) { + return Result::success(Domain{std::move(definition)}); +} + +const std::vector& Domain::nodes() const noexcept { + return definition_.nodes; +} + +const std::vector& Domain::elements() const noexcept { + return definition_.elements; +} + +const std::vector& Domain::materials() const noexcept { + return definition_.materials; +} + +const std::vector& Domain::sections() const noexcept { + return definition_.sections; +} + +const std::vector& Domain::nodeSets() const noexcept { + return definition_.nodeSets; +} + +const std::vector& Domain::elementSets() const noexcept { + return definition_.elementSets; +} + +const std::vector& Domain::steps() const noexcept { + return definition_.steps; +} + +const std::vector& Domain::warnings() const noexcept { + return definition_.warnings; +} + +const std::filesystem::path& Domain::sourcePath() const noexcept { + return definition_.sourcePath; +} + +const std::string& Domain::sourceContentIdentity() const noexcept { + return definition_.sourceContentIdentity; +} + +Domain::Domain(ModelDefinition definition) + : definition_{std::move(definition)} {} + +} // namespace fesa diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 058f733..84c5473 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,8 @@ add_executable( unit/core/status_test.cpp unit/math/matrix_test.cpp unit/math/vector_test.cpp + unit/model/domain_test.cpp + unit/model/model_types_test.cpp ) target_link_libraries( diff --git a/tests/unit/model/domain_test.cpp b/tests/unit/model/domain_test.cpp new file mode 100644 index 0000000..b329576 --- /dev/null +++ b/tests/unit/model/domain_test.cpp @@ -0,0 +1,184 @@ +#include "fesa/model/domain.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace { + +fesa::ModelDefinition makeOwnedDefinition() { + fesa::ModelDefinition definition{}; + definition.sourcePath = "models/owned.inp"; + definition.sourceContentIdentity = "fnv1a64:fedcba9876543210"; + definition.heading = "Stable declaration order"; + definition.nodes = { + {{"Instance-A", 20, "0020"}, {2.0, 0.0, 0.0}, {"models/owned.inp", 12U}}, + {{"Instance-A", 10, "0010"}, {1.0, 0.0, 0.0}, {"models/owned.inp", 11U}}}; + definition.elements = {{ + {"Instance-A", 5, "0005"}, + {fesa::EntityIndex{1}, fesa::EntityIndex{0}}, + fesa::EntityIndex{0}, + fesa::EntityIndex{0}, + {"models/owned.inp", 20U}}}; + definition.materials = {{ + "Steel", 210.0e9, 0.3, {"models/owned.inp", 30U}}}; + definition.sections = {{ + "Section-1", + 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}}, + {"models/owned.inp", 40U}}}; + definition.nodeSets = {{ + "Tip", std::string{"Instance-A"}, {0U}, {"models/owned.inp", 45U}}}; + definition.elementSets = {{ + "Beam", std::string{"Instance-A"}, {0U}, {"models/owned.inp", 46U}}}; + definition.parts = {{ + "BeamPart", {20, 10}, {5}, {"Tip"}, {"Beam"}, + {"models/owned.inp", 2U}}}; + definition.instances = {{ + "Instance-A", + "BeamPart", + {{20, 0U}, {10, 1U}}, + {{5, 0U}}, + {"models/owned.inp", 50U}}}; + definition.steps = {{ + "Step-1", + {{"Root", 1, 6, 0.0, {"models/owned.inp", 60U}}}, + {{"Tip", 3, -1000.0, {"models/owned.inp", 61U}}}, + 0.1, + 1.0, + 1.0e-5, + 1.0, + {"models/owned.inp", 55U}}}; + definition.warnings = {{ + fesa::Severity::warning, + "ignored-output-request", + {"models/owned.inp", 70U}, + "*OUTPUT", + "", + "Ignored output request."}}; + return definition; +} + +} // namespace + +TEST(DomainModel, ImmutableOwnershipPreservesStableOrder) { + auto definition = makeOwnedDefinition(); + auto result = fesa::Domain::create(definition); + ASSERT_TRUE(result.hasValue()); + + definition.sourcePath = "mutated.inp"; + definition.sourceContentIdentity = "mutated"; + definition.nodes[0].sourceId.sourceLabelText = "mutated"; + definition.nodes[0].coordinates[0] = -99.0; + definition.sections[0].sectionPoints[0][0] = -99.0; + definition.steps[0].loads[0].magnitude = 99.0; + definition.warnings[0].code = "mutated"; + + const fesa::Domain& domain = result.value(); + const fesa::Node* const firstNodeAddress = domain.nodes().data(); + static_assert(std::is_same_v< + decltype(std::declval().nodes()), + const std::vector&>); + static_assert(std::is_same_v< + decltype(std::declval().elements()), + const std::vector&>); + + EXPECT_EQ(domain.sourcePath(), std::filesystem::path{"models/owned.inp"}); + EXPECT_EQ(domain.sourceContentIdentity(), "fnv1a64:fedcba9876543210"); + ASSERT_EQ(domain.nodes().size(), 2U); + EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabel, 20); + EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabelText, "0020"); + EXPECT_DOUBLE_EQ(domain.nodes()[0].coordinates[0], 2.0); + EXPECT_EQ(domain.nodes()[1].sourceId.sourceLabel, 10); + EXPECT_EQ(domain.nodes().data(), firstNodeAddress); + + ASSERT_EQ(domain.elements().size(), 1U); + EXPECT_EQ(domain.elements()[0].nodeIndices[0], 1U); + EXPECT_EQ(domain.elements()[0].nodeIndices[1], 0U); + ASSERT_EQ(domain.materials().size(), 1U); + ASSERT_EQ(domain.sections().size(), 1U); + EXPECT_DOUBLE_EQ(domain.sections()[0].sectionPoints[0][0], -0.1); + ASSERT_EQ(domain.nodeSets().size(), 1U); + EXPECT_EQ(domain.nodeSets()[0].nodeIndices[0], 0U); + ASSERT_EQ(domain.elementSets().size(), 1U); + EXPECT_EQ(domain.elementSets()[0].elementIndices[0], 0U); + + ASSERT_EQ(domain.steps().size(), 1U); + EXPECT_EQ(domain.steps()[0].name, "Step-1"); + EXPECT_DOUBLE_EQ(domain.steps()[0].initialIncrement, 0.1); + EXPECT_DOUBLE_EQ(domain.steps()[0].timePeriod, 1.0); + EXPECT_DOUBLE_EQ(domain.steps()[0].minimumIncrement, 1.0e-5); + EXPECT_DOUBLE_EQ(domain.steps()[0].maximumIncrement, 1.0); + ASSERT_EQ(domain.steps()[0].boundaries.size(), 1U); + ASSERT_EQ(domain.steps()[0].loads.size(), 1U); + EXPECT_DOUBLE_EQ(domain.steps()[0].loads[0].magnitude, -1000.0); + + ASSERT_EQ(domain.warnings().size(), 1U); + EXPECT_EQ(domain.warnings()[0].code, "ignored-output-request"); +} + +TEST(DomainModel, MultipleIdentityInstancesDoNotMerge) { + fesa::ModelDefinition definition{}; + definition.sourcePath = "models/two-instances.inp"; + definition.sourceContentIdentity = "fnv1a64:0011223344556677"; + definition.nodes = { + {{"Instance-A", 1, "1"}, {0.0, 0.0, 0.0}, {"models/two-instances.inp", 10U}}, + {{"Instance-A", 2, "2"}, {1.0, 0.0, 0.0}, {"models/two-instances.inp", 11U}}, + {{"Instance-B", 1, "1"}, {0.0, 0.0, 0.0}, {"models/two-instances.inp", 10U}}, + {{"Instance-B", 2, "2"}, {1.0, 0.0, 0.0}, {"models/two-instances.inp", 11U}}}; + definition.elements = { + {{"Instance-A", 1, "1"}, {0U, 1U}, 0U, 0U, {"models/two-instances.inp", 20U}}, + {{"Instance-B", 1, "1"}, {2U, 3U}, 0U, 0U, {"models/two-instances.inp", 20U}}}; + definition.materials = {{ + "Steel", 210.0e9, 0.3, {"models/two-instances.inp", 30U}}}; + definition.sections = {{ + "Section-1", + 0.02, + 1.0e-5, + 0.0, + 2.0e-5, + 5.0e-6, + {0.0, 1.0, 0.0}, + {}, + {"models/two-instances.inp", 40U}}}; + definition.parts = {{ + "BeamPart", {1, 2}, {1}, {}, {}, {"models/two-instances.inp", 2U}}}; + definition.instances = { + {"Instance-A", "BeamPart", {{1, 0U}, {2, 1U}}, {{1, 0U}}, + {"models/two-instances.inp", 50U}}, + {"Instance-B", "BeamPart", {{1, 2U}, {2, 3U}}, {{1, 1U}}, + {"models/two-instances.inp", 51U}}}; + definition.steps = {{ + "Step-1", {}, {}, 0.1, 1.0, 1.0e-5, 1.0, + {"models/two-instances.inp", 60U}}}; + + auto result = fesa::Domain::create(std::move(definition)); + ASSERT_TRUE(result.hasValue()); + const fesa::Domain& domain = result.value(); + + ASSERT_EQ(domain.nodes().size(), 4U); + EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabel, 1); + EXPECT_EQ(domain.nodes()[2].sourceId.sourceLabel, 1); + EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabelText, "1"); + EXPECT_EQ(domain.nodes()[2].sourceId.sourceLabelText, "1"); + EXPECT_NE( + domain.nodes()[0].sourceId.instanceName, + domain.nodes()[2].sourceId.instanceName); + + ASSERT_EQ(domain.elements().size(), 2U); + EXPECT_EQ(domain.elements()[0].sourceId.sourceLabel, 1); + EXPECT_EQ(domain.elements()[1].sourceId.sourceLabel, 1); + EXPECT_EQ(domain.elements()[0].sourceId.instanceName, "Instance-A"); + EXPECT_EQ(domain.elements()[1].sourceId.instanceName, "Instance-B"); + EXPECT_EQ(domain.elements()[0].nodeIndices[0], 0U); + EXPECT_EQ(domain.elements()[1].nodeIndices[0], 2U); +} diff --git a/tests/unit/model/model_types_test.cpp b/tests/unit/model/model_types_test.cpp new file mode 100644 index 0000000..37e0bce --- /dev/null +++ b/tests/unit/model/model_types_test.cpp @@ -0,0 +1,144 @@ +#include "fesa/model/model_types.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +template +struct HasEquationId : std::false_type {}; + +template +struct HasEquationId().equationId)>> + : std::true_type {}; + +template +struct HasEquationIds : std::false_type {}; + +template +struct HasEquationIds().equationIds)>> + : std::true_type {}; + +} // namespace + +TEST(DomainModel, SourceAndInternalIdentityRemainDistinct) { + const fesa::SourceLocation nodeLocation{ + std::filesystem::path{"models/beam.inp"}, 11U}; + const fesa::SourceEntityId firstIdentity{"Instance-A", 7, "0007"}; + const fesa::SourceEntityId equalIdentity{"Instance-A", 7, "0007"}; + const fesa::SourceEntityId laterIdentity{"Instance-B", 7, "0007"}; + + EXPECT_EQ( + std::make_tuple( + firstIdentity.instanceName, + firstIdentity.sourceLabel, + firstIdentity.sourceLabelText), + std::make_tuple( + equalIdentity.instanceName, + equalIdentity.sourceLabel, + equalIdentity.sourceLabelText)); + EXPECT_LT( + std::make_tuple( + firstIdentity.instanceName, + firstIdentity.sourceLabel, + firstIdentity.sourceLabelText), + std::make_tuple( + laterIdentity.instanceName, + laterIdentity.sourceLabel, + laterIdentity.sourceLabelText)); + + const fesa::Node node{firstIdentity, {1.0, 2.0, 3.0}, nodeLocation}; + const fesa::LinearElasticMaterial material{ + "Steel", 210.0e9, 0.3, {"models/beam.inp", 30U}}; + const fesa::GeneralBeamSection section{ + "Section-1", + 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}}, + {"models/beam.inp", 40U}}; + const fesa::EulerBeam3DDefinition element{ + {"Instance-A", 3, "0003"}, + {fesa::EntityIndex{5}, fesa::EntityIndex{9}}, + fesa::EntityIndex{2}, + fesa::EntityIndex{4}, + {"models/beam.inp", 20U}}; + const fesa::BoundaryCondition boundary{ + "Root", 1, 6, 0.0, {"models/beam.inp", 60U}}; + const fesa::NodalLoad load{ + "Tip", 3, -1000.0, {"models/beam.inp", 70U}}; + const fesa::StaticStepDefinition step{ + "Step-1", + {boundary}, + {load}, + 0.1, + 1.0, + 1.0e-5, + 1.0, + {"models/beam.inp", 50U}}; + const fesa::NodeSet nodeSet{ + "Root", std::string{"Instance-A"}, {5U}, {"models/beam.inp", 25U}}; + const fesa::ElementSet elementSet{ + "Beam", std::string{"Instance-A"}, {4U}, {"models/beam.inp", 26U}}; + const fesa::PartDefinition part{ + "BeamPart", {7, 8}, {3}, {"Root", "Tip"}, {"Beam"}, + {"models/beam.inp", 2U}}; + const fesa::InstanceDefinition instance{ + "Instance-A", + "BeamPart", + {{7, 5U}, {8, 9U}}, + {{3, 4U}}, + {"models/beam.inp", 80U}}; + + fesa::ModelDefinition definition{}; + definition.sourcePath = "models/beam.inp"; + definition.sourceContentIdentity = "fnv1a64:0123456789abcdef"; + definition.heading = "Identity mapping fixture"; + definition.nodes = {node}; + definition.elements = {element}; + definition.materials = {material}; + definition.sections = {section}; + definition.nodeSets = {nodeSet}; + definition.elementSets = {elementSet}; + definition.parts = {part}; + definition.instances = {instance}; + definition.steps = {step}; + definition.warnings = {{ + fesa::Severity::warning, + "ignored-output-request", + {"models/beam.inp", 75U}, + "*OUTPUT", + "", + "Output request does not alter mandatory FESA results."}}; + + ASSERT_EQ(definition.nodes.size(), 1U); + EXPECT_EQ(definition.nodes[0].sourceId.sourceLabelText, "0007"); + EXPECT_EQ(definition.nodes[0].location.line, 11U); + ASSERT_EQ(definition.instances.size(), 1U); + EXPECT_EQ(definition.instances[0].partName, "BeamPart"); + EXPECT_EQ(definition.instances[0].nodeMappings[0].sourceLabel, 7); + EXPECT_EQ(definition.instances[0].nodeMappings[0].internalIndex, 5U); + EXPECT_EQ(definition.instances[0].elementMappings[0].internalIndex, 4U); + EXPECT_EQ(definition.steps[0].initialIncrement, 0.1); + EXPECT_EQ(definition.steps[0].timePeriod, 1.0); + EXPECT_EQ(definition.steps[0].minimumIncrement, 1.0e-5); + EXPECT_EQ(definition.steps[0].maximumIncrement, 1.0); + ASSERT_EQ(definition.warnings.size(), 1U); + EXPECT_EQ(definition.warnings[0].location.line, 75U); + + static_assert(!HasEquationId::value); + static_assert(!HasEquationIds::value); + static_assert(!HasEquationId::value); + static_assert(!HasEquationIds::value); +}