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,814 @@
|
|
|
1
|
+
"""Layer B typed SESAM formatted FEM document model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Mapping, Optional
|
|
10
|
+
|
|
11
|
+
from .diagnostics import FemDiagnostic, SesamFemError, raise_if_errors
|
|
12
|
+
from .records import FemRawRecord, read_raw_records, strict_int
|
|
13
|
+
from .schema import SUPPORTED_RECORDS, classify_record, get_element_spec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class FemHeader:
|
|
18
|
+
ident_values: tuple[float, ...] = ()
|
|
19
|
+
ident_text: tuple[str, ...] = ()
|
|
20
|
+
date_text: tuple[str, ...] = ()
|
|
21
|
+
unit_values: tuple[float, ...] = ()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class FemMaterial:
|
|
26
|
+
material_id: int
|
|
27
|
+
name: Optional[str] = None
|
|
28
|
+
elastic_modulus: Optional[float] = None
|
|
29
|
+
poisson_ratio: Optional[float] = None
|
|
30
|
+
density: Optional[float] = None
|
|
31
|
+
yield_stress: Optional[float] = None
|
|
32
|
+
raw_values: tuple[float, ...] = ()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class FemSection:
|
|
37
|
+
section_id: int
|
|
38
|
+
kind: str
|
|
39
|
+
name: Optional[str] = None
|
|
40
|
+
thickness: Optional[float] = None
|
|
41
|
+
area: Optional[float] = None
|
|
42
|
+
iy: Optional[float] = None
|
|
43
|
+
iz: Optional[float] = None
|
|
44
|
+
torsion: Optional[float] = None
|
|
45
|
+
web_height: Optional[float] = None
|
|
46
|
+
web_thickness: Optional[float] = None
|
|
47
|
+
flange_width: Optional[float] = None
|
|
48
|
+
flange_thickness: Optional[float] = None
|
|
49
|
+
section_type: Optional[str] = None
|
|
50
|
+
raw_values: tuple[float, ...] = ()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class FemConceptRecord:
|
|
55
|
+
record_name: str
|
|
56
|
+
concept_id: Optional[int]
|
|
57
|
+
text: tuple[str, ...]
|
|
58
|
+
raw_values: tuple[float, ...]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class FemCoordinate:
|
|
63
|
+
coordinate_id: int
|
|
64
|
+
raw_values: tuple[float, ...]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class FemUnitVector:
|
|
69
|
+
transform_id: int
|
|
70
|
+
vector: tuple[float, float, float]
|
|
71
|
+
raw_values: tuple[float, ...]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class FemCoordinateTransform:
|
|
76
|
+
transform_id: int
|
|
77
|
+
matrix: tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]]
|
|
78
|
+
raw_values: tuple[float, ...]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class FemNode:
|
|
83
|
+
node_id: int
|
|
84
|
+
coordinates: tuple[float, float, float]
|
|
85
|
+
coordinate_system_id: int = 0
|
|
86
|
+
raw_values: tuple[float, ...] = ()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class FemElementReference:
|
|
91
|
+
element_id: int
|
|
92
|
+
material_id: Optional[int]
|
|
93
|
+
section_id: Optional[int]
|
|
94
|
+
transform_id: Optional[int]
|
|
95
|
+
nodal_transform_ids: tuple[int, ...] = ()
|
|
96
|
+
raw_values: tuple[float, ...] = ()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class FemElement:
|
|
101
|
+
element_id: int
|
|
102
|
+
type_code: int
|
|
103
|
+
topology: str
|
|
104
|
+
node_ids: tuple[int, ...]
|
|
105
|
+
material_id: Optional[int] = None
|
|
106
|
+
section_id: Optional[int] = None
|
|
107
|
+
raw_values: tuple[float, ...] = ()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class FemBoundary:
|
|
112
|
+
node_id: int
|
|
113
|
+
dof_flags: tuple[int, ...]
|
|
114
|
+
prescribed_values: tuple[float, ...]
|
|
115
|
+
raw_values: tuple[float, ...]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class FemDependency:
|
|
120
|
+
record_name: str
|
|
121
|
+
raw_values: tuple[float, ...]
|
|
122
|
+
text: tuple[str, ...]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class FemLoadRecord:
|
|
127
|
+
record_name: str
|
|
128
|
+
load_case_id: Optional[int]
|
|
129
|
+
target_id: Optional[int]
|
|
130
|
+
raw_values: tuple[float, ...]
|
|
131
|
+
text: tuple[str, ...]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class SesamFemDocument:
|
|
136
|
+
source_path: Optional[Path]
|
|
137
|
+
header: FemHeader
|
|
138
|
+
raw_records: tuple[FemRawRecord, ...]
|
|
139
|
+
record_counts: Mapping[str, int]
|
|
140
|
+
materials: Mapping[int, FemMaterial]
|
|
141
|
+
sections: Mapping[int, FemSection]
|
|
142
|
+
concepts: tuple[FemConceptRecord, ...]
|
|
143
|
+
coordinate_systems: Mapping[int, FemCoordinate]
|
|
144
|
+
unit_vectors: Mapping[int, FemUnitVector]
|
|
145
|
+
coordinate_transforms: Mapping[int, FemCoordinateTransform]
|
|
146
|
+
nodes: Mapping[int, FemNode]
|
|
147
|
+
elements: Mapping[int, FemElement]
|
|
148
|
+
element_references: Mapping[int, FemElementReference]
|
|
149
|
+
boundaries: tuple[FemBoundary, ...]
|
|
150
|
+
dependencies: tuple[FemDependency, ...]
|
|
151
|
+
load_records: tuple[FemLoadRecord, ...]
|
|
152
|
+
unknown_records: tuple[FemRawRecord, ...]
|
|
153
|
+
diagnostics: tuple[FemDiagnostic, ...] = ()
|
|
154
|
+
|
|
155
|
+
def summary(self) -> dict[str, object]:
|
|
156
|
+
return {
|
|
157
|
+
"source_path": str(self.source_path) if self.source_path else None,
|
|
158
|
+
"record_counts": dict(self.record_counts),
|
|
159
|
+
"materials": len(self.materials),
|
|
160
|
+
"sections": len(self.sections),
|
|
161
|
+
"nodes": len(self.nodes),
|
|
162
|
+
"elements": len(self.elements),
|
|
163
|
+
"unit_vectors": len(self.unit_vectors),
|
|
164
|
+
"coordinate_transforms": len(self.coordinate_transforms),
|
|
165
|
+
"element_types": dict(Counter(element.type_code for element in self.elements.values())),
|
|
166
|
+
"boundaries": len(self.boundaries),
|
|
167
|
+
"dependencies": len(self.dependencies),
|
|
168
|
+
"loads": len(self.load_records),
|
|
169
|
+
"unknown_records": len(self.unknown_records),
|
|
170
|
+
"diagnostics": [item.as_dict() for item in self.diagnostics],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def read_sesam_fem_document(path: str | Path, *, strict: bool = True) -> SesamFemDocument:
|
|
175
|
+
"""Read a SESAM formatted sequential FEM document."""
|
|
176
|
+
|
|
177
|
+
raw_records = read_raw_records(path, strict=strict)
|
|
178
|
+
document = parse_sesam_fem_records(raw_records, source_path=Path(path), strict=False)
|
|
179
|
+
from .validation import validate_sesam_fem_document
|
|
180
|
+
|
|
181
|
+
diagnostics = tuple(document.diagnostics) + validate_sesam_fem_document(document)
|
|
182
|
+
document = replace(document, diagnostics=diagnostics)
|
|
183
|
+
if strict:
|
|
184
|
+
raise_if_errors(diagnostics, "SESAM FEM document failed validation")
|
|
185
|
+
return document
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_sesam_fem_records(
|
|
189
|
+
raw_records: tuple[FemRawRecord, ...],
|
|
190
|
+
*,
|
|
191
|
+
source_path: str | Path | None = None,
|
|
192
|
+
strict: bool = True,
|
|
193
|
+
) -> SesamFemDocument:
|
|
194
|
+
"""Parse raw records into the typed SESAM FEM document model."""
|
|
195
|
+
|
|
196
|
+
diagnostics: list[FemDiagnostic] = []
|
|
197
|
+
header = _parse_header(raw_records)
|
|
198
|
+
record_counts = Counter(record.name for record in raw_records)
|
|
199
|
+
materials, material_names = _parse_materials(raw_records, diagnostics)
|
|
200
|
+
sections, section_names = _parse_sections(raw_records, diagnostics)
|
|
201
|
+
materials = {
|
|
202
|
+
key: replace(value, name=material_names.get(key, value.name))
|
|
203
|
+
for key, value in materials.items()
|
|
204
|
+
}
|
|
205
|
+
sections = {
|
|
206
|
+
key: replace(value, name=section_names.get(key, value.name))
|
|
207
|
+
for key, value in sections.items()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
coordinate_systems = _parse_coordinates(raw_records, diagnostics)
|
|
211
|
+
unit_vectors = _parse_unit_vectors(raw_records, diagnostics)
|
|
212
|
+
coordinate_transforms = _parse_coordinate_transforms(raw_records, diagnostics)
|
|
213
|
+
known_transform_ids = set(unit_vectors) | set(coordinate_transforms)
|
|
214
|
+
nodes = _parse_nodes(raw_records, diagnostics)
|
|
215
|
+
element_references = _parse_element_references(raw_records, diagnostics, known_transform_ids)
|
|
216
|
+
elements = _parse_elements(raw_records, element_references, diagnostics)
|
|
217
|
+
boundaries = _parse_boundaries(raw_records, diagnostics)
|
|
218
|
+
concepts = _parse_concepts(raw_records, diagnostics)
|
|
219
|
+
dependencies = _parse_dependencies(raw_records)
|
|
220
|
+
load_records = _parse_loads(raw_records, diagnostics)
|
|
221
|
+
|
|
222
|
+
unknown_records = tuple(record for record in raw_records if record.name not in SUPPORTED_RECORDS)
|
|
223
|
+
for record in unknown_records:
|
|
224
|
+
diagnostics.append(
|
|
225
|
+
FemDiagnostic(
|
|
226
|
+
"FEM110",
|
|
227
|
+
f"unknown FEM record preserved as raw data: {record.name}",
|
|
228
|
+
severity="warning",
|
|
229
|
+
record_name=record.name,
|
|
230
|
+
line_start=record.source_line_start,
|
|
231
|
+
line_end=record.source_line_end,
|
|
232
|
+
context={"classification": classify_record(record.name)},
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
document = SesamFemDocument(
|
|
237
|
+
source_path=Path(source_path) if source_path is not None else None,
|
|
238
|
+
header=header,
|
|
239
|
+
raw_records=raw_records,
|
|
240
|
+
record_counts=dict(record_counts),
|
|
241
|
+
materials=materials,
|
|
242
|
+
sections=sections,
|
|
243
|
+
concepts=concepts,
|
|
244
|
+
coordinate_systems=coordinate_systems,
|
|
245
|
+
unit_vectors=unit_vectors,
|
|
246
|
+
coordinate_transforms=coordinate_transforms,
|
|
247
|
+
nodes=nodes,
|
|
248
|
+
elements=elements,
|
|
249
|
+
element_references=element_references,
|
|
250
|
+
boundaries=boundaries,
|
|
251
|
+
dependencies=dependencies,
|
|
252
|
+
load_records=load_records,
|
|
253
|
+
unknown_records=unknown_records,
|
|
254
|
+
diagnostics=tuple(diagnostics),
|
|
255
|
+
)
|
|
256
|
+
if strict:
|
|
257
|
+
from .validation import validate_sesam_fem_document
|
|
258
|
+
|
|
259
|
+
all_diagnostics = tuple(diagnostics) + validate_sesam_fem_document(document)
|
|
260
|
+
raise_if_errors(all_diagnostics, "SESAM FEM records failed validation")
|
|
261
|
+
document = replace(document, diagnostics=all_diagnostics)
|
|
262
|
+
return document
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _parse_header(records: tuple[FemRawRecord, ...]) -> FemHeader:
|
|
266
|
+
ident = next((record for record in records if record.name == "IDENT"), None)
|
|
267
|
+
date = next((record for record in records if record.name == "DATE"), None)
|
|
268
|
+
units = next((record for record in records if record.name == "UNITS"), None)
|
|
269
|
+
return FemHeader(
|
|
270
|
+
ident_values=ident.numeric_fields if ident else (),
|
|
271
|
+
ident_text=ident.text_fields if ident else (),
|
|
272
|
+
date_text=date.text_fields if date else (),
|
|
273
|
+
unit_values=units.numeric_fields if units else (),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _parse_materials(
|
|
278
|
+
records: tuple[FemRawRecord, ...],
|
|
279
|
+
diagnostics: list[FemDiagnostic],
|
|
280
|
+
) -> tuple[dict[int, FemMaterial], dict[int, str]]:
|
|
281
|
+
materials: dict[int, FemMaterial] = {}
|
|
282
|
+
names: dict[int, str] = {}
|
|
283
|
+
for record in records:
|
|
284
|
+
if record.name == "MISOSEL":
|
|
285
|
+
if not record.numeric_fields:
|
|
286
|
+
diagnostics.append(_diag("FEM101", "MISOSEL record has no material id", record))
|
|
287
|
+
continue
|
|
288
|
+
material_id = _int_field(record.numeric_fields[0], "material id", record, diagnostics)
|
|
289
|
+
if material_id is None:
|
|
290
|
+
continue
|
|
291
|
+
if material_id in materials:
|
|
292
|
+
diagnostics.append(_diag("FEM102", f"duplicate material id {material_id}", record))
|
|
293
|
+
continue
|
|
294
|
+
materials[material_id] = FemMaterial(
|
|
295
|
+
material_id=material_id,
|
|
296
|
+
elastic_modulus=_first_in_range(record.numeric_fields[1:], 1.0e7, 1.0e13),
|
|
297
|
+
poisson_ratio=_first_in_range(record.numeric_fields[1:], 0.0, 0.5),
|
|
298
|
+
density=_first_in_range(record.numeric_fields[1:], 10.0, 50000.0),
|
|
299
|
+
yield_stress=_first_in_range(record.numeric_fields[1:], 1.0e6, 5.0e9),
|
|
300
|
+
raw_values=record.numeric_fields,
|
|
301
|
+
)
|
|
302
|
+
elif record.name == "TDMATER" and record.numeric_fields:
|
|
303
|
+
material_id = _int_field(record.numeric_fields[0], "material id", record, diagnostics)
|
|
304
|
+
if material_id is not None and record.text_fields:
|
|
305
|
+
names[material_id] = " ".join(record.text_fields)
|
|
306
|
+
return materials, names
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _parse_sections(
|
|
310
|
+
records: tuple[FemRawRecord, ...],
|
|
311
|
+
diagnostics: list[FemDiagnostic],
|
|
312
|
+
) -> tuple[dict[int, FemSection], dict[int, str]]:
|
|
313
|
+
sections: dict[int, FemSection] = {}
|
|
314
|
+
names: dict[int, str] = {}
|
|
315
|
+
dimensions: dict[int, dict[str, float]] = {}
|
|
316
|
+
for record in records:
|
|
317
|
+
if record.name in {"GELTH", "GBEAMG"}:
|
|
318
|
+
if not record.numeric_fields:
|
|
319
|
+
diagnostics.append(_diag("FEM101", f"{record.name} record has no section id", record))
|
|
320
|
+
continue
|
|
321
|
+
section_id = _int_field(record.numeric_fields[0], "section id", record, diagnostics)
|
|
322
|
+
if section_id is None:
|
|
323
|
+
continue
|
|
324
|
+
if section_id in sections:
|
|
325
|
+
diagnostics.append(_diag("FEM102", f"duplicate section id {section_id}", record))
|
|
326
|
+
continue
|
|
327
|
+
values = record.numeric_fields
|
|
328
|
+
if record.name == "GELTH":
|
|
329
|
+
sections[section_id] = FemSection(
|
|
330
|
+
section_id=section_id,
|
|
331
|
+
kind="shell_thickness",
|
|
332
|
+
thickness=values[1] if len(values) > 1 and values[1] > 0.0 else None,
|
|
333
|
+
raw_values=values,
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
sections[section_id] = FemSection(
|
|
337
|
+
section_id=section_id,
|
|
338
|
+
kind="beam_section",
|
|
339
|
+
area=values[2] if len(values) > 2 and values[2] > 0.0 else None,
|
|
340
|
+
iy=values[4] if len(values) > 4 and values[4] > 0.0 else None,
|
|
341
|
+
iz=values[5] if len(values) > 5 and values[5] > 0.0 else None,
|
|
342
|
+
torsion=values[3] if len(values) > 3 and values[3] > 0.0 else None,
|
|
343
|
+
raw_values=values,
|
|
344
|
+
)
|
|
345
|
+
elif record.name == "TDSECT" and record.numeric_fields:
|
|
346
|
+
values = record.numeric_fields
|
|
347
|
+
section_id_index = 1 if len(values) > 1 and int(round(values[0])) == 4 else 0
|
|
348
|
+
section_id = _int_field(values[section_id_index], "section id", record, diagnostics)
|
|
349
|
+
if section_id is not None and record.text_fields:
|
|
350
|
+
names[section_id] = " ".join(record.text_fields)
|
|
351
|
+
elif record.name in {"GBARM", "GBOX", "GIORH", "GLSEC", "GPIPE"} and record.numeric_fields:
|
|
352
|
+
section_id = _int_field(record.numeric_fields[0], "section id", record, diagnostics)
|
|
353
|
+
if section_id is not None:
|
|
354
|
+
values = record.numeric_fields
|
|
355
|
+
dim: dict[str, float] = {}
|
|
356
|
+
text_dim: dict[str, object] = {}
|
|
357
|
+
if record.name == "GBARM":
|
|
358
|
+
if len(values) > 1 and values[1] > 0.0:
|
|
359
|
+
dim["web_height"] = float(values[1])
|
|
360
|
+
if len(values) > 2 and values[2] > 0.0:
|
|
361
|
+
dim["web_thickness"] = float(values[2])
|
|
362
|
+
text_dim["section_type"] = "FB"
|
|
363
|
+
elif record.name == "GBOX":
|
|
364
|
+
if len(values) > 1 and values[1] > 0.0:
|
|
365
|
+
dim["web_height"] = float(values[1])
|
|
366
|
+
dim["flange_width"] = float(values[1])
|
|
367
|
+
if len(values) > 2 and values[2] > 0.0:
|
|
368
|
+
dim["web_thickness"] = float(values[2])
|
|
369
|
+
if len(values) > 3 and values[3] > 0.0:
|
|
370
|
+
dim["flange_thickness"] = float(values[3])
|
|
371
|
+
text_dim["section_type"] = "Box"
|
|
372
|
+
elif record.name == "GIORH":
|
|
373
|
+
if len(values) > 1 and values[1] > 0.0:
|
|
374
|
+
dim["web_height"] = float(values[1])
|
|
375
|
+
if len(values) > 2 and values[2] > 0.0:
|
|
376
|
+
dim["web_thickness"] = float(values[2])
|
|
377
|
+
if len(values) > 5 and values[5] > 0.0:
|
|
378
|
+
dim["flange_width"] = float(values[5])
|
|
379
|
+
elif len(values) > 3 and values[3] > 0.0:
|
|
380
|
+
dim["flange_width"] = float(values[3])
|
|
381
|
+
if len(values) > 6 and values[6] > 0.0:
|
|
382
|
+
dim["flange_thickness"] = float(values[6])
|
|
383
|
+
elif len(values) > 4 and values[4] > 0.0:
|
|
384
|
+
dim["flange_thickness"] = float(values[4])
|
|
385
|
+
text_dim["section_type"] = "T"
|
|
386
|
+
elif record.name == "GLSEC":
|
|
387
|
+
if len(values) > 1 and values[1] > 0.0:
|
|
388
|
+
dim["web_height"] = float(values[1])
|
|
389
|
+
if len(values) > 2 and values[2] > 0.0:
|
|
390
|
+
dim["web_thickness"] = float(values[2])
|
|
391
|
+
if len(values) > 3 and values[3] > 0.0:
|
|
392
|
+
dim["flange_width"] = float(values[3])
|
|
393
|
+
if len(values) > 4 and values[4] > 0.0:
|
|
394
|
+
dim["flange_thickness"] = float(values[4])
|
|
395
|
+
text_dim["section_type"] = "L"
|
|
396
|
+
elif record.name == "GPIPE":
|
|
397
|
+
outer = values[2] if len(values) > 2 and values[2] > 0.0 else None
|
|
398
|
+
wall = values[3] if len(values) > 3 and values[3] > 0.0 else None
|
|
399
|
+
if outer is not None:
|
|
400
|
+
dim["web_height"] = float(outer)
|
|
401
|
+
dim["flange_width"] = float(outer)
|
|
402
|
+
if wall is not None:
|
|
403
|
+
dim["web_thickness"] = float(wall)
|
|
404
|
+
dim["flange_thickness"] = float(wall)
|
|
405
|
+
text_dim["section_type"] = "Pipe"
|
|
406
|
+
if dim or text_dim:
|
|
407
|
+
dimensions[section_id] = {**dim, **text_dim} # type: ignore[dict-item]
|
|
408
|
+
|
|
409
|
+
for section_id, dim in dimensions.items():
|
|
410
|
+
name_text = names.get(section_id, "")
|
|
411
|
+
if _is_bulb_section_name(name_text):
|
|
412
|
+
dim["section_type"] = "L-bulb"
|
|
413
|
+
if section_id in sections:
|
|
414
|
+
from dataclasses import replace
|
|
415
|
+
sections[section_id] = replace(sections[section_id], **dim)
|
|
416
|
+
else:
|
|
417
|
+
sections[section_id] = FemSection(section_id=section_id, kind="beam_section", **dim)
|
|
418
|
+
|
|
419
|
+
return sections, names
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _is_bulb_section_name(name: object) -> bool:
|
|
423
|
+
normalized = re.sub(r"[^a-z0-9]+", " ", str(name).lower()).strip()
|
|
424
|
+
words = set(normalized.split())
|
|
425
|
+
# Bulb names are often one token with a maker prefix (HPbulb220x11,
|
|
426
|
+
# BSRAbulb220x11x31x9x9x3), so match "bulb" as a substring too.
|
|
427
|
+
return "bulb" in normalized.replace(" ", "") or "hp" in words
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _parse_coordinates(
|
|
431
|
+
records: tuple[FemRawRecord, ...],
|
|
432
|
+
diagnostics: list[FemDiagnostic],
|
|
433
|
+
) -> dict[int, FemCoordinate]:
|
|
434
|
+
coordinates: dict[int, FemCoordinate] = {}
|
|
435
|
+
for record in records:
|
|
436
|
+
if record.name != "GCOORD":
|
|
437
|
+
continue
|
|
438
|
+
if not record.numeric_fields:
|
|
439
|
+
diagnostics.append(_diag("FEM101", "GCOORD record has no coordinate id", record))
|
|
440
|
+
continue
|
|
441
|
+
coordinate_id = _int_field(record.numeric_fields[0], "coordinate id", record, diagnostics)
|
|
442
|
+
if coordinate_id is None:
|
|
443
|
+
continue
|
|
444
|
+
if coordinate_id in coordinates:
|
|
445
|
+
diagnostics.append(_diag("FEM102", f"duplicate coordinate id {coordinate_id}", record))
|
|
446
|
+
continue
|
|
447
|
+
coordinates[coordinate_id] = FemCoordinate(coordinate_id, record.numeric_fields)
|
|
448
|
+
return coordinates
|
|
449
|
+
|
|
450
|
+
def _parse_unit_vectors(
|
|
451
|
+
records: tuple[FemRawRecord, ...],
|
|
452
|
+
diagnostics: list[FemDiagnostic],
|
|
453
|
+
) -> dict[int, FemUnitVector]:
|
|
454
|
+
vectors: dict[int, FemUnitVector] = {}
|
|
455
|
+
for record in records:
|
|
456
|
+
if record.name != "GUNIVEC":
|
|
457
|
+
continue
|
|
458
|
+
values = record.numeric_fields
|
|
459
|
+
if len(values) < 4:
|
|
460
|
+
diagnostics.append(_diag("FEM101", "GUNIVEC record must contain id and vector components", record))
|
|
461
|
+
continue
|
|
462
|
+
transform_id = _int_field(values[0], "unit vector id", record, diagnostics)
|
|
463
|
+
if transform_id is None:
|
|
464
|
+
continue
|
|
465
|
+
if transform_id in vectors:
|
|
466
|
+
diagnostics.append(_diag("FEM102", f"duplicate unit vector id {transform_id}", record))
|
|
467
|
+
continue
|
|
468
|
+
vectors[transform_id] = FemUnitVector(
|
|
469
|
+
transform_id=transform_id,
|
|
470
|
+
vector=(float(values[1]), float(values[2]), float(values[3])),
|
|
471
|
+
raw_values=values,
|
|
472
|
+
)
|
|
473
|
+
return vectors
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# BNTRCOS stores the transformation from global to local coordinates. The rows
|
|
477
|
+
# are local axis direction cosines in global coordinates for stress recovery.
|
|
478
|
+
def _parse_coordinate_transforms(
|
|
479
|
+
records: tuple[FemRawRecord, ...],
|
|
480
|
+
diagnostics: list[FemDiagnostic],
|
|
481
|
+
) -> dict[int, FemCoordinateTransform]:
|
|
482
|
+
transforms: dict[int, FemCoordinateTransform] = {}
|
|
483
|
+
for record in records:
|
|
484
|
+
if record.name != "BNTRCOS":
|
|
485
|
+
continue
|
|
486
|
+
values = record.numeric_fields
|
|
487
|
+
if len(values) < 10:
|
|
488
|
+
diagnostics.append(_diag("FEM101", "BNTRCOS record must contain id and 9 direction cosines", record))
|
|
489
|
+
continue
|
|
490
|
+
transform_id = _int_field(values[0], "coordinate transform id", record, diagnostics)
|
|
491
|
+
if transform_id is None:
|
|
492
|
+
continue
|
|
493
|
+
if transform_id in transforms:
|
|
494
|
+
diagnostics.append(_diag("FEM102", f"duplicate coordinate transform id {transform_id}", record))
|
|
495
|
+
continue
|
|
496
|
+
matrix = (
|
|
497
|
+
(float(values[1]), float(values[2]), float(values[3])),
|
|
498
|
+
(float(values[4]), float(values[5]), float(values[6])),
|
|
499
|
+
(float(values[7]), float(values[8]), float(values[9])),
|
|
500
|
+
)
|
|
501
|
+
transforms[transform_id] = FemCoordinateTransform(transform_id, matrix, values)
|
|
502
|
+
return transforms
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _parse_nodes(records: tuple[FemRawRecord, ...], diagnostics: list[FemDiagnostic]) -> dict[int, FemNode]:
|
|
506
|
+
nodes: dict[int, FemNode] = {}
|
|
507
|
+
for record in records:
|
|
508
|
+
if record.name != "GCOORD":
|
|
509
|
+
continue
|
|
510
|
+
values = record.numeric_fields
|
|
511
|
+
if len(values) < 4:
|
|
512
|
+
diagnostics.append(_diag("FEM101", "GCOORD record must contain node id and coordinates", record))
|
|
513
|
+
continue
|
|
514
|
+
node_id = _int_field(values[0], "node id", record, diagnostics)
|
|
515
|
+
if node_id is None:
|
|
516
|
+
continue
|
|
517
|
+
if node_id in nodes:
|
|
518
|
+
diagnostics.append(_diag("FEM102", f"duplicate node id {node_id}", record))
|
|
519
|
+
continue
|
|
520
|
+
nodes[node_id] = FemNode(
|
|
521
|
+
node_id=node_id,
|
|
522
|
+
coordinate_system_id=0,
|
|
523
|
+
coordinates=(float(values[1]), float(values[2]), float(values[3])),
|
|
524
|
+
raw_values=values,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
for record in records:
|
|
528
|
+
if record.name != "GNODE":
|
|
529
|
+
continue
|
|
530
|
+
values = record.numeric_fields
|
|
531
|
+
if len(values) < 4:
|
|
532
|
+
diagnostics.append(_diag("FEM101", "GNODE record must contain id and coordinates", record))
|
|
533
|
+
continue
|
|
534
|
+
node_id = _int_field(values[0], "node id", record, diagnostics)
|
|
535
|
+
if node_id is None:
|
|
536
|
+
continue
|
|
537
|
+
if node_id in nodes:
|
|
538
|
+
continue
|
|
539
|
+
if len(values) >= 5:
|
|
540
|
+
coordinate_id = _int_field(values[1], "node coordinate system id", record, diagnostics)
|
|
541
|
+
coords = values[2:5]
|
|
542
|
+
else:
|
|
543
|
+
coordinate_id = 0
|
|
544
|
+
coords = values[1:4]
|
|
545
|
+
if coordinate_id is None:
|
|
546
|
+
continue
|
|
547
|
+
nodes[node_id] = FemNode(
|
|
548
|
+
node_id=node_id,
|
|
549
|
+
coordinate_system_id=coordinate_id,
|
|
550
|
+
coordinates=(float(coords[0]), float(coords[1]), float(coords[2])),
|
|
551
|
+
raw_values=values,
|
|
552
|
+
)
|
|
553
|
+
return nodes
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _parse_element_references(
|
|
557
|
+
records: tuple[FemRawRecord, ...],
|
|
558
|
+
diagnostics: list[FemDiagnostic],
|
|
559
|
+
known_transform_ids: set[int],
|
|
560
|
+
) -> dict[int, FemElementReference]:
|
|
561
|
+
references: dict[int, FemElementReference] = {}
|
|
562
|
+
for record in records:
|
|
563
|
+
if record.name != "GELREF1":
|
|
564
|
+
continue
|
|
565
|
+
values = record.numeric_fields
|
|
566
|
+
if not values:
|
|
567
|
+
diagnostics.append(_diag("FEM101", "GELREF1 record has no element id", record))
|
|
568
|
+
continue
|
|
569
|
+
element_id = _int_field(values[0], "element id", record, diagnostics)
|
|
570
|
+
if element_id is None:
|
|
571
|
+
continue
|
|
572
|
+
material_id = _optional_int(values[1], "material id", record, diagnostics) if len(values) > 1 else None
|
|
573
|
+
section_id = None
|
|
574
|
+
for index in (8, 2):
|
|
575
|
+
if len(values) > index and values[index] > 0.0:
|
|
576
|
+
section_id = _optional_int(values[index], "section id", record, diagnostics)
|
|
577
|
+
break
|
|
578
|
+
transform_id = (
|
|
579
|
+
_parse_transform_reference(values[11], record, diagnostics, known_transform_ids)
|
|
580
|
+
if len(values) > 11
|
|
581
|
+
else None
|
|
582
|
+
)
|
|
583
|
+
nodal_transform_ids = _parse_nodal_transform_references(values, record, diagnostics, known_transform_ids)
|
|
584
|
+
references[element_id] = FemElementReference(
|
|
585
|
+
element_id,
|
|
586
|
+
material_id,
|
|
587
|
+
section_id,
|
|
588
|
+
transform_id,
|
|
589
|
+
nodal_transform_ids,
|
|
590
|
+
values,
|
|
591
|
+
)
|
|
592
|
+
return references
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _parse_transform_reference(
|
|
596
|
+
value: float,
|
|
597
|
+
record: FemRawRecord,
|
|
598
|
+
diagnostics: list[FemDiagnostic],
|
|
599
|
+
known_transform_ids: set[int],
|
|
600
|
+
) -> Optional[int]:
|
|
601
|
+
reference = _optional_int(value, "transform id", record, diagnostics)
|
|
602
|
+
if reference is None or reference <= 0:
|
|
603
|
+
return None
|
|
604
|
+
return reference if reference in known_transform_ids else None
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _parse_nodal_transform_references(
|
|
608
|
+
values: tuple[float, ...],
|
|
609
|
+
record: FemRawRecord,
|
|
610
|
+
diagnostics: list[FemDiagnostic],
|
|
611
|
+
known_transform_ids: set[int],
|
|
612
|
+
) -> tuple[int, ...]:
|
|
613
|
+
if len(values) <= 12 or len(values) <= 11 or values[11] >= 0.0:
|
|
614
|
+
return ()
|
|
615
|
+
tail_count = len(values) - 12
|
|
616
|
+
if tail_count <= 0 or tail_count % 4 != 0:
|
|
617
|
+
return ()
|
|
618
|
+
node_count = tail_count // 4
|
|
619
|
+
transform_values = values[12 + 3 * node_count:12 + 4 * node_count]
|
|
620
|
+
transform_ids: list[int] = []
|
|
621
|
+
for value in transform_values:
|
|
622
|
+
reference = _parse_transform_reference(value, record, diagnostics, known_transform_ids)
|
|
623
|
+
if reference is not None:
|
|
624
|
+
transform_ids.append(reference)
|
|
625
|
+
return tuple(transform_ids)
|
|
626
|
+
|
|
627
|
+
def _parse_elements(
|
|
628
|
+
records: tuple[FemRawRecord, ...],
|
|
629
|
+
references: Mapping[int, FemElementReference],
|
|
630
|
+
diagnostics: list[FemDiagnostic],
|
|
631
|
+
) -> dict[int, FemElement]:
|
|
632
|
+
elements: dict[int, FemElement] = {}
|
|
633
|
+
for record in records:
|
|
634
|
+
if record.name != "GELMNT1":
|
|
635
|
+
continue
|
|
636
|
+
values = record.numeric_fields
|
|
637
|
+
if len(values) < 3:
|
|
638
|
+
diagnostics.append(_diag("FEM101", "GELMNT1 record is too short", record))
|
|
639
|
+
continue
|
|
640
|
+
element_id = _int_field(values[0], "element id", record, diagnostics)
|
|
641
|
+
if element_id is None:
|
|
642
|
+
continue
|
|
643
|
+
if element_id in elements:
|
|
644
|
+
diagnostics.append(_diag("FEM102", f"duplicate element id {element_id}", record))
|
|
645
|
+
continue
|
|
646
|
+
layout = _element_layout(record, diagnostics)
|
|
647
|
+
if layout is None:
|
|
648
|
+
continue
|
|
649
|
+
type_code, node_start = layout
|
|
650
|
+
spec = get_element_spec(type_code)
|
|
651
|
+
if spec is None:
|
|
652
|
+
diagnostics.append(_diag("FEM103", f"unsupported SESAM element type {type_code}", record))
|
|
653
|
+
continue
|
|
654
|
+
if len(values) < node_start + spec.node_count:
|
|
655
|
+
diagnostics.append(
|
|
656
|
+
_diag(
|
|
657
|
+
"FEM104",
|
|
658
|
+
f"element {element_id} type {type_code} expects {spec.node_count} nodes",
|
|
659
|
+
record,
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
continue
|
|
663
|
+
node_ids: list[int] = []
|
|
664
|
+
for value in values[node_start:node_start + spec.node_count]:
|
|
665
|
+
node_id = _int_field(value, "element node id", record, diagnostics)
|
|
666
|
+
if node_id is not None:
|
|
667
|
+
node_ids.append(node_id)
|
|
668
|
+
if len(node_ids) != spec.node_count:
|
|
669
|
+
continue
|
|
670
|
+
internal_id = _int_field(values[1], "element internal id", record, diagnostics) if len(values) > 1 else None
|
|
671
|
+
if spec.is_shell and internal_id is not None:
|
|
672
|
+
reference = references.get(internal_id) or references.get(element_id)
|
|
673
|
+
elif spec.is_beam and internal_id is not None:
|
|
674
|
+
reference = references.get(element_id) or references.get(internal_id)
|
|
675
|
+
else:
|
|
676
|
+
reference = references.get(element_id)
|
|
677
|
+
elements[element_id] = FemElement(
|
|
678
|
+
element_id=element_id,
|
|
679
|
+
type_code=type_code,
|
|
680
|
+
topology=spec.topology,
|
|
681
|
+
node_ids=tuple(node_ids),
|
|
682
|
+
material_id=reference.material_id if reference else None,
|
|
683
|
+
section_id=reference.section_id if reference else None,
|
|
684
|
+
raw_values=values,
|
|
685
|
+
)
|
|
686
|
+
return elements
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _parse_boundaries(
|
|
690
|
+
records: tuple[FemRawRecord, ...],
|
|
691
|
+
diagnostics: list[FemDiagnostic],
|
|
692
|
+
) -> tuple[FemBoundary, ...]:
|
|
693
|
+
boundaries: list[FemBoundary] = []
|
|
694
|
+
for record in records:
|
|
695
|
+
if record.name != "BNBCD":
|
|
696
|
+
continue
|
|
697
|
+
values = record.numeric_fields
|
|
698
|
+
if len(values) < 2:
|
|
699
|
+
diagnostics.append(_diag("FEM101", "BNBCD record is too short", record))
|
|
700
|
+
continue
|
|
701
|
+
node_id = _int_field(values[0], "boundary node id", record, diagnostics)
|
|
702
|
+
if node_id is None:
|
|
703
|
+
continue
|
|
704
|
+
flags = []
|
|
705
|
+
start_index = 2 if len(values) >= 8 else 1
|
|
706
|
+
for value in values[start_index:start_index + 6]:
|
|
707
|
+
flag = _optional_int(value, "boundary dof flag", record, diagnostics)
|
|
708
|
+
if flag is not None:
|
|
709
|
+
flags.append(flag)
|
|
710
|
+
boundaries.append(
|
|
711
|
+
FemBoundary(
|
|
712
|
+
node_id=node_id,
|
|
713
|
+
dof_flags=tuple(flags),
|
|
714
|
+
prescribed_values=tuple(values[start_index + 6:start_index + 12]),
|
|
715
|
+
raw_values=values,
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
return tuple(boundaries)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _parse_concepts(records: tuple[FemRawRecord, ...], diagnostics: list[FemDiagnostic]) -> tuple[FemConceptRecord, ...]:
|
|
722
|
+
concepts: list[FemConceptRecord] = []
|
|
723
|
+
for record in records:
|
|
724
|
+
if record.name not in {"TDSCONC", "SCONCEPT", "SCONMESH"}:
|
|
725
|
+
continue
|
|
726
|
+
concept_id = None
|
|
727
|
+
if record.numeric_fields:
|
|
728
|
+
concept_id = _optional_int(record.numeric_fields[0], "concept id", record, diagnostics)
|
|
729
|
+
concepts.append(FemConceptRecord(record.name, concept_id, record.text_fields, record.numeric_fields))
|
|
730
|
+
return tuple(concepts)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _parse_dependencies(records: tuple[FemRawRecord, ...]) -> tuple[FemDependency, ...]:
|
|
734
|
+
return tuple(
|
|
735
|
+
FemDependency(record.name, record.numeric_fields, record.text_fields)
|
|
736
|
+
for record in records
|
|
737
|
+
if record.name == "BLDEP"
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _parse_loads(records: tuple[FemRawRecord, ...], diagnostics: list[FemDiagnostic]) -> tuple[FemLoadRecord, ...]:
|
|
742
|
+
loads: list[FemLoadRecord] = []
|
|
743
|
+
for record in records:
|
|
744
|
+
if record.name not in {"TDLOAD", "BEUSLO", "BNLOAD", "BNACCLO", "BGRAV"}:
|
|
745
|
+
continue
|
|
746
|
+
load_case_id = None
|
|
747
|
+
target_id = None
|
|
748
|
+
if record.numeric_fields:
|
|
749
|
+
load_case_id = _optional_int(record.numeric_fields[0], "load case id", record, diagnostics)
|
|
750
|
+
if len(record.numeric_fields) > 1:
|
|
751
|
+
target_id = _optional_int(record.numeric_fields[1], "load target id", record, diagnostics)
|
|
752
|
+
loads.append(FemLoadRecord(record.name, load_case_id, target_id, record.numeric_fields, record.text_fields))
|
|
753
|
+
return tuple(loads)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _element_layout(
|
|
757
|
+
record: FemRawRecord,
|
|
758
|
+
diagnostics: list[FemDiagnostic],
|
|
759
|
+
) -> Optional[tuple[int, int]]:
|
|
760
|
+
values = record.numeric_fields
|
|
761
|
+
candidate_indices = (2, 1)
|
|
762
|
+
for index in candidate_indices:
|
|
763
|
+
if len(values) <= index:
|
|
764
|
+
continue
|
|
765
|
+
try:
|
|
766
|
+
type_code = strict_int(values[index], field_name="element type", record=record)
|
|
767
|
+
except SesamFemError as exc:
|
|
768
|
+
diagnostics.extend(exc.diagnostics)
|
|
769
|
+
continue
|
|
770
|
+
if get_element_spec(type_code) is not None:
|
|
771
|
+
return type_code, 4 if index == 2 else 2
|
|
772
|
+
diagnostics.append(_diag("FEM103", "GELMNT1 record does not contain a supported element type", record))
|
|
773
|
+
return None
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _first_in_range(values: tuple[float, ...], lower: float, upper: float) -> Optional[float]:
|
|
777
|
+
for value in values:
|
|
778
|
+
if lower < abs(float(value)) < upper:
|
|
779
|
+
return float(value)
|
|
780
|
+
return None
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _int_field(
|
|
784
|
+
value: float,
|
|
785
|
+
field_name: str,
|
|
786
|
+
record: FemRawRecord,
|
|
787
|
+
diagnostics: list[FemDiagnostic],
|
|
788
|
+
) -> Optional[int]:
|
|
789
|
+
try:
|
|
790
|
+
return strict_int(value, field_name=field_name, record=record)
|
|
791
|
+
except SesamFemError as exc:
|
|
792
|
+
diagnostics.extend(exc.diagnostics)
|
|
793
|
+
return None
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _optional_int(
|
|
797
|
+
value: float,
|
|
798
|
+
field_name: str,
|
|
799
|
+
record: FemRawRecord,
|
|
800
|
+
diagnostics: list[FemDiagnostic],
|
|
801
|
+
) -> Optional[int]:
|
|
802
|
+
if abs(float(value)) < 1.0e-12:
|
|
803
|
+
return 0
|
|
804
|
+
return _int_field(value, field_name, record, diagnostics)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _diag(code: str, message: str, record: FemRawRecord) -> FemDiagnostic:
|
|
808
|
+
return FemDiagnostic(
|
|
809
|
+
code,
|
|
810
|
+
message,
|
|
811
|
+
record_name=record.name,
|
|
812
|
+
line_start=record.source_line_start,
|
|
813
|
+
line_end=record.source_line_end,
|
|
814
|
+
)
|