Compare commits
8 Commits
5618636f0d
...
51939fba5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 51939fba5b | |||
| f23deb0ade | |||
| fb67007527 | |||
| 4d04f3dbbe | |||
| 9d37465d92 | |||
| e0a6a6fa70 | |||
| 218cfa9d50 | |||
| 5fe57cb145 |
+154
-76
@@ -1,90 +1,168 @@
|
|||||||
# FESA HDF5 Schema 1.0.0
|
# FESA HDF5 Schema 2.0.0
|
||||||
|
|
||||||
## 1. 범위
|
## 1. Scope and version compatibility
|
||||||
|
|
||||||
Schema `1.0.0`은 `results-and-pipeline` Phase의 최소 수직 슬라이스를 정의한다.
|
Schema `2.0.0` is the self-contained Phase 1 result contract. One file contains
|
||||||
파일은 활성 `Domain`의 절점, Beam 연결성, 적용된 전단면적과 그 출처, 그리고 전역
|
the active normalized model, its single linear-static analysis definition and
|
||||||
좌표계 절점 변위·회전 및 반력·반력모멘트를 저장한다. 단위 변환은 수행하지 않는다.
|
solver settings, and every nodal and Beam result needed without the source
|
||||||
|
`.inp` file. FESA performs no unit conversion.
|
||||||
|
|
||||||
이 버전에는 재료 전체 속성, 집합, 하중·경계조건, solver 설정, 요소 결과, history,
|
Schema `1.0.0` was the earlier minimal vertical slice. Version `2.0.0` changes
|
||||||
reference CSV 및 진단 dataset을 저장하지 않는다. 이후 같은 major version에서
|
the required model and result objects, so it is a new major version rather than
|
||||||
dataset을 추가할 수 있지만 아래 required object의 의미, 형상 또는 datatype을
|
an in-place change to `1.0.0`. The current writer and reader accept exactly
|
||||||
변경해서는 안 된다.
|
`2.0.0`; every other version fails with `hdf5.unsupported_schema`. A future
|
||||||
|
reader may explicitly add support for compatible minor versions, but must not
|
||||||
|
infer compatibility from a version prefix.
|
||||||
|
|
||||||
## 2. 공통 규칙
|
## 2. Common rules
|
||||||
|
|
||||||
- root attribute `schema_version`은 UTF-8 문자열 `1.0.0`이다.
|
- Root attributes are variable-length UTF-8 strings:
|
||||||
- 정수 dataset은 명시한 little-endian 고정폭 타입을 사용한다.
|
`schema_version="2.0.0"`, `fesa_version`, and
|
||||||
- 실수 dataset은 IEEE 754 little-endian 64-bit 타입을 사용한다.
|
`unit_policy="consistent_input_units_no_conversion"`, `input_source`, and
|
||||||
- 문자열 dataset과 attribute는 UTF-8 variable-length string을 사용한다.
|
`input_fingerprint`. `input_source` is the UTF-8 path supplied to the solve
|
||||||
- `dense_index`는 해당 dataset 행의 0-based index이며 연속적이다.
|
request. `input_fingerprint` is `fnv1a64:` followed by the 16 lowercase
|
||||||
- `internal_id`와 결과의 `node_ids`는 FESA semantic model의 nonnegative ID다.
|
hexadecimal digits of FNV-1a 64 over the original input bytes; it is a
|
||||||
- flat/orphan mesh의 `part_name`과 `instance_name`은 빈 문자열이다.
|
reproducibility identifier, not a cryptographic integrity guarantee.
|
||||||
- 결과의 6개 component 순서는
|
- Integer datasets use the stated little-endian fixed-width type. Floating
|
||||||
`(Ux, Uy, Uz, Rx, Ry, Rz)` 및 `(RFx, RFy, RFz, RMx, RMy, RMz)`다.
|
datasets use IEEE 754 little-endian `float64`. Strings are variable-length
|
||||||
- Step과 frame group 이름은 각각 0부터 연속된 decimal index다. 원래 Step 이름은
|
UTF-8.
|
||||||
Step group의 `name` attribute에 저장한다.
|
- `dense_index` is a contiguous 0-based row index. Semantic `internal_id`
|
||||||
|
values are nonnegative and are not assumed to be dense or ordered.
|
||||||
|
- Flat/orphan mesh `part_name` and `instance_name` values are empty strings.
|
||||||
|
- Ragged arrays use an offset dataset of length `row_count + 1`. Offsets start
|
||||||
|
at zero, are nondecreasing, and the final offset equals the flattened row
|
||||||
|
count. Input order is preserved.
|
||||||
|
- Numeric field datasets carry UTF-8 `coordinate_system` and `components`
|
||||||
|
attributes where listed. `components` is a comma-separated ordered list.
|
||||||
|
- Step and frame group names are contiguous decimal indices beginning at zero.
|
||||||
|
Phase 1 requires exactly one analysis step and one result step with the same
|
||||||
|
name, and exactly one result frame in that step.
|
||||||
|
|
||||||
## 3. Required objects
|
## 3. Required objects
|
||||||
|
|
||||||
```text
|
### 3.1 Model
|
||||||
/
|
|
||||||
├── @schema_version UTF-8 = "1.0.0"
|
|
||||||
├── model
|
|
||||||
│ ├── nodes
|
|
||||||
│ │ ├── dense_index uint64 [node_count]
|
|
||||||
│ │ ├── internal_id int64 [node_count]
|
|
||||||
│ │ ├── part_name UTF-8 [node_count]
|
|
||||||
│ │ ├── instance_name UTF-8 [node_count]
|
|
||||||
│ │ ├── local_label int64 [node_count]
|
|
||||||
│ │ └── coordinates float64[node_count, 3]
|
|
||||||
│ ├── elements
|
|
||||||
│ │ ├── dense_index uint64 [element_count]
|
|
||||||
│ │ ├── internal_id int64 [element_count]
|
|
||||||
│ │ ├── connectivity uint64 [element_count, 2]
|
|
||||||
│ │ └── section_id int64 [element_count]
|
|
||||||
│ └── sections
|
|
||||||
│ ├── internal_id int64 [section_count]
|
|
||||||
│ ├── shear_area_y float64[section_count]
|
|
||||||
│ ├── shear_area_z float64[section_count]
|
|
||||||
│ └── shear_source uint8 [section_count]
|
|
||||||
└── results
|
|
||||||
└── steps
|
|
||||||
└── <step_index>
|
|
||||||
├── @name UTF-8
|
|
||||||
└── frames
|
|
||||||
└── <frame_index>
|
|
||||||
├── @step_time float64
|
|
||||||
└── nodal
|
|
||||||
├── node_ids int64 [result_node_count]
|
|
||||||
├── displacement float64[result_node_count, 6]
|
|
||||||
└── reaction float64[result_node_count, 6]
|
|
||||||
```
|
|
||||||
|
|
||||||
`model/elements/connectivity`는 `model/nodes/dense_index`를 참조한다. 따라서
|
Let `N`, `E`, `M`, `S`, `NS`, and `ES` be the node, Beam element, material,
|
||||||
`internal_id`가 연속적이거나 Domain 저장 순서와 같다고 가정하지 않는다.
|
section, node-set, and element-set counts. Let `P` be the total number of
|
||||||
`section_id`는 `model/sections/internal_id`를 참조한다.
|
section recovery points, `NM` the total node-set membership count, and `EM` the
|
||||||
|
total element-set membership count.
|
||||||
|
|
||||||
`shear_source` 값은 다음과 같다.
|
| Path | Type | Rank and shape | Attributes / meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/model/nodes/dense_index` | `uint64` | 1, `[N]` | contiguous row index |
|
||||||
|
| `/model/nodes/internal_id` | `int64` | 1, `[N]` | `NodeId` |
|
||||||
|
| `/model/nodes/part_name` | UTF-8 | 1, `[N]` | entity provenance |
|
||||||
|
| `/model/nodes/instance_name` | UTF-8 | 1, `[N]` | entity provenance |
|
||||||
|
| `/model/nodes/local_label` | `int64` | 1, `[N]` | external local label |
|
||||||
|
| `/model/nodes/coordinates` | `float64` | 2, `[N,3]` | `coordinate_system="global"`, `components="X,Y,Z"` |
|
||||||
|
| `/model/elements/dense_index` | `uint64` | 1, `[E]` | contiguous row index |
|
||||||
|
| `/model/elements/internal_id` | `int64` | 1, `[E]` | `ElementId` |
|
||||||
|
| `/model/elements/part_name` | UTF-8 | 1, `[E]` | entity provenance |
|
||||||
|
| `/model/elements/instance_name` | UTF-8 | 1, `[E]` | entity provenance |
|
||||||
|
| `/model/elements/local_label` | `int64` | 1, `[E]` | external local label |
|
||||||
|
| `/model/elements/connectivity` | `uint64` | 2, `[E,2]` | node `dense_index`, ordered end `-1,+1` |
|
||||||
|
| `/model/elements/material_id` | `int64` | 1, `[E]` | references material `internal_id` |
|
||||||
|
| `/model/elements/section_id` | `int64` | 1, `[E]` | references section `internal_id` |
|
||||||
|
| `/model/materials/internal_id` | `int64` | 1, `[M]` | `MaterialId` |
|
||||||
|
| `/model/materials/name` | UTF-8 | 1, `[M]` | material name |
|
||||||
|
| `/model/materials/young_modulus` | `float64` | 1, `[M]` | finite, positive |
|
||||||
|
| `/model/materials/poisson_ratio` | `float64` | 1, `[M]` | finite, `-1 < nu < 0.5` |
|
||||||
|
| `/model/sections/internal_id` | `int64` | 1, `[S]` | `SectionId` |
|
||||||
|
| `/model/sections/name` | UTF-8 | 1, `[S]` | section name |
|
||||||
|
| `/model/sections/area` | `float64` | 1, `[S]` | `A` |
|
||||||
|
| `/model/sections/moment_y` | `float64` | 1, `[S]` | `Iy` |
|
||||||
|
| `/model/sections/moment_z` | `float64` | 1, `[S]` | `Iz` |
|
||||||
|
| `/model/sections/torsion_constant` | `float64` | 1, `[S]` | `J` |
|
||||||
|
| `/model/sections/shear_area_y` | `float64` | 1, `[S]` | applied `Asy` |
|
||||||
|
| `/model/sections/shear_area_z` | `float64` | 1, `[S]` | applied `Asz` |
|
||||||
|
| `/model/sections/shear_source` | `uint8` | 1, `[S]` | `0=input`, `1=phase1_default` |
|
||||||
|
| `/model/sections/orientation` | `float64` | 2, `[S,3]` | `coordinate_system="global"`, `components="X,Y,Z"` |
|
||||||
|
| `/model/sections/recovery_point_offsets` | `uint64` | 1, `[S+1]` | offsets into `recovery_points` |
|
||||||
|
| `/model/sections/recovery_points` | `float64` | 2, `[P,2]` | `coordinate_system="element_local"`, `components="y,z"` |
|
||||||
|
| `/model/sets/node/names` | UTF-8 | 1, `[NS]` | exact node-set names |
|
||||||
|
| `/model/sets/node/member_offsets` | `uint64` | 1, `[NS+1]` | offsets into `members` |
|
||||||
|
| `/model/sets/node/members` | `int64` | 1, `[NM]` | `NodeId`, input set/member order |
|
||||||
|
| `/model/sets/element/names` | UTF-8 | 1, `[ES]` | exact element-set names |
|
||||||
|
| `/model/sets/element/member_offsets` | `uint64` | 1, `[ES+1]` | offsets into `members` |
|
||||||
|
| `/model/sets/element/members` | `int64` | 1, `[EM]` | `ElementId`, input set/member order |
|
||||||
|
|
||||||
| 값 | 의미 |
|
### 3.2 Analysis
|
||||||
|
|
||||||
|
`/analysis/steps/0` has UTF-8 attribute `name`. Let `B` be the prescribed-DOF
|
||||||
|
count and `L` the nodal-load count.
|
||||||
|
|
||||||
|
| Path | Type | Rank and shape | Attributes / meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/analysis/steps/0/boundary_conditions/node_ids` | `int64` | 1, `[B]` | target `NodeId` |
|
||||||
|
| `/analysis/steps/0/boundary_conditions/dofs` | `uint8` | 1, `[B]` | Abaqus/FESA DOF number 1 through 6 |
|
||||||
|
| `/analysis/steps/0/boundary_conditions/values` | `float64` | 1, `[B]` | prescribed value |
|
||||||
|
| `/analysis/steps/0/nodal_loads/node_ids` | `int64` | 1, `[L]` | target `NodeId` |
|
||||||
|
| `/analysis/steps/0/nodal_loads/values` | `float64` | 2, `[L,6]` | `coordinate_system="global"`, `components="Fx,Fy,Fz,Mx,My,Mz"` |
|
||||||
|
|
||||||
|
`/analysis/solver_settings` has the exact UTF-8 attributes used by the Phase 1
|
||||||
|
pipeline: `backend="mkl_pardiso"`,
|
||||||
|
`matrix_storage="symmetric_upper_csr"`,
|
||||||
|
`matrix_type="symmetric_positive_definite"`,
|
||||||
|
`constraint_method="essential_dof_elimination"`, and
|
||||||
|
`assembly="deterministic_serial"`. No unused future settings are stored.
|
||||||
|
|
||||||
|
### 3.3 Results
|
||||||
|
|
||||||
|
`/results/steps/0` has UTF-8 attribute `name`; frame group `0` has scalar
|
||||||
|
`float64` attribute `step_time`. Let `RN`, `RE`, `RP`, and `D` be the nodal
|
||||||
|
result, Beam result, flattened Beam recovery-point, and diagnostic counts.
|
||||||
|
|
||||||
|
| Path below `/results/steps/0/frames/0` | Type | Rank and shape | Attributes / meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `nodal/node_ids` | `int64` | 1, `[RN]` | `NodeId`; provenance is joined from `/model/nodes` |
|
||||||
|
| `nodal/displacement` | `float64` | 2, `[RN,6]` | `coordinate_system="global"`, `components="Ux,Uy,Uz,Rx,Ry,Rz"` |
|
||||||
|
| `nodal/reaction` | `float64` | 2, `[RN,6]` | `coordinate_system="global"`, `components="RFx,RFy,RFz,RMx,RMy,RMz"` |
|
||||||
|
| `element/beam/element_ids` | `int64` | 1, `[RE]` | `ElementId` |
|
||||||
|
| `element/beam/part_name` | UTF-8 | 1, `[RE]` | result provenance |
|
||||||
|
| `element/beam/instance_name` | UTF-8 | 1, `[RE]` | result provenance |
|
||||||
|
| `element/beam/local_label` | `int64` | 1, `[RE]` | result provenance |
|
||||||
|
| `element/beam/local_frame` | `float64` | 3, `[RE,3,3]` | `coordinate_system="global"`, `components="ex,ey,ez"`; last dimension is `X,Y,Z` |
|
||||||
|
| `element/beam/end_node_ids` | `int64` | 2, `[RE,2]` | ordered ends `-1,+1` |
|
||||||
|
| `element/beam/xi` | `float64` | 2, `[RE,2]` | `components="end_minus,end_plus"` |
|
||||||
|
| `element/beam/section_strain` | `float64` | 3, `[RE,2,6]` | `coordinate_system="element_local"`, `components="epsilon,gamma_y,gamma_z,kappa_x,kappa_y,kappa_z"` |
|
||||||
|
| `element/beam/section_force` | `float64` | 3, `[RE,2,6]` | `coordinate_system="element_local"`, `components="N,Vy,Vz,T,My,Mz"` |
|
||||||
|
| `element/beam/centroid_sigma_xx` | `float64` | 2, `[RE,2]` | `coordinate_system="element_local"`, `components="end_minus,end_plus"`, `quantity="sigma_xx"` |
|
||||||
|
| `element/beam/recovery_point_offsets` | `uint64` | 1, `[RE+1]` | offsets into recovery-point rows |
|
||||||
|
| `element/beam/recovery_point_sigma_xx` | `float64` | 2, `[RP,2]` | `coordinate_system="element_local"`, `components="end_minus,end_plus"`, `quantity="sigma_xx"`; point order comes from the referenced section |
|
||||||
|
| `diagnostics/stage` | `uint8` | 1, `[D]` | enum table below |
|
||||||
|
| `diagnostics/severity` | `uint8` | 1, `[D]` | `0=warning`, `1=error` |
|
||||||
|
| `diagnostics/code` | UTF-8 | 1, `[D]` | exact diagnostic code |
|
||||||
|
| `diagnostics/message` | UTF-8 | 1, `[D]` | exact diagnostic message |
|
||||||
|
| `diagnostics/has_source` | `uint8` | 1, `[D]` | `0=no source`, `1=source present` |
|
||||||
|
| `diagnostics/source_file` | UTF-8 | 1, `[D]` | empty when source is absent |
|
||||||
|
| `diagnostics/source_line` | `uint64` | 1, `[D]` | zero when source is absent |
|
||||||
|
| `diagnostics/source_column` | `uint64` | 1, `[D]` | zero when source is absent |
|
||||||
|
|
||||||
|
Diagnostic stage encoding follows the declaration order:
|
||||||
|
|
||||||
|
| Value | Stage |
|
||||||
|---:|---|
|
|---:|---|
|
||||||
| `0` | 입력에서 명시된 전단강성으로부터 구성한 값 (`input`) |
|
| 0 | `io` |
|
||||||
| `1` | Phase 1 기본값 `Asy=Asz=5A/6`, `SCF=0` (`phase1_default`) |
|
| 1 | `syntax` |
|
||||||
|
| 2 | `semantic` |
|
||||||
|
| 3 | `model` |
|
||||||
|
| 4 | `equation` |
|
||||||
|
| 5 | `solver` |
|
||||||
|
| 6 | `results` |
|
||||||
|
| 7 | `validation` |
|
||||||
|
|
||||||
## 4. Writer와 reader 계약
|
## 4. Writer and reader contract
|
||||||
|
|
||||||
- writer는 쓰기 전에 `ResultDatabase` 유효성과 schema version을 검사한다.
|
- The writer validates `ResultDatabase`, exact schema version, model/result ID
|
||||||
- schema `1.0.0`이 표현하지 않는 non-empty frame diagnostics는 파일을 만들기 전에
|
and provenance joins, Beam connectivity, recovery-point counts, and the
|
||||||
`hdf5.unsupported_result_diagnostics`로 거부한다.
|
single-step name before creating the file.
|
||||||
- required object 생성·쓰기·flush·close 중 HDF5 오류가 발생하면 성공으로 반환하지
|
- Required-object creation, write, flush, and close failures become
|
||||||
않고 `DiagnosticStage::results` 오류로 변환한다.
|
`DiagnosticStage::results` errors. A failed write is never reported as
|
||||||
- reader는 schema version, required object, datatype, rank와 shape를 검사한다.
|
success.
|
||||||
- reader는 model internal ID의 uniqueness, finite coordinates 및 finite positive
|
- The reader validates the exact version, required datatypes, ranks, shapes,
|
||||||
shear area를 검사한다.
|
offsets, finite values, uniqueness, references, field metadata, and result
|
||||||
- writer와 reader는 모든 nodal result ID가 `model/nodes/internal_id`에 존재하는지
|
contracts. It does not return a partial database or partial snapshots.
|
||||||
검사하며, 없는 ID를 성공 결과로 반환하지 않는다.
|
- The public reader returns adapter-owned, read-only metadata, model, and
|
||||||
- reader는 nodal result를 `ResultDatabase`로, model dataset을 HDF5 adapter 전용
|
analysis snapshots plus the semantic `ResultDatabase`. No HDF5 object or
|
||||||
read-only inspection model로 반환한다. `Domain`과 `ResultDatabase`에는 HDF5
|
handle escapes the adapter, and the snapshots contain enough information to
|
||||||
저장 계약을 추가하지 않는다.
|
reconstruct the Phase 1 model and run definition without the source deck.
|
||||||
- malformed 또는 지원하지 않는 파일은 부분 database를 반환하지 않는다.
|
|
||||||
|
|||||||
@@ -2,22 +2,34 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
#include <fesa/core/vec3.hpp>
|
#include <fesa/core/vec3.hpp>
|
||||||
#include <fesa/fem/beam_frame.hpp>
|
#include <fesa/fem/beam_frame.hpp>
|
||||||
#include <fesa/model/beam_section.hpp>
|
#include <fesa/model/beam_section.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
#include <fesa/model/material.hpp>
|
#include <fesa/model/material.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
struct Beam3D2Input final {
|
struct Beam3D2Input final {
|
||||||
std::array<Vec3, 2> coordinates;
|
std::array<Vec3, 2> coordinates;
|
||||||
|
std::array<NodeId, 2> node_ids;
|
||||||
IsotropicElastic material;
|
IsotropicElastic material;
|
||||||
BeamSection section;
|
BeamSection section;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct BeamSectionResult final {
|
||||||
|
double xi;
|
||||||
|
NodeId end_node;
|
||||||
|
std::array<double, 6> section_strain;
|
||||||
|
std::array<double, 6> section_force;
|
||||||
|
double centroid_sigma_xx;
|
||||||
|
std::vector<double> sigma_xx;
|
||||||
|
};
|
||||||
|
|
||||||
struct Beam3D2Contribution final {
|
struct Beam3D2Contribution final {
|
||||||
Matrix12 local_stiffness;
|
Matrix12 local_stiffness;
|
||||||
Matrix12 global_stiffness;
|
Matrix12 global_stiffness;
|
||||||
@@ -32,4 +44,9 @@ struct BeamKernelResult final {
|
|||||||
[[nodiscard]] BeamKernelResult compute_beam3d2(
|
[[nodiscard]] BeamKernelResult compute_beam3d2(
|
||||||
const Beam3D2Input& input);
|
const Beam3D2Input& input);
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<BeamSectionResult> recover_beam3d2(
|
||||||
|
const Beam3D2Input& input,
|
||||||
|
std::span<const double, 12> element_displacement,
|
||||||
|
std::span<const std::array<double, 2>> recovery_points);
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
@@ -12,6 +13,7 @@ namespace fesa {
|
|||||||
struct ParseDeckResult final {
|
struct ParseDeckResult final {
|
||||||
std::optional<ParsedDeck> deck;
|
std::optional<ParsedDeck> deck;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
std::string input_fingerprint;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] ParseDeckResult parse_deck(
|
[[nodiscard]] ParseDeckResult parse_deck(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
@@ -26,33 +27,74 @@ struct Hdf5NodeSnapshot final {
|
|||||||
struct Hdf5ElementSnapshot final {
|
struct Hdf5ElementSnapshot final {
|
||||||
std::uint64_t dense_index;
|
std::uint64_t dense_index;
|
||||||
ElementId id;
|
ElementId id;
|
||||||
|
EntityOrigin origin;
|
||||||
std::array<std::uint64_t, 2> connectivity;
|
std::array<std::uint64_t, 2> connectivity;
|
||||||
|
MaterialId material;
|
||||||
SectionId section;
|
SectionId section;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5SectionSnapshot final {
|
struct Hdf5SectionSnapshot final {
|
||||||
SectionId id;
|
SectionId id;
|
||||||
|
std::string name;
|
||||||
|
double area;
|
||||||
|
double iy;
|
||||||
|
double iz;
|
||||||
|
double torsion_j;
|
||||||
double shear_area_y;
|
double shear_area_y;
|
||||||
double shear_area_z;
|
double shear_area_z;
|
||||||
ShearPropertySource shear_source;
|
ShearPropertySource shear_source;
|
||||||
|
Vec3 orientation;
|
||||||
|
std::vector<std::array<double, 2>> recovery_points;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5ModelSnapshot final {
|
struct Hdf5ModelSnapshot final {
|
||||||
std::vector<Hdf5NodeSnapshot> nodes;
|
std::vector<Hdf5NodeSnapshot> nodes;
|
||||||
std::vector<Hdf5ElementSnapshot> elements;
|
std::vector<Hdf5ElementSnapshot> elements;
|
||||||
|
std::vector<IsotropicElastic> materials;
|
||||||
std::vector<Hdf5SectionSnapshot> sections;
|
std::vector<Hdf5SectionSnapshot> sections;
|
||||||
|
std::vector<NodeSet> node_sets;
|
||||||
|
std::vector<ElementSet> element_sets;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5MetadataSnapshot final {
|
||||||
|
std::string schema_version;
|
||||||
|
std::string fesa_version;
|
||||||
|
std::string unit_policy;
|
||||||
|
std::string input_source;
|
||||||
|
std::string input_fingerprint;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5InputIdentity final {
|
||||||
|
std::string source;
|
||||||
|
std::string fingerprint;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5SolverSettingsSnapshot final {
|
||||||
|
std::string backend;
|
||||||
|
std::string matrix_storage;
|
||||||
|
std::string matrix_type;
|
||||||
|
std::string constraint_method;
|
||||||
|
std::string assembly;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5AnalysisSnapshot final {
|
||||||
|
StepDefinition step;
|
||||||
|
Hdf5SolverSettingsSnapshot solver;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5ReadResult final {
|
struct Hdf5ReadResult final {
|
||||||
std::optional<ResultDatabase> database;
|
std::optional<ResultDatabase> database;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
std::optional<Hdf5ModelSnapshot> model;
|
std::optional<Hdf5ModelSnapshot> model;
|
||||||
|
std::optional<Hdf5MetadataSnapshot> metadata;
|
||||||
|
std::optional<Hdf5AnalysisSnapshot> analysis;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
const ResultDatabase& database);
|
const ResultDatabase& database,
|
||||||
|
const Hdf5InputIdentity& input_identity);
|
||||||
|
|
||||||
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
||||||
const std::filesystem::path& path);
|
const std::filesystem::path& path);
|
||||||
|
|||||||
@@ -2,23 +2,62 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
#include <fesa/core/status.hpp>
|
#include <fesa/core/status.hpp>
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
|
#include <fesa/model/entity_origin.hpp>
|
||||||
#include <fesa/model/ids.hpp>
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
|
enum class FieldCoordinateSystem { global, element_local };
|
||||||
|
|
||||||
struct NodalFrame final {
|
struct NodalFrame final {
|
||||||
|
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||||
|
FieldCoordinateSystem::global;
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
displacement_components{
|
||||||
|
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"};
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
reaction_components{
|
||||||
|
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"};
|
||||||
|
|
||||||
std::vector<NodeId> node_ids;
|
std::vector<NodeId> node_ids;
|
||||||
|
std::vector<EntityOrigin> origins;
|
||||||
std::vector<std::array<double, 6>> displacement;
|
std::vector<std::array<double, 6>> displacement;
|
||||||
std::vector<std::array<double, 6>> reaction;
|
std::vector<std::array<double, 6>> reaction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct BeamElementFrame final {
|
||||||
|
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||||
|
FieldCoordinateSystem::element_local;
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
section_strain_components{
|
||||||
|
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||||
|
"kappa_z"};
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
section_force_components{
|
||||||
|
"N", "Vy", "Vz", "T", "My", "Mz"};
|
||||||
|
inline static constexpr std::string_view axial_stress_component =
|
||||||
|
"sigma_xx";
|
||||||
|
|
||||||
|
ElementId element;
|
||||||
|
EntityOrigin origin;
|
||||||
|
BeamFrame local_frame;
|
||||||
|
std::array<BeamSectionResult, 2> end_results;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ElementFrame final {
|
||||||
|
std::vector<BeamElementFrame> beams;
|
||||||
|
};
|
||||||
|
|
||||||
struct ResultFrame final {
|
struct ResultFrame final {
|
||||||
double step_time;
|
double step_time;
|
||||||
NodalFrame nodal;
|
NodalFrame nodal;
|
||||||
|
ElementFrame element;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -37,7 +37,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "result-contract-completion",
|
"dir": "result-contract-completion",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"completed_at": "2026-08-02T01:51:25+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "beam-reference-qualification",
|
"dir": "beam-reference-qualification",
|
||||||
|
|||||||
@@ -5,17 +5,28 @@
|
|||||||
{
|
{
|
||||||
"step": 0,
|
"step": 0,
|
||||||
"name": "beam-element-end-recovery",
|
"name": "beam-element-end-recovery",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added actual Beam node IDs and signed two-end strain, resultant, centroid/recovery-point stress recovery using the stiffness frame and reduced-shear convention, with hand-calculated tests.",
|
||||||
|
"started_at": "2026-08-02T00:27:47+0900",
|
||||||
|
"completed_at": "2026-08-02T01:14:40+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 1,
|
"step": 1,
|
||||||
"name": "complete-result-contract",
|
"name": "complete-result-contract",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added nodal and Beam element provenance, explicit field metadata and validation, and production element-end recovery orchestration in LinearStaticAnalysis.",
|
||||||
|
"started_at": "2026-08-02T01:14:43+0900",
|
||||||
|
"completed_at": "2026-08-02T01:28:37+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 2,
|
"step": 2,
|
||||||
"name": "self-contained-hdf5",
|
"name": "self-contained-hdf5",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Defined schema 2.0.0 and completed self-contained HDF5 model, analysis, nodal/Beam result, diagnostic writer/reader round trips with strict metadata and version validation.",
|
||||||
|
"started_at": "2026-08-02T01:28:37+0900",
|
||||||
|
"completed_at": "2026-08-02T01:51:24+0900"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"created_at": "2026-08-02T00:27:47+0900",
|
||||||
|
"completed_at": "2026-08-02T01:51: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
@@ -4,6 +4,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
#include <stdexcept>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include <fesa/assembly/equation_system.hpp>
|
#include <fesa/assembly/equation_system.hpp>
|
||||||
#include <fesa/assembly/serial_assembler.hpp>
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
#include <fesa/constraints/essential_bc.hpp>
|
#include <fesa/constraints/essential_bc.hpp>
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
#include <fesa/fem/dof_manager.hpp>
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
||||||
|
|
||||||
@@ -34,6 +36,18 @@ AnalysisRunResult equation_failure(
|
|||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnalysisRunResult results_failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message) {
|
||||||
|
return failure({{
|
||||||
|
DiagnosticStage::results,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::nullopt,
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
NodalFrame build_nodal_frame(
|
NodalFrame build_nodal_frame(
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
const DofManager& dofs,
|
const DofManager& dofs,
|
||||||
@@ -48,9 +62,11 @@ NodalFrame build_nodal_frame(
|
|||||||
|
|
||||||
NodalFrame nodal;
|
NodalFrame nodal;
|
||||||
nodal.node_ids = std::move(node_ids);
|
nodal.node_ids = std::move(node_ids);
|
||||||
|
nodal.origins.reserve(nodal.node_ids.size());
|
||||||
nodal.displacement.reserve(nodal.node_ids.size());
|
nodal.displacement.reserve(nodal.node_ids.size());
|
||||||
nodal.reaction.reserve(nodal.node_ids.size());
|
nodal.reaction.reserve(nodal.node_ids.size());
|
||||||
for (const NodeId node_id : nodal.node_ids) {
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
|
nodal.origins.push_back(domain.node(node_id).origin);
|
||||||
std::array<double, 6> node_displacement{};
|
std::array<double, 6> node_displacement{};
|
||||||
std::array<double, 6> node_reaction{};
|
std::array<double, 6> node_reaction{};
|
||||||
for (std::size_t component = 0; component < 6; ++component) {
|
for (std::size_t component = 0; component < 6; ++component) {
|
||||||
@@ -67,6 +83,70 @@ NodalFrame build_nodal_frame(
|
|||||||
return nodal;
|
return nodal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElementFrame build_element_frame(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs,
|
||||||
|
const std::vector<double>& displacement) {
|
||||||
|
std::vector<const BeamElement*> elements;
|
||||||
|
elements.reserve(domain.beam_elements().size());
|
||||||
|
for (const BeamElement& element : domain.beam_elements()) {
|
||||||
|
elements.push_back(&element);
|
||||||
|
}
|
||||||
|
std::ranges::sort(
|
||||||
|
elements,
|
||||||
|
{},
|
||||||
|
[](const BeamElement* element) {
|
||||||
|
return element->id.value();
|
||||||
|
});
|
||||||
|
|
||||||
|
ElementFrame frame;
|
||||||
|
frame.beams.reserve(elements.size());
|
||||||
|
for (const BeamElement* element : elements) {
|
||||||
|
const Beam3D2Input input{
|
||||||
|
{
|
||||||
|
domain.node(element->nodes[0]).position,
|
||||||
|
domain.node(element->nodes[1]).position,
|
||||||
|
},
|
||||||
|
element->nodes,
|
||||||
|
domain.material(element->material),
|
||||||
|
domain.section(element->section),
|
||||||
|
};
|
||||||
|
const BeamKernelResult kernel = compute_beam3d2(input);
|
||||||
|
if (!kernel.contribution.has_value()) {
|
||||||
|
const std::string message = kernel.diagnostics.empty()
|
||||||
|
? "Beam recovery requires a valid element input."
|
||||||
|
: kernel.diagnostics.front().message;
|
||||||
|
throw std::runtime_error{message};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<double, 12> element_displacement{};
|
||||||
|
const std::array<std::size_t, 12> full_dofs =
|
||||||
|
dofs.element_full_dofs(*element);
|
||||||
|
for (std::size_t local = 0; local < full_dofs.size(); ++local) {
|
||||||
|
element_displacement[local] = displacement[full_dofs[local]];
|
||||||
|
}
|
||||||
|
std::vector<BeamSectionResult> recovered = recover_beam3d2(
|
||||||
|
input,
|
||||||
|
element_displacement,
|
||||||
|
input.section.recovery_points);
|
||||||
|
if (recovered.size() != 2U) {
|
||||||
|
throw std::logic_error{
|
||||||
|
"Beam recovery must return exactly two end results."};
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.beams.push_back({
|
||||||
|
element->id,
|
||||||
|
element->origin,
|
||||||
|
kernel.contribution->frame,
|
||||||
|
{
|
||||||
|
std::move(recovered[0]),
|
||||||
|
std::move(recovered[1]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||||
@@ -114,14 +194,23 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
|||||||
"equation.reaction_recovery_failed", error.what());
|
"equation.reaction_recovery_failed", error.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElementFrame element;
|
||||||
|
try {
|
||||||
|
element = build_element_frame(domain, dofs, displacement);
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
return results_failure(
|
||||||
|
"results.element_recovery_failed", error.what());
|
||||||
|
}
|
||||||
|
|
||||||
ResultDatabase database{
|
ResultDatabase database{
|
||||||
"1.0.0",
|
"2.0.0",
|
||||||
{{
|
{{
|
||||||
domain.step().name,
|
domain.step().name,
|
||||||
{{
|
{{
|
||||||
1.0,
|
1.0,
|
||||||
build_nodal_frame(
|
build_nodal_frame(
|
||||||
domain, dofs, displacement, reaction),
|
domain, dofs, displacement, reaction),
|
||||||
|
std::move(element),
|
||||||
{},
|
{},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <fesa/analysis/run_solver.hpp>
|
#include <fesa/analysis/run_solver.hpp>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <fesa/io/abaqus/parser.hpp>
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
@@ -8,6 +9,14 @@
|
|||||||
#include <fesa/io/hdf5/writer.hpp>
|
#include <fesa/io/hdf5/writer.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string path_utf8(const std::filesystem::path& path) {
|
||||||
|
const std::u8string value = path.u8string();
|
||||||
|
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
||||||
ParseDeckResult parsed = parse_deck(request.input_path);
|
ParseDeckResult parsed = parse_deck(request.input_path);
|
||||||
@@ -20,13 +29,18 @@ AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
|||||||
return {false, std::nullopt, std::move(mapped.diagnostics)};
|
return {false, std::nullopt, std::move(mapped.diagnostics)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Hdf5InputIdentity identity{
|
||||||
|
path_utf8(request.input_path),
|
||||||
|
parsed.input_fingerprint,
|
||||||
|
};
|
||||||
|
|
||||||
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
|
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
|
||||||
if (!run.succeeded || !run.results.has_value()) {
|
if (!run.succeeded || !run.results.has_value()) {
|
||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Diagnostic> write_diagnostics = write_hdf5(
|
std::vector<Diagnostic> write_diagnostics = write_hdf5(
|
||||||
request.output_path, *mapped.domain, *run.results);
|
request.output_path, *mapped.domain, *run.results, identity);
|
||||||
if (!write_diagnostics.empty()) {
|
if (!write_diagnostics.empty()) {
|
||||||
return {false, std::nullopt, std::move(write_diagnostics)};
|
return {false, std::nullopt, std::move(write_diagnostics)};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ ElementEvaluation evaluate_element(
|
|||||||
domain.node(element.nodes[0]).position,
|
domain.node(element.nodes[0]).position,
|
||||||
domain.node(element.nodes[1]).position,
|
domain.node(element.nodes[1]).position,
|
||||||
},
|
},
|
||||||
|
element.nodes,
|
||||||
domain.material(element.material),
|
domain.material(element.material),
|
||||||
domain.section(element.section),
|
domain.section(element.section),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ std::vector<NumericContribution> collect_numeric_contributions(
|
|||||||
domain.node(element.nodes[0]).position,
|
domain.node(element.nodes[0]).position,
|
||||||
domain.node(element.nodes[1]).position,
|
domain.node(element.nodes[1]).position,
|
||||||
},
|
},
|
||||||
|
element.nodes,
|
||||||
domain.material(element.material),
|
domain.material(element.material),
|
||||||
domain.section(element.section),
|
domain.section(element.section),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/fem/gauss_rule.hpp>
|
#include <fesa/fem/gauss_rule.hpp>
|
||||||
#include <fesa/fem/line2_shape.hpp>
|
#include <fesa/fem/line2_shape.hpp>
|
||||||
@@ -32,6 +34,20 @@ bool is_positive_finite(const double value) {
|
|||||||
return std::isfinite(value) && value > 0.0;
|
return std::isfinite(value) && value > 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<double, 6> constitutive_values(
|
||||||
|
const Beam3D2Input& input) {
|
||||||
|
const double shear_modulus =
|
||||||
|
input.material.young / (2.0 * (1.0 + input.material.poisson));
|
||||||
|
return {
|
||||||
|
input.material.young * input.section.area,
|
||||||
|
shear_modulus * input.section.shear_area_y,
|
||||||
|
shear_modulus * input.section.shear_area_z,
|
||||||
|
shear_modulus * input.section.torsion_j,
|
||||||
|
input.material.young * input.section.iy,
|
||||||
|
input.material.young * input.section.iz,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<BeamKernelResult> validate_properties(
|
std::optional<BeamKernelResult> validate_properties(
|
||||||
const Beam3D2Input& input) {
|
const Beam3D2Input& input) {
|
||||||
if (!std::isfinite(input.material.young) ||
|
if (!std::isfinite(input.material.young) ||
|
||||||
@@ -149,6 +165,34 @@ Matrix12 transform_stiffness(
|
|||||||
return global;
|
return global;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<double, 12> transform_displacement(
|
||||||
|
const Matrix12& transformation,
|
||||||
|
const std::span<const double, 12> global_displacement) {
|
||||||
|
std::array<double, 12> local_displacement{};
|
||||||
|
for (std::size_t row = 0; row < transformation.size(); ++row) {
|
||||||
|
for (std::size_t column = 0;
|
||||||
|
column < transformation[row].size();
|
||||||
|
++column) {
|
||||||
|
local_displacement[row] +=
|
||||||
|
transformation[row][column] * global_displacement[column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return local_displacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<double, 6> evaluate_strain(
|
||||||
|
const StrainMatrix& strain_matrix,
|
||||||
|
const std::array<double, 12>& local_displacement) {
|
||||||
|
std::array<double, 6> strain{};
|
||||||
|
for (std::size_t component = 0; component < strain.size(); ++component) {
|
||||||
|
for (std::size_t dof = 0; dof < local_displacement.size(); ++dof) {
|
||||||
|
strain[component] +=
|
||||||
|
strain_matrix[component][dof] * local_displacement[dof];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strain;
|
||||||
|
}
|
||||||
|
|
||||||
bool is_finite(const Matrix12& matrix) {
|
bool is_finite(const Matrix12& matrix) {
|
||||||
for (const auto& row : matrix) {
|
for (const auto& row : matrix) {
|
||||||
for (const double value : row) {
|
for (const double value : row) {
|
||||||
@@ -189,16 +233,8 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
|
|||||||
"Beam kernel requires a representable positive Jacobian.");
|
"Beam kernel requires a representable positive Jacobian.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const double shear_modulus =
|
const std::array<double, 6> constitutive =
|
||||||
input.material.young / (2.0 * (1.0 + input.material.poisson));
|
constitutive_values(input);
|
||||||
const std::array<double, 6> constitutive{
|
|
||||||
input.material.young * input.section.area,
|
|
||||||
shear_modulus * input.section.shear_area_y,
|
|
||||||
shear_modulus * input.section.shear_area_z,
|
|
||||||
shear_modulus * input.section.torsion_j,
|
|
||||||
input.material.young * input.section.iy,
|
|
||||||
input.material.young * input.section.iz,
|
|
||||||
};
|
|
||||||
for (const double value : constitutive) {
|
for (const double value : constitutive) {
|
||||||
if (!is_positive_finite(value)) {
|
if (!is_positive_finite(value)) {
|
||||||
return error_result(
|
return error_result(
|
||||||
@@ -241,4 +277,73 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<BeamSectionResult> recover_beam3d2(
|
||||||
|
const Beam3D2Input& input,
|
||||||
|
const std::span<const double, 12> element_displacement,
|
||||||
|
const std::span<const std::array<double, 2>> recovery_points) {
|
||||||
|
const BeamKernelResult kernel = compute_beam3d2(input);
|
||||||
|
if (!kernel.contribution.has_value()) {
|
||||||
|
const std::string message = kernel.diagnostics.empty()
|
||||||
|
? "Beam recovery requires a valid Beam3D2 input."
|
||||||
|
: kernel.diagnostics.front().message;
|
||||||
|
throw std::invalid_argument{message};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec3 axis{
|
||||||
|
input.coordinates[1].x - input.coordinates[0].x,
|
||||||
|
input.coordinates[1].y - input.coordinates[0].y,
|
||||||
|
input.coordinates[1].z - input.coordinates[0].z,
|
||||||
|
};
|
||||||
|
const double jacobian = std::hypot(axis.x, axis.y, axis.z) / 2.0;
|
||||||
|
const Matrix12 transformation =
|
||||||
|
beam_transformation(kernel.contribution->frame);
|
||||||
|
const std::array<double, 12> local_displacement =
|
||||||
|
transform_displacement(transformation, element_displacement);
|
||||||
|
const std::array<double, 6> center_strain = evaluate_strain(
|
||||||
|
strain_matrix(0.0, jacobian),
|
||||||
|
local_displacement);
|
||||||
|
const std::array<double, 6> constitutive =
|
||||||
|
constitutive_values(input);
|
||||||
|
|
||||||
|
std::vector<BeamSectionResult> results;
|
||||||
|
results.reserve(input.node_ids.size());
|
||||||
|
for (std::size_t end = 0; end < input.node_ids.size(); ++end) {
|
||||||
|
const double xi = end == 0 ? -1.0 : 1.0;
|
||||||
|
std::array<double, 6> section_strain = evaluate_strain(
|
||||||
|
strain_matrix(xi, jacobian),
|
||||||
|
local_displacement);
|
||||||
|
section_strain[1] = center_strain[1];
|
||||||
|
section_strain[2] = center_strain[2];
|
||||||
|
|
||||||
|
std::array<double, 6> section_force{};
|
||||||
|
for (std::size_t component = 0;
|
||||||
|
component < section_force.size();
|
||||||
|
++component) {
|
||||||
|
section_force[component] =
|
||||||
|
constitutive[component] * section_strain[component];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> sigma_xx;
|
||||||
|
sigma_xx.reserve(recovery_points.size());
|
||||||
|
for (const auto& point : recovery_points) {
|
||||||
|
const double y = point[0];
|
||||||
|
const double z = point[1];
|
||||||
|
sigma_xx.push_back(
|
||||||
|
input.material.young *
|
||||||
|
(section_strain[0] + z * section_strain[4] -
|
||||||
|
y * section_strain[5]));
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push_back({
|
||||||
|
xi,
|
||||||
|
input.node_ids[end],
|
||||||
|
section_strain,
|
||||||
|
section_force,
|
||||||
|
section_force[0] / input.section.area,
|
||||||
|
std::move(sigma_xx),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <iterator>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -50,6 +53,20 @@ std::string uppercase_ascii(std::string value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string input_fingerprint(const std::string_view bytes) {
|
||||||
|
std::uint64_t fingerprint = 14695981039346656037ULL;
|
||||||
|
for (const char byte : bytes) {
|
||||||
|
fingerprint ^=
|
||||||
|
static_cast<std::uint8_t>(static_cast<unsigned char>(byte));
|
||||||
|
fingerprint *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream encoded;
|
||||||
|
encoded << "fnv1a64:" << std::hex << std::setfill('0')
|
||||||
|
<< std::setw(16) << fingerprint;
|
||||||
|
return encoded.str();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> split_fields(const std::string_view value) {
|
std::vector<std::string> split_fields(const std::string_view value) {
|
||||||
std::vector<std::string> fields;
|
std::vector<std::string> fields;
|
||||||
std::size_t first = 0;
|
std::size_t first = 0;
|
||||||
@@ -374,14 +391,26 @@ ParseDeckResult missing_parameter(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||||
std::ifstream input{path, std::ios::binary};
|
std::ifstream file{path, std::ios::binary};
|
||||||
if (!input) {
|
if (!file) {
|
||||||
return failure(
|
return failure(
|
||||||
DiagnosticStage::io,
|
DiagnosticStage::io,
|
||||||
"abaqus.io.open_failed",
|
"abaqus.io.open_failed",
|
||||||
"Unable to open Abaqus input file.",
|
"Unable to open Abaqus input file.",
|
||||||
SourceLocation{path, 0U, 0U});
|
SourceLocation{path, 0U, 0U});
|
||||||
}
|
}
|
||||||
|
const std::string source_bytes{
|
||||||
|
std::istreambuf_iterator<char>{file},
|
||||||
|
std::istreambuf_iterator<char>{},
|
||||||
|
};
|
||||||
|
if (file.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"abaqus.io.read_failed",
|
||||||
|
"Failed while reading Abaqus input file.",
|
||||||
|
SourceLocation{path, 0U, 0U});
|
||||||
|
}
|
||||||
|
std::istringstream input{source_bytes};
|
||||||
|
|
||||||
ParsedDeck deck;
|
ParsedDeck deck;
|
||||||
Scope scope = Scope::global;
|
Scope scope = Scope::global;
|
||||||
@@ -846,7 +875,11 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
*current_step_source);
|
*current_step_source);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {std::move(deck), {}};
|
return {
|
||||||
|
std::move(deck),
|
||||||
|
{},
|
||||||
|
input_fingerprint(source_bytes),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
+1677
-175
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,12 @@ void validate_nodal_frame(
|
|||||||
"Nodal IDs, displacement, and reaction fields must have "
|
"Nodal IDs, displacement, and reaction fields must have "
|
||||||
"matching sizes.");
|
"matching sizes.");
|
||||||
}
|
}
|
||||||
|
if (nodal.origins.size() != nodal.node_ids.size()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nodal_origin_size_mismatch",
|
||||||
|
"Nodal IDs and origins must have matching sizes.");
|
||||||
|
}
|
||||||
|
|
||||||
std::set<std::int64_t> node_ids;
|
std::set<std::int64_t> node_ids;
|
||||||
for (const NodeId node_id : nodal.node_ids) {
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
@@ -74,6 +80,84 @@ void validate_nodal_frame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void validate_beam_section_result(
|
||||||
|
const BeamSectionResult& result,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
if (
|
||||||
|
!std::isfinite(result.xi) ||
|
||||||
|
!is_finite(result.section_strain) ||
|
||||||
|
!is_finite(result.section_force) ||
|
||||||
|
!std::isfinite(result.centroid_sigma_xx) ||
|
||||||
|
!std::ranges::all_of(
|
||||||
|
result.sigma_xx,
|
||||||
|
[](const double value) { return std::isfinite(value); })) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nonfinite_value",
|
||||||
|
"Beam section result contains a nonfinite value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate_element_frame(
|
||||||
|
const ElementFrame& element,
|
||||||
|
const NodalFrame& nodal,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
std::set<std::int64_t> nodal_ids;
|
||||||
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
|
nodal_ids.insert(node_id.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::int64_t> element_ids;
|
||||||
|
for (const BeamElementFrame& beam : element.beams) {
|
||||||
|
if (!element_ids.insert(beam.element.value()).second) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.duplicate_element_id",
|
||||||
|
"Element frame contains duplicate element ID " +
|
||||||
|
std::to_string(beam.element.value()) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!is_finite(beam.local_frame.ex) ||
|
||||||
|
!is_finite(beam.local_frame.ey) ||
|
||||||
|
!is_finite(beam.local_frame.ez)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nonfinite_value",
|
||||||
|
"Beam local frame contains a nonfinite component.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeamSectionResult& first = beam.end_results[0];
|
||||||
|
const BeamSectionResult& second = beam.end_results[1];
|
||||||
|
if (first.end_node == second.end_node) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.duplicate_beam_end_node",
|
||||||
|
"Beam element result contains the same node at both ends.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
first.xi != -1.0 || second.xi != 1.0 ||
|
||||||
|
!nodal_ids.contains(first.end_node.value()) ||
|
||||||
|
!nodal_ids.contains(second.end_node.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.invalid_beam_connectivity",
|
||||||
|
"Beam end results must follow (-1, +1) connectivity and "
|
||||||
|
"reference nodes in the nodal frame.");
|
||||||
|
}
|
||||||
|
if (first.sigma_xx.size() != second.sigma_xx.size()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.recovery_point_count_mismatch",
|
||||||
|
"Beam end results must have matching recovery-point "
|
||||||
|
"counts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_beam_section_result(first, diagnostics);
|
||||||
|
validate_beam_section_result(second, diagnostics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Status validate_result_database(const ResultDatabase& database) {
|
Status validate_result_database(const ResultDatabase& database) {
|
||||||
@@ -106,6 +190,8 @@ Status validate_result_database(const ResultDatabase& database) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_nodal_frame(frame.nodal, diagnostics);
|
validate_nodal_frame(frame.nodal, diagnostics);
|
||||||
|
validate_element_frame(
|
||||||
|
frame.element, frame.nodal, diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-2
@@ -360,6 +360,24 @@ add_test(
|
|||||||
--gtest_filter=RigidBody.*
|
--gtest_filter=RigidBody.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME BeamRecovery
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||||
|
--gtest_filter=BeamRecovery.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SectionForce
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||||
|
--gtest_filter=SectionForce.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CentroidStress
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||||
|
--gtest_filter=CentroidStress.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_serial_assembly_tests
|
add_executable(fesa_serial_assembly_tests
|
||||||
unit/assembly/serial_assembler_test.cpp
|
unit/assembly/serial_assembler_test.cpp
|
||||||
)
|
)
|
||||||
@@ -580,6 +598,18 @@ add_test(
|
|||||||
--gtest_filter=ResultDatabase.*
|
--gtest_filter=ResultDatabase.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ElementFrame
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=ElementFrame.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ResultContractMetadata
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=CompleteResultContract.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_hdf5_results_tests
|
add_executable(fesa_hdf5_results_tests
|
||||||
integration/io/hdf5_results_test.cpp
|
integration/io/hdf5_results_test.cpp
|
||||||
)
|
)
|
||||||
@@ -617,8 +647,14 @@ add_test(
|
|||||||
--gtest_filter=ResultRoundTrip.*
|
--gtest_filter=ResultRoundTrip.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SelfContainedHdf5
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
|
||||||
|
--gtest_filter=SelfContainedHdf5.*
|
||||||
|
)
|
||||||
|
|
||||||
set_property(
|
set_property(
|
||||||
TEST Hdf5 ResultRoundTrip
|
TEST Hdf5 ResultRoundTrip SelfContainedHdf5
|
||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
)
|
)
|
||||||
@@ -656,8 +692,14 @@ add_test(
|
|||||||
--gtest_filter=StaticEquilibrium.*
|
--gtest_filter=StaticEquilibrium.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CompleteResultContract
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_linear_static_analysis_tests>"
|
||||||
|
--gtest_filter=CompleteResultContract.*
|
||||||
|
)
|
||||||
|
|
||||||
set_property(
|
set_property(
|
||||||
TEST LinearStaticAnalysis StaticEquilibrium
|
TEST LinearStaticAnalysis StaticEquilibrium CompleteResultContract
|
||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -98,6 +98,63 @@ void replace_step_time_with_vector(const std::filesystem::path& path) {
|
|||||||
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void delete_link(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view link_path) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_link_path{link_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(
|
||||||
|
H5Ldelete(file.get(), owned_link_path.c_str(), H5P_DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void copy_object(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view source_path,
|
||||||
|
const std::string_view target_path) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_source_path{source_path};
|
||||||
|
const std::string owned_target_path{target_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(H5Ocopy(
|
||||||
|
file.get(),
|
||||||
|
owned_source_path.c_str(),
|
||||||
|
file.get(),
|
||||||
|
owned_target_path.c_str(),
|
||||||
|
H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_double_attribute(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view object_path,
|
||||||
|
const std::string_view attribute_name,
|
||||||
|
const double value) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_object_path{object_path};
|
||||||
|
const std::string owned_attribute_name{attribute_name};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle object{
|
||||||
|
H5Oopen(file.get(), owned_object_path.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Oclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle attribute{
|
||||||
|
H5Aopen(object.get(), owned_attribute_name.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Aclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(
|
||||||
|
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value));
|
||||||
|
}
|
||||||
|
|
||||||
void write_int64_dataset(
|
void write_int64_dataset(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const std::string_view dataset_path,
|
const std::string_view dataset_path,
|
||||||
@@ -146,12 +203,69 @@ void write_double_dataset(
|
|||||||
values.data()));
|
values.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void write_uint8_dataset(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view dataset_path,
|
||||||
|
const std::span<const std::uint8_t> values) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_dataset_path{dataset_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle dataset{
|
||||||
|
H5Dopen2(
|
||||||
|
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Dclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(H5Dwrite(
|
||||||
|
dataset.get(),
|
||||||
|
H5T_NATIVE_UINT8,
|
||||||
|
H5S_ALL,
|
||||||
|
H5S_ALL,
|
||||||
|
H5P_DEFAULT,
|
||||||
|
values.data()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_root_string_attribute(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view name,
|
||||||
|
const std::string_view value) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_name{name};
|
||||||
|
const std::string owned_value{value};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle attribute{
|
||||||
|
H5Aopen(file.get(), owned_name.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Aclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle type{H5Aget_type(attribute.get()), &H5Tclose};
|
||||||
|
const char* pointer = owned_value.c_str();
|
||||||
|
require_hdf5_status(H5Awrite(attribute.get(), type.get(), &pointer));
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path round_trip_path() {
|
std::filesystem::path round_trip_path() {
|
||||||
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||||
"Temporary" / "fesa-round-trip.h5";
|
"Temporary" / "fesa-round-trip.h5";
|
||||||
}
|
}
|
||||||
|
|
||||||
fesa::Domain make_domain() {
|
std::filesystem::path self_contained_path() {
|
||||||
|
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||||
|
"Temporary" / "fesa-self-contained.h5";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::Hdf5InputIdentity& test_input_identity() {
|
||||||
|
static const fesa::Hdf5InputIdentity identity{
|
||||||
|
"beam model.inp",
|
||||||
|
"fnv1a64:0123456789abcdef",
|
||||||
|
};
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Domain make_domain(const bool add_unreported_node = false) {
|
||||||
fesa::DomainBuilder builder;
|
fesa::DomainBuilder builder;
|
||||||
builder.add_node({
|
builder.add_node({
|
||||||
fesa::NodeId{42},
|
fesa::NodeId{42},
|
||||||
@@ -161,8 +275,15 @@ fesa::Domain make_domain() {
|
|||||||
builder.add_node({
|
builder.add_node({
|
||||||
fesa::NodeId{7},
|
fesa::NodeId{7},
|
||||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
||||||
fesa::Vec3{4.0, 5.5, -6.25},
|
fesa::Vec3{2.25, -2.5, 3.75},
|
||||||
});
|
});
|
||||||
|
if (add_unreported_node) {
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{99},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1003},
|
||||||
|
fesa::Vec3{8.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
}
|
||||||
builder.add_material({
|
builder.add_material({
|
||||||
fesa::MaterialId{6},
|
fesa::MaterialId{6},
|
||||||
"Steel",
|
"Steel",
|
||||||
@@ -180,7 +301,7 @@ fesa::Domain make_domain() {
|
|||||||
0.032,
|
0.032,
|
||||||
fesa::ShearPropertySource::input,
|
fesa::ShearPropertySource::input,
|
||||||
fesa::Vec3{0.0, 1.0, 0.0},
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
{},
|
{{0.25, -0.5}, {-0.75, 0.125}},
|
||||||
});
|
});
|
||||||
builder.add_section({
|
builder.add_section({
|
||||||
fesa::SectionId{12},
|
fesa::SectionId{12},
|
||||||
@@ -209,7 +330,22 @@ fesa::Domain make_domain() {
|
|||||||
fesa::MaterialId{6},
|
fesa::MaterialId{6},
|
||||||
fesa::SectionId{4},
|
fesa::SectionId{4},
|
||||||
});
|
});
|
||||||
builder.set_step({"Load/Case", {}, {}});
|
builder.add_node_set({"Fixed", {fesa::NodeId{42}}});
|
||||||
|
builder.add_node_set(
|
||||||
|
{"Loaded", {fesa::NodeId{7}, fesa::NodeId{42}}});
|
||||||
|
builder.add_element_set(
|
||||||
|
{"AllBeams", {fesa::ElementId{9}, fesa::ElementId{17}}});
|
||||||
|
builder.set_step({
|
||||||
|
"Load/Case",
|
||||||
|
{
|
||||||
|
{fesa::NodeId{42}, 1, 0.0},
|
||||||
|
{fesa::NodeId{7}, 6, 0.125},
|
||||||
|
},
|
||||||
|
{{
|
||||||
|
fesa::NodeId{7},
|
||||||
|
{100.0, -200.0, 300.0, -400.0, 500.0, -600.0},
|
||||||
|
}},
|
||||||
|
});
|
||||||
|
|
||||||
auto built = std::move(builder).build();
|
auto built = std::move(builder).build();
|
||||||
EXPECT_TRUE(built.domain.has_value());
|
EXPECT_TRUE(built.domain.has_value());
|
||||||
@@ -217,15 +353,26 @@ fesa::Domain make_domain() {
|
|||||||
return std::move(*built.domain);
|
return std::move(*built.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fesa::BeamSectionResult make_end_result(
|
||||||
|
double xi,
|
||||||
|
fesa::NodeId node,
|
||||||
|
double offset);
|
||||||
|
|
||||||
fesa::ResultDatabase make_database() {
|
fesa::ResultDatabase make_database() {
|
||||||
return {
|
fesa::ResultDatabase database{
|
||||||
"1.0.0",
|
"2.0.0",
|
||||||
{{
|
{{
|
||||||
"Load/Case",
|
"Load/Case",
|
||||||
{{
|
{{
|
||||||
1.25,
|
1.25,
|
||||||
{
|
{
|
||||||
{fesa::NodeId{7}, fesa::NodeId{42}},
|
{fesa::NodeId{7}, fesa::NodeId{42}},
|
||||||
|
{
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart", "Beam-1", 1002},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart", "Beam-1", 1001},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
||||||
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
||||||
@@ -236,9 +383,89 @@ fesa::ResultDatabase make_database() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
|
{},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
|
database.steps[0].frames[0].element.beams = {
|
||||||
|
{
|
||||||
|
fesa::ElementId{9},
|
||||||
|
{"BeamPart", "Beam-1", 2001},
|
||||||
|
{
|
||||||
|
{-1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, -1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
make_end_result(-1.0, fesa::NodeId{7}, 0.0),
|
||||||
|
make_end_result(1.0, fesa::NodeId{42}, 100.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::ElementId{17},
|
||||||
|
{"BeamPart", "Beam-1", 2002},
|
||||||
|
{
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
make_end_result(-1.0, fesa::NodeId{42}, 200.0),
|
||||||
|
make_end_result(1.0, fesa::NodeId{7}, 300.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::BeamSectionResult make_end_result(
|
||||||
|
const double xi,
|
||||||
|
const fesa::NodeId node,
|
||||||
|
const double offset) {
|
||||||
|
return {
|
||||||
|
xi,
|
||||||
|
node,
|
||||||
|
{
|
||||||
|
offset + 1.0,
|
||||||
|
offset + 2.0,
|
||||||
|
offset + 3.0,
|
||||||
|
offset + 4.0,
|
||||||
|
offset + 5.0,
|
||||||
|
offset + 6.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset + 10.0,
|
||||||
|
offset + 20.0,
|
||||||
|
offset + 30.0,
|
||||||
|
offset + 40.0,
|
||||||
|
offset + 50.0,
|
||||||
|
offset + 60.0,
|
||||||
|
},
|
||||||
|
offset + 70.0,
|
||||||
|
{offset + 80.0, offset + 90.0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::ResultDatabase make_complete_database() {
|
||||||
|
fesa::ResultDatabase database = make_database();
|
||||||
|
auto& frame = database.steps[0].frames[0];
|
||||||
|
frame.diagnostics = {
|
||||||
|
{
|
||||||
|
fesa::DiagnosticStage::solver,
|
||||||
|
fesa::Severity::warning,
|
||||||
|
"solver.residual",
|
||||||
|
"Residual diagnostic",
|
||||||
|
fesa::SourceLocation{"beam model.inp", 41, 7},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::DiagnosticStage::results,
|
||||||
|
fesa::Severity::error,
|
||||||
|
"results.equilibrium",
|
||||||
|
"Equilibrium diagnostic",
|
||||||
|
std::nullopt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_results_error(
|
bool has_results_error(
|
||||||
@@ -260,8 +487,8 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
const auto domain = make_domain();
|
const auto domain = make_domain();
|
||||||
const auto database = make_database();
|
const auto database = make_database();
|
||||||
|
|
||||||
const auto write_diagnostics =
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, domain, database);
|
path, domain, database, test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
|
|
||||||
const auto read = fesa::read_hdf5_results(path);
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
@@ -269,7 +496,7 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
ASSERT_TRUE(read.database.has_value());
|
ASSERT_TRUE(read.database.has_value());
|
||||||
ASSERT_TRUE(read.model.has_value());
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
|
||||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||||
ASSERT_EQ(read.database->steps.size(), 1U);
|
ASSERT_EQ(read.database->steps.size(), 1U);
|
||||||
const auto& step = read.database->steps[0];
|
const auto& step = read.database->steps[0];
|
||||||
EXPECT_EQ(step.name, "Load/Case");
|
EXPECT_EQ(step.name, "Load/Case");
|
||||||
@@ -279,6 +506,12 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
frame.nodal.node_ids,
|
frame.nodal.node_ids,
|
||||||
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.nodal.origins,
|
||||||
|
(std::vector<fesa::EntityOrigin>{
|
||||||
|
{"BeamPart", "Beam-1", 1002},
|
||||||
|
{"BeamPart", "Beam-1", 1001},
|
||||||
|
}));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
frame.nodal.displacement[0],
|
frame.nodal.displacement[0],
|
||||||
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
||||||
@@ -308,9 +541,9 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
read.model->nodes[1].origin,
|
read.model->nodes[1].origin,
|
||||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 4.0);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 2.25);
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, 5.5);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, -2.5);
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, -6.25);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, 3.75);
|
||||||
|
|
||||||
ASSERT_EQ(read.model->elements.size(), 2U);
|
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||||
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
||||||
@@ -341,6 +574,223 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
fesa::ShearPropertySource::phase1_default);
|
fesa::ShearPropertySource::phase1_default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
||||||
|
const auto path = self_contained_path();
|
||||||
|
std::filesystem::create_directories(path.parent_path());
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path,
|
||||||
|
make_domain(),
|
||||||
|
make_complete_database(),
|
||||||
|
test_input_identity())
|
||||||
|
.empty());
|
||||||
|
|
||||||
|
const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(path);
|
||||||
|
ASSERT_TRUE(read.diagnostics.empty());
|
||||||
|
ASSERT_TRUE(read.metadata.has_value());
|
||||||
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
ASSERT_TRUE(read.analysis.has_value());
|
||||||
|
ASSERT_TRUE(read.database.has_value());
|
||||||
|
|
||||||
|
EXPECT_EQ(read.metadata->schema_version, "2.0.0");
|
||||||
|
EXPECT_EQ(read.metadata->fesa_version, "0.1.0");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->unit_policy,
|
||||||
|
"consistent_input_units_no_conversion");
|
||||||
|
EXPECT_EQ(read.metadata->input_source, "beam model.inp");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_fingerprint,
|
||||||
|
"fnv1a64:0123456789abcdef");
|
||||||
|
|
||||||
|
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||||
|
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->elements[0].origin,
|
||||||
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 2001}));
|
||||||
|
EXPECT_EQ(read.model->elements[0].material, fesa::MaterialId{6});
|
||||||
|
ASSERT_EQ(read.model->materials.size(), 1U);
|
||||||
|
EXPECT_EQ(read.model->materials[0].name, "Steel");
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->materials[0].young, 210.0e9);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->materials[0].poisson, 0.3);
|
||||||
|
ASSERT_EQ(read.model->sections.size(), 2U);
|
||||||
|
EXPECT_EQ(read.model->sections[0].name, "InputShear");
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].area, 0.04);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].iy, 1.2e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].iz, 1.4e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].torsion_j, 2.0e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].orientation.y, 1.0);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->sections[0].recovery_points,
|
||||||
|
(std::vector<std::array<double, 2>>{
|
||||||
|
{0.25, -0.5}, {-0.75, 0.125}}));
|
||||||
|
ASSERT_EQ(read.model->node_sets.size(), 2U);
|
||||||
|
EXPECT_EQ(read.model->node_sets[0].name, "Fixed");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->node_sets[1].members,
|
||||||
|
(std::vector<fesa::NodeId>{
|
||||||
|
fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||||
|
ASSERT_EQ(read.model->element_sets.size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->element_sets[0].members,
|
||||||
|
(std::vector<fesa::ElementId>{
|
||||||
|
fesa::ElementId{9}, fesa::ElementId{17}}));
|
||||||
|
|
||||||
|
EXPECT_EQ(read.analysis->step.name, "Load/Case");
|
||||||
|
ASSERT_EQ(read.analysis->step.prescribed_dofs.size(), 2U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.analysis->step.prescribed_dofs[1].node,
|
||||||
|
fesa::NodeId{7});
|
||||||
|
EXPECT_EQ(read.analysis->step.prescribed_dofs[1].dof, 6U);
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
read.analysis->step.prescribed_dofs[1].value, 0.125);
|
||||||
|
ASSERT_EQ(read.analysis->step.nodal_loads.size(), 1U);
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
read.analysis->step.nodal_loads[0].values[5], -600.0);
|
||||||
|
EXPECT_EQ(read.analysis->solver.backend, "mkl_pardiso");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.analysis->solver.constraint_method,
|
||||||
|
"essential_dof_elimination");
|
||||||
|
EXPECT_EQ(read.analysis->solver.assembly, "deterministic_serial");
|
||||||
|
|
||||||
|
const auto& frame = read.database->steps[0].frames[0];
|
||||||
|
ASSERT_EQ(frame.element.beams.size(), 2U);
|
||||||
|
EXPECT_EQ(frame.element.beams[0].element, fesa::ElementId{9});
|
||||||
|
EXPECT_DOUBLE_EQ(frame.element.beams[0].local_frame.ex.x, -1.0);
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.element.beams[0].end_results[0].end_node,
|
||||||
|
fesa::NodeId{7});
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.element.beams[0].end_results[1].section_force,
|
||||||
|
(std::array<double, 6>{
|
||||||
|
110.0, 120.0, 130.0, 140.0, 150.0, 160.0}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.element.beams[1].end_results[0].sigma_xx,
|
||||||
|
(std::vector<double>{280.0, 290.0}));
|
||||||
|
ASSERT_EQ(frame.diagnostics.size(), 2U);
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].stage, fesa::DiagnosticStage::solver);
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].severity, fesa::Severity::warning);
|
||||||
|
ASSERT_TRUE(frame.diagnostics[0].source.has_value());
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].source->file, "beam model.inp");
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].source->line, 41U);
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].source->column, 7U);
|
||||||
|
EXPECT_FALSE(frame.diagnostics[1].source.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsMissingResultFrameBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-result-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames.clear();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsIncompleteNodalCoverageBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-incomplete-nodal-results.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
const auto database = make_complete_database();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(true), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsIncompleteBeamCoverageBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-incomplete-beam-results.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames[0].element.beams.pop_back();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsFiniteLocalFrameThatDisagreesWithModel) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-wrong-local-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames[0].element.beams[0].local_frame = {
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.result_element_mismatch"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" / "fesa-schema-1.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_database();
|
||||||
|
database.schema_version = "1.0.0";
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsMissingInputIdentityBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-input-identity.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), {"", ""});
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.invalid_input_identity"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsUnlistedMinorSchemaVersion) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" / "fesa-schema-2-1.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
write_root_string_attribute(path, "schema_version", "2.1.0");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.unsupported_schema"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" / "fesa-invalid.h5";
|
"Testing" / "Temporary" / "fesa-invalid.h5";
|
||||||
@@ -349,14 +799,15 @@ TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
|||||||
auto database = make_database();
|
auto database = make_database();
|
||||||
database.steps[0].frames[0].nodal.reaction.pop_back();
|
database.steps[0].frames[0].nodal.reaction.pop_back();
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(
|
EXPECT_TRUE(
|
||||||
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
||||||
EXPECT_FALSE(std::filesystem::exists(path));
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-unsupported-diagnostics.h5";
|
"fesa-unsupported-diagnostics.h5";
|
||||||
@@ -371,11 +822,21 @@ TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
|||||||
std::nullopt,
|
std::nullopt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(has_results_error(
|
ASSERT_TRUE(diagnostics.empty());
|
||||||
diagnostics, "hdf5.unsupported_result_diagnostics"));
|
|
||||||
EXPECT_FALSE(std::filesystem::exists(path));
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
ASSERT_TRUE(read.diagnostics.empty());
|
||||||
|
ASSERT_TRUE(read.database.has_value());
|
||||||
|
const auto& stored = read.database->steps[0].frames[0].diagnostics;
|
||||||
|
ASSERT_EQ(stored.size(), 1U);
|
||||||
|
EXPECT_EQ(stored[0].stage, fesa::DiagnosticStage::solver);
|
||||||
|
EXPECT_EQ(stored[0].severity, fesa::Severity::warning);
|
||||||
|
EXPECT_EQ(stored[0].code, "solver.residual");
|
||||||
|
EXPECT_EQ(stored[0].message, "Residual diagnostic");
|
||||||
|
EXPECT_FALSE(stored[0].source.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
||||||
@@ -385,9 +846,14 @@ TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
|||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto domain = make_domain();
|
const auto domain = make_domain();
|
||||||
auto database = make_database();
|
auto database = make_database();
|
||||||
database.steps[0].frames[0].nodal.node_ids[0] = fesa::NodeId{999};
|
auto& nodal = database.steps[0].frames[0].nodal;
|
||||||
|
nodal.node_ids.push_back(fesa::NodeId{999});
|
||||||
|
nodal.origins.push_back({"BeamPart", "Beam-1", 1999});
|
||||||
|
nodal.displacement.push_back({});
|
||||||
|
nodal.reaction.push_back({});
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(has_results_error(
|
EXPECT_TRUE(has_results_error(
|
||||||
diagnostics, "hdf5.result_node_not_in_model"));
|
diagnostics, "hdf5.result_node_not_in_model"));
|
||||||
@@ -411,8 +877,8 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonscalar-step-time.h5";
|
"fesa-nonscalar-step-time.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto write_diagnostics =
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database());
|
path, make_domain(), make_database(), test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
replace_step_time_with_vector(path);
|
replace_step_time_with_vector(path);
|
||||||
|
|
||||||
@@ -423,19 +889,88 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
|||||||
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedResultWithoutRequiredFrame) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-serialized-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
delete_link(path, "/results/steps/0/frames/0");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedResultWithExtraFrame) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-extra-serialized-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
copy_object(
|
||||||
|
path,
|
||||||
|
"/results/steps/0/frames/0",
|
||||||
|
"/results/steps/0/frames/1");
|
||||||
|
write_double_attribute(
|
||||||
|
path, "/results/steps/0/frames/1", "step_time", 2.5);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsInvalidSerializedInputFingerprint) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-invalid-input-fingerprint.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
write_root_string_attribute(
|
||||||
|
path, "input_fingerprint", "sha256:not-the-contract");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.invalid_input_identity"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-invalid-result-node.h5";
|
"fesa-invalid-result-node.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto write_diagnostics =
|
auto database = make_database();
|
||||||
fesa::write_hdf5(path, make_domain(), make_database());
|
auto& nodal = database.steps[0].frames[0].nodal;
|
||||||
|
nodal.node_ids.push_back(fesa::NodeId{99});
|
||||||
|
nodal.origins.push_back({"BeamPart", "Beam-1", 1003});
|
||||||
|
nodal.displacement.push_back({});
|
||||||
|
nodal.reaction.push_back({});
|
||||||
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(true), database, test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
const std::array<std::int64_t, 2> node_ids{999, 42};
|
const std::array<std::int64_t, 3> node_ids{42, 7, 100};
|
||||||
write_int64_dataset(
|
write_int64_dataset(path, "/model/nodes/internal_id", node_ids);
|
||||||
path,
|
|
||||||
"/results/steps/0/frames/0/nodal/node_ids",
|
|
||||||
node_ids);
|
|
||||||
|
|
||||||
const auto read = fesa::read_hdf5_results(path);
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
@@ -450,8 +985,9 @@ TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-node-ids.h5";
|
"fesa-duplicate-node-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{42, 42};
|
const std::array<std::int64_t, 2> ids{42, 42};
|
||||||
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
||||||
|
|
||||||
@@ -468,8 +1004,9 @@ TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-element-ids.h5";
|
"fesa-duplicate-element-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{9, 9};
|
const std::array<std::int64_t, 2> ids{9, 9};
|
||||||
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
||||||
|
|
||||||
@@ -486,8 +1023,9 @@ TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-section-ids.h5";
|
"fesa-duplicate-section-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{4, 4};
|
const std::array<std::int64_t, 2> ids{4, 4};
|
||||||
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
||||||
|
|
||||||
@@ -504,15 +1042,16 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonfinite-coordinates.h5";
|
"fesa-nonfinite-coordinates.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 6> coordinates{
|
const std::array<double, 6> coordinates{
|
||||||
std::numeric_limits<double>::quiet_NaN(),
|
std::numeric_limits<double>::quiet_NaN(),
|
||||||
-2.5,
|
-2.5,
|
||||||
3.75,
|
3.75,
|
||||||
4.0,
|
2.25,
|
||||||
5.5,
|
-2.5,
|
||||||
-6.25,
|
3.75,
|
||||||
};
|
};
|
||||||
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||||
|
|
||||||
@@ -524,13 +1063,128 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
|||||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedZeroLengthElement) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-zero-length-element.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 6> coordinates{
|
||||||
|
1.25, -2.5, 3.75, 1.25, -2.5, 3.75};
|
||||||
|
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedOrientationParallelToElement) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-parallel-orientation.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 6> orientations{
|
||||||
|
1.0, 0.0, 0.0, 0.0, 1.0, 0.0};
|
||||||
|
write_double_dataset(
|
||||||
|
path, "/model/sections/orientation", orientations);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsDuplicateSerializedNodeOrigins) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-duplicate-node-origins.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<std::int64_t, 2> labels{1001, 1001};
|
||||||
|
write_int64_dataset(path, "/model/nodes/local_label", labels);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsDuplicateSerializedBoundaryConditions) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-duplicate-boundary-conditions.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<std::int64_t, 2> node_ids{42, 42};
|
||||||
|
const std::array<std::uint8_t, 2> dofs{1, 1};
|
||||||
|
write_int64_dataset(
|
||||||
|
path,
|
||||||
|
"/analysis/steps/0/boundary_conditions/node_ids",
|
||||||
|
node_ids);
|
||||||
|
write_uint8_dataset(
|
||||||
|
path, "/analysis/steps/0/boundary_conditions/dofs", dofs);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedLocalFrameThatDisagreesWithModel) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-serialized-wrong-local-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 18> local_frames{
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
};
|
||||||
|
write_double_dataset(
|
||||||
|
path,
|
||||||
|
"/results/steps/0/frames/0/element/beam/local_frame",
|
||||||
|
local_frames);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.result_element_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonfinite-shear-area.h5";
|
"fesa-nonfinite-shear-area.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 2> shear_areas{
|
const std::array<double, 2> shear_areas{
|
||||||
std::numeric_limits<double>::infinity(),
|
std::numeric_limits<double>::infinity(),
|
||||||
0.05,
|
0.05,
|
||||||
@@ -550,8 +1204,9 @@ TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonpositive-shear-area.h5";
|
"fesa-nonpositive-shear-area.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
||||||
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ std::string quote(const std::filesystem::path& path) {
|
|||||||
return '"' + path.string() + '"';
|
return '"' + path.string() + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string path_utf8(const std::filesystem::path& path) {
|
||||||
|
const std::u8string value = path.u8string();
|
||||||
|
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||||
|
}
|
||||||
|
|
||||||
std::string read_text(const std::filesystem::path& path) {
|
std::string read_text(const std::filesystem::path& path) {
|
||||||
std::ifstream input{path, std::ios::binary};
|
std::ifstream input{path, std::ios::binary};
|
||||||
return {
|
return {
|
||||||
@@ -93,8 +98,15 @@ TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
|
|||||||
fesa::read_hdf5_results(output.path());
|
fesa::read_hdf5_results(output.path());
|
||||||
ASSERT_TRUE(read.database.has_value());
|
ASSERT_TRUE(read.database.has_value());
|
||||||
ASSERT_TRUE(read.model.has_value());
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
ASSERT_TRUE(read.metadata.has_value());
|
||||||
EXPECT_TRUE(read.diagnostics.empty());
|
EXPECT_TRUE(read.diagnostics.empty());
|
||||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_source,
|
||||||
|
path_utf8(fixture_path("minimal_cantilever.inp")));
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_fingerprint,
|
||||||
|
"fnv1a64:73f31da4615f09b3");
|
||||||
|
|
||||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||||
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ fesa::Domain build_axial_domain(const bool all_constrained) {
|
|||||||
2.0,
|
2.0,
|
||||||
fesa::ShearPropertySource::input,
|
fesa::ShearPropertySource::input,
|
||||||
fesa::Vec3{0.0, 1.0, 0.0},
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
{},
|
{{1.0, 2.0}, {-1.0, -2.0}},
|
||||||
});
|
});
|
||||||
builder.add_beam_element({
|
builder.add_beam_element({
|
||||||
fesa::ElementId{0},
|
fesa::ElementId{0},
|
||||||
@@ -149,7 +149,7 @@ TEST(LinearStaticAnalysis, SolvesHandCalculatedAxialBeamInNodeIdOrder) {
|
|||||||
ASSERT_TRUE(run.results.has_value());
|
ASSERT_TRUE(run.results.has_value());
|
||||||
EXPECT_TRUE(run.diagnostics.empty());
|
EXPECT_TRUE(run.diagnostics.empty());
|
||||||
EXPECT_TRUE(fesa::validate_result_database(*run.results).succeeded);
|
EXPECT_TRUE(fesa::validate_result_database(*run.results).succeeded);
|
||||||
EXPECT_EQ(run.results->schema_version, "1.0.0");
|
EXPECT_EQ(run.results->schema_version, "2.0.0");
|
||||||
ASSERT_EQ(run.results->steps.size(), 1);
|
ASSERT_EQ(run.results->steps.size(), 1);
|
||||||
EXPECT_EQ(run.results->steps[0].name, "Load");
|
EXPECT_EQ(run.results->steps[0].name, "Load");
|
||||||
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
|
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
|
||||||
@@ -214,4 +214,49 @@ TEST(LinearStaticAnalysis, SolvesAllConstrainedSystemWithoutPardiso) {
|
|||||||
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
|
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, PreservesOriginsConnectivityAndBeamRecovery) {
|
||||||
|
const fesa::Domain domain = build_axial_domain(false);
|
||||||
|
|
||||||
|
const fesa::AnalysisRunResult run =
|
||||||
|
fesa::LinearStaticAnalysis{}.run(domain);
|
||||||
|
|
||||||
|
ASSERT_TRUE(run.succeeded);
|
||||||
|
ASSERT_TRUE(run.results.has_value());
|
||||||
|
const fesa::ResultFrame& frame = run.results->steps[0].frames[0];
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.nodal.origins,
|
||||||
|
(std::vector<fesa::EntityOrigin>{
|
||||||
|
{"BeamPart", "Beam-1", 2},
|
||||||
|
{"BeamPart", "Beam-1", 1},
|
||||||
|
}));
|
||||||
|
|
||||||
|
ASSERT_EQ(frame.element.beams.size(), 1U);
|
||||||
|
const fesa::BeamElementFrame& beam = frame.element.beams[0];
|
||||||
|
EXPECT_EQ(beam.element, fesa::ElementId{0});
|
||||||
|
EXPECT_EQ(
|
||||||
|
beam.origin,
|
||||||
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1}));
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.x, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.y, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.z, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.x, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.y, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.z, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.x, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.y, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.z, 1.0);
|
||||||
|
EXPECT_EQ(beam.end_results[0].end_node, fesa::NodeId{20});
|
||||||
|
EXPECT_EQ(beam.end_results[1].end_node, fesa::NodeId{4});
|
||||||
|
EXPECT_DOUBLE_EQ(beam.end_results[0].xi, -1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.end_results[1].xi, 1.0);
|
||||||
|
for (const fesa::BeamSectionResult& end : beam.end_results) {
|
||||||
|
EXPECT_NEAR(end.section_strain[0], 0.05, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.section_force[0], 10.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.centroid_sigma_xx, 5.0, 1.0e-12);
|
||||||
|
ASSERT_EQ(end.sigma_xx.size(), 2U);
|
||||||
|
EXPECT_NEAR(end.sigma_xx[0], 5.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.sigma_xx[1], 5.0, 1.0e-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ constexpr double kShearAreaZ = 0.2;
|
|||||||
fesa::Beam3D2Input make_x_axis_input(const double length) {
|
fesa::Beam3D2Input make_x_axis_input(const double length) {
|
||||||
return {
|
return {
|
||||||
{{{0.0, 0.0, 0.0}, {length, 0.0, 0.0}}},
|
{{{0.0, 0.0, 0.0}, {length, 0.0, 0.0}}},
|
||||||
|
{fesa::NodeId{1}, fesa::NodeId{2}},
|
||||||
{fesa::MaterialId{1}, "elastic", kYoung, kPoisson},
|
{fesa::MaterialId{1}, "elastic", kYoung, kPoisson},
|
||||||
{
|
{
|
||||||
fesa::SectionId{1},
|
fesa::SectionId{1},
|
||||||
@@ -369,4 +370,191 @@ TEST(Beam3D2, PreservesGlobalEnergyUnderRigidCoordinateRotation) {
|
|||||||
4096.0 * std::numeric_limits<double>::epsilon());
|
4096.0 * std::numeric_limits<double>::epsilon());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, RecoversPureAxialStrainAndForceAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[6] = 0.4;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
EXPECT_DOUBLE_EQ(results[0].xi, -1.0);
|
||||||
|
EXPECT_EQ(results[0].end_node, fesa::NodeId{101});
|
||||||
|
EXPECT_DOUBLE_EQ(results[1].xi, 1.0);
|
||||||
|
EXPECT_EQ(results[1].end_node, fesa::NodeId{202});
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[0], 0.2);
|
||||||
|
expect_relative_near(result.section_force[0], 16.8);
|
||||||
|
expect_relative_near(result.centroid_sigma_xx, 42.0);
|
||||||
|
for (std::size_t component = 1; component < 6; ++component) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(result.sigma_xx.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, RecoversPureTorsionAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[9] = 0.6;
|
||||||
|
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[3], 0.3);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[3],
|
||||||
|
shear_modulus * kTorsionJ * 0.3);
|
||||||
|
for (const std::size_t component :
|
||||||
|
std::array<std::size_t, 5>{0, 1, 2, 4, 5}) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, UsesReducedIntegrationPointForShearAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[7] = 1.2;
|
||||||
|
displacement[5] = 0.2;
|
||||||
|
displacement[11] = 0.6;
|
||||||
|
displacement[8] = -0.4;
|
||||||
|
displacement[4] = 0.1;
|
||||||
|
displacement[10] = 0.5;
|
||||||
|
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[1], 0.2);
|
||||||
|
expect_relative_near(result.section_strain[2], 0.1);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[1],
|
||||||
|
shear_modulus * kShearAreaY * 0.2);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[2],
|
||||||
|
shear_modulus * kShearAreaZ * 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, UsesBeamFrameForGlobalDisplacements) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.coordinates = {{{0.0, 0.0, 0.0}, {0.0, 2.0, 0.0}}};
|
||||||
|
input.section.orientation = {0.0, 0.0, 1.0};
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[7] = 0.4;
|
||||||
|
displacement[10] = 0.6;
|
||||||
|
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[0], 0.2);
|
||||||
|
expect_relative_near(result.section_force[0], 16.8);
|
||||||
|
expect_relative_near(result.section_strain[3], 0.3);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[3],
|
||||||
|
shear_modulus * kTorsionJ * 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SectionForce, RecoversPureBendingAboutLocalYAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[4] = -0.4;
|
||||||
|
displacement[10] = 0.4;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[4], 0.4);
|
||||||
|
expect_relative_near(result.section_force[4], 2.52);
|
||||||
|
for (const std::size_t component :
|
||||||
|
std::array<std::size_t, 5>{0, 1, 2, 3, 5}) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SectionForce, RecoversPureBendingAboutLocalZAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[5] = 0.25;
|
||||||
|
displacement[11] = -0.25;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[5], -0.25);
|
||||||
|
expect_relative_near(result.section_force[5], -2.625);
|
||||||
|
for (const std::size_t component :
|
||||||
|
std::array<std::size_t, 5>{0, 1, 2, 3, 4}) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SectionForce, PreservesBiaxialBendingSignsAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[4] = -0.4;
|
||||||
|
displacement[10] = 0.4;
|
||||||
|
displacement[5] = 0.25;
|
||||||
|
displacement[11] = -0.25;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_force[4], 2.52);
|
||||||
|
expect_relative_near(result.section_force[5], -2.625);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CentroidStress, UsesAxialStressAndPreservesRecoveryPointOrder) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[6] = 0.2;
|
||||||
|
displacement[4] = -0.2;
|
||||||
|
displacement[10] = 0.2;
|
||||||
|
displacement[5] = 0.3;
|
||||||
|
displacement[11] = -0.3;
|
||||||
|
const std::array<std::array<double, 2>, 3> recovery_points{{
|
||||||
|
{2.0, 3.0},
|
||||||
|
{-1.0, 4.0},
|
||||||
|
{5.0, -2.0},
|
||||||
|
}};
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(
|
||||||
|
input,
|
||||||
|
displacement,
|
||||||
|
recovery_points);
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.centroid_sigma_xx, 21.0);
|
||||||
|
ASSERT_EQ(result.sigma_xx.size(), recovery_points.size());
|
||||||
|
expect_relative_near(result.sigma_xx[0], 273.0);
|
||||||
|
expect_relative_near(result.sigma_xx[1], 126.0);
|
||||||
|
expect_relative_near(result.sigma_xx[2], 252.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
|||||||
|
|
||||||
ASSERT_TRUE(result.deck.has_value());
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
EXPECT_TRUE(result.diagnostics.empty());
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.input_fingerprint,
|
||||||
|
"fnv1a64:73f31da4615f09b3");
|
||||||
EXPECT_TRUE(result.deck->parts.empty());
|
EXPECT_TRUE(result.deck->parts.empty());
|
||||||
EXPECT_FALSE(result.deck->assembly.has_value());
|
EXPECT_FALSE(result.deck->assembly.has_value());
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ std::array<double, 6> zeros() {
|
|||||||
fesa::ResultDatabase valid_database() {
|
fesa::ResultDatabase valid_database() {
|
||||||
fesa::NodalFrame nodal{
|
fesa::NodalFrame nodal{
|
||||||
{fesa::NodeId{0}, fesa::NodeId{1}},
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
{
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 10},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 20},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
zeros(),
|
zeros(),
|
||||||
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
|
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
|
||||||
@@ -26,9 +30,41 @@ fesa::ResultDatabase valid_database() {
|
|||||||
zeros(),
|
zeros(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
fesa::ResultFrame frame{1.0, std::move(nodal), {}};
|
fesa::BeamElementFrame beam{
|
||||||
|
fesa::ElementId{3},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 30},
|
||||||
|
{
|
||||||
|
fesa::Vec3{1.0, 0.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 0.0, 1.0},
|
||||||
|
},
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
-1.0,
|
||||||
|
fesa::NodeId{0},
|
||||||
|
zeros(),
|
||||||
|
zeros(),
|
||||||
|
0.0,
|
||||||
|
{1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
1.0,
|
||||||
|
fesa::NodeId{1},
|
||||||
|
zeros(),
|
||||||
|
zeros(),
|
||||||
|
0.0,
|
||||||
|
{2.0},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
fesa::ResultFrame frame{
|
||||||
|
1.0,
|
||||||
|
std::move(nodal),
|
||||||
|
{{std::move(beam)}},
|
||||||
|
{},
|
||||||
|
};
|
||||||
fesa::ResultStep step{"Load", {std::move(frame)}};
|
fesa::ResultStep step{"Load", {std::move(frame)}};
|
||||||
return {"1.0.0", {std::move(step)}};
|
return {"2.0.0", {std::move(step)}};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_diagnostic(
|
bool has_diagnostic(
|
||||||
@@ -72,6 +108,17 @@ TEST(NodalFrame, RejectsReactionSizeMismatch) {
|
|||||||
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
|
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, RejectsNodeOriginSizeMismatch) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0].nodal.origins.pop_back();
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
status, "results.nodal_origin_size_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(NodalFrame, RejectsDuplicateNodeId) {
|
TEST(NodalFrame, RejectsDuplicateNodeId) {
|
||||||
auto database = valid_database();
|
auto database = valid_database();
|
||||||
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
|
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
|
||||||
@@ -135,4 +182,110 @@ TEST(ResultDatabase, RejectsNonfiniteFrameTime) {
|
|||||||
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, DeclaresCoordinatesAndComponentOrdering) {
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::coordinate_system,
|
||||||
|
fesa::FieldCoordinateSystem::global);
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::displacement_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::reaction_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::coordinate_system,
|
||||||
|
fesa::FieldCoordinateSystem::element_local);
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::section_strain_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||||
|
"kappa_z"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::section_force_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"N", "Vy", "Vz", "T", "My", "Mz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::axial_stress_component,
|
||||||
|
std::string_view{"sigma_xx"});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, AcceptsFiniteConnectedBeamResults) {
|
||||||
|
const auto status = fesa::validate_result_database(valid_database());
|
||||||
|
|
||||||
|
EXPECT_TRUE(status.succeeded);
|
||||||
|
EXPECT_TRUE(status.diagnostics.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsDuplicateElementId) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& beams = database.steps[0].frames[0].element.beams;
|
||||||
|
beams.push_back(beams[0]);
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_element_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsDuplicateEndNode) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& ends = database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results;
|
||||||
|
ends[1].end_node = ends[0].end_node;
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_beam_end_node"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsWrongEndConnectivity) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[0].xi = 1.0;
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsEndNodeAbsentFromNodalFrame) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[1].end_node = fesa::NodeId{99};
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsNonfiniteLocalFrameAndSectionValue) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& beam = database.steps[0].frames[0].element.beams[0];
|
||||||
|
beam.local_frame.ey.y = std::numeric_limits<double>::infinity();
|
||||||
|
beam.end_results[1].section_force[4] =
|
||||||
|
std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsMismatchedRecoveryPointCount) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[1].sigma_xx.push_back(3.0);
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
status, "results.recovery_point_count_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user