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
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}
|