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
anysolver/baselines.py ADDED
@@ -0,0 +1,303 @@
1
+ """Deterministic local FE baseline cases and comparison helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import platform
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Dict, Mapping, Optional
12
+
13
+ import numpy as np
14
+
15
+ from .assembly import solve_linear
16
+ from .boundary import BoundaryCondition, FixedSupport, LoadCase
17
+ from .buckling import solve_eigenvalue_buckling
18
+ from .cylinder_benchmarks import CylinderBenchmarkConfig, run_cylindrical_shell_benchmark
19
+ from .dynamics import TransientConfig, solve_transient_newmark
20
+ from .elements import BeamElement
21
+ from .fe_core import FEModel
22
+ from .mesh_gen import generate_beam_mesh, generate_simple_panel_mesh
23
+ from .nonlinear_static import solve_static_nonlinear
24
+
25
+
26
+ DEFAULT_BASELINE_PATH = Path("tests/fixtures/fe_baselines/baseline.json")
27
+
28
+
29
+ def _git_sha() -> Optional[str]:
30
+ try:
31
+ result = subprocess.run(
32
+ ["git", "rev-parse", "HEAD"],
33
+ text=True,
34
+ capture_output=True,
35
+ check=False,
36
+ )
37
+ except Exception:
38
+ return None
39
+ if result.returncode != 0:
40
+ return None
41
+ return result.stdout.strip() or None
42
+
43
+
44
+ def _topology(model: FEModel) -> Dict[str, int]:
45
+ return {
46
+ "nodes": int(model.mesh.num_nodes),
47
+ "elements": int(model.mesh.num_elements),
48
+ "dofs": int(model.mesh.dof_manager.total_dofs),
49
+ "boundary_conditions": int(len(model.boundary_conditions)),
50
+ }
51
+
52
+
53
+ def _float(value: Any) -> float:
54
+ return float(np.asarray(value, dtype=float))
55
+
56
+
57
+ def _max_translation(model: FEModel, displacements: np.ndarray) -> float:
58
+ peak = 0.0
59
+ for node in model.mesh.nodes.values():
60
+ peak = max(peak, float(np.linalg.norm(displacements[node.dofs[:3]])))
61
+ return peak
62
+
63
+
64
+ def _beam_static_case() -> Dict[str, Any]:
65
+ model = generate_beam_mesh(1.0, num_divisions=2, cross_section={"area": 0.01, "Iy": 1.0e-6, "Iz": 1.0e-6, "J": 1.0e-6})
66
+ load_case = LoadCase("beam_tip")
67
+ load_case.add_nodal_load(3, [0.0, 0.0, -1000.0, 0.0, 0.0, 0.0])
68
+ u, info = solve_linear(model, load_case)
69
+ tip = model.mesh.get_node(3)
70
+ return {
71
+ "topology": _topology(model),
72
+ "results": {
73
+ "tip_uz": _float(u[tip.dofs[2]]),
74
+ "max_translation": _max_translation(model, u),
75
+ "solution_norm": _float(np.linalg.norm(u)),
76
+ },
77
+ "residuals": {
78
+ "status": str((info.get("convergence_info") or {}).get("status", "")),
79
+ "relative_residual": _float((info.get("convergence_info") or {}).get("relative_residual", 0.0)),
80
+ },
81
+ }
82
+
83
+
84
+ def _plate_shell_case() -> Dict[str, Any]:
85
+ model = generate_simple_panel_mesh(1.0, 0.6, 0.01, num_divisions_x=2, num_divisions_y=1)
86
+ load_case = LoadCase("plate_pressure")
87
+ for element_id in model.mesh.elements:
88
+ load_case.add_pressure_load(element_id, 1000.0)
89
+ u, info = solve_linear(model, load_case)
90
+ return {
91
+ "topology": _topology(model),
92
+ "results": {
93
+ "max_translation": _max_translation(model, u),
94
+ "solution_norm": _float(np.linalg.norm(u)),
95
+ },
96
+ "residuals": {
97
+ "status": str((info.get("convergence_info") or {}).get("status", "")),
98
+ "relative_residual": _float((info.get("convergence_info") or {}).get("relative_residual", 0.0)),
99
+ },
100
+ }
101
+
102
+
103
+ def _cylinder_case() -> Dict[str, Any]:
104
+ config = CylinderBenchmarkConfig(radius=1.0, height=1.0, thickness=0.02, pressure=1000.0, num_circumferential=8, num_height=2)
105
+ result = run_cylindrical_shell_benchmark(config)
106
+ return {
107
+ "topology": {
108
+ "nodes": int(result.node_count),
109
+ "elements": int(result.element_count),
110
+ "shell_elements": int(result.shell_element_count),
111
+ },
112
+ "results": {
113
+ "max_displacement_norm": float(result.max_displacement_norm),
114
+ "max_radial_displacement": float(result.max_radial_displacement),
115
+ "von_mises_mean": float(result.all_von_mises.mean),
116
+ "nominal_von_mises": float(result.nominal.von_mises_stress),
117
+ },
118
+ "residuals": {
119
+ "status": str(result.solver_status),
120
+ "relative_rigid_body_load_imbalance": float(result.relative_rigid_body_load_imbalance),
121
+ },
122
+ }
123
+
124
+
125
+ def _beam_column_model(num_elements: int = 4) -> FEModel:
126
+ length = 4.0
127
+ model = FEModel("baseline_beam_column")
128
+ model.add_material("steel", 210.0e9, 0.3, density=7850.0)
129
+ model.current_material = "steel"
130
+ for i in range(num_elements + 1):
131
+ model.add_node(i + 1, length * i / num_elements, 0.0, 0.0)
132
+ section = {"area": 0.02, "Iy": 3.0e-6, "Iz": 5.0e-6, "J": 2.0e-6}
133
+ for i in range(num_elements):
134
+ model.add_element(i + 1, BeamElement(i + 1, [i + 1, i + 2], "steel", section))
135
+ all_nodes = list(range(1, num_elements + 2))
136
+ end_nodes = [1, num_elements + 1]
137
+ model.add_boundary_condition(BoundaryCondition("suppress_unrelated_dofs", all_nodes, {"ux": 0.0, "uz": 0.0, "rx": 0.0, "ry": 0.0}))
138
+ model.add_boundary_condition(BoundaryCondition("pinned_lateral_ends", end_nodes, {"uy": 0.0}))
139
+ return model
140
+
141
+
142
+ def _buckling_case() -> Dict[str, Any]:
143
+ model = _beam_column_model(num_elements=4)
144
+ states = {element_id: {"axial_compression": 1.0} for element_id in model.mesh.elements}
145
+ result = solve_eigenvalue_buckling(model, states, num_modes=2)
146
+ return {
147
+ "topology": _topology(model),
148
+ "results": {
149
+ "critical_load_factor": float(result.critical_load_factor or 0.0),
150
+ "num_modes_returned": int(result.num_modes_returned),
151
+ },
152
+ "residuals": {"status": str(result.solver_status)},
153
+ }
154
+
155
+
156
+ def _nonlinear_case() -> Dict[str, Any]:
157
+ model = generate_beam_mesh(1.0, num_divisions=1, cross_section={"area": 0.01, "Iy": 1.0e-6, "Iz": 1.0e-6, "J": 1.0e-6})
158
+ load_case = LoadCase("small_axial")
159
+ load_case.add_nodal_load(2, [100.0, 0.0, 0.0, 0.0, 0.0, 0.0])
160
+ result = solve_static_nonlinear(model, load_case, num_steps=2, max_iterations=8, num_layers=3)
161
+ return {
162
+ "topology": _topology(model),
163
+ "results": {
164
+ "load_factor": float(result.load_factor),
165
+ "peak_load_factor": float(result.peak_load_factor),
166
+ "max_translation": _max_translation(model, result.displacements),
167
+ "steps": int(len(result.steps)),
168
+ },
169
+ "residuals": {"status": str(result.status), "failure_reason": str(result.failure_reason)},
170
+ }
171
+
172
+
173
+ def _transient_case() -> Dict[str, Any]:
174
+ model = FEModel("baseline_axial_sdof")
175
+ model.add_material("steel", elastic_modulus=100.0, poisson_ratio=0.3, density=2.0)
176
+ model.add_node(1, 0.0, 0.0, 0.0)
177
+ model.add_node(2, 1.0, 0.0, 0.0)
178
+ section = {"area": 1.0, "Iy": 1.0e-6, "Iz": 1.0e-6, "J": 1.0e-6}
179
+ model.add_element(1, BeamElement(1, [1, 2], "steel", section))
180
+ model.add_boundary_condition(FixedSupport("fixed", [1]))
181
+ model.add_boundary_condition(BoundaryCondition("slider", [2], {"uy": 0.0, "uz": 0.0, "rx": 0.0, "ry": 0.0, "rz": 0.0}))
182
+ load_case = LoadCase("step")
183
+ load_case.add_nodal_load(2, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0])
184
+ result = solve_transient_newmark(model, TransientConfig(dt=0.001, t_end=0.01), base_load_case=load_case)
185
+ ux = model.mesh.get_node(2).dofs[0]
186
+ return {
187
+ "topology": _topology(model),
188
+ "results": {
189
+ "final_ux": _float(result.displacements[-1, ux]),
190
+ "peak_displacement": float(result.peak_displacement),
191
+ "energy_drift": float(result.diagnostics.get("max_relative_energy_drift", 0.0)),
192
+ },
193
+ "residuals": {"status": str(result.status)},
194
+ }
195
+
196
+
197
+ CASE_BUILDERS: Mapping[str, Callable[[], Dict[str, Any]]] = {
198
+ "beam_static": _beam_static_case,
199
+ "plate_shell_static": _plate_shell_case,
200
+ "cylinder_static": _cylinder_case,
201
+ "beam_column_buckling": _buckling_case,
202
+ "nonlinear_static": _nonlinear_case,
203
+ "transient_newmark": _transient_case,
204
+ }
205
+
206
+
207
+ DEFAULT_TOLERANCES: Dict[str, Dict[str, Dict[str, float]]] = {
208
+ name: {
209
+ "results.*": {"rtol": 1.0e-8, "atol": 1.0e-10},
210
+ "residuals.relative_residual": {"rtol": 0.0, "atol": 1.0e-8},
211
+ "residuals.relative_rigid_body_load_imbalance": {"rtol": 0.0, "atol": 1.0e-8},
212
+ }
213
+ for name in CASE_BUILDERS
214
+ }
215
+
216
+
217
+ def generate_baseline_document(*, include_timing: bool = True, selected_cases: Optional[Mapping[str, Callable[[], Dict[str, Any]]]] = None) -> Dict[str, Any]:
218
+ """Run deterministic baseline cases and return a serializable document."""
219
+ cases: Dict[str, Any] = {}
220
+ for name, builder in (selected_cases or CASE_BUILDERS).items():
221
+ start = time.perf_counter()
222
+ case = builder()
223
+ elapsed = time.perf_counter() - start
224
+ case.setdefault("timing", {})["solve_seconds"] = float(elapsed) if include_timing else None
225
+ cases[name] = case
226
+ return {
227
+ "schema_version": 1,
228
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
229
+ "commit": _git_sha(),
230
+ "environment": {
231
+ "platform": platform.platform(),
232
+ "python": sys.version.split()[0],
233
+ "numpy": np.__version__,
234
+ },
235
+ "cases": cases,
236
+ "tolerances": DEFAULT_TOLERANCES,
237
+ "known_limitations": [
238
+ "Baselines are local deterministic smoke anchors, not external validation references.",
239
+ "Timing fields are informational and excluded from numeric baseline comparison.",
240
+ ],
241
+ }
242
+
243
+
244
+ def write_baseline(path: Path | str = DEFAULT_BASELINE_PATH, *, include_timing: bool = True) -> Dict[str, Any]:
245
+ """Generate and write a baseline JSON document."""
246
+ document = generate_baseline_document(include_timing=include_timing)
247
+ output = Path(path)
248
+ output.parent.mkdir(parents=True, exist_ok=True)
249
+ output.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8")
250
+ return document
251
+
252
+
253
+ def load_baseline(path: Path | str = DEFAULT_BASELINE_PATH) -> Dict[str, Any]:
254
+ return json.loads(Path(path).read_text(encoding="utf-8"))
255
+
256
+
257
+ def _tolerance_for(tolerances: Mapping[str, Mapping[str, Mapping[str, float]]], case_name: str, section: str, metric: str) -> Dict[str, float]:
258
+ case_tolerances = tolerances.get(case_name, {})
259
+ return dict(case_tolerances.get(f"{section}.{metric}", case_tolerances.get(f"{section}.*", {"rtol": 1.0e-8, "atol": 1.0e-10})))
260
+
261
+
262
+ def compare_baseline_documents(reference: Mapping[str, Any], candidate: Mapping[str, Any]) -> Dict[str, Any]:
263
+ """Compare baseline values while excluding metadata and timings."""
264
+ failures = []
265
+ warnings = []
266
+ ref_cases = reference.get("cases", {})
267
+ cand_cases = candidate.get("cases", {})
268
+ tolerances = reference.get("tolerances", DEFAULT_TOLERANCES)
269
+
270
+ for case_name, ref_case in ref_cases.items():
271
+ cand_case = cand_cases.get(case_name)
272
+ if cand_case is None:
273
+ failures.append({"case": case_name, "reason": "missing_candidate_case"})
274
+ continue
275
+ if ref_case.get("topology") != cand_case.get("topology"):
276
+ failures.append({"case": case_name, "reason": "topology_mismatch", "reference": ref_case.get("topology"), "candidate": cand_case.get("topology")})
277
+ for section in ("results", "residuals"):
278
+ ref_section = ref_case.get(section, {})
279
+ cand_section = cand_case.get(section, {})
280
+ for metric, ref_value in ref_section.items():
281
+ cand_value = cand_section.get(metric)
282
+ if isinstance(ref_value, (int, float)) and isinstance(cand_value, (int, float)):
283
+ tol = _tolerance_for(tolerances, case_name, section, metric)
284
+ rtol = float(tol.get("rtol", 0.0))
285
+ atol = float(tol.get("atol", 0.0))
286
+ if not np.isclose(float(cand_value), float(ref_value), rtol=rtol, atol=atol):
287
+ failures.append(
288
+ {
289
+ "case": case_name,
290
+ "metric": f"{section}.{metric}",
291
+ "reference": float(ref_value),
292
+ "candidate": float(cand_value),
293
+ "rtol": rtol,
294
+ "atol": atol,
295
+ }
296
+ )
297
+ elif cand_value != ref_value:
298
+ failures.append({"case": case_name, "metric": f"{section}.{metric}", "reference": ref_value, "candidate": cand_value})
299
+
300
+ for case_name in sorted(set(cand_cases) - set(ref_cases)):
301
+ warnings.append({"case": case_name, "reason": "extra_candidate_case"})
302
+
303
+ return {"status": "passed" if not failures else "failed", "failures": failures, "warnings": warnings}