ANYsolver 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anysolver/__init__.py +631 -0
- anysolver/anystructure_fem_mode.py +942 -0
- anysolver/arc_length.py +758 -0
- anysolver/assembly.py +950 -0
- anysolver/baselines.py +303 -0
- anysolver/beam_shell_verification.py +4276 -0
- anysolver/beam_validity.py +110 -0
- anysolver/benchmarks.py +495 -0
- anysolver/boundary.py +476 -0
- anysolver/buckling.py +442 -0
- anysolver/buckling_validity.py +91 -0
- anysolver/capacity_workflow.py +350 -0
- anysolver/cases.py +173 -0
- anysolver/composite_strip_verification.py +297 -0
- anysolver/contact.py +3461 -0
- anysolver/corotational.py +371 -0
- anysolver/cylinder_benchmarks.py +364 -0
- anysolver/dynamics.py +723 -0
- anysolver/element_qualification.py +432 -0
- anysolver/elements.py +2724 -0
- anysolver/external_references.py +369 -0
- anysolver/fe_core.py +327 -0
- anysolver/fracture.py +551 -0
- anysolver/imperfections.py +390 -0
- anysolver/jit_compiler.py +108 -0
- anysolver/kernel_warmup.py +180 -0
- anysolver/linalg.py +760 -0
- anysolver/mass_properties.py +179 -0
- anysolver/material_curves.py +231 -0
- anysolver/matrix_assembly.py +558 -0
- anysolver/mesh_gen.py +1065 -0
- anysolver/mesh_load_bc_verification.py +609 -0
- anysolver/modal.py +282 -0
- anysolver/nonlinear.py +300 -0
- anysolver/nonlinear_performance.py +920 -0
- anysolver/nonlinear_performance_batch_b.py +592 -0
- anysolver/nonlinear_performance_batch_c.py +506 -0
- anysolver/nonlinear_performance_bootstrap.py +120 -0
- anysolver/nonlinear_reduced_assembly.py +760 -0
- anysolver/nonlinear_static.py +1518 -0
- anysolver/plasticity.py +419 -0
- anysolver/plasticity_qualification.py +314 -0
- anysolver/production_readiness.py +304 -0
- anysolver/quality_control.py +1497 -0
- anysolver/recovery.py +505 -0
- anysolver/recovery_policy.py +205 -0
- anysolver/reference_cases.py +425 -0
- anysolver/results.py +503 -0
- anysolver/runtime.py +9030 -0
- anysolver/s4_validity.py +326 -0
- anysolver/sesam_fem/__init__.py +55 -0
- anysolver/sesam_fem/__main__.py +80 -0
- anysolver/sesam_fem/diagnostics.py +65 -0
- anysolver/sesam_fem/document.py +814 -0
- anysolver/sesam_fem/exporter.py +84 -0
- anysolver/sesam_fem/importer.py +365 -0
- anysolver/sesam_fem/records.py +257 -0
- anysolver/sesam_fem/schema.py +107 -0
- anysolver/sesam_fem/sif_importer.py +397 -0
- anysolver/sesam_fem/validation.py +62 -0
- anysolver/shell_benchmarks.py +367 -0
- anysolver/test_cases.py +610 -0
- anysolver/validation.py +416 -0
- anysolver/vectorized_nonlinear.py +334 -0
- anysolver/vectorized_stiffness.py +571 -0
- anysolver-0.1.0.dist-info/METADATA +165 -0
- anysolver-0.1.0.dist-info/RECORD +70 -0
- anysolver-0.1.0.dist-info/WHEEL +5 -0
- anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
- anysolver-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import math
|
|
3
|
+
from collections import Counter, defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Sequence
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .importer import import_sesam_fem
|
|
10
|
+
from .records import read_raw_records
|
|
11
|
+
from .schema import get_element_spec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SesamStressResult:
|
|
16
|
+
path: str
|
|
17
|
+
nodes: dict[int, tuple[float, float, float]]
|
|
18
|
+
element_nodes: dict[int, tuple[int, ...]]
|
|
19
|
+
components: tuple[str, ...]
|
|
20
|
+
nodal_stress: dict[int, tuple[float, ...]]
|
|
21
|
+
element_stress: dict[int, tuple[float, ...]]
|
|
22
|
+
units: str = "Pa"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _mean_rvstress_membrane_components(
|
|
26
|
+
values: Sequence[float],
|
|
27
|
+
*,
|
|
28
|
+
type_code: int | None = None,
|
|
29
|
+
) -> tuple[float, float, float] | None:
|
|
30
|
+
"""Return representative local membrane ``SIGXX, SIGYY, TAUXY`` values.
|
|
31
|
+
|
|
32
|
+
SESAM RVSTRESS shell payloads in the reference cases use two layouts:
|
|
33
|
+
lower/upper blocks of 3-component stress tuples for first-order shells and
|
|
34
|
+
5-column result-point rows for second-order T6/Q8 shells. Only the first three values are
|
|
35
|
+
membrane stress components; remaining columns are result metadata/derived
|
|
36
|
+
quantities and must not be averaged into the stress tensor.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
payload = [float(value) for value in values if math.isfinite(float(value))]
|
|
40
|
+
triplets: list[tuple[float, float, float]] = []
|
|
41
|
+
if type_code in {26, 28} and len(payload) % 5 == 0:
|
|
42
|
+
triplets = [
|
|
43
|
+
(payload[index], payload[index + 1], payload[index + 2])
|
|
44
|
+
for index in range(0, len(payload), 5)
|
|
45
|
+
]
|
|
46
|
+
elif len(payload) % 6 == 0:
|
|
47
|
+
triplets = [
|
|
48
|
+
(payload[index], payload[index + 1], payload[index + 2])
|
|
49
|
+
for index in range(0, len(payload), 3)
|
|
50
|
+
]
|
|
51
|
+
elif len(payload) % 5 == 0:
|
|
52
|
+
triplets = [
|
|
53
|
+
(payload[index], payload[index + 1], payload[index + 2])
|
|
54
|
+
for index in range(0, len(payload), 5)
|
|
55
|
+
]
|
|
56
|
+
elif len(payload) % 3 == 0:
|
|
57
|
+
triplets = [
|
|
58
|
+
(payload[index], payload[index + 1], payload[index + 2])
|
|
59
|
+
for index in range(0, len(payload), 3)
|
|
60
|
+
]
|
|
61
|
+
if not triplets:
|
|
62
|
+
return None
|
|
63
|
+
return (
|
|
64
|
+
sum(item[0] for item in triplets) / len(triplets),
|
|
65
|
+
sum(item[1] for item in triplets) / len(triplets),
|
|
66
|
+
sum(item[2] for item in triplets) / len(triplets),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _element_corner_node_ids(element, type_code: int) -> tuple[int, ...]:
|
|
71
|
+
if type_code == 28 and len(element.node_ids) >= 8:
|
|
72
|
+
return (element.node_ids[0], element.node_ids[2], element.node_ids[4], element.node_ids[6])
|
|
73
|
+
if type_code == 26 and len(element.node_ids) >= 6:
|
|
74
|
+
return (element.node_ids[0], element.node_ids[2], element.node_ids[4])
|
|
75
|
+
spec = get_element_spec(type_code)
|
|
76
|
+
if spec is None:
|
|
77
|
+
return tuple(element.node_ids)
|
|
78
|
+
corner_count = spec.pressure_node_count or spec.node_count
|
|
79
|
+
return tuple(element.node_ids[:corner_count])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _normalise_array(vector: np.ndarray) -> np.ndarray | None:
|
|
83
|
+
length = float(np.linalg.norm(vector))
|
|
84
|
+
if length <= 1.0e-12:
|
|
85
|
+
return None
|
|
86
|
+
return vector / length
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _element_geometry_normal(points: Sequence[tuple[float, float, float]]) -> np.ndarray | None:
|
|
90
|
+
p0 = np.asarray(points[0], dtype=float)
|
|
91
|
+
for index in range(1, len(points) - 1):
|
|
92
|
+
candidate = _normalise_array(
|
|
93
|
+
np.cross(
|
|
94
|
+
np.asarray(points[index], dtype=float) - p0,
|
|
95
|
+
np.asarray(points[index + 1], dtype=float) - p0,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
if candidate is not None:
|
|
99
|
+
return candidate
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _project_axis_to_plane(axis: np.ndarray, normal: np.ndarray) -> np.ndarray | None:
|
|
104
|
+
return _normalise_array(axis - np.dot(axis, normal) * normal)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _element_reference(document, element):
|
|
108
|
+
reference = document.element_references.get(element.element_id)
|
|
109
|
+
if reference is not None:
|
|
110
|
+
return reference
|
|
111
|
+
if len(element.raw_values) > 1:
|
|
112
|
+
internal_id = int(round(element.raw_values[1]))
|
|
113
|
+
return document.element_references.get(internal_id)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _mean_transform_axes(document, transform_ids: Sequence[int]) -> tuple[np.ndarray, np.ndarray, np.ndarray | None] | None:
|
|
118
|
+
rows_x: list[np.ndarray] = []
|
|
119
|
+
rows_y: list[np.ndarray] = []
|
|
120
|
+
rows_z: list[np.ndarray] = []
|
|
121
|
+
for transform_id in transform_ids:
|
|
122
|
+
transform = document.coordinate_transforms.get(transform_id)
|
|
123
|
+
if transform is None:
|
|
124
|
+
continue
|
|
125
|
+
matrix = np.asarray(transform.matrix, dtype=float)
|
|
126
|
+
if matrix.shape != (3, 3):
|
|
127
|
+
continue
|
|
128
|
+
rows_x.append(matrix[0])
|
|
129
|
+
rows_y.append(matrix[1])
|
|
130
|
+
rows_z.append(matrix[2])
|
|
131
|
+
if not rows_x or not rows_y:
|
|
132
|
+
return None
|
|
133
|
+
x_axis = _normalise_array(np.sum(rows_x, axis=0))
|
|
134
|
+
y_axis = _normalise_array(np.sum(rows_y, axis=0))
|
|
135
|
+
z_axis = _normalise_array(np.sum(rows_z, axis=0)) if rows_z else None
|
|
136
|
+
if x_axis is None or y_axis is None:
|
|
137
|
+
return None
|
|
138
|
+
return x_axis, y_axis, z_axis
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _mean_unit_vector_axis(document, transform_ids: Sequence[int]) -> np.ndarray | None:
|
|
142
|
+
vectors: list[np.ndarray] = []
|
|
143
|
+
for transform_id in transform_ids:
|
|
144
|
+
unit_vector = document.unit_vectors.get(transform_id)
|
|
145
|
+
if unit_vector is None:
|
|
146
|
+
continue
|
|
147
|
+
vectors.append(np.asarray(unit_vector.vector, dtype=float))
|
|
148
|
+
if not vectors:
|
|
149
|
+
return None
|
|
150
|
+
return _normalise_array(np.sum(vectors, axis=0))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _explicit_shell_local_frame(document, element, normal: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None:
|
|
154
|
+
reference = _element_reference(document, element)
|
|
155
|
+
if reference is None:
|
|
156
|
+
return None
|
|
157
|
+
transform_ids = reference.nodal_transform_ids or (() if reference.transform_id is None else (reference.transform_id,))
|
|
158
|
+
if not transform_ids:
|
|
159
|
+
return None
|
|
160
|
+
axes = _mean_transform_axes(document, transform_ids)
|
|
161
|
+
if axes is None:
|
|
162
|
+
return None
|
|
163
|
+
raw_x, raw_y, raw_z = axes
|
|
164
|
+
if raw_z is not None and np.dot(raw_z, normal) < 0.0:
|
|
165
|
+
normal = -normal
|
|
166
|
+
|
|
167
|
+
x_axis = _project_axis_to_plane(raw_x, normal)
|
|
168
|
+
y_axis = _project_axis_to_plane(raw_y, normal)
|
|
169
|
+
if x_axis is None and y_axis is None:
|
|
170
|
+
return None
|
|
171
|
+
if x_axis is None:
|
|
172
|
+
x_axis = _normalise_array(np.cross(y_axis, normal))
|
|
173
|
+
if x_axis is None:
|
|
174
|
+
return None
|
|
175
|
+
if y_axis is None or abs(float(np.dot(x_axis, y_axis))) > 1.0e-6:
|
|
176
|
+
y_axis = _normalise_array(raw_y - np.dot(raw_y, normal) * normal - np.dot(raw_y, x_axis) * x_axis)
|
|
177
|
+
if y_axis is None:
|
|
178
|
+
y_axis = _normalise_array(np.cross(normal, x_axis))
|
|
179
|
+
if y_axis is None:
|
|
180
|
+
return None
|
|
181
|
+
if np.dot(np.cross(x_axis, y_axis), normal) < 0.0:
|
|
182
|
+
y_axis = -y_axis
|
|
183
|
+
return x_axis, y_axis, normal
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _unit_vector_shell_local_frame(document, element, normal: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None:
|
|
187
|
+
reference = _element_reference(document, element)
|
|
188
|
+
if reference is None:
|
|
189
|
+
return None
|
|
190
|
+
transform_ids = reference.nodal_transform_ids or (() if reference.transform_id is None else (reference.transform_id,))
|
|
191
|
+
if not transform_ids:
|
|
192
|
+
return None
|
|
193
|
+
raw_y = _mean_unit_vector_axis(document, transform_ids)
|
|
194
|
+
if raw_y is None:
|
|
195
|
+
return None
|
|
196
|
+
y_axis = _project_axis_to_plane(raw_y, normal)
|
|
197
|
+
if y_axis is None:
|
|
198
|
+
return None
|
|
199
|
+
x_axis = _normalise_array(np.cross(y_axis, normal))
|
|
200
|
+
if x_axis is None:
|
|
201
|
+
return None
|
|
202
|
+
if np.dot(np.cross(x_axis, y_axis), normal) < 0.0:
|
|
203
|
+
x_axis = -x_axis
|
|
204
|
+
return x_axis, y_axis, normal
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _element_local_frame(document, element) -> tuple[tuple[float,...], tuple[float,...], tuple[float,...]] | None:
|
|
208
|
+
spec = get_element_spec(element.type_code)
|
|
209
|
+
if spec is None:
|
|
210
|
+
return None
|
|
211
|
+
corner_ids = _element_corner_node_ids(element, element.type_code)
|
|
212
|
+
points = [document.nodes[node_id].coordinates for node_id in corner_ids if node_id in document.nodes]
|
|
213
|
+
if len(points) < 3:
|
|
214
|
+
return None
|
|
215
|
+
normal = _element_geometry_normal(points)
|
|
216
|
+
if normal is None:
|
|
217
|
+
return None
|
|
218
|
+
if spec.is_shell:
|
|
219
|
+
explicit_frame = _explicit_shell_local_frame(document, element, normal)
|
|
220
|
+
if explicit_frame is not None:
|
|
221
|
+
x_axis, y_axis, normal = explicit_frame
|
|
222
|
+
return tuple(x_axis.tolist()), tuple(y_axis.tolist()), tuple(normal.tolist())
|
|
223
|
+
unit_vector_frame = _unit_vector_shell_local_frame(document, element, normal)
|
|
224
|
+
if unit_vector_frame is not None:
|
|
225
|
+
x_axis, y_axis, normal = unit_vector_frame
|
|
226
|
+
return tuple(x_axis.tolist()), tuple(y_axis.tolist()), tuple(normal.tolist())
|
|
227
|
+
|
|
228
|
+
p0 = np.asarray(points[0], dtype=float)
|
|
229
|
+
p1 = np.asarray(points[1], dtype=float)
|
|
230
|
+
# In SESAM shell result records without explicit transforms, the in-plane
|
|
231
|
+
# local y direction follows the first element edge. The local x direction
|
|
232
|
+
# is completed from y x normal so local membrane components can be rotated
|
|
233
|
+
# into the global tensor before panel-axis projection.
|
|
234
|
+
y_axis = _normalise_array(p1 - p0)
|
|
235
|
+
if y_axis is None:
|
|
236
|
+
return None
|
|
237
|
+
x_axis = _normalise_array(np.cross(y_axis, normal))
|
|
238
|
+
if x_axis is None:
|
|
239
|
+
return None
|
|
240
|
+
return tuple(x_axis.tolist()), tuple(y_axis.tolist()), tuple(normal.tolist())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _local_membrane_to_global_components(
|
|
244
|
+
document,
|
|
245
|
+
element,
|
|
246
|
+
local_stress: tuple[float, float, float],
|
|
247
|
+
) -> tuple[float, float, float, float, float, float] | None:
|
|
248
|
+
frame = _element_local_frame(document, element)
|
|
249
|
+
if frame is None:
|
|
250
|
+
return None
|
|
251
|
+
local_x, local_y, _normal = frame
|
|
252
|
+
sx, sy, txy = local_stress
|
|
253
|
+
tensor = np.zeros((3, 3), dtype=float)
|
|
254
|
+
x = np.asarray(local_x, dtype=float)
|
|
255
|
+
y = np.asarray(local_y, dtype=float)
|
|
256
|
+
tensor += sx * np.outer(x, x)
|
|
257
|
+
tensor += sy * np.outer(y, y)
|
|
258
|
+
tensor += txy * (np.outer(x, y) + np.outer(y, x))
|
|
259
|
+
return (
|
|
260
|
+
float(tensor[0, 0]),
|
|
261
|
+
float(tensor[1, 1]),
|
|
262
|
+
float(tensor[2, 2]),
|
|
263
|
+
float(tensor[0, 1]),
|
|
264
|
+
float(tensor[1, 2]),
|
|
265
|
+
float(tensor[2, 0]),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def read_sesam_sif_stress(
|
|
270
|
+
path: str | os.PathLike[str],
|
|
271
|
+
load_case: int | None = None,
|
|
272
|
+
) -> SesamStressResult:
|
|
273
|
+
"""Read RVSTRESS shell results as FRD-like global stress tensors."""
|
|
274
|
+
|
|
275
|
+
path_text = str(path)
|
|
276
|
+
result = import_sesam_fem(path_text, build_model=False, strict=False)
|
|
277
|
+
document = result.document
|
|
278
|
+
|
|
279
|
+
nodal_sums: dict[int, np.ndarray] = defaultdict(lambda: np.zeros(6, dtype=float))
|
|
280
|
+
nodal_counts: defaultdict[int, int] = defaultdict(int)
|
|
281
|
+
element_stress: dict[int, tuple[float, ...]] = {}
|
|
282
|
+
|
|
283
|
+
raw_records = read_raw_records(Path(path_text), strict=False)
|
|
284
|
+
internal_to_external: dict[int, int] = {}
|
|
285
|
+
for record in raw_records:
|
|
286
|
+
if record.name != "GELMNT1" or len(record.numeric_fields) < 2:
|
|
287
|
+
continue
|
|
288
|
+
external_id = int(round(record.numeric_fields[0]))
|
|
289
|
+
internal_id = int(round(record.numeric_fields[1]))
|
|
290
|
+
internal_to_external[internal_id] = external_id
|
|
291
|
+
|
|
292
|
+
if load_case is None:
|
|
293
|
+
# Lock onto the first load case present so element and nodal stresses
|
|
294
|
+
# stay from one consistent case instead of mixing every case in the
|
|
295
|
+
# file (element stresses would keep the last case per element while
|
|
296
|
+
# nodal averages blended all cases).
|
|
297
|
+
for record in raw_records:
|
|
298
|
+
if record.name == "RVSTRESS" and record.numeric_fields and len(record.numeric_fields) >= 8:
|
|
299
|
+
load_case = int(round(record.numeric_fields[1]))
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
for record in raw_records:
|
|
303
|
+
if record.name != "RVSTRESS" or not record.numeric_fields or len(record.numeric_fields) < 8:
|
|
304
|
+
continue
|
|
305
|
+
values = record.numeric_fields
|
|
306
|
+
if load_case is not None:
|
|
307
|
+
record_load_case = int(round(values[1]))
|
|
308
|
+
if record_load_case != load_case:
|
|
309
|
+
continue
|
|
310
|
+
result_element_id = int(values[2]) if values[2].is_integer() else int(round(values[2]))
|
|
311
|
+
element_id = internal_to_external.get(result_element_id, result_element_id)
|
|
312
|
+
type_code = int(values[4]) if values[4].is_integer() else int(round(values[4]))
|
|
313
|
+
|
|
314
|
+
element = document.elements.get(element_id)
|
|
315
|
+
if element is None:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
spec = get_element_spec(type_code)
|
|
319
|
+
if spec is None or not spec.is_shell:
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
local_stress = _mean_rvstress_membrane_components(values[5:], type_code=type_code)
|
|
323
|
+
if local_stress is None:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
global_components = _local_membrane_to_global_components(document, element, local_stress)
|
|
327
|
+
if global_components is None:
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
element_stress[element_id] = global_components
|
|
331
|
+
vector = np.asarray(global_components, dtype=float)
|
|
332
|
+
for node_id in element.node_ids:
|
|
333
|
+
nodal_sums[node_id] += vector
|
|
334
|
+
nodal_counts[node_id] += 1
|
|
335
|
+
|
|
336
|
+
nodal_stress = {
|
|
337
|
+
node_id: tuple((values / max(nodal_counts[node_id], 1)).tolist())
|
|
338
|
+
for node_id, values in nodal_sums.items()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
element_nodes = {}
|
|
342
|
+
for element_id, element in document.elements.items():
|
|
343
|
+
spec = get_element_spec(element.type_code)
|
|
344
|
+
if spec is not None and spec.is_shell:
|
|
345
|
+
element_nodes[element_id] = tuple(element.node_ids)
|
|
346
|
+
|
|
347
|
+
nodes = {node_id: tuple(node.coordinates) for node_id, node in document.nodes.items()}
|
|
348
|
+
|
|
349
|
+
return SesamStressResult(
|
|
350
|
+
path=path_text,
|
|
351
|
+
nodes=nodes,
|
|
352
|
+
element_nodes=element_nodes,
|
|
353
|
+
components=("SXX", "SYY", "SZZ", "SXY", "SYZ", "SZX"),
|
|
354
|
+
nodal_stress=nodal_stress,
|
|
355
|
+
element_stress=element_stress,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def read_sesam_sif_summary(path: str | os.PathLike[str]) -> dict[str, Any]:
|
|
360
|
+
"""Return lightweight metadata for a SESAM SIF/FEM-style file."""
|
|
361
|
+
|
|
362
|
+
path_text = str(path)
|
|
363
|
+
counts: Counter[str] = Counter()
|
|
364
|
+
load_cases = set()
|
|
365
|
+
load_case_names = {}
|
|
366
|
+
for record in read_raw_records(Path(path_text), strict=False):
|
|
367
|
+
counts[record.name] += 1
|
|
368
|
+
if record.name == "RVSTRESS" and len(record.numeric_fields) >= 8:
|
|
369
|
+
load_cases.add(int(round(record.numeric_fields[1])))
|
|
370
|
+
elif record.name in ("TDLOAD", "TDRESREF") and len(record.numeric_fields) >= 2 and record.text_fields:
|
|
371
|
+
lc_id = int(round(record.numeric_fields[1]))
|
|
372
|
+
load_case_names[lc_id] = record.text_fields[0]
|
|
373
|
+
|
|
374
|
+
result = import_sesam_fem(path_text, build_model=False, strict=False)
|
|
375
|
+
document = result.document
|
|
376
|
+
|
|
377
|
+
shell_count = 0
|
|
378
|
+
beam_count = 0
|
|
379
|
+
for element in document.elements.values():
|
|
380
|
+
spec = get_element_spec(element.type_code)
|
|
381
|
+
if spec:
|
|
382
|
+
if spec.is_shell:
|
|
383
|
+
shell_count += 1
|
|
384
|
+
elif spec.is_beam:
|
|
385
|
+
beam_count += 1
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
"path": path_text,
|
|
389
|
+
"format": "sesam_sif",
|
|
390
|
+
"nodes": len(document.nodes),
|
|
391
|
+
"elements": len(document.elements),
|
|
392
|
+
"shell_elements": shell_count,
|
|
393
|
+
"beam_elements": beam_count,
|
|
394
|
+
"records": dict(counts),
|
|
395
|
+
"load_cases": sorted(list(load_cases)),
|
|
396
|
+
"load_case_names": load_case_names,
|
|
397
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Layer C validation checks for SESAM FEM documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .diagnostics import FemDiagnostic
|
|
6
|
+
from .document import SesamFemDocument
|
|
7
|
+
from .schema import KNOWN_UNSUPPORTED_STRUCTURAL_RECORDS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_sesam_fem_document(document: SesamFemDocument) -> tuple[FemDiagnostic, ...]:
|
|
11
|
+
"""Validate document consistency without mutating it."""
|
|
12
|
+
|
|
13
|
+
diagnostics: list[FemDiagnostic] = []
|
|
14
|
+
if not document.raw_records:
|
|
15
|
+
diagnostics.append(FemDiagnostic("FEM003", "document contains no records"))
|
|
16
|
+
return tuple(diagnostics)
|
|
17
|
+
if document.raw_records[-1].name != "IEND":
|
|
18
|
+
diagnostics.append(FemDiagnostic("FEM003", "formatted FEM file is missing final IEND record"))
|
|
19
|
+
|
|
20
|
+
node_ids = set(document.nodes)
|
|
21
|
+
for element in document.elements.values():
|
|
22
|
+
missing_nodes = [node_id for node_id in element.node_ids if node_id not in node_ids]
|
|
23
|
+
if missing_nodes:
|
|
24
|
+
diagnostics.append(
|
|
25
|
+
FemDiagnostic(
|
|
26
|
+
"FEM105",
|
|
27
|
+
f"element {element.element_id} references missing nodes {missing_nodes}",
|
|
28
|
+
context={"element_id": element.element_id, "missing_nodes": missing_nodes},
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
if element.material_id not in (None, 0) and element.material_id not in document.materials:
|
|
32
|
+
diagnostics.append(
|
|
33
|
+
FemDiagnostic(
|
|
34
|
+
"FEM106",
|
|
35
|
+
f"element {element.element_id} references undefined material {element.material_id}",
|
|
36
|
+
severity="warning",
|
|
37
|
+
context={"element_id": element.element_id, "material_id": element.material_id},
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
if element.section_id not in (None, 0) and element.section_id not in document.sections:
|
|
41
|
+
diagnostics.append(
|
|
42
|
+
FemDiagnostic(
|
|
43
|
+
"FEM107",
|
|
44
|
+
f"element {element.element_id} references undefined section {element.section_id}",
|
|
45
|
+
severity="warning",
|
|
46
|
+
context={"element_id": element.element_id, "section_id": element.section_id},
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
for record in document.raw_records:
|
|
51
|
+
if record.name in KNOWN_UNSUPPORTED_STRUCTURAL_RECORDS:
|
|
52
|
+
diagnostics.append(
|
|
53
|
+
FemDiagnostic(
|
|
54
|
+
"FEM120",
|
|
55
|
+
f"{record.name} is preserved but not translated into solver semantics yet",
|
|
56
|
+
severity="warning",
|
|
57
|
+
record_name=record.name,
|
|
58
|
+
line_start=record.source_line_start,
|
|
59
|
+
line_end=record.source_line_end,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return tuple(diagnostics)
|