Compare commits

...

10 Commits

Author SHA1 Message Date
KOKO\Mimi 18361e4eb2 fix(results-and-pipeline): set CLI test runtime path 2026-08-01 01:26:06 +09:00
KOKO\Mimi e31ef999bd chore(results-and-pipeline): mark phase completed 2026-08-01 01:11:01 +09:00
KOKO\Mimi 93496c0157 chore(results-and-pipeline): step 3 output 2026-08-01 01:11:01 +09:00
KOKO\Mimi fe90528115 feat(results-and-pipeline): step 3 — cli-pipeline-integration 2026-08-01 01:11:01 +09:00
KOKO\Mimi 493219d3c2 chore(results-and-pipeline): step 2 output 2026-08-01 01:01:15 +09:00
KOKO\Mimi 11672df2c4 feat(results-and-pipeline): step 2 — linear-static-analysis 2026-08-01 01:01:15 +09:00
KOKO\Mimi d6306383a9 chore(results-and-pipeline): step 1 output 2026-08-01 00:51:34 +09:00
KOKO\Mimi 76a0c8ecde feat(results-and-pipeline): step 1 — minimal-hdf5-schema 2026-08-01 00:51:34 +09:00
KOKO\Mimi f8af2c1485 chore(results-and-pipeline): step 0 output 2026-08-01 00:14:54 +09:00
KOKO\Mimi 5b7de91988 feat(results-and-pipeline): step 0 — result-database 2026-08-01 00:14:53 +09:00
22 changed files with 3455 additions and 9 deletions
+5 -1
View File
@@ -26,6 +26,8 @@ include(CTest)
include(cmake/FesaDependencies.cmake) include(cmake/FesaDependencies.cmake)
add_library(fesa_core STATIC add_library(fesa_core STATIC
src/fesa/analysis/linear_static_analysis.cpp
src/fesa/analysis/run_solver.cpp
src/fesa/assembly/serial_assembler.cpp src/fesa/assembly/serial_assembler.cpp
src/fesa/constraints/essential_bc.cpp src/fesa/constraints/essential_bc.cpp
src/fesa/core/version.cpp src/fesa/core/version.cpp
@@ -36,8 +38,10 @@ add_library(fesa_core STATIC
src/fesa/fem/line2_shape.cpp src/fesa/fem/line2_shape.cpp
src/fesa/io/abaqus/parser.cpp src/fesa/io/abaqus/parser.cpp
src/fesa/io/abaqus/semantic_mapper.cpp src/fesa/io/abaqus/semantic_mapper.cpp
src/fesa/io/hdf5/writer.cpp
src/fesa/model/domain.cpp src/fesa/model/domain.cpp
src/fesa/model/domain_builder.cpp src/fesa/model/domain_builder.cpp
src/fesa/results/result_database.cpp
src/fesa/solvers/linear/pardiso_linear_solver.cpp src/fesa/solvers/linear/pardiso_linear_solver.cpp
) )
@@ -48,7 +52,7 @@ target_include_directories(fesa_core
target_compile_features(fesa_core PUBLIC cxx_std_20) target_compile_features(fesa_core PUBLIC cxx_std_20)
target_compile_options(fesa_core PRIVATE /W4 /permissive- /EHsc) target_compile_options(fesa_core PRIVATE /W4 /permissive- /EHsc)
target_link_libraries(fesa_core PRIVATE MKL::MKL) target_link_libraries(fesa_core PRIVATE MKL::MKL HDF5::HDF5)
add_executable(fesa add_executable(fesa
src/fesa/cli/main.cpp src/fesa/cli/main.cpp
+90
View File
@@ -0,0 +1,90 @@
# FESA HDF5 Schema 1.0.0
## 1. 범위
Schema `1.0.0``results-and-pipeline` Phase의 최소 수직 슬라이스를 정의한다.
파일은 활성 `Domain`의 절점, Beam 연결성, 적용된 전단면적과 그 출처, 그리고 전역
좌표계 절점 변위·회전 및 반력·반력모멘트를 저장한다. 단위 변환은 수행하지 않는다.
이 버전에는 재료 전체 속성, 집합, 하중·경계조건, solver 설정, 요소 결과, history,
reference CSV 및 진단 dataset을 저장하지 않는다. 이후 같은 major version에서
dataset을 추가할 수 있지만 아래 required object의 의미, 형상 또는 datatype을
변경해서는 안 된다.
## 2. 공통 규칙
- root attribute `schema_version`은 UTF-8 문자열 `1.0.0`이다.
- 정수 dataset은 명시한 little-endian 고정폭 타입을 사용한다.
- 실수 dataset은 IEEE 754 little-endian 64-bit 타입을 사용한다.
- 문자열 dataset과 attribute는 UTF-8 variable-length string을 사용한다.
- `dense_index`는 해당 dataset 행의 0-based index이며 연속적이다.
- `internal_id`와 결과의 `node_ids`는 FESA semantic model의 nonnegative ID다.
- flat/orphan mesh의 `part_name``instance_name`은 빈 문자열이다.
- 결과의 6개 component 순서는
`(Ux, Uy, Uz, Rx, Ry, Rz)``(RFx, RFy, RFz, RMx, RMy, RMz)`다.
- Step과 frame group 이름은 각각 0부터 연속된 decimal index다. 원래 Step 이름은
Step group의 `name` attribute에 저장한다.
## 3. Required objects
```text
/
├── @schema_version UTF-8 = "1.0.0"
├── model
│ ├── nodes
│ │ ├── dense_index uint64 [node_count]
│ │ ├── internal_id int64 [node_count]
│ │ ├── part_name UTF-8 [node_count]
│ │ ├── instance_name UTF-8 [node_count]
│ │ ├── local_label int64 [node_count]
│ │ └── coordinates float64[node_count, 3]
│ ├── elements
│ │ ├── dense_index uint64 [element_count]
│ │ ├── internal_id int64 [element_count]
│ │ ├── connectivity uint64 [element_count, 2]
│ │ └── section_id int64 [element_count]
│ └── sections
│ ├── internal_id int64 [section_count]
│ ├── shear_area_y float64[section_count]
│ ├── shear_area_z float64[section_count]
│ └── shear_source uint8 [section_count]
└── results
└── steps
└── <step_index>
├── @name UTF-8
└── frames
└── <frame_index>
├── @step_time float64
└── nodal
├── node_ids int64 [result_node_count]
├── displacement float64[result_node_count, 6]
└── reaction float64[result_node_count, 6]
```
`model/elements/connectivity``model/nodes/dense_index`를 참조한다. 따라서
`internal_id`가 연속적이거나 Domain 저장 순서와 같다고 가정하지 않는다.
`section_id``model/sections/internal_id`를 참조한다.
`shear_source` 값은 다음과 같다.
| 값 | 의미 |
|---:|---|
| `0` | 입력에서 명시된 전단강성으로부터 구성한 값 (`input`) |
| `1` | Phase 1 기본값 `Asy=Asz=5A/6`, `SCF=0` (`phase1_default`) |
## 4. Writer와 reader 계약
- writer는 쓰기 전에 `ResultDatabase` 유효성과 schema version을 검사한다.
- schema `1.0.0`이 표현하지 않는 non-empty frame diagnostics는 파일을 만들기 전에
`hdf5.unsupported_result_diagnostics`로 거부한다.
- required object 생성·쓰기·flush·close 중 HDF5 오류가 발생하면 성공으로 반환하지
않고 `DiagnosticStage::results` 오류로 변환한다.
- reader는 schema version, required object, datatype, rank와 shape를 검사한다.
- reader는 model internal ID의 uniqueness, finite coordinates 및 finite positive
shear area를 검사한다.
- writer와 reader는 모든 nodal result ID가 `model/nodes/internal_id`에 존재하는지
검사하며, 없는 ID를 성공 결과로 반환하지 않는다.
- reader는 nodal result를 `ResultDatabase`로, model dataset을 HDF5 adapter 전용
read-only inspection model로 반환한다. `Domain``ResultDatabase`에는 HDF5
저장 계약을 추가하지 않는다.
- malformed 또는 지원하지 않는 파일은 부분 database를 반환하지 않는다.
@@ -0,0 +1,23 @@
#pragma once
#include <optional>
#include <vector>
#include <fesa/core/diagnostic.hpp>
#include <fesa/model/domain.hpp>
#include <fesa/results/result_database.hpp>
namespace fesa {
struct AnalysisRunResult final {
bool succeeded;
std::optional<ResultDatabase> results;
std::vector<Diagnostic> diagnostics;
};
class LinearStaticAnalysis final {
public:
[[nodiscard]] AnalysisRunResult run(const Domain& domain) const;
};
} // namespace fesa
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <filesystem>
#include <fesa/analysis/linear_static_analysis.hpp>
namespace fesa {
struct AnalysisRequest final {
std::filesystem::path input_path;
std::filesystem::path output_path;
};
[[nodiscard]] AnalysisRunResult run_solver(
const AnalysisRequest& request);
} // namespace fesa
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <array>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <vector>
#include <fesa/core/diagnostic.hpp>
#include <fesa/core/vec3.hpp>
#include <fesa/model/beam_section.hpp>
#include <fesa/model/domain.hpp>
#include <fesa/model/entity_origin.hpp>
#include <fesa/model/ids.hpp>
#include <fesa/results/result_database.hpp>
namespace fesa {
struct Hdf5NodeSnapshot final {
std::uint64_t dense_index;
NodeId id;
EntityOrigin origin;
Vec3 coordinates;
};
struct Hdf5ElementSnapshot final {
std::uint64_t dense_index;
ElementId id;
std::array<std::uint64_t, 2> connectivity;
SectionId section;
};
struct Hdf5SectionSnapshot final {
SectionId id;
double shear_area_y;
double shear_area_z;
ShearPropertySource shear_source;
};
struct Hdf5ModelSnapshot final {
std::vector<Hdf5NodeSnapshot> nodes;
std::vector<Hdf5ElementSnapshot> elements;
std::vector<Hdf5SectionSnapshot> sections;
};
struct Hdf5ReadResult final {
std::optional<ResultDatabase> database;
std::vector<Diagnostic> diagnostics;
std::optional<Hdf5ModelSnapshot> model;
};
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
const std::filesystem::path& path,
const Domain& domain,
const ResultDatabase& database);
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
const std::filesystem::path& path);
} // namespace fesa
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <array>
#include <string>
#include <vector>
#include <fesa/core/diagnostic.hpp>
#include <fesa/core/status.hpp>
#include <fesa/model/ids.hpp>
namespace fesa {
struct NodalFrame final {
std::vector<NodeId> node_ids;
std::vector<std::array<double, 6>> displacement;
std::vector<std::array<double, 6>> reaction;
};
struct ResultFrame final {
double step_time;
NodalFrame nodal;
std::vector<Diagnostic> diagnostics;
};
struct ResultStep final {
std::string name;
std::vector<ResultFrame> frames;
};
struct ResultDatabase final {
std::string schema_version;
std::vector<ResultStep> steps;
};
[[nodiscard]] Status validate_result_database(
const ResultDatabase& database);
} // namespace fesa
+2 -1
View File
@@ -22,7 +22,8 @@
}, },
{ {
"dir": "results-and-pipeline", "dir": "results-and-pipeline",
"status": "pending" "status": "completed",
"completed_at": "2026-08-01T01:11:01+0900"
}, },
{ {
"dir": "abaqus-subset-completion", "dir": "abaqus-subset-completion",
+19 -5
View File
@@ -5,22 +5,36 @@
{ {
"step": 0, "step": 0,
"name": "result-database", "name": "result-database",
"status": "pending" "status": "completed",
"started_at": "2026-08-01T00:01:45+0900",
"summary": "Added HDF5-independent nodal result aggregates and results-stage validation for sizes, duplicate nodes/steps/frames, and finite values with focused tests.",
"completed_at": "2026-08-01T00:14:53+0900"
}, },
{ {
"step": 1, "step": 1,
"name": "minimal-hdf5-schema", "name": "minimal-hdf5-schema",
"status": "pending" "status": "completed",
"started_at": "2026-08-01T00:14:54+0900",
"summary": "Documented HDF5 schema 1.0.0 and added a move-only RAII writer/public reader round trip for model identity, connectivity, shear provenance, and nodal results.",
"completed_at": "2026-08-01T00:51:34+0900"
}, },
{ {
"step": 2, "step": 2,
"name": "linear-static-analysis", "name": "linear-static-analysis",
"status": "pending" "status": "completed",
"started_at": "2026-08-01T00:51:34+0900",
"summary": "Added LinearStaticAnalysis orchestration through serial assembly, BC elimination, PARDISO with an all-constrained bypass, reaction recovery, and deterministic nodal results with equilibrium tests.",
"completed_at": "2026-08-01T01:01:15+0900"
}, },
{ {
"step": 3, "step": 3,
"name": "cli-pipeline-integration", "name": "cli-pipeline-integration",
"status": "pending" "status": "completed",
"started_at": "2026-08-01T01:01:16+0900",
"summary": "Added public run_solver orchestration and the solve CLI with source-preserving diagnostics, plus end-to-end HDF5 pipeline tests.",
"completed_at": "2026-08-01T01:11:00+0900"
} }
] ],
"created_at": "2026-08-01T00:01:45+0900",
"completed_at": "2026-08-01T01:11:01+0900"
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,137 @@
#include <fesa/analysis/linear_static_analysis.hpp>
#include <algorithm>
#include <array>
#include <cstddef>
#include <exception>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <fesa/assembly/equation_system.hpp>
#include <fesa/assembly/serial_assembler.hpp>
#include <fesa/constraints/essential_bc.hpp>
#include <fesa/fem/dof_manager.hpp>
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
namespace fesa {
namespace {
AnalysisRunResult failure(std::vector<Diagnostic> diagnostics) {
return {false, std::nullopt, std::move(diagnostics)};
}
AnalysisRunResult equation_failure(
std::string code,
std::string message) {
return failure({{
DiagnosticStage::equation,
Severity::error,
std::move(code),
std::move(message),
std::nullopt,
}});
}
NodalFrame build_nodal_frame(
const Domain& domain,
const DofManager& dofs,
const std::vector<double>& displacement,
const std::vector<double>& reaction) {
std::vector<NodeId> node_ids;
node_ids.reserve(domain.nodes().size());
for (const Node& node : domain.nodes()) {
node_ids.push_back(node.id);
}
std::ranges::sort(node_ids);
NodalFrame nodal;
nodal.node_ids = std::move(node_ids);
nodal.displacement.reserve(nodal.node_ids.size());
nodal.reaction.reserve(nodal.node_ids.size());
for (const NodeId node_id : nodal.node_ids) {
std::array<double, 6> node_displacement{};
std::array<double, 6> node_reaction{};
for (std::size_t component = 0; component < 6; ++component) {
const std::size_t full_dof = dofs.full_dof({
node_id,
static_cast<NodeDof>(component),
});
node_displacement[component] = displacement[full_dof];
node_reaction[component] = reaction[full_dof];
}
nodal.displacement.push_back(node_displacement);
nodal.reaction.push_back(node_reaction);
}
return nodal;
}
} // namespace
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
const DofManager dofs = DofManager::build(domain);
std::optional<EquationSystem> original;
try {
original = assemble_serial(domain, dofs);
} catch (const std::exception& error) {
return equation_failure(
"equation.assembly_failed", error.what());
}
ConstraintResult constrained =
eliminate_essential_bcs(*original, dofs);
if (!constrained.reduced_system.has_value()) {
return failure(std::move(constrained.diagnostics));
}
std::vector<double> reduced_solution;
if (dofs.free_equation_count() != 0) {
PardisoLinearSolver solver;
LinearSolveResult solved = solver.solve(
constrained.reduced_system->stiffness,
constrained.reduced_system->force);
if (!solved.diagnostics.empty()) {
return failure(std::move(solved.diagnostics));
}
reduced_solution = std::move(solved.solution);
}
std::vector<double> displacement;
try {
displacement = dofs.reconstruct_full(reduced_solution);
} catch (const std::exception& error) {
return equation_failure(
"equation.solution_reconstruction_failed", error.what());
}
std::vector<double> reaction;
try {
reaction = recover_reaction(*original, displacement);
} catch (const std::exception& error) {
return equation_failure(
"equation.reaction_recovery_failed", error.what());
}
ResultDatabase database{
"1.0.0",
{{
domain.step().name,
{{
1.0,
build_nodal_frame(
domain, dofs, displacement, reaction),
{},
}},
}},
};
Status status = validate_result_database(database);
if (!status.succeeded) {
return failure(std::move(status.diagnostics));
}
return {true, std::move(database), {}};
}
} // namespace fesa
+37
View File
@@ -0,0 +1,37 @@
#include <fesa/analysis/run_solver.hpp>
#include <optional>
#include <utility>
#include <fesa/io/abaqus/parser.hpp>
#include <fesa/io/abaqus/semantic_mapper.hpp>
#include <fesa/io/hdf5/writer.hpp>
namespace fesa {
AnalysisRunResult run_solver(const AnalysisRequest& request) {
ParseDeckResult parsed = parse_deck(request.input_path);
if (!parsed.deck.has_value()) {
return {false, std::nullopt, std::move(parsed.diagnostics)};
}
DomainBuildResult mapped = map_deck_to_domain(*parsed.deck);
if (!mapped.domain.has_value()) {
return {false, std::nullopt, std::move(mapped.diagnostics)};
}
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
if (!run.succeeded || !run.results.has_value()) {
return run;
}
std::vector<Diagnostic> write_diagnostics = write_hdf5(
request.output_path, *mapped.domain, *run.results);
if (!write_diagnostics.empty()) {
return {false, std::nullopt, std::move(write_diagnostics)};
}
return run;
}
} // namespace fesa
+62 -1
View File
@@ -1,14 +1,75 @@
#include <fesa/analysis/run_solver.hpp>
#include <fesa/core/version.hpp> #include <fesa/core/version.hpp>
#include <filesystem>
#include <iostream> #include <iostream>
#include <string>
#include <string_view> #include <string_view>
namespace {
std::string_view stage_name(const fesa::DiagnosticStage stage) {
switch (stage) {
case fesa::DiagnosticStage::io:
return "io";
case fesa::DiagnosticStage::syntax:
return "syntax";
case fesa::DiagnosticStage::semantic:
return "semantic";
case fesa::DiagnosticStage::model:
return "model";
case fesa::DiagnosticStage::equation:
return "equation";
case fesa::DiagnosticStage::solver:
return "solver";
case fesa::DiagnosticStage::results:
return "results";
case fesa::DiagnosticStage::validation:
return "validation";
}
return "unknown";
}
void print_diagnostic(const fesa::Diagnostic& diagnostic) {
std::cerr << stage_name(diagnostic.stage) << " [" << diagnostic.code
<< "]";
if (diagnostic.source.has_value()) {
const fesa::SourceLocation& source = *diagnostic.source;
std::cerr << ' ' << source.file.string() << ':' << source.line << ':'
<< source.column;
}
std::cerr << ": " << diagnostic.message << '\n';
}
void print_usage() {
std::cerr << "Usage:\n"
<< " fesa solve <model.inp> --output <results.h5>\n"
<< " fesa --version\n";
}
} // namespace
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
if (argc == 2 && std::string_view{argv[1]} == "--version") { if (argc == 2 && std::string_view{argv[1]} == "--version") {
std::cout << fesa::version() << '\n'; std::cout << fesa::version() << '\n';
return 0; return 0;
} }
std::cerr << "Usage: fesa --version\n"; if (argc == 5 && std::string_view{argv[1]} == "solve" &&
std::string_view{argv[3]} == "--output") {
const fesa::AnalysisRunResult result = fesa::run_solver({
std::filesystem::path{argv[2]},
std::filesystem::path{argv[4]},
});
if (result.succeeded) {
return 0;
}
for (const fesa::Diagnostic& diagnostic : result.diagnostics) {
print_diagnostic(diagnostic);
}
return 1;
}
print_usage();
return 1; return 1;
} }
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
#include <fesa/results/result_database.hpp>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <set>
#include <string>
#include <utility>
namespace fesa {
namespace {
void add_error(
std::vector<Diagnostic>& diagnostics,
std::string code,
std::string message) {
diagnostics.push_back({
DiagnosticStage::results,
Severity::error,
std::move(code),
std::move(message),
std::nullopt,
});
}
bool is_finite(const std::array<double, 6>& field) {
return std::ranges::all_of(
field,
[](const double component) {
return std::isfinite(component);
});
}
void validate_nodal_frame(
const NodalFrame& nodal,
std::vector<Diagnostic>& diagnostics) {
if (
nodal.displacement.size() != nodal.node_ids.size() ||
nodal.reaction.size() != nodal.node_ids.size()) {
add_error(
diagnostics,
"results.nodal_size_mismatch",
"Nodal IDs, displacement, and reaction fields must have "
"matching sizes.");
}
std::set<std::int64_t> node_ids;
for (const NodeId node_id : nodal.node_ids) {
if (!node_ids.insert(node_id.value()).second) {
add_error(
diagnostics,
"results.duplicate_node_id",
"Nodal frame contains duplicate node ID " +
std::to_string(node_id.value()) + ".");
}
}
for (const auto& displacement : nodal.displacement) {
if (!is_finite(displacement)) {
add_error(
diagnostics,
"results.nonfinite_value",
"Nodal displacement contains a nonfinite component.");
}
}
for (const auto& reaction : nodal.reaction) {
if (!is_finite(reaction)) {
add_error(
diagnostics,
"results.nonfinite_value",
"Nodal reaction contains a nonfinite component.");
}
}
}
} // namespace
Status validate_result_database(const ResultDatabase& database) {
std::vector<Diagnostic> diagnostics;
std::set<std::string> step_names;
for (const ResultStep& step : database.steps) {
if (!step_names.insert(step.name).second) {
add_error(
diagnostics,
"results.duplicate_step_name",
"Result database contains duplicate step name '" +
step.name + "'.");
}
std::set<double> frame_times;
for (const ResultFrame& frame : step.frames) {
if (!std::isfinite(frame.step_time)) {
add_error(
diagnostics,
"results.nonfinite_value",
"Result frame has a nonfinite step time.");
} else if (!frame_times.insert(frame.step_time).second) {
add_error(
diagnostics,
"results.duplicate_frame_time",
"Result step '" + step.name +
"' contains duplicate frame time " +
std::to_string(frame.step_time) + ".");
}
validate_nodal_frame(frame.nodal, diagnostics);
}
}
return {diagnostics.empty(), std::move(diagnostics)};
}
} // namespace fesa
+159
View File
@@ -9,6 +9,12 @@ set_tests_properties(
PASS_REGULAR_EXPRESSION "[0-9]+\\.[0-9]+\\.[0-9]+" PASS_REGULAR_EXPRESSION "[0-9]+\\.[0-9]+\\.[0-9]+"
) )
set_property(
TEST VersionCommand
PROPERTY ENVIRONMENT_MODIFICATION
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
)
add_executable(fesa_dependency_smoke_test add_executable(fesa_dependency_smoke_test
unit/dependencies/dependency_smoke_test.cpp unit/dependencies/dependency_smoke_test.cpp
) )
@@ -375,3 +381,156 @@ set_property(
PROPERTY ENVIRONMENT_MODIFICATION PROPERTY ENVIRONMENT_MODIFICATION
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS} ${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
) )
add_executable(fesa_result_database_tests
unit/results/result_database_test.cpp
)
target_compile_features(fesa_result_database_tests PRIVATE cxx_std_20)
target_compile_options(
fesa_result_database_tests
PRIVATE
/W4
/permissive-
/EHsc
)
target_link_libraries(fesa_result_database_tests
PRIVATE
fesa_core
GTest::gtest_main
)
add_test(
NAME NodalFrame
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
--gtest_filter=NodalFrame.*
)
add_test(
NAME ResultDatabase
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
--gtest_filter=ResultDatabase.*
)
add_executable(fesa_hdf5_results_tests
integration/io/hdf5_results_test.cpp
)
target_compile_features(fesa_hdf5_results_tests PRIVATE cxx_std_20)
target_compile_options(
fesa_hdf5_results_tests
PRIVATE
/W4
/permissive-
/EHsc
)
target_compile_definitions(
fesa_hdf5_results_tests
PRIVATE
FESA_TEST_BINARY_DIR="${CMAKE_BINARY_DIR}"
)
target_link_libraries(fesa_hdf5_results_tests
PRIVATE
fesa_core
HDF5::HDF5
GTest::gtest_main
)
add_test(
NAME Hdf5
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
--gtest_filter=Hdf5.*
)
add_test(
NAME ResultRoundTrip
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
--gtest_filter=ResultRoundTrip.*
)
set_property(
TEST Hdf5 ResultRoundTrip
PROPERTY ENVIRONMENT_MODIFICATION
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
)
add_executable(fesa_linear_static_analysis_tests
unit/analysis/linear_static_analysis_test.cpp
)
target_compile_features(
fesa_linear_static_analysis_tests PRIVATE cxx_std_20
)
target_compile_options(
fesa_linear_static_analysis_tests
PRIVATE
/W4
/permissive-
/EHsc
)
target_link_libraries(fesa_linear_static_analysis_tests
PRIVATE
fesa_core
GTest::gtest_main
)
add_test(
NAME LinearStaticAnalysis
COMMAND "$<TARGET_FILE:fesa_linear_static_analysis_tests>"
--gtest_filter=LinearStaticAnalysis.*
)
add_test(
NAME StaticEquilibrium
COMMAND "$<TARGET_FILE:fesa_linear_static_analysis_tests>"
--gtest_filter=StaticEquilibrium.*
)
set_property(
TEST LinearStaticAnalysis StaticEquilibrium
PROPERTY ENVIRONMENT_MODIFICATION
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
)
add_executable(fesa_pipeline_integration_tests
integration/pipeline/minimal_cantilever_test.cpp
)
target_compile_features(
fesa_pipeline_integration_tests PRIVATE cxx_std_20
)
target_compile_options(
fesa_pipeline_integration_tests
PRIVATE
/W4
/permissive-
/EHsc
)
target_compile_definitions(
fesa_pipeline_integration_tests
PRIVATE
FESA_CLI_PATH="$<TARGET_FILE:fesa>"
FESA_TEST_BINARY_DIR="${CMAKE_BINARY_DIR}"
FESA_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
target_link_libraries(fesa_pipeline_integration_tests
PRIVATE
fesa_core
GTest::gtest_main
)
add_dependencies(fesa_pipeline_integration_tests fesa)
add_test(
NAME MinimalCantileverPipeline
COMMAND "$<TARGET_FILE:fesa_pipeline_integration_tests>"
--gtest_filter=MinimalCantileverPipeline.*
)
set_property(
TEST MinimalCantileverPipeline
PROPERTY ENVIRONMENT_MODIFICATION
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
)
+566
View File
@@ -0,0 +1,566 @@
#include <fesa/io/hdf5/writer.hpp>
#include <fesa/model/domain_builder.hpp>
#include <hdf5.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <filesystem>
#include <limits>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
namespace {
class TestHdf5Handle final {
public:
using CloseFunction = herr_t (*)(hid_t);
TestHdf5Handle(hid_t id, CloseFunction close)
: id_{id}, close_{close} {
if (id_ < 0) {
throw std::runtime_error{"Failed to open test HDF5 resource."};
}
}
~TestHdf5Handle() {
if (id_ >= 0) {
close_(id_);
}
}
TestHdf5Handle(const TestHdf5Handle&) = delete;
TestHdf5Handle& operator=(const TestHdf5Handle&) = delete;
TestHdf5Handle(TestHdf5Handle&& other) noexcept
: id_{std::exchange(other.id_, H5I_INVALID_HID)},
close_{other.close_} {}
TestHdf5Handle& operator=(TestHdf5Handle&&) = delete;
[[nodiscard]] hid_t get() const noexcept {
return id_;
}
private:
hid_t id_;
CloseFunction close_;
};
std::string hdf5_path(const std::filesystem::path& path) {
const std::u8string value = path.u8string();
return {reinterpret_cast<const char*>(value.data()), value.size()};
}
void require_hdf5_status(const herr_t status) {
if (status < 0) {
throw std::runtime_error{"Test HDF5 mutation failed."};
}
}
void replace_step_time_with_vector(const std::filesystem::path& path) {
const std::string encoded_path = hdf5_path(path);
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle frame{
H5Gopen2(
file.get(), "/results/steps/0/frames/0", H5P_DEFAULT),
&H5Gclose,
};
require_hdf5_status(H5Adelete(frame.get(), "step_time"));
const std::array<hsize_t, 1> dimensions{2U};
TestHdf5Handle space{
H5Screate_simple(1, dimensions.data(), nullptr),
&H5Sclose,
};
TestHdf5Handle attribute{
H5Acreate2(
frame.get(),
"step_time",
H5T_IEEE_F64LE,
space.get(),
H5P_DEFAULT,
H5P_DEFAULT),
&H5Aclose,
};
const std::array<double, 2> values{1.25, 2.5};
require_hdf5_status(
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
}
void write_int64_dataset(
const std::filesystem::path& path,
const std::string_view dataset_path,
const std::span<const std::int64_t> values) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_dataset_path{dataset_path};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle dataset{
H5Dopen2(
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
&H5Dclose,
};
require_hdf5_status(H5Dwrite(
dataset.get(),
H5T_NATIVE_INT64,
H5S_ALL,
H5S_ALL,
H5P_DEFAULT,
values.data()));
}
void write_double_dataset(
const std::filesystem::path& path,
const std::string_view dataset_path,
const std::span<const double> values) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_dataset_path{dataset_path};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle dataset{
H5Dopen2(
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
&H5Dclose,
};
require_hdf5_status(H5Dwrite(
dataset.get(),
H5T_NATIVE_DOUBLE,
H5S_ALL,
H5S_ALL,
H5P_DEFAULT,
values.data()));
}
std::filesystem::path round_trip_path() {
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
"Temporary" / "fesa-round-trip.h5";
}
fesa::Domain make_domain() {
fesa::DomainBuilder builder;
builder.add_node({
fesa::NodeId{42},
fesa::EntityOrigin{"BeamPart", "Beam-1", 1001},
fesa::Vec3{1.25, -2.5, 3.75},
});
builder.add_node({
fesa::NodeId{7},
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
fesa::Vec3{4.0, 5.5, -6.25},
});
builder.add_material({
fesa::MaterialId{6},
"Steel",
210.0e9,
0.3,
});
builder.add_section({
fesa::SectionId{4},
"InputShear",
0.04,
1.2e-4,
1.4e-4,
2.0e-4,
0.031,
0.032,
fesa::ShearPropertySource::input,
fesa::Vec3{0.0, 1.0, 0.0},
{},
});
builder.add_section({
fesa::SectionId{12},
"DefaultShear",
0.06,
1.5e-4,
1.7e-4,
2.2e-4,
0.05,
0.05,
fesa::ShearPropertySource::phase1_default,
fesa::Vec3{0.0, 1.0, 0.0},
{},
});
builder.add_beam_element({
fesa::ElementId{9},
fesa::EntityOrigin{"BeamPart", "Beam-1", 2001},
{fesa::NodeId{7}, fesa::NodeId{42}},
fesa::MaterialId{6},
fesa::SectionId{4},
});
builder.add_beam_element({
fesa::ElementId{17},
fesa::EntityOrigin{"BeamPart", "Beam-1", 2002},
{fesa::NodeId{42}, fesa::NodeId{7}},
fesa::MaterialId{6},
fesa::SectionId{4},
});
builder.set_step({"Load/Case", {}, {}});
auto built = std::move(builder).build();
EXPECT_TRUE(built.domain.has_value());
EXPECT_TRUE(built.diagnostics.empty());
return std::move(*built.domain);
}
fesa::ResultDatabase make_database() {
return {
"1.0.0",
{{
"Load/Case",
{{
1.25,
{
{fesa::NodeId{7}, fesa::NodeId{42}},
{
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
},
{
{10.0, 20.0, 30.0, 40.0, 50.0, 60.0},
{-10.0, -20.0, -30.0, -40.0, -50.0, -60.0},
},
},
{},
}},
}},
};
}
bool has_results_error(
const std::vector<fesa::Diagnostic>& diagnostics,
const std::string_view code) {
return std::ranges::any_of(
diagnostics,
[code](const fesa::Diagnostic& diagnostic) {
return diagnostic.stage == fesa::DiagnosticStage::results &&
diagnostic.severity == fesa::Severity::error &&
diagnostic.code == code;
});
}
TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
const auto path = round_trip_path();
std::filesystem::create_directories(path.parent_path());
std::filesystem::remove(path);
const auto domain = make_domain();
const auto database = make_database();
const auto write_diagnostics =
fesa::write_hdf5(path, domain, database);
ASSERT_TRUE(write_diagnostics.empty());
const auto read = fesa::read_hdf5_results(path);
ASSERT_TRUE(read.diagnostics.empty());
ASSERT_TRUE(read.database.has_value());
ASSERT_TRUE(read.model.has_value());
EXPECT_EQ(read.database->schema_version, "1.0.0");
ASSERT_EQ(read.database->steps.size(), 1U);
const auto& step = read.database->steps[0];
EXPECT_EQ(step.name, "Load/Case");
ASSERT_EQ(step.frames.size(), 1U);
const auto& frame = step.frames[0];
EXPECT_DOUBLE_EQ(frame.step_time, 1.25);
EXPECT_EQ(
frame.nodal.node_ids,
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
EXPECT_EQ(
frame.nodal.displacement[0],
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
EXPECT_EQ(
frame.nodal.displacement[1],
(std::array<double, 6>{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0}));
EXPECT_EQ(
frame.nodal.reaction[0],
(std::array<double, 6>{10.0, 20.0, 30.0, 40.0, 50.0, 60.0}));
EXPECT_EQ(
frame.nodal.reaction[1],
(std::array<double, 6>{
-10.0, -20.0, -30.0, -40.0, -50.0, -60.0}));
EXPECT_TRUE(frame.diagnostics.empty());
ASSERT_EQ(read.model->nodes.size(), 2U);
EXPECT_EQ(read.model->nodes[0].dense_index, 0U);
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{42});
EXPECT_EQ(
read.model->nodes[0].origin,
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1001}));
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.x, 1.25);
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.y, -2.5);
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.z, 3.75);
EXPECT_EQ(read.model->nodes[1].dense_index, 1U);
EXPECT_EQ(read.model->nodes[1].id, fesa::NodeId{7});
EXPECT_EQ(
read.model->nodes[1].origin,
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 4.0);
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, 5.5);
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, -6.25);
ASSERT_EQ(read.model->elements.size(), 2U);
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
EXPECT_EQ(read.model->elements[0].id, fesa::ElementId{9});
EXPECT_EQ(
read.model->elements[0].connectivity,
(std::array<std::uint64_t, 2>{1U, 0U}));
EXPECT_EQ(read.model->elements[0].section, fesa::SectionId{4});
EXPECT_EQ(read.model->elements[1].dense_index, 1U);
EXPECT_EQ(read.model->elements[1].id, fesa::ElementId{17});
EXPECT_EQ(
read.model->elements[1].connectivity,
(std::array<std::uint64_t, 2>{0U, 1U}));
EXPECT_EQ(read.model->elements[1].section, fesa::SectionId{4});
ASSERT_EQ(read.model->sections.size(), 2U);
EXPECT_EQ(read.model->sections[0].id, fesa::SectionId{4});
EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_y, 0.031);
EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_z, 0.032);
EXPECT_EQ(
read.model->sections[0].shear_source,
fesa::ShearPropertySource::input);
EXPECT_EQ(read.model->sections[1].id, fesa::SectionId{12});
EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_y, 0.05);
EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_z, 0.05);
EXPECT_EQ(
read.model->sections[1].shear_source,
fesa::ShearPropertySource::phase1_default);
}
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" / "fesa-invalid.h5";
std::filesystem::remove(path);
const auto domain = make_domain();
auto database = make_database();
database.steps[0].frames[0].nodal.reaction.pop_back();
const auto diagnostics = fesa::write_hdf5(path, domain, database);
EXPECT_TRUE(
has_results_error(diagnostics, "results.nodal_size_mismatch"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-unsupported-diagnostics.h5";
std::filesystem::remove(path);
const auto domain = make_domain();
auto database = make_database();
database.steps[0].frames[0].diagnostics.push_back({
fesa::DiagnosticStage::solver,
fesa::Severity::warning,
"solver.residual",
"Residual diagnostic",
std::nullopt,
});
const auto diagnostics = fesa::write_hdf5(path, domain, database);
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.unsupported_result_diagnostics"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-missing-result-node.h5";
std::filesystem::remove(path);
const auto domain = make_domain();
auto database = make_database();
database.steps[0].frames[0].nodal.node_ids[0] = fesa::NodeId{999};
const auto diagnostics = fesa::write_hdf5(path, domain, database);
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.result_node_not_in_model"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, ReportsMissingFileAtResultsStage) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" / "fesa-missing.h5";
std::filesystem::remove(path);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.open_failed"));
}
TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonscalar-step-time.h5";
std::filesystem::remove(path);
const auto write_diagnostics =
fesa::write_hdf5(path, make_domain(), make_database());
ASSERT_TRUE(write_diagnostics.empty());
replace_step_time_with_vector(path);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
}
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-invalid-result-node.h5";
std::filesystem::remove(path);
const auto write_diagnostics =
fesa::write_hdf5(path, make_domain(), make_database());
ASSERT_TRUE(write_diagnostics.empty());
const std::array<std::int64_t, 2> node_ids{999, 42};
write_int64_dataset(
path,
"/results/steps/0/frames/0/nodal/node_ids",
node_ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(has_results_error(
read.diagnostics, "hdf5.result_node_not_in_model"));
}
TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-node-ids.h5";
std::filesystem::remove(path);
ASSERT_TRUE(
fesa::write_hdf5(path, make_domain(), make_database()).empty());
const std::array<std::int64_t, 2> ids{42, 42};
write_int64_dataset(path, "/model/nodes/internal_id", ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-element-ids.h5";
std::filesystem::remove(path);
ASSERT_TRUE(
fesa::write_hdf5(path, make_domain(), make_database()).empty());
const std::array<std::int64_t, 2> ids{9, 9};
write_int64_dataset(path, "/model/elements/internal_id", ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-section-ids.h5";
std::filesystem::remove(path);
ASSERT_TRUE(
fesa::write_hdf5(path, make_domain(), make_database()).empty());
const std::array<std::int64_t, 2> ids{4, 4};
write_int64_dataset(path, "/model/sections/internal_id", ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonfinite-coordinates.h5";
std::filesystem::remove(path);
ASSERT_TRUE(
fesa::write_hdf5(path, make_domain(), make_database()).empty());
const std::array<double, 6> coordinates{
std::numeric_limits<double>::quiet_NaN(),
-2.5,
3.75,
4.0,
5.5,
-6.25,
};
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonfinite-shear-area.h5";
std::filesystem::remove(path);
ASSERT_TRUE(
fesa::write_hdf5(path, make_domain(), make_database()).empty());
const std::array<double, 2> shear_areas{
std::numeric_limits<double>::infinity(),
0.05,
};
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonpositive-shear-area.h5";
std::filesystem::remove(path);
ASSERT_TRUE(
fesa::write_hdf5(path, make_domain(), make_database()).empty());
const std::array<double, 2> shear_areas{-0.031, 0.05};
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
} // namespace
@@ -0,0 +1,159 @@
#include <fesa/analysis/run_solver.hpp>
#include <fesa/io/hdf5/writer.hpp>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <gtest/gtest.h>
namespace {
class TemporaryPath final {
public:
explicit TemporaryPath(std::filesystem::path path)
: path_{std::move(path)} {
std::error_code error;
std::filesystem::create_directories(path_.parent_path(), error);
if (error) {
throw std::runtime_error{"Failed to create pipeline test directory."};
}
std::filesystem::remove(path_, error);
}
~TemporaryPath() {
std::error_code error;
std::filesystem::remove(path_, error);
}
TemporaryPath(const TemporaryPath&) = delete;
TemporaryPath& operator=(const TemporaryPath&) = delete;
[[nodiscard]] const std::filesystem::path& path() const noexcept {
return path_;
}
private:
std::filesystem::path path_;
};
std::filesystem::path fixture_path(const std::string_view name) {
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
"abaqus" / name;
}
std::filesystem::path test_output_path(const std::string_view name) {
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "testing" / name;
}
void write_text(
const std::filesystem::path& path,
const std::string_view contents) {
std::filesystem::create_directories(path.parent_path());
std::ofstream output{path, std::ios::binary};
output.write(
contents.data(), static_cast<std::streamsize>(contents.size()));
if (!output) {
throw std::runtime_error{"Failed to write pipeline test input."};
}
}
std::string quote(const std::filesystem::path& path) {
return '"' + path.string() + '"';
}
std::string read_text(const std::filesystem::path& path) {
std::ifstream input{path, std::ios::binary};
return {
std::istreambuf_iterator<char>{input},
std::istreambuf_iterator<char>{},
};
}
TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
const TemporaryPath output{
test_output_path("minimal-cantilever-pipeline.h5")};
const fesa::AnalysisRunResult run = fesa::run_solver({
fixture_path("minimal_cantilever.inp"),
output.path(),
});
ASSERT_TRUE(run.succeeded);
EXPECT_TRUE(run.diagnostics.empty());
const fesa::Hdf5ReadResult read =
fesa::read_hdf5_results(output.path());
ASSERT_TRUE(read.database.has_value());
ASSERT_TRUE(read.model.has_value());
EXPECT_TRUE(read.diagnostics.empty());
EXPECT_EQ(read.database->schema_version, "1.0.0");
ASSERT_EQ(read.model->nodes.size(), 2U);
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
EXPECT_EQ(read.model->nodes[1].id, fesa::NodeId{1});
ASSERT_EQ(read.database->steps.size(), 1U);
ASSERT_EQ(read.database->steps[0].frames.size(), 1U);
const fesa::NodalFrame& nodal =
read.database->steps[0].frames[0].nodal;
ASSERT_EQ(nodal.node_ids.size(), 2U);
ASSERT_EQ(nodal.displacement.size(), 2U);
ASSERT_EQ(nodal.reaction.size(), 2U);
EXPECT_EQ(nodal.node_ids[0], fesa::NodeId{0});
EXPECT_EQ(nodal.node_ids[1], fesa::NodeId{1});
for (const auto& values : nodal.displacement) {
for (const double value : values) {
EXPECT_TRUE(std::isfinite(value));
}
}
for (const auto& values : nodal.reaction) {
for (const double value : values) {
EXPECT_TRUE(std::isfinite(value));
}
}
const double total_y_reaction =
nodal.reaction[0][1] + nodal.reaction[1][1];
EXPECT_NEAR(total_y_reaction - 1.0, 0.0, 1.0e-12);
EXPECT_LT(nodal.displacement[1][1], 0.0);
}
TEST(MinimalCantileverPipeline, CliPreservesFailureSourceDiagnostic) {
const TemporaryPath input{
test_output_path("minimal-cantilever-invalid.inp")};
const TemporaryPath output{
test_output_path("minimal-cantilever-invalid.h5")};
const TemporaryPath error_output{
test_output_path("minimal-cantilever-invalid.stderr.txt")};
write_text(
input.path(),
"*NODE\n"
"*UNSUPPORTED\n");
const std::string command =
'"' + quote(std::filesystem::path{FESA_CLI_PATH}) + " solve " +
quote(input.path()) + " --output " + quote(output.path()) +
" 1>NUL 2>" + quote(error_output.path()) + '"';
const int exit_code = std::system(command.c_str());
EXPECT_NE(exit_code, 0);
EXPECT_FALSE(std::filesystem::exists(output.path()));
const std::string diagnostic = read_text(error_output.path());
EXPECT_NE(diagnostic.find("syntax"), std::string::npos);
EXPECT_NE(
diagnostic.find("abaqus.unsupported_keyword"),
std::string::npos);
EXPECT_NE(diagnostic.find(input.path().string()), std::string::npos);
EXPECT_NE(diagnostic.find(":2:1"), std::string::npos);
}
} // namespace
@@ -0,0 +1,217 @@
#include <fesa/analysis/linear_static_analysis.hpp>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <utility>
#include <vector>
#include <fesa/assembly/serial_assembler.hpp>
#include <fesa/fem/dof_manager.hpp>
#include <fesa/model/domain_builder.hpp>
#include <fesa/results/result_database.hpp>
#include <gtest/gtest.h>
namespace {
constexpr std::size_t dofs_per_node = 6;
std::vector<fesa::PrescribedDof> fixed_dofs(
const fesa::NodeId node,
const double ux = 0.0) {
std::vector<fesa::PrescribedDof> prescribed;
prescribed.reserve(dofs_per_node);
for (std::uint8_t dof = 1; dof <= dofs_per_node; ++dof) {
prescribed.push_back({node, dof, dof == 1 ? ux : 0.0});
}
return prescribed;
}
fesa::Domain build_axial_domain(const bool all_constrained) {
fesa::DomainBuilder builder;
builder.add_node({
fesa::NodeId{20},
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
fesa::Vec3{0.0, 0.0, 0.0},
});
builder.add_node({
fesa::NodeId{4},
fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
fesa::Vec3{4.0, 0.0, 0.0},
});
builder.add_material({
fesa::MaterialId{0},
"HandCalculated",
100.0,
0.25,
});
builder.add_section({
fesa::SectionId{0},
"General",
2.0,
1.0,
1.0,
1.0,
2.0,
2.0,
fesa::ShearPropertySource::input,
fesa::Vec3{0.0, 1.0, 0.0},
{},
});
builder.add_beam_element({
fesa::ElementId{0},
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
{fesa::NodeId{20}, fesa::NodeId{4}},
fesa::MaterialId{0},
fesa::SectionId{0},
});
std::vector<fesa::PrescribedDof> prescribed =
fixed_dofs(fesa::NodeId{20});
std::vector<fesa::PrescribedDof> end_constraints = fixed_dofs(
fesa::NodeId{4}, all_constrained ? 0.1 : 0.0);
if (!all_constrained) {
end_constraints.erase(end_constraints.begin());
}
prescribed.insert(
prescribed.end(),
end_constraints.begin(),
end_constraints.end());
std::vector<fesa::NodalLoad> loads;
if (!all_constrained) {
loads.push_back({
fesa::NodeId{4},
{10.0, 0.0, 0.0, 0.0, 0.0, 0.0},
});
}
builder.set_step({"Load", std::move(prescribed), std::move(loads)});
auto result = std::move(builder).build();
if (!result.domain.has_value()) {
throw std::runtime_error{"Test Domain failed validation."};
}
return std::move(*result.domain);
}
std::vector<double> full_field(
const fesa::NodalFrame& nodal,
const fesa::DofManager& dofs,
const bool reaction) {
std::vector<double> field(dofs.full_dof_count(), 0.0);
for (std::size_t node = 0; node < nodal.node_ids.size(); ++node) {
const auto& values = reaction
? nodal.reaction[node]
: nodal.displacement[node];
for (std::size_t component = 0; component < dofs_per_node;
++component) {
field[dofs.full_dof({
nodal.node_ids[node],
static_cast<fesa::NodeDof>(component),
})] = values[component];
}
}
return field;
}
std::vector<double> multiply(
const fesa::SymmetricCsr& matrix,
const std::vector<double>& vector) {
std::vector<double> product(matrix.order, 0.0);
for (std::size_t row = 0; row < matrix.order; ++row) {
const std::size_t begin =
static_cast<std::size_t>(matrix.row_offsets[row]);
const std::size_t end =
static_cast<std::size_t>(matrix.row_offsets[row + 1]);
for (std::size_t entry = begin; entry < end; ++entry) {
const std::size_t column = static_cast<std::size_t>(
matrix.column_indices[entry]);
const double value = matrix.values[entry];
product[row] += value * vector[column];
if (column != row) {
product[column] += value * vector[row];
}
}
}
return product;
}
TEST(LinearStaticAnalysis, SolvesHandCalculatedAxialBeamInNodeIdOrder) {
const fesa::Domain domain = build_axial_domain(false);
const fesa::AnalysisRunResult run =
fesa::LinearStaticAnalysis{}.run(domain);
ASSERT_TRUE(run.succeeded);
ASSERT_TRUE(run.results.has_value());
EXPECT_TRUE(run.diagnostics.empty());
EXPECT_TRUE(fesa::validate_result_database(*run.results).succeeded);
EXPECT_EQ(run.results->schema_version, "1.0.0");
ASSERT_EQ(run.results->steps.size(), 1);
EXPECT_EQ(run.results->steps[0].name, "Load");
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
const fesa::ResultFrame& frame = run.results->steps[0].frames[0];
EXPECT_DOUBLE_EQ(frame.step_time, 1.0);
EXPECT_TRUE(frame.diagnostics.empty());
ASSERT_EQ(frame.nodal.node_ids.size(), 2);
EXPECT_EQ(
frame.nodal.node_ids,
(std::vector<fesa::NodeId>{
fesa::NodeId{4}, fesa::NodeId{20}}));
EXPECT_NEAR(frame.nodal.displacement[0][0], 0.2, 1.0e-12);
EXPECT_NEAR(frame.nodal.displacement[1][0], 0.0, 1.0e-12);
EXPECT_NEAR(frame.nodal.reaction[0][0], 0.0, 1.0e-12);
EXPECT_NEAR(frame.nodal.reaction[1][0], -10.0, 1.0e-12);
}
TEST(StaticEquilibrium, ReturnedFieldsSatisfyOriginalFullEquation) {
const fesa::Domain domain = build_axial_domain(false);
const fesa::DofManager dofs = fesa::DofManager::build(domain);
const fesa::EquationSystem original =
fesa::assemble_serial(domain, dofs);
const fesa::AnalysisRunResult run =
fesa::LinearStaticAnalysis{}.run(domain);
ASSERT_TRUE(run.succeeded);
ASSERT_TRUE(run.results.has_value());
const fesa::NodalFrame& nodal =
run.results->steps[0].frames[0].nodal;
const std::vector<double> displacement =
full_field(nodal, dofs, false);
const std::vector<double> reaction =
full_field(nodal, dofs, true);
const std::vector<double> internal =
multiply(original.stiffness, displacement);
for (std::size_t dof = 0; dof < internal.size(); ++dof) {
EXPECT_NEAR(
internal[dof] - original.force[dof] - reaction[dof],
0.0,
1.0e-12);
EXPECT_TRUE(std::isfinite(displacement[dof]));
EXPECT_TRUE(std::isfinite(reaction[dof]));
}
}
TEST(LinearStaticAnalysis, SolvesAllConstrainedSystemWithoutPardiso) {
const fesa::Domain domain = build_axial_domain(true);
const fesa::AnalysisRunResult run =
fesa::LinearStaticAnalysis{}.run(domain);
ASSERT_TRUE(run.succeeded);
ASSERT_TRUE(run.results.has_value());
EXPECT_TRUE(run.diagnostics.empty());
const fesa::NodalFrame& nodal =
run.results->steps[0].frames[0].nodal;
ASSERT_EQ(nodal.node_ids.size(), 2);
ASSERT_EQ(nodal.node_ids[0], fesa::NodeId{4});
EXPECT_NEAR(nodal.displacement[0][0], 0.1, 1.0e-12);
EXPECT_NEAR(nodal.reaction[0][0], 5.0, 1.0e-12);
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
}
} // namespace
+138
View File
@@ -0,0 +1,138 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <limits>
#include <string_view>
#include <utility>
#include <fesa/results/result_database.hpp>
namespace {
std::array<double, 6> zeros() {
return {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
}
fesa::ResultDatabase valid_database() {
fesa::NodalFrame nodal{
{fesa::NodeId{0}, fesa::NodeId{1}},
{
zeros(),
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
},
{
{-10.0, -20.0, -30.0, -1.0, -2.0, -3.0},
zeros(),
},
};
fesa::ResultFrame frame{1.0, std::move(nodal), {}};
fesa::ResultStep step{"Load", {std::move(frame)}};
return {"1.0.0", {std::move(step)}};
}
bool has_diagnostic(
const fesa::Status& status,
const std::string_view code) {
return std::ranges::any_of(
status.diagnostics,
[code](const fesa::Diagnostic& diagnostic) {
return diagnostic.code == code &&
diagnostic.stage == fesa::DiagnosticStage::results &&
diagnostic.severity == fesa::Severity::error;
});
}
TEST(NodalFrame, AcceptsFiniteSixComponentFieldsWithMatchingNodeIds) {
const auto database = valid_database();
const auto status = fesa::validate_result_database(database);
EXPECT_TRUE(status.succeeded);
EXPECT_TRUE(status.diagnostics.empty());
}
TEST(NodalFrame, RejectsDisplacementSizeMismatch) {
auto database = valid_database();
database.steps[0].frames[0].nodal.displacement.pop_back();
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
}
TEST(NodalFrame, RejectsReactionSizeMismatch) {
auto database = valid_database();
database.steps[0].frames[0].nodal.reaction.pop_back();
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
}
TEST(NodalFrame, RejectsDuplicateNodeId) {
auto database = valid_database();
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_node_id"));
}
TEST(NodalFrame, RejectsNonfiniteDisplacement) {
auto database = valid_database();
database.steps[0].frames[0].nodal.displacement[1][2] =
std::numeric_limits<double>::quiet_NaN();
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
}
TEST(NodalFrame, RejectsNonfiniteReaction) {
auto database = valid_database();
database.steps[0].frames[0].nodal.reaction[0][4] =
std::numeric_limits<double>::infinity();
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
}
TEST(ResultDatabase, RejectsDuplicateStepName) {
auto database = valid_database();
database.steps.push_back(database.steps[0]);
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_step_name"));
}
TEST(ResultDatabase, RejectsDuplicateFrameTimeWithinStep) {
auto database = valid_database();
database.steps[0].frames.push_back(database.steps[0].frames[0]);
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_frame_time"));
}
TEST(ResultDatabase, RejectsNonfiniteFrameTime) {
auto database = valid_database();
database.steps[0].frames[0].step_time =
std::numeric_limits<double>::quiet_NaN();
const auto status = fesa::validate_result_database(database);
EXPECT_FALSE(status.succeeded);
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
}
} // namespace