feat: start abaqus input parser

This commit is contained in:
김경종
2026-06-12 17:15:05 +09:00
parent 825e03dbaf
commit b57b0bf63a
14 changed files with 978 additions and 22 deletions
@@ -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.
+65
View File
@@ -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"
]
}
]
}
+65
View File
@@ -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.
+83
View File
@@ -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.
+63
View File
@@ -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.
+62
View File
@@ -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.
+65
View File
@@ -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.
+55
View File
@@ -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.
+4
View File
@@ -3,6 +3,10 @@
{
"dir": "solver-core-skeleton",
"status": "completed"
},
{
"dir": "abaqus-input-parser",
"status": "pending"
}
]
}
+26
View File
@@ -30,6 +30,7 @@ class ValidateWorkspaceTests(unittest.TestCase):
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
build_dir = root / "build" / "msvc-debug"
with patch.dict(os.environ, {}, clear=True):
with patch.object(validate_workspace.shutil, "which", return_value="C:\\tools\\cmake.exe"):
self.assertEqual(
validate_workspace.discover_commands(root),
[
@@ -60,6 +61,7 @@ class ValidateWorkspaceTests(unittest.TestCase):
encoding="utf-8",
)
with patch.dict(os.environ, {}, clear=True):
with patch.object(validate_workspace.shutil, "which", return_value="C:\\tools\\cmake.exe"):
self.assertEqual(
validate_workspace.discover_commands(root),
[
@@ -69,6 +71,30 @@ class ValidateWorkspaceTests(unittest.TestCase):
],
)
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):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
+21 -6
View File
@@ -32,6 +32,17 @@ def _cmake_config() -> tuple[str, str, str, Path]:
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:
presets_file = root / "CMakePresets.json"
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]:
payload = _read_presets(root)
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
cmake = _tool_command("cmake")
ctest = _tool_command("ctest")
for preset in payload.get("configurePresets", []):
if isinstance(preset, dict) and preset.get("name") == PRESET_NAME:
build_dir = _preset_binary_dir(root, preset)
return [
f"cmake --preset {PRESET_NAME}",
f'cmake --build "{build_dir}" --config {config}',
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
f"{cmake} --preset {PRESET_NAME}",
f'{cmake} --build "{build_dir}" --config {config}',
f'{ctest} --test-dir "{build_dir}" --output-on-failure -C {config}',
]
return []
@@ -71,10 +84,12 @@ def load_cmake_commands(root: Path) -> list[str]:
generator, platform, config, build_dir = _cmake_config()
if not build_dir.is_absolute():
build_dir = root / build_dir
cmake = _tool_command("cmake")
ctest = _tool_command("ctest")
return [
f'cmake -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}',
f'cmake --build "{build_dir}" --config {config}',
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
f'{cmake} -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}',
f'{cmake} --build "{build_dir}" --config {config}',
f'{ctest} --test-dir "{build_dir}" --output-on-failure -C {config}',
]
+214
View File
@@ -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
+20
View File
@@ -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,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;
}