refactor: extract abaqus input parser
This commit is contained in:
@@ -24,18 +24,25 @@ target_link_libraries(fesa_core_module_tests PRIVATE fesa_core)
|
|||||||
add_executable(fesa_math_module_tests tests/test_math_module_includes.cpp)
|
add_executable(fesa_math_module_tests tests/test_math_module_includes.cpp)
|
||||||
target_link_libraries(fesa_math_module_tests PRIVATE fesa_core)
|
target_link_libraries(fesa_math_module_tests PRIVATE fesa_core)
|
||||||
|
|
||||||
|
add_executable(fesa_io_module_tests tests/test_io_module_includes.cpp)
|
||||||
|
target_link_libraries(fesa_io_module_tests PRIVATE fesa_core)
|
||||||
|
target_compile_definitions(fesa_io_module_tests PRIVATE FESA_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(fesa_core PRIVATE /W4 /permissive-)
|
target_compile_options(fesa_core PRIVATE /W4 /permissive-)
|
||||||
target_compile_options(fesa_tests PRIVATE /W4 /permissive-)
|
target_compile_options(fesa_tests PRIVATE /W4 /permissive-)
|
||||||
target_compile_options(fesa_core_module_tests PRIVATE /W4 /permissive-)
|
target_compile_options(fesa_core_module_tests PRIVATE /W4 /permissive-)
|
||||||
target_compile_options(fesa_math_module_tests PRIVATE /W4 /permissive-)
|
target_compile_options(fesa_math_module_tests PRIVATE /W4 /permissive-)
|
||||||
|
target_compile_options(fesa_io_module_tests PRIVATE /W4 /permissive-)
|
||||||
else()
|
else()
|
||||||
target_compile_options(fesa_core PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(fesa_core PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
target_compile_options(fesa_tests PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(fesa_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
target_compile_options(fesa_core_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(fesa_core_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
target_compile_options(fesa_math_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(fesa_math_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
target_compile_options(fesa_io_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_test(NAME fesa_tests COMMAND fesa_tests)
|
add_test(NAME fesa_tests COMMAND fesa_tests)
|
||||||
add_test(NAME fesa_core_module_tests COMMAND fesa_core_module_tests)
|
add_test(NAME fesa_core_module_tests COMMAND fesa_core_module_tests)
|
||||||
add_test(NAME fesa_math_module_tests COMMAND fesa_math_module_tests)
|
add_test(NAME fesa_math_module_tests COMMAND fesa_math_module_tests)
|
||||||
|
add_test(NAME fesa_io_module_tests COMMAND fesa_io_module_tests)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Every new agent session must read this file together with `PROGRESS.md` before p
|
|||||||
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
|
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
|
||||||
|
|
||||||
## Current Objective
|
## Current Objective
|
||||||
Execute the Phase 1 structure-alignment refactor in `phases/1-structure-alignment-refactor`, continuing with P1A-04 IO parser extraction. P1A-00 completed the architecture drift audit, P1A-01 created the module scaffold, P1A-02 extracted Core/Util plus Phase 1 Boundary/Load/Property model ownership, and P1A-03 extracted Math primitives plus the solver adapter boundary without changing solver behavior. This phase must align the current monolithic `include/fesa/fesa.hpp` implementation with the module ownership model in `docs/ARCHITECTURE.md` without changing solver behavior. Product-level Phase 1 reference gaps R-010 and R-013 remain open and must not be hidden by the refactor.
|
Execute the Phase 1 structure-alignment refactor in `phases/1-structure-alignment-refactor`, continuing with P1A-05 Results/reference extraction. P1A-00 completed the architecture drift audit, P1A-01 created the module scaffold, P1A-02 extracted Core/Util plus Phase 1 Boundary/Load/Property model ownership, P1A-03 extracted Math primitives plus the solver adapter boundary, and P1A-04 extracted the Abaqus Phase 1 parser into IO without changing solver behavior. This phase must align the current monolithic `include/fesa/fesa.hpp` implementation with the module ownership model in `docs/ARCHITECTURE.md` without changing solver behavior. Product-level Phase 1 reference gaps R-010 and R-013 remain open and must not be hidden by the refactor.
|
||||||
|
|
||||||
## Required Reading For New Agents
|
## Required Reading For New Agents
|
||||||
1. `AGENTS.md`
|
1. `AGENTS.md`
|
||||||
@@ -37,7 +37,7 @@ Execute the Phase 1 structure-alignment refactor in `phases/1-structure-alignmen
|
|||||||
## Phase Files
|
## Phase Files
|
||||||
- Active phase directory: `phases/1-structure-alignment-refactor`
|
- Active phase directory: `phases/1-structure-alignment-refactor`
|
||||||
- Execute with: `python scripts/execute.py 1-structure-alignment-refactor`
|
- Execute with: `python scripts/execute.py 1-structure-alignment-refactor`
|
||||||
- Step numbering is zero-based. `step0.md` is complete and wrote `phases/1-structure-alignment-refactor/step0-architecture-map.md`; `step1.md` is complete and created module scaffold headers, source directories, CMake source discovery, and umbrella compatibility smoke coverage; `step2.md` is complete and extracted Core/Util domain, diagnostics, DofManager ownership, AnalysisModel/AnalysisState, and Phase 1 Boundary/Load/Property model ownership; `step3.md` is complete and extracted Math primitives, sparse pattern data, dense matrix support, and solver adapter boundary; `step4.md` extracts the Abaqus parser into IO; `step5.md` extracts Results and reference comparison code; `step6.md` extracts MITC4 geometry/strain helpers; `step7.md` extracts MITC4 material/stiffness helpers; `step8.md` extracts Assembly and Analysis workflow; `step9.md` is the independent architecture evaluator closeout.
|
- Step numbering is zero-based. `step0.md` is complete and wrote `phases/1-structure-alignment-refactor/step0-architecture-map.md`; `step1.md` is complete and created module scaffold headers, source directories, CMake source discovery, and umbrella compatibility smoke coverage; `step2.md` is complete and extracted Core/Util domain, diagnostics, DofManager ownership, AnalysisModel/AnalysisState, and Phase 1 Boundary/Load/Property model ownership; `step3.md` is complete and extracted Math primitives, sparse pattern data, dense matrix support, and solver adapter boundary; `step4.md` is complete and extracted the Abaqus parser into IO; `step5.md` extracts Results and reference comparison code; `step6.md` extracts MITC4 geometry/strain helpers; `step7.md` extracts MITC4 material/stiffness helpers; `step8.md` extracts Assembly and Analysis workflow; `step9.md` is the independent architecture evaluator closeout.
|
||||||
- Completed phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
- Completed phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
||||||
- Historical execution command: `python scripts/execute.py 1-linear-static-mitc4-rebaseline`
|
- Historical execution command: `python scripts/execute.py 1-linear-static-mitc4-rebaseline`
|
||||||
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step4.md` is complete and strengthened validation/singular diagnostics; `step5.md` is complete and revalidated the DofManager/reaction foundation; `step6.md` is complete and revalidated the minimum result model plus displacement CSV comparator; `step7.md` is complete and revalidated MITC4 natural coordinates, tying points, center directors, and integration bases; `step8.md` is complete and revalidated degenerated-continuum displacement, direct covariant strain rows, and MITC shear tying rows; `step9.md` is complete and revalidated plane-stress material, convected-to-local transform, and `2 x 2 x 2` material integration scaffolding; `step10.md` is complete and revalidated MITC4 stiffness, internal force, six-DOF transform, and drilling stabilization; `step11.md` is complete and added MITC4 membrane, bending, shear, twist, drilling-sensitivity, and thin-cantilever locking-sensitivity tests; `step12.md` is complete and revalidated full-space assembly, reduced projection, deterministic sparse-pattern scaffold, solver adapter injection, and full-vector internal/reaction force state; `step13.md` is complete and revalidated active AnalysisModel construction plus input-to-AnalysisState-to-U/RF result workflow; `step14.md` is complete and added the first stored Abaqus displacement regression for `quad_02_phase1`; `step15.md` is complete and recorded the independent evaluator closeout in `phases/1-linear-static-mitc4-rebaseline/step15-evaluator-report.md`.
|
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step4.md` is complete and strengthened validation/singular diagnostics; `step5.md` is complete and revalidated the DofManager/reaction foundation; `step6.md` is complete and revalidated the minimum result model plus displacement CSV comparator; `step7.md` is complete and revalidated MITC4 natural coordinates, tying points, center directors, and integration bases; `step8.md` is complete and revalidated degenerated-continuum displacement, direct covariant strain rows, and MITC shear tying rows; `step9.md` is complete and revalidated plane-stress material, convected-to-local transform, and `2 x 2 x 2` material integration scaffolding; `step10.md` is complete and revalidated MITC4 stiffness, internal force, six-DOF transform, and drilling stabilization; `step11.md` is complete and added MITC4 membrane, bending, shear, twist, drilling-sensitivity, and thin-cantilever locking-sensitivity tests; `step12.md` is complete and revalidated full-space assembly, reduced projection, deterministic sparse-pattern scaffold, solver adapter injection, and full-vector internal/reaction force state; `step13.md` is complete and revalidated active AnalysisModel construction plus input-to-AnalysisState-to-U/RF result workflow; `step14.md` is complete and added the first stored Abaqus displacement regression for `quad_02_phase1`; `step15.md` is complete and recorded the independent evaluator closeout in `phases/1-linear-static-mitc4-rebaseline/step15-evaluator-report.md`.
|
||||||
@@ -61,7 +61,7 @@ This phase is an architecture-preserving refactor. It must not change Phase 1 so
|
|||||||
| P1A-01 | completed | generator | Create module directory scaffold, CMake source boundaries, and umbrella facade policy. | P1A-00 | Module include smoke tests and build stability |
|
| P1A-01 | completed | generator | Create module directory scaffold, CMake source boundaries, and umbrella facade policy. | P1A-00 | Module include smoke tests and build stability |
|
||||||
| P1A-02 | completed | generator | Extract Core/Util domain, diagnostics, aliases, DOF mapping, `AnalysisModel`, `DofManager`, and Phase 1 Boundary/Load/Property model ownership. | P1A-01 | Core has no dependency on higher layers; Boundary/Load/Property types are no longer hidden in the umbrella header; DOF tests unchanged |
|
| P1A-02 | completed | generator | Extract Core/Util domain, diagnostics, aliases, DOF mapping, `AnalysisModel`, `DofManager`, and Phase 1 Boundary/Load/Property model ownership. | P1A-01 | Core has no dependency on higher layers; Boundary/Load/Property types are no longer hidden in the umbrella header; DOF tests unchanged |
|
||||||
| P1A-03 | completed | generator | Extract Math and solver adapter boundaries. | P1A-02 | Linear solver interface remains adapter-ready; int64 paths unchanged |
|
| P1A-03 | completed | generator | Extract Math and solver adapter boundaries. | P1A-02 | Linear solver interface remains adapter-ready; int64 paths unchanged |
|
||||||
| P1A-04 | pending | generator | Extract Abaqus parser into IO. | P1A-02 | Parser subset and unsupported-feature diagnostics unchanged |
|
| P1A-04 | completed | generator | Extract Abaqus parser into IO. | P1A-02 | Parser subset and unsupported-feature diagnostics unchanged |
|
||||||
| P1A-05 | pending | generator | Extract Results model, writer boundary, CSV loader, and reference comparator. | P1A-02, P1A-04 | `U`/`RF` schema and `quad_02_phase1` regression unchanged |
|
| P1A-05 | pending | generator | Extract Results model, writer boundary, CSV loader, and reference comparator. | P1A-02, P1A-04 | `U`/`RF` schema and `quad_02_phase1` regression unchanged |
|
||||||
| P1A-06 | pending | generator | Extract MITC4 geometry, director, strain, and tying helpers into Element. | P1A-03 | Geometry/strain tests and formulation signs unchanged |
|
| P1A-06 | pending | generator | Extract MITC4 geometry, director, strain, and tying helpers into Element. | P1A-03 | Geometry/strain tests and formulation signs unchanged |
|
||||||
| P1A-07 | pending | generator | Extract MITC4 material, integration, stiffness, drilling, and internal-force helpers. | P1A-06 | Patch, drilling, stiffness, and locking-sensitivity tests unchanged |
|
| P1A-07 | pending | generator | Extract MITC4 material, integration, stiffness, drilling, and internal-force helpers. | P1A-06 | Patch, drilling, stiffness, and locking-sensitivity tests unchanged |
|
||||||
|
|||||||
+32
-1
@@ -13,10 +13,41 @@ Every new agent session must read this file together with `PLAN.md` before plann
|
|||||||
- Do not remove history unless the user explicitly asks for archival cleanup.
|
- Do not remove history unless the user explicitly asks for archival cleanup.
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
Phase 1 has a completed rebaseline execution path in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 15 are complete, and P1R-15 recorded a pass-with-documented-gaps evaluator closeout. The follow-up architecture refactor phase in `phases/1-structure-alignment-refactor` is underway because the current production implementation is concentrated in `include/fesa/fesa.hpp` instead of the module directories documented in `docs/ARCHITECTURE.md`; P1A-00 through P1A-03 are complete, so the next step is P1A-04 IO parser extraction. `quad_02_phase1.inp` is the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, the Phase 1 parser/domain subset, validation/singular diagnostics, DofManager/reaction foundation, minimum result model metadata, displacement CSV comparator foundation, MITC4 geometry/director scaffolding, MITC4 displacement/strain/tying row scaffolding, MITC4 material/transform/integration scaffolding, MITC4 stiffness/drilling/internal-force scaffolding, MITC4 patch/locking-sensitivity tests, full-space assembly, reduced projection, sparse-pattern scaffold, solver adapter injection, full-vector internal/reaction force state, active AnalysisModel construction, input-to-AnalysisState-to-U/RF result workflow, and the first stored Abaqus displacement regression have been revalidated. Full PRD Phase 1 completion still depends on the open architecture/reference gaps R-014, R-010, and R-013. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
Phase 1 has a completed rebaseline execution path in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 15 are complete, and P1R-15 recorded a pass-with-documented-gaps evaluator closeout. The follow-up architecture refactor phase in `phases/1-structure-alignment-refactor` is underway because the current production implementation is concentrated in `include/fesa/fesa.hpp` instead of the module directories documented in `docs/ARCHITECTURE.md`; P1A-00 through P1A-04 are complete, so the next step is P1A-05 Results/reference extraction. `quad_02_phase1.inp` is the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, the Phase 1 parser/domain subset, validation/singular diagnostics, DofManager/reaction foundation, minimum result model metadata, displacement CSV comparator foundation, MITC4 geometry/director scaffolding, MITC4 displacement/strain/tying row scaffolding, MITC4 material/transform/integration scaffolding, MITC4 stiffness/drilling/internal-force scaffolding, MITC4 patch/locking-sensitivity tests, full-space assembly, reduced projection, sparse-pattern scaffold, solver adapter injection, full-vector internal/reaction force state, active AnalysisModel construction, input-to-AnalysisState-to-U/RF result workflow, and the first stored Abaqus displacement regression have been revalidated. Full PRD Phase 1 completion still depends on the open architecture/reference gaps R-014, R-010, and R-013. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||||
|
|
||||||
## Completed Work
|
## Completed Work
|
||||||
|
|
||||||
|
### 2026-05-05 - P1A-04 IO parser extraction completed
|
||||||
|
Author: Codex
|
||||||
|
|
||||||
|
Changed files:
|
||||||
|
- `CMakeLists.txt`
|
||||||
|
- `include/fesa/IO/AbaqusInputParser.hpp`
|
||||||
|
- `include/fesa/IO/IO.hpp`
|
||||||
|
- `include/fesa/fesa.hpp`
|
||||||
|
- `tests/test_io_module_includes.cpp`
|
||||||
|
- `phases/1-structure-alignment-refactor/index.json`
|
||||||
|
- `PLAN.md`
|
||||||
|
- `PROGRESS.md`
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
- Extracted `KeywordLine`, `ParseResult`, `parseKeywordLine`, and `AbaqusInputParser` from the umbrella header into `include/fesa/IO/AbaqusInputParser.hpp`.
|
||||||
|
- Updated `include/fesa/IO/IO.hpp` to expose the parser through the IO module while depending only on Core and Util.
|
||||||
|
- Updated `include/fesa/fesa.hpp` to include the IO module facade, preserving existing public symbols for umbrella consumers.
|
||||||
|
- Added `fesa_io_module_tests`, a direct IO include smoke/regression test that does not include `fesa/fesa.hpp`.
|
||||||
|
- Verified that `references/quad_02_phase1.inp` remains accepted with expected node, element, set, material, and shell-section counts.
|
||||||
|
- Verified that the original unsupported `references/quad_02.inp` still fails with unsupported-keyword diagnostics, and that nonzero prescribed displacement still reports `FESA-PARSE-BOUNDARY-NONZERO`.
|
||||||
|
- Remaining large groups in `fesa.hpp` are Assembly helpers (`buildReducedSparsePattern`, `recoverFullReaction`), MITC4 Element/Material helpers, Results/reference comparison, and Analysis workflow.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- First ran `python scripts\validate_workspace.py` after adding the direct IO include test; it failed as expected because `fesa/IO/IO.hpp` did not yet expose parser symbols.
|
||||||
|
- After extraction, `python scripts\validate_workspace.py` configured CMake, built `fesa_core`, `fesa_tests`, `fesa_core_module_tests`, `fesa_math_module_tests`, and `fesa_io_module_tests`, and ran CTest successfully.
|
||||||
|
- CTest result: 4 test executables passed.
|
||||||
|
|
||||||
|
Follow-up:
|
||||||
|
- Continue with P1A-05 Results/reference extraction.
|
||||||
|
- Keep R-014 open until P1A-09 independently accepts the final architecture alignment.
|
||||||
|
|
||||||
### 2026-05-05 - P1A-03 Math solver extraction completed
|
### 2026-05-05 - P1A-03 Math solver extraction completed
|
||||||
Author: Codex
|
Author: Codex
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,542 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Core.hpp"
|
||||||
|
#include "fesa/Util/Util.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <fstream>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct KeywordLine {
|
||||||
|
std::string name;
|
||||||
|
std::map<std::string, std::string> parameters;
|
||||||
|
std::set<std::string> flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline KeywordLine parseKeywordLine(const std::string& line) {
|
||||||
|
KeywordLine keyword;
|
||||||
|
std::vector<std::string> pieces = splitCsv(line.substr(1));
|
||||||
|
if (pieces.empty()) {
|
||||||
|
return keyword;
|
||||||
|
}
|
||||||
|
keyword.name = lower(trim(pieces.front()));
|
||||||
|
for (std::size_t i = 1; i < pieces.size(); ++i) {
|
||||||
|
const std::string piece = trim(pieces[i]);
|
||||||
|
if (piece.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto eq = piece.find('=');
|
||||||
|
if (eq == std::string::npos) {
|
||||||
|
keyword.flags.insert(lower(piece));
|
||||||
|
} else {
|
||||||
|
keyword.parameters[lower(trim(piece.substr(0, eq)))] = trim(piece.substr(eq + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParseResult {
|
||||||
|
Domain domain;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
|
||||||
|
bool ok() const {
|
||||||
|
return !hasError(diagnostics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class AbaqusInputParser {
|
||||||
|
public:
|
||||||
|
ParseResult parseString(const std::string& text, const std::string& file_name = "<memory>") const {
|
||||||
|
ParseResult result;
|
||||||
|
std::istringstream stream(text);
|
||||||
|
std::string line;
|
||||||
|
KeywordLine current;
|
||||||
|
std::string current_material_key;
|
||||||
|
KeywordLine current_shell_section;
|
||||||
|
LocalIndex line_number = 0;
|
||||||
|
LocalIndex current_keyword_line = 0;
|
||||||
|
|
||||||
|
auto add_error = [&](const std::string& code, const std::string& message) {
|
||||||
|
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
||||||
|
result.diagnostics.push_back({Severity::Error, code, message, {file_name, source_line, current.name}});
|
||||||
|
};
|
||||||
|
auto is_allowed = [](const std::string& value, std::initializer_list<const char*> allowed_values) {
|
||||||
|
return std::any_of(allowed_values.begin(), allowed_values.end(), [&](const char* allowed) {
|
||||||
|
return value == allowed;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
auto reject_unsupported_controls = [&](std::initializer_list<const char*> allowed_parameters,
|
||||||
|
std::initializer_list<const char*> allowed_flags) {
|
||||||
|
for (const auto& [parameter, value] : current.parameters) {
|
||||||
|
(void)value;
|
||||||
|
if (!is_allowed(parameter, allowed_parameters)) {
|
||||||
|
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-UNSUPPORTED-PARAMETER",
|
||||||
|
"Unsupported *" + current.name + " parameter: " + parameter,
|
||||||
|
{file_name, source_line, current.name}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const std::string& flag : current.flags) {
|
||||||
|
if (!is_allowed(flag, allowed_flags)) {
|
||||||
|
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-UNSUPPORTED-PARAMETER",
|
||||||
|
"Unsupported *" + current.name + " flag: " + flag,
|
||||||
|
{file_name, source_line, current.name}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while (std::getline(stream, line)) {
|
||||||
|
++line_number;
|
||||||
|
line = trim(line);
|
||||||
|
if (line.empty() || line.rfind("**", 0) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!line.empty() && line.front() == '*') {
|
||||||
|
current_keyword_line = line_number;
|
||||||
|
std::string keyword_line = line;
|
||||||
|
while (!keyword_line.empty() && keyword_line.back() == ',') {
|
||||||
|
std::string continuation;
|
||||||
|
if (!std::getline(stream, continuation)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++line_number;
|
||||||
|
continuation = trim(continuation);
|
||||||
|
if (continuation.empty() || continuation.rfind("**", 0) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
keyword_line += continuation;
|
||||||
|
}
|
||||||
|
current = parseKeywordLine(keyword_line);
|
||||||
|
if (current.name == "node") {
|
||||||
|
reject_unsupported_controls({}, {});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "element") {
|
||||||
|
reject_unsupported_controls({"type", "elset"}, {});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "nset") {
|
||||||
|
reject_unsupported_controls({"nset"}, {"generate"});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "elset") {
|
||||||
|
reject_unsupported_controls({"elset"}, {"generate"});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "elastic") {
|
||||||
|
reject_unsupported_controls({}, {});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "shell section") {
|
||||||
|
reject_unsupported_controls({"elset", "material"}, {});
|
||||||
|
current_shell_section = current;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "boundary" || current.name == "cload" || current.name == "static") {
|
||||||
|
reject_unsupported_controls({}, {});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "material") {
|
||||||
|
reject_unsupported_controls({"name"}, {});
|
||||||
|
auto name_it = current.parameters.find("name");
|
||||||
|
if (name_it == current.parameters.end() || trim(name_it->second).empty()) {
|
||||||
|
add_error("FESA-PARSE-MATERIAL-NAME", "*Material requires NAME");
|
||||||
|
current_material_key.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Material material;
|
||||||
|
material.name = trim(name_it->second);
|
||||||
|
current_material_key = Domain::key(material.name);
|
||||||
|
if (result.domain.materials.count(current_material_key) != 0) {
|
||||||
|
add_error("FESA-PARSE-DUPLICATE-MATERIAL", "Duplicate material: " + material.name);
|
||||||
|
} else {
|
||||||
|
result.domain.materials[current_material_key] = material;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "step") {
|
||||||
|
reject_unsupported_controls({"name", "nlgeom"}, {});
|
||||||
|
auto nlgeom = current.parameters.find("nlgeom");
|
||||||
|
if (nlgeom != current.parameters.end() && lower(trim(nlgeom->second)) == "yes") {
|
||||||
|
add_error("FESA-PARSE-UNSUPPORTED-NLGEOM", "NLGEOM=YES is not supported in Phase 1");
|
||||||
|
}
|
||||||
|
StepDefinition step;
|
||||||
|
auto name_it = current.parameters.find("name");
|
||||||
|
if (name_it != current.parameters.end() && !trim(name_it->second).empty()) {
|
||||||
|
step.name = trim(name_it->second);
|
||||||
|
}
|
||||||
|
result.domain.steps.push_back(step);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current.name == "end step") {
|
||||||
|
reject_unsupported_controls({}, {});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
add_error("FESA-PARSE-UNSUPPORTED-KEYWORD", "Unsupported keyword: *" + current.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string> fields = splitCsv(line);
|
||||||
|
if (current.name == "node") {
|
||||||
|
parseNode(fields, result, file_name, line_number);
|
||||||
|
} else if (current.name == "element") {
|
||||||
|
parseElement(fields, current, result, file_name, line_number);
|
||||||
|
} else if (current.name == "nset") {
|
||||||
|
parseNodeSet(fields, current, result, file_name, line_number);
|
||||||
|
} else if (current.name == "elset") {
|
||||||
|
parseElementSet(fields, current, result, file_name, line_number);
|
||||||
|
} else if (current.name == "elastic") {
|
||||||
|
parseElastic(fields, current_material_key, result, file_name, line_number);
|
||||||
|
} else if (current.name == "shell section") {
|
||||||
|
parseShellSection(fields, current_shell_section, result, file_name, line_number);
|
||||||
|
} else if (current.name == "boundary") {
|
||||||
|
parseBoundary(fields, result, file_name, line_number);
|
||||||
|
} else if (current.name == "cload") {
|
||||||
|
parseLoad(fields, result, file_name, line_number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.domain.steps.empty()) {
|
||||||
|
result.domain.steps.push_back({"Step-1", "linear_static"});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseResult parseFile(const std::string& path) const {
|
||||||
|
std::ifstream input(path);
|
||||||
|
std::ostringstream buffer;
|
||||||
|
buffer << input.rdbuf();
|
||||||
|
ParseResult result = parseString(buffer.str(), path);
|
||||||
|
if (!input.good() && buffer.str().empty()) {
|
||||||
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-FILE", "Could not read input file", {path, 0, ""}});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::size_t effectiveFieldCount(const std::vector<std::string>& fields) {
|
||||||
|
std::size_t count = fields.size();
|
||||||
|
while (count > 0 && trim(fields[count - 1]).empty()) {
|
||||||
|
--count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseNode(const std::vector<std::string>& fields,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
if (effectiveFieldCount(fields) != 4) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-NODE", "*Node data requires id,x,y,z", {file_name, line, "node"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto id = parseInt64(fields[0]);
|
||||||
|
auto x = parseReal(fields[1]);
|
||||||
|
auto y = parseReal(fields[2]);
|
||||||
|
auto z = parseReal(fields[3]);
|
||||||
|
if (!id || !x || !y || !z) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-NODE-NUMERIC", "Invalid node numeric field", {file_name, line, "node"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.domain.nodes.count(*id) != 0) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-DUPLICATE-NODE", "Duplicate node id", {file_name, line, "node"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.domain.nodes[*id] = {*id, {*x, *y, *z}};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseElement(const std::vector<std::string>& fields,
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
auto type_it = keyword.parameters.find("type");
|
||||||
|
if (type_it == keyword.parameters.end()) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-ELEMENT-TYPE", "*Element requires TYPE", {file_name, line, "element"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::string type = lower(trim(type_it->second));
|
||||||
|
if (type != "s4") {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-UNSUPPORTED-ELEMENT",
|
||||||
|
"Unsupported element type: " + type_it->second,
|
||||||
|
{file_name, line, "element"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (effectiveFieldCount(fields) != 5) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-ELEMENT",
|
||||||
|
"S4 element requires id,n1,n2,n3,n4",
|
||||||
|
{file_name, line, "element"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto id = parseInt64(fields[0]);
|
||||||
|
std::array<GlobalId, 4> nodes{};
|
||||||
|
bool ok = id.has_value();
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
auto node = parseInt64(fields[1 + static_cast<std::size_t>(i)]);
|
||||||
|
ok = ok && node.has_value();
|
||||||
|
if (node) {
|
||||||
|
nodes[static_cast<std::size_t>(i)] = *node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-ELEMENT-NUMERIC", "Invalid element numeric field", {file_name, line, "element"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.domain.elements.count(*id) != 0) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-DUPLICATE-ELEMENT", "Duplicate element id", {file_name, line, "element"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Element element;
|
||||||
|
element.id = *id;
|
||||||
|
element.node_ids = nodes;
|
||||||
|
auto elset_it = keyword.parameters.find("elset");
|
||||||
|
if (elset_it != keyword.parameters.end()) {
|
||||||
|
element.source_elset = trim(elset_it->second);
|
||||||
|
auto& set = result.domain.element_sets[Domain::key(element.source_elset)];
|
||||||
|
set.name = element.source_elset;
|
||||||
|
addUnique(set.element_ids, *id);
|
||||||
|
}
|
||||||
|
result.domain.elements[*id] = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseNodeSet(const std::vector<std::string>& fields,
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
auto name_it = keyword.parameters.find("nset");
|
||||||
|
if (name_it == keyword.parameters.end()) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-NSET-NAME", "*Nset requires NSET", {file_name, line, "nset"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& set = result.domain.node_sets[Domain::key(name_it->second)];
|
||||||
|
set.name = trim(name_it->second);
|
||||||
|
parseSetData(fields, keyword.flags.count("generate") != 0, set.node_ids, result.diagnostics, file_name, line, "nset");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseElementSet(const std::vector<std::string>& fields,
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
auto name_it = keyword.parameters.find("elset");
|
||||||
|
if (name_it == keyword.parameters.end()) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-ELSET-NAME", "*Elset requires ELSET", {file_name, line, "elset"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& set = result.domain.element_sets[Domain::key(name_it->second)];
|
||||||
|
set.name = trim(name_it->second);
|
||||||
|
parseSetData(fields, keyword.flags.count("generate") != 0, set.element_ids, result.diagnostics, file_name, line, "elset");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseSetData(const std::vector<std::string>& fields,
|
||||||
|
bool generate,
|
||||||
|
std::vector<GlobalId>& output,
|
||||||
|
std::vector<Diagnostic>& diagnostics,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line,
|
||||||
|
const std::string& keyword) {
|
||||||
|
if (generate) {
|
||||||
|
const std::size_t field_count = effectiveFieldCount(fields);
|
||||||
|
if (field_count != 3) {
|
||||||
|
diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-GENERATE",
|
||||||
|
"Generated set requires first,last,increment",
|
||||||
|
{file_name, line, keyword}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto first = parseInt64(fields[0]);
|
||||||
|
auto last = parseInt64(fields[1]);
|
||||||
|
auto increment = parseInt64(fields[2]);
|
||||||
|
if (!first || !last || !increment || *increment <= 0) {
|
||||||
|
diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-GENERATE", "Invalid generated set range", {file_name, line, keyword}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GlobalId value : generatedRange(*first, *last, *increment)) {
|
||||||
|
addUnique(output, value);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t field_count = effectiveFieldCount(fields);
|
||||||
|
for (std::size_t i = 0; i < field_count; ++i) {
|
||||||
|
const std::string& field = fields[i];
|
||||||
|
if (trim(field).empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto value = parseInt64(field);
|
||||||
|
if (!value) {
|
||||||
|
diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-SET-NUMERIC", "Invalid set id", {file_name, line, keyword}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addUnique(output, *value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseElastic(const std::vector<std::string>& fields,
|
||||||
|
const std::string& material_key,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
if (material_key.empty() || result.domain.materials.count(material_key) == 0) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-ELASTIC-MATERIAL", "*Elastic must follow *Material", {file_name, line, "elastic"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t field_count = effectiveFieldCount(fields);
|
||||||
|
if (field_count < 2) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-ELASTIC", "*Elastic requires E,nu", {file_name, line, "elastic"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (field_count > 2) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-ELASTIC-UNSUPPORTED",
|
||||||
|
"Only isotropic E,nu elastic data is supported",
|
||||||
|
{file_name, line, "elastic"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto e = parseReal(fields[0]);
|
||||||
|
auto nu = parseReal(fields[1]);
|
||||||
|
if (!e || !nu || *e <= 0.0 || *nu <= -1.0 || *nu >= 0.5) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-ELASTIC-RANGE",
|
||||||
|
"Invalid isotropic elastic constants",
|
||||||
|
{file_name, line, "elastic"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.domain.materials[material_key].elastic_modulus = *e;
|
||||||
|
result.domain.materials[material_key].poisson_ratio = *nu;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseShellSection(const std::vector<std::string>& fields,
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
auto elset_it = keyword.parameters.find("elset");
|
||||||
|
auto material_it = keyword.parameters.find("material");
|
||||||
|
if (elset_it == keyword.parameters.end() || material_it == keyword.parameters.end()) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-SHELL-SECTION-PARAM",
|
||||||
|
"*Shell Section requires ELSET and MATERIAL",
|
||||||
|
{file_name, line, "shell section"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t field_count = effectiveFieldCount(fields);
|
||||||
|
if (field_count == 0) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-SHELL-SECTION",
|
||||||
|
"*Shell Section requires thickness",
|
||||||
|
{file_name, line, "shell section"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (field_count > 1) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-SHELL-SECTION-UNSUPPORTED",
|
||||||
|
"Only homogeneous shell thickness data is supported",
|
||||||
|
{file_name, line, "shell section"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto thickness = parseReal(fields[0]);
|
||||||
|
if (!thickness || *thickness <= 0.0) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-SHELL-THICKNESS",
|
||||||
|
"Shell thickness must be positive",
|
||||||
|
{file_name, line, "shell section"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.domain.shell_sections.push_back({trim(elset_it->second), trim(material_it->second), *thickness});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseBoundary(const std::vector<std::string>& fields,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
const std::size_t field_count = effectiveFieldCount(fields);
|
||||||
|
if (field_count < 2) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-BOUNDARY", "*Boundary requires target,first_dof", {file_name, line, "boundary"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (field_count > 4) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-BOUNDARY-UNSUPPORTED",
|
||||||
|
"Only direct zero-valued boundary data is supported",
|
||||||
|
{file_name, line, "boundary"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto first = parseInt64(fields[1]);
|
||||||
|
auto last = field_count >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first;
|
||||||
|
auto magnitude = field_count >= 4 && !fields[3].empty() ? parseReal(fields[3]) : std::optional<Real>(0.0);
|
||||||
|
if (!first || !last || !magnitude || !dofFromAbaqus(static_cast<int>(*first)) ||
|
||||||
|
!dofFromAbaqus(static_cast<int>(*last)) || *first > *last) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-BOUNDARY-DOF",
|
||||||
|
"Invalid boundary DOF range",
|
||||||
|
{file_name, line, "boundary"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (std::fabs(*magnitude) > 0.0) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-BOUNDARY-NONZERO",
|
||||||
|
"Nonzero prescribed displacement is not supported in Phase 1",
|
||||||
|
{file_name, line, "boundary"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.domain.boundary_conditions.push_back({trim(fields[0]), static_cast<int>(*first), static_cast<int>(*last), *magnitude});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseLoad(const std::vector<std::string>& fields,
|
||||||
|
ParseResult& result,
|
||||||
|
const std::string& file_name,
|
||||||
|
LocalIndex line) {
|
||||||
|
const std::size_t field_count = effectiveFieldCount(fields);
|
||||||
|
if (field_count < 3) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-CLOAD", "*Cload requires target,dof,magnitude", {file_name, line, "cload"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (field_count > 3) {
|
||||||
|
result.diagnostics.push_back({Severity::Error,
|
||||||
|
"FESA-PARSE-CLOAD-UNSUPPORTED",
|
||||||
|
"Only direct concentrated load data is supported",
|
||||||
|
{file_name, line, "cload"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto dof = parseInt64(fields[1]);
|
||||||
|
auto magnitude = parseReal(fields[2]);
|
||||||
|
if (!dof || !magnitude || !dofFromAbaqus(static_cast<int>(*dof))) {
|
||||||
|
result.diagnostics.push_back(
|
||||||
|
{Severity::Error, "FESA-PARSE-CLOAD-DOF", "Invalid concentrated load", {file_name, line, "cload"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.domain.loads.push_back({trim(fields[0]), static_cast<int>(*dof), *magnitude});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/IO/AbaqusInputParser.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
|
|
||||||
namespace fesa::module {
|
namespace fesa::module {
|
||||||
|
|||||||
+1
-443
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "fesa/Boundary/Boundary.hpp"
|
#include "fesa/Boundary/Boundary.hpp"
|
||||||
#include "fesa/Core/Core.hpp"
|
#include "fesa/Core/Core.hpp"
|
||||||
|
#include "fesa/IO/IO.hpp"
|
||||||
#include "fesa/Load/Load.hpp"
|
#include "fesa/Load/Load.hpp"
|
||||||
#include "fesa/Math/Math.hpp"
|
#include "fesa/Math/Math.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
@@ -28,449 +29,6 @@
|
|||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
struct KeywordLine {
|
|
||||||
std::string name;
|
|
||||||
std::map<std::string, std::string> parameters;
|
|
||||||
std::set<std::string> flags;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline KeywordLine parseKeywordLine(const std::string& line) {
|
|
||||||
KeywordLine keyword;
|
|
||||||
std::vector<std::string> pieces = splitCsv(line.substr(1));
|
|
||||||
if (pieces.empty()) {
|
|
||||||
return keyword;
|
|
||||||
}
|
|
||||||
keyword.name = lower(trim(pieces.front()));
|
|
||||||
for (std::size_t i = 1; i < pieces.size(); ++i) {
|
|
||||||
const std::string piece = trim(pieces[i]);
|
|
||||||
if (piece.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto eq = piece.find('=');
|
|
||||||
if (eq == std::string::npos) {
|
|
||||||
keyword.flags.insert(lower(piece));
|
|
||||||
} else {
|
|
||||||
keyword.parameters[lower(trim(piece.substr(0, eq)))] = trim(piece.substr(eq + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keyword;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ParseResult {
|
|
||||||
Domain domain;
|
|
||||||
std::vector<Diagnostic> diagnostics;
|
|
||||||
|
|
||||||
bool ok() const {
|
|
||||||
return !hasError(diagnostics);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class AbaqusInputParser {
|
|
||||||
public:
|
|
||||||
ParseResult parseString(const std::string& text, const std::string& file_name = "<memory>") const {
|
|
||||||
ParseResult result;
|
|
||||||
std::istringstream stream(text);
|
|
||||||
std::string line;
|
|
||||||
KeywordLine current;
|
|
||||||
std::string current_material_key;
|
|
||||||
KeywordLine current_shell_section;
|
|
||||||
LocalIndex line_number = 0;
|
|
||||||
LocalIndex current_keyword_line = 0;
|
|
||||||
|
|
||||||
auto add_error = [&](const std::string& code, const std::string& message) {
|
|
||||||
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
|
||||||
result.diagnostics.push_back({Severity::Error, code, message, {file_name, source_line, current.name}});
|
|
||||||
};
|
|
||||||
auto is_allowed = [](const std::string& value, std::initializer_list<const char*> allowed_values) {
|
|
||||||
return std::any_of(allowed_values.begin(), allowed_values.end(), [&](const char* allowed) {
|
|
||||||
return value == allowed;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
auto reject_unsupported_controls = [&](std::initializer_list<const char*> allowed_parameters,
|
|
||||||
std::initializer_list<const char*> allowed_flags) {
|
|
||||||
for (const auto& [parameter, value] : current.parameters) {
|
|
||||||
(void)value;
|
|
||||||
if (!is_allowed(parameter, allowed_parameters)) {
|
|
||||||
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
|
||||||
result.diagnostics.push_back({Severity::Error,
|
|
||||||
"FESA-PARSE-UNSUPPORTED-PARAMETER",
|
|
||||||
"Unsupported *" + current.name + " parameter: " + parameter,
|
|
||||||
{file_name, source_line, current.name}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const std::string& flag : current.flags) {
|
|
||||||
if (!is_allowed(flag, allowed_flags)) {
|
|
||||||
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
|
||||||
result.diagnostics.push_back({Severity::Error,
|
|
||||||
"FESA-PARSE-UNSUPPORTED-PARAMETER",
|
|
||||||
"Unsupported *" + current.name + " flag: " + flag,
|
|
||||||
{file_name, source_line, current.name}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while (std::getline(stream, line)) {
|
|
||||||
++line_number;
|
|
||||||
line = trim(line);
|
|
||||||
if (line.empty() || line.rfind("**", 0) == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!line.empty() && line.front() == '*') {
|
|
||||||
current_keyword_line = line_number;
|
|
||||||
std::string keyword_line = line;
|
|
||||||
while (!keyword_line.empty() && keyword_line.back() == ',') {
|
|
||||||
std::string continuation;
|
|
||||||
if (!std::getline(stream, continuation)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
++line_number;
|
|
||||||
continuation = trim(continuation);
|
|
||||||
if (continuation.empty() || continuation.rfind("**", 0) == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
keyword_line += continuation;
|
|
||||||
}
|
|
||||||
current = parseKeywordLine(keyword_line);
|
|
||||||
if (current.name == "node") {
|
|
||||||
reject_unsupported_controls({}, {});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "element") {
|
|
||||||
reject_unsupported_controls({"type", "elset"}, {});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "nset") {
|
|
||||||
reject_unsupported_controls({"nset"}, {"generate"});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "elset") {
|
|
||||||
reject_unsupported_controls({"elset"}, {"generate"});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "elastic") {
|
|
||||||
reject_unsupported_controls({}, {});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "shell section") {
|
|
||||||
reject_unsupported_controls({"elset", "material"}, {});
|
|
||||||
current_shell_section = current;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "boundary" || current.name == "cload" || current.name == "static") {
|
|
||||||
reject_unsupported_controls({}, {});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "material") {
|
|
||||||
reject_unsupported_controls({"name"}, {});
|
|
||||||
auto name_it = current.parameters.find("name");
|
|
||||||
if (name_it == current.parameters.end() || trim(name_it->second).empty()) {
|
|
||||||
add_error("FESA-PARSE-MATERIAL-NAME", "*Material requires NAME");
|
|
||||||
current_material_key.clear();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Material material;
|
|
||||||
material.name = trim(name_it->second);
|
|
||||||
current_material_key = Domain::key(material.name);
|
|
||||||
if (result.domain.materials.count(current_material_key) != 0) {
|
|
||||||
add_error("FESA-PARSE-DUPLICATE-MATERIAL", "Duplicate material: " + material.name);
|
|
||||||
} else {
|
|
||||||
result.domain.materials[current_material_key] = material;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "step") {
|
|
||||||
reject_unsupported_controls({"name", "nlgeom"}, {});
|
|
||||||
auto nlgeom = current.parameters.find("nlgeom");
|
|
||||||
if (nlgeom != current.parameters.end() && lower(trim(nlgeom->second)) == "yes") {
|
|
||||||
add_error("FESA-PARSE-UNSUPPORTED-NLGEOM", "NLGEOM=YES is not supported in Phase 1");
|
|
||||||
}
|
|
||||||
StepDefinition step;
|
|
||||||
auto name_it = current.parameters.find("name");
|
|
||||||
if (name_it != current.parameters.end() && !trim(name_it->second).empty()) {
|
|
||||||
step.name = trim(name_it->second);
|
|
||||||
}
|
|
||||||
result.domain.steps.push_back(step);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (current.name == "end step") {
|
|
||||||
reject_unsupported_controls({}, {});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
add_error("FESA-PARSE-UNSUPPORTED-KEYWORD", "Unsupported keyword: *" + current.name);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<std::string> fields = splitCsv(line);
|
|
||||||
if (current.name == "node") {
|
|
||||||
parseNode(fields, result, file_name, line_number);
|
|
||||||
} else if (current.name == "element") {
|
|
||||||
parseElement(fields, current, result, file_name, line_number);
|
|
||||||
} else if (current.name == "nset") {
|
|
||||||
parseNodeSet(fields, current, result, file_name, line_number);
|
|
||||||
} else if (current.name == "elset") {
|
|
||||||
parseElementSet(fields, current, result, file_name, line_number);
|
|
||||||
} else if (current.name == "elastic") {
|
|
||||||
parseElastic(fields, current_material_key, result, file_name, line_number);
|
|
||||||
} else if (current.name == "shell section") {
|
|
||||||
parseShellSection(fields, current_shell_section, result, file_name, line_number);
|
|
||||||
} else if (current.name == "boundary") {
|
|
||||||
parseBoundary(fields, result, file_name, line_number);
|
|
||||||
} else if (current.name == "cload") {
|
|
||||||
parseLoad(fields, result, file_name, line_number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.domain.steps.empty()) {
|
|
||||||
result.domain.steps.push_back({"Step-1", "linear_static"});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
ParseResult parseFile(const std::string& path) const {
|
|
||||||
std::ifstream input(path);
|
|
||||||
std::ostringstream buffer;
|
|
||||||
buffer << input.rdbuf();
|
|
||||||
ParseResult result = parseString(buffer.str(), path);
|
|
||||||
if (!input.good() && buffer.str().empty()) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-FILE", "Could not read input file", {path, 0, ""}});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
static std::size_t effectiveFieldCount(const std::vector<std::string>& fields) {
|
|
||||||
std::size_t count = fields.size();
|
|
||||||
while (count > 0 && trim(fields[count - 1]).empty()) {
|
|
||||||
--count;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseNode(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
if (effectiveFieldCount(fields) != 4) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-NODE", "*Node data requires id,x,y,z", {file_name, line, "node"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto id = parseInt64(fields[0]);
|
|
||||||
auto x = parseReal(fields[1]);
|
|
||||||
auto y = parseReal(fields[2]);
|
|
||||||
auto z = parseReal(fields[3]);
|
|
||||||
if (!id || !x || !y || !z) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-NODE-NUMERIC", "Invalid node numeric field", {file_name, line, "node"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.domain.nodes.count(*id) != 0) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-DUPLICATE-NODE", "Duplicate node id", {file_name, line, "node"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.domain.nodes[*id] = {*id, {*x, *y, *z}};
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseElement(const std::vector<std::string>& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
auto type_it = keyword.parameters.find("type");
|
|
||||||
if (type_it == keyword.parameters.end()) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELEMENT-TYPE", "*Element requires TYPE", {file_name, line, "element"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::string type = lower(trim(type_it->second));
|
|
||||||
if (type != "s4") {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-UNSUPPORTED-ELEMENT", "Unsupported element type: " + type_it->second, {file_name, line, "element"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (effectiveFieldCount(fields) != 5) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELEMENT", "S4 element requires id,n1,n2,n3,n4", {file_name, line, "element"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto id = parseInt64(fields[0]);
|
|
||||||
std::array<GlobalId, 4> nodes{};
|
|
||||||
bool ok = id.has_value();
|
|
||||||
for (int i = 0; i < 4; ++i) {
|
|
||||||
auto node = parseInt64(fields[1 + static_cast<std::size_t>(i)]);
|
|
||||||
ok = ok && node.has_value();
|
|
||||||
if (node) {
|
|
||||||
nodes[static_cast<std::size_t>(i)] = *node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ok) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELEMENT-NUMERIC", "Invalid element numeric field", {file_name, line, "element"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.domain.elements.count(*id) != 0) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-DUPLICATE-ELEMENT", "Duplicate element id", {file_name, line, "element"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Element element;
|
|
||||||
element.id = *id;
|
|
||||||
element.node_ids = nodes;
|
|
||||||
auto elset_it = keyword.parameters.find("elset");
|
|
||||||
if (elset_it != keyword.parameters.end()) {
|
|
||||||
element.source_elset = trim(elset_it->second);
|
|
||||||
auto& set = result.domain.element_sets[Domain::key(element.source_elset)];
|
|
||||||
set.name = element.source_elset;
|
|
||||||
addUnique(set.element_ids, *id);
|
|
||||||
}
|
|
||||||
result.domain.elements[*id] = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseNodeSet(const std::vector<std::string>& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
auto name_it = keyword.parameters.find("nset");
|
|
||||||
if (name_it == keyword.parameters.end()) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-NSET-NAME", "*Nset requires NSET", {file_name, line, "nset"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto& set = result.domain.node_sets[Domain::key(name_it->second)];
|
|
||||||
set.name = trim(name_it->second);
|
|
||||||
parseSetData(fields, keyword.flags.count("generate") != 0, set.node_ids, result.diagnostics, file_name, line, "nset");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseElementSet(const std::vector<std::string>& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
auto name_it = keyword.parameters.find("elset");
|
|
||||||
if (name_it == keyword.parameters.end()) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELSET-NAME", "*Elset requires ELSET", {file_name, line, "elset"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto& set = result.domain.element_sets[Domain::key(name_it->second)];
|
|
||||||
set.name = trim(name_it->second);
|
|
||||||
parseSetData(fields, keyword.flags.count("generate") != 0, set.element_ids, result.diagnostics, file_name, line, "elset");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseSetData(const std::vector<std::string>& fields, bool generate, std::vector<GlobalId>& output,
|
|
||||||
std::vector<Diagnostic>& diagnostics, const std::string& file_name, LocalIndex line, const std::string& keyword) {
|
|
||||||
if (generate) {
|
|
||||||
const std::size_t field_count = effectiveFieldCount(fields);
|
|
||||||
if (field_count != 3) {
|
|
||||||
diagnostics.push_back({Severity::Error, "FESA-PARSE-GENERATE", "Generated set requires first,last,increment", {file_name, line, keyword}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto first = parseInt64(fields[0]);
|
|
||||||
auto last = parseInt64(fields[1]);
|
|
||||||
auto increment = parseInt64(fields[2]);
|
|
||||||
if (!first || !last || !increment || *increment <= 0) {
|
|
||||||
diagnostics.push_back({Severity::Error, "FESA-PARSE-GENERATE", "Invalid generated set range", {file_name, line, keyword}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (GlobalId value : generatedRange(*first, *last, *increment)) {
|
|
||||||
addUnique(output, value);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::size_t field_count = effectiveFieldCount(fields);
|
|
||||||
for (std::size_t i = 0; i < field_count; ++i) {
|
|
||||||
const std::string& field = fields[i];
|
|
||||||
if (trim(field).empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto value = parseInt64(field);
|
|
||||||
if (!value) {
|
|
||||||
diagnostics.push_back({Severity::Error, "FESA-PARSE-SET-NUMERIC", "Invalid set id", {file_name, line, keyword}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addUnique(output, *value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseElastic(const std::vector<std::string>& fields, const std::string& material_key, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
if (material_key.empty() || result.domain.materials.count(material_key) == 0) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC-MATERIAL", "*Elastic must follow *Material", {file_name, line, "elastic"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::size_t field_count = effectiveFieldCount(fields);
|
|
||||||
if (field_count < 2) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC", "*Elastic requires E,nu", {file_name, line, "elastic"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (field_count > 2) {
|
|
||||||
result.diagnostics.push_back(
|
|
||||||
{Severity::Error, "FESA-PARSE-ELASTIC-UNSUPPORTED", "Only isotropic E,nu elastic data is supported", {file_name, line, "elastic"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto e = parseReal(fields[0]);
|
|
||||||
auto nu = parseReal(fields[1]);
|
|
||||||
if (!e || !nu || *e <= 0.0 || *nu <= -1.0 || *nu >= 0.5) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC-RANGE", "Invalid isotropic elastic constants", {file_name, line, "elastic"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.domain.materials[material_key].elastic_modulus = *e;
|
|
||||||
result.domain.materials[material_key].poisson_ratio = *nu;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseShellSection(const std::vector<std::string>& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
auto elset_it = keyword.parameters.find("elset");
|
|
||||||
auto material_it = keyword.parameters.find("material");
|
|
||||||
if (elset_it == keyword.parameters.end() || material_it == keyword.parameters.end()) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION-PARAM", "*Shell Section requires ELSET and MATERIAL", {file_name, line, "shell section"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::size_t field_count = effectiveFieldCount(fields);
|
|
||||||
if (field_count == 0) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION", "*Shell Section requires thickness", {file_name, line, "shell section"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (field_count > 1) {
|
|
||||||
result.diagnostics.push_back({Severity::Error,
|
|
||||||
"FESA-PARSE-SHELL-SECTION-UNSUPPORTED",
|
|
||||||
"Only homogeneous shell thickness data is supported",
|
|
||||||
{file_name, line, "shell section"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto thickness = parseReal(fields[0]);
|
|
||||||
if (!thickness || *thickness <= 0.0) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-THICKNESS", "Shell thickness must be positive", {file_name, line, "shell section"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.domain.shell_sections.push_back({trim(elset_it->second), trim(material_it->second), *thickness});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseBoundary(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
const std::size_t field_count = effectiveFieldCount(fields);
|
|
||||||
if (field_count < 2) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY", "*Boundary requires target,first_dof", {file_name, line, "boundary"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (field_count > 4) {
|
|
||||||
result.diagnostics.push_back({Severity::Error,
|
|
||||||
"FESA-PARSE-BOUNDARY-UNSUPPORTED",
|
|
||||||
"Only direct zero-valued boundary data is supported",
|
|
||||||
{file_name, line, "boundary"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto first = parseInt64(fields[1]);
|
|
||||||
auto last = field_count >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first;
|
|
||||||
auto magnitude = field_count >= 4 && !fields[3].empty() ? parseReal(fields[3]) : std::optional<Real>(0.0);
|
|
||||||
if (!first || !last || !magnitude || !dofFromAbaqus(static_cast<int>(*first)) || !dofFromAbaqus(static_cast<int>(*last)) || *first > *last) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-DOF", "Invalid boundary DOF range", {file_name, line, "boundary"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (std::fabs(*magnitude) > 0.0) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-NONZERO", "Nonzero prescribed displacement is not supported in Phase 1", {file_name, line, "boundary"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.domain.boundary_conditions.push_back({trim(fields[0]), static_cast<int>(*first), static_cast<int>(*last), *magnitude});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void parseLoad(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
|
||||||
const std::size_t field_count = effectiveFieldCount(fields);
|
|
||||||
if (field_count < 3) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-CLOAD", "*Cload requires target,dof,magnitude", {file_name, line, "cload"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (field_count > 3) {
|
|
||||||
result.diagnostics.push_back({Severity::Error,
|
|
||||||
"FESA-PARSE-CLOAD-UNSUPPORTED",
|
|
||||||
"Only direct concentrated load data is supported",
|
|
||||||
{file_name, line, "cload"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto dof = parseInt64(fields[1]);
|
|
||||||
auto magnitude = parseReal(fields[2]);
|
|
||||||
if (!dof || !magnitude || !dofFromAbaqus(static_cast<int>(*dof))) {
|
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-CLOAD-DOF", "Invalid concentrated load", {file_name, line, "cload"}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.domain.loads.push_back({trim(fields[0]), static_cast<int>(*dof), *magnitude});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
inline SparsePattern buildReducedSparsePattern(const Domain& domain, const DofManager& dofs) {
|
inline SparsePattern buildReducedSparsePattern(const Domain& domain, const DofManager& dofs) {
|
||||||
SparsePattern pattern;
|
SparsePattern pattern;
|
||||||
pattern.equation_count = dofs.freeDofCount();
|
pattern.equation_count = dofs.freeDofCount();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{ "step": 1, "name": "module-scaffold-and-facade", "status": "completed" },
|
{ "step": 1, "name": "module-scaffold-and-facade", "status": "completed" },
|
||||||
{ "step": 2, "name": "core-domain-dof-extraction", "status": "completed" },
|
{ "step": 2, "name": "core-domain-dof-extraction", "status": "completed" },
|
||||||
{ "step": 3, "name": "math-solver-extraction", "status": "completed" },
|
{ "step": 3, "name": "math-solver-extraction", "status": "completed" },
|
||||||
{ "step": 4, "name": "io-parser-extraction", "status": "pending" },
|
{ "step": 4, "name": "io-parser-extraction", "status": "completed" },
|
||||||
{ "step": 5, "name": "results-reference-extraction", "status": "pending" },
|
{ "step": 5, "name": "results-reference-extraction", "status": "pending" },
|
||||||
{ "step": 6, "name": "mitc4-geometry-strain-extraction", "status": "pending" },
|
{ "step": 6, "name": "mitc4-geometry-strain-extraction", "status": "pending" },
|
||||||
{ "step": 7, "name": "mitc4-material-stiffness-extraction", "status": "pending" },
|
{ "step": 7, "name": "mitc4-material-stiffness-extraction", "status": "pending" },
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#include "fesa/IO/IO.hpp"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void check(bool value, const char* message) {
|
||||||
|
if (!value) {
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sourceRoot() {
|
||||||
|
#ifdef FESA_SOURCE_DIR
|
||||||
|
return FESA_SOURCE_DIR;
|
||||||
|
#else
|
||||||
|
return ".";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string readTextFile(const std::string& path) {
|
||||||
|
std::ifstream input(path);
|
||||||
|
if (!input) {
|
||||||
|
throw std::runtime_error("failed to open " + path);
|
||||||
|
}
|
||||||
|
std::ostringstream buffer;
|
||||||
|
buffer << input.rdbuf();
|
||||||
|
return buffer.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const auto continued = fesa::parseKeywordLine("*Element, type=S4,");
|
||||||
|
check(continued.name == "element", "Keyword name normalization changed");
|
||||||
|
check(continued.parameters.at("type") == "S4", "Keyword parameter parsing changed");
|
||||||
|
|
||||||
|
const fesa::AbaqusInputParser parser;
|
||||||
|
|
||||||
|
const auto normalized = parser.parseFile(sourceRoot() + "/references/quad_02_phase1.inp");
|
||||||
|
check(normalized.ok(), "quad_02_phase1 normalized input should remain accepted");
|
||||||
|
check(normalized.domain.nodes.size() == 121, "quad_02_phase1 node count changed");
|
||||||
|
check(normalized.domain.elements.size() == 100, "quad_02_phase1 element count changed");
|
||||||
|
check(normalized.domain.node_sets.at("fixed_boundary").node_ids.size() == 40, "quad_02_phase1 fixed set changed");
|
||||||
|
check(normalized.domain.node_sets.at("load_node").node_ids.size() == 1, "quad_02_phase1 load set changed");
|
||||||
|
check(normalized.domain.element_sets.at("all_elements").element_ids.size() == 100, "quad_02_phase1 element set changed");
|
||||||
|
check(normalized.domain.materials.at("material_1").elastic_modulus == 7.0e10, "quad_02_phase1 material changed");
|
||||||
|
check(normalized.domain.shell_sections.front().thickness == 1.0, "quad_02_phase1 shell section changed");
|
||||||
|
|
||||||
|
const auto original = parser.parseFile(sourceRoot() + "/references/quad_02.inp");
|
||||||
|
check(!original.ok(), "original quad_02.inp should remain unsupported provenance");
|
||||||
|
check(fesa::containsDiagnostic(original.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD"),
|
||||||
|
"original quad_02.inp unsupported keyword diagnostic changed");
|
||||||
|
|
||||||
|
const auto nonzero_bc = parser.parseString("*Node\n1, 0, 0, 0\n*Boundary\n1, 1, 1, 0.5\n", "nonzero_bc.inp");
|
||||||
|
check(!nonzero_bc.ok(), "nonzero prescribed displacement should remain unsupported");
|
||||||
|
check(fesa::containsDiagnostic(nonzero_bc.diagnostics, "FESA-PARSE-BOUNDARY-NONZERO"),
|
||||||
|
"nonzero prescribed displacement diagnostic changed");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user