Compare commits
14 Commits
18361e4eb2
...
6ac474f19b
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ac474f19b | |||
| cbb621bb28 | |||
| af886f3f90 | |||
| 9269847c83 | |||
| eec4fe1f49 | |||
| 94c83a39c5 | |||
| af076c39a3 | |||
| a1fed69c47 | |||
| 8cd2d5b2ae | |||
| e6bc708be3 | |||
| 3eeab2fbe4 | |||
| 3588aa2bb6 | |||
| 91041f28c9 | |||
| 2b83b1128f |
@@ -36,8 +36,10 @@ add_library(fesa_core STATIC
|
||||
src/fesa/fem/dof_manager.cpp
|
||||
src/fesa/fem/gauss_rule.cpp
|
||||
src/fesa/fem/line2_shape.cpp
|
||||
src/fesa/io/abaqus/active_input.cpp
|
||||
src/fesa/io/abaqus/parser.cpp
|
||||
src/fesa/io/abaqus/semantic_mapper.cpp
|
||||
src/fesa/io/abaqus/set_resolver.cpp
|
||||
src/fesa/io/hdf5/writer.cpp
|
||||
src/fesa/model/domain.cpp
|
||||
src/fesa/model/domain_builder.cpp
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
# FESA Phase 1 Abaqus Input Subset
|
||||
|
||||
## 1. Status and scope
|
||||
|
||||
This document is the normative input contract for the FESA Phase 1 Abaqus
|
||||
adapter. FESA accepts only the keywords, parameters, scopes, and data forms
|
||||
defined here. It does not implement general Abaqus input syntax, and it never
|
||||
silently ignores an unknown keyword or option.
|
||||
|
||||
Two mutually exclusive organizations are supported:
|
||||
|
||||
- a flat/orphan mesh whose mesh and set records are in global scope; or
|
||||
- one or more `*PART` definitions followed by exactly one `*ASSEMBLY` that
|
||||
contains exactly one untransformed `*INSTANCE` of the active Part.
|
||||
|
||||
Only the active Part is normalized into `Domain`. Names and external labels are
|
||||
input identity; generated nonnegative FESA IDs and dense indices are separate.
|
||||
FESA performs no unit conversion.
|
||||
|
||||
`tests/fixtures/abaqus/contract.tsv` is the executable valid/invalid fixture
|
||||
matrix for this document. Its expected diagnostic code and line are part of
|
||||
the contract. A diagnostic for a bad data row points to that row; a keyword,
|
||||
parameter, or scope error points to the keyword row.
|
||||
|
||||
## 2. Common lexical rules
|
||||
|
||||
- Input is UTF-8. A UTF-8 BOM is accepted only at the start of the file.
|
||||
- Blank lines and lines whose first non-whitespace characters are `**` are
|
||||
ignored. A comment or blank line does not end the current keyword record.
|
||||
- Keyword names, parameter names, flag parameters, and the enumerated values
|
||||
named below are ASCII case-insensitive. Entity names are trimmed and then
|
||||
matched exactly.
|
||||
- Fields are comma-separated and surrounding ASCII whitespace is ignored.
|
||||
- External node and element labels are positive signed 64-bit integers. DOF
|
||||
numbers are integers 1 through 6. Real fields must be finite.
|
||||
- A flag parameter has no `=` value. A valued parameter must have one nonempty
|
||||
value. Duplicate parameters are `abaqus.syntax.duplicate_parameter`.
|
||||
- Parameters not listed for a keyword are
|
||||
`abaqus.syntax.unsupported_parameter`. Unknown keywords, including
|
||||
`*INCLUDE`, are `abaqus.unsupported_keyword`.
|
||||
- A non-comment data line without an open data-bearing keyword is
|
||||
`abaqus.syntax.data_without_keyword`.
|
||||
|
||||
## 3. Scope and ordering
|
||||
|
||||
The parser maintains `global`, `part`, `assembly`, `instance`, and `step`
|
||||
scope. `*STEP` is a global child scope: model-data records already opened in
|
||||
global scope remain model data, while `*STATIC`, `*CLOAD`, `*RESTART`, and
|
||||
`*OUTPUT` belong to the open Step.
|
||||
|
||||
The accepted ordering is:
|
||||
|
||||
```text
|
||||
optional *HEADING and *PREPRINT
|
||||
flat mesh, or one or more *PART blocks and one *ASSEMBLY block
|
||||
global materials
|
||||
optional global model-data *BOUNDARY
|
||||
one *STEP
|
||||
one *STATIC
|
||||
optional *BOUNDARY and *CLOAD
|
||||
optional no-op *RESTART and *OUTPUT
|
||||
*END STEP
|
||||
```
|
||||
|
||||
References are resolved after the complete deck is parsed. A material may
|
||||
therefore follow the Part that uses it, and a nested set may refer to a set
|
||||
declared later in the same scope.
|
||||
|
||||
## 4. Mesh and hierarchy keywords
|
||||
|
||||
### 4.1 `*NODE`
|
||||
|
||||
- Scope: flat global or Part; not Assembly, Instance, or Step.
|
||||
- Parameters: none.
|
||||
- Data: one or more `label, x, y, z` rows, with exactly four nonempty fields.
|
||||
- Semantics: labels are unique in their mesh scope. Reuse in an inactive Part
|
||||
is allowed because it is a different Part scope.
|
||||
- Diagnostics: `abaqus.syntax.invalid_node_scope`,
|
||||
`abaqus.semantic.invalid_node_data`, `abaqus.semantic.duplicate_node_label`.
|
||||
|
||||
### 4.2 `*ELEMENT`
|
||||
|
||||
- Scope: flat global or Part.
|
||||
- Parameters: required `TYPE=B31`; optional `ELSET=<name>`; no others.
|
||||
- Data: one or more `element_label, node_1_label, node_2_label` rows.
|
||||
- Semantics: element labels are unique in their mesh scope. Both nodes must
|
||||
exist in that scope. `ELSET=` adds every row to the named element set and
|
||||
merges with an explicit set of the same name using sorted-unique membership.
|
||||
- Diagnostics: `abaqus.syntax.invalid_element_scope`,
|
||||
`abaqus.semantic.unsupported_element`,
|
||||
`abaqus.semantic.invalid_element_data`,
|
||||
`abaqus.semantic.duplicate_element_label`,
|
||||
`abaqus.semantic.missing_node`.
|
||||
|
||||
### 4.3 Part delimiters
|
||||
|
||||
`*PART` is global-only, requires exactly `NAME=<name>`, and accepts no data.
|
||||
Part names are unique. `*END PART` accepts no parameters or data and closes the
|
||||
open Part. Nesting or a mismatched delimiter is invalid.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_part_scope`,
|
||||
`abaqus.syntax.unexpected_end_part`, `abaqus.syntax.unclosed_part`, and
|
||||
`abaqus.semantic.duplicate_part`.
|
||||
|
||||
### 4.4 Assembly delimiters
|
||||
|
||||
`*ASSEMBLY` is global-only, requires exactly `NAME=<name>`, and accepts no
|
||||
data. Phase 1 accepts exactly one Assembly. `*END ASSEMBLY` has no parameters
|
||||
or data and closes the open Assembly.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_assembly_scope`,
|
||||
`abaqus.syntax.multiple_assemblies`,
|
||||
`abaqus.syntax.unexpected_end_assembly`, and
|
||||
`abaqus.syntax.unclosed_assembly`.
|
||||
|
||||
### 4.5 Instance delimiters
|
||||
|
||||
`*INSTANCE` is Assembly-only and requires exactly `NAME=<name>, PART=<name>`.
|
||||
Phase 1 accepts exactly one Instance. No translation or rotation data and no
|
||||
keyword record are allowed inside it. `*END INSTANCE` has no parameters or
|
||||
data.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_instance_scope`,
|
||||
`abaqus.syntax.instance_local_keyword`,
|
||||
`abaqus.syntax.unexpected_end_instance`,
|
||||
`abaqus.syntax.unclosed_instance`, `abaqus.semantic.instance_count`,
|
||||
`abaqus.semantic.instance_transform`, and `abaqus.semantic.missing_part`.
|
||||
A transform diagnostic points to the first transform data row.
|
||||
|
||||
Flat `*NODE`/`*ELEMENT` records combined with any Part/Assembly organization
|
||||
are `abaqus.semantic.mixed_mesh_organization`.
|
||||
|
||||
## 5. Sets
|
||||
|
||||
`*NSET` and `*ELSET` are allowed in flat global, Part, or Assembly scope.
|
||||
|
||||
- Required parameter: respectively `NSET=<name>` or `ELSET=<name>`.
|
||||
- Optional flag: `GENERATE`.
|
||||
- Assembly scope additionally requires `INSTANCE=<active-instance-name>`.
|
||||
`INSTANCE=` is forbidden in flat global and Part scope.
|
||||
- Explicit data consists of comma-separated positive labels and/or names of
|
||||
sets of the same kind and scope. Empty trailing fields are ignored.
|
||||
- `GENERATE` data consists of exactly one `start, end, increment` row. All
|
||||
values are positive integers, `start <= end`, and `(end-start)` is divisible
|
||||
by `increment`.
|
||||
- Repeated declarations of the same set merge. Nested references are resolved
|
||||
independent of declaration order, cycles are rejected, and final membership
|
||||
is deterministic sorted-unique.
|
||||
- An Assembly set lifts active-Part local labels through its named Instance.
|
||||
It cannot reference an inactive or unknown Instance.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_set_scope`,
|
||||
`abaqus.semantic.invalid_generate`, `abaqus.semantic.set_cycle`,
|
||||
`abaqus.semantic.missing_set_member`, and
|
||||
`abaqus.semantic.wrong_instance`.
|
||||
|
||||
## 6. Material and Beam section
|
||||
|
||||
### 6.1 `*MATERIAL` and `*ELASTIC`
|
||||
|
||||
`*MATERIAL` is global-only, requires exactly `NAME=<name>`, and has no data.
|
||||
Material names are unique. Its `*ELASTIC` child is global model data, has no
|
||||
parameters, and has exactly one `young_modulus, poisson_ratio` row. Young's
|
||||
modulus is finite and positive; Poisson's ratio is finite and satisfies
|
||||
`-1 < nu < 0.5`. Temperature and field dependencies are not supported.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_material_scope`,
|
||||
`abaqus.semantic.duplicate_material`,
|
||||
`abaqus.semantic.elastic_without_material`,
|
||||
`abaqus.semantic.missing_elastic`, and
|
||||
`abaqus.semantic.invalid_elastic_data`.
|
||||
|
||||
### 6.2 `*BEAM GENERAL SECTION`
|
||||
|
||||
- Scope: flat global or Part.
|
||||
- Parameters: required `SECTION=GENERAL`, `ELSET=<name>`, and
|
||||
`MATERIAL=<name>`; no others.
|
||||
- First data row: exactly `A, I_y, I_yz, I_z, J`. `A`, `I_y`, `I_z`, and `J`
|
||||
are finite and positive; `I_yz` must be finite and exactly zero for Phase 1.
|
||||
- Second data row: exactly three finite components of the local section-axis
|
||||
reference direction. Model validation rejects a zero direction or one
|
||||
parallel to an assigned element axis.
|
||||
- The material and set may be declared later, but must resolve. Every active
|
||||
B31 element has exactly one section assignment.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_section_scope`,
|
||||
`abaqus.semantic.unsupported_section`,
|
||||
`abaqus.semantic.invalid_section_data`,
|
||||
`abaqus.semantic.missing_material`,
|
||||
`abaqus.semantic.missing_element_set`,
|
||||
`abaqus.semantic.missing_section`, and
|
||||
`abaqus.semantic.duplicate_section_assignment`.
|
||||
|
||||
### 6.3 `*TRANSVERSE SHEAR STIFFNESS`
|
||||
|
||||
This optional record is in the same flat-global or Part scope as, and must
|
||||
immediately follow, the affected `*BEAM GENERAL SECTION`. It has no parameters
|
||||
and exactly one `K23, K13, SCF` data row. `K23` and `K13` are finite and
|
||||
positive. Phase 1 accepts only numeric `SCF=0`; omitted/default `0.25`, nonzero
|
||||
values, and the Abaqus `SCF` label are unsupported.
|
||||
|
||||
For isotropic `G=E/[2(1+nu)]`, FESA stores
|
||||
`A_sy=K23/G` and `A_sz=K13/G` with source `input`. If this keyword is absent,
|
||||
the semantic mapper stores `A_sy=A_sz=5A/6`, `SCF=0`, with source
|
||||
`phase1_default`.
|
||||
|
||||
The data order follows the Abaqus 2024
|
||||
[*TRANSVERSE SHEAR STIFFNESS* reference](https://docs.software.vt.edu/abaqusv2024/English/SIMACAEKEYRefMap/simakey-r-transverseshearstiffness.htm);
|
||||
the restriction to numeric zero SCF and the effective-area mapping are FESA
|
||||
Phase 1 decisions.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_transverse_shear_scope`,
|
||||
`abaqus.semantic.orphan_transverse_shear`,
|
||||
`abaqus.semantic.invalid_transverse_shear_data`, and
|
||||
`abaqus.semantic.nonzero_scf`.
|
||||
|
||||
## 7. Linear static step, BC, and load
|
||||
|
||||
### 7.1 `*BOUNDARY`
|
||||
|
||||
`*BOUNDARY` is allowed as global model data before the Step or inside the sole
|
||||
Step. It has no parameters. Each row is
|
||||
`node-or-nset, first_dof[, last_dof[, value]]`. `last_dof` defaults to
|
||||
`first_dof`; value defaults to zero. The inclusive DOF range is 1 through 6.
|
||||
Repeated identical prescriptions are deduplicated; differing values for one
|
||||
node/DOF are rejected.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_boundary_scope`,
|
||||
`abaqus.semantic.invalid_boundary_data`,
|
||||
`abaqus.semantic.invalid_dof`, `abaqus.semantic.invalid_dof_range`,
|
||||
`abaqus.semantic.missing_node_target`, and
|
||||
`abaqus.semantic.conflicting_boundary`.
|
||||
|
||||
### 7.2 `*CLOAD`
|
||||
|
||||
`*CLOAD` is Step-only and has no parameters. Each row is exactly
|
||||
`node-or-nset, dof, magnitude`; DOF is 1 through 6 and magnitude is finite.
|
||||
Loads on the same node/DOF are summed in input order after target resolution.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_cload_scope`,
|
||||
`abaqus.semantic.invalid_cload_data`, `abaqus.semantic.invalid_dof`, and
|
||||
`abaqus.semantic.missing_node_target`.
|
||||
|
||||
### 7.3 `*STEP`, `*STATIC`, and `*END STEP`
|
||||
|
||||
`*STEP` is global-only. Optional parameters are `NAME=<name>` and
|
||||
`NLGEOM=NO`; omitted name becomes `Step-1`. Exactly one Step is required.
|
||||
`NLGEOM=YES` and every other option are unsupported.
|
||||
|
||||
Exactly one `*STATIC` occurs inside the Step. It has no parameters and accepts
|
||||
either no data row or one row of one through four finite positive values
|
||||
`initial_increment[, time_period[, minimum_increment[, maximum_increment]]]`.
|
||||
The values are accepted as load-step metadata; Phase 1 performs one linear
|
||||
solve.
|
||||
|
||||
`*END STEP` has no parameters or data and closes the Step.
|
||||
|
||||
Diagnostics are `abaqus.syntax.invalid_step_scope`,
|
||||
`abaqus.syntax.unexpected_end_step`, `abaqus.syntax.unclosed_step`,
|
||||
`abaqus.semantic.step_count`, `abaqus.semantic.unsupported_step_option`,
|
||||
`abaqus.semantic.missing_static`, and
|
||||
`abaqus.semantic.invalid_static_data`.
|
||||
|
||||
## 8. Recognized no-op directives
|
||||
|
||||
These records are deliberately recognized and do not create Domain entities:
|
||||
|
||||
- `*HEADING`: global-only, no parameters, zero or more text data rows.
|
||||
- `*PREPRINT`: global-only, optional `ECHO`, `MODEL`, `HISTORY`, and `CONTACT`
|
||||
parameters, each with value `YES` or `NO`; no data.
|
||||
- `*RESTART`: Step-only, optional `WRITE` flag and optional nonnegative integer
|
||||
`FREQUENCY`; no data.
|
||||
- `*OUTPUT`: Step-only, exactly one `FIELD` or `HISTORY` flag and optional
|
||||
`VARIABLE=PRESELECT`; no data.
|
||||
|
||||
Diagnostics are the common unsupported-parameter diagnostic plus
|
||||
`abaqus.syntax.invalid_heading_scope`,
|
||||
`abaqus.syntax.invalid_preprint_scope`,
|
||||
`abaqus.syntax.invalid_restart_scope`, and
|
||||
`abaqus.syntax.invalid_output_scope`. There is no general no-op or
|
||||
ignore-unknown path.
|
||||
|
||||
## 9. Fixture matrix contract
|
||||
|
||||
The tab-separated manifest columns are:
|
||||
|
||||
```text
|
||||
case_id, outcome, fixture, expected_stage, expected_code, expected_line,
|
||||
node_count, element_count, node_set_count, element_set_count,
|
||||
prescribed_dof_count, nodal_load_count, shear_source, shear_area_y,
|
||||
shear_area_z, checked_node_set, checked_element_set
|
||||
```
|
||||
|
||||
For a valid case, the public `parse_deck()` and `map_deck_to_domain()` path must
|
||||
produce a Domain matching every populated expected field. Set checks use
|
||||
`name:local-label,...`. For an invalid case, that same public path must fail
|
||||
and contain the exact stage, code, and source line in the manifest. Partial
|
||||
decks and test-only semantic construction are not accepted.
|
||||
+348
-357
@@ -2,467 +2,458 @@
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
이 문서는 `equation-and-linear-solve` 완료 후 새 세션에서
|
||||
`results-and-pipeline` Phase를 바로 시작하기 위한 인수인계 기록이다.
|
||||
이 문서는 results-and-pipeline과 abaqus-subset-completion 완료 후 새 세션에서
|
||||
deterministic-parallel-assembly Phase를 바로 시작하기 위한 인수인계 기록이다.
|
||||
요구사항과 설계의 기준은 이 문서가 아니라 다음 파일이다.
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/PRD.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- `/docs/HARNESS.md`
|
||||
- `/docs/HDF5_SCHEMA.md` — 다음 Phase Step 1에서 writer보다 먼저 생성할 문서
|
||||
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
|
||||
- `/phases/results-and-pipeline/index.json`
|
||||
- `/phases/results-and-pipeline/step0.md`부터 `step3.md`
|
||||
- /AGENTS.md
|
||||
- /docs/PRD.md
|
||||
- /docs/ARCHITECTURE.md
|
||||
- /docs/ADR.md
|
||||
- /docs/HARNESS.md
|
||||
- /docs/ABAQUS_INPUT_SUBSET.md
|
||||
- /docs/HDF5_SCHEMA.md
|
||||
- /phases/deterministic-parallel-assembly/index.json
|
||||
- /phases/deterministic-parallel-assembly/step0.md부터 step2.md
|
||||
|
||||
내용이 충돌하면 `AGENTS.md`, 제품·아키텍처 문서와 `phases/`의 현재 상태를
|
||||
우선한다. 이 문서는 현재 구현과 실행환경에서 특히 놓치기 쉬운 계약을 보충한다.
|
||||
내용이 충돌하면 AGENTS.md, 제품·아키텍처 문서와 phases/의 현재 상태를 우선한다.
|
||||
이 문서는 현재 구현, 검증 baseline과 실행환경에서 특히 놓치기 쉬운 계약을
|
||||
보충한다.
|
||||
|
||||
## 2. 현재 저장소 상태
|
||||
|
||||
2026-07-31 확인 기준:
|
||||
2026-08-01 확인 기준:
|
||||
|
||||
- 현재 브랜치: `dev`
|
||||
- 현재 `dev` HEAD:
|
||||
`a02024929ca62c714335dd9811eea6eab816af38`
|
||||
- `origin/dev`, `origin/HEAD`:
|
||||
`d39340217421006c33bb7c61a0111d320cbaab2e`
|
||||
- 로컬 `dev`는 `origin/dev`보다 9 commit 앞서고 0 commit 뒤처져 있다.
|
||||
- 기준 브랜치: dev
|
||||
- 이 HANDOFF 작성 직전 구현 HEAD: cbb621bb282a301b6708d95ff697b40644188e46
|
||||
- cbb621b는 abaqus-subset-completion의 최종 보완 commit이다.
|
||||
- 이 문서는 cbb621b 다음 commit으로 dev에 기록하고 origin/dev에 함께 push한다.
|
||||
새 세션에서는 아래 명령으로 실제 동기화 상태를 다시 확인한다.
|
||||
- 완료 Phase:
|
||||
- `solver-bootstrap`
|
||||
- `domain-and-input-skeleton`
|
||||
- `fem-and-beam-kernel`
|
||||
- `equation-and-linear-solve`
|
||||
- 다음 Phase: `results-and-pipeline`
|
||||
- 다음 Step: `0 - result-database`
|
||||
- `results-and-pipeline`의 Step 0~3은 모두 `pending`이다.
|
||||
- `equation-and-linear-solve`는 `dev`에 fast-forward 병합되었고 로컬
|
||||
`feat-equation-and-linear-solve` 브랜치는 삭제되었다.
|
||||
- 원격 push는 수행하지 않았다.
|
||||
- solver-bootstrap
|
||||
- domain-and-input-skeleton
|
||||
- fem-and-beam-kernel
|
||||
- equation-and-linear-solve
|
||||
- results-and-pipeline
|
||||
- abaqus-subset-completion
|
||||
- 다음 Phase: deterministic-parallel-assembly
|
||||
- 다음 Step: 0 - canonical-contribution-order
|
||||
- deterministic-parallel-assembly의 Step 0~2는 모두 pending이다.
|
||||
- 후속 Phase인 result-contract-completion, beam-reference-qualification,
|
||||
internal-release도 아직 pending이다.
|
||||
- feat-abaqus-subset-completion은 dev에 fast-forward 병합된 뒤 삭제되었다.
|
||||
- 이 문서 갱신 전 작업 트리는 clean이었다.
|
||||
|
||||
이 문서를 갱신하기 직전 작업 트리는 clean이었다. 현재
|
||||
`docs/HANDOFF.md` 변경은 사용자가 별도로 요청하지 않는 한 커밋하지 않는다.
|
||||
새 세션에서 Harness를 실행하기 전에 이 변경을 먼저 커밋하거나 별도로 정리해야
|
||||
한다. 그렇지 않으면 executor의 feature branch와 Step commit에 인수인계 문서가
|
||||
섞일 수 있다.
|
||||
새 세션에서 원격에 맞추기 위한 reset, rebase, force push를 수행하지 않는다.
|
||||
먼저 다음 상태를 확인하고 dev와 origin/dev가 다르면 원인을 조사한다.
|
||||
|
||||
로컬 `dev`의 미push 9 commit을 보존한다. 원격에 맞추기 위한 reset, 강제 checkout,
|
||||
rebase 또는 force push를 수행하지 않는다.
|
||||
git switch dev
|
||||
git status --short --branch
|
||||
git rev-parse HEAD
|
||||
git rev-parse origin/dev
|
||||
git rev-list --left-right --count origin/dev...dev
|
||||
|
||||
## 3. 완료된 `equation-and-linear-solve`
|
||||
## 3. 완료된 results-and-pipeline
|
||||
|
||||
Phase metadata는 `/phases/equation-and-linear-solve/index.json`에 기록되어 있으며
|
||||
Step 0~2가 모두 `completed`다.
|
||||
Phase metadata는 /phases/results-and-pipeline/index.json에 기록되어 있으며 Step
|
||||
0~3이 모두 completed다.
|
||||
|
||||
주요 commit:
|
||||
주요 결과:
|
||||
|
||||
- `8342774` — deterministic serial symmetric CSR assembly
|
||||
- `1bf277c` — essential BC elimination과 reaction recovery
|
||||
- `e216660` — MKL PARDISO linear solver adapter
|
||||
- `2dd17be` — Phase 완료 metadata
|
||||
- `741fc9e` — 독립 review 지적과 HANDOFF 계약 수정
|
||||
- `a020249` — 완료된 Step 계약 문서 정합화
|
||||
|
||||
독립 재검토 결과 남은 Critical, Important, Minor 항목 없이 merge-ready 판정을
|
||||
받았으며, 아래 계약이 테스트로 고정되어 있다.
|
||||
|
||||
### 3.1 Symmetric CSR과 serial assembly
|
||||
- HDF5 API에 의존하지 않는 ResultDatabase, ResultStep, ResultFrame, NodalFrame
|
||||
semantic model과 results-stage validation
|
||||
- /docs/HDF5_SCHEMA.md의 schema 1.0.0 계약
|
||||
- move-only HDF5 RAII writer와 public reader round trip
|
||||
- serial assembly, essential BC, PARDISO, full reconstruction과 reaction recovery를
|
||||
조율하는 LinearStaticAnalysis
|
||||
- free equation이 0인 all-constrained 해석의 PARDISO 우회
|
||||
- parser부터 HDF5 writer까지 연결하는 run_solver와 solve CLI
|
||||
- fesa solve <model.inp> --output <results.h5> 수직 파이프라인
|
||||
|
||||
관련 파일:
|
||||
|
||||
- `/include/fesa/assembly/symmetric_csr.hpp`
|
||||
- `/include/fesa/assembly/equation_system.hpp`
|
||||
- `/include/fesa/assembly/serial_assembler.hpp`
|
||||
- `/src/fesa/assembly/serial_assembler.cpp`
|
||||
- `/tests/unit/assembly/serial_assembler_test.cpp`
|
||||
- /include/fesa/results/
|
||||
- /src/fesa/results/
|
||||
- /include/fesa/io/hdf5/
|
||||
- /src/fesa/io/hdf5/
|
||||
- /include/fesa/analysis/
|
||||
- /src/fesa/analysis/
|
||||
- /include/fesa/analysis/run_solver.hpp
|
||||
- /src/fesa/analysis/run_solver.cpp
|
||||
- /docs/HDF5_SCHEMA.md
|
||||
- /tests/integration/pipeline/minimal_cantilever_test.cpp
|
||||
|
||||
핵심 계약:
|
||||
|
||||
- `SymmetricCsr`는 0-based upper triangle만 저장한다.
|
||||
- 각 row의 column index는 strictly increasing이다.
|
||||
- 연결되지 않은 절점의 자유도를 포함해 모든 row가 diagonal entry를 갖는다.
|
||||
contribution이 없으면 값은 0이다. 따라서 이후 solver가 구조 오류가 아니라
|
||||
singular equation system으로 진단할 수 있다.
|
||||
- sparsity pattern 생성과 numeric merge는 분리되어 있다.
|
||||
- numeric contribution은
|
||||
`(row,column,element-origin,local-order)` 순서로 정렬·합산한다.
|
||||
- 비결합적인 `1 + 1 + 1e16` 규모의 테스트가 element origin 순서를 bit pattern으로
|
||||
고정한다. Domain storage order로 합산하도록 바꾸면 안 된다.
|
||||
- Beam kernel failure를 0 stiffness로 대체하지 않고 assembly 오류로 전달한다.
|
||||
- `EquationSystem`은 원래 full `stiffness`와 `force`를 소유하며 constraint 처리 전
|
||||
상태로 보존한다.
|
||||
- results semantic model은 HDF5 타입이나 handle을 노출하지 않는다.
|
||||
- 모든 HDF5 resource는 adapter 내부의 move-only RAII wrapper가 소유한다.
|
||||
- nodal result의 node ID와 6성분 displacement/reaction 배열 순서는 DofManager의
|
||||
full-vector 순서와 일치한다.
|
||||
- 반력은 reduced system이 아니라 원래 full system의 r=Ku-f에서 계산한다.
|
||||
- all-constrained system은 유효한 analysis case이며 PARDISO order 0 입력으로
|
||||
전달하지 않는다.
|
||||
- CLI와 run_solver는 parser, semantic mapper, analysis와 writer를 조율하는
|
||||
application orchestration 경계다. LinearStaticAnalysis 자체는 parser나 HDF5를
|
||||
호출하지 않는다.
|
||||
- CTest의 CLI test에는 설치된 oneAPI/HDF5 runtime PATH가 test property로
|
||||
전달된다. 기본 셸 PATH에 해당 디렉터리가 없어도 CTest가 성공해야 한다.
|
||||
|
||||
이 serial assembler는 후속 `deterministic-parallel-assembly` Phase의 oracle이다.
|
||||
이번 다음 Phase에서 TBB assembly를 선행 구현하지 않는다.
|
||||
이 vertical slice 완료는 요소 결과 계약, Abaqus reference 자격 또는 내부 배포
|
||||
완료를 의미하지 않는다.
|
||||
|
||||
### 3.2 `DofManager`와 essential BC
|
||||
## 4. 완료된 abaqus-subset-completion
|
||||
|
||||
Phase metadata는 /phases/abaqus-subset-completion/index.json에 기록되어 있으며
|
||||
Step 0~4가 모두 completed다.
|
||||
|
||||
주요 결과:
|
||||
|
||||
- /docs/ABAQUS_INPUT_SUBSET.md에 Phase 1 입력 계약을 명문화
|
||||
- public parser/mapper fixture matrix를 74 cases로 확대
|
||||
- strict keyword scope, parameter form, data ownership과 정확한 source diagnostic
|
||||
- Part/Assembly의 명시적, GENERATE, nested, forward set resolution
|
||||
- flat mesh 또는 좌표변환 없는 단일 Part/Assembly/Instance 선택
|
||||
- 전역 material, Part-local Beam section과 ELSET assignment
|
||||
- 명시적 transverse shear와 Phase 1 기본값 Asy=Asz=5A/6, SCF=0
|
||||
- 단일 Step/Static, Boundary, Cload와 명시적 no-op directive
|
||||
- active Part뿐 아니라 inactive Part의 NODE, ELEMENT, section record와 reference
|
||||
유효성 검증
|
||||
|
||||
관련 파일:
|
||||
|
||||
- `/include/fesa/fem/dof_manager.hpp`
|
||||
- `/include/fesa/constraints/essential_bc.hpp`
|
||||
- `/src/fesa/constraints/essential_bc.cpp`
|
||||
- `/tests/unit/constraints/essential_bc_test.cpp`
|
||||
- /docs/ABAQUS_INPUT_SUBSET.md
|
||||
- /include/fesa/io/abaqus/active_input.hpp
|
||||
- /include/fesa/io/abaqus/set_resolver.hpp
|
||||
- /src/fesa/io/abaqus/parser.cpp
|
||||
- /src/fesa/io/abaqus/active_input.cpp
|
||||
- /src/fesa/io/abaqus/set_resolver.cpp
|
||||
- /src/fesa/io/abaqus/semantic_mapper.cpp
|
||||
- /tests/fixtures/abaqus/contract.tsv
|
||||
- /tests/unit/io/abaqus/
|
||||
- /tests/integration/io/minimal_deck_to_domain_test.cpp
|
||||
|
||||
핵심 계약:
|
||||
특히 유지할 계약:
|
||||
|
||||
- `DofManager`가 full DOF, free equation mapping과 prescribed value를 단독 소유한다.
|
||||
- full index 기반 `equation(std::size_t)`와
|
||||
`prescribed_value(std::size_t)` query가 추가되어 있다.
|
||||
- `eliminate_essential_bcs(const EquationSystem&, const DofManager&)`에는 별도
|
||||
prescribed 목록을 전달하지 않는다.
|
||||
- `ReducedSystem`은 reduced `stiffness`와 `force`만 소유한다. free/full mapping이나
|
||||
prescribed full vector를 중복 저장하지 않는다.
|
||||
- full solution은 `DofManager::reconstruct_full()`로 복원한다.
|
||||
- 0과 비영 prescribed value의 RHS shift, all-constrained order 0 system, 원본 system
|
||||
불변성이 검증되어 있다.
|
||||
- 반력은 reduced system이 아니라 원래 full system의 `r=Ku-f`에서 계산한다.
|
||||
- 입력뿐 아니라 RHS shift와 reaction 산술 결과가 NaN/Inf가 되는 경우도 성공
|
||||
결과로 반환하지 않는다.
|
||||
- 사용되지 않는 Part는 파싱·record/reference validation하지만 Domain에는 넣지 않는다.
|
||||
- NODE와 ELEMENT data row의 field 수는 정확해야 한다.
|
||||
- enum 값 B31과 GENERAL은 대소문자를 구분하지 않는다.
|
||||
- 계층형 Step의 Boundary/Cload target은 Assembly node set으로 해석한다.
|
||||
- 모든 active B31 element만 정확히 하나의 section assignment를 가져야 한다.
|
||||
inactive Part의 완전한 해석 가능성을 요구하도록 범위를 넓히지 않는다.
|
||||
- missing_section diagnostic은 element keyword가 아니라 해당 element data row를
|
||||
source로 사용한다.
|
||||
- unsupported keyword/parameter를 일반 ignore 경로로 숨기지 않는다.
|
||||
- Heading, Preprint, Restart, Output만 문서화된 조건에서 no-op으로 허용한다.
|
||||
|
||||
`Domain::step().prescribed_dofs`를 다시 순회해 별도 constraint 상태를 만들지 않는다.
|
||||
최종 독립 review에서 Critical, Important, Minor finding은 모두 0건이었다.
|
||||
|
||||
### 3.3 MKL PARDISO adapter
|
||||
## 5. 현재 serial assembly oracle
|
||||
|
||||
관련 파일:
|
||||
다음 Phase가 변경할 핵심 코드는 현재 /src/fesa/assembly/serial_assembler.cpp 한
|
||||
파일에 private helper로 모여 있다.
|
||||
|
||||
- `/include/fesa/solvers/linear/linear_solver.hpp`
|
||||
- `/include/fesa/solvers/linear/pardiso_linear_solver.hpp`
|
||||
- `/src/fesa/solvers/linear/pardiso_linear_solver.cpp`
|
||||
- `/tests/unit/solvers/linear/pardiso_linear_solver_test.cpp`
|
||||
현재 흐름:
|
||||
|
||||
핵심 계약:
|
||||
1. 모든 full DOF의 diagonal을 포함하는 upper-triangle CSR sparsity pattern 생성
|
||||
2. 각 Beam 요소의 compute_beam3d2 호출
|
||||
3. local upper triangle 78개를 NumericContribution으로 수집
|
||||
4. row, column, EntityOrigin, local_order 순으로 정렬
|
||||
5. 같은 row/column을 고정된 순서로 합산
|
||||
6. nodal load를 full force vector로 조립
|
||||
|
||||
- public `LinearSolver` 계약에는 MKL 타입이 노출되지 않는다.
|
||||
- `PardisoLinearSolver`는 noncopyable이다.
|
||||
- MKL LP64 `MKL_INT == std::int32_t`, `mtype=2`, `iparm[26]=1` matrix checker,
|
||||
`iparm[34]=1` 0-based indexing을 사용한다.
|
||||
- analysis, factorization, solve와 release phase를 adapter 내부에서 관리한다.
|
||||
- release는 destructor fallback 외에 명시적으로 호출되어 release error도
|
||||
`DiagnosticStage::solver`의 `solver.release_failed`로 변환된다.
|
||||
- matrix/RHS validation, singular diagnostic, repeated solve와 adapter 밖의 독립
|
||||
상대잔차 계산이 검증되어 있다.
|
||||
- order 0 reduced system은 constraint 계층에서는 유효하지만 PARDISO 입력으로는
|
||||
거부된다. analysis orchestration이 all-constrained case를 별도로 처리해야 한다.
|
||||
관련 public 계약:
|
||||
|
||||
## 4. 검증된 baseline과 개발환경
|
||||
- /include/fesa/assembly/symmetric_csr.hpp
|
||||
- /include/fesa/assembly/equation_system.hpp
|
||||
- /include/fesa/assembly/serial_assembler.hpp
|
||||
- /src/fesa/assembly/serial_assembler.cpp
|
||||
- /tests/unit/assembly/serial_assembler_test.cpp
|
||||
|
||||
현재 테스트가 고정하는 oracle:
|
||||
|
||||
- SymmetricCsr는 0-based upper triangle만 저장한다.
|
||||
- row_offsets와 column_indices는 유효하며 각 row의 column이 strictly increasing이다.
|
||||
- 연결되지 않은 node DOF도 값 0의 diagonal entry를 갖는다.
|
||||
- element storage order와 external label 순열이 결과를 바꾸지 않는다.
|
||||
- 1 + 1 + 1e16 규모의 contribution은 EntityOrigin 순으로 합산한 bit pattern을
|
||||
고정한다.
|
||||
- Beam kernel failure를 0 contribution으로 바꾸지 않고 assembly failure로
|
||||
전달한다.
|
||||
- full force vector의 load accumulation도 기존 결과와 같아야 한다.
|
||||
|
||||
parallel 구현을 이유로 이 serial oracle을 먼저 변경하거나 tolerance 비교로
|
||||
약화하지 않는다.
|
||||
|
||||
## 6. 검증된 baseline과 개발환경
|
||||
|
||||
2026-08-01 현재 확인한 도구:
|
||||
|
||||
- CMake 4.4.0
|
||||
- MSBuild 18.8.2.30814
|
||||
- Visual Studio 2026 MSVC v145, Windows x64
|
||||
- codex-cli 0.146.0
|
||||
- Intel oneAPI MKL/TBB 2026.1
|
||||
- HDF5 2.1.1
|
||||
- GoogleTest 1.17.0, v145 x64 CRT build
|
||||
|
||||
새 PowerShell 세션에서 configure 또는 Harness 실행 전에 다음 환경 변수를 설정한다.
|
||||
절대경로를 tracked CMake 파일이나 Preset에 넣지 않는다.
|
||||
|
||||
```powershell
|
||||
$env:MKL_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\mkl"
|
||||
$env:TBB_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\tbb"
|
||||
$env:HDF5_DIR = "C:\Program Files\HDF_Group\HDF5\2.1.1\cmake"
|
||||
$env:GTest_DIR = "C:\Users\baram\AppData\Local\FESA\dependencies\googletest-1.17.0-v145-x64-crt\lib\cmake\GTest"
|
||||
$env:MKL_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\mkl"
|
||||
$env:TBB_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\tbb"
|
||||
$env:HDF5_DIR = "C:\Program Files\HDF_Group\HDF5\2.1.1\cmake"
|
||||
$env:GTest_DIR = "C:\Users\baram\AppData\Local\FESA\dependencies\googletest-1.17.0-v145-x64-crt\lib\cmake\GTest"
|
||||
|
||||
Test-Path "$env:MKL_DIR\MKLConfig.cmake"
|
||||
Test-Path "$env:TBB_DIR\TBBConfig.cmake"
|
||||
Test-Path "$env:HDF5_DIR\hdf5-config.cmake"
|
||||
Test-Path "$env:GTest_DIR\GTestConfig.cmake"
|
||||
```
|
||||
Test-Path "$env:MKL_DIR\MKLConfig.cmake"
|
||||
Test-Path "$env:TBB_DIR\TBBConfig.cmake"
|
||||
Test-Path "$env:HDF5_DIR\hdf5-config.cmake"
|
||||
Test-Path "$env:GTest_DIR\GTestConfig.cmake"
|
||||
|
||||
2026-07-31 현재 네 package 경로가 모두 존재한다. 현재 `dev` HEAD에서 다음
|
||||
baseline을 검증했다.
|
||||
네 package config와 다음 h5ls 경로가 존재함을 확인했다.
|
||||
|
||||
- MSBuild 18.8.2, MSVC v145 Debug build 성공
|
||||
- build 출력에 새 warning 없음
|
||||
- CTest 29개 중 29개 성공
|
||||
- Harness pytest 20개 중 20개 성공
|
||||
- pytest가 실제로 20개를 수집했으므로 0-test 성공이 아님
|
||||
C:\Program Files\HDF_Group\HDF5\2.1.1\bin\h5ls.exe
|
||||
|
||||
검증 명령:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
uv run --with pytest python -m pytest -v -rs
|
||||
```
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
uv run --with pytest python -m pytest -v -rs
|
||||
|
||||
현재 baseline:
|
||||
|
||||
- MSVC Debug build 성공, 새 warning 없음
|
||||
- CTest 49개 중 49개 성공
|
||||
- Harness pytest 20개 중 20개 성공
|
||||
- pytest가 실제 20개를 수집했으므로 0-test 성공이 아님
|
||||
- 최소 cantilever CLI solve와 HDF5 public schema inspection 성공
|
||||
|
||||
CMake cache가 없거나 package 경로가 바뀐 경우에만 같은 환경 변수 세션에서 먼저
|
||||
다음을 실행한다.
|
||||
|
||||
```powershell
|
||||
cmake --fresh --preset windows-debug
|
||||
```
|
||||
cmake --fresh --preset windows-debug
|
||||
|
||||
HDF5 command-line tool은 현재 PATH에 없지만 다음 파일은 존재한다.
|
||||
h5ls를 직접 실행할 때는 HDF5 DLL 외에 Intel libmmd.dll이 필요하다. HDF5 bin만
|
||||
PATH에 추가하면 Windows exit 0xC0000135가 발생할 수 있으므로 두 runtime
|
||||
디렉터리를 현재 세션 PATH에 추가한다. 시스템 PATH는 영구 변경하지 않는다.
|
||||
|
||||
```text
|
||||
C:\Program Files\HDF_Group\HDF5\2.1.1\bin\h5ls.exe
|
||||
```
|
||||
$env:PATH = @(
|
||||
"C:\Program Files\HDF_Group\HDF5\2.1.1\bin",
|
||||
"C:\Program Files (x86)\Intel\oneAPI\2026.1\bin",
|
||||
$env:PATH
|
||||
) -join ";"
|
||||
h5ls --version
|
||||
|
||||
`h5ls.exe`는 HDF5 DLL 외에 Intel `libmmd.dll`도 요구한다. HDF5 `bin`만 PATH에
|
||||
추가하면 Windows exit `0xC0000135`로 실패하므로 Step 1과 Step 3 Acceptance
|
||||
Criteria에서는 HDF5와 oneAPI `bin`을 모두 현재 세션 PATH에 추가한다. 시스템
|
||||
PATH를 영구 변경하지 않는다.
|
||||
## 7. 다음 Phase 목표와 Step 순서
|
||||
|
||||
```powershell
|
||||
$env:PATH = @(
|
||||
"C:\Program Files\HDF_Group\HDF5\2.1.1\bin",
|
||||
"C:\Program Files (x86)\Intel\oneAPI\2026.1\bin",
|
||||
$env:PATH
|
||||
) -join ";"
|
||||
h5ls --version
|
||||
```
|
||||
deterministic-parallel-assembly의 목표는 oneTBB로 요소 계산을 병렬화하면서
|
||||
serial oracle과 thread count 사이의 CSR, RHS와 최종 해석 결과를 bit-for-bit
|
||||
동일하게 유지하는 것이다.
|
||||
|
||||
2026-07-31 현재 이 설정에서 `h5ls: Version 2.1.1`과 exit code 0을 확인했다.
|
||||
### Step 0 - canonical-contribution-order
|
||||
|
||||
## 5. 다음 Phase 목표와 Step 순서
|
||||
- serial assembly를 변경하지 않은 상태에서 병렬 경로가 공유할
|
||||
MatrixContribution과 canonical merge contract를 분리한다.
|
||||
- 입력 순열, 같은 row/column의 여러 element, cancellation과 signed zero를 포함한
|
||||
실패 테스트를 먼저 작성한다.
|
||||
- 정렬 key는 row, column, stable element identity, local_order다.
|
||||
- canonicalize_contributions와 merge_contributions는 하나의 고정 total order와
|
||||
하나의 merge 구현만 가져야 한다.
|
||||
- 아직 TBB code를 추가하지 않는다.
|
||||
|
||||
`results-and-pipeline`의 독립 deliverable은 nodal result semantic model과 최소 HDF5
|
||||
schema를 만들고, 이미 구현된 parser부터 linear solve까지 production 경로를
|
||||
조율해 `fesa solve ... --output ...` 수직 슬라이스를 완성하는 것이다.
|
||||
Focused acceptance:
|
||||
|
||||
이 milestone은 입력-해석-출력 경로가 연결되었다는 뜻일 뿐 Beam의 수치 자격 완료나
|
||||
Phase 1 내부 배포 완료를 의미하지 않는다. 요소 결과 회복, 완전한 자기완결 HDF5,
|
||||
Abaqus reference qualification은 뒤의 별도 Phase에 남아 있다.
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R "CanonicalContribution|DeterministicMerge" --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
|
||||
### Step 0 — `result-database`
|
||||
### Step 1 - tbb-element-evaluation
|
||||
|
||||
- `/include/fesa/results/`와 `/src/fesa/results/`에 HDF5와 독립적인 최소 semantic
|
||||
result model을 만든다.
|
||||
- 현재 Step 계약의 실체는 `ResultDatabase -> ResultStep -> ResultFrame ->
|
||||
NodalFrame`이다.
|
||||
- 이 Phase에는 전역 좌표계 nodal displacement/rotation과 reaction/moment만 담는다.
|
||||
- node ID와 6-component field의 size 일치, duplicate node, duplicate step/frame,
|
||||
nonfinite 값을 실패 테스트로 먼저 고정한다.
|
||||
- aggregate를 그대로 공개하면서 invalid state를 나중에 검사할지, 검증된 factory를
|
||||
둘지 구현 전에 하나로 정한다. 현재 Step의 “invalid result model을 거부” 조건을
|
||||
실제 호출 가능한 API로 표현해야 한다.
|
||||
- element result, field/history 범용 hierarchy, velocity, acceleration, temperature를
|
||||
미리 만들지 않는다.
|
||||
- oneTBB는 독립적인 Beam element evaluation에만 사용한다.
|
||||
- worker는 thread-local contribution을 만들며 공유 CSR values에 쓰지 않는다.
|
||||
- merge는 Step 0의 canonical serial 순서를 그대로 사용한다.
|
||||
- AssemblyOptions의 max_threads와 grain_size로 실행을 제한한다.
|
||||
- max_threads=1과 2 이상에서 serial/parallel CSR와 force를 bit-for-bit 비교한다.
|
||||
- PARDISO를 TBB task 안에서 호출하지 않는다.
|
||||
|
||||
### Step 1 — `minimal-hdf5-schema`
|
||||
Focused acceptance:
|
||||
|
||||
- writer code보다 먼저 `/docs/HDF5_SCHEMA.md`에 schema `1.0.0`의 정확한 계약을
|
||||
작성한다.
|
||||
- 최소 writer/reader adapter는 `/include/fesa/io/hdf5/`와
|
||||
`/src/fesa/io/hdf5/`에 둔다.
|
||||
- schema/version, node origin `(part,instance,local label)`, dense ID map, 좌표,
|
||||
connectivity, 적용 전단강성과 input/default source, nodal displacement/reaction을
|
||||
round trip한다.
|
||||
- public reader와 `h5ls`로 writer 산출물을 다시 연다. production writer 내부 상태나
|
||||
test-only parser로 검증하지 않는다.
|
||||
- 모든 `hid_t`와 HDF5 resource는 move-only RAII wrapper 내부에 둔다.
|
||||
- 모든 HDF5 실패를 `DiagnosticStage::results`의 diagnostic 또는 adapter 경계에서
|
||||
포착되는 오류로 변환한다.
|
||||
- reference CSV와 아직 존재하지 않는 결과를 위한 빈 group을 추가하지 않는다.
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R "ParallelAssembly|TbbElementEvaluation" --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
|
||||
### Step 2 — `linear-static-analysis`
|
||||
### Step 2 - thread-count-determinism
|
||||
|
||||
- `/include/fesa/analysis/`와 `/src/fesa/analysis/`에 orchestration만 구현한다.
|
||||
- 이미 검증된 `Domain`을 입력받으며 parser, CLI, HDF5를 호출하지 않는다.
|
||||
- 실행 순서는 `DofManager -> serial assembly -> essential BC -> PARDISO -> full
|
||||
reconstruction -> reaction -> nodal ResultDatabase`다.
|
||||
- free equation 수가 0이면 PARDISO를 호출하지 않고 prescribed full vector와 원래
|
||||
평형식으로 결과를 만든다.
|
||||
- nodal result의 ID 순서와 full vector의 6-DOF block 순서가 반드시 일치해야 한다.
|
||||
`DofManager` numbering이 internal `NodeId` 정렬 기반이므로 Domain storage order를
|
||||
묵시적으로 사용하지 않는다.
|
||||
- hand-check 가능한 한 요소 Domain으로 displacement, reaction, solver residual과
|
||||
`Ku-f-r` 평형을 실패 테스트로 먼저 고정한다.
|
||||
- 수치 kernel, constraint 또는 solver 로직을 analysis에 복제하지 않는다.
|
||||
- thread count 1, 2, available concurrency에서 CSR, RHS, displacement와 reaction을
|
||||
bit-for-bit 비교한다.
|
||||
- 최소 10회 반복해 scheduling 변화 회귀를 검사한다.
|
||||
- 측정용 fesa_assembly_benchmark를 추가해 serial/parallel 시간과 element count를
|
||||
출력한다.
|
||||
- benchmark speedup은 환경 의존적이므로 pass 조건으로 만들지 않는다.
|
||||
- assembly test 동안 MKL thread 수를 중첩해 키우지 않는다.
|
||||
|
||||
### Step 3 — `cli-pipeline-integration`
|
||||
Focused acceptance:
|
||||
|
||||
- `run_solver(const AnalysisRequest&)`가 parser, semantic mapper, Domain, analysis와
|
||||
HDF5 writer를 application 경계에서 조율한다.
|
||||
- 현재 parser API는 `parse_deck(path)`, semantic mapping API는
|
||||
`map_deck_to_domain(deck)`이다.
|
||||
- CLI 계약은 다음 두 가지다.
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R ThreadCountDeterminism --output-on-failure
|
||||
.\out\build\windows-debug\Debug\fesa_assembly_benchmark.exe
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
|
||||
```text
|
||||
fesa solve <model.inp> --output <results.h5>
|
||||
fesa --version
|
||||
```
|
||||
## 8. 구현 전에 정렬할 설계점
|
||||
|
||||
- `/tests/fixtures/abaqus/minimal_cantilever.inp`는 이미 존재한다.
|
||||
- `MinimalCantileverPipeline`은 `run_solver` 또는 CLI와 public HDF5 reader만 사용한다.
|
||||
- 성공 시 node ID, finite displacement, reaction/equilibrium diagnostic과 schema path를
|
||||
검증한다.
|
||||
- parse, semantic, equation, solver 또는 results 실패는 원래 diagnostic stage와
|
||||
source 정보를 보존하고 CLI nonzero exit로 전달한다.
|
||||
- hierarchical fixture의 아직 미지원 keyword를 Step 3에서 우회 처리하지 않는다.
|
||||
전체 Abaqus subset은 다음 `abaqus-subset-completion` Phase의 책임이다.
|
||||
아래 항목은 범위를 늘리라는 의미가 아니다. Step 계약과 현재 serial oracle 사이에서
|
||||
구현 전에 결정하고 테스트로 고정할 최소 질문이다.
|
||||
|
||||
## 6. 구현 전에 명시적으로 정렬할 설계점
|
||||
1. Stable element identity
|
||||
- Step 0 초안의 MatrixContribution은 ElementId를 제시한다.
|
||||
- 현재 oracle은 EntityOrigin의 instance_name, local_label, part_name 순으로
|
||||
정렬한다.
|
||||
- ElementId만으로 기존 bit pattern과 storage-order 독립성을 보존할 수 있는지
|
||||
먼저 확인한다. 확인 없이 oracle의 정렬 key를 바꾸지 않는다.
|
||||
|
||||
아래 항목은 범위를 늘리라는 의미가 아니다. 현재 Step 문서와 최종 PRD 사이의
|
||||
모호함을 구현 전에 드러내고 가장 단순한 일관된 계약을 선택하기 위한 확인 목록이다.
|
||||
2. Canonical API와 serial 경로
|
||||
- Step 0은 serial assembly를 변경하지 말라고 요구하면서 공통 merge contract를
|
||||
분리한다.
|
||||
- 먼저 public/internal 경계를 최소화하고, 기존 assemble_serial의 관찰 가능한
|
||||
결과가 bit-for-bit 유지되는 테스트를 둔다.
|
||||
- 실제 두 번째 사용처가 생기기 전에 범용 registry나 backend hierarchy를 만들지
|
||||
않는다.
|
||||
|
||||
1. **Result model 유효성 API**
|
||||
- Step 0은 invalid model을 거부하라고 하지만 제시된 타입은 public aggregate다.
|
||||
- raw aggregate + 별도 validation과 validated factory 중 하나를 선택하고 테스트가
|
||||
production validation 경로를 통과하게 한다.
|
||||
3. Parallel failure ordering
|
||||
- worker에서 여러 Beam kernel failure가 발생해도 scheduling 순서로 어느 오류를
|
||||
반환할지 결정하면 재현성이 깨진다.
|
||||
- shared CSR write나 first-writer-wins exception 상태를 만들지 말고 canonical
|
||||
element identity 기준으로 deterministic하게 처리한다.
|
||||
|
||||
2. **HDF5 reader의 model metadata 반환 범위**
|
||||
- Step 1의 `Hdf5ReadResult` 초안은 `ResultDatabase`만 반환하지만 round-trip 조건은
|
||||
node origin, 좌표, connectivity, shear source까지 재검증하라고 한다.
|
||||
- 이 metadata를 `ResultDatabase`에 억지로 넣지 말고, reader inspection model을
|
||||
최소로 추가하거나 read result 계약을 정렬한다. `Domain`을 HDF5 API 타입으로
|
||||
오염시키지 않는다.
|
||||
4. AssemblyOptions validation
|
||||
- max_threads와 grain_size의 0 의미를 묵시적으로 정하지 않는다.
|
||||
- automatic 또는 invalid 중 가장 단순한 계약을 선택하고 focused test로 고정한다.
|
||||
|
||||
3. **schema `1.0.0`의 최소/최종 범위**
|
||||
- 이번 Phase는 nodal vertical slice만 구현하고 `result-contract-completion`이
|
||||
element 결과와 완전한 자기완결 계약을 뒤에서 채운다.
|
||||
- 빈 미래 hierarchy는 만들지 않되, `/docs/HDF5_SCHEMA.md`에 이번 최소 required
|
||||
dataset과 이후 additive compatibility 규칙을 분명히 구분한다.
|
||||
5. Force assembly
|
||||
- 현재 RHS는 nodal load를 serial full-vector 순서로 누적한다.
|
||||
- 이번 Phase는 element evaluation 병렬화가 목적이다. force accumulation을 별도
|
||||
병렬 기능으로 확장하지 말고 serial oracle과 bitwise equality를 유지한다.
|
||||
|
||||
4. **deterministic nodal ordering**
|
||||
- `DofManager` full vector는 sorted internal `NodeId` 순서다.
|
||||
- `NodalFrame::node_ids`와 6-component displacement/reaction 배열을 같은 순서로
|
||||
만드는 최소 query 또는 정렬 로직을 한 곳에서만 소유한다.
|
||||
6. Benchmark 격리
|
||||
- benchmark는 제품 correctness test를 우회하는 별도 assembly를 사용하지 않는다.
|
||||
- fixed Domain과 production serial/parallel API를 호출한다.
|
||||
- speedup이나 release 성능 목표를 임의 assertion으로 추가하지 않는다.
|
||||
|
||||
5. **all-constrained analysis**
|
||||
- reduced order 0은 constraint 성공이고 PARDISO invalid input이다.
|
||||
- Step 2가 solver 호출을 생략하는 명시적 branch를 갖고 prescribed displacement와
|
||||
full reaction을 계산한다.
|
||||
## 9. 아키텍처와 범위 경계
|
||||
|
||||
6. **analysis와 application 경계**
|
||||
- Step 2 `LinearStaticAnalysis`는 Domain-to-ResultDatabase만 담당한다.
|
||||
- Step 3 `run_solver`가 parser와 HDF5를 담당한다. ARCHITECTURE의 포괄적 lifecycle
|
||||
설명을 이유로 HDF5 API를 analysis에 직접 넣지 않는다.
|
||||
- oneTBB 의존성은 assembly adapter/implementation 경계에 가둔다.
|
||||
- core, model, fem, elements의 public contract에 TBB type을 노출하지 않는다.
|
||||
- Beam kernel은 element-local contribution만 계산하고 전역 CSR을 알지 않는다.
|
||||
- DofManager가 DOF와 equation mapping을 계속 단독 소유한다.
|
||||
- worker는 공유 CSR values에 atomic add하지 않는다.
|
||||
- contribution의 부동소수점 합산 순서를 thread scheduling과 분리한다.
|
||||
- TBB element work가 모두 끝난 후에만 PARDISO를 호출한다.
|
||||
- 기존 assemble_serial은 oracle로 유지한다.
|
||||
- tolerance 비교를 bitwise 재현성의 대체물로 사용하지 않는다.
|
||||
- result-contract-completion의 element result, HDF5 확장, CSV adapter를 선행하지
|
||||
않는다.
|
||||
- beam-reference-qualification의 Abaqus 골든 비교와 tolerance 작업을 선행하지
|
||||
않는다.
|
||||
- internal-release의 installer, Release package와 validation report를 선행하지
|
||||
않는다.
|
||||
|
||||
## 7. 아키텍처와 범위 경계
|
||||
## 10. Harness child 환경
|
||||
|
||||
- `results`는 HDF5 API에 의존하지 않는 semantic model이다.
|
||||
- `io/hdf5`만 HDF5 C API, schema version과 resource lifetime을 안다.
|
||||
- `analysis`는 기존 production 모듈을 조율하지만 수치 kernel과 외부 API를
|
||||
재구현하지 않는다.
|
||||
- `run_solver`와 CLI는 application 경계다. CLI parsing을 `fesa_core`의 analysis
|
||||
객체에 넣지 않는다.
|
||||
- `core`, `model`, `fem`, `elements`는 HDF5 API에 의존하지 않는다.
|
||||
- 외부 ID와 internal dense index mapping을 혼동하지 않는다.
|
||||
- HDF5 파일에 단위 변환을 추가하지 않는다. FESA 입력과 결과는 일관 단위계를
|
||||
전제로 한다.
|
||||
- element section result, point stress, reference CSV, TBB parallel assembly,
|
||||
다중 Step/Instance와 미지원 Abaqus keyword를 선행 구현하지 않는다.
|
||||
- 성공 경로에 fake stiffness, fake result 또는 test-only solver를 사용하지 않는다.
|
||||
이전 Harness 실행에서 WindowsApps PowerShell을 child process로 시작할 때 access
|
||||
denied가 발생했다. 확인된 구성:
|
||||
|
||||
## 8. Child sandbox의 MSBuild 실행 조건
|
||||
- standalone Codex release:
|
||||
C:\Users\baram\.codex\packages\standalone\releases\0.146.0-x86_64-pc-windows-msvc
|
||||
- WindowsApps가 제거된 PATH
|
||||
- Windows PowerShell 5.1
|
||||
- Harness 실행 동안만 [windows] sandbox = "unelevated"
|
||||
|
||||
이전 두 Harness Phase에서 child sandbox를 조사한 결과:
|
||||
현재 C:\Users\baram\.codex\config.toml은 원래 값인
|
||||
[windows] sandbox = "elevated"로 복원되어 있음을 2026-08-01에 확인했다.
|
||||
|
||||
- 앱 설치 경로의 `codex-cli 0.146.0`은 matching `codex-resources`를 찾지 못했다.
|
||||
- 완전한 standalone 배포는 다음 위치에 있으며 현재 `codex.exe`와
|
||||
`codex-resources`가 모두 존재한다.
|
||||
`C:\Users\baram\.codex\packages\standalone\releases\0.146.0-x86_64-pc-windows-msvc`
|
||||
- WindowsApps 경유 PowerShell에서는 child command가 실패했고 Windows PowerShell
|
||||
5.1에서는 정상 동작했다.
|
||||
- `[windows] sandbox = "elevated"`에서는 MSBuild FileTracker가 access denied로
|
||||
실패했다.
|
||||
- standalone Codex, WindowsApps가 제거된 PATH와
|
||||
`[windows] sandbox = "unelevated"` 조합에서는 child MSBuild가 성공했다.
|
||||
|
||||
현재 `C:\Users\baram\.codex\config.toml`은 원래 값인
|
||||
`[windows] sandbox = "elevated"`로 복원되어 있다. 다음 Harness 실행에서도 같은
|
||||
문제가 재현되면 사용자가 이전에 승인한 임시 전환 방식에 따라 다음 순서를 사용한다.
|
||||
|
||||
1. 다른 Codex 작업에 미칠 영향을 확인하고 global config 원래 값을 기록한다.
|
||||
2. Harness 실행 동안만 `[windows] sandbox = "unelevated"`로 바꾼다.
|
||||
3. 현재 PowerShell 세션의 PATH 앞에 standalone `bin`과 Windows PowerShell 5.1을
|
||||
둔다. 모든 `WindowsApps` entry를 제거해야 하며 alias 디렉터리 하나만 제거하면
|
||||
direct package 경로가 남을 수 있다.
|
||||
|
||||
```powershell
|
||||
$codexReleaseBin = "C:\Users\baram\.codex\packages\standalone\releases\0.146.0-x86_64-pc-windows-msvc\bin"
|
||||
$windowsPowerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0"
|
||||
$filteredPath = $env:PATH -split ";" | Where-Object {
|
||||
$_ -and
|
||||
$_ -ne $codexReleaseBin -and
|
||||
$_ -ne $windowsPowerShell -and
|
||||
$_ -notmatch "WindowsApps" -and
|
||||
$_ -notmatch "\\OpenAI\\Codex\\bin$"
|
||||
}
|
||||
$env:PATH = (@($codexReleaseBin, $windowsPowerShell) + $filteredPath) -join ";"
|
||||
|
||||
(Get-Command codex).Source
|
||||
(Get-Command powershell).Source
|
||||
```
|
||||
같은 문제가 재현될 때만 다음 순서를 사용한다.
|
||||
|
||||
1. 다른 Codex 작업에 미칠 영향을 확인하고 config 원래 값을 기록한다.
|
||||
2. Harness 실행 동안만 sandbox를 unelevated로 바꾼다.
|
||||
3. 현재 PowerShell 세션 PATH 앞에 standalone bin과 Windows PowerShell 5.1을
|
||||
두고 모든 WindowsApps entry를 제거한다.
|
||||
4. package 환경 변수와 baseline을 확인한 뒤 같은 세션에서 Harness를 실행한다.
|
||||
5. 성공·실패와 무관하게 `finally`에 해당하는 정리 단계에서 global config를 즉시
|
||||
`[windows] sandbox = "elevated"`로 복원하고 실제 값을 다시 읽어 확인한다.
|
||||
5. 성공·실패와 무관하게 finally에 해당하는 정리 단계에서 global config를 즉시
|
||||
elevated로 복원하고 실제 값을 다시 읽는다.
|
||||
|
||||
사용자 profile 전체나 드라이브 루트를 `--codex-add-dir`로 허용하지 않는다. 설치
|
||||
버전이나 경로가 달라졌다면 위 절대경로를 맹목적으로 사용하지 말고 실제 standalone
|
||||
release와 `codex-resources` 존재를 먼저 확인한다.
|
||||
$codexReleaseBin = "C:\Users\baram\.codex\packages\standalone\releases\0.146.0-x86_64-pc-windows-msvc\bin"
|
||||
$windowsPowerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0"
|
||||
$filteredPath = $env:PATH -split ";" | Where-Object {
|
||||
$_ -and
|
||||
$_ -ne $codexReleaseBin -and
|
||||
$_ -ne $windowsPowerShell -and
|
||||
$_ -notmatch "WindowsApps" -and
|
||||
$_ -notmatch "\\OpenAI\\Codex\\bin$"
|
||||
}
|
||||
$env:PATH = (@($codexReleaseBin, $windowsPowerShell) + $filteredPath) -join ";"
|
||||
|
||||
이전 Harness는 모든 Step과 phase commit을 완료한 뒤 stderr reader의 CP949/UTF-8
|
||||
decode 예외를 출력한 적이 있다. exit code, phase metadata와 Git commit이 정상이면
|
||||
그 메시지만으로 제품 실패로 단정하지 않는다.
|
||||
(Get-Command codex).Source
|
||||
(Get-Command powershell).Source
|
||||
|
||||
## 9. 새 세션 시작 절차
|
||||
사용자 profile 전체나 drive root를 --codex-add-dir로 허용하지 않는다. 설치 버전이나
|
||||
경로가 달라졌다면 위 절대경로를 그대로 사용하지 말고 실제 standalone release와
|
||||
codex-resources 존재를 먼저 확인한다.
|
||||
|
||||
먼저 이 문서와 다음 Phase 파일을 모두 읽는다.
|
||||
Harness가 모든 Step과 phase commit을 완료한 뒤 stderr reader의 CP949/UTF-8 decode
|
||||
예외를 출력할 수 있다. exit code만 보지 말고 phase metadata, output JSON, Git
|
||||
commit과 전체 검증 결과를 함께 확인한다.
|
||||
|
||||
```text
|
||||
/phases/results-and-pipeline/index.json
|
||||
/phases/results-and-pipeline/step0.md
|
||||
/phases/results-and-pipeline/step1.md
|
||||
/phases/results-and-pipeline/step2.md
|
||||
/phases/results-and-pipeline/step3.md
|
||||
```
|
||||
## 11. 새 세션 시작 절차
|
||||
|
||||
그 다음 저장소 상태를 재검증한다.
|
||||
먼저 이 문서와 다음 파일을 모두 읽는다.
|
||||
|
||||
```powershell
|
||||
git switch dev
|
||||
git status --short --branch
|
||||
git rev-parse HEAD
|
||||
git rev-parse origin/dev
|
||||
git rev-list --left-right --count origin/dev...dev
|
||||
```
|
||||
/phases/deterministic-parallel-assembly/index.json
|
||||
/phases/deterministic-parallel-assembly/step0.md
|
||||
/phases/deterministic-parallel-assembly/step1.md
|
||||
/phases/deterministic-parallel-assembly/step2.md
|
||||
/src/fesa/assembly/serial_assembler.cpp
|
||||
/tests/unit/assembly/serial_assembler_test.cpp
|
||||
|
||||
`docs/HANDOFF.md` 변경이 남아 있으면 먼저 사용자의 의도대로 커밋하거나 정리한다.
|
||||
4절의 package 환경 변수를 설정하고 baseline을 실행한다. 필요하면 8절의 child
|
||||
sandbox 조건을 적용한 뒤 다음을 실행한다.
|
||||
그 다음 Git과 package 상태를 재검증하고 6절의 baseline 명령을 실행한다. 필요하면
|
||||
10절의 child sandbox 조건을 적용한 뒤 다음을 실행한다.
|
||||
|
||||
```powershell
|
||||
python scripts/execute.py results-and-pipeline
|
||||
```
|
||||
python scripts/execute.py deterministic-parallel-assembly
|
||||
|
||||
executor는 `feat-results-and-pipeline` 브랜치를 생성하거나 checkout하고 Step 상태와
|
||||
output metadata를 기록한다. 사용자가 명시적으로 요청하지 않은 한 `--push`를
|
||||
사용하지 않는다.
|
||||
executor는 feat-deterministic-parallel-assembly 브랜치를 생성하거나 checkout하고
|
||||
Step 상태와 output metadata를 기록한다. 사용자가 명시적으로 요청하지 않은 한
|
||||
--push를 사용하지 않는다.
|
||||
|
||||
각 Step은 다음 순서를 지킨다.
|
||||
|
||||
1. Step 파일의 필수 문서와 선행 구현을 모두 읽는다.
|
||||
2. 성공 기준과 semantic/schema/lifecycle invariant를 명시한다.
|
||||
3. 모호한 계약은 6절을 기준으로 구현 전에 정렬한다.
|
||||
2. 성공 기준과 canonical ordering/threading invariant를 명시한다.
|
||||
3. 8절의 모호한 계약을 구현 전에 정렬한다.
|
||||
4. 실패 테스트를 먼저 작성하고 예상한 이유로 실패함을 확인한다.
|
||||
5. 테스트를 통과시키는 최소 production code만 구현한다.
|
||||
6. focused test, 전체 CTest와 Harness pytest를 실행한다.
|
||||
7. Step summary와 output metadata가 실제 결과와 일치하는지 확인한다.
|
||||
8. Phase 종료 전 전체 diff를 아키텍처, schema, diagnostic과 resource lifetime
|
||||
기준으로 review한다.
|
||||
8. Phase 종료 전 전체 diff를 determinism, race avoidance, TBB 경계와 기존 serial
|
||||
oracle 기준으로 review한다.
|
||||
|
||||
## 10. 다음 Phase 완료 조건
|
||||
## 12. 다음 Phase 완료 조건
|
||||
|
||||
- `/phases/results-and-pipeline/index.json`의 Step 0~3이 모두 `completed`
|
||||
- `/phases/index.json`에서 `results-and-pipeline`이 `completed`
|
||||
- invalid nodal result model과 finite-data invariant 테스트 통과
|
||||
- `/docs/HDF5_SCHEMA.md`가 writer보다 먼저 작성되고 실제 산출물과 일치
|
||||
- public HDF5 reader round trip과 `h5ls` schema inspection 통과
|
||||
- 모든 HDF5 handle이 adapter 내부 RAII wrapper에서 해제됨
|
||||
- one-element linear static displacement, reaction, residual과 equilibrium 검증 통과
|
||||
- all-constrained analysis가 PARDISO 없이 성공
|
||||
- `fesa solve tests\fixtures\abaqus\minimal_cantilever.inp --output ...` 성공
|
||||
- CLI 실패 입력이 nonzero exit와 원래 stage/source diagnostic을 반환
|
||||
- /phases/deterministic-parallel-assembly/index.json의 Step 0~2가 모두 completed
|
||||
- /phases/index.json에서 deterministic-parallel-assembly가 completed
|
||||
- shuffled/cancellation/signed-zero contribution의 canonical 결과가 bit-for-bit 동일
|
||||
- serial과 parallel의 CSR row offsets, column indices, values와 force가 동일
|
||||
- thread count 1, 2, available concurrency와 최소 10회 반복에서 결과가 동일
|
||||
- 최종 linear static displacement와 reaction이 thread count에 무관하게 동일
|
||||
- worker가 thread-local contribution만 생성하고 공유 CSR에 atomic add하지 않음
|
||||
- TBB task와 PARDISO 실행이 중첩되지 않음
|
||||
- deterministic Beam kernel failure propagation 검증
|
||||
- benchmark가 production API를 사용하고 speedup을 pass 조건으로 만들지 않음
|
||||
- focused test와 전체 CTest 통과
|
||||
- Harness pytest가 0개가 아닌 상태로 전체 통과
|
||||
- 새 MSVC warning 없음
|
||||
- HDF5 API가 `io/hdf5` 밖의 public semantic contract로 노출되지 않음
|
||||
- element result, full Abaqus subset, TBB assembly, reference 비교를 선행 구현하지 않음
|
||||
- 이 결과를 Beam 수치 자격 완료나 Phase 1 내부 배포 완료로 표시하지 않음
|
||||
- review의 Critical/Important 항목 해결
|
||||
- 사용자 선택 전 원격 push나 `dev` 병합을 수행하지 않음
|
||||
- 독립 review의 Critical/Important finding 해결
|
||||
- 사용자 선택 전 원격 push나 dev 병합을 수행하지 않음
|
||||
|
||||
새 세션의 권장 첫 요청:
|
||||
|
||||
> `docs/HANDOFF.md`와 `phases/results-and-pipeline/step0.md`부터 `step3.md`를 읽고
|
||||
> 현재 baseline, HDF5 도구 경로와 child sandbox의 MSBuild 실행 조건을 확인한 뒤
|
||||
> `results-and-pipeline` Phase를 시작해주세요.
|
||||
> docs/HANDOFF.md와 deterministic-parallel-assembly의 index/step0~2를 읽고 현재
|
||||
> dev baseline, oneTBB 환경과 Harness child 실행 조건을 확인한 뒤
|
||||
> deterministic-parallel-assembly Phase를 시작해주세요.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/io/abaqus/deck_record.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct ActiveInputView final {
|
||||
bool flat = true;
|
||||
std::string part_name;
|
||||
std::string instance_name;
|
||||
std::span<const DeckRecord> part_records;
|
||||
std::span<const DeckRecord> assembly_records;
|
||||
};
|
||||
|
||||
struct ActiveInputResult final {
|
||||
std::optional<ActiveInputView> input;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
[[nodiscard]] ActiveInputResult select_active_input(const ParsedDeck& deck);
|
||||
|
||||
} // namespace fesa
|
||||
@@ -15,6 +15,7 @@ struct DeckRecord final {
|
||||
std::map<std::string, std::string, std::less<>> parameters;
|
||||
std::vector<std::vector<std::string>> data;
|
||||
SourceLocation source;
|
||||
std::vector<SourceLocation> data_sources;
|
||||
};
|
||||
|
||||
struct ParsedPart final {
|
||||
@@ -27,6 +28,7 @@ struct ParsedInstance final {
|
||||
std::string name;
|
||||
std::string part_name;
|
||||
std::vector<std::vector<std::string>> transform_data;
|
||||
std::vector<SourceLocation> transform_sources;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/io/abaqus/deck_record.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
enum class ResolvedSetScope { global, part, assembly };
|
||||
|
||||
enum class ResolvedSetKind { node, element };
|
||||
|
||||
struct ResolvedSet final {
|
||||
std::string scope_name;
|
||||
std::string set_name;
|
||||
std::vector<std::int64_t> sorted_unique_labels;
|
||||
ResolvedSetScope scope = ResolvedSetScope::global;
|
||||
ResolvedSetKind kind = ResolvedSetKind::node;
|
||||
};
|
||||
|
||||
struct SetResolutionResult final {
|
||||
std::vector<ResolvedSet> sets;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
[[nodiscard]] SetResolutionResult resolve_sets(const ParsedDeck& deck);
|
||||
|
||||
} // namespace fesa
|
||||
@@ -5,27 +5,44 @@
|
||||
{
|
||||
"step": 0,
|
||||
"name": "abaqus-input-contract",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Defined the normative Phase 1 Abaqus subset and expanded the public parser/mapper fixture matrix to 74 cases covering strict scopes, parameter forms, data ownership, exact mesh rows and references in active and inactive Parts, duplicate Parts, Assembly-only hierarchical targets, and source-accurate diagnostics.",
|
||||
"started_at": "2026-08-01T02:16:39+0900",
|
||||
"completed_at": "2026-08-01T02:24:23+0900"
|
||||
},
|
||||
{
|
||||
"step": 1,
|
||||
"name": "part-and-assembly-set-resolution",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Added deterministic scope- and kind-aware Part/Assembly set resolution with explicit, generated, nested, forward, duplicate, empty, cycle, missing-member, and active-Instance validation.",
|
||||
"started_at": "2026-08-01T02:24:24+0900",
|
||||
"completed_at": "2026-08-01T02:37:05+0900"
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "single-instance-semantic-validation",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Added ActiveInputView selection and semantic validation for exclusive flat or single untransformed Instance input, preserving source diagnostics and excluding inactive Parts.",
|
||||
"started_at": "2026-08-01T02:37:05+0900",
|
||||
"completed_at": "2026-08-01T02:44:01+0900"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "material-section-and-shear-defaults",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Added complete-deck global material and active-ELSET Beam section mapping with exact property diagnostics, explicit shear-area conversion, and preserved Phase 1 default shear source.",
|
||||
"started_at": "2026-08-01T02:44:01+0900",
|
||||
"completed_at": "2026-08-01T02:59:40+0900"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "step-bc-load-and-noop-directives",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Completed single static Step mapping with deterministic BC canonicalization, accumulated nodal loads, explicit no-op directives, exact source diagnostics, and supplied cantilever normalization.",
|
||||
"started_at": "2026-08-01T02:59:40+0900",
|
||||
"completed_at": "2026-08-01T03:23:25+0900"
|
||||
}
|
||||
]
|
||||
],
|
||||
"created_at": "2026-08-01T01:59:08+0900",
|
||||
"completed_at": "2026-08-01T03:23:25+0900"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-1
@@ -27,7 +27,8 @@
|
||||
},
|
||||
{
|
||||
"dir": "abaqus-subset-completion",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"completed_at": "2026-08-01T03:23:25+0900"
|
||||
},
|
||||
{
|
||||
"dir": "deterministic-parallel-assembly",
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#include <fesa/io/abaqus/active_input.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
const std::string* parameter(
|
||||
const DeckRecord& record,
|
||||
const std::string_view name) {
|
||||
const auto found = record.parameters.find(name);
|
||||
return found == record.parameters.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
bool is_flat_model_record(const DeckRecord& record) {
|
||||
return record.keyword == "NODE" || record.keyword == "ELEMENT" ||
|
||||
record.keyword == "NSET" || record.keyword == "ELSET" ||
|
||||
record.keyword == "BEAM GENERAL SECTION" ||
|
||||
record.keyword == "TRANSVERSE SHEAR STIFFNESS";
|
||||
}
|
||||
|
||||
ActiveInputResult failure(
|
||||
std::string code,
|
||||
std::string message,
|
||||
const SourceLocation& source) {
|
||||
return {
|
||||
std::nullopt,
|
||||
{{
|
||||
DiagnosticStage::semantic,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
source,
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ActiveInputResult select_active_input(const ParsedDeck& deck) {
|
||||
const bool hierarchical =
|
||||
!deck.parts.empty() || deck.assembly.has_value();
|
||||
if (!hierarchical) {
|
||||
return {
|
||||
ActiveInputView{
|
||||
true,
|
||||
{},
|
||||
{},
|
||||
deck.global_records,
|
||||
{},
|
||||
},
|
||||
{},
|
||||
};
|
||||
}
|
||||
|
||||
const auto flat_record =
|
||||
std::ranges::find_if(deck.global_records, is_flat_model_record);
|
||||
if (flat_record != deck.global_records.end()) {
|
||||
return failure(
|
||||
"abaqus.semantic.mixed_mesh_organization",
|
||||
"Flat model records cannot be mixed with Part/Assembly input.",
|
||||
flat_record->source);
|
||||
}
|
||||
|
||||
std::set<std::string, std::less<>> part_names;
|
||||
for (const ParsedPart& part : deck.parts) {
|
||||
if (!part_names.insert(part.name).second) {
|
||||
return failure(
|
||||
"abaqus.semantic.duplicate_part",
|
||||
"Part name '" + part.name + "' is defined more than once.",
|
||||
part.source);
|
||||
}
|
||||
}
|
||||
|
||||
if (!deck.assembly.has_value()) {
|
||||
const SourceLocation source =
|
||||
deck.parts.empty() ? SourceLocation{} : deck.parts.front().source;
|
||||
return failure(
|
||||
"abaqus.semantic.assembly_count",
|
||||
"Hierarchical Phase 1 input requires exactly one Assembly.",
|
||||
source);
|
||||
}
|
||||
|
||||
const ParsedAssembly& assembly = *deck.assembly;
|
||||
if (assembly.instances.size() != 1U) {
|
||||
const SourceLocation& source = assembly.instances.size() > 1U
|
||||
? assembly.instances[1].source
|
||||
: assembly.source;
|
||||
return failure(
|
||||
"abaqus.semantic.instance_count",
|
||||
"Phase 1 requires exactly one Instance.",
|
||||
source);
|
||||
}
|
||||
|
||||
const ParsedInstance& instance = assembly.instances.front();
|
||||
if (!instance.transform_data.empty()) {
|
||||
const SourceLocation& source = instance.transform_sources.empty()
|
||||
? instance.source
|
||||
: instance.transform_sources.front();
|
||||
return failure(
|
||||
"abaqus.semantic.instance_transform",
|
||||
"Instance translation and rotation data are unsupported.",
|
||||
source);
|
||||
}
|
||||
|
||||
const auto part =
|
||||
std::ranges::find(deck.parts, instance.part_name, &ParsedPart::name);
|
||||
if (part == deck.parts.end()) {
|
||||
return failure(
|
||||
"abaqus.semantic.missing_part",
|
||||
"Instance '" + instance.name + "' references missing Part '" +
|
||||
instance.part_name + "'.",
|
||||
instance.source);
|
||||
}
|
||||
|
||||
for (const DeckRecord& record : assembly.records) {
|
||||
if (record.keyword != "NSET" && record.keyword != "ELSET") {
|
||||
continue;
|
||||
}
|
||||
const std::string* record_instance = parameter(record, "INSTANCE");
|
||||
if (record_instance == nullptr || *record_instance != instance.name) {
|
||||
const std::string* name = parameter(
|
||||
record, record.keyword == "NSET" ? "NSET" : "ELSET");
|
||||
return failure(
|
||||
"abaqus.semantic.wrong_instance",
|
||||
"Assembly set '" +
|
||||
(name == nullptr ? std::string{} : *name) +
|
||||
"' must reference the active Instance.",
|
||||
record.source);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ActiveInputView{
|
||||
false,
|
||||
part->name,
|
||||
instance.name,
|
||||
part->records,
|
||||
assembly.records,
|
||||
},
|
||||
{},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -13,14 +17,19 @@
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
enum class Scope { global, part, assembly, instance };
|
||||
enum class Scope { global, part, assembly, instance, step };
|
||||
|
||||
struct KeywordLine final {
|
||||
std::string keyword;
|
||||
std::map<std::string, std::string, std::less<>> parameters;
|
||||
std::set<std::string, std::less<>> flag_parameters;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
const std::string* parameter(
|
||||
const KeywordLine& keyword,
|
||||
std::string_view name);
|
||||
|
||||
std::string_view trim(const std::string_view value) {
|
||||
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
||||
const std::size_t first = value.find_first_not_of(whitespace);
|
||||
@@ -93,17 +102,202 @@ bool is_supported_record(const std::string_view keyword) {
|
||||
std::string_view{"ELASTIC"},
|
||||
std::string_view{"ELEMENT"},
|
||||
std::string_view{"ELSET"},
|
||||
std::string_view{"END STEP"},
|
||||
std::string_view{"HEADING"},
|
||||
std::string_view{"MATERIAL"},
|
||||
std::string_view{"NODE"},
|
||||
std::string_view{"NSET"},
|
||||
std::string_view{"OUTPUT"},
|
||||
std::string_view{"PREPRINT"},
|
||||
std::string_view{"RESTART"},
|
||||
std::string_view{"STATIC"},
|
||||
std::string_view{"STEP"},
|
||||
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
|
||||
};
|
||||
return std::ranges::find(supported, keyword) != supported.end();
|
||||
}
|
||||
|
||||
bool is_known_keyword(const std::string_view keyword) {
|
||||
constexpr std::array scope_keywords{
|
||||
std::string_view{"ASSEMBLY"},
|
||||
std::string_view{"END ASSEMBLY"},
|
||||
std::string_view{"END INSTANCE"},
|
||||
std::string_view{"END PART"},
|
||||
std::string_view{"INSTANCE"},
|
||||
std::string_view{"PART"},
|
||||
};
|
||||
return is_supported_record(keyword) ||
|
||||
std::ranges::find(scope_keywords, keyword) != scope_keywords.end();
|
||||
}
|
||||
|
||||
ParseDeckResult invalid_record_scope(const KeywordLine& keyword) {
|
||||
std::string code = "abaqus.syntax.invalid_step_scope";
|
||||
if (keyword.keyword == "NODE") {
|
||||
code = "abaqus.syntax.invalid_node_scope";
|
||||
} else if (keyword.keyword == "ELEMENT") {
|
||||
code = "abaqus.syntax.invalid_element_scope";
|
||||
} else if (keyword.keyword == "NSET" || keyword.keyword == "ELSET") {
|
||||
code = "abaqus.syntax.invalid_set_scope";
|
||||
} else if (keyword.keyword == "MATERIAL" ||
|
||||
keyword.keyword == "ELASTIC") {
|
||||
code = "abaqus.syntax.invalid_material_scope";
|
||||
} else if (keyword.keyword == "BEAM GENERAL SECTION") {
|
||||
code = "abaqus.syntax.invalid_section_scope";
|
||||
} else if (keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") {
|
||||
code = "abaqus.syntax.invalid_transverse_shear_scope";
|
||||
} else if (keyword.keyword == "BOUNDARY") {
|
||||
code = "abaqus.syntax.invalid_boundary_scope";
|
||||
} else if (keyword.keyword == "CLOAD") {
|
||||
code = "abaqus.syntax.invalid_cload_scope";
|
||||
} else if (keyword.keyword == "HEADING") {
|
||||
code = "abaqus.syntax.invalid_heading_scope";
|
||||
} else if (keyword.keyword == "PREPRINT") {
|
||||
code = "abaqus.syntax.invalid_preprint_scope";
|
||||
} else if (keyword.keyword == "RESTART") {
|
||||
code = "abaqus.syntax.invalid_restart_scope";
|
||||
} else if (keyword.keyword == "OUTPUT") {
|
||||
code = "abaqus.syntax.invalid_output_scope";
|
||||
} else if (keyword.keyword == "PART") {
|
||||
code = "abaqus.syntax.invalid_part_scope";
|
||||
} else if (keyword.keyword == "ASSEMBLY") {
|
||||
code = "abaqus.syntax.invalid_assembly_scope";
|
||||
} else if (keyword.keyword == "INSTANCE") {
|
||||
code = "abaqus.syntax.invalid_instance_scope";
|
||||
}
|
||||
return syntax_failure(
|
||||
std::move(code),
|
||||
"*" + keyword.keyword + " is invalid in the current input scope.",
|
||||
keyword.source);
|
||||
}
|
||||
|
||||
bool is_allowed_parameter(
|
||||
const std::string_view name,
|
||||
const std::initializer_list<std::string_view> allowed) {
|
||||
return std::ranges::find(allowed, name) != allowed.end();
|
||||
}
|
||||
|
||||
std::optional<ParseDeckResult> validate_parameter_names(
|
||||
const KeywordLine& keyword,
|
||||
const std::initializer_list<std::string_view> allowed) {
|
||||
for (const auto& [name, value] : keyword.parameters) {
|
||||
static_cast<void>(value);
|
||||
if (!is_allowed_parameter(name, allowed)) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unsupported_parameter",
|
||||
"Unsupported parameter '" + name + "' on *" +
|
||||
keyword.keyword + ".",
|
||||
keyword.source);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<ParseDeckResult> unsupported_parameter_value(
|
||||
const KeywordLine& keyword,
|
||||
std::string_view name);
|
||||
|
||||
std::optional<ParseDeckResult> validate_parameter_schema(
|
||||
const KeywordLine& keyword,
|
||||
const std::initializer_list<std::string_view> valued,
|
||||
const std::initializer_list<std::string_view> flags = {}) {
|
||||
for (const auto& [name, value] : keyword.parameters) {
|
||||
if (is_allowed_parameter(name, valued)) {
|
||||
if (keyword.flag_parameters.contains(name) || value.empty()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_parameter",
|
||||
"Parameter '" + name + "' on *" + keyword.keyword +
|
||||
" requires a nonempty value.",
|
||||
keyword.source);
|
||||
}
|
||||
} else if (is_allowed_parameter(name, flags)) {
|
||||
if (!keyword.flag_parameters.contains(name)) {
|
||||
return unsupported_parameter_value(keyword, name);
|
||||
}
|
||||
} else {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unsupported_parameter",
|
||||
"Unsupported parameter '" + name + "' on *" +
|
||||
keyword.keyword + ".",
|
||||
keyword.source);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<ParseDeckResult> unsupported_parameter_value(
|
||||
const KeywordLine& keyword,
|
||||
const std::string_view name) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unsupported_parameter",
|
||||
"Unsupported value for parameter '" + std::string{name} +
|
||||
"' on *" + keyword.keyword + ".",
|
||||
keyword.source);
|
||||
}
|
||||
|
||||
std::optional<ParseDeckResult> validate_noop_parameters(
|
||||
const KeywordLine& keyword) {
|
||||
if (keyword.keyword == "HEADING") {
|
||||
return validate_parameter_schema(keyword, {});
|
||||
}
|
||||
if (keyword.keyword == "PREPRINT") {
|
||||
if (auto error = validate_parameter_schema(
|
||||
keyword, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) {
|
||||
return error;
|
||||
}
|
||||
for (const auto& [name, value] : keyword.parameters) {
|
||||
const std::string normalized = uppercase_ascii(value);
|
||||
if (normalized != "YES" && normalized != "NO") {
|
||||
return unsupported_parameter_value(keyword, name);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
if (keyword.keyword == "RESTART") {
|
||||
if (auto error = validate_parameter_schema(
|
||||
keyword, {"FREQUENCY"}, {"WRITE"})) {
|
||||
return error;
|
||||
}
|
||||
if (const std::string* write = parameter(keyword, "WRITE");
|
||||
write != nullptr && !write->empty()) {
|
||||
return unsupported_parameter_value(keyword, "WRITE");
|
||||
}
|
||||
if (const std::string* frequency = parameter(keyword, "FREQUENCY");
|
||||
frequency != nullptr) {
|
||||
std::int64_t value = 0;
|
||||
const auto parsed = std::from_chars(
|
||||
frequency->data(), frequency->data() + frequency->size(), value);
|
||||
if (frequency->empty() || parsed.ec != std::errc{} ||
|
||||
parsed.ptr != frequency->data() + frequency->size() ||
|
||||
value < 0) {
|
||||
return unsupported_parameter_value(keyword, "FREQUENCY");
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
if (keyword.keyword == "OUTPUT") {
|
||||
if (auto error = validate_parameter_schema(
|
||||
keyword, {"VARIABLE"}, {"FIELD", "HISTORY"})) {
|
||||
return error;
|
||||
}
|
||||
const bool field = keyword.parameters.contains("FIELD");
|
||||
const bool history = keyword.parameters.contains("HISTORY");
|
||||
if (field == history) {
|
||||
return unsupported_parameter_value(keyword, "FIELD/HISTORY");
|
||||
}
|
||||
const std::string* flag = parameter(
|
||||
keyword, field ? std::string_view{"FIELD"}
|
||||
: std::string_view{"HISTORY"});
|
||||
if (flag == nullptr || !flag->empty()) {
|
||||
return unsupported_parameter_value(
|
||||
keyword, field ? "FIELD" : "HISTORY");
|
||||
}
|
||||
if (const std::string* variable = parameter(keyword, "VARIABLE");
|
||||
variable != nullptr && uppercase_ascii(*variable) != "PRESELECT") {
|
||||
return unsupported_parameter_value(keyword, "VARIABLE");
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<KeywordLine> parse_keyword_line(
|
||||
const std::string_view line,
|
||||
const SourceLocation& source,
|
||||
@@ -120,6 +314,7 @@ std::optional<KeywordLine> parse_keyword_line(
|
||||
KeywordLine parsed{
|
||||
uppercase_ascii(fields[0]),
|
||||
{},
|
||||
{},
|
||||
source,
|
||||
};
|
||||
for (std::size_t index = 1; index < fields.size(); ++index) {
|
||||
@@ -152,6 +347,9 @@ std::optional<KeywordLine> parse_keyword_line(
|
||||
source);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (equals == std::string_view::npos) {
|
||||
parsed.flag_parameters.insert(key);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -190,6 +388,8 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
std::optional<ParsedPart> current_part;
|
||||
std::optional<ParsedAssembly> current_assembly;
|
||||
std::optional<ParsedInstance> current_instance;
|
||||
std::optional<SourceLocation> current_step_source;
|
||||
bool completed_step = false;
|
||||
DeckRecord* current_record = nullptr;
|
||||
|
||||
std::string line;
|
||||
@@ -214,8 +414,18 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
const std::vector<std::string> fields = split_fields(content);
|
||||
if (scope == Scope::instance) {
|
||||
current_instance->transform_data.push_back(fields);
|
||||
current_instance->transform_sources.push_back(SourceLocation{
|
||||
path,
|
||||
line_number,
|
||||
first_nonspace + 1U,
|
||||
});
|
||||
} else if (current_record != nullptr) {
|
||||
current_record->data.push_back(fields);
|
||||
current_record->data_sources.push_back(SourceLocation{
|
||||
path,
|
||||
line_number,
|
||||
first_nonspace + 1U,
|
||||
});
|
||||
} else {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.data_without_keyword",
|
||||
@@ -239,6 +449,69 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
KeywordLine& keyword = *parsed;
|
||||
current_record = nullptr;
|
||||
|
||||
if (keyword.keyword == "STEP") {
|
||||
if (scope != Scope::global) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_step_scope",
|
||||
"*STEP is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_schema(
|
||||
keyword, {"NAME", "NLGEOM"})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
if (const std::string* name = parameter(keyword, "NAME");
|
||||
name != nullptr && name->empty()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_parameter",
|
||||
"*STEP parameter NAME requires a nonempty value.",
|
||||
source);
|
||||
}
|
||||
if (const std::string* nlgeom = parameter(keyword, "NLGEOM");
|
||||
nlgeom != nullptr && nlgeom->empty()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_parameter",
|
||||
"*STEP parameter NLGEOM requires a value.",
|
||||
source);
|
||||
}
|
||||
deck.global_records.push_back({
|
||||
std::move(keyword.keyword),
|
||||
std::move(keyword.parameters),
|
||||
{},
|
||||
source,
|
||||
});
|
||||
current_step_source = source;
|
||||
scope = Scope::step;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "END STEP") {
|
||||
if (auto error = validate_parameter_names(keyword, {})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
if (scope != Scope::step || !current_step_source.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unexpected_end_step",
|
||||
"*END STEP does not match an open *STEP.",
|
||||
source);
|
||||
}
|
||||
deck.global_records.push_back({
|
||||
std::move(keyword.keyword),
|
||||
std::move(keyword.parameters),
|
||||
{},
|
||||
source,
|
||||
});
|
||||
current_step_source.reset();
|
||||
completed_step = true;
|
||||
scope = Scope::global;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scope == Scope::global && completed_step &&
|
||||
is_known_keyword(keyword.keyword)) {
|
||||
return invalid_record_scope(keyword);
|
||||
}
|
||||
|
||||
if (keyword.keyword == "PART") {
|
||||
if (scope != Scope::global) {
|
||||
return syntax_failure(
|
||||
@@ -246,6 +519,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"*PART is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_schema(keyword, {"NAME"})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
@@ -262,6 +538,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"*END PART does not match an open *PART.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_schema(keyword, {})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
deck.parts.push_back(std::move(*current_part));
|
||||
current_part.reset();
|
||||
scope = Scope::global;
|
||||
@@ -275,6 +554,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"*ASSEMBLY is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_schema(keyword, {"NAME"})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
if (deck.assembly.has_value() ||
|
||||
current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
@@ -299,6 +581,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"*END ASSEMBLY does not match an open *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_schema(keyword, {})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
deck.assembly = std::move(*current_assembly);
|
||||
current_assembly.reset();
|
||||
scope = Scope::global;
|
||||
@@ -313,6 +598,10 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"*INSTANCE is only valid in an open *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
if (auto error =
|
||||
validate_parameter_schema(keyword, {"NAME", "PART"})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
@@ -322,7 +611,7 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
return missing_parameter(keyword, "PART");
|
||||
}
|
||||
current_instance =
|
||||
ParsedInstance{*name, *part_name, {}, source};
|
||||
ParsedInstance{*name, *part_name, {}, {}, source};
|
||||
scope = Scope::instance;
|
||||
continue;
|
||||
}
|
||||
@@ -336,6 +625,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"*END INSTANCE does not match an open *INSTANCE.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_schema(keyword, {})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
current_assembly->instances.push_back(
|
||||
std::move(*current_instance));
|
||||
current_instance.reset();
|
||||
@@ -343,12 +635,149 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "HEADING" ||
|
||||
keyword.keyword == "PREPRINT") {
|
||||
if (scope != Scope::global) {
|
||||
return syntax_failure(
|
||||
keyword.keyword == "HEADING"
|
||||
? "abaqus.syntax.invalid_heading_scope"
|
||||
: "abaqus.syntax.invalid_preprint_scope",
|
||||
"*" + keyword.keyword +
|
||||
" is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_noop_parameters(keyword)) {
|
||||
return std::move(*error);
|
||||
}
|
||||
deck.global_records.push_back({
|
||||
std::move(keyword.keyword),
|
||||
std::move(keyword.parameters),
|
||||
{},
|
||||
source,
|
||||
});
|
||||
if (deck.global_records.back().keyword == "HEADING") {
|
||||
current_record = &deck.global_records.back();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "RESTART" || keyword.keyword == "OUTPUT") {
|
||||
if (scope != Scope::step) {
|
||||
return syntax_failure(
|
||||
keyword.keyword == "RESTART"
|
||||
? "abaqus.syntax.invalid_restart_scope"
|
||||
: "abaqus.syntax.invalid_output_scope",
|
||||
"*" + keyword.keyword +
|
||||
" is only valid inside *STEP.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_noop_parameters(keyword)) {
|
||||
return std::move(*error);
|
||||
}
|
||||
deck.global_records.push_back({
|
||||
std::move(keyword.keyword),
|
||||
std::move(keyword.parameters),
|
||||
{},
|
||||
source,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "STATIC") {
|
||||
if (scope != Scope::step) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_step_scope",
|
||||
"*STATIC is only valid inside *STEP.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_names(keyword, {})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
} else if (keyword.keyword == "CLOAD") {
|
||||
if (scope != Scope::step) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_cload_scope",
|
||||
"*CLOAD is only valid inside *STEP.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_names(keyword, {})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
} else if (keyword.keyword == "BOUNDARY") {
|
||||
if (scope != Scope::global && scope != Scope::step) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_boundary_scope",
|
||||
"*BOUNDARY is valid only in global or Step scope.",
|
||||
source);
|
||||
}
|
||||
if (auto error = validate_parameter_names(keyword, {})) {
|
||||
return std::move(*error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_supported_record(keyword.keyword)) {
|
||||
return syntax_failure(
|
||||
"abaqus.unsupported_keyword",
|
||||
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
|
||||
source);
|
||||
}
|
||||
if (scope == Scope::step && keyword.keyword != "STATIC" &&
|
||||
keyword.keyword != "BOUNDARY" && keyword.keyword != "CLOAD") {
|
||||
return invalid_record_scope(keyword);
|
||||
}
|
||||
if (scope != Scope::instance) {
|
||||
const bool mesh_scope = scope == Scope::global || scope == Scope::part;
|
||||
const bool set_scope = mesh_scope || scope == Scope::assembly;
|
||||
if ((keyword.keyword == "NODE" || keyword.keyword == "ELEMENT" ||
|
||||
keyword.keyword == "BEAM GENERAL SECTION" ||
|
||||
keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") &&
|
||||
!mesh_scope) {
|
||||
return invalid_record_scope(keyword);
|
||||
}
|
||||
if ((keyword.keyword == "NSET" || keyword.keyword == "ELSET") &&
|
||||
!set_scope) {
|
||||
return invalid_record_scope(keyword);
|
||||
}
|
||||
if ((keyword.keyword == "MATERIAL" ||
|
||||
keyword.keyword == "ELASTIC") &&
|
||||
scope != Scope::global) {
|
||||
return invalid_record_scope(keyword);
|
||||
}
|
||||
}
|
||||
if (scope != Scope::instance) {
|
||||
std::optional<ParseDeckResult> parameter_error;
|
||||
if (keyword.keyword == "NODE" || keyword.keyword == "ELASTIC" ||
|
||||
keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") {
|
||||
parameter_error = validate_parameter_schema(keyword, {});
|
||||
} else if (keyword.keyword == "ELEMENT") {
|
||||
parameter_error = validate_parameter_schema(
|
||||
keyword, {"TYPE", "ELSET"});
|
||||
} else if (keyword.keyword == "NSET") {
|
||||
parameter_error = scope == Scope::assembly
|
||||
? validate_parameter_schema(
|
||||
keyword,
|
||||
{"NSET", "INSTANCE"},
|
||||
{"GENERATE"})
|
||||
: validate_parameter_schema(
|
||||
keyword, {"NSET"}, {"GENERATE"});
|
||||
} else if (keyword.keyword == "ELSET") {
|
||||
parameter_error = scope == Scope::assembly
|
||||
? validate_parameter_schema(
|
||||
keyword,
|
||||
{"ELSET", "INSTANCE"},
|
||||
{"GENERATE"})
|
||||
: validate_parameter_schema(
|
||||
keyword, {"ELSET"}, {"GENERATE"});
|
||||
} else if (keyword.keyword == "MATERIAL") {
|
||||
parameter_error = validate_parameter_schema(keyword, {"NAME"});
|
||||
} else if (keyword.keyword == "BEAM GENERAL SECTION") {
|
||||
parameter_error = validate_parameter_schema(
|
||||
keyword, {"SECTION", "ELSET", "MATERIAL"});
|
||||
}
|
||||
if (parameter_error.has_value()) {
|
||||
return std::move(*parameter_error);
|
||||
}
|
||||
}
|
||||
|
||||
DeckRecord next_record{
|
||||
std::move(keyword.keyword),
|
||||
@@ -359,7 +788,9 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
switch (scope) {
|
||||
case Scope::global:
|
||||
deck.global_records.push_back(std::move(next_record));
|
||||
current_record = &deck.global_records.back();
|
||||
if (deck.global_records.back().keyword != "MATERIAL") {
|
||||
current_record = &deck.global_records.back();
|
||||
}
|
||||
break;
|
||||
case Scope::part:
|
||||
current_part->records.push_back(std::move(next_record));
|
||||
@@ -375,6 +806,10 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"abaqus.syntax.instance_local_keyword",
|
||||
"Keyword records inside *INSTANCE are unsupported.",
|
||||
source);
|
||||
case Scope::step:
|
||||
deck.global_records.push_back(std::move(next_record));
|
||||
current_record = &deck.global_records.back();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +839,12 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
"Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
|
||||
current_assembly->source);
|
||||
}
|
||||
if (current_step_source.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unclosed_step",
|
||||
"Abaqus *STEP is not closed by *END STEP.",
|
||||
*current_step_source);
|
||||
}
|
||||
|
||||
return {std::move(deck), {}};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,411 @@
|
||||
#include <fesa/io/abaqus/set_resolver.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <compare>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
struct SetNamespace final {
|
||||
ResolvedSetScope scope;
|
||||
std::string scope_name;
|
||||
ResolvedSetKind kind;
|
||||
|
||||
auto operator<=>(const SetNamespace&) const = default;
|
||||
};
|
||||
|
||||
struct SetKey final {
|
||||
SetNamespace name_space;
|
||||
std::string set_name;
|
||||
|
||||
auto operator<=>(const SetKey&) const = default;
|
||||
};
|
||||
|
||||
struct RawMember final {
|
||||
std::string text;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
struct RawSet final {
|
||||
std::vector<RawMember> members;
|
||||
};
|
||||
|
||||
enum class VisitState { unvisited, visiting, resolved, failed };
|
||||
|
||||
const std::string* parameter(
|
||||
const DeckRecord& record,
|
||||
const std::string_view name) {
|
||||
const auto found = record.parameters.find(name);
|
||||
return found == record.parameters.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
bool has_parameter(
|
||||
const DeckRecord& record,
|
||||
const std::string_view name) {
|
||||
return record.parameters.contains(name);
|
||||
}
|
||||
|
||||
SourceLocation data_source(
|
||||
const DeckRecord& record,
|
||||
const std::size_t row) {
|
||||
return row < record.data_sources.size()
|
||||
? record.data_sources[row]
|
||||
: record.source;
|
||||
}
|
||||
|
||||
bool parse_positive_label(
|
||||
const std::string_view text,
|
||||
std::int64_t& value) {
|
||||
const auto parsed =
|
||||
std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return parsed.ec == std::errc{} &&
|
||||
parsed.ptr == text.data() + text.size() && value > 0;
|
||||
}
|
||||
|
||||
class SetResolver final {
|
||||
public:
|
||||
explicit SetResolver(const ParsedDeck& deck) : deck_{deck} {}
|
||||
|
||||
[[nodiscard]] SetResolutionResult resolve() {
|
||||
collect_scope(
|
||||
deck_.global_records,
|
||||
{ResolvedSetScope::global, "global", ResolvedSetKind::node});
|
||||
|
||||
for (const ParsedPart& part : deck_.parts) {
|
||||
collect_scope(
|
||||
part.records,
|
||||
{ResolvedSetScope::part,
|
||||
part.name,
|
||||
ResolvedSetKind::node});
|
||||
}
|
||||
|
||||
collect_assembly();
|
||||
|
||||
std::vector<ResolvedSet> sets;
|
||||
sets.reserve(raw_sets_.size());
|
||||
for (const auto& [key, raw_set] : raw_sets_) {
|
||||
static_cast<void>(raw_set);
|
||||
if (!resolve_set(key)) {
|
||||
continue;
|
||||
}
|
||||
sets.push_back({
|
||||
key.name_space.scope_name,
|
||||
key.set_name,
|
||||
resolved_sets_.at(key),
|
||||
key.name_space.scope,
|
||||
key.name_space.kind,
|
||||
});
|
||||
}
|
||||
|
||||
if (!diagnostics_.empty()) {
|
||||
sets.clear();
|
||||
}
|
||||
return {std::move(sets), std::move(diagnostics_)};
|
||||
}
|
||||
|
||||
private:
|
||||
void add_error(
|
||||
std::string code,
|
||||
std::string message,
|
||||
const SourceLocation& source) {
|
||||
diagnostics_.push_back({
|
||||
DiagnosticStage::semantic,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
void collect_scope(
|
||||
const std::vector<DeckRecord>& records,
|
||||
SetNamespace name_space) {
|
||||
collect_entities(records, name_space);
|
||||
collect_set_records(records, std::move(name_space), nullptr);
|
||||
}
|
||||
|
||||
void collect_entities(
|
||||
const std::vector<DeckRecord>& records,
|
||||
const SetNamespace& base_namespace) {
|
||||
for (const DeckRecord& record : records) {
|
||||
ResolvedSetKind kind;
|
||||
if (record.keyword == "NODE") {
|
||||
kind = ResolvedSetKind::node;
|
||||
} else if (record.keyword == "ELEMENT") {
|
||||
kind = ResolvedSetKind::element;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
SetNamespace entity_namespace = base_namespace;
|
||||
entity_namespace.kind = kind;
|
||||
std::set<std::int64_t>& labels = entities_[entity_namespace];
|
||||
for (std::size_t row = 0; row < record.data.size(); ++row) {
|
||||
if (record.data[row].empty()) {
|
||||
continue;
|
||||
}
|
||||
std::int64_t label = 0;
|
||||
if (!parse_positive_label(record.data[row][0], label)) {
|
||||
continue;
|
||||
}
|
||||
labels.insert(label);
|
||||
|
||||
if (kind != ResolvedSetKind::element) {
|
||||
continue;
|
||||
}
|
||||
const std::string* set_name = parameter(record, "ELSET");
|
||||
if (set_name == nullptr || set_name->empty()) {
|
||||
continue;
|
||||
}
|
||||
raw_sets_[{entity_namespace, *set_name}].members.push_back({
|
||||
std::to_string(label),
|
||||
data_source(record, row),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_set_records(
|
||||
const std::vector<DeckRecord>& records,
|
||||
const SetNamespace& base_namespace,
|
||||
const std::string* active_instance) {
|
||||
for (const DeckRecord& record : records) {
|
||||
SetNamespace set_namespace = base_namespace;
|
||||
std::string_view name_parameter;
|
||||
if (record.keyword == "NSET") {
|
||||
set_namespace.kind = ResolvedSetKind::node;
|
||||
name_parameter = "NSET";
|
||||
} else if (record.keyword == "ELSET") {
|
||||
set_namespace.kind = ResolvedSetKind::element;
|
||||
name_parameter = "ELSET";
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string* set_name = parameter(record, name_parameter);
|
||||
if (set_name == nullptr || set_name->empty()) {
|
||||
add_error(
|
||||
"abaqus.semantic.missing_parameter",
|
||||
"*" + record.keyword + " requires parameter " +
|
||||
std::string{name_parameter} + ".",
|
||||
record.source);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (set_namespace.scope == ResolvedSetScope::assembly) {
|
||||
const std::string* instance = parameter(record, "INSTANCE");
|
||||
if (active_instance == nullptr || instance == nullptr ||
|
||||
*instance != *active_instance) {
|
||||
add_error(
|
||||
"abaqus.semantic.wrong_instance",
|
||||
"Assembly set '" + *set_name +
|
||||
"' must reference the active Instance.",
|
||||
record.source);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
RawSet& raw_set = raw_sets_[{set_namespace, *set_name}];
|
||||
if (has_parameter(record, "GENERATE")) {
|
||||
collect_generate(record, raw_set);
|
||||
} else {
|
||||
collect_explicit(record, raw_set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_explicit(
|
||||
const DeckRecord& record,
|
||||
RawSet& raw_set) {
|
||||
for (std::size_t row = 0; row < record.data.size(); ++row) {
|
||||
for (const std::string& field : record.data[row]) {
|
||||
if (field.empty()) {
|
||||
continue;
|
||||
}
|
||||
raw_set.members.push_back({field, data_source(record, row)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_generate(
|
||||
const DeckRecord& record,
|
||||
RawSet& raw_set) {
|
||||
const SourceLocation source =
|
||||
record.data.empty() ? record.source : data_source(record, 0U);
|
||||
if (record.data.size() != 1U || record.data[0].size() != 3U ||
|
||||
std::ranges::any_of(
|
||||
record.data[0],
|
||||
[](const std::string& field) { return field.empty(); })) {
|
||||
add_error(
|
||||
"abaqus.semantic.invalid_generate",
|
||||
"*" + record.keyword +
|
||||
", GENERATE requires exactly start, end, increment.",
|
||||
source);
|
||||
return;
|
||||
}
|
||||
|
||||
std::int64_t start = 0;
|
||||
std::int64_t end = 0;
|
||||
std::int64_t increment = 0;
|
||||
if (!parse_positive_label(record.data[0][0], start) ||
|
||||
!parse_positive_label(record.data[0][1], end) ||
|
||||
!parse_positive_label(record.data[0][2], increment) ||
|
||||
start > end || (end - start) % increment != 0) {
|
||||
add_error(
|
||||
"abaqus.semantic.invalid_generate",
|
||||
"Invalid *" + record.keyword + " generate range.",
|
||||
source);
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::int64_t label = start;; label += increment) {
|
||||
raw_set.members.push_back({std::to_string(label), source});
|
||||
if (label == end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_assembly() {
|
||||
if (!deck_.assembly.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ParsedAssembly& assembly = *deck_.assembly;
|
||||
const ParsedInstance* active_instance =
|
||||
assembly.instances.size() == 1U
|
||||
? &assembly.instances.front()
|
||||
: nullptr;
|
||||
SetNamespace assembly_namespace{
|
||||
ResolvedSetScope::assembly,
|
||||
assembly.name,
|
||||
ResolvedSetKind::node,
|
||||
};
|
||||
|
||||
if (active_instance != nullptr) {
|
||||
const auto part = std::ranges::find(
|
||||
deck_.parts, active_instance->part_name, &ParsedPart::name);
|
||||
if (part != deck_.parts.end()) {
|
||||
for (const ResolvedSetKind kind : {
|
||||
ResolvedSetKind::node,
|
||||
ResolvedSetKind::element}) {
|
||||
const SetNamespace part_namespace{
|
||||
ResolvedSetScope::part,
|
||||
part->name,
|
||||
kind,
|
||||
};
|
||||
SetNamespace lifted_namespace = assembly_namespace;
|
||||
lifted_namespace.kind = kind;
|
||||
const auto labels = entities_.find(part_namespace);
|
||||
if (labels != entities_.end()) {
|
||||
entities_[lifted_namespace] = labels->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string* instance_name =
|
||||
active_instance == nullptr ? nullptr : &active_instance->name;
|
||||
collect_set_records(
|
||||
assembly.records, assembly_namespace, instance_name);
|
||||
}
|
||||
|
||||
bool resolve_set(const SetKey& key) {
|
||||
VisitState& state = states_[key];
|
||||
if (state == VisitState::resolved) {
|
||||
return true;
|
||||
}
|
||||
if (state == VisitState::failed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state = VisitState::visiting;
|
||||
bool succeeded = true;
|
||||
std::vector<std::int64_t> labels;
|
||||
for (const RawMember& member : raw_sets_.at(key).members) {
|
||||
std::int64_t label = 0;
|
||||
if (parse_positive_label(member.text, label)) {
|
||||
const auto entity_namespace = entities_.find(key.name_space);
|
||||
if (entity_namespace == entities_.end() ||
|
||||
!entity_namespace->second.contains(label)) {
|
||||
add_error(
|
||||
"abaqus.semantic.missing_set_member",
|
||||
"Set '" + key.set_name +
|
||||
"' references missing entity label " +
|
||||
std::to_string(label) + ".",
|
||||
member.source);
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
labels.push_back(label);
|
||||
continue;
|
||||
}
|
||||
|
||||
const SetKey nested_key{key.name_space, member.text};
|
||||
const auto nested = raw_sets_.find(nested_key);
|
||||
if (nested == raw_sets_.end()) {
|
||||
add_error(
|
||||
"abaqus.semantic.missing_set_member",
|
||||
"Set '" + key.set_name + "' references missing set '" +
|
||||
member.text + "'.",
|
||||
member.source);
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (states_[nested_key] == VisitState::visiting) {
|
||||
add_error(
|
||||
"abaqus.semantic.set_cycle",
|
||||
"Set '" + key.set_name +
|
||||
"' closes a nested set reference cycle through '" +
|
||||
member.text + "'.",
|
||||
member.source);
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
if (!resolve_set(nested_key)) {
|
||||
succeeded = false;
|
||||
continue;
|
||||
}
|
||||
const std::vector<std::int64_t>& nested_labels =
|
||||
resolved_sets_.at(nested_key);
|
||||
labels.insert(
|
||||
labels.end(), nested_labels.begin(), nested_labels.end());
|
||||
}
|
||||
|
||||
if (!succeeded) {
|
||||
state = VisitState::failed;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ranges::sort(labels);
|
||||
labels.erase(std::ranges::unique(labels).begin(), labels.end());
|
||||
resolved_sets_[key] = std::move(labels);
|
||||
state = VisitState::resolved;
|
||||
return true;
|
||||
}
|
||||
|
||||
const ParsedDeck& deck_;
|
||||
std::map<SetNamespace, std::set<std::int64_t>> entities_;
|
||||
std::map<SetKey, RawSet> raw_sets_;
|
||||
std::map<SetKey, VisitState> states_;
|
||||
std::map<SetKey, std::vector<std::int64_t>> resolved_sets_;
|
||||
std::vector<Diagnostic> diagnostics_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SetResolutionResult resolve_sets(const ParsedDeck& deck) {
|
||||
return SetResolver{deck}.resolve();
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
+83
-1
@@ -118,7 +118,11 @@ add_test(
|
||||
)
|
||||
|
||||
add_executable(fesa_abaqus_parser_tests
|
||||
unit/io/abaqus/active_input_test.cpp
|
||||
unit/io/abaqus/input_contract_test.cpp
|
||||
unit/io/abaqus/material_section_mapping_test.cpp
|
||||
unit/io/abaqus/parser_test.cpp
|
||||
unit/io/abaqus/set_resolution_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(fesa_abaqus_parser_tests PRIVATE cxx_std_20)
|
||||
@@ -147,6 +151,60 @@ add_test(
|
||||
--gtest_filter=ScopedDeck.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME AbaqusInputContract
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=AbaqusInputContract/*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME SetResolution
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=SetResolution.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME PartSet
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=PartSet.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME AssemblySet
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=AssemblySet.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ActiveInput
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=ActiveInput.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME SemanticScope
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=SemanticScope.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME MaterialMapping
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=MaterialMapping.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME BeamSection
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=BeamSection.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ShearDefault
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=ShearDefault.*
|
||||
)
|
||||
|
||||
add_executable(fesa_deck_to_domain_tests
|
||||
integration/io/minimal_deck_to_domain_test.cpp
|
||||
)
|
||||
@@ -172,11 +230,35 @@ add_test(
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ActiveInstance
|
||||
NAME SingleInstance
|
||||
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
||||
--gtest_filter=ActiveInstance.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME StepMapping
|
||||
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
||||
--gtest_filter=StepMapping.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME Boundary
|
||||
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
||||
--gtest_filter=Boundary.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME Cload
|
||||
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
||||
--gtest_filter=Cload.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME SuppliedCantilever
|
||||
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
||||
--gtest_filter=SuppliedCantilever.*
|
||||
)
|
||||
|
||||
add_executable(fesa_fem_primitives_tests
|
||||
unit/fem/beam_frame_test.cpp
|
||||
unit/fem/dof_manager_test.cpp
|
||||
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
# case_id outcome fixture expected_stage expected_code expected_line node_count element_count node_set_count element_set_count prescribed_dof_count nodal_load_count shear_source shear_area_y shear_area_z checked_node_set checked_element_set
|
||||
node_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
element_b31_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
nset_explicit_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
elset_explicit_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
material_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
elastic_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
beam_general_section_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
boundary_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
cload_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
step_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
static_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
end_step_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
part_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
end_part_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
assembly_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
end_assembly_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
instance_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
end_instance_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
assembly_nset_instance_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
generate_nset_valid valid valid/generated_nested_sets.inp - - 0 3 2 2 2 18 1 phase1_default 0.8333333333333334 0.8333333333333334 AllNodes:1,2,3 AllElements:1,2
|
||||
generate_elset_valid valid valid/generated_nested_sets.inp - - 0 3 2 2 2 18 1 phase1_default 0.8333333333333334 0.8333333333333334 AllNodes:1,2,3 AllElements:1,2
|
||||
nested_sets_valid valid valid/generated_nested_sets.inp - - 0 3 2 2 2 18 1 phase1_default 0.8333333333333334 0.8333333333333334 AllNodes:1,2,3 AllElements:1,2
|
||||
transverse_shear_valid valid valid/explicit_transverse_shear.inp - - 0 2 1 1 1 6 1 input 0.8 0.5 Fixed:1 Beam:1
|
||||
heading_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
preprint_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
restart_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
output_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
enum_case_insensitive_valid valid valid/case_insensitive_enums.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1
|
||||
node_duplicate_invalid invalid invalid/node_duplicate.inp semantic abaqus.semantic.duplicate_node_label 3 - - - - - - - - - - -
|
||||
element_type_invalid invalid invalid/element_wrong_type.inp semantic abaqus.semantic.unsupported_element 4 - - - - - - - - - - -
|
||||
part_unclosed_invalid invalid invalid/part_unclosed.inp syntax abaqus.syntax.unclosed_part 1 - - - - - - - - - - -
|
||||
assembly_multiple_invalid invalid invalid/assembly_multiple.inp syntax abaqus.syntax.multiple_assemblies 3 - - - - - - - - - - -
|
||||
instance_transform_invalid invalid invalid/instance_transform.inp semantic abaqus.semantic.instance_transform 5 - - - - - - - - - - -
|
||||
instance_local_mesh_invalid invalid invalid/instance_local_mesh.inp syntax abaqus.syntax.instance_local_keyword 5 - - - - - - - - - - -
|
||||
mixed_mesh_invalid invalid invalid/mixed_mesh.inp semantic abaqus.semantic.mixed_mesh_organization 1 - - - - - - - - - - -
|
||||
nset_parameter_invalid invalid invalid/nset_missing_name.inp semantic abaqus.semantic.missing_parameter 1 - - - - - - - - - - -
|
||||
elset_generate_invalid invalid invalid/elset_invalid_generate.inp semantic abaqus.semantic.invalid_generate 2 - - - - - - - - - - -
|
||||
nested_set_cycle_invalid invalid invalid/nested_set_cycle.inp semantic abaqus.semantic.set_cycle 4 - - - - - - - - - - -
|
||||
set_instance_invalid invalid invalid/assembly_set_wrong_instance.inp semantic abaqus.semantic.wrong_instance 6 - - - - - - - - - - -
|
||||
material_missing_elastic_invalid invalid invalid/material_missing_elastic.inp semantic abaqus.semantic.missing_elastic 1 - - - - - - - - - - -
|
||||
elastic_data_invalid invalid invalid/elastic_invalid_data.inp semantic abaqus.semantic.invalid_elastic_data 3 - - - - - - - - - - -
|
||||
section_type_invalid invalid invalid/section_wrong_type.inp semantic abaqus.semantic.unsupported_section 4 - - - - - - - - - - -
|
||||
section_duplicate_invalid invalid invalid/section_duplicate_assignment.inp semantic abaqus.semantic.duplicate_section_assignment 7 - - - - - - - - - - -
|
||||
transverse_scf_invalid invalid invalid/transverse_nonzero_scf.inp semantic abaqus.semantic.nonzero_scf 8 - - - - - - - - - - -
|
||||
boundary_conflict_invalid invalid invalid/boundary_conflict.inp semantic abaqus.semantic.conflicting_boundary 10 - - - - - - - - - - -
|
||||
cload_dof_invalid invalid invalid/cload_invalid_dof.inp semantic abaqus.semantic.invalid_dof 6 - - - - - - - - - - -
|
||||
step_multiple_invalid invalid invalid/step_multiple.inp semantic abaqus.semantic.step_count 4 - - - - - - - - - - -
|
||||
static_data_invalid invalid invalid/static_invalid_data.inp semantic abaqus.semantic.invalid_static_data 3 - - - - - - - - - - -
|
||||
end_step_missing_invalid invalid invalid/end_step_missing.inp syntax abaqus.syntax.unclosed_step 1 - - - - - - - - - - -
|
||||
heading_parameter_invalid invalid invalid/heading_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
|
||||
preprint_parameter_invalid invalid invalid/preprint_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
|
||||
restart_scope_invalid invalid invalid/restart_outside_step.inp syntax abaqus.syntax.invalid_restart_scope 1 - - - - - - - - - - -
|
||||
output_parameter_invalid invalid invalid/output_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 3 - - - - - - - - - - -
|
||||
unknown_keyword_invalid invalid invalid/unknown_include.inp syntax abaqus.unsupported_keyword 1 - - - - - - - - - - -
|
||||
node_parameter_invalid invalid invalid/node_unsupported_parameter.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
|
||||
node_scope_invalid invalid invalid/node_wrong_scope.inp syntax abaqus.syntax.invalid_node_scope 2 - - - - - - - - - - -
|
||||
material_scope_invalid invalid invalid/material_wrong_scope.inp syntax abaqus.syntax.invalid_material_scope 2 - - - - - - - - - - -
|
||||
generate_form_invalid invalid invalid/generate_valued.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
|
||||
flat_set_instance_invalid invalid invalid/flat_set_instance.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - -
|
||||
global_set_hierarchy_invalid invalid invalid/global_set_hierarchical.inp semantic abaqus.semantic.mixed_mesh_organization 1 - - - - - - - - - - -
|
||||
duplicate_part_invalid invalid invalid/duplicate_part.inp semantic abaqus.semantic.duplicate_part 3 - - - - - - - - - - -
|
||||
node_surplus_field_invalid invalid invalid/node_surplus_field.inp semantic abaqus.semantic.invalid_node_data 2 - - - - - - - - - - -
|
||||
node_empty_invalid invalid invalid/node_empty.inp semantic abaqus.semantic.invalid_node_data 1 - - - - - - - - - - -
|
||||
element_surplus_field_invalid invalid invalid/element_surplus_field.inp semantic abaqus.semantic.invalid_element_data 2 - - - - - - - - - - -
|
||||
element_empty_invalid invalid invalid/element_empty.inp semantic abaqus.semantic.invalid_element_data 1 - - - - - - - - - - -
|
||||
hierarchical_part_target_invalid invalid invalid/hierarchical_part_set_target.inp semantic abaqus.semantic.missing_node_target 25 - - - - - - - - - - -
|
||||
material_data_invalid invalid invalid/material_data.inp syntax abaqus.syntax.data_without_keyword 2 - - - - - - - - - - -
|
||||
node_coordinate_invalid invalid invalid/node_invalid_coordinate.inp semantic abaqus.semantic.invalid_number 2 - - - - - - - - - - -
|
||||
element_missing_node_invalid invalid invalid/element_missing_node.inp semantic abaqus.semantic.missing_node 4 - - - - - - - - - - -
|
||||
inactive_part_node_invalid invalid invalid/inactive_part_node_surplus.inp semantic abaqus.semantic.invalid_node_data 3 - - - - - - - - - - -
|
||||
inactive_part_element_invalid invalid invalid/inactive_part_element_type.inp semantic abaqus.semantic.unsupported_element 2 - - - - - - - - - - -
|
||||
inactive_part_section_invalid invalid invalid/inactive_part_section_data.inp semantic abaqus.semantic.invalid_section_data 3 - - - - - - - - - - -
|
||||
inactive_part_duplicate_node_invalid invalid invalid/inactive_part_duplicate_node.inp semantic abaqus.semantic.duplicate_node_label 4 - - - - - - - - - - -
|
||||
inactive_part_missing_node_invalid invalid invalid/inactive_part_missing_node.inp semantic abaqus.semantic.missing_node 5 - - - - - - - - - - -
|
||||
|
@@ -0,0 +1,4 @@
|
||||
*ASSEMBLY, NAME=First
|
||||
*END ASSEMBLY
|
||||
*ASSEMBLY, NAME=Second
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,8 @@
|
||||
*PART, NAME=BeamPart
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=Root
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*END INSTANCE
|
||||
*NSET, NSET=Fixed, INSTANCE=Other
|
||||
1
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,11 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*STEP
|
||||
*STATIC
|
||||
*BOUNDARY
|
||||
Fixed, 1, 1, 0.0
|
||||
*BOUNDARY
|
||||
Fixed, 1, 1, 1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,7 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*CLOAD
|
||||
1, 7, 1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,8 @@
|
||||
*PART, NAME=BeamPart
|
||||
*END PART
|
||||
*PART, NAME=BeamPart
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,6 @@
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
-1.0, 0.25
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,4 @@
|
||||
*ELEMENT, TYPE=B31
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,13 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
210000.0, 0.3
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,5 @@
|
||||
*ELEMENT, TYPE=B31
|
||||
1, 1, 2, 3
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,8 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B32, ELSET=Beam
|
||||
1, 1, 2
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,5 @@
|
||||
*ELSET, ELSET=Beam, GENERATE
|
||||
3, 1, 1
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,2 @@
|
||||
*STEP
|
||||
*STATIC
|
||||
@@ -0,0 +1,2 @@
|
||||
*NSET, NSET=Fixed, INSTANCE=Beam-1
|
||||
1
|
||||
@@ -0,0 +1,2 @@
|
||||
*NSET, NSET=Generated, GENERATE=YES
|
||||
1, 2, 1
|
||||
@@ -0,0 +1,7 @@
|
||||
*NSET, NSET=Ignored
|
||||
*PART, NAME=BeamPart
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1 @@
|
||||
*HEADING, NAME=Unsupported
|
||||
@@ -0,0 +1,26 @@
|
||||
*PART, NAME=BeamPart
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*STEP
|
||||
*STATIC
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*END STEP
|
||||
@@ -0,0 +1,11 @@
|
||||
*PART, NAME=Unused
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
1, 1.0, 0.0, 0.0
|
||||
*END PART
|
||||
*PART, NAME=Active
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Active-1, PART=Active
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,10 @@
|
||||
*PART, NAME=Unused
|
||||
*ELEMENT, TYPE=B32
|
||||
1, 1, 2
|
||||
*END PART
|
||||
*PART, NAME=Active
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Active-1, PART=Active
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,12 @@
|
||||
*PART, NAME=Unused
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31
|
||||
1, 1, 2
|
||||
*END PART
|
||||
*PART, NAME=Active
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Active-1, PART=Active
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,10 @@
|
||||
*PART, NAME=Unused
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0, 9.0
|
||||
*END PART
|
||||
*PART, NAME=Active
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Active-1, PART=Active
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,11 @@
|
||||
*PART, NAME=Unused
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
0.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*END PART
|
||||
*PART, NAME=Active
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Active-1, PART=Active
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,8 @@
|
||||
*PART, NAME=BeamPart
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=Root
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,7 @@
|
||||
*PART, NAME=BeamPart
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=Root
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
1.0, 2.0, 3.0
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,2 @@
|
||||
*MATERIAL, NAME=Steel
|
||||
unexpected data
|
||||
@@ -0,0 +1,4 @@
|
||||
*MATERIAL, NAME=Steel
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,3 @@
|
||||
*PART, NAME=BeamPart
|
||||
*MATERIAL, NAME=Steel
|
||||
*END PART
|
||||
@@ -0,0 +1,8 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
*PART, NAME=BeamPart
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=Root
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*END INSTANCE
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,7 @@
|
||||
*NSET, NSET=First
|
||||
Second
|
||||
*NSET, NSET=Second
|
||||
First
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,6 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
1, 1.0, 0.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,4 @@
|
||||
*NODE
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,5 @@
|
||||
*NODE
|
||||
1, invalid, 0.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,5 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0, 9.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,2 @@
|
||||
*NODE, EXTRA=1
|
||||
1, 0.0, 0.0, 0.0
|
||||
@@ -0,0 +1,4 @@
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
*END ASSEMBLY
|
||||
@@ -0,0 +1,5 @@
|
||||
*NSET
|
||||
1
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,4 @@
|
||||
*STEP
|
||||
*STATIC
|
||||
*OUTPUT, FIELD, VARIABLE=ALL
|
||||
*END STEP
|
||||
@@ -0,0 +1,3 @@
|
||||
*PART, NAME=BeamPart
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
@@ -0,0 +1 @@
|
||||
*PREPRINT, COLOR=YES
|
||||
@@ -0,0 +1 @@
|
||||
*RESTART, WRITE, FREQUENCY=0
|
||||
@@ -0,0 +1,12 @@
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 2.0, 0.0, 2.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,9 @@
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=ARBITRARY, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,4 @@
|
||||
*STEP
|
||||
*STATIC
|
||||
1.0, -1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,6 @@
|
||||
*STEP, NAME=First
|
||||
*STATIC
|
||||
*END STEP
|
||||
*STEP, NAME=Second
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1,11 @@
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*TRANSVERSE SHEAR STIFFNESS
|
||||
64.0, 40.0, 0.25
|
||||
*STEP
|
||||
*STATIC
|
||||
*END STEP
|
||||
@@ -0,0 +1 @@
|
||||
*INCLUDE, INPUT=other.inp
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
1, 0.0, 0.0, 0.0
|
||||
** Comments and blank lines do not terminate the current data record.
|
||||
|
||||
2, 1.0, 0.0, 0.0,
|
||||
2, 1.0, 0.0, 0.0
|
||||
*eLeMeNt, TYPE=B31, elset=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
@@ -15,7 +15,7 @@
|
||||
*ELASTIC
|
||||
210000.0, 0.3
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 1.0, 1.0, 1.0
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 1.0, 1.0, 1.0
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*END PART
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=b31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=general, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*STEP, NAME=Load
|
||||
*STATIC
|
||||
*CLOAD
|
||||
2, 2, -1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,24 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*TRANSVERSE SHEAR STIFFNESS
|
||||
64.0, 40.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*CLOAD
|
||||
2, 2, -1.0
|
||||
*END STEP
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*STEP, NAME=Load
|
||||
*STATIC
|
||||
*CLOAD
|
||||
2, 2, -1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,28 @@
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
3, 2.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31
|
||||
1, 1, 2
|
||||
2, 2, 3
|
||||
*NSET, NSET=FirstTwo, GENERATE
|
||||
1, 2, 1
|
||||
*NSET, NSET=AllNodes
|
||||
FirstTwo, 3
|
||||
*ELSET, ELSET=GeneratedElements, GENERATE
|
||||
1, 2, 1
|
||||
*ELSET, ELSET=AllElements
|
||||
GeneratedElements
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=AllElements, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*BOUNDARY
|
||||
AllNodes, 1, 6
|
||||
*CLOAD
|
||||
3, 2, -1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,31 @@
|
||||
*PART, NAME=BeamPart
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*END PART
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*END INSTANCE
|
||||
*NSET, NSET=Fixed, INSTANCE=Beam-1
|
||||
1
|
||||
*NSET, NSET=Tip, INSTANCE=Beam-1
|
||||
2
|
||||
*END ASSEMBLY
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*STEP, NAME=Load, NLGEOM=NO
|
||||
*STATIC
|
||||
1.0, 1.0, 0.01, 1.0
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*CLOAD
|
||||
Tip, 2, -1.0
|
||||
*END STEP
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
*HEADING
|
||||
FESA recognized no-op heading
|
||||
*PREPRINT, ECHO=NO, MODEL=NO, HISTORY=NO, CONTACT=NO
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
200.0, 0.25
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 0.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*STEP
|
||||
*STATIC
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*CLOAD
|
||||
2, 2, -1.0
|
||||
*RESTART, WRITE, FREQUENCY=0
|
||||
*OUTPUT, FIELD, VARIABLE=PRESELECT
|
||||
*OUTPUT, HISTORY, VARIABLE=PRESELECT
|
||||
*END STEP
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
@@ -64,6 +65,14 @@ bool has_diagnostic(
|
||||
});
|
||||
}
|
||||
|
||||
const fesa::Diagnostic* find_diagnostic(
|
||||
const fesa::DomainBuildResult& result,
|
||||
const std::string_view code) {
|
||||
const auto found = std::ranges::find(
|
||||
result.diagnostics, code, &fesa::Diagnostic::code);
|
||||
return found == result.diagnostics.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
void expect_equivalent_analysis_data(
|
||||
const fesa::Domain& flat,
|
||||
const fesa::Domain& hierarchical) {
|
||||
@@ -164,6 +173,128 @@ TEST(DeckToDomain, NormalizesFlatAndSingleInstanceDecksEquivalently) {
|
||||
fesa::ShearPropertySource::phase1_default);
|
||||
}
|
||||
|
||||
TEST(DeckToDomain, RequiresExactNodeAndB31DataWidthsAndNonemptyRecords) {
|
||||
struct Case final {
|
||||
std::string_view name;
|
||||
std::string_view mesh;
|
||||
std::string_view code;
|
||||
};
|
||||
const Case cases[]{
|
||||
{
|
||||
"node-surplus-field",
|
||||
"*NODE\n1, 0.0, 0.0, 0.0, 9.0\n"
|
||||
"2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
|
||||
"abaqus.semantic.invalid_node_data",
|
||||
},
|
||||
{
|
||||
"node-empty",
|
||||
"*NODE\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
|
||||
"abaqus.semantic.invalid_node_data",
|
||||
},
|
||||
{
|
||||
"element-surplus-field",
|
||||
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2, 3\n",
|
||||
"abaqus.semantic.invalid_element_data",
|
||||
},
|
||||
{
|
||||
"element-empty",
|
||||
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n",
|
||||
"abaqus.semantic.invalid_element_data",
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto& test_case : cases) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-exact-mesh-data-" + std::string{test_case.name} + ".inp",
|
||||
std::string{test_case.mesh} +
|
||||
"*ELSET, ELSET=Beam\n1\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n210000.0, 0.3\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, "
|
||||
"MATERIAL=Steel\n"
|
||||
"1.0, 1.0, 0.0, 1.0, 1.0\n0.0, 1.0, 0.0\n"
|
||||
"*STEP\n*STATIC\n*END STEP\n",
|
||||
};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
SCOPED_TRACE(test_case.name);
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
EXPECT_TRUE(has_diagnostic(result, test_case.code));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeckToDomain, ReportsMeshDataErrorsAtTheOffendingRows) {
|
||||
const TemporaryDeck invalid_coordinate{
|
||||
"fesa-node-coordinate-source.inp",
|
||||
"*NODE\n"
|
||||
"1, invalid, 0.0, 0.0\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
const TemporaryDeck missing_node{
|
||||
"fesa-element-node-source.inp",
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
|
||||
"1, 1, 2\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"210000.0, 0.3\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto coordinate_result = parse_and_map(invalid_coordinate.path());
|
||||
const auto missing_node_result = parse_and_map(missing_node.path());
|
||||
|
||||
const fesa::Diagnostic* coordinate =
|
||||
find_diagnostic(coordinate_result, "abaqus.semantic.invalid_number");
|
||||
ASSERT_NE(coordinate, nullptr);
|
||||
ASSERT_TRUE(coordinate->source.has_value());
|
||||
EXPECT_EQ(coordinate->source->line, 2U);
|
||||
|
||||
const fesa::Diagnostic* node =
|
||||
find_diagnostic(missing_node_result, "abaqus.semantic.missing_node");
|
||||
ASSERT_NE(node, nullptr);
|
||||
ASSERT_TRUE(node->source.has_value());
|
||||
EXPECT_EQ(node->source->line, 4U);
|
||||
}
|
||||
|
||||
TEST(DeckToDomain, AcceptsCaseInsensitiveElementAndSectionValues) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-case-insensitive-enums.inp",
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=b31, ELSET=Beam\n"
|
||||
"1, 1, 2\n"
|
||||
"*ELSET, ELSET=Beam\n"
|
||||
"1\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"210000.0, 0.3\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=general, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
ASSERT_TRUE(result.domain.has_value());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(result.domain->beam_elements().size(), 1U);
|
||||
}
|
||||
|
||||
TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-active-instance.inp",
|
||||
@@ -202,6 +333,92 @@ TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) {
|
||||
EXPECT_EQ(result.domain->nodes()[1].origin.part_name, "BeamPart");
|
||||
}
|
||||
|
||||
TEST(ActiveInstance, RejectsInvalidRecordsInUnreferencedParts) {
|
||||
struct Case final {
|
||||
std::string_view name;
|
||||
std::string_view unused_records;
|
||||
std::string_view code;
|
||||
std::size_t line;
|
||||
};
|
||||
const Case cases[]{
|
||||
{
|
||||
"node-surplus",
|
||||
"*NODE\n1, 0.0, 0.0, 0.0, 9.0\n",
|
||||
"abaqus.semantic.invalid_node_data",
|
||||
3U,
|
||||
},
|
||||
{
|
||||
"element-type",
|
||||
"*ELEMENT, TYPE=B32\n1, 1, 2\n",
|
||||
"abaqus.semantic.unsupported_element",
|
||||
2U,
|
||||
},
|
||||
{
|
||||
"duplicate-node",
|
||||
"*NODE\n1, 0.0, 0.0, 0.0\n1, 1.0, 0.0, 0.0\n",
|
||||
"abaqus.semantic.duplicate_node_label",
|
||||
4U,
|
||||
},
|
||||
{
|
||||
"missing-node",
|
||||
"*NODE\n1, 0.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31\n1, 1, 2\n",
|
||||
"abaqus.semantic.missing_node",
|
||||
5U,
|
||||
},
|
||||
{
|
||||
"section-data",
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, "
|
||||
"MATERIAL=Steel\n"
|
||||
"0.0, 1.0, 0.0, 1.0, 1.0\n"
|
||||
"0.0, 1.0, 0.0\n",
|
||||
"abaqus.semantic.invalid_section_data",
|
||||
3U,
|
||||
},
|
||||
};
|
||||
const std::string active_model{
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
|
||||
"1, 1, 2\n"
|
||||
"*ELSET, ELSET=Beam\n"
|
||||
"1\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"210000.0, 0.3\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
for (const auto& test_case : cases) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-inactive-part-" + std::string{test_case.name} + ".inp",
|
||||
"*PART, NAME=Unused\n" + std::string{test_case.unused_records} +
|
||||
"*END PART\n" + active_model,
|
||||
};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
SCOPED_TRACE(test_case.name);
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
const fesa::Diagnostic* diagnostic =
|
||||
find_diagnostic(result, test_case.code);
|
||||
ASSERT_NE(diagnostic, nullptr);
|
||||
ASSERT_TRUE(diagnostic->source.has_value());
|
||||
EXPECT_EQ(diagnostic->source->line, test_case.line);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-instance-transform.inp",
|
||||
@@ -219,7 +436,7 @@ TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
|
||||
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_transform"));
|
||||
ASSERT_FALSE(result.diagnostics.empty());
|
||||
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
||||
EXPECT_EQ(result.diagnostics.front().source->line, 4U);
|
||||
EXPECT_EQ(result.diagnostics.front().source->line, 5U);
|
||||
}
|
||||
|
||||
TEST(ActiveInstance, RejectsMissingPartReferenceWithSourceDiagnostic) {
|
||||
@@ -265,4 +482,248 @@ TEST(ActiveInstance, RejectsMultipleInstances) {
|
||||
EXPECT_EQ(diagnostic->source->line, 6U);
|
||||
}
|
||||
|
||||
TEST(StepMapping, MapsOneStaticStepAndRejectsUnsupportedConfigurations) {
|
||||
const auto valid =
|
||||
parse_and_map(fixture_path("valid/noop_directives.inp"));
|
||||
|
||||
ASSERT_TRUE(valid.domain.has_value());
|
||||
EXPECT_TRUE(valid.diagnostics.empty());
|
||||
EXPECT_EQ(valid.domain->step().name, "Step-1");
|
||||
|
||||
struct InvalidCase final {
|
||||
std::string_view name;
|
||||
std::string_view contents;
|
||||
std::string_view code;
|
||||
std::size_t line;
|
||||
};
|
||||
const InvalidCase invalid_cases[]{
|
||||
{
|
||||
"fesa-step-multiple.inp",
|
||||
"*STEP, NAME=First\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"
|
||||
"*STEP, NAME=Second\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n",
|
||||
"abaqus.semantic.step_count",
|
||||
4U,
|
||||
},
|
||||
{
|
||||
"fesa-step-nlgeom.inp",
|
||||
"*STEP, NLGEOM=YES\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n",
|
||||
"abaqus.semantic.unsupported_step_option",
|
||||
1U,
|
||||
},
|
||||
{
|
||||
"fesa-static-invalid.inp",
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"1.0, -1.0\n"
|
||||
"*END STEP\n",
|
||||
"abaqus.semantic.invalid_static_data",
|
||||
3U,
|
||||
},
|
||||
{
|
||||
"fesa-static-extra-row.inp",
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"1.0\n"
|
||||
"2.0\n"
|
||||
"*END STEP\n",
|
||||
"abaqus.semantic.invalid_static_data",
|
||||
4U,
|
||||
},
|
||||
};
|
||||
|
||||
for (const InvalidCase& test_case : invalid_cases) {
|
||||
const TemporaryDeck input{test_case.name, test_case.contents};
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
SCOPED_TRACE(test_case.name);
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
const fesa::Diagnostic* diagnostic =
|
||||
find_diagnostic(result, test_case.code);
|
||||
ASSERT_NE(diagnostic, nullptr);
|
||||
ASSERT_TRUE(diagnostic->source.has_value());
|
||||
EXPECT_EQ(diagnostic->source->line, test_case.line);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StepMapping, RejectsModelDataInsideStep) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-node-inside-step.inp",
|
||||
"*STEP\n"
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
const fesa::Diagnostic* diagnostic =
|
||||
find_diagnostic(result, "abaqus.syntax.invalid_node_scope");
|
||||
ASSERT_NE(diagnostic, nullptr);
|
||||
ASSERT_TRUE(diagnostic->source.has_value());
|
||||
EXPECT_EQ(diagnostic->source->line, 2U);
|
||||
}
|
||||
|
||||
TEST(StepMapping, RejectsBoundaryAfterCompletedStep) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-boundary-after-step.inp",
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"
|
||||
"*BOUNDARY\n"
|
||||
"1, 1\n"};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
const fesa::Diagnostic* diagnostic =
|
||||
find_diagnostic(result, "abaqus.syntax.invalid_boundary_scope");
|
||||
ASSERT_NE(diagnostic, nullptr);
|
||||
ASSERT_TRUE(diagnostic->source.has_value());
|
||||
EXPECT_EQ(diagnostic->source->line, 4U);
|
||||
}
|
||||
|
||||
TEST(Boundary, CanonicalizesIdenticalGlobalAndStepPrescriptions) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-boundary-canonical.inp",
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"*BOUNDARY\n"
|
||||
"1, 1, 3, 2.5\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*BOUNDARY\n"
|
||||
"1, 1, 3, 2.5\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
ASSERT_TRUE(result.domain.has_value());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
const auto& prescribed = result.domain->step().prescribed_dofs;
|
||||
ASSERT_EQ(prescribed.size(), 3U);
|
||||
for (std::size_t index = 0; index < prescribed.size(); ++index) {
|
||||
EXPECT_EQ(prescribed[index].node, fesa::NodeId{0});
|
||||
EXPECT_EQ(prescribed[index].dof, index + 1U);
|
||||
EXPECT_DOUBLE_EQ(prescribed[index].value, 2.5);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Boundary, HierarchicalTargetsRequireAssemblySets) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-hierarchical-part-set-target.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
|
||||
"1, 1, 2\n"
|
||||
"*NSET, NSET=Fixed\n"
|
||||
"1\n"
|
||||
"*ELSET, ELSET=Beam\n"
|
||||
"1\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"210000.0, 0.3\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*BOUNDARY\n"
|
||||
"Fixed, 1, 6\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
EXPECT_TRUE(has_diagnostic(result, "abaqus.semantic.missing_node_target"));
|
||||
}
|
||||
|
||||
TEST(Boundary, ReportsConflictAtTheConflictingDataRow) {
|
||||
const auto result =
|
||||
parse_and_map(fixture_path("invalid/boundary_conflict.inp"));
|
||||
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
const fesa::Diagnostic* diagnostic =
|
||||
find_diagnostic(result, "abaqus.semantic.conflicting_boundary");
|
||||
ASSERT_NE(diagnostic, nullptr);
|
||||
ASSERT_TRUE(diagnostic->source.has_value());
|
||||
EXPECT_EQ(diagnostic->source->line, 10U);
|
||||
}
|
||||
|
||||
TEST(Cload, SumsForceAndMomentComponentsInInputOrder) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-cload-sum.inp",
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"*STEP, NAME=Load\n"
|
||||
"*STATIC\n"
|
||||
"*CLOAD\n"
|
||||
"1, 1, 2.0\n"
|
||||
"1, 1, -0.5\n"
|
||||
"1, 6, 4.0\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input.path());
|
||||
|
||||
ASSERT_TRUE(result.domain.has_value());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
ASSERT_EQ(result.domain->step().nodal_loads.size(), 1U);
|
||||
const auto& load = result.domain->step().nodal_loads.front();
|
||||
EXPECT_EQ(load.node, fesa::NodeId{0});
|
||||
EXPECT_EQ(
|
||||
load.values,
|
||||
(std::array<double, 6>{1.5, 0.0, 0.0, 0.0, 0.0, 4.0}));
|
||||
}
|
||||
|
||||
TEST(Cload, ReportsInvalidDofAtTheDataRow) {
|
||||
const auto result =
|
||||
parse_and_map(fixture_path("invalid/cload_invalid_dof.inp"));
|
||||
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
const fesa::Diagnostic* diagnostic =
|
||||
find_diagnostic(result, "abaqus.semantic.invalid_dof");
|
||||
ASSERT_NE(diagnostic, nullptr);
|
||||
ASSERT_TRUE(diagnostic->source.has_value());
|
||||
EXPECT_EQ(diagnostic->source->line, 6U);
|
||||
}
|
||||
|
||||
TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
|
||||
"reference" / "cantilever beam" / "cantilever beam.inp";
|
||||
|
||||
const auto result = parse_and_map(path);
|
||||
|
||||
ASSERT_TRUE(result.domain.has_value());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
const fesa::Domain& domain = *result.domain;
|
||||
EXPECT_EQ(domain.nodes().size(), 11U);
|
||||
EXPECT_EQ(domain.beam_elements().size(), 10U);
|
||||
EXPECT_EQ(domain.step().prescribed_dofs.size(), 6U);
|
||||
ASSERT_EQ(domain.step().nodal_loads.size(), 1U);
|
||||
|
||||
const auto node = std::ranges::find_if(
|
||||
domain.nodes(),
|
||||
[](const fesa::Node& candidate) {
|
||||
return candidate.origin.local_label == 11;
|
||||
});
|
||||
ASSERT_NE(node, domain.nodes().end());
|
||||
EXPECT_EQ(domain.step().nodal_loads.front().node, node->id);
|
||||
EXPECT_EQ(
|
||||
domain.step().nodal_loads.front().values,
|
||||
(std::array<double, 6>{0.0, 0.0, -10000.0, 0.0, 0.0, 0.0}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#include <fesa/io/abaqus/active_input.hpp>
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
class TemporaryDeck final {
|
||||
public:
|
||||
TemporaryDeck(std::string_view name, std::string_view contents)
|
||||
: path_{std::filesystem::path{testing::TempDir()} / name} {
|
||||
std::ofstream output{path_, std::ios::binary};
|
||||
output.write(
|
||||
contents.data(), static_cast<std::streamsize>(contents.size()));
|
||||
if (!output) {
|
||||
throw std::runtime_error{"Failed to write temporary Abaqus deck."};
|
||||
}
|
||||
}
|
||||
|
||||
~TemporaryDeck() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path_, error);
|
||||
}
|
||||
|
||||
TemporaryDeck(const TemporaryDeck&) = delete;
|
||||
TemporaryDeck& operator=(const TemporaryDeck&) = delete;
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||
return path_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
std::filesystem::path fixture_path(std::string_view name) {
|
||||
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
|
||||
"abaqus" / name;
|
||||
}
|
||||
|
||||
fesa::ParsedDeck parse(const std::filesystem::path& path) {
|
||||
auto parsed = fesa::parse_deck(path);
|
||||
if (!parsed.deck.has_value()) {
|
||||
throw std::runtime_error{"Test deck did not parse."};
|
||||
}
|
||||
return std::move(*parsed.deck);
|
||||
}
|
||||
|
||||
fesa::Diagnostic expect_failure(
|
||||
const fesa::ParsedDeck& deck,
|
||||
const std::string_view code,
|
||||
const std::size_t line) {
|
||||
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||
EXPECT_FALSE(result.input.has_value());
|
||||
const auto diagnostic = std::ranges::find(
|
||||
result.diagnostics, code, &fesa::Diagnostic::code);
|
||||
if (diagnostic == result.diagnostics.end()) {
|
||||
throw std::runtime_error{"Expected active-input diagnostic was not found."};
|
||||
}
|
||||
EXPECT_EQ(diagnostic->stage, fesa::DiagnosticStage::semantic);
|
||||
EXPECT_TRUE(diagnostic->source.has_value());
|
||||
if (diagnostic->source.has_value()) {
|
||||
EXPECT_EQ(diagnostic->source->line, line);
|
||||
}
|
||||
return *diagnostic;
|
||||
}
|
||||
|
||||
TEST(ActiveInput, SelectsFlatGlobalRecords) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("minimal_cantilever.inp"));
|
||||
|
||||
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||
|
||||
ASSERT_TRUE(result.input.has_value());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_TRUE(result.input->flat);
|
||||
EXPECT_TRUE(result.input->part_name.empty());
|
||||
EXPECT_TRUE(result.input->instance_name.empty());
|
||||
EXPECT_EQ(result.input->part_records.data(), deck.global_records.data());
|
||||
EXPECT_EQ(result.input->part_records.size(), deck.global_records.size());
|
||||
EXPECT_TRUE(result.input->assembly_records.empty());
|
||||
}
|
||||
|
||||
TEST(ActiveInput, SelectsOnlyTheReferencedPartAndAssemblyRecords) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-active-input-selection.inp",
|
||||
"*PART, NAME=Unused\n"
|
||||
"*NODE\n"
|
||||
"99, 9.0, 0.0, 0.0\n"
|
||||
"*END PART\n"
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*NSET, NSET=Fixed, INSTANCE=Beam-1\n"
|
||||
"1\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
const fesa::ParsedDeck deck = parse(input.path());
|
||||
|
||||
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||
|
||||
ASSERT_TRUE(result.input.has_value());
|
||||
EXPECT_FALSE(result.input->flat);
|
||||
EXPECT_EQ(result.input->part_name, "BeamPart");
|
||||
EXPECT_EQ(result.input->instance_name, "Beam-1");
|
||||
ASSERT_EQ(result.input->part_records.size(), 1U);
|
||||
EXPECT_EQ(result.input->part_records[0].keyword, "NODE");
|
||||
ASSERT_EQ(result.input->assembly_records.size(), 1U);
|
||||
EXPECT_EQ(result.input->assembly_records[0].keyword, "NSET");
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsMixedFlatAndHierarchicalMeshAtTheFlatRecord) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("invalid/mixed_mesh.inp"));
|
||||
|
||||
const fesa::Diagnostic diagnostic = expect_failure(
|
||||
deck, "abaqus.semantic.mixed_mesh_organization", 1U);
|
||||
|
||||
EXPECT_EQ(diagnostic.source->file, fixture_path("invalid/mixed_mesh.inp"));
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsGlobalSetsInHierarchicalInput) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-global-set-with-hierarchy.inp",
|
||||
"*NSET, NSET=Ignored\n"
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
expect_failure(
|
||||
parse(input.path()), "abaqus.semantic.mixed_mesh_organization", 1U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsDuplicatePartNamesAtTheSecondDefinition) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-duplicate-parts.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
expect_failure(
|
||||
parse(input.path()), "abaqus.semantic.duplicate_part", 3U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsHierarchicalInputWithoutAnAssembly) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-assembly.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"};
|
||||
const fesa::ParsedDeck deck = parse(input.path());
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.assembly_count", 1U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsAnAssemblyWithoutExactlyOneInstance) {
|
||||
const TemporaryDeck no_instance{
|
||||
"fesa-no-instance.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
const TemporaryDeck multiple_instances{
|
||||
"fesa-multiple-instances.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*INSTANCE, NAME=Beam-2, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
expect_failure(
|
||||
parse(no_instance.path()), "abaqus.semantic.instance_count", 3U);
|
||||
expect_failure(
|
||||
parse(multiple_instances.path()),
|
||||
"abaqus.semantic.instance_count",
|
||||
6U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsTransformAtTheFirstTransformDataRow) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("invalid/instance_transform.inp"));
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.instance_transform", 5U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsAnInstanceThatReferencesAMissingPart) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-part.inp",
|
||||
"*PART, NAME=OtherPart\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=MissingPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
const fesa::ParsedDeck deck = parse(input.path());
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.missing_part", 4U);
|
||||
}
|
||||
|
||||
TEST(ActiveInput, RejectsAssemblySetsForAnotherInstance) {
|
||||
const fesa::ParsedDeck deck =
|
||||
parse(fixture_path("invalid/assembly_set_wrong_instance.inp"));
|
||||
|
||||
expect_failure(deck, "abaqus.semantic.wrong_instance", 6U);
|
||||
}
|
||||
|
||||
TEST(SemanticScope, RejectsInstanceLocalMeshAtTheKeywordRow) {
|
||||
const auto path = fixture_path("invalid/instance_local_mesh.inp");
|
||||
|
||||
const fesa::ParseDeckResult result = fesa::parse_deck(path);
|
||||
|
||||
EXPECT_FALSE(result.deck.has_value());
|
||||
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||
EXPECT_EQ(result.diagnostics[0].stage, fesa::DiagnosticStage::syntax);
|
||||
EXPECT_EQ(
|
||||
result.diagnostics[0].code,
|
||||
"abaqus.syntax.instance_local_keyword");
|
||||
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||
EXPECT_EQ(result.diagnostics[0].source->file, path);
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 5U);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,294 @@
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
struct ContractCase final {
|
||||
std::string id;
|
||||
bool valid;
|
||||
std::filesystem::path fixture;
|
||||
std::optional<fesa::DiagnosticStage> expected_stage;
|
||||
std::string expected_code;
|
||||
std::size_t expected_line;
|
||||
std::optional<std::size_t> node_count;
|
||||
std::optional<std::size_t> element_count;
|
||||
std::optional<std::size_t> node_set_count;
|
||||
std::optional<std::size_t> element_set_count;
|
||||
std::optional<std::size_t> prescribed_dof_count;
|
||||
std::optional<std::size_t> nodal_load_count;
|
||||
std::string shear_source;
|
||||
std::optional<double> shear_area_y;
|
||||
std::optional<double> shear_area_z;
|
||||
std::string checked_node_set;
|
||||
std::string checked_element_set;
|
||||
};
|
||||
|
||||
struct ContractOutcome final {
|
||||
std::optional<fesa::Domain> domain;
|
||||
std::vector<fesa::Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
std::vector<std::string> split(
|
||||
const std::string_view text,
|
||||
const char delimiter) {
|
||||
std::vector<std::string> fields;
|
||||
std::size_t first = 0;
|
||||
while (true) {
|
||||
const std::size_t next = text.find(delimiter, first);
|
||||
fields.emplace_back(
|
||||
text.substr(
|
||||
first,
|
||||
next == std::string_view::npos
|
||||
? std::string_view::npos
|
||||
: next - first));
|
||||
if (next == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
first = next + 1U;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> optional_size(const std::string& field) {
|
||||
if (field == "-") {
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<std::size_t>(std::stoull(field));
|
||||
}
|
||||
|
||||
std::optional<double> optional_real(const std::string& field) {
|
||||
if (field == "-") {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::stod(field);
|
||||
}
|
||||
|
||||
std::optional<fesa::DiagnosticStage> optional_stage(
|
||||
const std::string& field) {
|
||||
if (field == "-") {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (field == "io") {
|
||||
return fesa::DiagnosticStage::io;
|
||||
}
|
||||
if (field == "syntax") {
|
||||
return fesa::DiagnosticStage::syntax;
|
||||
}
|
||||
if (field == "semantic") {
|
||||
return fesa::DiagnosticStage::semantic;
|
||||
}
|
||||
if (field == "model") {
|
||||
return fesa::DiagnosticStage::model;
|
||||
}
|
||||
throw std::runtime_error{"Unknown diagnostic stage in contract manifest."};
|
||||
}
|
||||
|
||||
std::vector<ContractCase> load_contract_cases() {
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" /
|
||||
"contract.tsv";
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
if (!input) {
|
||||
throw std::runtime_error{"Unable to open Abaqus contract manifest."};
|
||||
}
|
||||
|
||||
std::vector<ContractCase> cases;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.empty() || line.front() == '#') {
|
||||
continue;
|
||||
}
|
||||
const std::vector<std::string> fields = split(line, '\t');
|
||||
if (fields.size() != 17U) {
|
||||
throw std::runtime_error{
|
||||
"Abaqus contract manifest row must contain 17 fields."};
|
||||
}
|
||||
const bool valid = fields[1] == "valid";
|
||||
if (!valid && fields[1] != "invalid") {
|
||||
throw std::runtime_error{
|
||||
"Abaqus contract outcome must be valid or invalid."};
|
||||
}
|
||||
cases.push_back({
|
||||
fields[0],
|
||||
valid,
|
||||
fields[2],
|
||||
optional_stage(fields[3]),
|
||||
fields[4] == "-" ? std::string{} : fields[4],
|
||||
static_cast<std::size_t>(std::stoull(fields[5])),
|
||||
optional_size(fields[6]),
|
||||
optional_size(fields[7]),
|
||||
optional_size(fields[8]),
|
||||
optional_size(fields[9]),
|
||||
optional_size(fields[10]),
|
||||
optional_size(fields[11]),
|
||||
fields[12],
|
||||
optional_real(fields[13]),
|
||||
optional_real(fields[14]),
|
||||
fields[15],
|
||||
fields[16],
|
||||
});
|
||||
}
|
||||
if (cases.empty()) {
|
||||
throw std::runtime_error{"Abaqus contract manifest is empty."};
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
ContractOutcome parse_and_map(const std::filesystem::path& path) {
|
||||
auto parsed = fesa::parse_deck(path);
|
||||
if (!parsed.deck.has_value()) {
|
||||
return {std::nullopt, std::move(parsed.diagnostics)};
|
||||
}
|
||||
auto mapped = fesa::map_deck_to_domain(*parsed.deck);
|
||||
return {std::move(mapped.domain), std::move(mapped.diagnostics)};
|
||||
}
|
||||
|
||||
std::string diagnostics_text(
|
||||
const std::vector<fesa::Diagnostic>& diagnostics) {
|
||||
std::ostringstream output;
|
||||
for (const auto& diagnostic : diagnostics) {
|
||||
output << diagnostic.code;
|
||||
if (diagnostic.source.has_value()) {
|
||||
output << '@' << diagnostic.source->line;
|
||||
}
|
||||
output << '\n';
|
||||
}
|
||||
return output.str();
|
||||
}
|
||||
|
||||
std::pair<std::string, std::vector<std::int64_t>> expected_set(
|
||||
const std::string& field) {
|
||||
const std::size_t colon = field.find(':');
|
||||
if (colon == std::string::npos) {
|
||||
throw std::runtime_error{"Set check must use name:label,... format."};
|
||||
}
|
||||
std::vector<std::int64_t> labels;
|
||||
for (const std::string& label :
|
||||
split(std::string_view{field}.substr(colon + 1U), ',')) {
|
||||
labels.push_back(std::stoll(label));
|
||||
}
|
||||
return {field.substr(0, colon), std::move(labels)};
|
||||
}
|
||||
|
||||
void expect_node_set(
|
||||
const fesa::Domain& domain,
|
||||
const std::string& field) {
|
||||
if (field == "-") {
|
||||
return;
|
||||
}
|
||||
const auto [name, expected_labels] = expected_set(field);
|
||||
const auto found = std::ranges::find(
|
||||
domain.node_sets(), name, &fesa::NodeSet::name);
|
||||
ASSERT_NE(found, domain.node_sets().end());
|
||||
|
||||
std::vector<std::int64_t> actual_labels;
|
||||
for (const fesa::NodeId member : found->members) {
|
||||
actual_labels.push_back(domain.node(member).origin.local_label);
|
||||
}
|
||||
std::ranges::sort(actual_labels);
|
||||
EXPECT_EQ(actual_labels, expected_labels);
|
||||
}
|
||||
|
||||
void expect_element_set(
|
||||
const fesa::Domain& domain,
|
||||
const std::string& field) {
|
||||
if (field == "-") {
|
||||
return;
|
||||
}
|
||||
const auto [name, expected_labels] = expected_set(field);
|
||||
const auto found = std::ranges::find(
|
||||
domain.element_sets(), name, &fesa::ElementSet::name);
|
||||
ASSERT_NE(found, domain.element_sets().end());
|
||||
|
||||
std::vector<std::int64_t> actual_labels;
|
||||
for (const fesa::ElementId member : found->members) {
|
||||
const auto element = std::ranges::find(
|
||||
domain.beam_elements(), member, &fesa::BeamElement::id);
|
||||
ASSERT_NE(element, domain.beam_elements().end());
|
||||
actual_labels.push_back(element->origin.local_label);
|
||||
}
|
||||
std::ranges::sort(actual_labels);
|
||||
EXPECT_EQ(actual_labels, expected_labels);
|
||||
}
|
||||
|
||||
class AbaqusInputContractTest
|
||||
: public testing::TestWithParam<ContractCase> {};
|
||||
|
||||
TEST_P(AbaqusInputContractTest, FixtureMatchesNormativeContract) {
|
||||
const ContractCase& contract = GetParam();
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" /
|
||||
contract.fixture;
|
||||
SCOPED_TRACE(contract.id);
|
||||
ASSERT_TRUE(std::filesystem::is_regular_file(path));
|
||||
|
||||
const ContractOutcome outcome = parse_and_map(path);
|
||||
if (!contract.valid) {
|
||||
EXPECT_FALSE(outcome.domain.has_value());
|
||||
ASSERT_TRUE(contract.expected_stage.has_value());
|
||||
const auto diagnostic = std::ranges::find_if(
|
||||
outcome.diagnostics,
|
||||
[&contract](const fesa::Diagnostic& candidate) {
|
||||
return candidate.stage == *contract.expected_stage &&
|
||||
candidate.code == contract.expected_code &&
|
||||
candidate.source.has_value() &&
|
||||
candidate.source->line == contract.expected_line;
|
||||
});
|
||||
EXPECT_NE(diagnostic, outcome.diagnostics.end())
|
||||
<< "Expected " << contract.expected_code << '@'
|
||||
<< contract.expected_line << " but received:\n"
|
||||
<< diagnostics_text(outcome.diagnostics);
|
||||
return;
|
||||
}
|
||||
|
||||
ASSERT_TRUE(outcome.domain.has_value())
|
||||
<< diagnostics_text(outcome.diagnostics);
|
||||
ASSERT_TRUE(outcome.diagnostics.empty())
|
||||
<< diagnostics_text(outcome.diagnostics);
|
||||
const fesa::Domain& domain = *outcome.domain;
|
||||
EXPECT_EQ(domain.nodes().size(), *contract.node_count);
|
||||
EXPECT_EQ(domain.beam_elements().size(), *contract.element_count);
|
||||
EXPECT_EQ(domain.node_sets().size(), *contract.node_set_count);
|
||||
EXPECT_EQ(domain.element_sets().size(), *contract.element_set_count);
|
||||
EXPECT_EQ(
|
||||
domain.step().prescribed_dofs.size(),
|
||||
*contract.prescribed_dof_count);
|
||||
EXPECT_EQ(domain.step().nodal_loads.size(), *contract.nodal_load_count);
|
||||
|
||||
ASSERT_EQ(domain.sections().size(), 1U);
|
||||
const fesa::BeamSection& section = domain.sections().front();
|
||||
const auto expected_source = contract.shear_source == "input"
|
||||
? fesa::ShearPropertySource::input
|
||||
: fesa::ShearPropertySource::phase1_default;
|
||||
EXPECT_EQ(section.shear_source, expected_source);
|
||||
EXPECT_NEAR(section.shear_area_y, *contract.shear_area_y, 1.0e-12);
|
||||
EXPECT_NEAR(section.shear_area_z, *contract.shear_area_z, 1.0e-12);
|
||||
expect_node_set(domain, contract.checked_node_set);
|
||||
expect_element_set(domain, contract.checked_element_set);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
AbaqusInputContract,
|
||||
AbaqusInputContractTest,
|
||||
testing::ValuesIn(load_contract_cases()),
|
||||
[](const testing::TestParamInfo<ContractCase>& info) {
|
||||
return info.param.id;
|
||||
});
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,338 @@
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
class TemporaryDeck final {
|
||||
public:
|
||||
TemporaryDeck(std::string_view name, std::string_view contents)
|
||||
: path_{std::filesystem::path{testing::TempDir()} / name} {
|
||||
std::ofstream output{path_, std::ios::binary};
|
||||
output.write(
|
||||
contents.data(), static_cast<std::streamsize>(contents.size()));
|
||||
if (!output) {
|
||||
throw std::runtime_error{"Failed to write temporary Abaqus deck."};
|
||||
}
|
||||
}
|
||||
|
||||
~TemporaryDeck() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path_, error);
|
||||
}
|
||||
|
||||
TemporaryDeck(const TemporaryDeck&) = delete;
|
||||
TemporaryDeck& operator=(const TemporaryDeck&) = delete;
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||
return path_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
fesa::DomainBuildResult parse_and_map(const TemporaryDeck& input) {
|
||||
auto parsed = fesa::parse_deck(input.path());
|
||||
if (!parsed.deck.has_value()) {
|
||||
return {std::nullopt, std::move(parsed.diagnostics)};
|
||||
}
|
||||
return fesa::map_deck_to_domain(*parsed.deck);
|
||||
}
|
||||
|
||||
void expect_diagnostic(
|
||||
const fesa::DomainBuildResult& result,
|
||||
const std::string_view code,
|
||||
const std::size_t line) {
|
||||
EXPECT_FALSE(result.domain.has_value());
|
||||
const auto diagnostic = std::ranges::find_if(
|
||||
result.diagnostics,
|
||||
[code, line](const fesa::Diagnostic& candidate) {
|
||||
return candidate.stage == fesa::DiagnosticStage::semantic &&
|
||||
candidate.code == code && candidate.source.has_value() &&
|
||||
candidate.source->line == line;
|
||||
});
|
||||
EXPECT_NE(diagnostic, result.diagnostics.end());
|
||||
}
|
||||
|
||||
std::string_view valid_flat_prefix() {
|
||||
return
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
|
||||
"1, 1, 2\n"
|
||||
"*ELSET, ELSET=Beam\n"
|
||||
"1\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"200.0, 0.25\n";
|
||||
}
|
||||
|
||||
std::string complete_flat_deck(std::string_view section_and_shear) {
|
||||
std::string contents{valid_flat_prefix()};
|
||||
contents.append(section_and_shear);
|
||||
contents.append(
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n");
|
||||
return contents;
|
||||
}
|
||||
|
||||
TEST(MaterialMapping, ResolvesForwardMaterialAndExplicitElsetAssignment) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-forward-material.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"2, 1.0, 0.0, 0.0\n"
|
||||
"*ELEMENT, TYPE=B31\n"
|
||||
"1, 1, 2\n"
|
||||
"*ELSET, ELSET=Beam\n"
|
||||
"1\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=Assembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"200.0, 0.25\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
ASSERT_TRUE(result.domain.has_value());
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
ASSERT_EQ(result.domain->materials().size(), 1U);
|
||||
EXPECT_DOUBLE_EQ(result.domain->materials()[0].young, 200.0);
|
||||
EXPECT_DOUBLE_EQ(result.domain->materials()[0].poisson, 0.25);
|
||||
ASSERT_EQ(result.domain->sections().size(), 1U);
|
||||
EXPECT_EQ(
|
||||
result.domain->beam_elements()[0].material,
|
||||
result.domain->materials()[0].id);
|
||||
EXPECT_EQ(
|
||||
result.domain->beam_elements()[0].section,
|
||||
result.domain->sections()[0].id);
|
||||
}
|
||||
|
||||
TEST(ShearDefault, PreservesPhase1DefaultEffectiveAreasAndSource) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-default-shear.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
ASSERT_TRUE(result.domain.has_value());
|
||||
ASSERT_EQ(result.domain->sections().size(), 1U);
|
||||
const fesa::BeamSection& section = result.domain->sections()[0];
|
||||
EXPECT_DOUBLE_EQ(section.shear_area_y, 1.0);
|
||||
EXPECT_DOUBLE_EQ(section.shear_area_z, 1.0);
|
||||
EXPECT_EQ(
|
||||
section.shear_source,
|
||||
fesa::ShearPropertySource::phase1_default);
|
||||
}
|
||||
|
||||
TEST(ShearDefault, ConvertsExplicitK23AndK13UsingIsotropicShearModulus) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-explicit-shear.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*TRANSVERSE SHEAR STIFFNESS\n"
|
||||
"64.0, 40.0, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
ASSERT_TRUE(result.domain.has_value());
|
||||
ASSERT_EQ(result.domain->sections().size(), 1U);
|
||||
const fesa::BeamSection& section = result.domain->sections()[0];
|
||||
EXPECT_DOUBLE_EQ(section.shear_area_y, 0.8);
|
||||
EXPECT_DOUBLE_EQ(section.shear_area_z, 0.5);
|
||||
EXPECT_EQ(section.shear_source, fesa::ShearPropertySource::input);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsInvalidElasticPropertiesAtTheDataRow) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-invalid-elastic.inp",
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"-1.0, 0.25\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(
|
||||
result, "abaqus.semantic.invalid_elastic_data", 3U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsMaterialWithoutElasticData) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-elastic.inp",
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(result, "abaqus.semantic.missing_elastic", 1U);
|
||||
}
|
||||
|
||||
TEST(MaterialMapping, RejectsDuplicateMaterialDefinition) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-duplicate-material.inp",
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"200.0, 0.25\n"
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"*ELASTIC\n"
|
||||
"210.0, 0.3\n"
|
||||
"*STEP\n"
|
||||
"*STATIC\n"
|
||||
"*END STEP\n"};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(
|
||||
result, "abaqus.semantic.duplicate_material", 4U);
|
||||
}
|
||||
|
||||
TEST(MaterialMapping, RejectsSectionWithMissingMaterialReference) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-material.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Missing\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(result, "abaqus.semantic.missing_material", 11U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsNonzeroProductMomentAtTheDataRow) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-nonzero-iyz.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.1, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(
|
||||
result, "abaqus.semantic.invalid_section_data", 12U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsNonpositiveGeneralPropertyAtTheDataRow) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-invalid-area.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"0.0, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(
|
||||
result, "abaqus.semantic.invalid_section_data", 12U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsNonfiniteOrientationAtTheOrientationRow) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-nonfinite-orientation.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, nan, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(
|
||||
result, "abaqus.semantic.invalid_section_data", 13U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsSectionWhoseElementSetDoesNotExist) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-section-elset.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Missing, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(
|
||||
result, "abaqus.semantic.missing_element_set", 11U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsNonzeroScfAtTheDataRow) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-nonzero-scf.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*TRANSVERSE SHEAR STIFFNESS\n"
|
||||
"64.0, 40.0, 0.25\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(result, "abaqus.semantic.nonzero_scf", 15U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsElementWithoutSectionAssignment) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-missing-section.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Other, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*ELSET, ELSET=Other\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(result, "abaqus.semantic.missing_section", 5U);
|
||||
}
|
||||
|
||||
TEST(BeamSection, RejectsDuplicateSectionAssignment) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-duplicate-section.inp",
|
||||
complete_flat_deck(
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
|
||||
"1.2, 2.0, 0.0, 3.0, 4.0\n"
|
||||
"0.0, 1.0, 0.0\n")};
|
||||
|
||||
const auto result = parse_and_map(input);
|
||||
|
||||
expect_diagnostic(
|
||||
result, "abaqus.semantic.duplicate_section_assignment", 14U);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -74,7 +74,7 @@ TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
||||
EXPECT_EQ(nodes.data[0], (std::vector<std::string>{
|
||||
"1", "0.0", "0.0", "0.0"}));
|
||||
EXPECT_EQ(nodes.data[1], (std::vector<std::string>{
|
||||
"2", "1.0", "0.0", "0.0", ""}));
|
||||
"2", "1.0", "0.0", "0.0"}));
|
||||
EXPECT_EQ(nodes.source.file, path);
|
||||
EXPECT_EQ(nodes.source.line, 3U);
|
||||
EXPECT_EQ(nodes.source.column, 1U);
|
||||
@@ -121,6 +121,214 @@ TEST(AbaqusParser, RejectsUnsupportedKeywordsInsteadOfIgnoringThem) {
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
||||
}
|
||||
|
||||
TEST(AbaqusParser, RejectsUnsupportedParametersOnSupportedKeywords) {
|
||||
struct Case final {
|
||||
std::string_view name;
|
||||
std::string_view contents;
|
||||
std::size_t line;
|
||||
};
|
||||
const Case cases[]{
|
||||
{"node", "*NODE, EXTRA=1\n1, 0.0, 0.0, 0.0\n", 1U},
|
||||
{"element", "*ELEMENT, TYPE=B31, EXTRA=1\n1, 1, 2\n", 1U},
|
||||
{"part", "*PART, NAME=P, EXTRA=1\n*END PART\n", 1U},
|
||||
{"end-part", "*PART, NAME=P\n*END PART, EXTRA=1\n", 2U},
|
||||
{
|
||||
"assembly",
|
||||
"*ASSEMBLY, NAME=A, EXTRA=1\n*END ASSEMBLY\n",
|
||||
1U,
|
||||
},
|
||||
{
|
||||
"end-assembly",
|
||||
"*ASSEMBLY, NAME=A\n*END ASSEMBLY, EXTRA=1\n",
|
||||
2U,
|
||||
},
|
||||
{
|
||||
"instance",
|
||||
"*ASSEMBLY, NAME=A\n"
|
||||
"*INSTANCE, NAME=I, PART=P, EXTRA=1\n"
|
||||
"*END INSTANCE\n"
|
||||
"*END ASSEMBLY\n",
|
||||
2U,
|
||||
},
|
||||
{
|
||||
"end-instance",
|
||||
"*ASSEMBLY, NAME=A\n"
|
||||
"*INSTANCE, NAME=I, PART=P\n"
|
||||
"*END INSTANCE, EXTRA=1\n"
|
||||
"*END ASSEMBLY\n",
|
||||
3U,
|
||||
},
|
||||
{"nset", "*NSET, NSET=S, EXTRA=1\n1\n", 1U},
|
||||
{"elset", "*ELSET, ELSET=S, EXTRA=1\n1\n", 1U},
|
||||
{"material", "*MATERIAL, NAME=M, EXTRA=1\n", 1U},
|
||||
{
|
||||
"elastic",
|
||||
"*MATERIAL, NAME=M\n*ELASTIC, EXTRA=1\n1.0, 0.3\n",
|
||||
2U,
|
||||
},
|
||||
{
|
||||
"beam-general-section",
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, "
|
||||
"MATERIAL=M, EXTRA=1\n"
|
||||
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
||||
"0.0, 1.0, 0.0\n",
|
||||
1U,
|
||||
},
|
||||
{
|
||||
"transverse-shear-stiffness",
|
||||
"*TRANSVERSE SHEAR STIFFNESS, EXTRA=1\n1.0, 1.0, 0.0\n",
|
||||
1U,
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto& test_case : cases) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-parser-unsupported-parameter-" +
|
||||
std::string{test_case.name} + ".inp",
|
||||
test_case.contents,
|
||||
};
|
||||
|
||||
const auto result = fesa::parse_deck(input.path());
|
||||
|
||||
SCOPED_TRACE(test_case.name);
|
||||
EXPECT_FALSE(result.deck.has_value());
|
||||
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||
EXPECT_EQ(
|
||||
result.diagnostics[0].code,
|
||||
"abaqus.syntax.unsupported_parameter");
|
||||
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AbaqusParser, RejectsInvalidParameterFormsAndSetScopeOptions) {
|
||||
struct Case final {
|
||||
std::string_view name;
|
||||
std::string_view contents;
|
||||
std::string_view code;
|
||||
};
|
||||
const Case cases[]{
|
||||
{
|
||||
"generate-valued",
|
||||
"*NSET, NSET=S, GENERATE=YES\n1, 2, 1\n",
|
||||
"abaqus.syntax.unsupported_parameter",
|
||||
},
|
||||
{
|
||||
"generate-empty-valued",
|
||||
"*NSET, NSET=S, GENERATE=\n1, 2, 1\n",
|
||||
"abaqus.syntax.unsupported_parameter",
|
||||
},
|
||||
{
|
||||
"empty-element-set",
|
||||
"*ELEMENT, TYPE=B31, ELSET=\n1, 1, 2\n",
|
||||
"abaqus.syntax.invalid_parameter",
|
||||
},
|
||||
{
|
||||
"instance-on-flat-set",
|
||||
"*NSET, NSET=S, INSTANCE=I\n1\n",
|
||||
"abaqus.syntax.unsupported_parameter",
|
||||
},
|
||||
{
|
||||
"instance-on-part-set",
|
||||
"*PART, NAME=P\n"
|
||||
"*NSET, NSET=S, INSTANCE=I\n"
|
||||
"1\n"
|
||||
"*END PART\n",
|
||||
"abaqus.syntax.unsupported_parameter",
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto& test_case : cases) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-parser-parameter-form-" + std::string{test_case.name} +
|
||||
".inp",
|
||||
test_case.contents,
|
||||
};
|
||||
|
||||
const auto result = fesa::parse_deck(input.path());
|
||||
|
||||
SCOPED_TRACE(test_case.name);
|
||||
EXPECT_FALSE(result.deck.has_value());
|
||||
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
|
||||
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 1U +
|
||||
(test_case.name == "instance-on-part-set" ? 1U : 0U));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AbaqusParser, RejectsModelKeywordsOutsideTheirDocumentedScopes) {
|
||||
struct Case final {
|
||||
std::string_view name;
|
||||
std::string_view contents;
|
||||
std::string_view code;
|
||||
std::size_t line;
|
||||
};
|
||||
const Case cases[]{
|
||||
{
|
||||
"node-in-assembly",
|
||||
"*ASSEMBLY, NAME=A\n"
|
||||
"*NODE\n"
|
||||
"1, 0.0, 0.0, 0.0\n"
|
||||
"*END ASSEMBLY\n",
|
||||
"abaqus.syntax.invalid_node_scope",
|
||||
2U,
|
||||
},
|
||||
{
|
||||
"material-in-part",
|
||||
"*PART, NAME=P\n"
|
||||
"*MATERIAL, NAME=M\n"
|
||||
"*END PART\n",
|
||||
"abaqus.syntax.invalid_material_scope",
|
||||
2U,
|
||||
},
|
||||
{
|
||||
"section-in-assembly",
|
||||
"*ASSEMBLY, NAME=A\n"
|
||||
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, MATERIAL=M\n"
|
||||
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
||||
"0.0, 1.0, 0.0\n"
|
||||
"*END ASSEMBLY\n",
|
||||
"abaqus.syntax.invalid_section_scope",
|
||||
2U,
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto& test_case : cases) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-parser-invalid-scope-" + std::string{test_case.name} +
|
||||
".inp",
|
||||
test_case.contents,
|
||||
};
|
||||
|
||||
const auto result = fesa::parse_deck(input.path());
|
||||
|
||||
SCOPED_TRACE(test_case.name);
|
||||
EXPECT_FALSE(result.deck.has_value());
|
||||
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
|
||||
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AbaqusParser, RejectsDataRowsOnMaterial) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-parser-material-data.inp",
|
||||
"*MATERIAL, NAME=Steel\n"
|
||||
"unexpected data\n"};
|
||||
|
||||
const auto result = fesa::parse_deck(input.path());
|
||||
|
||||
EXPECT_FALSE(result.deck.has_value());
|
||||
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||
EXPECT_EQ(
|
||||
result.diagnostics[0].code,
|
||||
"abaqus.syntax.data_without_keyword");
|
||||
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
||||
}
|
||||
|
||||
TEST(ScopedDeck, PreservesPartAssemblyAndInstanceScopes) {
|
||||
const auto path =
|
||||
fixture_path("minimal_part_instance_cantilever.inp");
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
#include <fesa/io/abaqus/set_resolver.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
class TemporaryDeck final {
|
||||
public:
|
||||
TemporaryDeck(std::string_view name, std::string_view contents)
|
||||
: path_{std::filesystem::path{testing::TempDir()} / name} {
|
||||
std::ofstream output{path_, std::ios::binary};
|
||||
output.write(
|
||||
contents.data(), static_cast<std::streamsize>(contents.size()));
|
||||
if (!output) {
|
||||
throw std::runtime_error{"Failed to write temporary Abaqus deck."};
|
||||
}
|
||||
}
|
||||
|
||||
~TemporaryDeck() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path_, error);
|
||||
}
|
||||
|
||||
TemporaryDeck(const TemporaryDeck&) = delete;
|
||||
TemporaryDeck& operator=(const TemporaryDeck&) = delete;
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||
return path_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
fesa::SetResolutionResult parse_and_resolve(
|
||||
const TemporaryDeck& input) {
|
||||
const fesa::ParseDeckResult parsed = fesa::parse_deck(input.path());
|
||||
if (!parsed.deck.has_value()) {
|
||||
throw std::runtime_error{"Test deck did not parse."};
|
||||
}
|
||||
return fesa::resolve_sets(*parsed.deck);
|
||||
}
|
||||
|
||||
const fesa::ResolvedSet& find_set(
|
||||
const fesa::SetResolutionResult& result,
|
||||
const fesa::ResolvedSetScope scope,
|
||||
const std::string_view scope_name,
|
||||
const fesa::ResolvedSetKind kind,
|
||||
const std::string_view set_name) {
|
||||
const auto found = std::ranges::find_if(
|
||||
result.sets,
|
||||
[=](const fesa::ResolvedSet& set) {
|
||||
return set.scope == scope && set.scope_name == scope_name &&
|
||||
set.kind == kind && set.set_name == set_name;
|
||||
});
|
||||
if (found == result.sets.end()) {
|
||||
throw std::runtime_error{"Expected resolved set was not found."};
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
const fesa::Diagnostic& find_diagnostic(
|
||||
const fesa::SetResolutionResult& result,
|
||||
const std::string_view code,
|
||||
const std::size_t line) {
|
||||
const auto found = std::ranges::find_if(
|
||||
result.diagnostics,
|
||||
[=](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.code == code && diagnostic.source.has_value() &&
|
||||
diagnostic.source->line == line;
|
||||
});
|
||||
if (found == result.diagnostics.end()) {
|
||||
throw std::runtime_error{"Expected set diagnostic was not found."};
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
TEST(
|
||||
SetResolution,
|
||||
CanonicalizesExplicitGenerateNestedForwardDuplicateAndEmptySets) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-valid.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"3, 2, 0, 0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=ImplicitElements\n"
|
||||
"10, 1, 2\n"
|
||||
"20, 2, 3\n"
|
||||
"*NSET, NSET=AllNodes\n"
|
||||
"GeneratedNodes, 3, 1, 3\n"
|
||||
"*NSET, NSET=GeneratedNodes, GENERATE\n"
|
||||
"1, 3, 1\n"
|
||||
"*NSET, NSET=EmptyNodes\n"
|
||||
"*ELSET, ELSET=AllElements\n"
|
||||
"GeneratedElements, ImplicitElements, 20\n"
|
||||
"*ELSET, ELSET=GeneratedElements, GENERATE\n"
|
||||
"10, 20, 10\n"
|
||||
"*END PART\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"AllNodes")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1, 2, 3}));
|
||||
EXPECT_TRUE(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"EmptyNodes")
|
||||
.sorted_unique_labels.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::element,
|
||||
"AllElements")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{10, 20}));
|
||||
}
|
||||
|
||||
TEST(PartSet, KeepsNodeAndElementSetNamespacesSeparate) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-part-set-kind-collision.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"*ELEMENT, TYPE=B31\n"
|
||||
"1, 1, 2\n"
|
||||
"*NSET, NSET=Shared\n"
|
||||
"1\n"
|
||||
"*ELSET, ELSET=Shared\n"
|
||||
"1\n"
|
||||
"*END PART\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::element,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
}
|
||||
|
||||
TEST(AssemblySet, KeepsPartScopeSeparateAndLiftsActivePartLabels) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-assembly-set-scope-collision.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"*NSET, NSET=Shared\n"
|
||||
"1\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*NSET, NSET=Shared, INSTANCE=Beam-1\n"
|
||||
"2, 2\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
ASSERT_EQ(result.sets.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::assembly,
|
||||
"RootAssembly",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{2}));
|
||||
EXPECT_TRUE(std::ranges::is_sorted(
|
||||
result.sets,
|
||||
[](const fesa::ResolvedSet& left, const fesa::ResolvedSet& right) {
|
||||
return std::tuple{
|
||||
left.scope,
|
||||
left.scope_name,
|
||||
left.kind,
|
||||
left.set_name} <
|
||||
std::tuple{
|
||||
right.scope,
|
||||
right.scope_name,
|
||||
right.kind,
|
||||
right.set_name};
|
||||
}));
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsCycleAtTheClosingReferenceSource) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-cycle.inp",
|
||||
"*NSET, NSET=First\n"
|
||||
"Second\n"
|
||||
"*NSET, NSET=Second\n"
|
||||
"First\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
const fesa::Diagnostic& diagnostic =
|
||||
find_diagnostic(result, "abaqus.semantic.set_cycle", 4U);
|
||||
EXPECT_EQ(diagnostic.stage, fesa::DiagnosticStage::semantic);
|
||||
EXPECT_EQ(diagnostic.source->file, input.path());
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsUnknownSetAndEntityAtTheirMemberRows) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-missing.inp",
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"*NSET, NSET=MissingEntity\n"
|
||||
"2\n"
|
||||
"*NSET, NSET=MissingSet\n"
|
||||
"Unknown\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
EXPECT_EQ(result.diagnostics.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(
|
||||
result, "abaqus.semantic.missing_set_member", 4U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(
|
||||
result, "abaqus.semantic.missing_set_member", 6U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsInvalidGenerateRangesInInputOrder) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-generate-invalid.inp",
|
||||
"*NSET, NSET=Reversed, GENERATE\n"
|
||||
"3, 1, 1\n"
|
||||
"*NSET, NSET=NotDivisible, GENERATE\n"
|
||||
"1, 4, 2\n"
|
||||
"*ELSET, ELSET=WrongFieldCount, GENERATE\n"
|
||||
"1, 2\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
ASSERT_EQ(result.diagnostics.size(), 3U);
|
||||
for (std::size_t index = 0; index < result.diagnostics.size(); ++index) {
|
||||
EXPECT_EQ(
|
||||
result.diagnostics[index].code,
|
||||
"abaqus.semantic.invalid_generate");
|
||||
ASSERT_TRUE(result.diagnostics[index].source.has_value());
|
||||
}
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
||||
EXPECT_EQ(result.diagnostics[1].source->line, 4U);
|
||||
EXPECT_EQ(result.diagnostics[2].source->line, 6U);
|
||||
}
|
||||
|
||||
TEST(AssemblySet, RejectsAnInstanceOtherThanTheSingleActiveInstance) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-assembly-set-wrong-instance.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*NSET, NSET=Fixed, INSTANCE=Other\n"
|
||||
"1\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(result, "abaqus.semantic.wrong_instance", 8U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user