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.
Files changed (70) hide show
  1. anysolver/__init__.py +631 -0
  2. anysolver/anystructure_fem_mode.py +942 -0
  3. anysolver/arc_length.py +758 -0
  4. anysolver/assembly.py +950 -0
  5. anysolver/baselines.py +303 -0
  6. anysolver/beam_shell_verification.py +4276 -0
  7. anysolver/beam_validity.py +110 -0
  8. anysolver/benchmarks.py +495 -0
  9. anysolver/boundary.py +476 -0
  10. anysolver/buckling.py +442 -0
  11. anysolver/buckling_validity.py +91 -0
  12. anysolver/capacity_workflow.py +350 -0
  13. anysolver/cases.py +173 -0
  14. anysolver/composite_strip_verification.py +297 -0
  15. anysolver/contact.py +3461 -0
  16. anysolver/corotational.py +371 -0
  17. anysolver/cylinder_benchmarks.py +364 -0
  18. anysolver/dynamics.py +723 -0
  19. anysolver/element_qualification.py +432 -0
  20. anysolver/elements.py +2724 -0
  21. anysolver/external_references.py +369 -0
  22. anysolver/fe_core.py +327 -0
  23. anysolver/fracture.py +551 -0
  24. anysolver/imperfections.py +390 -0
  25. anysolver/jit_compiler.py +108 -0
  26. anysolver/kernel_warmup.py +180 -0
  27. anysolver/linalg.py +760 -0
  28. anysolver/mass_properties.py +179 -0
  29. anysolver/material_curves.py +231 -0
  30. anysolver/matrix_assembly.py +558 -0
  31. anysolver/mesh_gen.py +1065 -0
  32. anysolver/mesh_load_bc_verification.py +609 -0
  33. anysolver/modal.py +282 -0
  34. anysolver/nonlinear.py +300 -0
  35. anysolver/nonlinear_performance.py +920 -0
  36. anysolver/nonlinear_performance_batch_b.py +592 -0
  37. anysolver/nonlinear_performance_batch_c.py +506 -0
  38. anysolver/nonlinear_performance_bootstrap.py +120 -0
  39. anysolver/nonlinear_reduced_assembly.py +760 -0
  40. anysolver/nonlinear_static.py +1518 -0
  41. anysolver/plasticity.py +419 -0
  42. anysolver/plasticity_qualification.py +314 -0
  43. anysolver/production_readiness.py +304 -0
  44. anysolver/quality_control.py +1497 -0
  45. anysolver/recovery.py +505 -0
  46. anysolver/recovery_policy.py +205 -0
  47. anysolver/reference_cases.py +425 -0
  48. anysolver/results.py +503 -0
  49. anysolver/runtime.py +9030 -0
  50. anysolver/s4_validity.py +326 -0
  51. anysolver/sesam_fem/__init__.py +55 -0
  52. anysolver/sesam_fem/__main__.py +80 -0
  53. anysolver/sesam_fem/diagnostics.py +65 -0
  54. anysolver/sesam_fem/document.py +814 -0
  55. anysolver/sesam_fem/exporter.py +84 -0
  56. anysolver/sesam_fem/importer.py +365 -0
  57. anysolver/sesam_fem/records.py +257 -0
  58. anysolver/sesam_fem/schema.py +107 -0
  59. anysolver/sesam_fem/sif_importer.py +397 -0
  60. anysolver/sesam_fem/validation.py +62 -0
  61. anysolver/shell_benchmarks.py +367 -0
  62. anysolver/test_cases.py +610 -0
  63. anysolver/validation.py +416 -0
  64. anysolver/vectorized_nonlinear.py +334 -0
  65. anysolver/vectorized_stiffness.py +571 -0
  66. anysolver-0.1.0.dist-info/METADATA +165 -0
  67. anysolver-0.1.0.dist-info/RECORD +70 -0
  68. anysolver-0.1.0.dist-info/WHEEL +5 -0
  69. anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
  70. 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)