feat(results-and-pipeline): step 3 — cli-pipeline-integration
This commit is contained in:
@@ -27,6 +27,7 @@ include(cmake/FesaDependencies.cmake)
|
|||||||
|
|
||||||
add_library(fesa_core STATIC
|
add_library(fesa_core STATIC
|
||||||
src/fesa/analysis/linear_static_analysis.cpp
|
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
|
||||||
|
|||||||
@@ -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,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 <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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,3 +488,43 @@ set_property(
|
|||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${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,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
|
||||||
Reference in New Issue
Block a user