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,110 @@
|
|
|
1
|
+
"""Beam/member geometric validity evidence 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, Dict, Optional
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .elements import BeamElement
|
|
16
|
+
from .fe_core import FEModel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_BEAM_VALIDITY_PATH = Path("reports/beam_validity/beam_validity_report.json")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _git_sha() -> Optional[str]:
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(["git", "rev-parse", "HEAD"], text=True, capture_output=True, check=False)
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _single_beam(corotational: bool) -> tuple[FEModel, BeamElement]:
|
|
31
|
+
model = FEModel("beam_validity")
|
|
32
|
+
model.add_material("steel", 210.0e9, 0.3)
|
|
33
|
+
model.add_node(1, 0.0, 0.0, 0.0)
|
|
34
|
+
model.add_node(2, 2.0, 0.0, 0.0)
|
|
35
|
+
section = {"area": 0.01, "Iy": 2.0e-6, "Iz": 1.0e-6, "J": 1.0e-6, "orientation": (0.0, 0.0, 1.0)}
|
|
36
|
+
if corotational:
|
|
37
|
+
section["geometric_nonlinearity"] = "corotational"
|
|
38
|
+
element = BeamElement(1, [1, 2], "steel", section)
|
|
39
|
+
model.add_element(1, element)
|
|
40
|
+
return model, element
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def corotational_rigid_rotation_metric(angle_degrees: float = 35.0) -> Dict[str, Any]:
|
|
44
|
+
"""Compare default and corotational internal force for a finite rigid rotation."""
|
|
45
|
+
theta = float(np.deg2rad(angle_degrees))
|
|
46
|
+
length = 2.0
|
|
47
|
+
u = np.zeros(12)
|
|
48
|
+
u[6] = length * np.cos(theta) - length
|
|
49
|
+
u[7] = length * np.sin(theta)
|
|
50
|
+
u[5] = theta
|
|
51
|
+
u[11] = theta
|
|
52
|
+
|
|
53
|
+
default_model, default_element = _single_beam(corotational=False)
|
|
54
|
+
corot_model, corot_element = _single_beam(corotational=True)
|
|
55
|
+
f_default, _k_default, _state_default = default_element.compute_nonlinear_response(default_model.mesh, default_model.get_material("steel"), u)
|
|
56
|
+
f_corot, _k_corot, state_corot = corot_element.compute_nonlinear_response(corot_model.mesh, corot_model.get_material("steel"), u)
|
|
57
|
+
return {
|
|
58
|
+
"angle_degrees": float(angle_degrees),
|
|
59
|
+
"default_force_norm": float(np.linalg.norm(f_default)),
|
|
60
|
+
"corotational_force_norm": float(np.linalg.norm(f_corot)),
|
|
61
|
+
"force_norm_ratio_corot_to_default": float(np.linalg.norm(f_corot) / max(np.linalg.norm(f_default), 1.0e-30)),
|
|
62
|
+
"corotational_basic_deformation_norm": float(state_corot.get("basic_deformation_norm", 0.0)),
|
|
63
|
+
"corotational_axial_extension": float(state_corot.get("axial_extension", 0.0)),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def corotational_axial_extension_metric(extension: float = 0.002) -> Dict[str, Any]:
|
|
68
|
+
"""Check axial extension response for the corotational beam."""
|
|
69
|
+
model, element = _single_beam(corotational=True)
|
|
70
|
+
u = np.zeros(12)
|
|
71
|
+
u[6] = float(extension)
|
|
72
|
+
force, _k, state = element.compute_nonlinear_response(model.mesh, model.get_material("steel"), u)
|
|
73
|
+
expected = 210.0e9 * 0.01 / 2.0 * float(extension)
|
|
74
|
+
return {
|
|
75
|
+
"extension": float(extension),
|
|
76
|
+
"expected_end_force": float(expected),
|
|
77
|
+
"computed_end_force": float(force[6]),
|
|
78
|
+
"relative_error": float(abs(force[6] - expected) / max(abs(expected), 1.0)),
|
|
79
|
+
"current_length": float(state.get("current_length", 0.0)),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def generate_beam_validity_report() -> Dict[str, Any]:
|
|
84
|
+
"""Generate local beam/member geometric validity report."""
|
|
85
|
+
return {
|
|
86
|
+
"schema_version": 1,
|
|
87
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
88
|
+
"commit": _git_sha(),
|
|
89
|
+
"environment": {
|
|
90
|
+
"platform": platform.platform(),
|
|
91
|
+
"python": sys.version.split()[0],
|
|
92
|
+
"numpy": np.__version__,
|
|
93
|
+
},
|
|
94
|
+
"corotational_v1": {
|
|
95
|
+
"rigid_rotation": corotational_rigid_rotation_metric(),
|
|
96
|
+
"axial_extension": corotational_axial_extension_metric(),
|
|
97
|
+
},
|
|
98
|
+
"known_limitations": [
|
|
99
|
+
"Corotational v1 is opt-in for 2-node elastic beam geometric response via cross_section['geometric_nonlinearity']='corotational'.",
|
|
100
|
+
"Fiber-section plasticity keeps the existing beam-column path in this batch.",
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def write_beam_validity_report(path: Path | str = DEFAULT_BEAM_VALIDITY_PATH) -> Dict[str, Any]:
|
|
106
|
+
report = generate_beam_validity_report()
|
|
107
|
+
output = Path(path)
|
|
108
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
110
|
+
return report
|
anysolver/benchmarks.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""Local FE solver infrastructure benchmark helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import tracemalloc
|
|
11
|
+
import importlib.metadata
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import scipy
|
|
17
|
+
from scipy import sparse
|
|
18
|
+
|
|
19
|
+
from .assembly import solve_linear, solve_linear_many
|
|
20
|
+
from .boundary import BoundaryCondition, FixedSupport, LoadCase
|
|
21
|
+
from .buckling import solve_eigenvalue_buckling
|
|
22
|
+
from .dynamics import TransientConfig, solve_transient_newmark
|
|
23
|
+
from .contact import RigidSphereImpact, SphereContactConfig, solve_transient_sphere_impact
|
|
24
|
+
from .elements import BeamElement
|
|
25
|
+
from .fe_core import FEModel
|
|
26
|
+
from .fracture import FractureConfig, ImpactDamageConfig, detect_new_deletions
|
|
27
|
+
from .matrix_assembly import assemble_load_vector, assemble_mass_matrix, assemble_stiffness_matrix
|
|
28
|
+
from .mesh_gen import generate_beam_mesh, generate_simple_panel_mesh
|
|
29
|
+
from .recovery import RecoveryConfig, ResourceConfig, recover_element_stresses_with_report
|
|
30
|
+
from .linalg import FactorizationCache, MatrixClass, factorize_cached
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_BENCHMARK_PATH = Path("reports/benchmarks/fe_infrastructure_benchmarks.json")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _git_sha() -> Optional[str]:
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(["git", "rev-parse", "HEAD"], text=True, capture_output=True, check=False)
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _optional_version(package_name: str) -> Optional[str]:
|
|
45
|
+
try:
|
|
46
|
+
return importlib.metadata.version(package_name)
|
|
47
|
+
except importlib.metadata.PackageNotFoundError:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _measure(func: Callable[[], Dict[str, Any]]) -> Dict[str, Any]:
|
|
52
|
+
tracemalloc.start()
|
|
53
|
+
start = time.perf_counter()
|
|
54
|
+
try:
|
|
55
|
+
payload = func()
|
|
56
|
+
finally:
|
|
57
|
+
elapsed = time.perf_counter() - start
|
|
58
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
59
|
+
tracemalloc.stop()
|
|
60
|
+
payload.setdefault("timing", {})
|
|
61
|
+
payload["timing"]["wall_seconds"] = float(elapsed)
|
|
62
|
+
payload["memory"] = {"current_bytes": int(current), "peak_bytes": int(peak)}
|
|
63
|
+
return payload
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _topology(model: FEModel) -> Dict[str, int]:
|
|
67
|
+
return {
|
|
68
|
+
"nodes": int(model.mesh.num_nodes),
|
|
69
|
+
"elements": int(model.mesh.num_elements),
|
|
70
|
+
"dofs": int(model.mesh.dof_manager.total_dofs),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _static_beam_case() -> Dict[str, Any]:
|
|
75
|
+
model = generate_beam_mesh(2.0, num_divisions=8, cross_section={"area": 0.01, "Iy": 1.0e-6, "Iz": 1.0e-6, "J": 1.0e-6})
|
|
76
|
+
load_case = LoadCase("tip_load")
|
|
77
|
+
load_case.add_nodal_load(9, [0.0, 0.0, -1000.0, 0.0, 0.0, 0.0])
|
|
78
|
+
K, stiffness_info = assemble_stiffness_matrix(model)
|
|
79
|
+
M, mass_info = assemble_mass_matrix(model)
|
|
80
|
+
F, load_info = assemble_load_vector(model, load_case)
|
|
81
|
+
u, solver_info = solve_linear(model, load_case)
|
|
82
|
+
backend = (solver_info.get("convergence_info") or {}).get("backend", {})
|
|
83
|
+
return {
|
|
84
|
+
"topology": _topology(model),
|
|
85
|
+
"matrix_nnz": {"K": int(K.nnz), "M": int(M.nnz)},
|
|
86
|
+
"load_norm": float(np.linalg.norm(F)),
|
|
87
|
+
"timing": {
|
|
88
|
+
"stiffness_assembly_seconds": float(stiffness_info.get("assembly_time", 0.0)),
|
|
89
|
+
"mass_assembly_seconds": float(mass_info.get("assembly_time", 0.0)),
|
|
90
|
+
"load_assembly_seconds": float(load_info.get("assembly_time", 0.0)),
|
|
91
|
+
"solve_seconds": float(solver_info.get("solve_time", 0.0)),
|
|
92
|
+
"factorization_seconds": float(backend.get("factorization_time", 0.0)) if isinstance(backend, dict) else 0.0,
|
|
93
|
+
"backend_solve_seconds": float(backend.get("solve_time", 0.0)) if isinstance(backend, dict) else 0.0,
|
|
94
|
+
},
|
|
95
|
+
"results": {"max_abs_displacement": float(np.max(np.abs(u)))},
|
|
96
|
+
"backend": backend,
|
|
97
|
+
"status": str((solver_info.get("convergence_info") or {}).get("status", "unknown")),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _multi_rhs_case() -> Dict[str, Any]:
|
|
102
|
+
model = generate_beam_mesh(2.0, num_divisions=8, cross_section={"area": 0.01, "Iy": 1.0e-6, "Iz": 1.0e-6, "J": 1.0e-6})
|
|
103
|
+
tip = 9
|
|
104
|
+
load_x = LoadCase("tip_x")
|
|
105
|
+
load_x.add_nodal_load(tip, [100.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
|
106
|
+
load_z = LoadCase("tip_z")
|
|
107
|
+
load_z.add_nodal_load(tip, [0.0, 0.0, -100.0, 0.0, 0.0, 0.0])
|
|
108
|
+
load_m = LoadCase("tip_m")
|
|
109
|
+
load_m.add_nodal_load(tip, [0.0, 0.0, 0.0, 0.0, 50.0, 0.0])
|
|
110
|
+
U, info = solve_linear_many(model, [load_x, load_z, load_m])
|
|
111
|
+
backend = info.get("backend", {})
|
|
112
|
+
return {
|
|
113
|
+
"topology": _topology(model),
|
|
114
|
+
"num_rhs": 3,
|
|
115
|
+
"timing": {
|
|
116
|
+
"solve_many_seconds": float(info.get("solve_time", 0.0)),
|
|
117
|
+
"factorization_seconds": float(backend.get("factorization_time", 0.0)) if isinstance(backend, dict) else 0.0,
|
|
118
|
+
"backend_solve_seconds": float(backend.get("solve_time", 0.0)) if isinstance(backend, dict) else 0.0,
|
|
119
|
+
},
|
|
120
|
+
"results": {"solution_matrix_norm": float(np.linalg.norm(U))},
|
|
121
|
+
"backend": backend,
|
|
122
|
+
"status": str(info.get("status", "unknown")),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _set_reduced_integration_for_q8r(model: FEModel) -> None:
|
|
127
|
+
for element in model.mesh.elements.values():
|
|
128
|
+
if getattr(element, "_is_8node", False):
|
|
129
|
+
element.reduced_integration = True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _shell_stiffness_cold_warm_case(shell_order: str) -> Dict[str, Any]:
|
|
133
|
+
from .jit_compiler import JIT_DISABLED_REASON, JIT_ENABLED, jit_diagnostics
|
|
134
|
+
|
|
135
|
+
order = str(shell_order).upper()
|
|
136
|
+
use_8node = order in {"Q8", "Q8R", "S8", "S8R"}
|
|
137
|
+
model = generate_simple_panel_mesh(1.5, 1.0, 0.01, num_divisions_x=2, num_divisions_y=2, use_8node_elements=use_8node)
|
|
138
|
+
normalized_order = "Q8R" if order in {"Q8R", "S8R"} else ("Q8" if use_8node else "S4")
|
|
139
|
+
if normalized_order == "Q8R":
|
|
140
|
+
_set_reduced_integration_for_q8r(model)
|
|
141
|
+
|
|
142
|
+
cold_start = time.perf_counter()
|
|
143
|
+
K_cold, cold_info = assemble_stiffness_matrix(model)
|
|
144
|
+
cold_seconds = time.perf_counter() - cold_start
|
|
145
|
+
warm_start = time.perf_counter()
|
|
146
|
+
K_warm, warm_info = assemble_stiffness_matrix(model)
|
|
147
|
+
warm_seconds = time.perf_counter() - warm_start
|
|
148
|
+
|
|
149
|
+
denominator = max(float(sparse.linalg.norm(K_cold)), 1.0)
|
|
150
|
+
matrix_difference_norm = float(sparse.linalg.norm(K_cold - K_warm) / denominator)
|
|
151
|
+
jit = jit_diagnostics()
|
|
152
|
+
return {
|
|
153
|
+
"topology": _topology(model),
|
|
154
|
+
"shell_order": normalized_order,
|
|
155
|
+
"element_count": int(model.mesh.num_elements),
|
|
156
|
+
"jit_enabled": bool(JIT_ENABLED),
|
|
157
|
+
"jit_disabled_reason": JIT_DISABLED_REASON,
|
|
158
|
+
"parallel_threads": jit.get("num_threads"),
|
|
159
|
+
"timing": {
|
|
160
|
+
"cold_assembly_seconds": float(cold_seconds),
|
|
161
|
+
"warm_assembly_seconds": float(warm_seconds),
|
|
162
|
+
"warm_speedup": float(cold_seconds / warm_seconds) if warm_seconds > 0.0 else 0.0,
|
|
163
|
+
},
|
|
164
|
+
"results": {
|
|
165
|
+
"matrix_difference_norm": matrix_difference_norm,
|
|
166
|
+
"cold_diagnostics": cold_info.get("diagnostics", {}),
|
|
167
|
+
"warm_diagnostics": warm_info.get("diagnostics", {}),
|
|
168
|
+
},
|
|
169
|
+
"status": "completed" if matrix_difference_norm < 1.0e-12 else "failed",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _shell_assembly_case() -> Dict[str, Any]:
|
|
174
|
+
model = generate_simple_panel_mesh(2.0, 1.0, 0.01, num_divisions_x=8, num_divisions_y=4)
|
|
175
|
+
load_case = LoadCase("pressure")
|
|
176
|
+
for element_id in model.mesh.elements:
|
|
177
|
+
load_case.add_pressure_load(element_id, 1000.0)
|
|
178
|
+
K, stiffness_info = assemble_stiffness_matrix(model)
|
|
179
|
+
M, mass_info = assemble_mass_matrix(model)
|
|
180
|
+
_F, load_info = assemble_load_vector(model, load_case)
|
|
181
|
+
return {
|
|
182
|
+
"topology": _topology(model),
|
|
183
|
+
"matrix_nnz": {"K": int(K.nnz), "M": int(M.nnz)},
|
|
184
|
+
"timing": {
|
|
185
|
+
"stiffness_assembly_seconds": float(stiffness_info.get("assembly_time", 0.0)),
|
|
186
|
+
"mass_assembly_seconds": float(mass_info.get("assembly_time", 0.0)),
|
|
187
|
+
"load_assembly_seconds": float(load_info.get("assembly_time", 0.0)),
|
|
188
|
+
},
|
|
189
|
+
"diagnostics": {"stiffness_symmetry": stiffness_info.get("diagnostics", {}).get("assembled_symmetry_error")},
|
|
190
|
+
"status": "completed",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _buckling_case() -> Dict[str, Any]:
|
|
195
|
+
model = FEModel("benchmark_column")
|
|
196
|
+
model.add_material("steel", 210.0e9, 0.3, density=7850.0)
|
|
197
|
+
section = {"area": 0.02, "Iy": 3.0e-6, "Iz": 5.0e-6, "J": 2.0e-6}
|
|
198
|
+
for i in range(9):
|
|
199
|
+
model.add_node(i + 1, 4.0 * i / 8, 0.0, 0.0)
|
|
200
|
+
for i in range(8):
|
|
201
|
+
model.add_element(i + 1, BeamElement(i + 1, [i + 1, i + 2], "steel", section))
|
|
202
|
+
all_nodes = list(range(1, 10))
|
|
203
|
+
model.add_boundary_condition(BoundaryCondition("suppress", all_nodes, {"ux": 0.0, "uz": 0.0, "rx": 0.0, "ry": 0.0}))
|
|
204
|
+
model.add_boundary_condition(BoundaryCondition("pins", [1, 9], {"uy": 0.0}))
|
|
205
|
+
states = {element_id: {"axial_compression": 1.0} for element_id in model.mesh.elements}
|
|
206
|
+
result = solve_eigenvalue_buckling(model, states, num_modes=2)
|
|
207
|
+
return {
|
|
208
|
+
"topology": _topology(model),
|
|
209
|
+
"timing": {
|
|
210
|
+
"stiffness_assembly_seconds": float(result.assembly_info.get("stiffness", {}).get("assembly_time", 0.0)),
|
|
211
|
+
"geometric_assembly_seconds": float(result.assembly_info.get("geometric_stiffness", {}).get("assembly_time", 0.0)),
|
|
212
|
+
},
|
|
213
|
+
"results": {"critical_load_factor": float(result.critical_load_factor or 0.0), "num_modes": int(result.num_modes_returned)},
|
|
214
|
+
"status": result.solver_status,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _transient_case() -> Dict[str, Any]:
|
|
219
|
+
model = FEModel("benchmark_sdof")
|
|
220
|
+
model.add_material("steel", elastic_modulus=100.0, poisson_ratio=0.3, density=2.0)
|
|
221
|
+
model.add_node(1, 0.0, 0.0, 0.0)
|
|
222
|
+
model.add_node(2, 1.0, 0.0, 0.0)
|
|
223
|
+
model.add_element(1, BeamElement(1, [1, 2], "steel", {"area": 1.0, "Iy": 1.0e-6, "Iz": 1.0e-6, "J": 1.0e-6}))
|
|
224
|
+
model.add_boundary_condition(FixedSupport("fixed", [1]))
|
|
225
|
+
model.add_boundary_condition(BoundaryCondition("slider", [2], {"uy": 0.0, "uz": 0.0, "rx": 0.0, "ry": 0.0, "rz": 0.0}))
|
|
226
|
+
load_case = LoadCase("step")
|
|
227
|
+
load_case.add_nodal_load(2, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
|
228
|
+
result = solve_transient_newmark(model, TransientConfig(dt=0.001, t_end=0.05), base_load_case=load_case)
|
|
229
|
+
return {
|
|
230
|
+
"topology": _topology(model),
|
|
231
|
+
"timing": {
|
|
232
|
+
"factorization_count": int(result.diagnostics.get("factorization_count", 0)),
|
|
233
|
+
"solve_count": int(result.diagnostics.get("solve_count", 0)),
|
|
234
|
+
},
|
|
235
|
+
"results": {"peak_displacement": float(result.peak_displacement), "energy_drift": float(result.diagnostics.get("max_relative_energy_drift", 0.0))},
|
|
236
|
+
"status": result.status,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _selective_recovery_case() -> Dict[str, Any]:
|
|
241
|
+
model = generate_simple_panel_mesh(3.0, 2.0, 0.01, num_divisions_x=6, num_divisions_y=4)
|
|
242
|
+
displacement = np.zeros(model.mesh.dof_manager.total_dofs, dtype=float)
|
|
243
|
+
recovery = RecoveryConfig(components=["von_mises"])
|
|
244
|
+
serial, serial_report = recover_element_stresses_with_report(
|
|
245
|
+
model,
|
|
246
|
+
displacement,
|
|
247
|
+
recovery,
|
|
248
|
+
resource_config=ResourceConfig(recovery_threads=1),
|
|
249
|
+
)
|
|
250
|
+
threaded, threaded_report = recover_element_stresses_with_report(
|
|
251
|
+
model,
|
|
252
|
+
displacement,
|
|
253
|
+
recovery,
|
|
254
|
+
resource_config=ResourceConfig(recovery_threads=2),
|
|
255
|
+
)
|
|
256
|
+
results_match = sorted(serial) == sorted(threaded) and all(
|
|
257
|
+
np.allclose(serial[element_id]["von_mises"], threaded[element_id]["von_mises"])
|
|
258
|
+
for element_id in serial
|
|
259
|
+
)
|
|
260
|
+
return {
|
|
261
|
+
"topology": _topology(model),
|
|
262
|
+
"timing": {
|
|
263
|
+
"serial_recovery_seconds": float(serial_report.elapsed_seconds),
|
|
264
|
+
"threaded_recovery_seconds": float(threaded_report.elapsed_seconds),
|
|
265
|
+
"observed_speedup": (
|
|
266
|
+
float(serial_report.elapsed_seconds / threaded_report.elapsed_seconds)
|
|
267
|
+
if threaded_report.elapsed_seconds > 0.0
|
|
268
|
+
else 0.0
|
|
269
|
+
),
|
|
270
|
+
},
|
|
271
|
+
"resources": {
|
|
272
|
+
"serial": serial_report.to_dict(),
|
|
273
|
+
"threaded": threaded_report.to_dict(),
|
|
274
|
+
},
|
|
275
|
+
"results": {"num_stress_results": len(serial), "results_match": bool(results_match)},
|
|
276
|
+
"status": "completed" if results_match else "failed",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _factorization_reuse_case() -> Dict[str, Any]:
|
|
281
|
+
matrix = sparse.diags([1.0, 4.0, 1.0], offsets=[-1, 0, 1], shape=(200, 200), format="csr")
|
|
282
|
+
rhs = np.ones(200, dtype=float)
|
|
283
|
+
cache = FactorizationCache(name="benchmark_factorization_reuse", max_entries=2)
|
|
284
|
+
first = factorize_cached(matrix, MatrixClass.SPD, cache=cache)
|
|
285
|
+
first.solve(rhs)
|
|
286
|
+
second = factorize_cached(matrix.copy(), MatrixClass.SPD, cache=cache)
|
|
287
|
+
second.solve(rhs)
|
|
288
|
+
changed = factorize_cached(matrix + sparse.eye(200, format="csr") * 0.01, MatrixClass.SPD, cache=cache)
|
|
289
|
+
changed.solve(rhs)
|
|
290
|
+
return {
|
|
291
|
+
"topology": {"dofs": 200, "nnz": int(matrix.nnz)},
|
|
292
|
+
"timing": {
|
|
293
|
+
"first_factorization_seconds": float(first.factorization_time),
|
|
294
|
+
"reused_solve_seconds_total": float(second.solve_time),
|
|
295
|
+
"changed_factorization_seconds": float(changed.factorization_time),
|
|
296
|
+
},
|
|
297
|
+
"cache": cache.diagnostics(),
|
|
298
|
+
"results": {"same_handle_reused": bool(first is second), "changed_matrix_new_handle": bool(changed is not first)},
|
|
299
|
+
"status": "completed" if first is second and changed is not first else "failed",
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _nonlinear_assembly_case() -> Dict[str, Any]:
|
|
304
|
+
from . import nonlinear_performance, nonlinear_static
|
|
305
|
+
from .nonlinear_performance_bootstrap import get_nonlinear_assembly_plan, nonlinear_performance_status
|
|
306
|
+
|
|
307
|
+
model = generate_simple_panel_mesh(1.2, 0.8, 0.01, num_divisions_x=2, num_divisions_y=2)
|
|
308
|
+
rng = np.random.default_rng(20260630)
|
|
309
|
+
displacement = rng.normal(scale=2.0e-5, size=model.mesh.dof_manager.total_dofs)
|
|
310
|
+
committed: Dict[int, Any] = {}
|
|
311
|
+
original = nonlinear_performance._ORIGINAL_ASSEMBLER
|
|
312
|
+
if original is None:
|
|
313
|
+
return {
|
|
314
|
+
"topology": _topology(model),
|
|
315
|
+
"timing": {},
|
|
316
|
+
"results": {},
|
|
317
|
+
"performance_layer": nonlinear_performance_status(),
|
|
318
|
+
"status": "skipped",
|
|
319
|
+
"reason": "legacy nonlinear assembler is unavailable",
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
start = time.perf_counter()
|
|
323
|
+
force_reference, tangent_reference, _states_reference = original(
|
|
324
|
+
model,
|
|
325
|
+
displacement,
|
|
326
|
+
committed,
|
|
327
|
+
5,
|
|
328
|
+
tangent=True,
|
|
329
|
+
)
|
|
330
|
+
legacy_seconds = time.perf_counter() - start
|
|
331
|
+
|
|
332
|
+
plan = get_nonlinear_assembly_plan(model, 5)
|
|
333
|
+
start = time.perf_counter()
|
|
334
|
+
force_fast, tangent_fast, _states_fast = nonlinear_static._assemble_nonlinear_system(
|
|
335
|
+
model,
|
|
336
|
+
displacement,
|
|
337
|
+
committed,
|
|
338
|
+
5,
|
|
339
|
+
tangent=True,
|
|
340
|
+
)
|
|
341
|
+
fast_seconds = time.perf_counter() - start
|
|
342
|
+
|
|
343
|
+
force_error = float(
|
|
344
|
+
np.linalg.norm(force_fast - force_reference) / max(float(np.linalg.norm(force_reference)), 1.0)
|
|
345
|
+
)
|
|
346
|
+
tangent_error = float(
|
|
347
|
+
sparse.linalg.norm(tangent_fast - tangent_reference) / max(float(sparse.linalg.norm(tangent_reference)), 1.0)
|
|
348
|
+
)
|
|
349
|
+
return {
|
|
350
|
+
"topology": _topology(model),
|
|
351
|
+
"timing": {
|
|
352
|
+
"legacy_assembly_seconds": float(legacy_seconds),
|
|
353
|
+
"active_fast_path_seconds": float(fast_seconds),
|
|
354
|
+
"observed_speedup": float(legacy_seconds / fast_seconds) if fast_seconds > 0.0 else 0.0,
|
|
355
|
+
},
|
|
356
|
+
"results": {
|
|
357
|
+
"relative_force_error": force_error,
|
|
358
|
+
"relative_tangent_error": tangent_error,
|
|
359
|
+
"plan_diagnostics": plan.diagnostics(),
|
|
360
|
+
},
|
|
361
|
+
"performance_layer": nonlinear_performance_status(),
|
|
362
|
+
"status": "completed" if force_error < 1.0e-10 and tangent_error < 1.0e-9 else "failed",
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _fracture_damage_case() -> Dict[str, Any]:
|
|
367
|
+
static_model = generate_simple_panel_mesh(2.0, 1.0, 0.01, num_divisions_x=8, num_divisions_y=4)
|
|
368
|
+
states: Dict[int, Any] = {}
|
|
369
|
+
element_ids = list(static_model.mesh.elements)
|
|
370
|
+
for element_id in element_ids:
|
|
371
|
+
states[int(element_id)] = {"alpha": np.zeros((4, 3), dtype=float)}
|
|
372
|
+
for element_id in element_ids[: max(1, len(element_ids) // 16)]:
|
|
373
|
+
states[int(element_id)]["alpha"][0, 0] = 0.02
|
|
374
|
+
fracture_config = FractureConfig(threshold=0.01, max_deleted_fraction=1.0)
|
|
375
|
+
scan_start = time.perf_counter()
|
|
376
|
+
deletion_records, max_utilization = detect_new_deletions(
|
|
377
|
+
static_model,
|
|
378
|
+
states,
|
|
379
|
+
fracture_config,
|
|
380
|
+
(),
|
|
381
|
+
step_index=1,
|
|
382
|
+
load_factor=1.0,
|
|
383
|
+
)
|
|
384
|
+
scan_seconds = time.perf_counter() - scan_start
|
|
385
|
+
|
|
386
|
+
impact_model = FEModel("benchmark_impact_damage_panel")
|
|
387
|
+
impact_model.add_material("soft", 1.0e5, 0.3, density=20.0)
|
|
388
|
+
impact_model.add_node(1, 0.0, 0.0, 0.0)
|
|
389
|
+
impact_model.add_node(2, 1.0, 0.0, 0.0)
|
|
390
|
+
impact_model.add_node(3, 1.0, 1.0, 0.0)
|
|
391
|
+
impact_model.add_node(4, 0.0, 1.0, 0.0)
|
|
392
|
+
from .elements import ShellElement
|
|
393
|
+
|
|
394
|
+
impact_model.add_element(1, ShellElement(1, [1, 2, 3, 4], "soft", thickness=0.05))
|
|
395
|
+
impact_model.add_boundary_condition(
|
|
396
|
+
BoundaryCondition(
|
|
397
|
+
"restrain_shell_nonimpact_modes",
|
|
398
|
+
[1, 2, 3, 4],
|
|
399
|
+
{"ux": 0.0, "uy": 0.0, "rx": 0.0, "ry": 0.0, "rz": 0.0},
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
sphere = RigidSphereImpact(
|
|
403
|
+
"benchmark_damage",
|
|
404
|
+
radius=0.1,
|
|
405
|
+
mass=1.0,
|
|
406
|
+
start_point=(0.5, 0.5, 0.25),
|
|
407
|
+
travel_direction=(0.0, 0.0, -1.0),
|
|
408
|
+
speed=2.0,
|
|
409
|
+
)
|
|
410
|
+
impact_result = solve_transient_sphere_impact(
|
|
411
|
+
impact_model,
|
|
412
|
+
TransientConfig(dt=0.0025, t_end=0.12),
|
|
413
|
+
sphere,
|
|
414
|
+
SphereContactConfig(penalty_stiffness=4000.0, max_contact_iterations=40),
|
|
415
|
+
damage_config=ImpactDamageConfig(
|
|
416
|
+
capacity_basis="user",
|
|
417
|
+
user_capacity=1.0e6,
|
|
418
|
+
softening_start=0.9,
|
|
419
|
+
delete_at=1.0,
|
|
420
|
+
max_deleted_fraction=1.0,
|
|
421
|
+
),
|
|
422
|
+
)
|
|
423
|
+
damage_summary = impact_result.diagnostics.get("impact_damage_summary", {})
|
|
424
|
+
return {
|
|
425
|
+
"topology": {
|
|
426
|
+
"static_fracture_elements": int(static_model.mesh.num_elements),
|
|
427
|
+
"impact_damage_elements": int(impact_model.mesh.num_elements),
|
|
428
|
+
},
|
|
429
|
+
"timing": {
|
|
430
|
+
"static_deletion_scan_seconds": float(scan_seconds),
|
|
431
|
+
"impact_wall_solver_steps": int(impact_result.diagnostics.get("num_steps", 0)),
|
|
432
|
+
"impact_solve_count": int(impact_result.diagnostics.get("solve_count", 0)),
|
|
433
|
+
"impact_factorization_count": int(impact_result.diagnostics.get("factorization_count", 0)),
|
|
434
|
+
"impact_eroded_matrix_rebuild_count": int(impact_result.diagnostics.get("eroded_matrix_rebuild_count", 0)),
|
|
435
|
+
"impact_damage_state_update_count": int(impact_result.diagnostics.get("damage_state_update_count", 0)),
|
|
436
|
+
},
|
|
437
|
+
"results": {
|
|
438
|
+
"static_deleted_records": len(deletion_records),
|
|
439
|
+
"static_max_fracture_utilization": float(max_utilization),
|
|
440
|
+
"impact_status": impact_result.status,
|
|
441
|
+
"impact_max_damage": float(damage_summary.get("max_damage", 0.0)),
|
|
442
|
+
"impact_deleted_count": int(damage_summary.get("deleted_count", 0)),
|
|
443
|
+
"sub_softening_rebuilds_skipped": int(impact_result.diagnostics.get("eroded_matrix_rebuild_count", 0)) == 0,
|
|
444
|
+
},
|
|
445
|
+
"status": "completed"
|
|
446
|
+
if len(deletion_records) > 0 and int(impact_result.diagnostics.get("eroded_matrix_rebuild_count", 0)) == 0
|
|
447
|
+
else "failed",
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
BENCHMARK_CASES: Dict[str, Callable[[], Dict[str, Any]]] = {
|
|
452
|
+
"static_beam": _static_beam_case,
|
|
453
|
+
"multi_rhs_static": _multi_rhs_case,
|
|
454
|
+
"shell_stiffness_S4_cold_warm": lambda: _shell_stiffness_cold_warm_case("S4"),
|
|
455
|
+
"shell_stiffness_Q8_cold_warm": lambda: _shell_stiffness_cold_warm_case("Q8"),
|
|
456
|
+
"shell_stiffness_Q8R_cold_warm": lambda: _shell_stiffness_cold_warm_case("Q8R"),
|
|
457
|
+
"shell_assembly": _shell_assembly_case,
|
|
458
|
+
"beam_column_buckling": _buckling_case,
|
|
459
|
+
"transient_newmark": _transient_case,
|
|
460
|
+
"selective_recovery": _selective_recovery_case,
|
|
461
|
+
"factorization_reuse": _factorization_reuse_case,
|
|
462
|
+
"nonlinear_assembly": _nonlinear_assembly_case,
|
|
463
|
+
"fracture_damage": _fracture_damage_case,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def run_infrastructure_benchmarks() -> Dict[str, Any]:
|
|
468
|
+
"""Run local benchmark smoke cases and return a serializable report."""
|
|
469
|
+
cases = {name: _measure(builder) for name, builder in BENCHMARK_CASES.items()}
|
|
470
|
+
return {
|
|
471
|
+
"schema_version": 1,
|
|
472
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
473
|
+
"commit": _git_sha(),
|
|
474
|
+
"environment": {
|
|
475
|
+
"platform": platform.platform(),
|
|
476
|
+
"python": sys.version.split()[0],
|
|
477
|
+
"numpy": np.__version__,
|
|
478
|
+
"scipy": scipy.__version__,
|
|
479
|
+
"pypardiso": _optional_version("pypardiso"),
|
|
480
|
+
},
|
|
481
|
+
"cases": cases,
|
|
482
|
+
"known_limitations": [
|
|
483
|
+
"Benchmarks are local smoke measurements; compare trends on the same machine and Python environment.",
|
|
484
|
+
"tracemalloc reports Python allocation peaks, not full process resident memory.",
|
|
485
|
+
"Threaded recovery speedups are informational; deterministic correctness is the gate.",
|
|
486
|
+
],
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def write_benchmark_report(path: Path | str = DEFAULT_BENCHMARK_PATH) -> Dict[str, Any]:
|
|
491
|
+
report = run_infrastructure_benchmarks()
|
|
492
|
+
output = Path(path)
|
|
493
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
494
|
+
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
495
|
+
return report
|