feat(cpp-object-oriented-modular-refactoring): step 21 - domain-mapper-modules

This commit is contained in:
KOKO\Mimi
2026-08-16 11:45:34 +09:00
parent f37324eeaf
commit f2c150b490
14 changed files with 2721 additions and 2166 deletions
+5
View File
@@ -18,8 +18,13 @@ add_library(
elements/euler_beam_3d.cpp
elements/mitc4_shell.cpp
fem/dof_manager.cpp
io/abaqus/domain_builder.cpp
io/abaqus/domain_mapping_context.cpp
io/abaqus/domain_mapper.cpp
io/abaqus/input_reader.cpp
io/abaqus/material_property_mapper.cpp
io/abaqus/step_definition_mapper.cpp
io/abaqus/topology_mapper.cpp
io/hdf5/hdf5_results_writer.cpp
loads/concentrated_nodal_load.cpp
materials/isotropic_linear_elastic_material.cpp
+11
View File
@@ -0,0 +1,11 @@
#include "io/abaqus/domain_builder.h"
#include "io/abaqus/domain_mapping_context.h"
namespace fesa::abaqus_internal {
Result<Domain> DomainBuilder::Build(DomainMappingContext& context) const {
return context.Commit();
}
} // namespace fesa::abaqus_internal
+20
View File
@@ -0,0 +1,20 @@
#ifndef FESA_IO_ABAQUS_DOMAIN_BUILDER_H_
#define FESA_IO_ABAQUS_DOMAIN_BUILDER_H_
#include "fesa/core/status.h"
#include "fesa/model/domain.h"
namespace fesa::abaqus_internal {
class DomainMappingContext;
/// @brief Commits a complete validated mapping candidate atomically.
class DomainBuilder {
public:
/// @brief Transfers the candidate to Domain only after every stage succeeds.
Result<Domain> Build(DomainMappingContext& context) const;
};
} // namespace fesa::abaqus_internal
#endif // FESA_IO_ABAQUS_DOMAIN_BUILDER_H_
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
#ifndef FESA_IO_ABAQUS_DOMAIN_MAPPING_CONTEXT_H_
#define FESA_IO_ABAQUS_DOMAIN_MAPPING_CONTEXT_H_
#include <cstddef>
#include <memory>
#include "fesa/core/status.h"
#include "fesa/io/abaqus/input_syntax.h"
#include "fesa/model/domain.h"
namespace fesa::abaqus_internal {
class DomainBuilder;
class MaterialPropertyMapper;
class StepDefinitionMapper;
class TopologyMapper;
/// @brief Summarizes syntax-derived candidates before Domain ownership begins.
struct DomainMappingCandidateSummary {
std::size_t part_count;
std::size_t node_count;
std::size_t element_count;
std::size_t instance_count;
std::size_t node_set_count;
std::size_t element_set_count;
std::size_t material_count;
std::size_t beam_section_count;
std::size_t shell_section_count;
std::size_t model_boundary_count;
std::size_t step_boundary_count;
std::size_t step_load_count;
bool has_static_step;
};
/// @brief Owns uncommitted Abaqus mapping candidates shared by private stages.
/// @note The ParsedInput reference must outlive this context and all stages.
class DomainMappingContext {
public:
/// @brief Starts an empty mapping candidate for one parsed input.
explicit DomainMappingContext(const ParsedInput& input);
~DomainMappingContext();
DomainMappingContext(const DomainMappingContext&) = delete;
DomainMappingContext& operator=(const DomainMappingContext&) = delete;
DomainMappingContext(DomainMappingContext&&) = delete;
DomainMappingContext& operator=(DomainMappingContext&&) = delete;
/// @brief Reports stable raw candidate counts for component verification.
DomainMappingCandidateSummary CandidateSummary() const noexcept;
private:
class Impl;
friend class DomainBuilder;
friend class MaterialPropertyMapper;
friend class StepDefinitionMapper;
friend class TopologyMapper;
/// @brief Parses syntax into uncommitted topology and semantic candidates.
Status ParseSyntax();
/// @brief Validates and projects material and property candidates.
Status FinalizeMaterialProperties();
/// @brief Expands identity instances and stable topology candidates.
Status FinalizeTopology();
/// @brief Resolves the sole static step against finalized source targets.
Status FinalizeStep();
/// @brief Atomically transfers a complete candidate into Domain ownership.
Result<Domain> Commit();
std::unique_ptr<Impl> impl_;
};
} // namespace fesa::abaqus_internal
#endif // FESA_IO_ABAQUS_DOMAIN_MAPPING_CONTEXT_H_
@@ -0,0 +1,11 @@
#include "io/abaqus/material_property_mapper.h"
#include "io/abaqus/domain_mapping_context.h"
namespace fesa::abaqus_internal {
Status MaterialPropertyMapper::Map(DomainMappingContext& context) const {
return context.FinalizeMaterialProperties();
}
} // namespace fesa::abaqus_internal
@@ -0,0 +1,19 @@
#ifndef FESA_IO_ABAQUS_MATERIAL_PROPERTY_MAPPER_H_
#define FESA_IO_ABAQUS_MATERIAL_PROPERTY_MAPPER_H_
#include "fesa/core/status.h"
namespace fesa::abaqus_internal {
class DomainMappingContext;
/// @brief Validates current isotropic material and section candidates.
class MaterialPropertyMapper {
public:
/// @brief Projects validated material and property records in source order.
Status Map(DomainMappingContext& context) const;
};
} // namespace fesa::abaqus_internal
#endif // FESA_IO_ABAQUS_MATERIAL_PROPERTY_MAPPER_H_
@@ -0,0 +1,11 @@
#include "io/abaqus/step_definition_mapper.h"
#include "io/abaqus/domain_mapping_context.h"
namespace fesa::abaqus_internal {
Status StepDefinitionMapper::Map(DomainMappingContext& context) const {
return context.FinalizeStep();
}
} // namespace fesa::abaqus_internal
@@ -0,0 +1,19 @@
#ifndef FESA_IO_ABAQUS_STEP_DEFINITION_MAPPER_H_
#define FESA_IO_ABAQUS_STEP_DEFINITION_MAPPER_H_
#include "fesa/core/status.h"
namespace fesa::abaqus_internal {
class DomainMappingContext;
/// @brief Resolves the approved static step, loads, and boundaries.
class StepDefinitionMapper {
public:
/// @brief Resolves source targets and finalizes the static-step candidate.
Status Map(DomainMappingContext& context) const;
};
} // namespace fesa::abaqus_internal
#endif // FESA_IO_ABAQUS_STEP_DEFINITION_MAPPER_H_
+15
View File
@@ -0,0 +1,15 @@
#include "io/abaqus/topology_mapper.h"
#include "io/abaqus/domain_mapping_context.h"
namespace fesa::abaqus_internal {
Status TopologyMapper::Map(DomainMappingContext& context) const {
return context.ParseSyntax();
}
Status TopologyMapper::Finalize(DomainMappingContext& context) const {
return context.FinalizeTopology();
}
} // namespace fesa::abaqus_internal
+22
View File
@@ -0,0 +1,22 @@
#ifndef FESA_IO_ABAQUS_TOPOLOGY_MAPPER_H_
#define FESA_IO_ABAQUS_TOPOLOGY_MAPPER_H_
#include "fesa/core/status.h"
namespace fesa::abaqus_internal {
class DomainMappingContext;
/// @brief Maps and finalizes topology without constructing a public Domain.
class TopologyMapper {
public:
/// @brief Parses ordered syntax into shared uncommitted candidates.
Status Map(DomainMappingContext& context) const;
/// @brief Expands identity instances and stable set mappings.
Status Finalize(DomainMappingContext& context) const;
};
} // namespace fesa::abaqus_internal
#endif // FESA_IO_ABAQUS_TOPOLOGY_MAPPER_H_
+1
View File
@@ -24,6 +24,7 @@ add_executable(
unit/math/sparse_matrix_test.cpp
unit/math/vector3_test.cpp
unit/math/vector_test.cpp
unit/io/abaqus/domain_mapping_components_test.cpp
unit/io/abaqus/domain_mapper_test.cpp
unit/io/abaqus/input_reader_test.cpp
unit/io/abaqus/input_syntax_test.cpp
@@ -0,0 +1,236 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <utility>
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
#include "io/abaqus/domain_builder.h"
#include "io/abaqus/domain_mapping_context.h"
#include "io/abaqus/material_property_mapper.h"
#include "io/abaqus/step_definition_mapper.h"
#include "io/abaqus/topology_mapper.h"
namespace fesa::abaqus_internal {
namespace {
class TemporaryComponentInput {
public:
TemporaryComponentInput(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() /
("fesa-domain-components-" + stem + ".inp")} {
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
if (!stream) {
throw std::runtime_error{"Unable to create mapper component fixture."};
}
}
~TemporaryComponentInput() {
std::error_code error;
std::filesystem::remove(path_, error);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
ParsedInput ParseComponentInput(const std::string& stem,
const std::string& content) {
const TemporaryComponentInput input{stem, content};
auto parsed = AbaqusInputReader{}.Read(input.Path());
if (!parsed.HasValue()) {
throw std::runtime_error{"Unable to parse mapper component fixture."};
}
return std::move(parsed.Value());
}
std::string ValidComponentDeck() {
return R"inp(*Preprint
*Part, name=BeamPart
*Node
1, 0., 0., 0.
2, 1., 0., 0.
*Element, type=B33
1, 1, 2
*Nset, nset=LocalRoot
1
*Elset, elset=BeamSet
1
*Beam General Section, elset=BeamSet, material=Steel, section=GENERAL
1., 1., 0., 1., 1.
0., 1., 0.
*End Part
*Assembly, name=Assembly
*Instance, name=Beam-1, part=BeamPart
*End Instance
*Nset, nset=Root, instance=Beam-1
1
*Nset, nset=Tip, instance=Beam-1
2
*End Assembly
*Material, name=Steel
*Elastic
100., 0.25
*Boundary
Root, 1, 6
*Step, name=Load, nlgeom=NO
*Static
0.1, 1., 0.01, 1.
*Boundary
Tip, 1, 1, 0.125
*Cload
Tip, 2, -1.
*Output, field
*Node Output
U
*End Step
)inp";
}
std::string ReplaceOnce(std::string text, const std::string& from,
const std::string& to) {
const std::size_t position = text.find(from);
if (position == std::string::npos) {
throw std::logic_error{"Mapper component mutation source was not found."};
}
text.replace(position, from.size(), to);
return text;
}
void ExpectSameDiagnostic(const Diagnostic& actual,
const Diagnostic& expected) {
EXPECT_EQ(actual.severity, expected.severity);
EXPECT_EQ(actual.code, expected.code);
EXPECT_EQ(actual.location.file, expected.location.file);
EXPECT_EQ(actual.location.line, expected.location.line);
EXPECT_EQ(actual.keyword, expected.keyword);
EXPECT_EQ(actual.entity_identity, expected.entity_identity);
EXPECT_EQ(actual.message, expected.message);
}
TEST(DomainMappingComponents,
CModule001BuildsStableCandidatesBeforeAtomicDomainCommit) {
ParsedInput input = ParseComponentInput("valid", ValidComponentDeck());
DomainMappingContext context{input};
TopologyMapper topology_mapper;
MaterialPropertyMapper material_property_mapper;
StepDefinitionMapper step_definition_mapper;
ASSERT_TRUE(topology_mapper.Map(context).IsOk());
DomainMappingCandidateSummary summary = context.CandidateSummary();
EXPECT_EQ(summary.part_count, 1U);
EXPECT_EQ(summary.node_count, 2U);
EXPECT_EQ(summary.element_count, 1U);
EXPECT_EQ(summary.instance_count, 1U);
EXPECT_EQ(summary.node_set_count, 3U);
EXPECT_EQ(summary.element_set_count, 1U);
EXPECT_EQ(summary.material_count, 1U);
EXPECT_EQ(summary.beam_section_count, 1U);
EXPECT_EQ(summary.shell_section_count, 0U);
EXPECT_EQ(summary.model_boundary_count, 1U);
EXPECT_EQ(summary.step_boundary_count, 1U);
EXPECT_EQ(summary.step_load_count, 1U);
EXPECT_TRUE(summary.has_static_step);
ASSERT_TRUE(material_property_mapper.Map(context).IsOk());
ASSERT_TRUE(topology_mapper.Finalize(context).IsOk());
ASSERT_TRUE(step_definition_mapper.Map(context).IsOk());
auto result = DomainBuilder{}.Build(context);
ASSERT_TRUE(result.HasValue());
const Domain& domain = result.Value();
EXPECT_EQ(domain.Nodes().size(), 2U);
EXPECT_EQ(domain.Elements().Size(), 1U);
EXPECT_EQ(domain.Materials().Size(), 1U);
EXPECT_EQ(domain.Properties().Size(), 1U);
ASSERT_EQ(domain.Steps().Size(), 1U);
EXPECT_EQ(domain.Steps()[0].BoundaryConditions().size(), 7U);
EXPECT_EQ(domain.Steps()[0].Loads().size(), 1U);
ASSERT_EQ(domain.Warnings().size(), 3U);
EXPECT_EQ(domain.Warnings()[0].keyword, "PREPRINT");
EXPECT_EQ(domain.Warnings()[1].keyword, "OUTPUT");
EXPECT_EQ(domain.Warnings()[2].keyword, "NODE OUTPUT");
}
TEST(DomainMappingComponents,
CModule001PreservesExactFirstFailureThroughFacade) {
ParsedInput input = ParseComponentInput(
"invalid", "*Node\n1, 0., 0., 0.\n*Material, name=Steel\n");
DomainMappingContext context{input};
const Status component_status = TopologyMapper{}.Map(context);
ASSERT_FALSE(component_status.IsOk());
ASSERT_EQ(component_status.Diagnostics().size(), 1U);
const Diagnostic& component_diagnostic =
component_status.Diagnostics().front();
EXPECT_EQ(component_status.Category(), FailureCategory::kInput);
EXPECT_EQ(component_diagnostic.severity, Severity::kError);
EXPECT_EQ(component_diagnostic.code, "unsupported-keyword");
EXPECT_EQ(component_diagnostic.location.line, 1U);
EXPECT_EQ(component_diagnostic.keyword, "NODE");
EXPECT_TRUE(component_diagnostic.entity_identity.empty());
EXPECT_EQ(component_diagnostic.message,
"The keyword is outside the approved Abaqus subset.");
auto facade_result = AbaqusDomainMapper{}.Map(input);
ASSERT_FALSE(facade_result.HasValue());
ASSERT_EQ(facade_result.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(facade_result.GetStatus().Category(), component_status.Category());
ExpectSameDiagnostic(facade_result.GetStatus().Diagnostics().front(),
component_diagnostic);
}
TEST(DomainMappingComponents,
CModule001RejectsInvalidMaterialAtOwningComponent) {
ParsedInput input = ParseComponentInput(
"invalid-material",
ReplaceOnce(ValidComponentDeck(), "100., 0.25", "-100., 0.25"));
DomainMappingContext context{input};
ASSERT_TRUE(TopologyMapper{}.Map(context).IsOk());
const Status status = MaterialPropertyMapper{}.Map(context);
ASSERT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Category(), FailureCategory::kModel);
const Diagnostic& diagnostic = status.Diagnostics().front();
EXPECT_EQ(diagnostic.code, "invalid-beam-property");
EXPECT_EQ(diagnostic.location.line, 26U);
EXPECT_EQ(diagnostic.keyword, "ELASTIC");
EXPECT_EQ(diagnostic.entity_identity, "Steel");
EXPECT_EQ(diagnostic.message,
"E and the derived G=E/(2*(1+nu)) must be positive.");
}
TEST(DomainMappingComponents,
CModule001RejectsUnresolvedLoadAtOwningComponent) {
ParsedInput input = ParseComponentInput(
"invalid-step",
ReplaceOnce(ValidComponentDeck(), "Tip, 2, -1.", "Missing, 2, -1."));
DomainMappingContext context{input};
TopologyMapper topology_mapper;
ASSERT_TRUE(topology_mapper.Map(context).IsOk());
ASSERT_TRUE(MaterialPropertyMapper{}.Map(context).IsOk());
ASSERT_TRUE(topology_mapper.Finalize(context).IsOk());
const Status status = StepDefinitionMapper{}.Map(context);
ASSERT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Category(), FailureCategory::kInput);
const Diagnostic& diagnostic = status.Diagnostics().front();
EXPECT_EQ(diagnostic.code, "unresolved-reference");
EXPECT_EQ(diagnostic.location.line, 35U);
EXPECT_EQ(diagnostic.keyword, "CLOAD");
EXPECT_EQ(diagnostic.entity_identity, "Missing");
EXPECT_EQ(diagnostic.message,
"The boundary or load target must resolve unambiguously to one "
"node or one node set.");
}
} // namespace
} // namespace fesa::abaqus_internal