Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b57b0bf63a | |||
| 825e03dbaf | |||
| cbd1a6c5d7 | |||
| 4e7fd1087d |
@@ -0,0 +1,12 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
project(FESA LANGUAGES CXX)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE FESA_CORE_SOURCES CONFIGURE_DEPENDS src/fesa/*.cpp)
|
||||||
|
|
||||||
|
add_library(fesa_core STATIC ${FESA_CORE_SOURCES})
|
||||||
|
target_compile_features(fesa_core PUBLIC cxx_std_17)
|
||||||
|
target_include_directories(fesa_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Solver Core Skeleton Build/Test Report
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
- phase: solver-core-skeleton
|
||||||
|
- scope: C++ skeleton classes only
|
||||||
|
- status: passed
|
||||||
|
|
||||||
|
## Commands Run
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest scripts.test_header_declaration_only
|
||||||
|
```
|
||||||
|
|
||||||
|
- exit_code: 0
|
||||||
|
- summary: Solver headers contain declarations only; function bodies are implemented in `.cpp` files.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
- exit_code: 0
|
||||||
|
- summary: 99 Python Harness tests passed.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
- exit_code: 0
|
||||||
|
- summary: CMake configure, MSVC Debug build, and full CTest suite passed.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R solver_core_skeleton_integration_test
|
||||||
|
```
|
||||||
|
|
||||||
|
- exit_code: 0
|
||||||
|
- summary: `solver_core_skeleton_integration_test` passed.
|
||||||
|
|
||||||
|
## CTest Tests Added
|
||||||
|
|
||||||
|
- `harness_smoke_test`
|
||||||
|
- `core_diagnostic_test`
|
||||||
|
- `core_ids_test`
|
||||||
|
- `core_primitives_test`
|
||||||
|
- `core_status_test`
|
||||||
|
- `model_analysis_step_test`
|
||||||
|
- `model_boundary_condition_test`
|
||||||
|
- `model_domain_test`
|
||||||
|
- `model_element_test`
|
||||||
|
- `model_load_test`
|
||||||
|
- `model_material_test`
|
||||||
|
- `model_node_test`
|
||||||
|
- `model_property_test`
|
||||||
|
- `analysis_model_view_test`
|
||||||
|
- `dof_manager_dof_key_test`
|
||||||
|
- `dof_manager_numbering_test`
|
||||||
|
- `analysis_state_vectors_test`
|
||||||
|
- `analysis_flow_linear_static_analysis_test`
|
||||||
|
- `analysis_flow_template_test`
|
||||||
|
- `results_containers_test`
|
||||||
|
- `solver_core_skeleton_integration_test`
|
||||||
|
|
||||||
|
## Structural Tests Added
|
||||||
|
|
||||||
|
- `scripts.test_header_declaration_only`
|
||||||
|
|
||||||
|
## Build Structure
|
||||||
|
|
||||||
|
- `fesa_core` now builds as a static library from `src/fesa/**/*.cpp`.
|
||||||
|
- Solver headers under `src/fesa/**/*.hpp` declare functions only; method bodies live in matching `.cpp` translation units.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- No element stiffness, residual, tangent, or stress recovery calculation is implemented.
|
||||||
|
- No material law evaluation is implemented.
|
||||||
|
- No sparse assembly implementation is implemented beyond `DofManager` sparse pattern ownership.
|
||||||
|
- No linear solver backend is implemented.
|
||||||
|
- No HDF5 writer or reader is implemented.
|
||||||
|
- No Abaqus `.inp` parser is implemented.
|
||||||
|
- No reference comparison against Abaqus CSV artifacts is implemented.
|
||||||
|
- `LinearStaticAnalysis` currently prepares the analysis model, DOF map, and zero-valued state only.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Abaqus Input Parser I/O Definition
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
- feature_id: abaqus-input-parser
|
||||||
|
- status: ready-for-implementation-planning
|
||||||
|
- owner_agent: io-definition-agent
|
||||||
|
- date: 2026-06-12
|
||||||
|
|
||||||
|
## Abaqus Input Scope
|
||||||
|
|
||||||
|
FESA supports only the Abaqus input keyword subset listed here. This document
|
||||||
|
does not claim full Abaqus compatibility.
|
||||||
|
|
||||||
|
| keyword | support_status | level | required_parameters | mapped_internal_concept | unsupported-case behavior |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `*HEADING` | supported | model | none | model title metadata | accepted; title storage may be deferred |
|
||||||
|
| `*NODE` | supported | model | none | node id and global coordinates | malformed rows are errors |
|
||||||
|
| `*ELEMENT` | supported | model | `TYPE` | element id, topology, connectivity | missing/unsupported `TYPE` is an error |
|
||||||
|
| `*NSET` | supported | model | `NSET` | named node set | missing name is an error |
|
||||||
|
| `*ELSET` | supported | model | `ELSET` | named element set | missing name is an error |
|
||||||
|
| `*MATERIAL` | supported | model | `NAME` | material identity | missing name is an error |
|
||||||
|
| `*ELASTIC` | supported | model | none | linear elastic constants | malformed rows are errors |
|
||||||
|
| section keyword | supported | model | `ELSET`, `MATERIAL` | property assignment | unsupported section type is an error |
|
||||||
|
| `*STEP` | supported | history | optional `NAME` | analysis step | malformed parameters are errors |
|
||||||
|
| `*STATIC` | supported | history | none | static procedure marker | unsupported procedures are errors |
|
||||||
|
| `*BOUNDARY` | supported | model/history | none | prescribed nodal dof values | malformed rows are errors |
|
||||||
|
| `*CLOAD` | supported | history | none | concentrated nodal load | malformed rows are errors |
|
||||||
|
| `*OUTPUT` | supported | history | none | output request root | unsupported options warn or error by context |
|
||||||
|
| `*NODE OUTPUT` | supported | history | none | nodal output request | unsupported quantities warn or error by context |
|
||||||
|
| `*ELEMENT OUTPUT` | supported | history | none | element output request | unsupported quantities warn or error by context |
|
||||||
|
|
||||||
|
Initial V0 element names are two-node line/bar types only: `T2D2`, `T3D2`,
|
||||||
|
`C3D2`, and `B31`.
|
||||||
|
|
||||||
|
## Syntax Policy
|
||||||
|
|
||||||
|
- Keywords and parameter names are case-insensitive.
|
||||||
|
- Keyword lines begin with `*`.
|
||||||
|
- Comment lines begin with `**` and are ignored.
|
||||||
|
- Blank lines are ignored.
|
||||||
|
- Fields are comma-separated. Leading and trailing whitespace around fields is
|
||||||
|
ignored.
|
||||||
|
- Empty required fields are parse errors.
|
||||||
|
- Unsupported keywords are errors unless this contract explicitly marks them as
|
||||||
|
ignored-with-warning.
|
||||||
|
- Diagnostics must include severity, a stable code, a human-readable message,
|
||||||
|
and enough line context to locate the input row.
|
||||||
|
- Include files are out of scope for this parser phase.
|
||||||
|
|
||||||
|
## Model Data Mapping
|
||||||
|
|
||||||
|
- Nodes map an Abaqus node label to a FESA node id and three global coordinate
|
||||||
|
components. Missing trailing coordinate components are interpreted as `0.0`
|
||||||
|
only when the keyword-specific test documents that behavior.
|
||||||
|
- Elements map an Abaqus element label, supported element type, and connectivity
|
||||||
|
labels to a FESA element. Section assignment supplies material/property
|
||||||
|
linkage in a later parser slice.
|
||||||
|
- Node sets and element sets preserve deterministic membership order.
|
||||||
|
- Materials map by Abaqus `NAME`. Linear elastic data maps to the semantic
|
||||||
|
material contract used by solver implementation.
|
||||||
|
- Section keywords bind an element set to a material and create the semantic
|
||||||
|
property assignment needed by the solver.
|
||||||
|
- The global coordinate system is assumed. Units are user-consistent and are not
|
||||||
|
converted by the parser.
|
||||||
|
|
||||||
|
## History Data Mapping
|
||||||
|
|
||||||
|
- `*STEP` begins an ordered analysis step. If no name is provided, the parser
|
||||||
|
assigns a deterministic step name or id.
|
||||||
|
- `*STATIC` marks the step as a linear static procedure for V0.
|
||||||
|
- `*BOUNDARY` maps node id, dof range, and value to prescribed boundary
|
||||||
|
conditions.
|
||||||
|
- `*CLOAD` maps node id, dof component, and magnitude to concentrated loads.
|
||||||
|
- Output request keywords define requested quantities when semantic storage
|
||||||
|
exists; unsupported quantities must not be silently accepted.
|
||||||
|
|
||||||
|
## Internal Model Contract
|
||||||
|
|
||||||
|
- `Domain` owns model definition data created from the input file.
|
||||||
|
- Parsed model objects should be treated as immutable after parsing where
|
||||||
|
practical.
|
||||||
|
- `AnalysisStep` owns step-local boundary conditions and loads.
|
||||||
|
- The parser must not store equation ids on nodes or elements.
|
||||||
|
- Parser diagnostics are part of the result; callers do not need to inspect
|
||||||
|
partial `Domain` state to detect failure.
|
||||||
|
|
||||||
|
## Output HDF5 Schema
|
||||||
|
|
||||||
|
The parser itself does not write HDF5. Solver output remains authoritative in
|
||||||
|
`results.h5` and follows the project HDF5 contract:
|
||||||
|
|
||||||
|
| quantity | dataset_path | location | component policy |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| displacement | `/steps/<step>/frames/<frame>/field_outputs/U` | nodal | `U1`, `U2`, `U3` as applicable |
|
||||||
|
| reaction | `/steps/<step>/frames/<frame>/field_outputs/RF` | nodal | `RF1`, `RF2`, `RF3` as applicable |
|
||||||
|
| internal force | `/steps/<step>/frames/<frame>/field_outputs/element_forces` | element | feature-specific |
|
||||||
|
| stress | `/steps/<step>/frames/<frame>/field_outputs/S` | integration point or element | feature-specific |
|
||||||
|
|
||||||
|
Required metadata includes schema version, model id, source input identity,
|
||||||
|
coordinate system, units policy, solver version, step/frame identity, and row
|
||||||
|
identity fields.
|
||||||
|
|
||||||
|
## FESA HDF5 To Reference CSV Comparison Schema
|
||||||
|
|
||||||
|
Reference comparison reads `results.h5` and matches deterministic rows against
|
||||||
|
Abaqus-generated CSV files under `reference/<model-id>/`. This parser phase does
|
||||||
|
not generate or modify those reference artifacts.
|
||||||
|
|
||||||
|
Common row identity:
|
||||||
|
|
||||||
|
- sort order: step, frame, entity id, location, component
|
||||||
|
- node id and element id are Abaqus labels preserved by the parser
|
||||||
|
- component names follow the HDF5 component policy
|
||||||
|
|
||||||
|
CSV files remain:
|
||||||
|
|
||||||
|
- `<model-id>_displacements.csv`
|
||||||
|
- `<model-id>_reactions.csv`
|
||||||
|
- `<model-id>_internalforces.csv`
|
||||||
|
- `<model-id>_stresses.csv`
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
- Duplicate node, element, material, property, set, or step labels are errors.
|
||||||
|
- Missing references are errors.
|
||||||
|
- Unsupported keywords are errors unless explicitly documented otherwise.
|
||||||
|
- Malformed numeric fields are errors.
|
||||||
|
- Parser unit tests must cover valid subset mapping and invalid subset
|
||||||
|
diagnostics before production parser changes.
|
||||||
|
- Workspace validation remains `python scripts/validate_workspace.py`.
|
||||||
|
|
||||||
|
## Downstream Handoff
|
||||||
|
|
||||||
|
Implementation planning should split work into mesh keyword parsing,
|
||||||
|
diagnostics, set/section mapping, material/history mapping, and integration
|
||||||
|
validation. Reference model work may later use this subset to prepare
|
||||||
|
`reference/<model-id>/model.inp`, but this phase must not create or modify
|
||||||
|
Abaqus reference CSV artifacts.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"project": "FESA Structural Solver",
|
||||||
|
"phase": "abaqus-input-parser",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": 0,
|
||||||
|
"name": "io-contract",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"docs/io-definitions/abaqus-input-parser-io.md"
|
||||||
|
],
|
||||||
|
"summary": "Abaqus input parser I/O contract added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 1,
|
||||||
|
"name": "mesh-keyword-parser",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/io/abaqus/",
|
||||||
|
"tests/unit/abaqus_input_parser_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "Mesh keyword parser maps NODE and ELEMENT data into Domain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 2,
|
||||||
|
"name": "syntax-diagnostics",
|
||||||
|
"status": "pending",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/io/abaqus/",
|
||||||
|
"tests/unit/abaqus_input_parser_*_test.cpp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"name": "sets-and-section-properties",
|
||||||
|
"status": "pending",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/io/abaqus/",
|
||||||
|
"src/fesa/model/",
|
||||||
|
"tests/unit/abaqus_input_parser_*_test.cpp",
|
||||||
|
"tests/unit/model_*_test.cpp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 4,
|
||||||
|
"name": "material-step-load-parser",
|
||||||
|
"status": "pending",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/io/abaqus/",
|
||||||
|
"src/fesa/model/",
|
||||||
|
"tests/unit/abaqus_input_parser_*_test.cpp",
|
||||||
|
"tests/unit/model_*_test.cpp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 5,
|
||||||
|
"name": "integration-validation-report",
|
||||||
|
"status": "pending",
|
||||||
|
"allowed_paths": [
|
||||||
|
"tests/integration/",
|
||||||
|
"docs/build-test-reports/abaqus-input-parser.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Step 0: io-contract
|
||||||
|
|
||||||
|
## Read First
|
||||||
|
|
||||||
|
Read these files before editing:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/docs/SOLVER_AGENT_DESIGN.md`
|
||||||
|
- `/docs/io-definitions/README.md`
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Create the feature-level I/O contract for the Abaqus input parser at
|
||||||
|
`/docs/io-definitions/abaqus-input-parser-io.md`.
|
||||||
|
|
||||||
|
The contract must explicitly state that FESA supports only the documented
|
||||||
|
Abaqus keyword subset, not full Abaqus compatibility. Keep this document at a
|
||||||
|
semantic level. Do not design C++ APIs and do not implement parser code in this
|
||||||
|
step.
|
||||||
|
|
||||||
|
Required scope:
|
||||||
|
|
||||||
|
- Model data keywords: `*HEADING`, `*NODE`, `*ELEMENT`, `*NSET`, `*ELSET`,
|
||||||
|
`*MATERIAL`, `*ELASTIC`, and section keywords needed for V0 bar/truss use.
|
||||||
|
- History data keywords: `*STEP`, `*STATIC`, `*BOUNDARY`, `*CLOAD`,
|
||||||
|
`*OUTPUT`, `*NODE OUTPUT`, and `*ELEMENT OUTPUT`.
|
||||||
|
- Syntax policy for comments beginning with `**`, case-insensitive keywords,
|
||||||
|
comma-separated parameters and data, blank lines, unsupported keywords, and
|
||||||
|
parse diagnostics.
|
||||||
|
- Internal semantic mapping to `Domain`, nodes, elements, sets, materials,
|
||||||
|
properties, `AnalysisStep`, boundary conditions, loads, and output requests.
|
||||||
|
- HDF5 and reference CSV comparison schema summary consistent with existing
|
||||||
|
PRD/architecture documents. Do not create or modify reference artifacts.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
No C++ tests are required in this documentation-only step.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Procedure
|
||||||
|
|
||||||
|
1. Run the acceptance criteria commands.
|
||||||
|
2. Confirm the contract does not claim full Abaqus compatibility.
|
||||||
|
3. Confirm every supported keyword has purpose, data requirements, mapping, and
|
||||||
|
unsupported-case behavior.
|
||||||
|
4. Update `phases/abaqus-input-parser/index.json` step 0:
|
||||||
|
- success: `"status": "completed"`, `"summary": "Abaqus input parser I/O contract added"`
|
||||||
|
- error after three attempts: `"status": "error"`, `"error_message": "<specific error>"`
|
||||||
|
- blocked: `"status": "blocked"`, `"blocked_reason": "<specific reason>"`
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not implement parser code.
|
||||||
|
- Do not design C++ APIs.
|
||||||
|
- Do not run Abaqus, Nastran, or any reference solver.
|
||||||
|
- Do not generate or modify Abaqus reference CSV files.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Step 1: mesh-keyword-parser
|
||||||
|
|
||||||
|
## Read First
|
||||||
|
|
||||||
|
Read these files before editing:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/docs/io-definitions/abaqus-input-parser-io.md`
|
||||||
|
- `/src/fesa/model/domain.hpp`
|
||||||
|
- `/src/fesa/model/node.hpp`
|
||||||
|
- `/src/fesa/model/element.hpp`
|
||||||
|
- `/tests/unit/model_domain_test.cpp`
|
||||||
|
|
||||||
|
Review step 0 output before implementing. Keep this step limited to parser API
|
||||||
|
and mesh keyword mapping.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Add the first C++ parser slice under `/src/fesa/io/abaqus/`.
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
|
||||||
|
- Provide a small `InputParser` that accepts an in-memory `.inp` string and
|
||||||
|
returns a parse result containing `fesa::model::Domain` and
|
||||||
|
`fesa::core::Status`.
|
||||||
|
- Parse `*HEADING` as accepted but not yet stored.
|
||||||
|
- Parse `*NODE` data lines into `Domain::add_node`.
|
||||||
|
- Parse `*ELEMENT, TYPE=<type>` data lines into `Domain::add_element`.
|
||||||
|
- Support only two-node line/bar element names needed by the current model:
|
||||||
|
`T2D2`, `T3D2`, `B31`, and `C3D2`.
|
||||||
|
- Map `T2D2`, `T3D2`, and `C3D2` to `ElementTopology::truss2`; map `B31` to
|
||||||
|
`ElementTopology::bar2`.
|
||||||
|
- Use `PropertyId{0}` for elements in this first mesh-only slice because
|
||||||
|
section parsing is introduced later.
|
||||||
|
- Treat keywords and parameter names case-insensitively.
|
||||||
|
- Skip blank lines and comment lines that begin with `**`.
|
||||||
|
|
||||||
|
Keep implementation C++17/MSVC-compatible. Do not introduce external
|
||||||
|
libraries, regex-heavy parser frameworks, JavaScript, TypeScript, or npm
|
||||||
|
fallbacks.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
Add `/tests/unit/abaqus_input_parser_mesh_test.cpp` before production code.
|
||||||
|
|
||||||
|
The test must first fail because `fesa/io/abaqus/input_parser.hpp` does not
|
||||||
|
exist, then pass after implementation. It should verify:
|
||||||
|
|
||||||
|
- a small input with `*HEADING`, `*NODE`, and `*ELEMENT, TYPE=T3D2` returns
|
||||||
|
`Status::is_ok() == true`;
|
||||||
|
- the returned `Domain` contains the expected node ids and 3D coordinates;
|
||||||
|
- the returned `Domain` contains the expected element id, topology,
|
||||||
|
connectivity, and `PropertyId{0}`;
|
||||||
|
- keyword and parameter casing are accepted case-insensitively.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R abaqus_input_parser_mesh_test
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Procedure
|
||||||
|
|
||||||
|
1. Run the targeted CTest after writing the test and before production code;
|
||||||
|
confirm the failure is due to the missing parser API.
|
||||||
|
2. Implement the minimum code needed for the test to pass.
|
||||||
|
3. Run all acceptance criteria commands.
|
||||||
|
4. Confirm C++ production changes have a related C++ test file.
|
||||||
|
5. Update `phases/abaqus-input-parser/index.json` step 1:
|
||||||
|
- success: `"status": "completed"`, `"summary": "Mesh keyword parser maps NODE and ELEMENT data into Domain"`
|
||||||
|
- error after three attempts: `"status": "error"`, `"error_message": "<specific error>"`
|
||||||
|
- blocked: `"status": "blocked"`, `"blocked_reason": "<specific reason>"`
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not add set, material, section, load, boundary condition, output request,
|
||||||
|
include-file, or HDF5 behavior in this step.
|
||||||
|
- Do not mutate reference artifacts.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Step 2: syntax-diagnostics
|
||||||
|
|
||||||
|
## Read First
|
||||||
|
|
||||||
|
Read these files before editing:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/docs/io-definitions/abaqus-input-parser-io.md`
|
||||||
|
- `/src/fesa/io/abaqus/input_parser.hpp`
|
||||||
|
- `/src/fesa/io/abaqus/input_parser.cpp`
|
||||||
|
- `/tests/unit/abaqus_input_parser_mesh_test.cpp`
|
||||||
|
|
||||||
|
Review completed step summaries in `/phases/abaqus-input-parser/index.json`.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Add structured diagnostics for parser syntax and unsupported subset behavior.
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
|
||||||
|
- Unsupported keywords produce `Status::is_ok() == false` with an error
|
||||||
|
diagnostic code and message that identifies the keyword.
|
||||||
|
- Malformed `*NODE` and `*ELEMENT` data lines produce error diagnostics.
|
||||||
|
- Missing required `TYPE` on `*ELEMENT` produces an error diagnostic.
|
||||||
|
- Unsupported element types produce an error diagnostic and do not add that
|
||||||
|
element.
|
||||||
|
- Diagnostics must include enough context in the message to locate the line
|
||||||
|
number.
|
||||||
|
- Existing valid mesh parser behavior remains unchanged.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
Extend `/tests/unit/abaqus_input_parser_mesh_test.cpp` or add
|
||||||
|
`/tests/unit/abaqus_input_parser_diagnostics_test.cpp` before production code.
|
||||||
|
|
||||||
|
Test:
|
||||||
|
|
||||||
|
- unsupported keyword failure;
|
||||||
|
- malformed node row failure;
|
||||||
|
- missing element `TYPE` failure;
|
||||||
|
- unsupported element type failure.
|
||||||
|
|
||||||
|
Run the targeted CTest and confirm the new tests fail for missing behavior
|
||||||
|
before implementation.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R abaqus_input_parser
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Procedure
|
||||||
|
|
||||||
|
Update step 2 status with summary or a concrete error/blocked reason.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not add new semantic model fields except what diagnostics require.
|
||||||
|
- Do not add material, section, step, boundary, or load parsing.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Step 3: sets-and-section-properties
|
||||||
|
|
||||||
|
## Read First
|
||||||
|
|
||||||
|
Read these files before editing:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/docs/io-definitions/abaqus-input-parser-io.md`
|
||||||
|
- `/src/fesa/model/domain.hpp`
|
||||||
|
- `/src/fesa/model/element.hpp`
|
||||||
|
- `/src/fesa/model/property.hpp`
|
||||||
|
- `/src/fesa/io/abaqus/input_parser.hpp`
|
||||||
|
- `/src/fesa/io/abaqus/input_parser.cpp`
|
||||||
|
|
||||||
|
Review completed parser tests and phase summaries first.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Extend the model and parser only as needed to represent set membership and
|
||||||
|
section-to-property assignment for V0 bar/truss models.
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
|
||||||
|
- Parse `*NSET, NSET=<name>` node membership rows.
|
||||||
|
- Parse `*ELSET, ELSET=<name>` element membership rows.
|
||||||
|
- Parse one V0 section keyword that assigns a material name to an element set.
|
||||||
|
Prefer the smallest section subset compatible with the current V0 element
|
||||||
|
path.
|
||||||
|
- Preserve deterministic member ordering as read, unless the I/O contract
|
||||||
|
explicitly states otherwise.
|
||||||
|
- Keep `Domain` as the owner of model definition data. Do not store equation ids
|
||||||
|
on nodes or elements.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
Add focused C++ tests before production code:
|
||||||
|
|
||||||
|
- a model test for any new set/section semantic model API;
|
||||||
|
- a parser test proving `*NSET`, `*ELSET`, and the selected section keyword map
|
||||||
|
into the semantic model.
|
||||||
|
|
||||||
|
Run targeted CTest and confirm the tests fail before implementation.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "(model_|abaqus_input_parser_)"
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Procedure
|
||||||
|
|
||||||
|
Update step 3 status with summary or a concrete error/blocked reason.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not parse materials beyond names needed for section linkage.
|
||||||
|
- Do not add loads, boundary conditions, or analysis steps.
|
||||||
|
- Do not generate or modify reference artifacts.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Step 4: material-step-load-parser
|
||||||
|
|
||||||
|
## Read First
|
||||||
|
|
||||||
|
Read these files before editing:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/docs/io-definitions/abaqus-input-parser-io.md`
|
||||||
|
- `/src/fesa/model/material.hpp`
|
||||||
|
- `/src/fesa/model/analysis_step.hpp`
|
||||||
|
- `/src/fesa/model/boundary_condition.hpp`
|
||||||
|
- `/src/fesa/model/load.hpp`
|
||||||
|
- `/src/fesa/io/abaqus/input_parser.hpp`
|
||||||
|
- `/src/fesa/io/abaqus/input_parser.cpp`
|
||||||
|
|
||||||
|
Review completed parser and model tests first.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Extend the parser for the V0 material and history data subset.
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
|
||||||
|
- Parse `*MATERIAL, NAME=<name>` and `*ELASTIC` data into the internal material
|
||||||
|
representation. If the current `Material` model cannot store elastic data,
|
||||||
|
add the smallest semantic extension with tests.
|
||||||
|
- Parse `*STEP` and `*STATIC` into ordered `AnalysisStep` definitions.
|
||||||
|
- Parse `*BOUNDARY` rows into step boundary conditions using existing
|
||||||
|
`DofComponent` mapping.
|
||||||
|
- Parse `*CLOAD` rows into step concentrated loads using existing
|
||||||
|
`DofComponent` mapping.
|
||||||
|
- Parse `*OUTPUT`, `*NODE OUTPUT`, and `*ELEMENT OUTPUT` only to the extent
|
||||||
|
documented in the I/O contract. If semantic storage is not yet present, record
|
||||||
|
an explicit ignored-with-warning diagnostic rather than inventing output model
|
||||||
|
APIs.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
Add focused C++ tests before production code:
|
||||||
|
|
||||||
|
- model tests for any new material or output request semantic fields;
|
||||||
|
- parser tests for material/elastic, step/static, boundary, and cload rows.
|
||||||
|
|
||||||
|
Run targeted CTest and confirm the tests fail before implementation.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "(model_|abaqus_input_parser_)"
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Procedure
|
||||||
|
|
||||||
|
Update step 4 status with summary or a concrete error/blocked reason.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not implement analysis execution, assembly, HDF5 writing, or reference
|
||||||
|
comparison in this step.
|
||||||
|
- Do not mutate reference artifacts.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Step 5: integration-validation-report
|
||||||
|
|
||||||
|
## Read First
|
||||||
|
|
||||||
|
Read these files before editing:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/docs/io-definitions/abaqus-input-parser-io.md`
|
||||||
|
- `/src/fesa/io/abaqus/input_parser.hpp`
|
||||||
|
- `/tests/unit/abaqus_input_parser_mesh_test.cpp`
|
||||||
|
- existing parser/model tests added by prior steps
|
||||||
|
|
||||||
|
Review all completed step summaries in `/phases/abaqus-input-parser/index.json`.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Add an integration-level parser test and a build/test report.
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
|
||||||
|
- Add a small integration test that parses a V0 Abaqus `.inp` string containing
|
||||||
|
the supported parser subset and validates the resulting `Domain` and
|
||||||
|
`AnalysisStep` data.
|
||||||
|
- Keep the test in memory; do not create or modify Abaqus reference CSV
|
||||||
|
artifacts.
|
||||||
|
- Write `/docs/build-test-reports/abaqus-input-parser.md` with command evidence,
|
||||||
|
exit codes, and known limitations.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
Add `/tests/integration/abaqus_input_parser_integration_test.cpp` before any
|
||||||
|
final integration adjustments. Confirm it fails for the missing integrated
|
||||||
|
behavior, then make it pass with the minimum implementation-owned changes.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R abaqus_input_parser
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Procedure
|
||||||
|
|
||||||
|
1. Run the acceptance criteria commands.
|
||||||
|
2. Confirm no reference artifacts were generated or modified.
|
||||||
|
3. Update step 5 status with summary or a concrete error/blocked reason.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not claim reference comparison, physics sanity, or release readiness.
|
||||||
|
- Do not generate, restore, or modify Abaqus reference CSV files.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"dir": "solver-core-skeleton",
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dir": "abaqus-input-parser",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"project": "FESA Structural Solver",
|
||||||
|
"phase": "solver-core-skeleton",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": 0,
|
||||||
|
"name": "cmake-ctest-bootstrap",
|
||||||
|
"status": "pending",
|
||||||
|
"allowed_paths": [
|
||||||
|
"CMakeLists.txt",
|
||||||
|
"tests/"
|
||||||
|
],
|
||||||
|
"started_at": "2026-06-12T02:09:10+0900",
|
||||||
|
"summary": "CMake/CTest bootstrap with fesa_core interface target and smoke test",
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 1,
|
||||||
|
"name": "core-primitives",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/core/",
|
||||||
|
"tests/unit/core_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "Core ID, diagnostic, and status primitives added with tests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 2,
|
||||||
|
"name": "domain-model-entities",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/model/",
|
||||||
|
"tests/unit/model_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "Model entities and Domain ownership API added with tests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"name": "analysis-model-view",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/analysis/",
|
||||||
|
"tests/unit/analysis_model_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "AnalysisModel step view added without Domain copies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 4,
|
||||||
|
"name": "dof-manager",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/fem/",
|
||||||
|
"tests/unit/dof_manager_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "DofManager deterministic numbering and constrained/free mapping added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 5,
|
||||||
|
"name": "analysis-state",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/analysis/",
|
||||||
|
"tests/unit/analysis_state_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "AnalysisState vector ownership and residual update added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 6,
|
||||||
|
"name": "analysis-template-flow",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/analysis/",
|
||||||
|
"tests/unit/analysis_flow_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "Analysis template method and LinearStaticAnalysis skeleton added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 7,
|
||||||
|
"name": "results-containers",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"src/fesa/results/",
|
||||||
|
"tests/unit/results_*_test.cpp"
|
||||||
|
],
|
||||||
|
"summary": "ResultStep, ResultFrame, FieldOutput, and HistoryOutput containers added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 8,
|
||||||
|
"name": "solver-skeleton-integration-report",
|
||||||
|
"status": "completed",
|
||||||
|
"allowed_paths": [
|
||||||
|
"tests/integration/",
|
||||||
|
"docs/build-test-reports/"
|
||||||
|
],
|
||||||
|
"summary": "Solver skeleton integration test and build/test report added"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_at": "2026-06-12T02:09:10+0900",
|
||||||
|
"completed_at": "2026-06-12T02:42:00+0900"
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Step 0: cmake-ctest-bootstrap
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 검증 규칙을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/scripts/validate_workspace.py`
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
C++ solver skeleton을 구현할 수 있도록 최소 CMake/CTest 부트스트랩을 만든다.
|
||||||
|
|
||||||
|
요구사항:
|
||||||
|
|
||||||
|
- 루트 `/CMakeLists.txt`를 생성한다.
|
||||||
|
- C++ 표준은 C++17 이상으로 고정한다.
|
||||||
|
- MSVC에서 warning-as-error를 강제하지 않는다.
|
||||||
|
- 아직 production source가 없으므로 solver target은 `INTERFACE` library `fesa_core`로 시작한다.
|
||||||
|
- `fesa_core`는 repository root의 `src`를 include directory로 노출한다.
|
||||||
|
- `enable_testing()`과 `tests/` 하위 CMake 구성을 연결한다.
|
||||||
|
- `/tests/CMakeLists.txt`, `/tests/unit/CMakeLists.txt`, `/tests/integration/CMakeLists.txt`를 생성한다.
|
||||||
|
- `tests/unit/*_test.cpp`와 `tests/integration/*_test.cpp` 파일을 각각 독립 test executable로 등록한다.
|
||||||
|
- 각 test executable은 `fesa_core`에 link한다.
|
||||||
|
- `/tests/unit/harness_smoke_test.cpp`를 생성하고, 표준 라이브러리만 사용해 CTest가 동작함을 확인하는 최소 `main()`을 둔다.
|
||||||
|
- npm, JavaScript, TypeScript fallback은 추가하지 않는다.
|
||||||
|
|
||||||
|
권장 CMake 구조:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(FESA LANGUAGES CXX)
|
||||||
|
|
||||||
|
add_library(fesa_core INTERFACE)
|
||||||
|
target_compile_features(fesa_core INTERFACE cxx_std_17)
|
||||||
|
target_include_directories(fesa_core INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
```
|
||||||
|
|
||||||
|
`tests/unit/CMakeLists.txt`와 `tests/integration/CMakeLists.txt`는 `file(GLOB CONFIGURE_DEPENDS ...)`와 `foreach`를 사용해 새 `*_test.cpp`가 자동으로 CTest에 등록되도록 만든다.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/harness_smoke_test.cpp`
|
||||||
|
- `main()`이 `std::string{"fesa"}.size() == 4` 같은 deterministic smoke assertion을 검증한다.
|
||||||
|
- 실패 시 non-zero를 반환한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. `tests/unit/harness_smoke_test.cpp`를 먼저 만든다.
|
||||||
|
2. `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R harness_smoke_test`를 실행해 아직 CMake 구성이 없어 실패함을 확인한다.
|
||||||
|
3. 그 뒤 CMake 파일을 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R harness_smoke_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- ARCHITECTURE.md의 `src/`, `tests/unit/` 구조를 따르는가?
|
||||||
|
- ADR-002의 C++17/MSVC/CMake/CTest 기본값을 벗어나지 않았는가?
|
||||||
|
- AGENTS.md의 `python scripts/validate_workspace.py` 기본 검증 경로를 유지하는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 0을 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "CMake/CTest bootstrap with fesa_core interface target and smoke test"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- C++ production solver class를 이 step에서 만들지 마라.
|
||||||
|
- 외부 test framework를 추가하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Step 1: core-primitives
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/CMakeLists.txt`
|
||||||
|
- `/tests/unit/CMakeLists.txt`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 CMake/CTest 구성을 꼼꼼히 읽고, 새 unit test가 자동 등록되는 방식과 일관성을 유지하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
solver skeleton의 공통 primitive를 `/src/fesa/core/` 아래 header-only로 구현한다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/src/fesa/core/ids.hpp`
|
||||||
|
- `/src/fesa/core/diagnostic.hpp`
|
||||||
|
- `/src/fesa/core/status.hpp`
|
||||||
|
- `/tests/unit/core_primitives_test.cpp`
|
||||||
|
|
||||||
|
필수 interface:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace fesa::core {
|
||||||
|
|
||||||
|
struct NodeId { int value; };
|
||||||
|
struct ElementId { int value; };
|
||||||
|
struct MaterialId { int value; };
|
||||||
|
struct PropertyId { int value; };
|
||||||
|
struct StepId { int value; };
|
||||||
|
|
||||||
|
enum class Severity { info, warning, error };
|
||||||
|
|
||||||
|
struct Diagnostic {
|
||||||
|
Severity severity;
|
||||||
|
std::string code;
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Status {
|
||||||
|
public:
|
||||||
|
static Status ok();
|
||||||
|
static Status failure(Diagnostic diagnostic);
|
||||||
|
|
||||||
|
bool is_ok() const;
|
||||||
|
const std::vector<Diagnostic>& diagnostics() const;
|
||||||
|
void add(Diagnostic diagnostic);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::core
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 규칙:
|
||||||
|
|
||||||
|
- 모든 ID type은 strong typedef 역할을 하며 서로 암시적으로 대체되지 않아야 한다.
|
||||||
|
- ID에는 equation id 또는 DOF numbering 정보를 넣지 않는다.
|
||||||
|
- `Status::ok()`는 diagnostics가 비어 있고 `is_ok() == true`여야 한다.
|
||||||
|
- `Status::failure(...)`는 최소 하나의 diagnostic을 포함하고 `is_ok() == false`여야 한다.
|
||||||
|
- 외부 라이브러리에 의존하지 않는다.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/core_primitives_test.cpp`
|
||||||
|
- `NodeId{1}`과 `ElementId{1}`이 서로 다른 타입임을 compile-time으로 확인한다.
|
||||||
|
- `Status::ok().is_ok()`가 true임을 확인한다.
|
||||||
|
- `Status::failure(...)`가 false이고 diagnostic code/message를 보존함을 확인한다.
|
||||||
|
- `Status::add(...)`로 warning을 추가해도 diagnostic 순서가 보존됨을 확인한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 테스트 파일을 먼저 작성한다.
|
||||||
|
2. `python scripts/validate_workspace.py` 또는 targeted CTest를 실행해 `fesa/core/ids.hpp` 등 missing include로 실패함을 확인한다.
|
||||||
|
3. 그 뒤 production header를 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R core_primitives_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- `core`가 외부 라이브러리에 의존하지 않는가?
|
||||||
|
- ID type에 equation numbering을 저장하지 않는가?
|
||||||
|
- C++ production header 변경에 대응하는 test file이 있는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 1을 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "Core ID, diagnostic, and status primitives added with tests"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- Node, Element, Domain 같은 model class를 이 step에서 만들지 마라.
|
||||||
|
- MKL, TBB, HDF5 API를 include하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# Step 2: domain-model-entities
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/src/fesa/core/ids.hpp`
|
||||||
|
- `/src/fesa/core/status.hpp`
|
||||||
|
- `/tests/unit/core_primitives_test.cpp`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 core primitive를 꼼꼼히 읽고, ID ownership과 diagnostic convention을 유지하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
입력 파일에서 생성된 전체 semantic model을 소유하는 최소 model layer를 `/src/fesa/model/`에 구현한다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/src/fesa/model/node.hpp`
|
||||||
|
- `/src/fesa/model/element.hpp`
|
||||||
|
- `/src/fesa/model/material.hpp`
|
||||||
|
- `/src/fesa/model/property.hpp`
|
||||||
|
- `/src/fesa/model/boundary_condition.hpp`
|
||||||
|
- `/src/fesa/model/load.hpp`
|
||||||
|
- `/src/fesa/model/analysis_step.hpp`
|
||||||
|
- `/src/fesa/model/domain.hpp`
|
||||||
|
- `/tests/unit/model_domain_test.cpp`
|
||||||
|
|
||||||
|
필수 interface:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
public:
|
||||||
|
Node(core::NodeId id, std::array<double, 3> coordinates);
|
||||||
|
core::NodeId id() const;
|
||||||
|
const std::array<double, 3>& coordinates() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ElementTopology { truss2, bar2, unknown };
|
||||||
|
|
||||||
|
class Element {
|
||||||
|
public:
|
||||||
|
Element(core::ElementId id, ElementTopology topology,
|
||||||
|
std::vector<core::NodeId> node_ids, core::PropertyId property_id);
|
||||||
|
core::ElementId id() const;
|
||||||
|
ElementTopology topology() const;
|
||||||
|
const std::vector<core::NodeId>& node_ids() const;
|
||||||
|
core::PropertyId property_id() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Material {
|
||||||
|
public:
|
||||||
|
Material(core::MaterialId id, std::string name);
|
||||||
|
core::MaterialId id() const;
|
||||||
|
const std::string& name() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Property {
|
||||||
|
public:
|
||||||
|
Property(core::PropertyId id, std::string name, core::MaterialId material_id);
|
||||||
|
core::PropertyId id() const;
|
||||||
|
const std::string& name() const;
|
||||||
|
core::MaterialId material_id() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DofComponent { ux, uy, uz, rx, ry, rz, temperature };
|
||||||
|
|
||||||
|
class BoundaryCondition {
|
||||||
|
public:
|
||||||
|
BoundaryCondition(core::NodeId node_id, DofComponent component, double value);
|
||||||
|
core::NodeId node_id() const;
|
||||||
|
DofComponent component() const;
|
||||||
|
double value() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Load {
|
||||||
|
public:
|
||||||
|
Load(core::NodeId node_id, DofComponent component, double value);
|
||||||
|
core::NodeId node_id() const;
|
||||||
|
DofComponent component() const;
|
||||||
|
double value() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AnalysisStep {
|
||||||
|
public:
|
||||||
|
AnalysisStep(core::StepId id, std::string name);
|
||||||
|
core::StepId id() const;
|
||||||
|
const std::string& name() const;
|
||||||
|
void add_boundary_condition(BoundaryCondition bc);
|
||||||
|
void add_load(Load load);
|
||||||
|
const std::vector<BoundaryCondition>& boundary_conditions() const;
|
||||||
|
const std::vector<Load>& loads() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Domain {
|
||||||
|
public:
|
||||||
|
void add_node(Node node);
|
||||||
|
void add_element(Element element);
|
||||||
|
void add_material(Material material);
|
||||||
|
void add_property(Property property);
|
||||||
|
void add_step(AnalysisStep step);
|
||||||
|
|
||||||
|
const std::vector<Node>& nodes() const;
|
||||||
|
const std::vector<Element>& elements() const;
|
||||||
|
const std::vector<Material>& materials() const;
|
||||||
|
const std::vector<Property>& properties() const;
|
||||||
|
const std::vector<AnalysisStep>& steps() const;
|
||||||
|
|
||||||
|
const Node* find_node(core::NodeId id) const;
|
||||||
|
const Element* find_element(core::ElementId id) const;
|
||||||
|
const Material* find_material(core::MaterialId id) const;
|
||||||
|
const Property* find_property(core::PropertyId id) const;
|
||||||
|
const AnalysisStep* find_step(core::StepId id) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 규칙:
|
||||||
|
|
||||||
|
- Domain은 semantic model 객체를 소유한다.
|
||||||
|
- `nodes()`, `elements()`, `materials()`, `properties()`, `steps()`는 const reference를 반환한다.
|
||||||
|
- `find_*`는 없으면 `nullptr`을 반환한다.
|
||||||
|
- Node와 Element 내부에 equation id, constrained/free equation mapping, sparse pattern 정보를 저장하지 않는다.
|
||||||
|
- `DofComponent`는 아직 equation number가 아니라 물리 DOF component만 표현한다.
|
||||||
|
- Abaqus keyword 문자열이나 parser detail을 model object에 저장하지 않는다.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/model_domain_test.cpp`
|
||||||
|
- Node가 id와 3D coordinates를 보존한다.
|
||||||
|
- Element가 topology, connectivity, property id를 보존한다.
|
||||||
|
- Material과 Property가 id/name/material link를 보존한다.
|
||||||
|
- AnalysisStep이 boundary condition과 load를 저장한다.
|
||||||
|
- Domain이 add/find를 통해 각 객체를 조회한다.
|
||||||
|
- 없는 id는 `nullptr`을 반환한다.
|
||||||
|
- Node/Element public interface에 equation id를 노출하지 않는다는 점을 테스트 코드 사용 방식으로 확인한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 테스트 파일을 먼저 작성한다.
|
||||||
|
2. targeted CTest를 실행해 missing model headers로 실패함을 확인한다.
|
||||||
|
3. 그 뒤 production headers를 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model_domain_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- Domain이 전체 모델 정의를 소유하는가?
|
||||||
|
- model layer가 parser keyword 문자열이나 analysis state를 소유하지 않는가?
|
||||||
|
- Node/Element에 equation id가 분산 저장되지 않는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 2를 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "Model entities and Domain ownership API added with tests"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- Parser, assembler, solver backend를 만들지 마라.
|
||||||
|
- `Domain`을 실행 중 state container로 사용하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Step 3: analysis-model-view
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/src/fesa/model/domain.hpp`
|
||||||
|
- `/src/fesa/model/analysis_step.hpp`
|
||||||
|
- `/tests/unit/model_domain_test.cpp`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 Domain과 AnalysisStep을 꼼꼼히 읽고, Domain 복사 없는 step view를 유지하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
현재 step에서 활성화되는 해석 객체 view를 제공하는 `AnalysisModel`을 `/src/fesa/analysis/`에 구현한다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/src/fesa/analysis/analysis_model.hpp`
|
||||||
|
- `/tests/unit/analysis_model_view_test.cpp`
|
||||||
|
|
||||||
|
필수 interface:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
class AnalysisModel {
|
||||||
|
public:
|
||||||
|
AnalysisModel(const model::Domain& domain, core::StepId step_id);
|
||||||
|
|
||||||
|
const model::Domain& domain() const;
|
||||||
|
const model::AnalysisStep& step() const;
|
||||||
|
const std::vector<const model::Element*>& active_elements() const;
|
||||||
|
const std::vector<const model::BoundaryCondition*>& active_boundary_conditions() const;
|
||||||
|
const std::vector<const model::Load*>& active_loads() const;
|
||||||
|
|
||||||
|
const model::Property* property_for(const model::Element& element) const;
|
||||||
|
const model::Material* material_for(const model::Property& property) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 규칙:
|
||||||
|
|
||||||
|
- `AnalysisModel`은 `Domain`을 복사하지 않고 const reference 또는 pointer view만 보유한다.
|
||||||
|
- Phase skeleton에서는 모든 Domain elements가 active라고 간주한다.
|
||||||
|
- Boundary condition과 load는 선택된 `AnalysisStep`에서 가져온다.
|
||||||
|
- `property_for`와 `material_for`는 Domain lookup을 사용한다.
|
||||||
|
- 없는 step id는 구조화된 exception 대신 `std::invalid_argument`로 실패해도 된다. 이 skeleton 단계에서는 별도 error hierarchy를 만들지 않는다.
|
||||||
|
- `AnalysisModel`은 displacement, residual, equation number를 소유하지 않는다.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/analysis_model_view_test.cpp`
|
||||||
|
- Domain에 node/element/material/property/step을 만든다.
|
||||||
|
- `AnalysisModel(domain, step_id)`가 원본 Domain reference를 유지함을 pointer identity로 확인한다.
|
||||||
|
- 모든 Domain element가 `active_elements()`에 const pointer로 나타난다.
|
||||||
|
- step의 boundary condition과 load가 active view에 const pointer로 나타난다.
|
||||||
|
- `property_for(element)`와 `material_for(property)`가 Domain 소유 객체를 반환한다.
|
||||||
|
- 없는 step id는 `std::invalid_argument`를 던진다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 테스트 파일을 먼저 작성한다.
|
||||||
|
2. targeted CTest를 실행해 missing `analysis_model.hpp`로 실패함을 확인한다.
|
||||||
|
3. 그 뒤 production header를 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis_model_view_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- AnalysisModel이 Domain을 복사하지 않는가?
|
||||||
|
- AnalysisModel이 현재 step view만 제공하고 mutable state를 소유하지 않는가?
|
||||||
|
- active BC/load가 AnalysisStep에서 온 것임이 테스트되는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 3을 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "AnalysisModel step view added without Domain copies"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- DofManager나 equation numbering을 이 step에서 구현하지 마라.
|
||||||
|
- AnalysisState를 이 step에서 구현하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Step 4: dof-manager
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/src/fesa/model/boundary_condition.hpp`
|
||||||
|
- `/src/fesa/analysis/analysis_model.hpp`
|
||||||
|
- `/tests/unit/analysis_model_view_test.cpp`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 model object와 AnalysisModel을 꼼꼼히 읽고, equation numbering이 Node/Element로 새지 않도록 유지하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
node별 DOF 정의, constrained/free mapping, equation numbering, sparse pattern ownership의 최소 골격을 `/src/fesa/fem/`에 구현한다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/src/fesa/fem/dof_key.hpp`
|
||||||
|
- `/src/fesa/fem/dof_manager.hpp`
|
||||||
|
- `/tests/unit/dof_manager_numbering_test.cpp`
|
||||||
|
|
||||||
|
필수 interface:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace fesa::fem {
|
||||||
|
|
||||||
|
struct DofKey {
|
||||||
|
core::NodeId node_id;
|
||||||
|
model::DofComponent component;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool operator==(const DofKey& lhs, const DofKey& rhs);
|
||||||
|
|
||||||
|
class DofManager {
|
||||||
|
public:
|
||||||
|
void define_node_dofs(core::NodeId node_id, std::vector<model::DofComponent> components);
|
||||||
|
void apply_boundary_condition(const model::BoundaryCondition& bc);
|
||||||
|
void number_equations();
|
||||||
|
|
||||||
|
int total_dof_count() const;
|
||||||
|
int free_dof_count() const;
|
||||||
|
int constrained_dof_count() const;
|
||||||
|
|
||||||
|
bool is_constrained(DofKey key) const;
|
||||||
|
int equation_id(DofKey key) const;
|
||||||
|
std::optional<int> free_equation_id(DofKey key) const;
|
||||||
|
|
||||||
|
std::vector<double> expand_free_vector(const std::vector<double>& free_values) const;
|
||||||
|
const std::vector<std::pair<int, int>>& sparse_pattern() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::fem
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 규칙:
|
||||||
|
|
||||||
|
- Equation id는 `DofManager` 내부에만 저장한다.
|
||||||
|
- `equation_id`는 전체 DOF ordering의 dense id를 반환한다.
|
||||||
|
- `free_equation_id`는 constrained DOF면 `std::nullopt`를 반환한다.
|
||||||
|
- `number_equations()`는 deterministic ordering을 보장한다:
|
||||||
|
- node id value 오름차순
|
||||||
|
- component enum 순서 오름차순
|
||||||
|
- `expand_free_vector`는 constrained DOF 값을 0.0으로 채우고 free DOF 값만 배치한다.
|
||||||
|
- `sparse_pattern()`은 skeleton 단계에서 free equation ids의 dense full matrix pattern을 deterministic pair list로 보유해도 된다.
|
||||||
|
- missing DOF 조회는 `std::invalid_argument`를 던진다.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/dof_manager_numbering_test.cpp`
|
||||||
|
- 두 node에 `ux`, `uy`를 정의하고 deterministic equation id ordering을 확인한다.
|
||||||
|
- boundary condition 적용 후 constrained/free count가 맞는지 확인한다.
|
||||||
|
- constrained key의 `free_equation_id`가 `std::nullopt`임을 확인한다.
|
||||||
|
- free vector가 full vector로 재구성될 때 constrained DOF가 0.0으로 채워지는지 확인한다.
|
||||||
|
- sparse pattern pair list가 free equation id 기반 deterministic dense pattern을 가진다.
|
||||||
|
- model::Node나 model::Element를 수정하지 않고도 equation numbering이 가능함을 확인한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 테스트 파일을 먼저 작성한다.
|
||||||
|
2. targeted CTest를 실행해 missing `dof_manager.hpp`로 실패함을 확인한다.
|
||||||
|
3. 그 뒤 production headers를 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R dof_manager_numbering_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- DofManager가 equation numbering을 전담하는가?
|
||||||
|
- Node/Element public interface가 equation id로 오염되지 않았는가?
|
||||||
|
- sparse pattern ownership이 DofManager 내부에 있는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 4를 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "DofManager deterministic numbering and constrained/free mapping added"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- Solver backend, assembly matrix, MKL adapter를 구현하지 마라.
|
||||||
|
- Node 또는 Element에 equation id field를 추가하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Step 5: analysis-state
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/src/fesa/fem/dof_manager.hpp`
|
||||||
|
- `/tests/unit/dof_manager_numbering_test.cpp`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 DofManager를 꼼꼼히 읽고, 해석 중 변하는 물리량은 AnalysisState가 소유하도록 유지하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
displacement 중심의 최소 `AnalysisState`를 `/src/fesa/analysis/`에 구현한다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/src/fesa/analysis/analysis_state.hpp`
|
||||||
|
- `/tests/unit/analysis_state_vectors_test.cpp`
|
||||||
|
|
||||||
|
필수 interface:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
struct IterationState {
|
||||||
|
double time = 0.0;
|
||||||
|
int increment = 0;
|
||||||
|
int iteration = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AnalysisState {
|
||||||
|
public:
|
||||||
|
explicit AnalysisState(int total_dof_count);
|
||||||
|
|
||||||
|
const std::vector<double>& displacement() const;
|
||||||
|
const std::vector<double>& velocity() const;
|
||||||
|
const std::vector<double>& acceleration() const;
|
||||||
|
const std::vector<double>& temperature() const;
|
||||||
|
const std::vector<double>& external_force() const;
|
||||||
|
const std::vector<double>& internal_force() const;
|
||||||
|
const std::vector<double>& residual() const;
|
||||||
|
|
||||||
|
void set_displacement(std::vector<double> values);
|
||||||
|
void set_external_force(std::vector<double> values);
|
||||||
|
void set_internal_force(std::vector<double> values);
|
||||||
|
void update_residual();
|
||||||
|
|
||||||
|
IterationState& iteration_state();
|
||||||
|
const IterationState& iteration_state() const;
|
||||||
|
|
||||||
|
void set_element_state(core::ElementId element_id, std::vector<double> state);
|
||||||
|
const std::vector<double>* element_state(core::ElementId element_id) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 규칙:
|
||||||
|
|
||||||
|
- 모든 vector는 `total_dof_count` 크기로 초기화한다.
|
||||||
|
- `update_residual()`은 `external_force - internal_force`를 component-wise로 계산한다.
|
||||||
|
- setter는 입력 vector 크기가 맞지 않으면 `std::invalid_argument`를 던진다.
|
||||||
|
- temperature는 Phase skeleton에서 0.0 vector로 둔다.
|
||||||
|
- element state는 향후 integration point state 확장을 위한 최소 map으로 둔다.
|
||||||
|
- AnalysisState는 Domain, AnalysisModel, DofManager를 소유하지 않는다.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/analysis_state_vectors_test.cpp`
|
||||||
|
- 생성 시 displacement/velocity/acceleration/temperature/force/residual vector 크기와 0.0 초기값을 확인한다.
|
||||||
|
- displacement setter가 값을 보존한다.
|
||||||
|
- external/internal force 설정 후 residual이 `Fext - Fint`가 되는지 확인한다.
|
||||||
|
- 크기가 맞지 않는 vector setter가 `std::invalid_argument`를 던진다.
|
||||||
|
- time/increment/iteration 값이 저장된다.
|
||||||
|
- element state를 element id로 저장/조회한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 테스트 파일을 먼저 작성한다.
|
||||||
|
2. targeted CTest를 실행해 missing `analysis_state.hpp`로 실패함을 확인한다.
|
||||||
|
3. 그 뒤 production header를 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis_state_vectors_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- AnalysisState가 해석 중 변하는 물리량을 소유하는가?
|
||||||
|
- Domain이나 AnalysisModel을 복사/소유하지 않는가?
|
||||||
|
- displacement 중심이되 velocity/acceleration/temperature 확장 지점을 유지하는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 5를 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "AnalysisState vector ownership and residual update added"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- Solver backend나 numerical integration loop를 구현하지 마라.
|
||||||
|
- HDF5 writer를 이 step에서 구현하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Step 6: analysis-template-flow
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/src/fesa/analysis/analysis_model.hpp`
|
||||||
|
- `/src/fesa/analysis/analysis_state.hpp`
|
||||||
|
- `/src/fesa/fem/dof_manager.hpp`
|
||||||
|
- `/tests/unit/analysis_state_vectors_test.cpp`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 AnalysisModel, DofManager, AnalysisState를 꼼꼼히 읽고, ARCHITECTURE.md의 Template Method 실행 흐름과 일관성을 유지하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
공통 해석 실행 흐름을 고정하는 `Analysis` base class와 선형 정적 해석 skeleton을 `/src/fesa/analysis/`에 구현한다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/src/fesa/analysis/analysis.hpp`
|
||||||
|
- `/src/fesa/analysis/linear_static_analysis.hpp`
|
||||||
|
- `/tests/unit/analysis_flow_template_test.cpp`
|
||||||
|
|
||||||
|
필수 interface:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
class Analysis {
|
||||||
|
public:
|
||||||
|
virtual ~Analysis() = default;
|
||||||
|
void run();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void initialize() {}
|
||||||
|
virtual void build_analysis_model() {}
|
||||||
|
virtual void build_dof_map() {}
|
||||||
|
virtual void build_sparse_pattern() {}
|
||||||
|
virtual void assemble() {}
|
||||||
|
virtual void apply_boundary_conditions() {}
|
||||||
|
virtual void solve() {}
|
||||||
|
virtual void update_state() {}
|
||||||
|
virtual void write_results() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class LinearStaticAnalysis : public Analysis {
|
||||||
|
public:
|
||||||
|
LinearStaticAnalysis(const model::Domain& domain, core::StepId step_id);
|
||||||
|
|
||||||
|
const AnalysisModel* analysis_model() const;
|
||||||
|
const AnalysisState* state() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void build_analysis_model() override;
|
||||||
|
void build_dof_map() override;
|
||||||
|
void update_state() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 규칙:
|
||||||
|
|
||||||
|
- `Analysis::run()`은 반드시 다음 순서로 hook을 호출한다:
|
||||||
|
`initialize -> build_analysis_model -> build_dof_map -> build_sparse_pattern -> assemble -> apply_boundary_conditions -> solve -> update_state -> write_results`
|
||||||
|
- `LinearStaticAnalysis`는 skeleton 단계에서 실제 stiffness assembly나 solve를 하지 않는다.
|
||||||
|
- `LinearStaticAnalysis::build_analysis_model()`은 `AnalysisModel`을 생성한다.
|
||||||
|
- `LinearStaticAnalysis::build_dof_map()`은 active element connectivity에서 등장한 node에 `ux`, `uy`, `uz`를 정의하고 active BC를 적용한 뒤 equation numbering을 수행한다.
|
||||||
|
- `LinearStaticAnalysis::update_state()`는 total DOF count 크기의 `AnalysisState`를 준비한다.
|
||||||
|
- MKL, TBB, HDF5 adapter는 만들지 않는다.
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/analysis_flow_template_test.cpp`
|
||||||
|
- test-only derived `RecordingAnalysis`가 hook 호출 순서를 vector에 기록한다.
|
||||||
|
- `run()` 호출 후 ARCHITECTURE.md 순서와 정확히 일치하는지 확인한다.
|
||||||
|
- 최소 Domain과 Step으로 `LinearStaticAnalysis`를 실행하면 `analysis_model()`과 `state()`가 null이 아니게 되는지 확인한다.
|
||||||
|
- `LinearStaticAnalysis`가 실제 solver 결과를 계산한다고 주장하지 않음을 테스트 이름과 assertion 범위에 반영한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 테스트 파일을 먼저 작성한다.
|
||||||
|
2. targeted CTest를 실행해 missing `analysis.hpp` 또는 `linear_static_analysis.hpp`로 실패함을 확인한다.
|
||||||
|
3. 그 뒤 production headers를 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis_flow_template_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- Analysis::run()이 Template Method 흐름을 고정하는가?
|
||||||
|
- LinearStaticAnalysis가 skeleton 범위를 넘어 solver backend를 구현하지 않는가?
|
||||||
|
- 외부 라이브러리 API가 solver core에 노출되지 않는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 6을 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "Analysis template method and LinearStaticAnalysis skeleton added"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- 실제 stiffness matrix assembly, linear solve, MKL PARDISO adapter를 구현하지 마라.
|
||||||
|
- HDF5 writer를 구현하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Step 7: results-containers
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/src/fesa/analysis/analysis_state.hpp`
|
||||||
|
- `/src/fesa/analysis/analysis.hpp`
|
||||||
|
- `/tests/unit/analysis_flow_template_test.cpp`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 AnalysisState와 Analysis flow를 꼼꼼히 읽고, 결과 container가 HDF5 API에 직접 의존하지 않도록 유지하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
HDF5 writer 구현 전 단계의 results data model skeleton을 `/src/fesa/results/`에 구현한다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/src/fesa/results/results.hpp`
|
||||||
|
- `/tests/unit/results_containers_test.cpp`
|
||||||
|
|
||||||
|
필수 interface:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace fesa::results {
|
||||||
|
|
||||||
|
enum class FieldLocation { nodal, element, integration_point };
|
||||||
|
|
||||||
|
struct FieldOutput {
|
||||||
|
std::string name;
|
||||||
|
FieldLocation location;
|
||||||
|
std::vector<std::string> components;
|
||||||
|
std::vector<int> entity_ids;
|
||||||
|
std::vector<double> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HistoryOutput {
|
||||||
|
std::string name;
|
||||||
|
std::vector<double> time;
|
||||||
|
std::vector<double> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResultFrame {
|
||||||
|
public:
|
||||||
|
ResultFrame(int frame_id, double time);
|
||||||
|
int frame_id() const;
|
||||||
|
double time() const;
|
||||||
|
void add_field_output(FieldOutput output);
|
||||||
|
void add_history_output(HistoryOutput output);
|
||||||
|
const std::vector<FieldOutput>& field_outputs() const;
|
||||||
|
const std::vector<HistoryOutput>& history_outputs() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResultStep {
|
||||||
|
public:
|
||||||
|
explicit ResultStep(std::string name);
|
||||||
|
const std::string& name() const;
|
||||||
|
ResultFrame& add_frame(int frame_id, double time);
|
||||||
|
const std::vector<ResultFrame>& frames() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::results
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 규칙:
|
||||||
|
|
||||||
|
- Result hierarchy는 `ResultStep -> ResultFrame -> FieldOutput/HistoryOutput` 구조를 따른다.
|
||||||
|
- Field output은 row identity를 위해 `entity_ids`를 보존한다.
|
||||||
|
- Field output values layout은 skeleton 단계에서 row-major flat vector로 둔다.
|
||||||
|
- HDF5 file/dataset, schema writer, CSV export는 구현하지 않는다.
|
||||||
|
- validation은 크기 consistency만 최소로 확인한다:
|
||||||
|
- `components`가 비어 있으면 `std::invalid_argument`
|
||||||
|
- `entity_ids.size() * components.size() == values.size()`가 아니면 `std::invalid_argument`
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/unit/results_containers_test.cpp`
|
||||||
|
- ResultStep이 이름과 frame 목록을 보존한다.
|
||||||
|
- ResultFrame이 frame id/time을 보존한다.
|
||||||
|
- nodal displacement FieldOutput을 추가하면 components/entity ids/values가 보존된다.
|
||||||
|
- invalid FieldOutput shape가 `std::invalid_argument`를 던진다.
|
||||||
|
- HistoryOutput이 time/value series를 보존한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 테스트 파일을 먼저 작성한다.
|
||||||
|
2. targeted CTest를 실행해 missing `results.hpp`로 실패함을 확인한다.
|
||||||
|
3. 그 뒤 production header를 작성한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R results_containers_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- Results hierarchy가 ARCHITECTURE.md와 일치하는가?
|
||||||
|
- HDF5 API가 results container에 직접 노출되지 않는가?
|
||||||
|
- reference comparison을 위한 entity row identity가 보존되는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 7을 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "ResultStep, ResultFrame, FieldOutput, and HistoryOutput containers added"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- HDF5 writer/reader를 구현하지 마라.
|
||||||
|
- deterministic CSV export를 구현하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Step 8: solver-skeleton-integration-report
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 이전 step 산출물을 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/PRD.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- `/src/fesa/model/domain.hpp`
|
||||||
|
- `/src/fesa/analysis/linear_static_analysis.hpp`
|
||||||
|
- `/src/fesa/results/results.hpp`
|
||||||
|
- `/tests/unit/results_containers_test.cpp`
|
||||||
|
- `/docs/build-test-reports/README.md`
|
||||||
|
|
||||||
|
이전 step에서 만들어진 전체 skeleton API를 꼼꼼히 읽고, integration smoke test가 실제 solver 수치해석 완료를 주장하지 않도록 범위를 제한하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
solver skeleton의 주요 class가 함께 컴파일되고 기본 data flow를 구성할 수 있음을 통합 테스트와 build/test report로 남긴다.
|
||||||
|
|
||||||
|
필수 파일:
|
||||||
|
|
||||||
|
- `/tests/integration/solver_core_skeleton_integration_test.cpp`
|
||||||
|
- `/docs/build-test-reports/solver-core-skeleton.md`
|
||||||
|
|
||||||
|
통합 테스트 요구사항:
|
||||||
|
|
||||||
|
- Domain에 두 개 Node, 하나 Element, Material, Property, AnalysisStep을 구성한다.
|
||||||
|
- AnalysisStep에는 최소 하나의 BoundaryCondition과 Load를 추가한다.
|
||||||
|
- `LinearStaticAnalysis`를 생성하고 `run()`을 호출한다.
|
||||||
|
- `analysis_model()`과 `state()`가 생성되는지 확인한다.
|
||||||
|
- `ResultStep`과 `ResultFrame`을 만들고 nodal displacement `FieldOutput`을 추가한다.
|
||||||
|
- 이 테스트는 stiffness assembly, linear solve, HDF5 write, reference comparison을 검증하지 않는다.
|
||||||
|
|
||||||
|
보고서 요구사항:
|
||||||
|
|
||||||
|
- `docs/build-test-reports/solver-core-skeleton.md`에 아래 항목을 기록한다.
|
||||||
|
- phase: solver-core-skeleton
|
||||||
|
- scope: C++ skeleton classes only
|
||||||
|
- commands run
|
||||||
|
- exit code summary
|
||||||
|
- CTest tests added
|
||||||
|
- known limitations
|
||||||
|
- known limitations에는 최소한 다음을 명시한다:
|
||||||
|
- 실제 element stiffness/residual/tangent 계산 없음
|
||||||
|
- 실제 linear solver backend 없음
|
||||||
|
- HDF5 writer 없음
|
||||||
|
- Abaqus parser 및 reference comparison 없음
|
||||||
|
|
||||||
|
## Tests To Write First
|
||||||
|
|
||||||
|
- `/tests/integration/solver_core_skeleton_integration_test.cpp`
|
||||||
|
- 전체 skeleton header를 include한다.
|
||||||
|
- 위 통합 테스트 요구사항을 `main()` assertion으로 검증한다.
|
||||||
|
|
||||||
|
RED 확인:
|
||||||
|
|
||||||
|
1. 통합 테스트 파일을 먼저 작성한다.
|
||||||
|
2. targeted CTest를 실행해 아직 integration test registration 또는 required API 문제로 실패하면 실패 내용을 확인한다.
|
||||||
|
3. Step 0의 CMake glob이 `tests/integration/*_test.cpp`를 자동 등록해야 한다. 등록되지 않는다면 현재 step에서 CMake 파일을 수정하지 말고 `blocked`로 표시하고 사용자에게 allowed_paths 확장을 요청한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s scripts -p "test_*.py"
|
||||||
|
python scripts/validate_workspace.py
|
||||||
|
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R solver_core_skeleton_integration_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. 위 AC 커맨드를 실행한다.
|
||||||
|
2. 아키텍처 체크리스트를 확인한다:
|
||||||
|
- Domain -> AnalysisModel -> DofManager/AnalysisState -> Results data flow가 컴파일되는가?
|
||||||
|
- 통합 테스트가 skeleton 범위를 넘어 수치 정확성을 주장하지 않는가?
|
||||||
|
- build/test report가 실제 실행 evidence와 known limitations를 기록하는가?
|
||||||
|
3. 결과에 따라 `phases/solver-core-skeleton/index.json`의 step 8을 업데이트한다:
|
||||||
|
- 성공: `"status": "completed"`, `"summary": "Solver skeleton integration test and build/test report added"`
|
||||||
|
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
|
||||||
|
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- 실제 FEM stiffness, residual, tangent, material law 계산을 구현하지 마라.
|
||||||
|
- Abaqus reference artifact를 생성, 수정, 복원하지 마라.
|
||||||
|
- HDF5 writer를 구현하지 마라.
|
||||||
|
- JavaScript/TypeScript/npm fallback을 추가하지 마라.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
HEADER_ROOT = ROOT / "src" / "fesa"
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_function_definition(prefix):
|
||||||
|
if "(" not in prefix or ")" not in prefix:
|
||||||
|
return False
|
||||||
|
|
||||||
|
stripped = prefix.strip()
|
||||||
|
control_prefixes = ("if ", "for ", "while ", "switch ", "catch ")
|
||||||
|
declaration_prefixes = ("namespace ", "class ", "struct ", "enum ")
|
||||||
|
return not stripped.startswith(control_prefixes + declaration_prefixes)
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderDeclarationOnlyTests(unittest.TestCase):
|
||||||
|
def test_solver_headers_do_not_contain_function_bodies(self):
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
for header in sorted(HEADER_ROOT.rglob("*.hpp")):
|
||||||
|
candidate = ""
|
||||||
|
for line_number, line in enumerate(header.read_text(encoding="utf-8").splitlines(), start=1):
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidate = f"{candidate} {stripped}".strip()
|
||||||
|
if "{" in stripped and _looks_like_function_definition(candidate.split("{", 1)[0]):
|
||||||
|
violations.append(f"{header.relative_to(ROOT)}:{line_number}: function body in header")
|
||||||
|
|
||||||
|
if re.search(r"\([^;{}]*\)\s*=\s*(default|delete)\s*;", stripped):
|
||||||
|
violations.append(f"{header.relative_to(ROOT)}:{line_number}: function definition in header")
|
||||||
|
|
||||||
|
if stripped.endswith(";") or stripped.endswith("}") or stripped.endswith(":"):
|
||||||
|
candidate = ""
|
||||||
|
|
||||||
|
self.assertEqual([], violations)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -30,14 +30,15 @@ class ValidateWorkspaceTests(unittest.TestCase):
|
|||||||
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
|
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
|
||||||
build_dir = root / "build" / "msvc-debug"
|
build_dir = root / "build" / "msvc-debug"
|
||||||
with patch.dict(os.environ, {}, clear=True):
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
self.assertEqual(
|
with patch.object(validate_workspace.shutil, "which", return_value="C:\\tools\\cmake.exe"):
|
||||||
validate_workspace.discover_commands(root),
|
self.assertEqual(
|
||||||
[
|
validate_workspace.discover_commands(root),
|
||||||
f'cmake -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64',
|
[
|
||||||
f'cmake --build "{build_dir}" --config Debug',
|
f'cmake -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64',
|
||||||
f'ctest --test-dir "{build_dir}" --output-on-failure -C Debug',
|
f'cmake --build "{build_dir}" --config Debug',
|
||||||
],
|
f'ctest --test-dir "{build_dir}" --output-on-failure -C Debug',
|
||||||
)
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_msvc_debug_configure_preset_is_preferred_when_present(self):
|
def test_msvc_debug_configure_preset_is_preferred_when_present(self):
|
||||||
validate_workspace = load_validate_workspace()
|
validate_workspace = load_validate_workspace()
|
||||||
@@ -60,14 +61,39 @@ class ValidateWorkspaceTests(unittest.TestCase):
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
with patch.dict(os.environ, {}, clear=True):
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
self.assertEqual(
|
with patch.object(validate_workspace.shutil, "which", return_value="C:\\tools\\cmake.exe"):
|
||||||
validate_workspace.discover_commands(root),
|
self.assertEqual(
|
||||||
[
|
validate_workspace.discover_commands(root),
|
||||||
"cmake --preset msvc-debug",
|
[
|
||||||
f'cmake --build "{root / "out" / "msvc-debug"}" --config Debug',
|
"cmake --preset msvc-debug",
|
||||||
f'ctest --test-dir "{root / "out" / "msvc-debug"}" --output-on-failure -C Debug',
|
f'cmake --build "{root / "out" / "msvc-debug"}" --config Debug',
|
||||||
],
|
f'ctest --test-dir "{root / "out" / "msvc-debug"}" --output-on-failure -C Debug',
|
||||||
)
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cmake_commands_use_common_install_path_when_tools_are_not_on_path(self):
|
||||||
|
validate_workspace = load_validate_workspace()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp) / "project"
|
||||||
|
root.mkdir()
|
||||||
|
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
|
||||||
|
common_bin = Path(tmp) / "CMake" / "bin"
|
||||||
|
common_bin.mkdir(parents=True)
|
||||||
|
(common_bin / "cmake.exe").write_text("", encoding="utf-8")
|
||||||
|
(common_bin / "ctest.exe").write_text("", encoding="utf-8")
|
||||||
|
build_dir = root / "build" / "msvc-debug"
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
|
||||||
|
with patch.object(validate_workspace.shutil, "which", return_value=None):
|
||||||
|
self.assertEqual(
|
||||||
|
validate_workspace.discover_commands(root),
|
||||||
|
[
|
||||||
|
f'"{common_bin / "cmake.exe"}" -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64',
|
||||||
|
f'"{common_bin / "cmake.exe"}" --build "{build_dir}" --config Debug',
|
||||||
|
f'"{common_bin / "ctest.exe"}" --test-dir "{build_dir}" --output-on-failure -C Debug',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_no_cmake_project_has_no_validation_commands(self):
|
def test_no_cmake_project_has_no_validation_commands(self):
|
||||||
validate_workspace = load_validate_workspace()
|
validate_workspace = load_validate_workspace()
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ def _cmake_config() -> tuple[str, str, str, Path]:
|
|||||||
return generator, platform, config, build_dir
|
return generator, platform, config, build_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_command(tool_name: str) -> str:
|
||||||
|
if shutil.which(tool_name) is not None:
|
||||||
|
return tool_name
|
||||||
|
|
||||||
|
exe = COMMON_CMAKE_BIN / f"{tool_name}.exe"
|
||||||
|
if exe.exists():
|
||||||
|
return f'"{exe}"'
|
||||||
|
|
||||||
|
return tool_name
|
||||||
|
|
||||||
|
|
||||||
def _read_presets(root: Path) -> dict:
|
def _read_presets(root: Path) -> dict:
|
||||||
presets_file = root / "CMakePresets.json"
|
presets_file = root / "CMakePresets.json"
|
||||||
if not presets_file.exists():
|
if not presets_file.exists():
|
||||||
@@ -53,13 +64,15 @@ def _preset_binary_dir(root: Path, preset: dict) -> Path:
|
|||||||
def load_preset_commands(root: Path) -> list[str]:
|
def load_preset_commands(root: Path) -> list[str]:
|
||||||
payload = _read_presets(root)
|
payload = _read_presets(root)
|
||||||
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
|
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
|
||||||
|
cmake = _tool_command("cmake")
|
||||||
|
ctest = _tool_command("ctest")
|
||||||
for preset in payload.get("configurePresets", []):
|
for preset in payload.get("configurePresets", []):
|
||||||
if isinstance(preset, dict) and preset.get("name") == PRESET_NAME:
|
if isinstance(preset, dict) and preset.get("name") == PRESET_NAME:
|
||||||
build_dir = _preset_binary_dir(root, preset)
|
build_dir = _preset_binary_dir(root, preset)
|
||||||
return [
|
return [
|
||||||
f"cmake --preset {PRESET_NAME}",
|
f"{cmake} --preset {PRESET_NAME}",
|
||||||
f'cmake --build "{build_dir}" --config {config}',
|
f'{cmake} --build "{build_dir}" --config {config}',
|
||||||
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
|
f'{ctest} --test-dir "{build_dir}" --output-on-failure -C {config}',
|
||||||
]
|
]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -71,10 +84,12 @@ def load_cmake_commands(root: Path) -> list[str]:
|
|||||||
generator, platform, config, build_dir = _cmake_config()
|
generator, platform, config, build_dir = _cmake_config()
|
||||||
if not build_dir.is_absolute():
|
if not build_dir.is_absolute():
|
||||||
build_dir = root / build_dir
|
build_dir = root / build_dir
|
||||||
|
cmake = _tool_command("cmake")
|
||||||
|
ctest = _tool_command("ctest")
|
||||||
return [
|
return [
|
||||||
f'cmake -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}',
|
f'{cmake} -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}',
|
||||||
f'cmake --build "{build_dir}" --config {config}',
|
f'{cmake} --build "{build_dir}" --config {config}',
|
||||||
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
|
f'{ctest} --test-dir "{build_dir}" --output-on-failure -C {config}',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#include <fesa/analysis/analysis.hpp>
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
Analysis::~Analysis() = default;
|
||||||
|
|
||||||
|
void Analysis::run()
|
||||||
|
{
|
||||||
|
initialize();
|
||||||
|
build_analysis_model();
|
||||||
|
build_dof_map();
|
||||||
|
build_sparse_pattern();
|
||||||
|
assemble();
|
||||||
|
apply_boundary_conditions();
|
||||||
|
solve();
|
||||||
|
update_state();
|
||||||
|
write_results();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Analysis::initialize() {}
|
||||||
|
void Analysis::build_analysis_model() {}
|
||||||
|
void Analysis::build_dof_map() {}
|
||||||
|
void Analysis::build_sparse_pattern() {}
|
||||||
|
void Analysis::assemble() {}
|
||||||
|
void Analysis::apply_boundary_conditions() {}
|
||||||
|
void Analysis::solve() {}
|
||||||
|
void Analysis::update_state() {}
|
||||||
|
void Analysis::write_results() {}
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
class Analysis {
|
||||||
|
public:
|
||||||
|
virtual ~Analysis();
|
||||||
|
|
||||||
|
void run();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void initialize();
|
||||||
|
virtual void build_analysis_model();
|
||||||
|
virtual void build_dof_map();
|
||||||
|
virtual void build_sparse_pattern();
|
||||||
|
virtual void assemble();
|
||||||
|
virtual void apply_boundary_conditions();
|
||||||
|
virtual void solve();
|
||||||
|
virtual void update_state();
|
||||||
|
virtual void write_results();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#include <fesa/analysis/analysis_model.hpp>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
AnalysisModel::AnalysisModel(const model::Domain& domain, core::StepId step_id)
|
||||||
|
: domain_(domain), step_(domain.find_step(step_id))
|
||||||
|
{
|
||||||
|
if (step_ == nullptr) {
|
||||||
|
throw std::invalid_argument("analysis step not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& element : domain_.elements()) {
|
||||||
|
active_elements_.push_back(&element);
|
||||||
|
}
|
||||||
|
for (const auto& boundary_condition : step_->boundary_conditions()) {
|
||||||
|
active_boundary_conditions_.push_back(&boundary_condition);
|
||||||
|
}
|
||||||
|
for (const auto& load : step_->loads()) {
|
||||||
|
active_loads_.push_back(&load);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const model::Domain& AnalysisModel::domain() const
|
||||||
|
{
|
||||||
|
return domain_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model::AnalysisStep& AnalysisModel::step() const
|
||||||
|
{
|
||||||
|
return *step_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<const model::Element*>& AnalysisModel::active_elements() const
|
||||||
|
{
|
||||||
|
return active_elements_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<const model::BoundaryCondition*>& AnalysisModel::active_boundary_conditions() const
|
||||||
|
{
|
||||||
|
return active_boundary_conditions_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<const model::Load*>& AnalysisModel::active_loads() const
|
||||||
|
{
|
||||||
|
return active_loads_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model::Property* AnalysisModel::property_for(const model::Element& element) const
|
||||||
|
{
|
||||||
|
return domain_.find_property(element.property_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
const model::Material* AnalysisModel::material_for(const model::Property& property) const
|
||||||
|
{
|
||||||
|
return domain_.find_material(property.material_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
class AnalysisModel {
|
||||||
|
public:
|
||||||
|
AnalysisModel(const model::Domain& domain, core::StepId step_id);
|
||||||
|
|
||||||
|
const model::Domain& domain() const;
|
||||||
|
const model::AnalysisStep& step() const;
|
||||||
|
const std::vector<const model::Element*>& active_elements() const;
|
||||||
|
const std::vector<const model::BoundaryCondition*>& active_boundary_conditions() const;
|
||||||
|
const std::vector<const model::Load*>& active_loads() const;
|
||||||
|
const model::Property* property_for(const model::Element& element) const;
|
||||||
|
const model::Material* material_for(const model::Property& property) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const model::Domain& domain_;
|
||||||
|
const model::AnalysisStep* step_;
|
||||||
|
std::vector<const model::Element*> active_elements_;
|
||||||
|
std::vector<const model::BoundaryCondition*> active_boundary_conditions_;
|
||||||
|
std::vector<const model::Load*> active_loads_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#include <fesa/analysis/analysis_state.hpp>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
AnalysisState::AnalysisState(int total_dof_count)
|
||||||
|
: displacement_(vector_of(total_dof_count)),
|
||||||
|
velocity_(vector_of(total_dof_count)),
|
||||||
|
acceleration_(vector_of(total_dof_count)),
|
||||||
|
temperature_(vector_of(total_dof_count)),
|
||||||
|
external_force_(vector_of(total_dof_count)),
|
||||||
|
internal_force_(vector_of(total_dof_count)),
|
||||||
|
residual_(vector_of(total_dof_count))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& AnalysisState::displacement() const
|
||||||
|
{
|
||||||
|
return displacement_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& AnalysisState::velocity() const
|
||||||
|
{
|
||||||
|
return velocity_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& AnalysisState::acceleration() const
|
||||||
|
{
|
||||||
|
return acceleration_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& AnalysisState::temperature() const
|
||||||
|
{
|
||||||
|
return temperature_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& AnalysisState::external_force() const
|
||||||
|
{
|
||||||
|
return external_force_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& AnalysisState::internal_force() const
|
||||||
|
{
|
||||||
|
return internal_force_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& AnalysisState::residual() const
|
||||||
|
{
|
||||||
|
return residual_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisState::set_displacement(std::vector<double> values)
|
||||||
|
{
|
||||||
|
assign_same_size(displacement_, std::move(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisState::set_external_force(std::vector<double> values)
|
||||||
|
{
|
||||||
|
assign_same_size(external_force_, std::move(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisState::set_internal_force(std::vector<double> values)
|
||||||
|
{
|
||||||
|
assign_same_size(internal_force_, std::move(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisState::update_residual()
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < residual_.size(); ++index) {
|
||||||
|
residual_[index] = external_force_[index] - internal_force_[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IterationState& AnalysisState::iteration_state()
|
||||||
|
{
|
||||||
|
return iteration_state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IterationState& AnalysisState::iteration_state() const
|
||||||
|
{
|
||||||
|
return iteration_state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisState::set_element_state(core::ElementId element_id, std::vector<double> state)
|
||||||
|
{
|
||||||
|
for (auto& entry : element_states_) {
|
||||||
|
if (entry.first.value == element_id.value) {
|
||||||
|
entry.second = std::move(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element_states_.push_back({element_id, std::move(state)});
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>* AnalysisState::element_state(core::ElementId element_id) const
|
||||||
|
{
|
||||||
|
for (const auto& entry : element_states_) {
|
||||||
|
if (entry.first.value == element_id.value) {
|
||||||
|
return &entry.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> AnalysisState::vector_of(int size)
|
||||||
|
{
|
||||||
|
if (size < 0) {
|
||||||
|
throw std::invalid_argument("negative dof count");
|
||||||
|
}
|
||||||
|
return std::vector<double>(static_cast<std::size_t>(size), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisState::assign_same_size(std::vector<double>& target, std::vector<double> values)
|
||||||
|
{
|
||||||
|
if (target.size() != values.size()) {
|
||||||
|
throw std::invalid_argument("vector size mismatch");
|
||||||
|
}
|
||||||
|
target = std::move(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
struct IterationState {
|
||||||
|
double time = 0.0;
|
||||||
|
int increment = 0;
|
||||||
|
int iteration = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AnalysisState {
|
||||||
|
public:
|
||||||
|
explicit AnalysisState(int total_dof_count);
|
||||||
|
|
||||||
|
const std::vector<double>& displacement() const;
|
||||||
|
const std::vector<double>& velocity() const;
|
||||||
|
const std::vector<double>& acceleration() const;
|
||||||
|
const std::vector<double>& temperature() const;
|
||||||
|
const std::vector<double>& external_force() const;
|
||||||
|
const std::vector<double>& internal_force() const;
|
||||||
|
const std::vector<double>& residual() const;
|
||||||
|
|
||||||
|
void set_displacement(std::vector<double> values);
|
||||||
|
void set_external_force(std::vector<double> values);
|
||||||
|
void set_internal_force(std::vector<double> values);
|
||||||
|
void update_residual();
|
||||||
|
|
||||||
|
IterationState& iteration_state();
|
||||||
|
const IterationState& iteration_state() const;
|
||||||
|
|
||||||
|
void set_element_state(core::ElementId element_id, std::vector<double> state);
|
||||||
|
const std::vector<double>* element_state(core::ElementId element_id) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::vector<double> vector_of(int size);
|
||||||
|
static void assign_same_size(std::vector<double>& target, std::vector<double> values);
|
||||||
|
|
||||||
|
std::vector<double> displacement_;
|
||||||
|
std::vector<double> velocity_;
|
||||||
|
std::vector<double> acceleration_;
|
||||||
|
std::vector<double> temperature_;
|
||||||
|
std::vector<double> external_force_;
|
||||||
|
std::vector<double> internal_force_;
|
||||||
|
std::vector<double> residual_;
|
||||||
|
IterationState iteration_state_;
|
||||||
|
std::vector<std::pair<core::ElementId, std::vector<double>>> element_states_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include <fesa/analysis/linear_static_analysis.hpp>
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
LinearStaticAnalysis::LinearStaticAnalysis(const model::Domain& domain, core::StepId step_id)
|
||||||
|
: domain_(domain), step_id_(step_id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalysisModel* LinearStaticAnalysis::analysis_model() const
|
||||||
|
{
|
||||||
|
return analysis_model_.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalysisState* LinearStaticAnalysis::state() const
|
||||||
|
{
|
||||||
|
return state_.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinearStaticAnalysis::build_analysis_model()
|
||||||
|
{
|
||||||
|
analysis_model_ = std::make_unique<AnalysisModel>(domain_, step_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinearStaticAnalysis::build_dof_map()
|
||||||
|
{
|
||||||
|
dof_manager_ = std::make_unique<fem::DofManager>();
|
||||||
|
for (const auto* element : analysis_model_->active_elements()) {
|
||||||
|
for (const auto node_id : element->node_ids()) {
|
||||||
|
dof_manager_->define_node_dofs(node_id, {
|
||||||
|
model::DofComponent::ux,
|
||||||
|
model::DofComponent::uy,
|
||||||
|
model::DofComponent::uz
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto* boundary_condition : analysis_model_->active_boundary_conditions()) {
|
||||||
|
dof_manager_->apply_boundary_condition(*boundary_condition);
|
||||||
|
}
|
||||||
|
dof_manager_->number_equations();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinearStaticAnalysis::update_state()
|
||||||
|
{
|
||||||
|
state_ = std::make_unique<AnalysisState>(dof_manager_->total_dof_count());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/analysis/analysis.hpp>
|
||||||
|
#include <fesa/analysis/analysis_model.hpp>
|
||||||
|
#include <fesa/analysis/analysis_state.hpp>
|
||||||
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace fesa::analysis {
|
||||||
|
|
||||||
|
class LinearStaticAnalysis : public Analysis {
|
||||||
|
public:
|
||||||
|
LinearStaticAnalysis(const model::Domain& domain, core::StepId step_id);
|
||||||
|
|
||||||
|
const AnalysisModel* analysis_model() const;
|
||||||
|
const AnalysisState* state() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void build_analysis_model() override;
|
||||||
|
void build_dof_map() override;
|
||||||
|
void update_state() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const model::Domain& domain_;
|
||||||
|
core::StepId step_id_;
|
||||||
|
std::unique_ptr<AnalysisModel> analysis_model_;
|
||||||
|
std::unique_ptr<fem::DofManager> dof_manager_;
|
||||||
|
std::unique_ptr<AnalysisState> state_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::analysis
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fesa::core {
|
||||||
|
|
||||||
|
enum class Severity {
|
||||||
|
info,
|
||||||
|
warning,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Diagnostic {
|
||||||
|
Severity severity;
|
||||||
|
std::string code;
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::core
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace fesa::core {
|
||||||
|
|
||||||
|
struct NodeId {
|
||||||
|
int value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ElementId {
|
||||||
|
int value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MaterialId {
|
||||||
|
int value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PropertyId {
|
||||||
|
int value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StepId {
|
||||||
|
int value;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::core
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#include <fesa/core/status.hpp>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::core {
|
||||||
|
|
||||||
|
Status Status::ok()
|
||||||
|
{
|
||||||
|
return Status{};
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Status::failure(Diagnostic diagnostic)
|
||||||
|
{
|
||||||
|
Status status;
|
||||||
|
status.add(std::move(diagnostic));
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Status::is_ok() const
|
||||||
|
{
|
||||||
|
return diagnostics_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Diagnostic>& Status::diagnostics() const
|
||||||
|
{
|
||||||
|
return diagnostics_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Status::add(Diagnostic diagnostic)
|
||||||
|
{
|
||||||
|
diagnostics_.push_back(std::move(diagnostic));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::core
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::core {
|
||||||
|
|
||||||
|
class Status {
|
||||||
|
public:
|
||||||
|
static Status ok();
|
||||||
|
static Status failure(Diagnostic diagnostic);
|
||||||
|
|
||||||
|
bool is_ok() const;
|
||||||
|
const std::vector<Diagnostic>& diagnostics() const;
|
||||||
|
void add(Diagnostic diagnostic);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Diagnostic> diagnostics_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::core
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#include <fesa/fem/dof_key.hpp>
|
||||||
|
|
||||||
|
namespace fesa::fem {
|
||||||
|
|
||||||
|
bool operator==(const DofKey& lhs, const DofKey& rhs)
|
||||||
|
{
|
||||||
|
return lhs.node_id.value == rhs.node_id.value && lhs.component == rhs.component;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::fem
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
#include <fesa/model/boundary_condition.hpp>
|
||||||
|
|
||||||
|
namespace fesa::fem {
|
||||||
|
|
||||||
|
struct DofKey {
|
||||||
|
core::NodeId node_id;
|
||||||
|
model::DofComponent component;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool operator==(const DofKey& lhs, const DofKey& rhs);
|
||||||
|
|
||||||
|
} // namespace fesa::fem
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace fesa::fem {
|
||||||
|
|
||||||
|
void DofManager::define_node_dofs(core::NodeId node_id, std::vector<model::DofComponent> components)
|
||||||
|
{
|
||||||
|
for (const auto component : components) {
|
||||||
|
DofKey key{node_id, component};
|
||||||
|
if (find_record(key) == records_.end()) {
|
||||||
|
records_.push_back({key});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DofManager::apply_boundary_condition(const model::BoundaryCondition& bc)
|
||||||
|
{
|
||||||
|
auto record = find_record({bc.node_id(), bc.component()});
|
||||||
|
if (record == records_.end()) {
|
||||||
|
throw std::invalid_argument("boundary condition references undefined dof");
|
||||||
|
}
|
||||||
|
record->constrained = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DofManager::number_equations()
|
||||||
|
{
|
||||||
|
std::sort(records_.begin(), records_.end(), [](const Record& lhs, const Record& rhs) {
|
||||||
|
if (lhs.key.node_id.value != rhs.key.node_id.value) {
|
||||||
|
return lhs.key.node_id.value < rhs.key.node_id.value;
|
||||||
|
}
|
||||||
|
return static_cast<int>(lhs.key.component) < static_cast<int>(rhs.key.component);
|
||||||
|
});
|
||||||
|
|
||||||
|
int free_id = 0;
|
||||||
|
for (int equation_id = 0; equation_id < static_cast<int>(records_.size()); ++equation_id) {
|
||||||
|
records_[equation_id].equation_id = equation_id;
|
||||||
|
if (records_[equation_id].constrained) {
|
||||||
|
records_[equation_id].free_equation_id = std::nullopt;
|
||||||
|
} else {
|
||||||
|
records_[equation_id].free_equation_id = free_id++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sparse_pattern_.clear();
|
||||||
|
for (int row = 0; row < free_id; ++row) {
|
||||||
|
for (int column = 0; column < free_id; ++column) {
|
||||||
|
sparse_pattern_.push_back({row, column});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int DofManager::total_dof_count() const
|
||||||
|
{
|
||||||
|
return static_cast<int>(records_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int DofManager::free_dof_count() const
|
||||||
|
{
|
||||||
|
return static_cast<int>(
|
||||||
|
std::count_if(records_.begin(), records_.end(), [](const Record& record) {
|
||||||
|
return !record.constrained;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int DofManager::constrained_dof_count() const
|
||||||
|
{
|
||||||
|
return total_dof_count() - free_dof_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DofManager::is_constrained(DofKey key) const
|
||||||
|
{
|
||||||
|
return require_record(key).constrained;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DofManager::equation_id(DofKey key) const
|
||||||
|
{
|
||||||
|
return require_record(key).equation_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> DofManager::free_equation_id(DofKey key) const
|
||||||
|
{
|
||||||
|
return require_record(key).free_equation_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> DofManager::expand_free_vector(const std::vector<double>& free_values) const
|
||||||
|
{
|
||||||
|
if (free_values.size() != static_cast<std::size_t>(free_dof_count())) {
|
||||||
|
throw std::invalid_argument("free vector size does not match dof manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> full(records_.size(), 0.0);
|
||||||
|
for (const auto& record : records_) {
|
||||||
|
if (record.free_equation_id.has_value()) {
|
||||||
|
full[static_cast<std::size_t>(record.equation_id)] =
|
||||||
|
free_values[static_cast<std::size_t>(*record.free_equation_id)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::pair<int, int>>& DofManager::sparse_pattern() const
|
||||||
|
{
|
||||||
|
return sparse_pattern_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DofManager::Record>::iterator DofManager::find_record(DofKey key)
|
||||||
|
{
|
||||||
|
return std::find_if(records_.begin(), records_.end(), [key](const Record& record) {
|
||||||
|
return record.key == key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DofManager::Record>::const_iterator DofManager::find_record(DofKey key) const
|
||||||
|
{
|
||||||
|
return std::find_if(records_.begin(), records_.end(), [key](const Record& record) {
|
||||||
|
return record.key == key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const DofManager::Record& DofManager::require_record(DofKey key) const
|
||||||
|
{
|
||||||
|
const auto record = find_record(key);
|
||||||
|
if (record == records_.end()) {
|
||||||
|
throw std::invalid_argument("dof is not defined");
|
||||||
|
}
|
||||||
|
return *record;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::fem
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/fem/dof_key.hpp>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::fem {
|
||||||
|
|
||||||
|
class DofManager {
|
||||||
|
public:
|
||||||
|
void define_node_dofs(core::NodeId node_id, std::vector<model::DofComponent> components);
|
||||||
|
void apply_boundary_condition(const model::BoundaryCondition& bc);
|
||||||
|
void number_equations();
|
||||||
|
|
||||||
|
int total_dof_count() const;
|
||||||
|
int free_dof_count() const;
|
||||||
|
int constrained_dof_count() const;
|
||||||
|
bool is_constrained(DofKey key) const;
|
||||||
|
int equation_id(DofKey key) const;
|
||||||
|
std::optional<int> free_equation_id(DofKey key) const;
|
||||||
|
std::vector<double> expand_free_vector(const std::vector<double>& free_values) const;
|
||||||
|
const std::vector<std::pair<int, int>>& sparse_pattern() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Record {
|
||||||
|
DofKey key;
|
||||||
|
bool constrained = false;
|
||||||
|
int equation_id = -1;
|
||||||
|
std::optional<int> free_equation_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Record>::iterator find_record(DofKey key);
|
||||||
|
std::vector<Record>::const_iterator find_record(DofKey key) const;
|
||||||
|
const Record& require_record(DofKey key) const;
|
||||||
|
|
||||||
|
std::vector<Record> records_;
|
||||||
|
std::vector<std::pair<int, int>> sparse_pattern_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::fem
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
#include <fesa/io/abaqus/input_parser.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::io::abaqus {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class Section {
|
||||||
|
none,
|
||||||
|
heading,
|
||||||
|
node,
|
||||||
|
element
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ElementSection {
|
||||||
|
model::ElementTopology topology = model::ElementTopology::unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string trim(std::string_view text)
|
||||||
|
{
|
||||||
|
auto first = text.begin();
|
||||||
|
auto last = text.end();
|
||||||
|
while (first != last && std::isspace(static_cast<unsigned char>(*first)) != 0) {
|
||||||
|
++first;
|
||||||
|
}
|
||||||
|
while (first != last && std::isspace(static_cast<unsigned char>(*(last - 1))) != 0) {
|
||||||
|
--last;
|
||||||
|
}
|
||||||
|
return std::string(first, last);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lower(std::string text)
|
||||||
|
{
|
||||||
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
|
||||||
|
return static_cast<char>(std::tolower(ch));
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> split_commas(std::string_view line)
|
||||||
|
{
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
std::size_t start = 0;
|
||||||
|
while (start <= line.size()) {
|
||||||
|
const auto comma = line.find(',', start);
|
||||||
|
const auto end = comma == std::string_view::npos ? line.size() : comma;
|
||||||
|
fields.push_back(trim(line.substr(start, end - start)));
|
||||||
|
if (comma == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start = comma + 1;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_int(const std::string& text, int& value)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
std::size_t parsed = 0;
|
||||||
|
value = std::stoi(text, &parsed);
|
||||||
|
return parsed == text.size();
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
return false;
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_double(const std::string& text, double& value)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
std::size_t parsed = 0;
|
||||||
|
value = std::stod(text, &parsed);
|
||||||
|
return parsed == text.size();
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
return false;
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model::ElementTopology topology_for_type(const std::string& raw_type)
|
||||||
|
{
|
||||||
|
const auto type = lower(raw_type);
|
||||||
|
if (type == "t2d2" || type == "t3d2" || type == "c3d2") {
|
||||||
|
return model::ElementTopology::truss2;
|
||||||
|
}
|
||||||
|
if (type == "b31") {
|
||||||
|
return model::ElementTopology::bar2;
|
||||||
|
}
|
||||||
|
return model::ElementTopology::unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string parameter_value(const std::vector<std::string>& fields, const std::string& key)
|
||||||
|
{
|
||||||
|
const auto expected_key = lower(key);
|
||||||
|
for (std::size_t i = 1; i < fields.size(); ++i) {
|
||||||
|
const auto equals = fields[i].find('=');
|
||||||
|
if (equals == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto field_key = lower(trim(std::string_view{fields[i]}.substr(0, equals)));
|
||||||
|
if (field_key == expected_key) {
|
||||||
|
return trim(std::string_view{fields[i]}.substr(equals + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_node_line(model::Domain& domain, const std::vector<std::string>& fields)
|
||||||
|
{
|
||||||
|
if (fields.size() != 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
std::array<double, 3> coordinates{};
|
||||||
|
if (!parse_int(fields[0], id) ||
|
||||||
|
!parse_double(fields[1], coordinates[0]) ||
|
||||||
|
!parse_double(fields[2], coordinates[1]) ||
|
||||||
|
!parse_double(fields[3], coordinates[2])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.add_node(model::Node{core::NodeId{id}, coordinates});
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_element_line(model::Domain& domain,
|
||||||
|
const ElementSection& element_section,
|
||||||
|
const std::vector<std::string>& fields)
|
||||||
|
{
|
||||||
|
if (element_section.topology == model::ElementTopology::unknown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fields.size() != 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
int first_node = 0;
|
||||||
|
int second_node = 0;
|
||||||
|
if (!parse_int(fields[0], id) || !parse_int(fields[1], first_node) || !parse_int(fields[2], second_node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.add_element(model::Element{
|
||||||
|
core::ElementId{id},
|
||||||
|
element_section.topology,
|
||||||
|
{core::NodeId{first_node}, core::NodeId{second_node}},
|
||||||
|
core::PropertyId{0}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ParseResult InputParser::parse(std::string_view input) const
|
||||||
|
{
|
||||||
|
ParseResult result{model::Domain{}, core::Status::ok()};
|
||||||
|
Section section = Section::none;
|
||||||
|
ElementSection element_section{};
|
||||||
|
std::size_t start = 0;
|
||||||
|
|
||||||
|
while (start <= input.size()) {
|
||||||
|
const auto newline = input.find('\n', start);
|
||||||
|
const auto end = newline == std::string_view::npos ? input.size() : newline;
|
||||||
|
auto line = input.substr(start, end - start);
|
||||||
|
if (!line.empty() && line.back() == '\r') {
|
||||||
|
line.remove_suffix(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto cleaned = trim(line);
|
||||||
|
if (cleaned.empty() || cleaned.rfind("**", 0) == 0) {
|
||||||
|
if (newline == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start = newline + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaned.front() == '*') {
|
||||||
|
const auto keyword_fields = split_commas(std::string_view{cleaned}.substr(1));
|
||||||
|
const auto keyword = keyword_fields.empty() ? std::string{} : lower(keyword_fields[0]);
|
||||||
|
if (keyword == "heading") {
|
||||||
|
section = Section::heading;
|
||||||
|
} else if (keyword == "node") {
|
||||||
|
section = Section::node;
|
||||||
|
} else if (keyword == "element") {
|
||||||
|
section = Section::element;
|
||||||
|
const auto type = parameter_value(keyword_fields, "type");
|
||||||
|
element_section.topology = topology_for_type(type);
|
||||||
|
} else {
|
||||||
|
section = Section::none;
|
||||||
|
}
|
||||||
|
} else if (section == Section::node) {
|
||||||
|
parse_node_line(result.domain, split_commas(cleaned));
|
||||||
|
} else if (section == Section::element) {
|
||||||
|
parse_element_line(result.domain, element_section, split_commas(cleaned));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newline == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start = newline + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::io::abaqus
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/status.hpp>
|
||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace fesa::io::abaqus {
|
||||||
|
|
||||||
|
struct ParseResult {
|
||||||
|
model::Domain domain;
|
||||||
|
core::Status status;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputParser {
|
||||||
|
public:
|
||||||
|
ParseResult parse(std::string_view input) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::io::abaqus
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include <fesa/model/analysis_step.hpp>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
AnalysisStep::AnalysisStep(core::StepId id, std::string name)
|
||||||
|
: id_(id), name_(std::move(name))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
core::StepId AnalysisStep::id() const
|
||||||
|
{
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& AnalysisStep::name() const
|
||||||
|
{
|
||||||
|
return name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisStep::add_boundary_condition(BoundaryCondition bc)
|
||||||
|
{
|
||||||
|
boundary_conditions_.push_back(std::move(bc));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalysisStep::add_load(Load load)
|
||||||
|
{
|
||||||
|
loads_.push_back(std::move(load));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<BoundaryCondition>& AnalysisStep::boundary_conditions() const
|
||||||
|
{
|
||||||
|
return boundary_conditions_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Load>& AnalysisStep::loads() const
|
||||||
|
{
|
||||||
|
return loads_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/model/boundary_condition.hpp>
|
||||||
|
#include <fesa/model/load.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
class AnalysisStep {
|
||||||
|
public:
|
||||||
|
AnalysisStep(core::StepId id, std::string name);
|
||||||
|
|
||||||
|
core::StepId id() const;
|
||||||
|
const std::string& name() const;
|
||||||
|
void add_boundary_condition(BoundaryCondition bc);
|
||||||
|
void add_load(Load load);
|
||||||
|
const std::vector<BoundaryCondition>& boundary_conditions() const;
|
||||||
|
const std::vector<Load>& loads() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::StepId id_;
|
||||||
|
std::string name_;
|
||||||
|
std::vector<BoundaryCondition> boundary_conditions_;
|
||||||
|
std::vector<Load> loads_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#include <fesa/model/boundary_condition.hpp>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
BoundaryCondition::BoundaryCondition(core::NodeId node_id, DofComponent component, double value)
|
||||||
|
: node_id_(node_id), component_(component), value_(value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
core::NodeId BoundaryCondition::node_id() const
|
||||||
|
{
|
||||||
|
return node_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
DofComponent BoundaryCondition::component() const
|
||||||
|
{
|
||||||
|
return component_;
|
||||||
|
}
|
||||||
|
|
||||||
|
double BoundaryCondition::value() const
|
||||||
|
{
|
||||||
|
return value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
enum class DofComponent {
|
||||||
|
ux,
|
||||||
|
uy,
|
||||||
|
uz,
|
||||||
|
rx,
|
||||||
|
ry,
|
||||||
|
rz,
|
||||||
|
temperature
|
||||||
|
};
|
||||||
|
|
||||||
|
class BoundaryCondition {
|
||||||
|
public:
|
||||||
|
BoundaryCondition(core::NodeId node_id, DofComponent component, double value);
|
||||||
|
|
||||||
|
core::NodeId node_id() const;
|
||||||
|
DofComponent component() const;
|
||||||
|
double value() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::NodeId node_id_;
|
||||||
|
DofComponent component_;
|
||||||
|
double value_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
void Domain::add_node(Node node)
|
||||||
|
{
|
||||||
|
nodes_.push_back(std::move(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Domain::add_element(Element element)
|
||||||
|
{
|
||||||
|
elements_.push_back(std::move(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Domain::add_material(Material material)
|
||||||
|
{
|
||||||
|
materials_.push_back(std::move(material));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Domain::add_property(Property property)
|
||||||
|
{
|
||||||
|
properties_.push_back(std::move(property));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Domain::add_step(AnalysisStep step)
|
||||||
|
{
|
||||||
|
steps_.push_back(std::move(step));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Node>& Domain::nodes() const
|
||||||
|
{
|
||||||
|
return nodes_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Element>& Domain::elements() const
|
||||||
|
{
|
||||||
|
return elements_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Material>& Domain::materials() const
|
||||||
|
{
|
||||||
|
return materials_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Property>& Domain::properties() const
|
||||||
|
{
|
||||||
|
return properties_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<AnalysisStep>& Domain::steps() const
|
||||||
|
{
|
||||||
|
return steps_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node* Domain::find_node(core::NodeId id) const
|
||||||
|
{
|
||||||
|
for (const auto& node : nodes_) {
|
||||||
|
if (node.id().value == id.value) {
|
||||||
|
return &node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Element* Domain::find_element(core::ElementId id) const
|
||||||
|
{
|
||||||
|
for (const auto& element : elements_) {
|
||||||
|
if (element.id().value == id.value) {
|
||||||
|
return &element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Material* Domain::find_material(core::MaterialId id) const
|
||||||
|
{
|
||||||
|
for (const auto& material : materials_) {
|
||||||
|
if (material.id().value == id.value) {
|
||||||
|
return &material;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Property* Domain::find_property(core::PropertyId id) const
|
||||||
|
{
|
||||||
|
for (const auto& property : properties_) {
|
||||||
|
if (property.id().value == id.value) {
|
||||||
|
return &property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnalysisStep* Domain::find_step(core::StepId id) const
|
||||||
|
{
|
||||||
|
for (const auto& step : steps_) {
|
||||||
|
if (step.id().value == id.value) {
|
||||||
|
return &step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/model/analysis_step.hpp>
|
||||||
|
#include <fesa/model/element.hpp>
|
||||||
|
#include <fesa/model/material.hpp>
|
||||||
|
#include <fesa/model/node.hpp>
|
||||||
|
#include <fesa/model/property.hpp>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
class Domain {
|
||||||
|
public:
|
||||||
|
void add_node(Node node);
|
||||||
|
void add_element(Element element);
|
||||||
|
void add_material(Material material);
|
||||||
|
void add_property(Property property);
|
||||||
|
void add_step(AnalysisStep step);
|
||||||
|
|
||||||
|
const std::vector<Node>& nodes() const;
|
||||||
|
const std::vector<Element>& elements() const;
|
||||||
|
const std::vector<Material>& materials() const;
|
||||||
|
const std::vector<Property>& properties() const;
|
||||||
|
const std::vector<AnalysisStep>& steps() const;
|
||||||
|
|
||||||
|
const Node* find_node(core::NodeId id) const;
|
||||||
|
const Element* find_element(core::ElementId id) const;
|
||||||
|
const Material* find_material(core::MaterialId id) const;
|
||||||
|
const Property* find_property(core::PropertyId id) const;
|
||||||
|
const AnalysisStep* find_step(core::StepId id) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Node> nodes_;
|
||||||
|
std::vector<Element> elements_;
|
||||||
|
std::vector<Material> materials_;
|
||||||
|
std::vector<Property> properties_;
|
||||||
|
std::vector<AnalysisStep> steps_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#include <fesa/model/element.hpp>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
Element::Element(core::ElementId id,
|
||||||
|
ElementTopology topology,
|
||||||
|
std::vector<core::NodeId> node_ids,
|
||||||
|
core::PropertyId property_id)
|
||||||
|
: id_(id),
|
||||||
|
topology_(topology),
|
||||||
|
node_ids_(std::move(node_ids)),
|
||||||
|
property_id_(property_id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
core::ElementId Element::id() const
|
||||||
|
{
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementTopology Element::topology() const
|
||||||
|
{
|
||||||
|
return topology_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<core::NodeId>& Element::node_ids() const
|
||||||
|
{
|
||||||
|
return node_ids_;
|
||||||
|
}
|
||||||
|
|
||||||
|
core::PropertyId Element::property_id() const
|
||||||
|
{
|
||||||
|
return property_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
enum class ElementTopology {
|
||||||
|
truss2,
|
||||||
|
bar2,
|
||||||
|
unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
class Element {
|
||||||
|
public:
|
||||||
|
Element(core::ElementId id,
|
||||||
|
ElementTopology topology,
|
||||||
|
std::vector<core::NodeId> node_ids,
|
||||||
|
core::PropertyId property_id);
|
||||||
|
|
||||||
|
core::ElementId id() const;
|
||||||
|
ElementTopology topology() const;
|
||||||
|
const std::vector<core::NodeId>& node_ids() const;
|
||||||
|
core::PropertyId property_id() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::ElementId id_;
|
||||||
|
ElementTopology topology_;
|
||||||
|
std::vector<core::NodeId> node_ids_;
|
||||||
|
core::PropertyId property_id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#include <fesa/model/load.hpp>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
Load::Load(core::NodeId node_id, DofComponent component, double value)
|
||||||
|
: node_id_(node_id), component_(component), value_(value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
core::NodeId Load::node_id() const
|
||||||
|
{
|
||||||
|
return node_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
DofComponent Load::component() const
|
||||||
|
{
|
||||||
|
return component_;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Load::value() const
|
||||||
|
{
|
||||||
|
return value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/model/boundary_condition.hpp>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
class Load {
|
||||||
|
public:
|
||||||
|
Load(core::NodeId node_id, DofComponent component, double value);
|
||||||
|
|
||||||
|
core::NodeId node_id() const;
|
||||||
|
DofComponent component() const;
|
||||||
|
double value() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::NodeId node_id_;
|
||||||
|
DofComponent component_;
|
||||||
|
double value_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#include <fesa/model/material.hpp>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
Material::Material(core::MaterialId id, std::string name)
|
||||||
|
: id_(id), name_(std::move(name))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
core::MaterialId Material::id() const
|
||||||
|
{
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Material::name() const
|
||||||
|
{
|
||||||
|
return name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
class Material {
|
||||||
|
public:
|
||||||
|
Material(core::MaterialId id, std::string name);
|
||||||
|
|
||||||
|
core::MaterialId id() const;
|
||||||
|
const std::string& name() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::MaterialId id_;
|
||||||
|
std::string name_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include <fesa/model/node.hpp>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
Node::Node(core::NodeId id, std::array<double, 3> coordinates)
|
||||||
|
: id_(id), coordinates_(coordinates)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
core::NodeId Node::id() const
|
||||||
|
{
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<double, 3>& Node::coordinates() const
|
||||||
|
{
|
||||||
|
return coordinates_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
public:
|
||||||
|
Node(core::NodeId id, std::array<double, 3> coordinates);
|
||||||
|
|
||||||
|
core::NodeId id() const;
|
||||||
|
const std::array<double, 3>& coordinates() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::NodeId id_;
|
||||||
|
std::array<double, 3> coordinates_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include <fesa/model/property.hpp>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
Property::Property(core::PropertyId id, std::string name, core::MaterialId material_id)
|
||||||
|
: id_(id), name_(std::move(name)), material_id_(material_id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
core::PropertyId Property::id() const
|
||||||
|
{
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Property::name() const
|
||||||
|
{
|
||||||
|
return name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
core::MaterialId Property::material_id() const
|
||||||
|
{
|
||||||
|
return material_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fesa::model {
|
||||||
|
|
||||||
|
class Property {
|
||||||
|
public:
|
||||||
|
Property(core::PropertyId id, std::string name, core::MaterialId material_id);
|
||||||
|
|
||||||
|
core::PropertyId id() const;
|
||||||
|
const std::string& name() const;
|
||||||
|
core::MaterialId material_id() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::PropertyId id_;
|
||||||
|
std::string name_;
|
||||||
|
core::MaterialId material_id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::model
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include <fesa/results/results.hpp>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa::results {
|
||||||
|
|
||||||
|
ResultFrame::ResultFrame(int frame_id, double time)
|
||||||
|
: frame_id_(frame_id), time_(time)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int ResultFrame::frame_id() const
|
||||||
|
{
|
||||||
|
return frame_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ResultFrame::time() const
|
||||||
|
{
|
||||||
|
return time_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResultFrame::add_field_output(FieldOutput output)
|
||||||
|
{
|
||||||
|
if (output.components.empty()) {
|
||||||
|
throw std::invalid_argument("field output must have components");
|
||||||
|
}
|
||||||
|
if (output.entity_ids.size() * output.components.size() != output.values.size()) {
|
||||||
|
throw std::invalid_argument("field output values do not match row shape");
|
||||||
|
}
|
||||||
|
field_outputs_.push_back(std::move(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResultFrame::add_history_output(HistoryOutput output)
|
||||||
|
{
|
||||||
|
history_outputs_.push_back(std::move(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<FieldOutput>& ResultFrame::field_outputs() const
|
||||||
|
{
|
||||||
|
return field_outputs_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<HistoryOutput>& ResultFrame::history_outputs() const
|
||||||
|
{
|
||||||
|
return history_outputs_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultStep::ResultStep(std::string name)
|
||||||
|
: name_(std::move(name))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& ResultStep::name() const
|
||||||
|
{
|
||||||
|
return name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultFrame& ResultStep::add_frame(int frame_id, double time)
|
||||||
|
{
|
||||||
|
frames_.push_back(ResultFrame{frame_id, time});
|
||||||
|
return frames_.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<ResultFrame>& ResultStep::frames() const
|
||||||
|
{
|
||||||
|
return frames_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa::results
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa::results {
|
||||||
|
|
||||||
|
enum class FieldLocation {
|
||||||
|
nodal,
|
||||||
|
element,
|
||||||
|
integration_point
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FieldOutput {
|
||||||
|
std::string name;
|
||||||
|
FieldLocation location;
|
||||||
|
std::vector<std::string> components;
|
||||||
|
std::vector<int> entity_ids;
|
||||||
|
std::vector<double> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HistoryOutput {
|
||||||
|
std::string name;
|
||||||
|
std::vector<double> time;
|
||||||
|
std::vector<double> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResultFrame {
|
||||||
|
public:
|
||||||
|
ResultFrame(int frame_id, double time);
|
||||||
|
|
||||||
|
int frame_id() const;
|
||||||
|
double time() const;
|
||||||
|
void add_field_output(FieldOutput output);
|
||||||
|
void add_history_output(HistoryOutput output);
|
||||||
|
const std::vector<FieldOutput>& field_outputs() const;
|
||||||
|
const std::vector<HistoryOutput>& history_outputs() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int frame_id_;
|
||||||
|
double time_;
|
||||||
|
std::vector<FieldOutput> field_outputs_;
|
||||||
|
std::vector<HistoryOutput> history_outputs_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResultStep {
|
||||||
|
public:
|
||||||
|
explicit ResultStep(std::string name);
|
||||||
|
|
||||||
|
const std::string& name() const;
|
||||||
|
ResultFrame& add_frame(int frame_id, double time);
|
||||||
|
const std::vector<ResultFrame>& frames() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string name_;
|
||||||
|
std::vector<ResultFrame> frames_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::results
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
add_subdirectory(unit)
|
||||||
|
add_subdirectory(integration)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
file(GLOB FESA_INTEGRATION_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*_test.cpp")
|
||||||
|
|
||||||
|
foreach(test_source IN LISTS FESA_INTEGRATION_TEST_SOURCES)
|
||||||
|
get_filename_component(test_name "${test_source}" NAME_WE)
|
||||||
|
add_executable("${test_name}" "${test_source}")
|
||||||
|
target_link_libraries("${test_name}" PRIVATE fesa_core)
|
||||||
|
add_test(NAME "${test_name}" COMMAND "${test_name}")
|
||||||
|
endforeach()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#include <fesa/analysis/linear_static_analysis.hpp>
|
||||||
|
#include <fesa/results/results.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::model::Domain make_domain()
|
||||||
|
{
|
||||||
|
fesa::model::Domain domain;
|
||||||
|
domain.add_node({fesa::core::NodeId{1}, {0.0, 0.0, 0.0}});
|
||||||
|
domain.add_node({fesa::core::NodeId{2}, {1.0, 0.0, 0.0}});
|
||||||
|
domain.add_material({fesa::core::MaterialId{3}, "steel"});
|
||||||
|
domain.add_property({fesa::core::PropertyId{4}, "bar", fesa::core::MaterialId{3}});
|
||||||
|
domain.add_element({
|
||||||
|
fesa::core::ElementId{5},
|
||||||
|
fesa::model::ElementTopology::truss2,
|
||||||
|
{fesa::core::NodeId{1}, fesa::core::NodeId{2}},
|
||||||
|
fesa::core::PropertyId{4}
|
||||||
|
});
|
||||||
|
fesa::model::AnalysisStep step{fesa::core::StepId{6}, "static-step"};
|
||||||
|
step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0});
|
||||||
|
step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0});
|
||||||
|
domain.add_step(step);
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const auto domain = make_domain();
|
||||||
|
fesa::analysis::LinearStaticAnalysis analysis{domain, fesa::core::StepId{6}};
|
||||||
|
analysis.run();
|
||||||
|
|
||||||
|
if (analysis.analysis_model() == nullptr || analysis.state() == nullptr) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (analysis.analysis_model()->active_elements().size() != 1 ||
|
||||||
|
analysis.analysis_model()->active_boundary_conditions().size() != 1 ||
|
||||||
|
analysis.analysis_model()->active_loads().size() != 1) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::results::ResultStep result_step{"static-step"};
|
||||||
|
auto& frame = result_step.add_frame(0, 0.0);
|
||||||
|
frame.add_field_output({
|
||||||
|
"U",
|
||||||
|
fesa::results::FieldLocation::nodal,
|
||||||
|
{"ux", "uy", "uz"},
|
||||||
|
{1, 2},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result_step.frames().size() != 1 ||
|
||||||
|
result_step.frames()[0].field_outputs().size() != 1) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
file(GLOB FESA_UNIT_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*_test.cpp")
|
||||||
|
|
||||||
|
foreach(test_source IN LISTS FESA_UNIT_TEST_SOURCES)
|
||||||
|
get_filename_component(test_name "${test_source}" NAME_WE)
|
||||||
|
add_executable("${test_name}" "${test_source}")
|
||||||
|
target_link_libraries("${test_name}" PRIVATE fesa_core)
|
||||||
|
add_test(NAME "${test_name}" COMMAND "${test_name}")
|
||||||
|
endforeach()
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#include <fesa/io/abaqus/input_parser.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
fesa::io::abaqus::InputParser parser;
|
||||||
|
|
||||||
|
const std::string input = R"inp(
|
||||||
|
** mesh parser smoke case
|
||||||
|
*HeAdInG
|
||||||
|
small truss model
|
||||||
|
*NoDe
|
||||||
|
1, 0.0, 0.0, 0.0
|
||||||
|
2, 1.5, 0.0, 0.0
|
||||||
|
*ElEmEnT, TyPe=t3d2
|
||||||
|
10, 1, 2
|
||||||
|
)inp";
|
||||||
|
|
||||||
|
const auto result = parser.parse(input);
|
||||||
|
if (!result.status.is_ok()) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& domain = result.domain;
|
||||||
|
if (domain.nodes().size() != 2 || domain.elements().size() != 1) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* first_node = domain.find_node(fesa::core::NodeId{1});
|
||||||
|
if (first_node == nullptr || first_node->coordinates()[0] != 0.0 ||
|
||||||
|
first_node->coordinates()[1] != 0.0 || first_node->coordinates()[2] != 0.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* second_node = domain.find_node(fesa::core::NodeId{2});
|
||||||
|
if (second_node == nullptr || second_node->coordinates()[0] != 1.5 ||
|
||||||
|
second_node->coordinates()[1] != 0.0 || second_node->coordinates()[2] != 0.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* element = domain.find_element(fesa::core::ElementId{10});
|
||||||
|
if (element == nullptr ||
|
||||||
|
element->topology() != fesa::model::ElementTopology::truss2 ||
|
||||||
|
element->node_ids().size() != 2 ||
|
||||||
|
element->node_ids()[0].value != 1 ||
|
||||||
|
element->node_ids()[1].value != 2 ||
|
||||||
|
element->property_id().value != 0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string beam_input = R"inp(
|
||||||
|
*NODE
|
||||||
|
1, 0.0, 0.0, 0.0
|
||||||
|
2, 0.0, 2.0, 0.0
|
||||||
|
*ELEMENT, TYPE=B31
|
||||||
|
20, 1, 2
|
||||||
|
)inp";
|
||||||
|
|
||||||
|
const auto beam_result = parser.parse(beam_input);
|
||||||
|
const auto* beam = beam_result.domain.find_element(fesa::core::ElementId{20});
|
||||||
|
if (!beam_result.status.is_ok() ||
|
||||||
|
beam == nullptr ||
|
||||||
|
beam->topology() != fesa::model::ElementTopology::bar2 ||
|
||||||
|
beam->node_ids().size() != 2 ||
|
||||||
|
beam->property_id().value != 0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#include <fesa/analysis/linear_static_analysis.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::model::Domain make_domain()
|
||||||
|
{
|
||||||
|
fesa::model::Domain domain;
|
||||||
|
domain.add_node({fesa::core::NodeId{1}, {0.0, 0.0, 0.0}});
|
||||||
|
domain.add_node({fesa::core::NodeId{2}, {1.0, 0.0, 0.0}});
|
||||||
|
domain.add_material({fesa::core::MaterialId{3}, "steel"});
|
||||||
|
domain.add_property({fesa::core::PropertyId{4}, "bar", fesa::core::MaterialId{3}});
|
||||||
|
domain.add_element({
|
||||||
|
fesa::core::ElementId{5},
|
||||||
|
fesa::model::ElementTopology::truss2,
|
||||||
|
{fesa::core::NodeId{1}, fesa::core::NodeId{2}},
|
||||||
|
fesa::core::PropertyId{4}
|
||||||
|
});
|
||||||
|
fesa::model::AnalysisStep step{fesa::core::StepId{6}, "static"};
|
||||||
|
step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0});
|
||||||
|
step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0});
|
||||||
|
domain.add_step(step);
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const auto domain = make_domain();
|
||||||
|
fesa::analysis::LinearStaticAnalysis analysis{domain, fesa::core::StepId{6}};
|
||||||
|
analysis.run();
|
||||||
|
|
||||||
|
if (analysis.analysis_model() == nullptr || analysis.state() == nullptr) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return analysis.state()->displacement().size() == 6 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#include <fesa/analysis/analysis.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class RecordingAnalysis : public fesa::analysis::Analysis {
|
||||||
|
public:
|
||||||
|
const std::vector<std::string>& calls() const
|
||||||
|
{
|
||||||
|
return calls_;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void initialize() override { calls_.push_back("initialize"); }
|
||||||
|
void build_analysis_model() override { calls_.push_back("build_analysis_model"); }
|
||||||
|
void build_dof_map() override { calls_.push_back("build_dof_map"); }
|
||||||
|
void build_sparse_pattern() override { calls_.push_back("build_sparse_pattern"); }
|
||||||
|
void assemble() override { calls_.push_back("assemble"); }
|
||||||
|
void apply_boundary_conditions() override { calls_.push_back("apply_boundary_conditions"); }
|
||||||
|
void solve() override { calls_.push_back("solve"); }
|
||||||
|
void update_state() override { calls_.push_back("update_state"); }
|
||||||
|
void write_results() override { calls_.push_back("write_results"); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::string> calls_;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
RecordingAnalysis analysis;
|
||||||
|
analysis.run();
|
||||||
|
const std::vector<std::string> expected{
|
||||||
|
"initialize",
|
||||||
|
"build_analysis_model",
|
||||||
|
"build_dof_map",
|
||||||
|
"build_sparse_pattern",
|
||||||
|
"assemble",
|
||||||
|
"apply_boundary_conditions",
|
||||||
|
"solve",
|
||||||
|
"update_state",
|
||||||
|
"write_results"
|
||||||
|
};
|
||||||
|
return analysis.calls() == expected ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#include <fesa/analysis/analysis_model.hpp>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::model::Domain make_domain()
|
||||||
|
{
|
||||||
|
fesa::model::Domain domain;
|
||||||
|
domain.add_node({fesa::core::NodeId{1}, {0.0, 0.0, 0.0}});
|
||||||
|
domain.add_node({fesa::core::NodeId{2}, {1.0, 0.0, 0.0}});
|
||||||
|
domain.add_material({fesa::core::MaterialId{3}, "steel"});
|
||||||
|
domain.add_property({fesa::core::PropertyId{4}, "bar", fesa::core::MaterialId{3}});
|
||||||
|
domain.add_element({
|
||||||
|
fesa::core::ElementId{5},
|
||||||
|
fesa::model::ElementTopology::truss2,
|
||||||
|
{fesa::core::NodeId{1}, fesa::core::NodeId{2}},
|
||||||
|
fesa::core::PropertyId{4}
|
||||||
|
});
|
||||||
|
|
||||||
|
fesa::model::AnalysisStep step{fesa::core::StepId{6}, "static"};
|
||||||
|
step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0});
|
||||||
|
step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0});
|
||||||
|
domain.add_step(step);
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const auto domain = make_domain();
|
||||||
|
const fesa::analysis::AnalysisModel model{domain, fesa::core::StepId{6}};
|
||||||
|
|
||||||
|
if (&model.domain() != &domain) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (&model.step() != domain.find_step(fesa::core::StepId{6})) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (model.active_elements().size() != 1 ||
|
||||||
|
model.active_elements()[0] != domain.find_element(fesa::core::ElementId{5})) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (model.active_boundary_conditions().size() != 1 ||
|
||||||
|
model.active_loads().size() != 1) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* property = model.property_for(*model.active_elements()[0]);
|
||||||
|
if (property == nullptr || property != domain.find_property(fesa::core::PropertyId{4})) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* material = model.material_for(*property);
|
||||||
|
if (material == nullptr || material != domain.find_material(fesa::core::MaterialId{3})) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fesa::analysis::AnalysisModel missing{domain, fesa::core::StepId{99}};
|
||||||
|
(void)missing;
|
||||||
|
return fail();
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#include <fesa/analysis/analysis_state.hpp>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool all_zero(const std::vector<double>& values)
|
||||||
|
{
|
||||||
|
for (const double value : values) {
|
||||||
|
if (value != 0.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
fesa::analysis::AnalysisState state{3};
|
||||||
|
if (state.displacement().size() != 3 ||
|
||||||
|
state.velocity().size() != 3 ||
|
||||||
|
state.acceleration().size() != 3 ||
|
||||||
|
state.temperature().size() != 3 ||
|
||||||
|
state.external_force().size() != 3 ||
|
||||||
|
state.internal_force().size() != 3 ||
|
||||||
|
state.residual().size() != 3) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (!all_zero(state.displacement()) || !all_zero(state.residual())) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.set_displacement({1.0, 2.0, 3.0});
|
||||||
|
if (state.displacement()[2] != 3.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.set_external_force({10.0, 20.0, 30.0});
|
||||||
|
state.set_internal_force({1.0, 2.0, 3.0});
|
||||||
|
state.update_residual();
|
||||||
|
if (state.residual()[0] != 9.0 ||
|
||||||
|
state.residual()[1] != 18.0 ||
|
||||||
|
state.residual()[2] != 27.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.set_displacement({1.0});
|
||||||
|
return fail();
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
}
|
||||||
|
|
||||||
|
state.iteration_state().time = 1.25;
|
||||||
|
state.iteration_state().increment = 2;
|
||||||
|
state.iteration_state().iteration = 3;
|
||||||
|
if (state.iteration_state().time != 1.25 ||
|
||||||
|
state.iteration_state().increment != 2 ||
|
||||||
|
state.iteration_state().iteration != 3) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.set_element_state(fesa::core::ElementId{7}, {4.0, 5.0});
|
||||||
|
const auto* element_state = state.element_state(fesa::core::ElementId{7});
|
||||||
|
if (element_state == nullptr || element_state->size() != 2 || (*element_state)[1] != 5.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (state.element_state(fesa::core::ElementId{8}) != nullptr) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::core::Diagnostic diagnostic{
|
||||||
|
fesa::core::Severity::info,
|
||||||
|
"core.info",
|
||||||
|
"diagnostic"
|
||||||
|
};
|
||||||
|
return diagnostic.code == "core.info" ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
static_assert(!std::is_same_v<fesa::core::NodeId, fesa::core::ElementId>);
|
||||||
|
return fesa::core::NodeId{7}.value == 7 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/core/ids.hpp>
|
||||||
|
#include <fesa/core/status.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
static_assert(!std::is_same_v<fesa::core::NodeId, fesa::core::ElementId>);
|
||||||
|
|
||||||
|
const auto ok = fesa::core::Status::ok();
|
||||||
|
if (!ok.is_ok() || !ok.diagnostics().empty()) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::core::Diagnostic error{
|
||||||
|
fesa::core::Severity::error,
|
||||||
|
"core.error",
|
||||||
|
"core failure"
|
||||||
|
};
|
||||||
|
auto failed = fesa::core::Status::failure(error);
|
||||||
|
if (failed.is_ok() || failed.diagnostics().size() != 1) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (failed.diagnostics()[0].code != "core.error" ||
|
||||||
|
failed.diagnostics()[0].message != "core failure") {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
failed.add({fesa::core::Severity::warning, "core.warning", "check warning"});
|
||||||
|
if (failed.diagnostics().size() != 2) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (failed.diagnostics()[0].code != "core.error" ||
|
||||||
|
failed.diagnostics()[1].code != "core.warning") {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <fesa/core/status.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const auto status = fesa::core::Status::ok();
|
||||||
|
return status.is_ok() ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include <fesa/fem/dof_key.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::fem::DofKey lhs{fesa::core::NodeId{1}, fesa::model::DofComponent::ux};
|
||||||
|
const fesa::fem::DofKey rhs{fesa::core::NodeId{1}, fesa::model::DofComponent::ux};
|
||||||
|
return lhs == rhs ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
fesa::fem::DofManager dofs;
|
||||||
|
dofs.define_node_dofs(fesa::core::NodeId{2}, {
|
||||||
|
fesa::model::DofComponent::ux,
|
||||||
|
fesa::model::DofComponent::uy
|
||||||
|
});
|
||||||
|
dofs.define_node_dofs(fesa::core::NodeId{1}, {
|
||||||
|
fesa::model::DofComponent::uy,
|
||||||
|
fesa::model::DofComponent::ux
|
||||||
|
});
|
||||||
|
dofs.apply_boundary_condition({
|
||||||
|
fesa::core::NodeId{1},
|
||||||
|
fesa::model::DofComponent::ux,
|
||||||
|
0.0
|
||||||
|
});
|
||||||
|
dofs.number_equations();
|
||||||
|
|
||||||
|
const fesa::fem::DofKey n1ux{fesa::core::NodeId{1}, fesa::model::DofComponent::ux};
|
||||||
|
const fesa::fem::DofKey n1uy{fesa::core::NodeId{1}, fesa::model::DofComponent::uy};
|
||||||
|
const fesa::fem::DofKey n2ux{fesa::core::NodeId{2}, fesa::model::DofComponent::ux};
|
||||||
|
const fesa::fem::DofKey n2uy{fesa::core::NodeId{2}, fesa::model::DofComponent::uy};
|
||||||
|
|
||||||
|
if (dofs.total_dof_count() != 4 ||
|
||||||
|
dofs.constrained_dof_count() != 1 ||
|
||||||
|
dofs.free_dof_count() != 3) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (dofs.equation_id(n1ux) != 0 ||
|
||||||
|
dofs.equation_id(n1uy) != 1 ||
|
||||||
|
dofs.equation_id(n2ux) != 2 ||
|
||||||
|
dofs.equation_id(n2uy) != 3) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (!dofs.is_constrained(n1ux) ||
|
||||||
|
dofs.free_equation_id(n1ux).has_value()) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
if (!dofs.free_equation_id(n1uy).has_value() ||
|
||||||
|
*dofs.free_equation_id(n1uy) != 0 ||
|
||||||
|
*dofs.free_equation_id(n2ux) != 1 ||
|
||||||
|
*dofs.free_equation_id(n2uy) != 2) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto full = dofs.expand_free_vector({11.0, 22.0, 33.0});
|
||||||
|
if (full.size() != 4 ||
|
||||||
|
full[0] != 0.0 ||
|
||||||
|
full[1] != 11.0 ||
|
||||||
|
full[2] != 22.0 ||
|
||||||
|
full[3] != 33.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& pattern = dofs.sparse_pattern();
|
||||||
|
if (pattern.size() != 9 ||
|
||||||
|
pattern.front() != std::pair<int, int>{0, 0} ||
|
||||||
|
pattern.back() != std::pair<int, int>{2, 2}) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
(void)dofs.equation_id({fesa::core::NodeId{99}, fesa::model::DofComponent::ux});
|
||||||
|
return fail();
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <string>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const std::string project = "fesa";
|
||||||
|
return project.size() == 4 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <fesa/model/analysis_step.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::model::AnalysisStep step{fesa::core::StepId{1}, "static"};
|
||||||
|
return step.name() == "static" ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include <fesa/model/boundary_condition.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::model::BoundaryCondition bc{
|
||||||
|
fesa::core::NodeId{1},
|
||||||
|
fesa::model::DofComponent::ux,
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
return bc.node_id().value == 1 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
fesa::model::Domain domain;
|
||||||
|
|
||||||
|
domain.add_node(fesa::model::Node{fesa::core::NodeId{1}, {1.0, 2.0, 3.0}});
|
||||||
|
domain.add_element(fesa::model::Element{
|
||||||
|
fesa::core::ElementId{10},
|
||||||
|
fesa::model::ElementTopology::truss2,
|
||||||
|
{fesa::core::NodeId{1}, fesa::core::NodeId{2}},
|
||||||
|
fesa::core::PropertyId{20}
|
||||||
|
});
|
||||||
|
domain.add_material(fesa::model::Material{fesa::core::MaterialId{30}, "steel"});
|
||||||
|
domain.add_property(fesa::model::Property{
|
||||||
|
fesa::core::PropertyId{20},
|
||||||
|
"bar",
|
||||||
|
fesa::core::MaterialId{30}
|
||||||
|
});
|
||||||
|
|
||||||
|
fesa::model::AnalysisStep step{fesa::core::StepId{40}, "load"};
|
||||||
|
step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0});
|
||||||
|
step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0});
|
||||||
|
domain.add_step(step);
|
||||||
|
|
||||||
|
const auto* node = domain.find_node(fesa::core::NodeId{1});
|
||||||
|
if (node == nullptr || node->coordinates()[2] != 3.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* element = domain.find_element(fesa::core::ElementId{10});
|
||||||
|
if (element == nullptr ||
|
||||||
|
element->topology() != fesa::model::ElementTopology::truss2 ||
|
||||||
|
element->node_ids().size() != 2 ||
|
||||||
|
element->property_id().value != 20) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* material = domain.find_material(fesa::core::MaterialId{30});
|
||||||
|
if (material == nullptr || material->name() != "steel") {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* property = domain.find_property(fesa::core::PropertyId{20});
|
||||||
|
if (property == nullptr ||
|
||||||
|
property->name() != "bar" ||
|
||||||
|
property->material_id().value != 30) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* analysis_step = domain.find_step(fesa::core::StepId{40});
|
||||||
|
if (analysis_step == nullptr ||
|
||||||
|
analysis_step->boundary_conditions().size() != 1 ||
|
||||||
|
analysis_step->loads().size() != 1) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.find_node(fesa::core::NodeId{999}) != nullptr ||
|
||||||
|
domain.find_element(fesa::core::ElementId{999}) != nullptr ||
|
||||||
|
domain.find_material(fesa::core::MaterialId{999}) != nullptr ||
|
||||||
|
domain.find_property(fesa::core::PropertyId{999}) != nullptr ||
|
||||||
|
domain.find_step(fesa::core::StepId{999}) != nullptr) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#include <fesa/model/element.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::model::Element element{
|
||||||
|
fesa::core::ElementId{1},
|
||||||
|
fesa::model::ElementTopology::bar2,
|
||||||
|
{fesa::core::NodeId{1}, fesa::core::NodeId{2}},
|
||||||
|
fesa::core::PropertyId{3}
|
||||||
|
};
|
||||||
|
return element.node_ids().size() == 2 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include <fesa/model/load.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::model::Load load{
|
||||||
|
fesa::core::NodeId{2},
|
||||||
|
fesa::model::DofComponent::ux,
|
||||||
|
5.0
|
||||||
|
};
|
||||||
|
return load.value() == 5.0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <fesa/model/material.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::model::Material material{fesa::core::MaterialId{1}, "steel"};
|
||||||
|
return material.name() == "steel" ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <fesa/model/node.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::model::Node node{fesa::core::NodeId{1}, {0.0, 1.0, 2.0}};
|
||||||
|
return node.coordinates()[1] == 1.0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include <fesa/model/property.hpp>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const fesa::model::Property property{
|
||||||
|
fesa::core::PropertyId{2},
|
||||||
|
"section",
|
||||||
|
fesa::core::MaterialId{3}
|
||||||
|
};
|
||||||
|
return property.material_id().value == 3 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include <fesa/results/results.hpp>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int fail()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
fesa::results::ResultStep step{"static"};
|
||||||
|
auto& frame = step.add_frame(1, 0.0);
|
||||||
|
if (step.name() != "static" ||
|
||||||
|
step.frames().size() != 1 ||
|
||||||
|
frame.frame_id() != 1 ||
|
||||||
|
frame.time() != 0.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.add_field_output({
|
||||||
|
"U",
|
||||||
|
fesa::results::FieldLocation::nodal,
|
||||||
|
{"ux", "uy"},
|
||||||
|
{1, 2},
|
||||||
|
{0.0, 0.1, 1.0, 1.1}
|
||||||
|
});
|
||||||
|
if (frame.field_outputs().size() != 1 ||
|
||||||
|
frame.field_outputs()[0].entity_ids[1] != 2 ||
|
||||||
|
frame.field_outputs()[0].values[3] != 1.1) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.add_history_output({"load-factor", {0.0, 1.0}, {0.0, 10.0}});
|
||||||
|
if (frame.history_outputs().size() != 1 ||
|
||||||
|
frame.history_outputs()[0].values[1] != 10.0) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
frame.add_field_output({
|
||||||
|
"bad",
|
||||||
|
fesa::results::FieldLocation::nodal,
|
||||||
|
{},
|
||||||
|
{1},
|
||||||
|
{0.0}
|
||||||
|
});
|
||||||
|
return fail();
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
frame.add_field_output({
|
||||||
|
"bad-shape",
|
||||||
|
fesa::results::FieldLocation::nodal,
|
||||||
|
{"ux", "uy"},
|
||||||
|
{1},
|
||||||
|
{0.0}
|
||||||
|
});
|
||||||
|
return fail();
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user