refactor: extract core domain dof modules
This commit is contained in:
@@ -18,12 +18,18 @@ add_executable(fesa_tests tests/test_main.cpp)
|
|||||||
target_link_libraries(fesa_tests PRIVATE fesa_core)
|
target_link_libraries(fesa_tests PRIVATE fesa_core)
|
||||||
target_compile_definitions(fesa_tests PRIVATE FESA_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
target_compile_definitions(fesa_tests PRIVATE FESA_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
|
add_executable(fesa_core_module_tests tests/test_core_module_includes.cpp)
|
||||||
|
target_link_libraries(fesa_core_module_tests PRIVATE fesa_core)
|
||||||
|
|
||||||
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-)
|
||||||
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)
|
||||||
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)
|
||||||
|
|||||||
@@ -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-02 Core/Util extraction. P1A-00 completed the architecture drift audit and P1A-01 created the module scaffold, source directories, CMake source boundary, and include smoke test without moving production 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-03 Math and solver adapter extraction. P1A-00 completed the architecture drift audit, P1A-01 created the module scaffold, and P1A-02 extracted Core/Util plus Phase 1 Boundary/Load/Property model ownership 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` extracts Core/Util domain, diagnostics, DofManager ownership, and Phase 1 Boundary/Load/Property model ownership; `step3.md` extracts Math and solver adapter boundaries; `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` extracts Math and solver adapter boundaries; `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.
|
||||||
- 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`.
|
||||||
@@ -59,7 +59,7 @@ This phase is an architecture-preserving refactor. It must not change Phase 1 so
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| P1A-00 | completed | planner/evaluator | Audit `fesa.hpp` architecture drift and create a symbol-to-module migration map. | P1R-15 | Complete migration map and validation baseline |
|
| P1A-00 | completed | planner/evaluator | Audit `fesa.hpp` architecture drift and create a symbol-to-module migration map. | P1R-15 | Complete migration map and validation baseline |
|
||||||
| 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 | pending | 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 | pending | generator | Extract Math and solver adapter boundaries. | P1A-02 | Linear solver interface remains adapter-ready; int64 paths unchanged |
|
| P1A-03 | pending | 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 | pending | 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 |
|
||||||
|
|||||||
+43
-1
@@ -13,10 +13,52 @@ 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 and P1A-01 are complete, so the next step is P1A-02 Core/Util 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, P1A-01, and P1A-02 are complete, so the next step is P1A-03 Math and solver adapter 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-02 Core domain DOF extraction completed
|
||||||
|
Author: Codex
|
||||||
|
|
||||||
|
Changed files:
|
||||||
|
- `CMakeLists.txt`
|
||||||
|
- `include/fesa/Boundary/Boundary.hpp`
|
||||||
|
- `include/fesa/Core/AnalysisModel.hpp`
|
||||||
|
- `include/fesa/Core/AnalysisState.hpp`
|
||||||
|
- `include/fesa/Core/Core.hpp`
|
||||||
|
- `include/fesa/Core/Dof.hpp`
|
||||||
|
- `include/fesa/Core/DofManager.hpp`
|
||||||
|
- `include/fesa/Core/Domain.hpp`
|
||||||
|
- `include/fesa/Core/Types.hpp`
|
||||||
|
- `include/fesa/Core/Validation.hpp`
|
||||||
|
- `include/fesa/Load/Load.hpp`
|
||||||
|
- `include/fesa/Property/Property.hpp`
|
||||||
|
- `include/fesa/Util/Diagnostics.hpp`
|
||||||
|
- `include/fesa/Util/String.hpp`
|
||||||
|
- `include/fesa/Util/Util.hpp`
|
||||||
|
- `include/fesa/fesa.hpp`
|
||||||
|
- `tests/test_core_module_includes.cpp`
|
||||||
|
- `phases/1-structure-alignment-refactor/index.json`
|
||||||
|
- `PLAN.md`
|
||||||
|
- `PROGRESS.md`
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
- Extracted numeric aliases, diagnostics, string/parse helpers, DOF mapping, Domain records, validation helpers, `AnalysisModel`, `AnalysisState`, and `DofManager` from the umbrella header into Core and Util module headers.
|
||||||
|
- Moved Phase 1 `BoundaryCondition`, `NodalLoad`, and `ShellSection` model records into their Boundary, Load, and Property module headers.
|
||||||
|
- Added `fesa_core_module_tests`, a direct module include smoke test that does not include `fesa/fesa.hpp` and checks aliases, Domain, diagnostics, DOF mapping, Boundary/Load/Property records, `DofManager`, `AnalysisModel`, and `AnalysisState`.
|
||||||
|
- Preserved public symbol names and namespace `fesa`; `fesa/fesa.hpp` still works as the umbrella facade.
|
||||||
|
- Kept lightweight `Material` as a Domain record under Core for now to avoid a Core-to-Material dependency cycle before IO extraction; later material-law helpers remain part of P1A-07.
|
||||||
|
- Remaining large groups in `fesa.hpp` are Math/solver helpers, IO parser, MITC4 Element helpers, Assembly, Results/reference comparison, and Analysis workflow. `Vec3` arithmetic helpers also remain there until P1A-03 extracts Math.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- First ran `python scripts/validate_workspace.py` after adding the direct module include test; it failed as expected because Core/Boundary/Load/Property/Util headers did not yet expose the required symbols.
|
||||||
|
- After extraction, `python scripts/validate_workspace.py` configured CMake, built `fesa_core`, `fesa_tests`, and `fesa_core_module_tests`, and ran CTest successfully.
|
||||||
|
- CTest result: 2 test executables passed.
|
||||||
|
|
||||||
|
Follow-up:
|
||||||
|
- Continue with P1A-03 Math and solver adapter extraction.
|
||||||
|
- Keep R-014 open until P1A-09 independently accepts the final architecture alignment.
|
||||||
|
|
||||||
### 2026-05-05 - P1A-01 module scaffold and facade completed
|
### 2026-05-05 - P1A-01 module scaffold and facade completed
|
||||||
Author: Codex
|
Author: Codex
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace fesa::module {
|
namespace fesa::module {
|
||||||
|
|
||||||
inline constexpr std::string_view kBoundary = "Boundary";
|
inline constexpr std::string_view kBoundary = "Boundary";
|
||||||
|
|
||||||
} // namespace fesa::module
|
} // namespace fesa::module
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct BoundaryCondition {
|
||||||
|
std::string target;
|
||||||
|
int first_dof = 0;
|
||||||
|
int last_dof = 0;
|
||||||
|
Real magnitude = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Domain.hpp"
|
||||||
|
#include "fesa/Util/Diagnostics.hpp"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct AnalysisModel {
|
||||||
|
StepDefinition step;
|
||||||
|
std::vector<GlobalId> active_element_ids;
|
||||||
|
std::vector<std::size_t> active_boundary_condition_indices;
|
||||||
|
std::vector<std::size_t> active_load_indices;
|
||||||
|
std::vector<std::size_t> active_shell_section_indices;
|
||||||
|
std::vector<std::string> active_material_keys;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
|
||||||
|
bool ok() const {
|
||||||
|
return !hasError(diagnostics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline AnalysisModel buildLinearStaticAnalysisModel(const Domain& domain, LocalIndex step_index = 0) {
|
||||||
|
AnalysisModel model;
|
||||||
|
if (domain.steps.empty()) {
|
||||||
|
model.step = {"Step-1", "linear_static"};
|
||||||
|
} else {
|
||||||
|
if (step_index < 0 || step_index >= static_cast<LocalIndex>(domain.steps.size())) {
|
||||||
|
model.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ANALYSIS-STEP-INDEX",
|
||||||
|
"Requested analysis step index is out of range", "analysis model"));
|
||||||
|
model.step = domain.steps.front();
|
||||||
|
} else {
|
||||||
|
model.step = domain.steps[static_cast<std::size_t>(step_index)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (domain.steps.size() > 1) {
|
||||||
|
model.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ANALYSIS-MULTIPLE-STEPS",
|
||||||
|
"Phase 1 execution supports one active linear static step", "analysis model"));
|
||||||
|
}
|
||||||
|
if (model.step.analysis_type != "linear_static") {
|
||||||
|
model.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ANALYSIS-UNSUPPORTED-STEP",
|
||||||
|
"Only linear static steps are supported in Phase 1", "analysis model"));
|
||||||
|
}
|
||||||
|
for (const auto& [element_id, element] : domain.elements) {
|
||||||
|
(void)element;
|
||||||
|
model.active_element_ids.push_back(element_id);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < domain.boundary_conditions.size(); ++i) {
|
||||||
|
model.active_boundary_condition_indices.push_back(i);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < domain.loads.size(); ++i) {
|
||||||
|
model.active_load_indices.push_back(i);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < domain.shell_sections.size(); ++i) {
|
||||||
|
model.active_shell_section_indices.push_back(i);
|
||||||
|
}
|
||||||
|
for (const auto& [material_key, material] : domain.materials) {
|
||||||
|
(void)material;
|
||||||
|
model.active_material_keys.push_back(material_key);
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct AnalysisState {
|
||||||
|
std::vector<Real> u_full;
|
||||||
|
std::vector<Real> f_external_full;
|
||||||
|
std::vector<Real> f_internal_full;
|
||||||
|
std::vector<Real> reaction_full;
|
||||||
|
bool converged = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/AnalysisModel.hpp"
|
||||||
|
#include "fesa/Core/AnalysisState.hpp"
|
||||||
|
#include "fesa/Core/Dof.hpp"
|
||||||
|
#include "fesa/Core/DofManager.hpp"
|
||||||
|
#include "fesa/Core/Domain.hpp"
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
|
#include "fesa/Core/Validation.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
|
|
||||||
namespace fesa::module {
|
namespace fesa::module {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
enum class Dof : int { UX = 0, UY = 1, UZ = 2, RX = 3, RY = 4, RZ = 5 };
|
||||||
|
|
||||||
|
inline std::array<Dof, 6> allDofs() {
|
||||||
|
return {Dof::UX, Dof::UY, Dof::UZ, Dof::RX, Dof::RY, Dof::RZ};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int dofIndex(Dof dof) {
|
||||||
|
return static_cast<int>(dof);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int abaqusDofNumber(Dof dof) {
|
||||||
|
return dofIndex(dof) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::optional<Dof> dofFromAbaqus(int dof) {
|
||||||
|
if (dof < 1 || dof > 6) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return static_cast<Dof>(dof - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* dofLabel(Dof dof) {
|
||||||
|
switch (dof) {
|
||||||
|
case Dof::UX:
|
||||||
|
return "UX";
|
||||||
|
case Dof::UY:
|
||||||
|
return "UY";
|
||||||
|
case Dof::UZ:
|
||||||
|
return "UZ";
|
||||||
|
case Dof::RX:
|
||||||
|
return "RX";
|
||||||
|
case Dof::RY:
|
||||||
|
return "RY";
|
||||||
|
case Dof::RZ:
|
||||||
|
return "RZ";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<std::string> displacementComponentLabels() {
|
||||||
|
return {"UX", "UY", "UZ", "RX", "RY", "RZ"};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<std::string> reactionComponentLabels() {
|
||||||
|
return {"RFX", "RFY", "RFZ", "RMX", "RMY", "RMZ"};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Dof.hpp"
|
||||||
|
#include "fesa/Core/Domain.hpp"
|
||||||
|
#include "fesa/Core/Validation.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct DofAddress {
|
||||||
|
GlobalId node_id = 0;
|
||||||
|
Dof dof = Dof::UX;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DofManager {
|
||||||
|
public:
|
||||||
|
explicit DofManager(const Domain& domain) {
|
||||||
|
for (const auto& [node_id, node] : domain.nodes) {
|
||||||
|
(void)node;
|
||||||
|
node_ids_.push_back(node_id);
|
||||||
|
for (Dof dof : allDofs()) {
|
||||||
|
const LocalIndex full_index = static_cast<LocalIndex>(all_dofs_.size());
|
||||||
|
const auto key = std::make_pair(node_id, dofIndex(dof));
|
||||||
|
all_dofs_.push_back(key);
|
||||||
|
full_index_by_key_[key] = full_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
||||||
|
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (GlobalId node_id : resolveNodeTarget(domain, boundary.target)) {
|
||||||
|
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
|
||||||
|
constrained_.insert(std::make_pair(node_id, dof - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& key : all_dofs_) {
|
||||||
|
const LocalIndex full_index = full_index_by_key_.at(key);
|
||||||
|
if (constrained_.count(key) == 0) {
|
||||||
|
equation_by_key_[key] = static_cast<EquationId>(free_full_indices_.size());
|
||||||
|
free_full_indices_.push_back(full_index);
|
||||||
|
} else {
|
||||||
|
equation_by_key_[key] = -1;
|
||||||
|
constrained_full_indices_.push_back(full_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalIndex fullDofCount() const {
|
||||||
|
return static_cast<LocalIndex>(all_dofs_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalIndex freeDofCount() const {
|
||||||
|
return static_cast<LocalIndex>(free_full_indices_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalIndex constrainedDofCount() const {
|
||||||
|
return static_cast<LocalIndex>(constrained_full_indices_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<GlobalId>& nodeIds() const {
|
||||||
|
return node_ids_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<LocalIndex>& freeFullIndices() const {
|
||||||
|
return free_full_indices_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<LocalIndex>& constrainedFullIndices() const {
|
||||||
|
return constrained_full_indices_;
|
||||||
|
}
|
||||||
|
|
||||||
|
DofAddress fullDof(LocalIndex full_index) const {
|
||||||
|
const auto& key = all_dofs_.at(static_cast<std::size_t>(full_index));
|
||||||
|
return {key.first, static_cast<Dof>(key.second)};
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalIndex fullIndex(GlobalId node_id, Dof dof) const {
|
||||||
|
return full_index_by_key_.at(std::make_pair(node_id, dofIndex(dof)));
|
||||||
|
}
|
||||||
|
|
||||||
|
EquationId equation(GlobalId node_id, Dof dof) const {
|
||||||
|
return equation_by_key_.at(std::make_pair(node_id, dofIndex(dof)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isConstrained(GlobalId node_id, Dof dof) const {
|
||||||
|
return constrained_.count(std::make_pair(node_id, dofIndex(dof))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Real> reduceFullVector(const std::vector<Real>& full) const {
|
||||||
|
std::vector<Real> reduced;
|
||||||
|
reduced.reserve(free_full_indices_.size());
|
||||||
|
for (LocalIndex full_index : free_full_indices_) {
|
||||||
|
reduced.push_back(full.at(static_cast<std::size_t>(full_index)));
|
||||||
|
}
|
||||||
|
return reduced;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Real> reconstructFullVector(const std::vector<Real>& reduced) const {
|
||||||
|
std::vector<Real> full(static_cast<std::size_t>(fullDofCount()), 0.0);
|
||||||
|
for (std::size_t i = 0; i < free_full_indices_.size(); ++i) {
|
||||||
|
full[static_cast<std::size_t>(free_full_indices_[i])] = reduced.at(i);
|
||||||
|
}
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<LocalIndex, 24> elementFullDofIndices(const Element& element) const {
|
||||||
|
std::array<LocalIndex, 24> indices{};
|
||||||
|
for (LocalIndex node = 0; node < 4; ++node) {
|
||||||
|
for (Dof dof : allDofs()) {
|
||||||
|
const LocalIndex local = 6 * node + dofIndex(dof);
|
||||||
|
indices[static_cast<std::size_t>(local)] = fullIndex(element.node_ids[static_cast<std::size_t>(node)], dof);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<EquationId, 24> elementEquationIds(const Element& element) const {
|
||||||
|
std::array<EquationId, 24> equations{};
|
||||||
|
for (LocalIndex node = 0; node < 4; ++node) {
|
||||||
|
for (Dof dof : allDofs()) {
|
||||||
|
const LocalIndex local = 6 * node + dofIndex(dof);
|
||||||
|
equations[static_cast<std::size_t>(local)] = equation(element.node_ids[static_cast<std::size_t>(node)], dof);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return equations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<GlobalId> node_ids_;
|
||||||
|
std::vector<std::pair<GlobalId, int>> all_dofs_;
|
||||||
|
std::set<std::pair<GlobalId, int>> constrained_;
|
||||||
|
std::map<std::pair<GlobalId, int>, LocalIndex> full_index_by_key_;
|
||||||
|
std::map<std::pair<GlobalId, int>, EquationId> equation_by_key_;
|
||||||
|
std::vector<LocalIndex> free_full_indices_;
|
||||||
|
std::vector<LocalIndex> constrained_full_indices_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Boundary/Boundary.hpp"
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
|
#include "fesa/Load/Load.hpp"
|
||||||
|
#include "fesa/Property/Property.hpp"
|
||||||
|
#include "fesa/Util/String.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct Vec3 {
|
||||||
|
Real x = 0.0;
|
||||||
|
Real y = 0.0;
|
||||||
|
Real z = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
GlobalId id = 0;
|
||||||
|
Vec3 coordinates;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ElementType { MITC4 };
|
||||||
|
|
||||||
|
inline std::string elementTypeLabel(ElementType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ElementType::MITC4:
|
||||||
|
return "MITC4";
|
||||||
|
}
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Element {
|
||||||
|
GlobalId id = 0;
|
||||||
|
ElementType type = ElementType::MITC4;
|
||||||
|
std::array<GlobalId, 4> node_ids{};
|
||||||
|
std::string source_elset;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NodeSet {
|
||||||
|
std::string name;
|
||||||
|
std::vector<GlobalId> node_ids;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ElementSet {
|
||||||
|
std::string name;
|
||||||
|
std::vector<GlobalId> element_ids;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Material {
|
||||||
|
std::string name;
|
||||||
|
Real elastic_modulus = 0.0;
|
||||||
|
Real poisson_ratio = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StepDefinition {
|
||||||
|
std::string name = "Step-1";
|
||||||
|
std::string analysis_type = "linear_static";
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Domain {
|
||||||
|
std::map<GlobalId, Node> nodes;
|
||||||
|
std::map<GlobalId, Element> elements;
|
||||||
|
std::map<std::string, NodeSet> node_sets;
|
||||||
|
std::map<std::string, ElementSet> element_sets;
|
||||||
|
std::map<std::string, Material> materials;
|
||||||
|
std::vector<ShellSection> shell_sections;
|
||||||
|
std::vector<BoundaryCondition> boundary_conditions;
|
||||||
|
std::vector<NodalLoad> loads;
|
||||||
|
std::vector<StepDefinition> steps;
|
||||||
|
|
||||||
|
static std::string key(const std::string& label) {
|
||||||
|
return lower(trim(label));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
using Real = double;
|
||||||
|
using GlobalId = std::int64_t;
|
||||||
|
using LocalIndex = std::int64_t;
|
||||||
|
using EquationId = std::int64_t;
|
||||||
|
using SparseIndex = std::int64_t;
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Dof.hpp"
|
||||||
|
#include "fesa/Core/Domain.hpp"
|
||||||
|
#include "fesa/Util/Diagnostics.hpp"
|
||||||
|
#include "fesa/Util/String.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
inline std::optional<GlobalId> numericTarget(const std::string& target) {
|
||||||
|
return parseInt64(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<GlobalId> resolveNodeTarget(const Domain& domain, const std::string& target, std::vector<Diagnostic>* diagnostics = nullptr,
|
||||||
|
const std::string& diagnostic_keyword = "node target") {
|
||||||
|
if (auto node_id = numericTarget(target)) {
|
||||||
|
if (domain.nodes.count(*node_id) == 0) {
|
||||||
|
if (diagnostics != nullptr) {
|
||||||
|
diagnostics->push_back(
|
||||||
|
makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NODE", "Missing node target: " + target, diagnostic_keyword));
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {*node_id};
|
||||||
|
}
|
||||||
|
auto set_it = domain.node_sets.find(Domain::key(target));
|
||||||
|
if (set_it == domain.node_sets.end()) {
|
||||||
|
if (diagnostics != nullptr) {
|
||||||
|
diagnostics->push_back(
|
||||||
|
makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NSET", "Missing node set: " + target, diagnostic_keyword));
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return set_it->second.node_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const ShellSection* shellSectionForElement(const Domain& domain, GlobalId element_id) {
|
||||||
|
for (const ShellSection& section : domain.shell_sections) {
|
||||||
|
auto set_it = domain.element_sets.find(Domain::key(section.element_set));
|
||||||
|
if (set_it == domain.element_sets.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::find(set_it->second.element_ids.begin(), set_it->second.element_ids.end(), element_id) != set_it->second.element_ids.end()) {
|
||||||
|
return §ion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string dofNameOrNumber(int abaqus_dof) {
|
||||||
|
auto dof = dofFromAbaqus(abaqus_dof);
|
||||||
|
if (dof) {
|
||||||
|
return dofLabel(*dof);
|
||||||
|
}
|
||||||
|
return "DOF " + std::to_string(abaqus_dof);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool validAbaqusDofRange(int first, int last) {
|
||||||
|
return dofFromAbaqus(first).has_value() && dofFromAbaqus(last).has_value() && first <= last;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
if (domain.elements.empty()) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-ACTIVE-ELEMENTS",
|
||||||
|
"No active elements exist in the current model", "analysis model"));
|
||||||
|
}
|
||||||
|
if (domain.boundary_conditions.empty()) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-BOUNDARY", "No boundary constraints are defined", "boundary"));
|
||||||
|
}
|
||||||
|
for (const auto& [set_key, set] : domain.node_sets) {
|
||||||
|
(void)set_key;
|
||||||
|
for (GlobalId node_id : set.node_ids) {
|
||||||
|
if (domain.nodes.count(node_id) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NSET-MISSING-NODE",
|
||||||
|
"Node set " + set.name + " references missing node " + std::to_string(node_id),
|
||||||
|
"nset"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& [set_key, set] : domain.element_sets) {
|
||||||
|
(void)set_key;
|
||||||
|
for (GlobalId element_id : set.element_ids) {
|
||||||
|
if (domain.elements.count(element_id) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELSET-MISSING-ELEMENT",
|
||||||
|
"Element set " + set.name + " references missing element " + std::to_string(element_id),
|
||||||
|
"elset"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& [id, element] : domain.elements) {
|
||||||
|
for (GlobalId node_id : element.node_ids) {
|
||||||
|
if (domain.nodes.count(node_id) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELEMENT-MISSING-NODE",
|
||||||
|
"Element " + std::to_string(id) + " references missing node " + std::to_string(node_id),
|
||||||
|
"element"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ShellSection* section = shellSectionForElement(domain, id);
|
||||||
|
if (section == nullptr) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-PROPERTY",
|
||||||
|
"Element " + std::to_string(id) + " has no assigned shell section", "element"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ShellSection& section : domain.shell_sections) {
|
||||||
|
if (section.thickness <= 0.0 || !std::isfinite(section.thickness)) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NONPOSITIVE-THICKNESS",
|
||||||
|
"Shell section for element set " + section.element_set + " has non-positive thickness",
|
||||||
|
"shell section"));
|
||||||
|
}
|
||||||
|
if (domain.element_sets.count(Domain::key(section.element_set)) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-ELSET",
|
||||||
|
"Shell section references missing element set: " + section.element_set, "shell section"));
|
||||||
|
}
|
||||||
|
auto material_it = domain.materials.find(Domain::key(section.material));
|
||||||
|
if (material_it == domain.materials.end()) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-MATERIAL",
|
||||||
|
"Shell section references missing material: " + section.material, "shell section"));
|
||||||
|
} else if (material_it->second.elastic_modulus <= 0.0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-INCOMPLETE-MATERIAL",
|
||||||
|
"Material has no valid elastic constants: " + section.material, "material"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
||||||
|
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-BOUNDARY-DOF",
|
||||||
|
"Boundary target " + boundary.target + " has invalid DOF range " +
|
||||||
|
dofNameOrNumber(boundary.first_dof) + " to " + dofNameOrNumber(boundary.last_dof),
|
||||||
|
"boundary"));
|
||||||
|
}
|
||||||
|
(void)resolveNodeTarget(domain, boundary.target, &diagnostics, "boundary");
|
||||||
|
}
|
||||||
|
for (const NodalLoad& load : domain.loads) {
|
||||||
|
if (!dofFromAbaqus(load.dof)) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-CLOAD-DOF",
|
||||||
|
"Load target " + load.target + " has invalid " + dofNameOrNumber(load.dof), "cload"));
|
||||||
|
}
|
||||||
|
(void)resolveNodeTarget(domain, load.target, &diagnostics, "cload");
|
||||||
|
}
|
||||||
|
const bool any_nonzero_load = std::any_of(domain.loads.begin(), domain.loads.end(), [](const NodalLoad& load) {
|
||||||
|
return std::fabs(load.magnitude) > 0.0;
|
||||||
|
});
|
||||||
|
if (!any_nonzero_load) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-NONZERO-LOAD", "No nonzero load is defined", "cload"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::pair<GlobalId, int>> constrained_dofs;
|
||||||
|
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
||||||
|
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (GlobalId node_id : resolveNodeTarget(domain, boundary.target)) {
|
||||||
|
if (domain.nodes.count(node_id) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
|
||||||
|
constrained_dofs.insert(std::make_pair(node_id, dof - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<GlobalId> active_connectivity_nodes;
|
||||||
|
for (const auto& [element_id, element] : domain.elements) {
|
||||||
|
(void)element_id;
|
||||||
|
for (GlobalId node_id : element.node_ids) {
|
||||||
|
if (domain.nodes.count(node_id) != 0) {
|
||||||
|
active_connectivity_nodes.insert(node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalIndex free_dof_count = 0;
|
||||||
|
LocalIndex weak_drilling_count = 0;
|
||||||
|
GlobalId weak_drilling_example = 0;
|
||||||
|
for (const auto& [node_id, node] : domain.nodes) {
|
||||||
|
(void)node;
|
||||||
|
for (Dof dof : allDofs()) {
|
||||||
|
const auto key = std::make_pair(node_id, dofIndex(dof));
|
||||||
|
if (constrained_dofs.count(key) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++free_dof_count;
|
||||||
|
if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-DOF-UNTOUCHED",
|
||||||
|
"Node " + std::to_string(node_id) + " DOF " + dofLabel(dof) +
|
||||||
|
" is free but is not touched by active element connectivity",
|
||||||
|
"dof"));
|
||||||
|
}
|
||||||
|
if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) != 0 && dof == Dof::RZ) {
|
||||||
|
if (weak_drilling_count == 0) {
|
||||||
|
weak_drilling_example = node_id;
|
||||||
|
}
|
||||||
|
++weak_drilling_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!domain.nodes.empty() && free_dof_count == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS",
|
||||||
|
"No free DOFs exist after applying boundary constraints", "dof"));
|
||||||
|
}
|
||||||
|
if (weak_drilling_count > 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-WEAK-DRILLING-DOF",
|
||||||
|
"Node " + std::to_string(weak_drilling_example) +
|
||||||
|
" DOF RZ is free; drilling rotation is weakly stabilized in Phase 1 (" +
|
||||||
|
std::to_string(weak_drilling_count) + " free drilling DOF(s))",
|
||||||
|
"dof"));
|
||||||
|
}
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace fesa::module {
|
namespace fesa::module {
|
||||||
|
|
||||||
inline constexpr std::string_view kLoad = "Load";
|
inline constexpr std::string_view kLoad = "Load";
|
||||||
|
|
||||||
} // namespace fesa::module
|
} // namespace fesa::module
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct NodalLoad {
|
||||||
|
std::string target;
|
||||||
|
int dof = 0;
|
||||||
|
Real magnitude = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace fesa::module {
|
namespace fesa::module {
|
||||||
|
|
||||||
inline constexpr std::string_view kProperty = "Property";
|
inline constexpr std::string_view kProperty = "Property";
|
||||||
|
|
||||||
} // namespace fesa::module
|
} // namespace fesa::module
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct ShellSection {
|
||||||
|
std::string element_set;
|
||||||
|
std::string material;
|
||||||
|
Real thickness = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
enum class Severity { Info, Warning, Error };
|
||||||
|
|
||||||
|
struct SourceLocation {
|
||||||
|
std::string file;
|
||||||
|
LocalIndex line = 0;
|
||||||
|
std::string keyword;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Diagnostic {
|
||||||
|
Severity severity = Severity::Error;
|
||||||
|
std::string code;
|
||||||
|
std::string message;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool hasError(const std::vector<Diagnostic>& diagnostics) {
|
||||||
|
return std::any_of(diagnostics.begin(), diagnostics.end(), [](const Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.severity == Severity::Error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool containsDiagnostic(const std::vector<Diagnostic>& diagnostics, const std::string& code) {
|
||||||
|
return std::any_of(diagnostics.begin(), diagnostics.end(), [&](const Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.code == code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Diagnostic makeDiagnostic(Severity severity, std::string code, std::string message, std::string keyword,
|
||||||
|
std::string file = "<domain>", LocalIndex line = 0) {
|
||||||
|
return {severity, std::move(code), std::move(message), {std::move(file), line, std::move(keyword)}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Core/Types.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
inline std::string trim(std::string text) {
|
||||||
|
auto is_space = [](unsigned char c) { return std::isspace(c) != 0; };
|
||||||
|
text.erase(text.begin(), std::find_if(text.begin(), text.end(), [&](unsigned char c) { return !is_space(c); }));
|
||||||
|
text.erase(std::find_if(text.rbegin(), text.rend(), [&](unsigned char c) { return !is_space(c); }).base(), text.end());
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string lower(std::string text) {
|
||||||
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<std::string> splitCsv(const std::string& line) {
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
std::string field;
|
||||||
|
std::istringstream stream(line);
|
||||||
|
while (std::getline(stream, field, ',')) {
|
||||||
|
fields.push_back(trim(field));
|
||||||
|
}
|
||||||
|
if (!line.empty() && line.back() == ',') {
|
||||||
|
fields.emplace_back();
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::optional<Real> parseReal(std::string token) {
|
||||||
|
token = trim(token);
|
||||||
|
if (token.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
std::replace(token.begin(), token.end(), 'D', 'E');
|
||||||
|
std::replace(token.begin(), token.end(), 'd', 'e');
|
||||||
|
try {
|
||||||
|
std::size_t used = 0;
|
||||||
|
Real value = std::stod(token, &used);
|
||||||
|
if (used != token.size()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} catch (...) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::optional<GlobalId> parseInt64(const std::string& token) {
|
||||||
|
std::string value_text = trim(token);
|
||||||
|
if (value_text.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
std::size_t used = 0;
|
||||||
|
long long value = std::stoll(value_text, &used);
|
||||||
|
if (used != value_text.size()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return static_cast<GlobalId>(value);
|
||||||
|
} catch (...) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void addUnique(std::vector<GlobalId>& values, GlobalId value) {
|
||||||
|
if (std::find(values.begin(), values.end(), value) == values.end()) {
|
||||||
|
values.push_back(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<GlobalId> generatedRange(GlobalId first, GlobalId last, GlobalId increment) {
|
||||||
|
std::vector<GlobalId> values;
|
||||||
|
if (increment <= 0) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
for (GlobalId value = first; value <= last; value += increment) {
|
||||||
|
values.push_back(value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Util/Diagnostics.hpp"
|
||||||
|
#include "fesa/Util/String.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
|
|
||||||
namespace fesa::module {
|
namespace fesa::module {
|
||||||
|
|||||||
+5
-643
@@ -1,6 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/Boundary/Boundary.hpp"
|
||||||
|
#include "fesa/Core/Core.hpp"
|
||||||
|
#include "fesa/Load/Load.hpp"
|
||||||
#include "fesa/ModuleInfo.hpp"
|
#include "fesa/ModuleInfo.hpp"
|
||||||
|
#include "fesa/Property/Property.hpp"
|
||||||
|
#include "fesa/Util/Util.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -22,160 +27,6 @@
|
|||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
using Real = double;
|
|
||||||
using GlobalId = std::int64_t;
|
|
||||||
using LocalIndex = std::int64_t;
|
|
||||||
using EquationId = std::int64_t;
|
|
||||||
using SparseIndex = std::int64_t;
|
|
||||||
|
|
||||||
enum class Severity { Info, Warning, Error };
|
|
||||||
|
|
||||||
struct SourceLocation {
|
|
||||||
std::string file;
|
|
||||||
LocalIndex line = 0;
|
|
||||||
std::string keyword;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Diagnostic {
|
|
||||||
Severity severity = Severity::Error;
|
|
||||||
std::string code;
|
|
||||||
std::string message;
|
|
||||||
SourceLocation source;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline bool hasError(const std::vector<Diagnostic>& diagnostics) {
|
|
||||||
return std::any_of(diagnostics.begin(), diagnostics.end(), [](const Diagnostic& diagnostic) {
|
|
||||||
return diagnostic.severity == Severity::Error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool containsDiagnostic(const std::vector<Diagnostic>& diagnostics, const std::string& code) {
|
|
||||||
return std::any_of(diagnostics.begin(), diagnostics.end(), [&](const Diagnostic& diagnostic) {
|
|
||||||
return diagnostic.code == code;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Diagnostic makeDiagnostic(Severity severity, std::string code, std::string message, std::string keyword,
|
|
||||||
std::string file = "<domain>", LocalIndex line = 0) {
|
|
||||||
return {severity, std::move(code), std::move(message), {std::move(file), line, std::move(keyword)}};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string trim(std::string text) {
|
|
||||||
auto is_space = [](unsigned char c) { return std::isspace(c) != 0; };
|
|
||||||
text.erase(text.begin(), std::find_if(text.begin(), text.end(), [&](unsigned char c) { return !is_space(c); }));
|
|
||||||
text.erase(std::find_if(text.rbegin(), text.rend(), [&](unsigned char c) { return !is_space(c); }).base(), text.end());
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string lower(std::string text) {
|
|
||||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
|
|
||||||
return static_cast<char>(std::tolower(c));
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<std::string> splitCsv(const std::string& line) {
|
|
||||||
std::vector<std::string> fields;
|
|
||||||
std::string field;
|
|
||||||
std::istringstream stream(line);
|
|
||||||
while (std::getline(stream, field, ',')) {
|
|
||||||
fields.push_back(trim(field));
|
|
||||||
}
|
|
||||||
if (!line.empty() && line.back() == ',') {
|
|
||||||
fields.emplace_back();
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::optional<Real> parseReal(std::string token) {
|
|
||||||
token = trim(token);
|
|
||||||
if (token.empty()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
std::replace(token.begin(), token.end(), 'D', 'E');
|
|
||||||
std::replace(token.begin(), token.end(), 'd', 'e');
|
|
||||||
try {
|
|
||||||
std::size_t used = 0;
|
|
||||||
Real value = std::stod(token, &used);
|
|
||||||
if (used != token.size()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} catch (...) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::optional<GlobalId> parseInt64(const std::string& token) {
|
|
||||||
std::string value_text = trim(token);
|
|
||||||
if (value_text.empty()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
std::size_t used = 0;
|
|
||||||
long long value = std::stoll(value_text, &used);
|
|
||||||
if (used != value_text.size()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return static_cast<GlobalId>(value);
|
|
||||||
} catch (...) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Dof : int { UX = 0, UY = 1, UZ = 2, RX = 3, RY = 4, RZ = 5 };
|
|
||||||
|
|
||||||
inline std::array<Dof, 6> allDofs() {
|
|
||||||
return {Dof::UX, Dof::UY, Dof::UZ, Dof::RX, Dof::RY, Dof::RZ};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int dofIndex(Dof dof) {
|
|
||||||
return static_cast<int>(dof);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int abaqusDofNumber(Dof dof) {
|
|
||||||
return dofIndex(dof) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::optional<Dof> dofFromAbaqus(int dof) {
|
|
||||||
if (dof < 1 || dof > 6) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return static_cast<Dof>(dof - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const char* dofLabel(Dof dof) {
|
|
||||||
switch (dof) {
|
|
||||||
case Dof::UX:
|
|
||||||
return "UX";
|
|
||||||
case Dof::UY:
|
|
||||||
return "UY";
|
|
||||||
case Dof::UZ:
|
|
||||||
return "UZ";
|
|
||||||
case Dof::RX:
|
|
||||||
return "RX";
|
|
||||||
case Dof::RY:
|
|
||||||
return "RY";
|
|
||||||
case Dof::RZ:
|
|
||||||
return "RZ";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<std::string> displacementComponentLabels() {
|
|
||||||
return {"UX", "UY", "UZ", "RX", "RY", "RZ"};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<std::string> reactionComponentLabels() {
|
|
||||||
return {"RFX", "RFY", "RFZ", "RMX", "RMY", "RMZ"};
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Vec3 {
|
|
||||||
Real x = 0.0;
|
|
||||||
Real y = 0.0;
|
|
||||||
Real z = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline Vec3 operator+(const Vec3& a, const Vec3& b) {
|
inline Vec3 operator+(const Vec3& a, const Vec3& b) {
|
||||||
return {a.x + b.x, a.y + b.y, a.z + b.z};
|
return {a.x + b.x, a.y + b.y, a.z + b.z};
|
||||||
}
|
}
|
||||||
@@ -224,101 +75,6 @@ inline Vec3 normalized(const Vec3& value) {
|
|||||||
return (1.0 / length) * value;
|
return (1.0 / length) * value;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Node {
|
|
||||||
GlobalId id = 0;
|
|
||||||
Vec3 coordinates;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class ElementType { MITC4 };
|
|
||||||
|
|
||||||
inline std::string elementTypeLabel(ElementType type) {
|
|
||||||
switch (type) {
|
|
||||||
case ElementType::MITC4:
|
|
||||||
return "MITC4";
|
|
||||||
}
|
|
||||||
return "UNKNOWN";
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Element {
|
|
||||||
GlobalId id = 0;
|
|
||||||
ElementType type = ElementType::MITC4;
|
|
||||||
std::array<GlobalId, 4> node_ids{};
|
|
||||||
std::string source_elset;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NodeSet {
|
|
||||||
std::string name;
|
|
||||||
std::vector<GlobalId> node_ids;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ElementSet {
|
|
||||||
std::string name;
|
|
||||||
std::vector<GlobalId> element_ids;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Material {
|
|
||||||
std::string name;
|
|
||||||
Real elastic_modulus = 0.0;
|
|
||||||
Real poisson_ratio = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ShellSection {
|
|
||||||
std::string element_set;
|
|
||||||
std::string material;
|
|
||||||
Real thickness = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BoundaryCondition {
|
|
||||||
std::string target;
|
|
||||||
int first_dof = 0;
|
|
||||||
int last_dof = 0;
|
|
||||||
Real magnitude = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NodalLoad {
|
|
||||||
std::string target;
|
|
||||||
int dof = 0;
|
|
||||||
Real magnitude = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct StepDefinition {
|
|
||||||
std::string name = "Step-1";
|
|
||||||
std::string analysis_type = "linear_static";
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Domain {
|
|
||||||
std::map<GlobalId, Node> nodes;
|
|
||||||
std::map<GlobalId, Element> elements;
|
|
||||||
std::map<std::string, NodeSet> node_sets;
|
|
||||||
std::map<std::string, ElementSet> element_sets;
|
|
||||||
std::map<std::string, Material> materials;
|
|
||||||
std::vector<ShellSection> shell_sections;
|
|
||||||
std::vector<BoundaryCondition> boundary_conditions;
|
|
||||||
std::vector<NodalLoad> loads;
|
|
||||||
std::vector<StepDefinition> steps;
|
|
||||||
|
|
||||||
static std::string key(const std::string& label) {
|
|
||||||
return lower(trim(label));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
inline void addUnique(std::vector<GlobalId>& values, GlobalId value) {
|
|
||||||
if (std::find(values.begin(), values.end(), value) == values.end()) {
|
|
||||||
values.push_back(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<GlobalId> generatedRange(GlobalId first, GlobalId last, GlobalId increment) {
|
|
||||||
std::vector<GlobalId> values;
|
|
||||||
if (increment <= 0) {
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
for (GlobalId value = first; value <= last; value += increment) {
|
|
||||||
values.push_back(value);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct KeywordLine {
|
struct KeywordLine {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::map<std::string, std::string> parameters;
|
std::map<std::string, std::string> parameters;
|
||||||
@@ -762,392 +518,6 @@ class AbaqusInputParser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
inline std::optional<GlobalId> numericTarget(const std::string& target) {
|
|
||||||
return parseInt64(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<GlobalId> resolveNodeTarget(const Domain& domain, const std::string& target, std::vector<Diagnostic>* diagnostics = nullptr,
|
|
||||||
const std::string& diagnostic_keyword = "node target") {
|
|
||||||
if (auto node_id = numericTarget(target)) {
|
|
||||||
if (domain.nodes.count(*node_id) == 0) {
|
|
||||||
if (diagnostics != nullptr) {
|
|
||||||
diagnostics->push_back(
|
|
||||||
makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NODE", "Missing node target: " + target, diagnostic_keyword));
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {*node_id};
|
|
||||||
}
|
|
||||||
auto set_it = domain.node_sets.find(Domain::key(target));
|
|
||||||
if (set_it == domain.node_sets.end()) {
|
|
||||||
if (diagnostics != nullptr) {
|
|
||||||
diagnostics->push_back(
|
|
||||||
makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NSET", "Missing node set: " + target, diagnostic_keyword));
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return set_it->second.node_ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const ShellSection* shellSectionForElement(const Domain& domain, GlobalId element_id) {
|
|
||||||
for (const ShellSection& section : domain.shell_sections) {
|
|
||||||
auto set_it = domain.element_sets.find(Domain::key(section.element_set));
|
|
||||||
if (set_it == domain.element_sets.end()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (std::find(set_it->second.element_ids.begin(), set_it->second.element_ids.end(), element_id) != set_it->second.element_ids.end()) {
|
|
||||||
return §ion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string dofNameOrNumber(int abaqus_dof) {
|
|
||||||
auto dof = dofFromAbaqus(abaqus_dof);
|
|
||||||
if (dof) {
|
|
||||||
return dofLabel(*dof);
|
|
||||||
}
|
|
||||||
return "DOF " + std::to_string(abaqus_dof);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool validAbaqusDofRange(int first, int last) {
|
|
||||||
return dofFromAbaqus(first).has_value() && dofFromAbaqus(last).has_value() && first <= last;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
|
||||||
std::vector<Diagnostic> diagnostics;
|
|
||||||
if (domain.elements.empty()) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-ACTIVE-ELEMENTS",
|
|
||||||
"No active elements exist in the current model", "analysis model"));
|
|
||||||
}
|
|
||||||
if (domain.boundary_conditions.empty()) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-BOUNDARY", "No boundary constraints are defined", "boundary"));
|
|
||||||
}
|
|
||||||
for (const auto& [set_key, set] : domain.node_sets) {
|
|
||||||
(void)set_key;
|
|
||||||
for (GlobalId node_id : set.node_ids) {
|
|
||||||
if (domain.nodes.count(node_id) == 0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NSET-MISSING-NODE",
|
|
||||||
"Node set " + set.name + " references missing node " + std::to_string(node_id),
|
|
||||||
"nset"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto& [set_key, set] : domain.element_sets) {
|
|
||||||
(void)set_key;
|
|
||||||
for (GlobalId element_id : set.element_ids) {
|
|
||||||
if (domain.elements.count(element_id) == 0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELSET-MISSING-ELEMENT",
|
|
||||||
"Element set " + set.name + " references missing element " + std::to_string(element_id),
|
|
||||||
"elset"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto& [id, element] : domain.elements) {
|
|
||||||
for (GlobalId node_id : element.node_ids) {
|
|
||||||
if (domain.nodes.count(node_id) == 0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELEMENT-MISSING-NODE",
|
|
||||||
"Element " + std::to_string(id) + " references missing node " + std::to_string(node_id),
|
|
||||||
"element"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const ShellSection* section = shellSectionForElement(domain, id);
|
|
||||||
if (section == nullptr) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-PROPERTY",
|
|
||||||
"Element " + std::to_string(id) + " has no assigned shell section", "element"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const ShellSection& section : domain.shell_sections) {
|
|
||||||
if (section.thickness <= 0.0 || !std::isfinite(section.thickness)) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NONPOSITIVE-THICKNESS",
|
|
||||||
"Shell section for element set " + section.element_set + " has non-positive thickness",
|
|
||||||
"shell section"));
|
|
||||||
}
|
|
||||||
if (domain.element_sets.count(Domain::key(section.element_set)) == 0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-ELSET",
|
|
||||||
"Shell section references missing element set: " + section.element_set, "shell section"));
|
|
||||||
}
|
|
||||||
auto material_it = domain.materials.find(Domain::key(section.material));
|
|
||||||
if (material_it == domain.materials.end()) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-MATERIAL",
|
|
||||||
"Shell section references missing material: " + section.material, "shell section"));
|
|
||||||
} else if (material_it->second.elastic_modulus <= 0.0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-INCOMPLETE-MATERIAL",
|
|
||||||
"Material has no valid elastic constants: " + section.material, "material"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
|
||||||
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-BOUNDARY-DOF",
|
|
||||||
"Boundary target " + boundary.target + " has invalid DOF range " +
|
|
||||||
dofNameOrNumber(boundary.first_dof) + " to " + dofNameOrNumber(boundary.last_dof),
|
|
||||||
"boundary"));
|
|
||||||
}
|
|
||||||
(void)resolveNodeTarget(domain, boundary.target, &diagnostics, "boundary");
|
|
||||||
}
|
|
||||||
for (const NodalLoad& load : domain.loads) {
|
|
||||||
if (!dofFromAbaqus(load.dof)) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-CLOAD-DOF",
|
|
||||||
"Load target " + load.target + " has invalid " + dofNameOrNumber(load.dof), "cload"));
|
|
||||||
}
|
|
||||||
(void)resolveNodeTarget(domain, load.target, &diagnostics, "cload");
|
|
||||||
}
|
|
||||||
const bool any_nonzero_load = std::any_of(domain.loads.begin(), domain.loads.end(), [](const NodalLoad& load) {
|
|
||||||
return std::fabs(load.magnitude) > 0.0;
|
|
||||||
});
|
|
||||||
if (!any_nonzero_load) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-NONZERO-LOAD", "No nonzero load is defined", "cload"));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::pair<GlobalId, int>> constrained_dofs;
|
|
||||||
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
|
||||||
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (GlobalId node_id : resolveNodeTarget(domain, boundary.target)) {
|
|
||||||
if (domain.nodes.count(node_id) == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
|
|
||||||
constrained_dofs.insert(std::make_pair(node_id, dof - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<GlobalId> active_connectivity_nodes;
|
|
||||||
for (const auto& [element_id, element] : domain.elements) {
|
|
||||||
(void)element_id;
|
|
||||||
for (GlobalId node_id : element.node_ids) {
|
|
||||||
if (domain.nodes.count(node_id) != 0) {
|
|
||||||
active_connectivity_nodes.insert(node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalIndex free_dof_count = 0;
|
|
||||||
LocalIndex weak_drilling_count = 0;
|
|
||||||
GlobalId weak_drilling_example = 0;
|
|
||||||
for (const auto& [node_id, node] : domain.nodes) {
|
|
||||||
(void)node;
|
|
||||||
for (Dof dof : allDofs()) {
|
|
||||||
const auto key = std::make_pair(node_id, dofIndex(dof));
|
|
||||||
if (constrained_dofs.count(key) != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
++free_dof_count;
|
|
||||||
if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) == 0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-DOF-UNTOUCHED",
|
|
||||||
"Node " + std::to_string(node_id) + " DOF " + dofLabel(dof) +
|
|
||||||
" is free but is not touched by active element connectivity",
|
|
||||||
"dof"));
|
|
||||||
}
|
|
||||||
if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) != 0 && dof == Dof::RZ) {
|
|
||||||
if (weak_drilling_count == 0) {
|
|
||||||
weak_drilling_example = node_id;
|
|
||||||
}
|
|
||||||
++weak_drilling_count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!domain.nodes.empty() && free_dof_count == 0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS",
|
|
||||||
"No free DOFs exist after applying boundary constraints", "dof"));
|
|
||||||
}
|
|
||||||
if (weak_drilling_count > 0) {
|
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-WEAK-DRILLING-DOF",
|
|
||||||
"Node " + std::to_string(weak_drilling_example) +
|
|
||||||
" DOF RZ is free; drilling rotation is weakly stabilized in Phase 1 (" +
|
|
||||||
std::to_string(weak_drilling_count) + " free drilling DOF(s))",
|
|
||||||
"dof"));
|
|
||||||
}
|
|
||||||
return diagnostics;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AnalysisModel {
|
|
||||||
StepDefinition step;
|
|
||||||
std::vector<GlobalId> active_element_ids;
|
|
||||||
std::vector<std::size_t> active_boundary_condition_indices;
|
|
||||||
std::vector<std::size_t> active_load_indices;
|
|
||||||
std::vector<std::size_t> active_shell_section_indices;
|
|
||||||
std::vector<std::string> active_material_keys;
|
|
||||||
std::vector<Diagnostic> diagnostics;
|
|
||||||
|
|
||||||
bool ok() const {
|
|
||||||
return !hasError(diagnostics);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
inline AnalysisModel buildLinearStaticAnalysisModel(const Domain& domain, LocalIndex step_index = 0) {
|
|
||||||
AnalysisModel model;
|
|
||||||
if (domain.steps.empty()) {
|
|
||||||
model.step = {"Step-1", "linear_static"};
|
|
||||||
} else {
|
|
||||||
if (step_index < 0 || step_index >= static_cast<LocalIndex>(domain.steps.size())) {
|
|
||||||
model.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ANALYSIS-STEP-INDEX",
|
|
||||||
"Requested analysis step index is out of range", "analysis model"));
|
|
||||||
model.step = domain.steps.front();
|
|
||||||
} else {
|
|
||||||
model.step = domain.steps[static_cast<std::size_t>(step_index)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (domain.steps.size() > 1) {
|
|
||||||
model.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ANALYSIS-MULTIPLE-STEPS",
|
|
||||||
"Phase 1 execution supports one active linear static step", "analysis model"));
|
|
||||||
}
|
|
||||||
if (model.step.analysis_type != "linear_static") {
|
|
||||||
model.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ANALYSIS-UNSUPPORTED-STEP",
|
|
||||||
"Only linear static steps are supported in Phase 1", "analysis model"));
|
|
||||||
}
|
|
||||||
for (const auto& [element_id, element] : domain.elements) {
|
|
||||||
(void)element;
|
|
||||||
model.active_element_ids.push_back(element_id);
|
|
||||||
}
|
|
||||||
for (std::size_t i = 0; i < domain.boundary_conditions.size(); ++i) {
|
|
||||||
model.active_boundary_condition_indices.push_back(i);
|
|
||||||
}
|
|
||||||
for (std::size_t i = 0; i < domain.loads.size(); ++i) {
|
|
||||||
model.active_load_indices.push_back(i);
|
|
||||||
}
|
|
||||||
for (std::size_t i = 0; i < domain.shell_sections.size(); ++i) {
|
|
||||||
model.active_shell_section_indices.push_back(i);
|
|
||||||
}
|
|
||||||
for (const auto& [material_key, material] : domain.materials) {
|
|
||||||
(void)material;
|
|
||||||
model.active_material_keys.push_back(material_key);
|
|
||||||
}
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DofAddress {
|
|
||||||
GlobalId node_id = 0;
|
|
||||||
Dof dof = Dof::UX;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DofManager {
|
|
||||||
public:
|
|
||||||
explicit DofManager(const Domain& domain) {
|
|
||||||
for (const auto& [node_id, node] : domain.nodes) {
|
|
||||||
(void)node;
|
|
||||||
node_ids_.push_back(node_id);
|
|
||||||
for (Dof dof : allDofs()) {
|
|
||||||
const LocalIndex full_index = static_cast<LocalIndex>(all_dofs_.size());
|
|
||||||
const auto key = std::make_pair(node_id, dofIndex(dof));
|
|
||||||
all_dofs_.push_back(key);
|
|
||||||
full_index_by_key_[key] = full_index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
|
||||||
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (GlobalId node_id : resolveNodeTarget(domain, boundary.target)) {
|
|
||||||
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
|
|
||||||
constrained_.insert(std::make_pair(node_id, dof - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto& key : all_dofs_) {
|
|
||||||
const LocalIndex full_index = full_index_by_key_.at(key);
|
|
||||||
if (constrained_.count(key) == 0) {
|
|
||||||
equation_by_key_[key] = static_cast<EquationId>(free_full_indices_.size());
|
|
||||||
free_full_indices_.push_back(full_index);
|
|
||||||
} else {
|
|
||||||
equation_by_key_[key] = -1;
|
|
||||||
constrained_full_indices_.push_back(full_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalIndex fullDofCount() const {
|
|
||||||
return static_cast<LocalIndex>(all_dofs_.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalIndex freeDofCount() const {
|
|
||||||
return static_cast<LocalIndex>(free_full_indices_.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalIndex constrainedDofCount() const {
|
|
||||||
return static_cast<LocalIndex>(constrained_full_indices_.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<GlobalId>& nodeIds() const {
|
|
||||||
return node_ids_;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<LocalIndex>& freeFullIndices() const {
|
|
||||||
return free_full_indices_;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<LocalIndex>& constrainedFullIndices() const {
|
|
||||||
return constrained_full_indices_;
|
|
||||||
}
|
|
||||||
|
|
||||||
DofAddress fullDof(LocalIndex full_index) const {
|
|
||||||
const auto& key = all_dofs_.at(static_cast<std::size_t>(full_index));
|
|
||||||
return {key.first, static_cast<Dof>(key.second)};
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalIndex fullIndex(GlobalId node_id, Dof dof) const {
|
|
||||||
return full_index_by_key_.at(std::make_pair(node_id, dofIndex(dof)));
|
|
||||||
}
|
|
||||||
|
|
||||||
EquationId equation(GlobalId node_id, Dof dof) const {
|
|
||||||
return equation_by_key_.at(std::make_pair(node_id, dofIndex(dof)));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isConstrained(GlobalId node_id, Dof dof) const {
|
|
||||||
return constrained_.count(std::make_pair(node_id, dofIndex(dof))) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Real> reduceFullVector(const std::vector<Real>& full) const {
|
|
||||||
std::vector<Real> reduced;
|
|
||||||
reduced.reserve(free_full_indices_.size());
|
|
||||||
for (LocalIndex full_index : free_full_indices_) {
|
|
||||||
reduced.push_back(full.at(static_cast<std::size_t>(full_index)));
|
|
||||||
}
|
|
||||||
return reduced;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Real> reconstructFullVector(const std::vector<Real>& reduced) const {
|
|
||||||
std::vector<Real> full(static_cast<std::size_t>(fullDofCount()), 0.0);
|
|
||||||
for (std::size_t i = 0; i < free_full_indices_.size(); ++i) {
|
|
||||||
full[static_cast<std::size_t>(free_full_indices_[i])] = reduced.at(i);
|
|
||||||
}
|
|
||||||
return full;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::array<LocalIndex, 24> elementFullDofIndices(const Element& element) const {
|
|
||||||
std::array<LocalIndex, 24> indices{};
|
|
||||||
for (LocalIndex node = 0; node < 4; ++node) {
|
|
||||||
for (Dof dof : allDofs()) {
|
|
||||||
const LocalIndex local = 6 * node + dofIndex(dof);
|
|
||||||
indices[static_cast<std::size_t>(local)] = fullIndex(element.node_ids[static_cast<std::size_t>(node)], dof);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return indices;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::array<EquationId, 24> elementEquationIds(const Element& element) const {
|
|
||||||
std::array<EquationId, 24> equations{};
|
|
||||||
for (LocalIndex node = 0; node < 4; ++node) {
|
|
||||||
for (Dof dof : allDofs()) {
|
|
||||||
const LocalIndex local = 6 * node + dofIndex(dof);
|
|
||||||
equations[static_cast<std::size_t>(local)] = equation(element.node_ids[static_cast<std::size_t>(node)], dof);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return equations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::vector<GlobalId> node_ids_;
|
|
||||||
std::vector<std::pair<GlobalId, int>> all_dofs_;
|
|
||||||
std::set<std::pair<GlobalId, int>> constrained_;
|
|
||||||
std::map<std::pair<GlobalId, int>, LocalIndex> full_index_by_key_;
|
|
||||||
std::map<std::pair<GlobalId, int>, EquationId> equation_by_key_;
|
|
||||||
std::vector<LocalIndex> free_full_indices_;
|
|
||||||
std::vector<LocalIndex> constrained_full_indices_;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SparsePatternEntry {
|
struct SparsePatternEntry {
|
||||||
EquationId row = 0;
|
EquationId row = 0;
|
||||||
EquationId col = 0;
|
EquationId col = 0;
|
||||||
@@ -2419,14 +1789,6 @@ class InMemoryResultsWriter {
|
|||||||
ResultFile result_;
|
ResultFile result_;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AnalysisState {
|
|
||||||
std::vector<Real> u_full;
|
|
||||||
std::vector<Real> f_external_full;
|
|
||||||
std::vector<Real> f_internal_full;
|
|
||||||
std::vector<Real> reaction_full;
|
|
||||||
bool converged = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AnalysisResult {
|
struct AnalysisResult {
|
||||||
AnalysisModel model;
|
AnalysisModel model;
|
||||||
AnalysisState state;
|
AnalysisState state;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{ "step": 0, "name": "architecture-drift-audit", "status": "completed", "artifact": "step0-architecture-map.md" },
|
{ "step": 0, "name": "architecture-drift-audit", "status": "completed", "artifact": "step0-architecture-map.md" },
|
||||||
{ "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": "pending" },
|
{ "step": 2, "name": "core-domain-dof-extraction", "status": "completed" },
|
||||||
{ "step": 3, "name": "math-solver-extraction", "status": "pending" },
|
{ "step": 3, "name": "math-solver-extraction", "status": "pending" },
|
||||||
{ "step": 4, "name": "io-parser-extraction", "status": "pending" },
|
{ "step": 4, "name": "io-parser-extraction", "status": "pending" },
|
||||||
{ "step": 5, "name": "results-reference-extraction", "status": "pending" },
|
{ "step": 5, "name": "results-reference-extraction", "status": "pending" },
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include "fesa/Boundary/Boundary.hpp"
|
||||||
|
#include "fesa/Core/Core.hpp"
|
||||||
|
#include "fesa/Load/Load.hpp"
|
||||||
|
#include "fesa/Property/Property.hpp"
|
||||||
|
#include "fesa/Util/Util.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void check(bool value, const char* message) {
|
||||||
|
if (!value) {
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
static_assert(std::is_same_v<fesa::Real, double>, "Real must remain double");
|
||||||
|
static_assert(std::is_same_v<fesa::GlobalId, std::int64_t>, "GlobalId must remain int64");
|
||||||
|
static_assert(std::is_same_v<fesa::LocalIndex, std::int64_t>, "LocalIndex must remain int64");
|
||||||
|
static_assert(std::is_same_v<fesa::EquationId, std::int64_t>, "EquationId must remain int64");
|
||||||
|
static_assert(std::is_same_v<fesa::SparseIndex, std::int64_t>, "SparseIndex must remain int64");
|
||||||
|
|
||||||
|
fesa::Domain domain;
|
||||||
|
domain.nodes[10] = {10, {0.0, 0.0, 0.0}};
|
||||||
|
domain.nodes[20] = {20, {1.0, 0.0, 0.0}};
|
||||||
|
domain.nodes[30] = {30, {1.0, 1.0, 0.0}};
|
||||||
|
domain.nodes[40] = {40, {0.0, 1.0, 0.0}};
|
||||||
|
domain.elements[100] = {100, fesa::ElementType::MITC4, {10, 20, 30, 40}, "EALL"};
|
||||||
|
domain.element_sets["eall"] = {"EALL", {100}};
|
||||||
|
domain.node_sets["fixed"] = {"FIXED", {10, 40}};
|
||||||
|
domain.materials["mat"] = {"MAT", 1000.0, 0.3};
|
||||||
|
domain.shell_sections.push_back(fesa::ShellSection{"EALL", "MAT", 0.1});
|
||||||
|
domain.boundary_conditions.push_back(fesa::BoundaryCondition{"FIXED", 1, 6, 0.0});
|
||||||
|
domain.loads.push_back(fesa::NodalLoad{"20", 3, -1.0});
|
||||||
|
domain.steps.push_back({"Step-1", "linear_static"});
|
||||||
|
|
||||||
|
check(fesa::Domain::key(" EALL ") == "eall", "Domain key normalization changed");
|
||||||
|
check(fesa::dofFromAbaqus(6).value() == fesa::Dof::RZ, "Abaqus DOF mapping changed");
|
||||||
|
check(std::string(fesa::dofLabel(fesa::Dof::UX)) == "UX", "DOF label changed");
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::validateDomain(domain);
|
||||||
|
check(!fesa::hasError(diagnostics), "Direct module validation reported an error");
|
||||||
|
|
||||||
|
const fesa::DofManager dofs(domain);
|
||||||
|
check(dofs.fullDofCount() == 24, "DofManager full DOF count changed");
|
||||||
|
check(dofs.constrainedDofCount() == 12, "DofManager constrained DOF count changed");
|
||||||
|
check(dofs.freeDofCount() == 12, "DofManager free DOF count changed");
|
||||||
|
check(dofs.equation(20, fesa::Dof::UZ) >= 0, "Free equation numbering changed");
|
||||||
|
check(dofs.equation(10, fesa::Dof::UX) == -1, "Constrained equation numbering changed");
|
||||||
|
|
||||||
|
const auto diagnostic = fesa::makeDiagnostic(fesa::Severity::Warning, "FESA-SMOKE", "module smoke", "core");
|
||||||
|
check(diagnostic.source.keyword == "core", "Diagnostic source keyword changed");
|
||||||
|
|
||||||
|
const auto model = fesa::buildLinearStaticAnalysisModel(domain);
|
||||||
|
check(model.ok(), "AnalysisModel direct module construction failed");
|
||||||
|
check(model.active_element_ids.size() == 1, "AnalysisModel active elements changed");
|
||||||
|
|
||||||
|
fesa::AnalysisState state;
|
||||||
|
state.u_full = dofs.reconstructFullVector(std::vector<fesa::Real>(static_cast<std::size_t>(dofs.freeDofCount()), 0.0));
|
||||||
|
check(state.u_full.size() == static_cast<std::size_t>(dofs.fullDofCount()), "AnalysisState full vector ownership changed");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user