Compare commits
10 Commits
4b75b72968
...
18361e4eb2
| Author | SHA1 | Date | |
|---|---|---|---|
| 18361e4eb2 | |||
| e31ef999bd | |||
| 93496c0157 | |||
| fe90528115 | |||
| 493219d3c2 | |||
| 11672df2c4 | |||
| d6306383a9 | |||
| 76a0c8ecde | |||
| f8af2c1485 | |||
| 5b7de91988 |
+5
-1
@@ -26,6 +26,8 @@ include(CTest)
|
||||
include(cmake/FesaDependencies.cmake)
|
||||
|
||||
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/constraints/essential_bc.cpp
|
||||
src/fesa/core/version.cpp
|
||||
@@ -36,8 +38,10 @@ add_library(fesa_core STATIC
|
||||
src/fesa/fem/line2_shape.cpp
|
||||
src/fesa/io/abaqus/parser.cpp
|
||||
src/fesa/io/abaqus/semantic_mapper.cpp
|
||||
src/fesa/io/hdf5/writer.cpp
|
||||
src/fesa/model/domain.cpp
|
||||
src/fesa/model/domain_builder.cpp
|
||||
src/fesa/results/result_database.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_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
|
||||
src/fesa/cli/main.cpp
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -22,7 +22,8 @@
|
||||
},
|
||||
{
|
||||
"dir": "results-and-pipeline",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"completed_at": "2026-08-01T01:11:01+0900"
|
||||
},
|
||||
{
|
||||
"dir": "abaqus-subset-completion",
|
||||
|
||||
@@ -5,22 +5,36 @@
|
||||
{
|
||||
"step": 0,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"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
|
||||
@@ -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
@@ -1,14 +1,75 @@
|
||||
#include <fesa/analysis/run_solver.hpp>
|
||||
#include <fesa/core/version.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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[]) {
|
||||
if (argc == 2 && std::string_view{argv[1]} == "--version") {
|
||||
std::cout << fesa::version() << '\n';
|
||||
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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -9,6 +9,12 @@ set_tests_properties(
|
||||
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
|
||||
unit/dependencies/dependency_smoke_test.cpp
|
||||
)
|
||||
@@ -375,3 +381,156 @@ set_property(
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${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}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user