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,84 @@
|
|
|
1
|
+
"""SESAM formatted FEM document writer and guarded semantic exporter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .diagnostics import FemDiagnostic, SesamFemError
|
|
9
|
+
from .document import SesamFemDocument
|
|
10
|
+
from .records import records_to_text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class SesamFemExportReport:
|
|
15
|
+
path: Path
|
|
16
|
+
records_written: int
|
|
17
|
+
bytes_written: int
|
|
18
|
+
mode: str
|
|
19
|
+
diagnostics: tuple[FemDiagnostic, ...] = ()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def write_sesam_fem_document(
|
|
23
|
+
document: SesamFemDocument,
|
|
24
|
+
path: str | Path,
|
|
25
|
+
*,
|
|
26
|
+
mode: str = "canonical",
|
|
27
|
+
overwrite: bool = False,
|
|
28
|
+
newline: str = "\r\n",
|
|
29
|
+
) -> SesamFemExportReport:
|
|
30
|
+
"""Write a typed SESAM FEM document back to formatted sequential FEM text."""
|
|
31
|
+
|
|
32
|
+
output_path = Path(path)
|
|
33
|
+
if output_path.exists() and not overwrite:
|
|
34
|
+
raise SesamFemError(f"refusing to overwrite existing file: {output_path}", code="FEM201")
|
|
35
|
+
if mode not in {"canonical", "raw"}:
|
|
36
|
+
raise SesamFemError(f"unsupported FEM write mode: {mode}", code="FEM202")
|
|
37
|
+
|
|
38
|
+
if mode == "raw":
|
|
39
|
+
lines: list[str] = []
|
|
40
|
+
saw_iend = False
|
|
41
|
+
for record in document.raw_records:
|
|
42
|
+
if record.name == "IEND":
|
|
43
|
+
saw_iend = True
|
|
44
|
+
lines.extend(record.raw_lines)
|
|
45
|
+
if not saw_iend:
|
|
46
|
+
lines.append("IEND")
|
|
47
|
+
text = newline.join(lines) + newline
|
|
48
|
+
else:
|
|
49
|
+
text = records_to_text(document.raw_records, newline=newline)
|
|
50
|
+
|
|
51
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
tmp_path = output_path.with_name(output_path.name + ".tmp")
|
|
53
|
+
tmp_path.write_text(text, encoding="ascii", newline="")
|
|
54
|
+
tmp_path.replace(output_path)
|
|
55
|
+
return SesamFemExportReport(
|
|
56
|
+
path=output_path,
|
|
57
|
+
records_written=sum(1 for record in document.raw_records if record.name != "") + (
|
|
58
|
+
0 if any(record.name == "IEND" for record in document.raw_records) else 1
|
|
59
|
+
),
|
|
60
|
+
bytes_written=len(text.encode("ascii")),
|
|
61
|
+
mode=mode,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def export_sesam_fem(model_or_document: object, path: str | Path, **kwargs: object) -> SesamFemExportReport:
|
|
66
|
+
"""Export a SESAM FEM document.
|
|
67
|
+
|
|
68
|
+
Full semantic export from arbitrary FEModel objects is a later gate. Passing
|
|
69
|
+
a SesamFemDocument is supported now for validation, canonicalization and
|
|
70
|
+
fixture round-trips.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if isinstance(model_or_document, SesamFemDocument):
|
|
74
|
+
return write_sesam_fem_document(model_or_document, path, **kwargs)
|
|
75
|
+
raise SesamFemError(
|
|
76
|
+
"semantic export from FEModel to SESAM FEM is not implemented in this gate",
|
|
77
|
+
code="FEM203",
|
|
78
|
+
diagnostics=(
|
|
79
|
+
FemDiagnostic(
|
|
80
|
+
"FEM203",
|
|
81
|
+
"pass a SesamFemDocument for round-trip export; FEModel export is deferred",
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""SESAM formatted FEM importer for ANYsolver FEModel objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional, Sequence
|
|
9
|
+
|
|
10
|
+
from .diagnostics import FemDiagnostic, SesamFemError, raise_if_errors
|
|
11
|
+
from .document import FemElement, FemMaterial, FemSection, SesamFemDocument, read_sesam_fem_document
|
|
12
|
+
from .schema import DOF_NAMES, get_element_spec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SesamFemImportResult:
|
|
17
|
+
document: SesamFemDocument
|
|
18
|
+
model: object | None
|
|
19
|
+
diagnostics: tuple[FemDiagnostic, ...]
|
|
20
|
+
element_count_by_type: dict[int, int]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def import_sesam_fem(
|
|
24
|
+
path: str | Path,
|
|
25
|
+
*,
|
|
26
|
+
strict: bool = True,
|
|
27
|
+
build_model: bool = True,
|
|
28
|
+
) -> SesamFemImportResult:
|
|
29
|
+
"""Import a SESAM formatted FEM file.
|
|
30
|
+
|
|
31
|
+
The document layer is always populated. With ``build_model=True`` the
|
|
32
|
+
importer creates an FEModel for supported beam/shell topology, basic
|
|
33
|
+
material/section data and simple nodal boundary flags.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
document = read_sesam_fem_document(path, strict=strict)
|
|
37
|
+
diagnostics = list(document.diagnostics)
|
|
38
|
+
model = None
|
|
39
|
+
if build_model:
|
|
40
|
+
model, model_diagnostics = build_fe_model_from_sesam_document(document)
|
|
41
|
+
diagnostics.extend(model_diagnostics)
|
|
42
|
+
if strict:
|
|
43
|
+
raise_if_errors(diagnostics, "SESAM FEM import failed")
|
|
44
|
+
element_count_by_type: dict[int, int] = {}
|
|
45
|
+
for element in document.elements.values():
|
|
46
|
+
element_count_by_type[element.type_code] = element_count_by_type.get(element.type_code, 0) + 1
|
|
47
|
+
return SesamFemImportResult(document, model, tuple(diagnostics), element_count_by_type)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_fe_model_from_sesam_document(document: SesamFemDocument) -> tuple[object, tuple[FemDiagnostic, ...]]:
|
|
51
|
+
"""Build a native FEModel from supported SESAM FEM document semantics."""
|
|
52
|
+
|
|
53
|
+
from ..boundary import BoundaryCondition
|
|
54
|
+
from ..elements import BeamElement, QuadraticBeamElement, ShellElement
|
|
55
|
+
from ..fe_core import FEModel
|
|
56
|
+
|
|
57
|
+
name = f"sesam:{document.source_path.name}" if document.source_path else "sesam:fem"
|
|
58
|
+
model = FEModel(name=name)
|
|
59
|
+
diagnostics: list[FemDiagnostic] = []
|
|
60
|
+
material_names = _add_materials(model, document, diagnostics)
|
|
61
|
+
|
|
62
|
+
for node in document.nodes.values():
|
|
63
|
+
model.add_node(node.node_id, *node.coordinates)
|
|
64
|
+
|
|
65
|
+
for element in document.elements.values():
|
|
66
|
+
spec = get_element_spec(element.type_code)
|
|
67
|
+
if spec is None:
|
|
68
|
+
diagnostics.append(
|
|
69
|
+
FemDiagnostic(
|
|
70
|
+
"FEM103",
|
|
71
|
+
f"unsupported SESAM element type {element.type_code}",
|
|
72
|
+
context={"element_id": element.element_id},
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
missing_nodes = [node_id for node_id in element.node_ids if node_id not in model.mesh.nodes]
|
|
77
|
+
if missing_nodes:
|
|
78
|
+
diagnostics.append(
|
|
79
|
+
FemDiagnostic(
|
|
80
|
+
"FEM105",
|
|
81
|
+
f"element {element.element_id} references missing nodes {missing_nodes}",
|
|
82
|
+
context={"element_id": element.element_id, "missing_nodes": missing_nodes},
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
continue
|
|
86
|
+
material_name = material_names.get(element.material_id or 0, "default")
|
|
87
|
+
try:
|
|
88
|
+
if spec.is_shell:
|
|
89
|
+
thickness = _shell_thickness(document.sections.get(element.section_id or 0))
|
|
90
|
+
solver_element = ShellElement(
|
|
91
|
+
element.element_id,
|
|
92
|
+
list(element.node_ids),
|
|
93
|
+
material_name=material_name,
|
|
94
|
+
thickness=thickness,
|
|
95
|
+
)
|
|
96
|
+
elif spec.type_code == 23:
|
|
97
|
+
cross_section = _beam_section(document.sections.get(element.section_id or 0))
|
|
98
|
+
_apply_beam_orientation(cross_section, document, element)
|
|
99
|
+
solver_element = QuadraticBeamElement(
|
|
100
|
+
element.element_id,
|
|
101
|
+
list(element.node_ids),
|
|
102
|
+
material_name=material_name,
|
|
103
|
+
cross_section=cross_section,
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
cross_section = _beam_section(document.sections.get(element.section_id or 0))
|
|
107
|
+
_apply_beam_orientation(cross_section, document, element)
|
|
108
|
+
solver_element = BeamElement(
|
|
109
|
+
element.element_id,
|
|
110
|
+
list(element.node_ids),
|
|
111
|
+
material_name=material_name,
|
|
112
|
+
cross_section=cross_section,
|
|
113
|
+
)
|
|
114
|
+
except Exception as exc: # Element constructors perform useful validation.
|
|
115
|
+
diagnostics.append(
|
|
116
|
+
FemDiagnostic(
|
|
117
|
+
"FEM130",
|
|
118
|
+
f"could not construct element {element.element_id}: {exc}",
|
|
119
|
+
context={"element_id": element.element_id},
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
continue
|
|
123
|
+
_attach_sesam_element_metadata(solver_element, document, element)
|
|
124
|
+
model.add_element(element.element_id, solver_element)
|
|
125
|
+
|
|
126
|
+
for index, boundary in enumerate(document.boundaries, start=1):
|
|
127
|
+
if boundary.node_id not in model.mesh.nodes:
|
|
128
|
+
diagnostics.append(
|
|
129
|
+
FemDiagnostic(
|
|
130
|
+
"FEM105",
|
|
131
|
+
f"boundary references missing node {boundary.node_id}",
|
|
132
|
+
context={"node_id": boundary.node_id},
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
constraints = {
|
|
137
|
+
dof_name: 0.0
|
|
138
|
+
for dof_name, flag in zip(DOF_NAMES, boundary.dof_flags)
|
|
139
|
+
if int(flag) != 0
|
|
140
|
+
}
|
|
141
|
+
if constraints:
|
|
142
|
+
model.add_boundary_condition(
|
|
143
|
+
BoundaryCondition(f"sesam_BNBCD_{index}", [boundary.node_id], constraints)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if document.load_records:
|
|
147
|
+
from ..boundary import LoadCase
|
|
148
|
+
load_case = LoadCase("sesam_imported_load")
|
|
149
|
+
has_loads = False
|
|
150
|
+
shell_element_ids = {eid for eid, el in model.mesh.elements.items() if el.__class__.__name__ == "ShellElement"}
|
|
151
|
+
|
|
152
|
+
pressure_loads = {}
|
|
153
|
+
for load_record in document.load_records:
|
|
154
|
+
if load_record.record_name == "BEUSLO" and len(load_record.raw_values) >= 9:
|
|
155
|
+
element_id = int(load_record.raw_values[4])
|
|
156
|
+
if element_id in shell_element_ids:
|
|
157
|
+
# load values start at index 8
|
|
158
|
+
import math
|
|
159
|
+
load_values = [float(v) for v in load_record.raw_values[8:] if math.isfinite(float(v))]
|
|
160
|
+
if load_values:
|
|
161
|
+
pressure_loads[element_id] = pressure_loads.get(element_id, 0.0) + sum(load_values) / len(load_values)
|
|
162
|
+
|
|
163
|
+
elif load_record.record_name == "BGRAV" and len(load_record.raw_values) >= 7:
|
|
164
|
+
gx = float(load_record.raw_values[-3])
|
|
165
|
+
gy = float(load_record.raw_values[-2])
|
|
166
|
+
gz = float(load_record.raw_values[-1])
|
|
167
|
+
if abs(gx) > 0 or abs(gy) > 0 or abs(gz) > 0:
|
|
168
|
+
load_case.set_gravity(gx, gy, gz)
|
|
169
|
+
has_loads = True
|
|
170
|
+
|
|
171
|
+
for element_id, pressure in sorted(pressure_loads.items()):
|
|
172
|
+
if pressure != 0.0:
|
|
173
|
+
load_case.add_pressure_load(element_id, pressure)
|
|
174
|
+
has_loads = True
|
|
175
|
+
|
|
176
|
+
if has_loads:
|
|
177
|
+
model.add_load_case(load_case)
|
|
178
|
+
else:
|
|
179
|
+
diagnostics.append(
|
|
180
|
+
FemDiagnostic(
|
|
181
|
+
"FEM121",
|
|
182
|
+
"SESAM load records were found but yielded no active loads.",
|
|
183
|
+
severity="warning",
|
|
184
|
+
context={"load_records": len(document.load_records)},
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
if document.dependencies:
|
|
188
|
+
diagnostics.append(
|
|
189
|
+
FemDiagnostic(
|
|
190
|
+
"FEM122",
|
|
191
|
+
"SESAM dependency records are preserved but not translated into solver MPCs yet",
|
|
192
|
+
severity="warning",
|
|
193
|
+
context={"dependency_records": len(document.dependencies)},
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
setattr(model, "sesam_document", document)
|
|
198
|
+
setattr(model, "sesam_import_diagnostics", tuple(diagnostics))
|
|
199
|
+
return model, tuple(diagnostics)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _element_transform_ids(document: SesamFemDocument, element: FemElement) -> tuple[int, ...]:
|
|
203
|
+
reference = document.element_references.get(element.element_id)
|
|
204
|
+
if reference is None:
|
|
205
|
+
return ()
|
|
206
|
+
if reference.nodal_transform_ids:
|
|
207
|
+
return tuple(reference.nodal_transform_ids)
|
|
208
|
+
if reference.transform_id is not None:
|
|
209
|
+
return (reference.transform_id,)
|
|
210
|
+
return ()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _normalise_vector(values: Sequence[float]) -> tuple[float, float, float] | None:
|
|
214
|
+
if len(values) < 3:
|
|
215
|
+
return None
|
|
216
|
+
x, y, z = (float(values[0]), float(values[1]), float(values[2]))
|
|
217
|
+
length = (x * x + y * y + z * z) ** 0.5
|
|
218
|
+
if length <= 1.0e-12:
|
|
219
|
+
return None
|
|
220
|
+
return (x / length, y / length, z / length)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _mean_vector(vectors: Sequence[Sequence[float]]) -> tuple[float, float, float] | None:
|
|
224
|
+
if not vectors:
|
|
225
|
+
return None
|
|
226
|
+
return _normalise_vector(
|
|
227
|
+
(
|
|
228
|
+
sum(float(vector[0]) for vector in vectors),
|
|
229
|
+
sum(float(vector[1]) for vector in vectors),
|
|
230
|
+
sum(float(vector[2]) for vector in vectors),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _beam_orientation_vector(document: SesamFemDocument, transform_ids: Sequence[int]) -> tuple[float, float, float] | None:
|
|
236
|
+
vectors = []
|
|
237
|
+
for transform_id in transform_ids:
|
|
238
|
+
unit_vector = document.unit_vectors.get(transform_id)
|
|
239
|
+
if unit_vector is not None:
|
|
240
|
+
vectors.append(unit_vector.vector)
|
|
241
|
+
continue
|
|
242
|
+
transform = document.coordinate_transforms.get(transform_id)
|
|
243
|
+
if transform is not None:
|
|
244
|
+
vectors.append(transform.matrix[2])
|
|
245
|
+
return _mean_vector(vectors)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _apply_beam_orientation(
|
|
249
|
+
cross_section: dict[str, object],
|
|
250
|
+
document: SesamFemDocument,
|
|
251
|
+
element: FemElement,
|
|
252
|
+
) -> None:
|
|
253
|
+
orientation = _beam_orientation_vector(document, _element_transform_ids(document, element))
|
|
254
|
+
if orientation is not None:
|
|
255
|
+
cross_section["orientation"] = orientation
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _mean_coordinate_transform_axes(
|
|
259
|
+
document: SesamFemDocument,
|
|
260
|
+
transform_ids: Sequence[int],
|
|
261
|
+
) -> dict[str, tuple[float, float, float]] | None:
|
|
262
|
+
rows = {"x": [], "y": [], "z": []}
|
|
263
|
+
for transform_id in transform_ids:
|
|
264
|
+
transform = document.coordinate_transforms.get(transform_id)
|
|
265
|
+
if transform is None:
|
|
266
|
+
continue
|
|
267
|
+
rows["x"].append(transform.matrix[0])
|
|
268
|
+
rows["y"].append(transform.matrix[1])
|
|
269
|
+
rows["z"].append(transform.matrix[2])
|
|
270
|
+
axes = {name: _mean_vector(vectors) for name, vectors in rows.items()}
|
|
271
|
+
if any(value is None for value in axes.values()):
|
|
272
|
+
return None
|
|
273
|
+
return {name: value for name, value in axes.items() if value is not None}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _attach_sesam_element_metadata(solver_element: object, document: SesamFemDocument, element: FemElement) -> None:
|
|
277
|
+
transform_ids = _element_transform_ids(document, element)
|
|
278
|
+
reference = document.element_references.get(element.element_id)
|
|
279
|
+
if reference is not None:
|
|
280
|
+
setattr(solver_element, "sesam_reference", reference)
|
|
281
|
+
if not transform_ids:
|
|
282
|
+
return
|
|
283
|
+
setattr(solver_element, "sesam_transform_ids", transform_ids)
|
|
284
|
+
spec = get_element_spec(element.type_code)
|
|
285
|
+
if spec is not None and spec.is_shell:
|
|
286
|
+
axes = _mean_coordinate_transform_axes(document, transform_ids)
|
|
287
|
+
if axes is not None:
|
|
288
|
+
setattr(solver_element, "sesam_local_axes", axes)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _add_materials(
|
|
292
|
+
model: object,
|
|
293
|
+
document: SesamFemDocument,
|
|
294
|
+
diagnostics: list[FemDiagnostic],
|
|
295
|
+
) -> dict[int, str]:
|
|
296
|
+
material_names: dict[int, str] = {0: "default"}
|
|
297
|
+
for material_id, material in document.materials.items():
|
|
298
|
+
name = _material_name(material)
|
|
299
|
+
material_names[material_id] = name
|
|
300
|
+
elastic_modulus = material.elastic_modulus or 210.0e9
|
|
301
|
+
poisson_ratio = material.poisson_ratio if material.poisson_ratio is not None else 0.3
|
|
302
|
+
density = material.density or 0.0
|
|
303
|
+
yield_stress = material.yield_stress or 0.0
|
|
304
|
+
if elastic_modulus < 1.0e9:
|
|
305
|
+
diagnostics.append(
|
|
306
|
+
FemDiagnostic(
|
|
307
|
+
"FEM123",
|
|
308
|
+
f"material {material_id} elastic modulus is unusually small for SI units",
|
|
309
|
+
severity="warning",
|
|
310
|
+
context={"material_id": material_id, "elastic_modulus": elastic_modulus},
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
model.add_material(
|
|
314
|
+
name,
|
|
315
|
+
elastic_modulus=elastic_modulus,
|
|
316
|
+
poisson_ratio=poisson_ratio,
|
|
317
|
+
density=density,
|
|
318
|
+
yield_stress=yield_stress,
|
|
319
|
+
)
|
|
320
|
+
return material_names
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _material_name(material: FemMaterial) -> str:
|
|
324
|
+
suffix = _safe_name(material.name) if material.name else str(material.material_id)
|
|
325
|
+
return f"sesam_material_{suffix}"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _safe_name(value: str) -> str:
|
|
329
|
+
cleaned = re.sub(r"[^0-9A-Za-z_]+", "_", value.strip())
|
|
330
|
+
return cleaned.strip("_") or "unnamed"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _shell_thickness(section: Optional[FemSection]) -> float:
|
|
334
|
+
if section is not None and section.thickness is not None and section.thickness > 0.0:
|
|
335
|
+
return float(section.thickness)
|
|
336
|
+
return 0.01
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _beam_section(section: Optional[FemSection]) -> dict[str, object]:
|
|
340
|
+
if section is None:
|
|
341
|
+
return {}
|
|
342
|
+
data: dict[str, object] = {}
|
|
343
|
+
if section.name:
|
|
344
|
+
data["name"] = section.name
|
|
345
|
+
if section.area is not None:
|
|
346
|
+
data["area"] = float(section.area)
|
|
347
|
+
if section.iy is not None:
|
|
348
|
+
data["Iy"] = float(section.iy)
|
|
349
|
+
if section.iz is not None:
|
|
350
|
+
data["Iz"] = float(section.iz)
|
|
351
|
+
if section.torsion is not None:
|
|
352
|
+
data["J"] = float(section.torsion)
|
|
353
|
+
if section.web_height is not None:
|
|
354
|
+
data["web_height"] = float(section.web_height)
|
|
355
|
+
if section.web_thickness is not None:
|
|
356
|
+
data["web_thickness"] = float(section.web_thickness)
|
|
357
|
+
if section.flange_width is not None:
|
|
358
|
+
data["flange_width"] = float(section.flange_width)
|
|
359
|
+
if section.flange_thickness is not None:
|
|
360
|
+
data["flange_thickness"] = float(section.flange_thickness)
|
|
361
|
+
if section.section_type:
|
|
362
|
+
data["section_type"] = section.section_type
|
|
363
|
+
elif data.get("flange_width", 0.0) > 0.0:
|
|
364
|
+
data["section_type"] = "T"
|
|
365
|
+
return data
|