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,367 @@
|
|
|
1
|
+
"""Internal shell benchmark runners.
|
|
2
|
+
|
|
3
|
+
These helpers produce FE-solver-side values in the same shape as the upstream
|
|
4
|
+
CalculiX shell convergence tables: mesh size, node count, maximum stress,
|
|
5
|
+
maximum displacement and normalized values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .assembly import solve_linear
|
|
17
|
+
from .boundary import LoadCase
|
|
18
|
+
from .mesh_gen import generate_simple_panel_mesh
|
|
19
|
+
from .reference_cases import ShellConvergencePoint, ShellConvergenceTable, upstream_calculix_shell_reference_values
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ShellBenchmarkResult:
|
|
24
|
+
"""One internal shell benchmark result point."""
|
|
25
|
+
|
|
26
|
+
element_type: str
|
|
27
|
+
mesh_size: float
|
|
28
|
+
divisions_x: int
|
|
29
|
+
divisions_y: int
|
|
30
|
+
node_count: int
|
|
31
|
+
element_count: int
|
|
32
|
+
pressure: float
|
|
33
|
+
max_out_of_plane_displacement: float
|
|
34
|
+
max_displacement_norm: float
|
|
35
|
+
max_von_mises_stress: float
|
|
36
|
+
displacement_reference: float
|
|
37
|
+
stress_reference: float
|
|
38
|
+
displacement_normalized: float
|
|
39
|
+
stress_normalized: float
|
|
40
|
+
solver_status: str
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"element_type": self.element_type,
|
|
45
|
+
"mesh_size": self.mesh_size,
|
|
46
|
+
"divisions_x": self.divisions_x,
|
|
47
|
+
"divisions_y": self.divisions_y,
|
|
48
|
+
"node_count": self.node_count,
|
|
49
|
+
"element_count": self.element_count,
|
|
50
|
+
"pressure": self.pressure,
|
|
51
|
+
"max_out_of_plane_displacement": self.max_out_of_plane_displacement,
|
|
52
|
+
"max_displacement_norm": self.max_displacement_norm,
|
|
53
|
+
"max_von_mises_stress": self.max_von_mises_stress,
|
|
54
|
+
"displacement_reference": self.displacement_reference,
|
|
55
|
+
"stress_reference": self.stress_reference,
|
|
56
|
+
"displacement_normalized": self.displacement_normalized,
|
|
57
|
+
"stress_normalized": self.stress_normalized,
|
|
58
|
+
"solver_status": self.solver_status,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ShellBenchmarkComparisonPoint:
|
|
63
|
+
"""Loose/informational comparison between one external and one internal row."""
|
|
64
|
+
|
|
65
|
+
external_size: float
|
|
66
|
+
internal_size: float
|
|
67
|
+
external_node_count: int
|
|
68
|
+
internal_node_count: int
|
|
69
|
+
external_stress_normalized: float
|
|
70
|
+
internal_stress_normalized: float
|
|
71
|
+
external_displacement_normalized: float
|
|
72
|
+
internal_displacement_normalized: float
|
|
73
|
+
stress_normalized_delta: float
|
|
74
|
+
displacement_normalized_delta: float
|
|
75
|
+
stress_ratio_internal_to_external: float
|
|
76
|
+
displacement_ratio_internal_to_external: float
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
79
|
+
return {
|
|
80
|
+
"external_size": self.external_size,
|
|
81
|
+
"internal_size": self.internal_size,
|
|
82
|
+
"external_node_count": self.external_node_count,
|
|
83
|
+
"internal_node_count": self.internal_node_count,
|
|
84
|
+
"external_stress_normalized": self.external_stress_normalized,
|
|
85
|
+
"internal_stress_normalized": self.internal_stress_normalized,
|
|
86
|
+
"external_displacement_normalized": self.external_displacement_normalized,
|
|
87
|
+
"internal_displacement_normalized": self.internal_displacement_normalized,
|
|
88
|
+
"stress_normalized_delta": self.stress_normalized_delta,
|
|
89
|
+
"displacement_normalized_delta": self.displacement_normalized_delta,
|
|
90
|
+
"stress_ratio_internal_to_external": self.stress_ratio_internal_to_external,
|
|
91
|
+
"displacement_ratio_internal_to_external": self.displacement_ratio_internal_to_external,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class ShellBenchmarkComparison:
|
|
97
|
+
"""Summary of a loose external-vs-internal shell benchmark comparison."""
|
|
98
|
+
|
|
99
|
+
external_element_type: str
|
|
100
|
+
internal_element_type: str
|
|
101
|
+
points: Tuple[ShellBenchmarkComparisonPoint, ...]
|
|
102
|
+
notes: Tuple[str, ...]
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def max_abs_stress_normalized_delta(self) -> float:
|
|
106
|
+
return max((abs(point.stress_normalized_delta) for point in self.points), default=0.0)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def max_abs_displacement_normalized_delta(self) -> float:
|
|
110
|
+
return max((abs(point.displacement_normalized_delta) for point in self.points), default=0.0)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
113
|
+
return {
|
|
114
|
+
"external_element_type": self.external_element_type,
|
|
115
|
+
"internal_element_type": self.internal_element_type,
|
|
116
|
+
"max_abs_stress_normalized_delta": self.max_abs_stress_normalized_delta,
|
|
117
|
+
"max_abs_displacement_normalized_delta": self.max_abs_displacement_normalized_delta,
|
|
118
|
+
"points": [point.to_dict() for point in self.points],
|
|
119
|
+
"notes": list(self.notes),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _reference(value: Optional[float], default: float, name: str) -> float:
|
|
124
|
+
ref = float(default if value is None else value)
|
|
125
|
+
if ref == 0.0:
|
|
126
|
+
raise ValueError(f"{name} must be non-zero")
|
|
127
|
+
return ref
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _add_pressure_loads(load_case: LoadCase, model, pressure: float) -> None:
|
|
131
|
+
for element_id in model.mesh.elements:
|
|
132
|
+
load_case.add_pressure_load(int(element_id), float(pressure))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _max_displacements(model, displacements: np.ndarray) -> Tuple[float, float]:
|
|
136
|
+
u = np.asarray(displacements, dtype=float).reshape(-1)
|
|
137
|
+
max_w = 0.0
|
|
138
|
+
max_norm = 0.0
|
|
139
|
+
for node in model.mesh.nodes.values():
|
|
140
|
+
translation = u[node.dofs[:3]]
|
|
141
|
+
max_w = max(max_w, abs(float(translation[2])))
|
|
142
|
+
max_norm = max(max_norm, float(np.linalg.norm(translation)))
|
|
143
|
+
return max_w, max_norm
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _max_von_mises(model, displacements: np.ndarray) -> float:
|
|
147
|
+
values: List[float] = []
|
|
148
|
+
for element in model.mesh.elements.values():
|
|
149
|
+
if not hasattr(element, "compute_stresses"):
|
|
150
|
+
continue
|
|
151
|
+
material = model.get_material(element.material_name)
|
|
152
|
+
stresses = element.compute_stresses(model.mesh, displacements, material)
|
|
153
|
+
if "von_mises" in stresses:
|
|
154
|
+
values.extend(float(value) for value in np.asarray(stresses["von_mises"], dtype=float).reshape(-1))
|
|
155
|
+
return max((abs(value) for value in values), default=0.0)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _safe_ratio(numerator: float, denominator: float) -> float:
|
|
159
|
+
if denominator == 0.0:
|
|
160
|
+
return float("nan")
|
|
161
|
+
return float(numerator / denominator)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_simple_supported_shell_benchmark(
|
|
165
|
+
length: float = 1.0,
|
|
166
|
+
width: float = 1.0,
|
|
167
|
+
thickness: float = 0.01,
|
|
168
|
+
divisions_x: int = 4,
|
|
169
|
+
divisions_y: int = 4,
|
|
170
|
+
pressure: float = 1000.0,
|
|
171
|
+
use_8node_elements: bool = False,
|
|
172
|
+
stress_reference: Optional[float] = None,
|
|
173
|
+
displacement_reference: Optional[float] = None,
|
|
174
|
+
) -> ShellBenchmarkResult:
|
|
175
|
+
"""Run a rectangular shell panel under a constant surface load."""
|
|
176
|
+
references = upstream_calculix_shell_reference_values()
|
|
177
|
+
sref = _reference(stress_reference, references.get("sref", 1.0), "stress_reference")
|
|
178
|
+
wref = _reference(displacement_reference, references.get("wref", 1.0), "displacement_reference")
|
|
179
|
+
|
|
180
|
+
model = generate_simple_panel_mesh(
|
|
181
|
+
length,
|
|
182
|
+
width,
|
|
183
|
+
thickness,
|
|
184
|
+
num_divisions_x=divisions_x,
|
|
185
|
+
num_divisions_y=divisions_y,
|
|
186
|
+
use_8node_elements=use_8node_elements,
|
|
187
|
+
)
|
|
188
|
+
load_case = LoadCase("constant_surface_load")
|
|
189
|
+
_add_pressure_loads(load_case, model, pressure)
|
|
190
|
+
displacements, solver_info = solve_linear(model, load_case)
|
|
191
|
+
solver_status = str((solver_info.get("convergence_info") or {}).get("status", "unknown"))
|
|
192
|
+
|
|
193
|
+
max_w, max_norm = _max_displacements(model, displacements)
|
|
194
|
+
max_vm = _max_von_mises(model, displacements)
|
|
195
|
+
return ShellBenchmarkResult(
|
|
196
|
+
element_type="S8" if use_8node_elements else "S4",
|
|
197
|
+
mesh_size=max(float(length) / float(divisions_x), float(width) / float(divisions_y)),
|
|
198
|
+
divisions_x=int(divisions_x),
|
|
199
|
+
divisions_y=int(divisions_y),
|
|
200
|
+
node_count=int(model.mesh.num_nodes),
|
|
201
|
+
element_count=int(len(model.mesh.elements)),
|
|
202
|
+
pressure=float(pressure),
|
|
203
|
+
max_out_of_plane_displacement=max_w,
|
|
204
|
+
max_displacement_norm=max_norm,
|
|
205
|
+
max_von_mises_stress=max_vm,
|
|
206
|
+
displacement_reference=wref,
|
|
207
|
+
stress_reference=sref,
|
|
208
|
+
displacement_normalized=max_w / wref,
|
|
209
|
+
stress_normalized=max_vm / sref,
|
|
210
|
+
solver_status=solver_status,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def run_simple_supported_shell_convergence(
|
|
215
|
+
divisions: Sequence[int] = (2, 4, 8),
|
|
216
|
+
length: float = 1.0,
|
|
217
|
+
width: float = 1.0,
|
|
218
|
+
thickness: float = 0.01,
|
|
219
|
+
pressure: float = 1000.0,
|
|
220
|
+
use_8node_elements: bool = False,
|
|
221
|
+
stress_reference: Optional[float] = None,
|
|
222
|
+
displacement_reference: Optional[float] = None,
|
|
223
|
+
) -> Tuple[ShellBenchmarkResult, ...]:
|
|
224
|
+
"""Run a small internal shell mesh-convergence sweep."""
|
|
225
|
+
results: List[ShellBenchmarkResult] = []
|
|
226
|
+
for division in divisions:
|
|
227
|
+
div = int(division)
|
|
228
|
+
if div <= 0:
|
|
229
|
+
raise ValueError("divisions must contain positive integers")
|
|
230
|
+
results.append(
|
|
231
|
+
run_simple_supported_shell_benchmark(
|
|
232
|
+
length=length,
|
|
233
|
+
width=width,
|
|
234
|
+
thickness=thickness,
|
|
235
|
+
divisions_x=div,
|
|
236
|
+
divisions_y=div,
|
|
237
|
+
pressure=pressure,
|
|
238
|
+
use_8node_elements=use_8node_elements,
|
|
239
|
+
stress_reference=stress_reference,
|
|
240
|
+
displacement_reference=displacement_reference,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
return tuple(results)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def shell_benchmark_results_to_convergence_table(
|
|
247
|
+
results: Sequence[ShellBenchmarkResult],
|
|
248
|
+
path: Path | str = Path("<internal>"),
|
|
249
|
+
) -> ShellConvergenceTable:
|
|
250
|
+
"""Convert internal benchmark results to the same table shape as parsed CalculiX files."""
|
|
251
|
+
if not results:
|
|
252
|
+
raise ValueError("results must contain at least one ShellBenchmarkResult")
|
|
253
|
+
|
|
254
|
+
element_type = results[0].element_type
|
|
255
|
+
stress_reference = results[0].stress_reference
|
|
256
|
+
displacement_reference = results[0].displacement_reference
|
|
257
|
+
points = []
|
|
258
|
+
for result in results:
|
|
259
|
+
if result.element_type != element_type:
|
|
260
|
+
raise ValueError("All internal results must use the same element type")
|
|
261
|
+
points.append(
|
|
262
|
+
ShellConvergencePoint(
|
|
263
|
+
element_type=result.element_type,
|
|
264
|
+
size=result.mesh_size,
|
|
265
|
+
node_count=result.node_count,
|
|
266
|
+
stress_max=result.max_von_mises_stress,
|
|
267
|
+
displacement_max=result.max_out_of_plane_displacement,
|
|
268
|
+
stress_normalized=result.stress_normalized,
|
|
269
|
+
displacement_normalized=result.displacement_normalized,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return ShellConvergenceTable(
|
|
274
|
+
element_type=element_type,
|
|
275
|
+
path=Path(path),
|
|
276
|
+
stress_reference=float(stress_reference),
|
|
277
|
+
displacement_reference=float(displacement_reference),
|
|
278
|
+
points=tuple(points),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def write_internal_shell_convergence_table(
|
|
283
|
+
results: Sequence[ShellBenchmarkResult],
|
|
284
|
+
path: Path | str,
|
|
285
|
+
) -> Path:
|
|
286
|
+
"""Write an internal shell convergence table.
|
|
287
|
+
|
|
288
|
+
The output format intentionally mirrors the upstream CalculiX text tables
|
|
289
|
+
with two added normalized columns:
|
|
290
|
+
|
|
291
|
+
# size NoN smax umax s_norm u_norm
|
|
292
|
+
"""
|
|
293
|
+
table_path = Path(path)
|
|
294
|
+
table_path.parent.mkdir(parents=True, exist_ok=True)
|
|
295
|
+
lines = ["# size NoN smax umax s_norm u_norm"]
|
|
296
|
+
for result in results:
|
|
297
|
+
lines.append(
|
|
298
|
+
" ".join(
|
|
299
|
+
[
|
|
300
|
+
f"{result.mesh_size:.12g}",
|
|
301
|
+
str(int(result.node_count)),
|
|
302
|
+
f"{result.max_von_mises_stress:.12g}",
|
|
303
|
+
f"{result.max_out_of_plane_displacement:.12g}",
|
|
304
|
+
f"{result.stress_normalized:.12g}",
|
|
305
|
+
f"{result.displacement_normalized:.12g}",
|
|
306
|
+
]
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
table_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
310
|
+
return table_path
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _as_internal_table(
|
|
314
|
+
internal: Union[ShellConvergenceTable, Sequence[ShellBenchmarkResult]],
|
|
315
|
+
) -> ShellConvergenceTable:
|
|
316
|
+
if isinstance(internal, ShellConvergenceTable):
|
|
317
|
+
return internal
|
|
318
|
+
return shell_benchmark_results_to_convergence_table(internal)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def compare_shell_benchmark_to_reference(
|
|
322
|
+
external_table: ShellConvergenceTable,
|
|
323
|
+
internal: Union[ShellConvergenceTable, Sequence[ShellBenchmarkResult]],
|
|
324
|
+
) -> ShellBenchmarkComparison:
|
|
325
|
+
"""Compare external CalculiX and internal shell convergence data loosely.
|
|
326
|
+
|
|
327
|
+
Rows are paired by order after sorting both tables from coarse to fine
|
|
328
|
+
``size``. This is informational for now: the upstream CalculiX model does
|
|
329
|
+
not yet exactly match the internal geometry, loading or supports.
|
|
330
|
+
"""
|
|
331
|
+
internal_table = _as_internal_table(internal)
|
|
332
|
+
external_points = sorted(external_table.points, key=lambda point: point.size, reverse=True)
|
|
333
|
+
internal_points = sorted(internal_table.points, key=lambda point: point.size, reverse=True)
|
|
334
|
+
point_count = min(len(external_points), len(internal_points))
|
|
335
|
+
|
|
336
|
+
points: List[ShellBenchmarkComparisonPoint] = []
|
|
337
|
+
for external_point, internal_point in zip(external_points[:point_count], internal_points[:point_count]):
|
|
338
|
+
points.append(
|
|
339
|
+
ShellBenchmarkComparisonPoint(
|
|
340
|
+
external_size=external_point.size,
|
|
341
|
+
internal_size=internal_point.size,
|
|
342
|
+
external_node_count=external_point.node_count,
|
|
343
|
+
internal_node_count=internal_point.node_count,
|
|
344
|
+
external_stress_normalized=external_point.stress_normalized,
|
|
345
|
+
internal_stress_normalized=internal_point.stress_normalized,
|
|
346
|
+
external_displacement_normalized=external_point.displacement_normalized,
|
|
347
|
+
internal_displacement_normalized=internal_point.displacement_normalized,
|
|
348
|
+
stress_normalized_delta=internal_point.stress_normalized - external_point.stress_normalized,
|
|
349
|
+
displacement_normalized_delta=internal_point.displacement_normalized - external_point.displacement_normalized,
|
|
350
|
+
stress_ratio_internal_to_external=_safe_ratio(internal_point.stress_normalized, external_point.stress_normalized),
|
|
351
|
+
displacement_ratio_internal_to_external=_safe_ratio(
|
|
352
|
+
internal_point.displacement_normalized,
|
|
353
|
+
external_point.displacement_normalized,
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
notes = (
|
|
359
|
+
"Informational only: upstream CalculiX and internal benchmark geometry/load/supports are not yet identical.",
|
|
360
|
+
"Rows are matched by coarse-to-fine order, not by exact mesh topology.",
|
|
361
|
+
)
|
|
362
|
+
return ShellBenchmarkComparison(
|
|
363
|
+
external_element_type=external_table.element_type,
|
|
364
|
+
internal_element_type=internal_table.element_type,
|
|
365
|
+
points=tuple(points),
|
|
366
|
+
notes=notes,
|
|
367
|
+
)
|