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,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
+ )