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/buckling.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Linear eigenvalue buckling helpers.
|
|
2
|
+
|
|
3
|
+
The first production buckling path is intentionally modest: element routines
|
|
4
|
+
assemble a reference geometric stiffness matrix ``KG`` and this module solves
|
|
5
|
+
the constrained generalized problem
|
|
6
|
+
|
|
7
|
+
K phi = lambda KG phi
|
|
8
|
+
|
|
9
|
+
Positive eigenvalues are critical load multipliers relative to the supplied
|
|
10
|
+
reference compression state.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from scipy import linalg
|
|
20
|
+
from scipy import sparse
|
|
21
|
+
from scipy.sparse import linalg as sparse_linalg
|
|
22
|
+
|
|
23
|
+
from .assembly import build_constraint_transformation, build_reduced_rigid_body_modes
|
|
24
|
+
from .cases import make_result_case
|
|
25
|
+
from .linalg import FactorizationCache, MatrixClass, cached_inverse_operator
|
|
26
|
+
from .matrix_assembly import assemble_geometric_stiffness_matrix, assemble_stiffness_matrix
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from .fe_core import FEModel
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class BucklingMode:
|
|
34
|
+
"""One positive eigenvalue buckling mode."""
|
|
35
|
+
|
|
36
|
+
mode_number: int
|
|
37
|
+
load_factor: float
|
|
38
|
+
eigenvalue: float
|
|
39
|
+
mode_shape: np.ndarray
|
|
40
|
+
reduced_mode_shape: np.ndarray
|
|
41
|
+
modal_stiffness: float
|
|
42
|
+
modal_geometric_stiffness: float
|
|
43
|
+
residual_norm: float = 0.0
|
|
44
|
+
rigid_body_correlation: float = 0.0
|
|
45
|
+
validity_status: str = "ok"
|
|
46
|
+
repeated_group: Optional[int] = None
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
49
|
+
return {
|
|
50
|
+
"mode_number": self.mode_number,
|
|
51
|
+
"load_factor": self.load_factor,
|
|
52
|
+
"eigenvalue": self.eigenvalue,
|
|
53
|
+
"modal_stiffness": self.modal_stiffness,
|
|
54
|
+
"modal_geometric_stiffness": self.modal_geometric_stiffness,
|
|
55
|
+
"residual_norm": self.residual_norm,
|
|
56
|
+
"rigid_body_correlation": self.rigid_body_correlation,
|
|
57
|
+
"validity_status": self.validity_status,
|
|
58
|
+
"repeated_group": self.repeated_group,
|
|
59
|
+
"mode_shape": self.mode_shape.tolist(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class BucklingResult:
|
|
65
|
+
"""Result bundle from the linear eigenvalue buckling solve."""
|
|
66
|
+
|
|
67
|
+
modes: List[BucklingMode]
|
|
68
|
+
num_modes_requested: int
|
|
69
|
+
solver_status: str
|
|
70
|
+
constraint_info: Dict[str, Any]
|
|
71
|
+
assembly_info: Dict[str, Any]
|
|
72
|
+
result_case: Optional[Dict[str, Any]] = None
|
|
73
|
+
diagnostics: Optional[Dict[str, Any]] = None
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def num_modes_returned(self) -> int:
|
|
77
|
+
return len(self.modes)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def critical_load_factor(self) -> Optional[float]:
|
|
81
|
+
if not self.modes:
|
|
82
|
+
return None
|
|
83
|
+
return self.modes[0].load_factor
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
86
|
+
return {
|
|
87
|
+
"solver_status": self.solver_status,
|
|
88
|
+
"num_modes_requested": self.num_modes_requested,
|
|
89
|
+
"num_modes_returned": self.num_modes_returned,
|
|
90
|
+
"critical_load_factor": self.critical_load_factor,
|
|
91
|
+
"constraint_info": self.constraint_info,
|
|
92
|
+
"assembly_info": self.assembly_info,
|
|
93
|
+
"result_case": self.result_case,
|
|
94
|
+
"diagnostics": self.diagnostics or {},
|
|
95
|
+
"modes": [mode.to_dict() for mode in self.modes],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _as_symmetric_dense(matrix: sparse.spmatrix) -> np.ndarray:
|
|
100
|
+
dense = np.asarray(matrix.toarray(), dtype=float)
|
|
101
|
+
return 0.5 * (dense + dense.T)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _normalize_mode(full_mode: np.ndarray, reduced_mode: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
105
|
+
scale = float(np.max(np.abs(full_mode))) if full_mode.size else 0.0
|
|
106
|
+
if scale <= 0.0:
|
|
107
|
+
return full_mode, reduced_mode
|
|
108
|
+
return full_mode / scale, reduced_mode / scale
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _mode_residual(
|
|
112
|
+
K: sparse.spmatrix,
|
|
113
|
+
KG: sparse.spmatrix,
|
|
114
|
+
reduced_mode: np.ndarray,
|
|
115
|
+
load_factor: float,
|
|
116
|
+
) -> float:
|
|
117
|
+
residual = np.asarray(K @ reduced_mode - float(load_factor) * (KG @ reduced_mode), dtype=float).reshape(-1)
|
|
118
|
+
denominator = max(
|
|
119
|
+
float(np.linalg.norm(K @ reduced_mode))
|
|
120
|
+
+ abs(float(load_factor)) * float(np.linalg.norm(KG @ reduced_mode)),
|
|
121
|
+
1.0,
|
|
122
|
+
)
|
|
123
|
+
return float(np.linalg.norm(residual) / denominator)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _factor_in_range(value: float, load_factor_range: Optional[Tuple[Optional[float], Optional[float]]]) -> bool:
|
|
127
|
+
if load_factor_range is None:
|
|
128
|
+
return True
|
|
129
|
+
lower, upper = load_factor_range
|
|
130
|
+
if lower is not None and value < float(lower):
|
|
131
|
+
return False
|
|
132
|
+
if upper is not None and value > float(upper):
|
|
133
|
+
return False
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _sort_key(value: float, shift_load_factor: Optional[float]) -> Tuple[float, float]:
|
|
138
|
+
if shift_load_factor is None:
|
|
139
|
+
return (float(value), 0.0)
|
|
140
|
+
return (abs(float(value) - float(shift_load_factor)), float(value))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _assign_repeated_groups(modes: List[BucklingMode], tolerance: float) -> List[Dict[str, Any]]:
|
|
144
|
+
groups: List[Dict[str, Any]] = []
|
|
145
|
+
if not modes:
|
|
146
|
+
return groups
|
|
147
|
+
current = [modes[0]]
|
|
148
|
+
group_index = 1
|
|
149
|
+
for mode in modes[1:]:
|
|
150
|
+
reference = current[0].load_factor
|
|
151
|
+
relative = abs(mode.load_factor - reference) / max(abs(reference), 1.0)
|
|
152
|
+
if relative <= tolerance:
|
|
153
|
+
current.append(mode)
|
|
154
|
+
else:
|
|
155
|
+
if len(current) > 1:
|
|
156
|
+
for item in current:
|
|
157
|
+
item.repeated_group = group_index
|
|
158
|
+
groups.append(
|
|
159
|
+
{
|
|
160
|
+
"group": group_index,
|
|
161
|
+
"mode_numbers": [int(item.mode_number) for item in current],
|
|
162
|
+
"load_factors": [float(item.load_factor) for item in current],
|
|
163
|
+
"relative_spread": float(
|
|
164
|
+
(max(item.load_factor for item in current) - min(item.load_factor for item in current))
|
|
165
|
+
/ max(abs(reference), 1.0)
|
|
166
|
+
),
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
group_index += 1
|
|
170
|
+
current = [mode]
|
|
171
|
+
if len(current) > 1:
|
|
172
|
+
reference = current[0].load_factor
|
|
173
|
+
for item in current:
|
|
174
|
+
item.repeated_group = group_index
|
|
175
|
+
groups.append(
|
|
176
|
+
{
|
|
177
|
+
"group": group_index,
|
|
178
|
+
"mode_numbers": [int(item.mode_number) for item in current],
|
|
179
|
+
"load_factors": [float(item.load_factor) for item in current],
|
|
180
|
+
"relative_spread": float(
|
|
181
|
+
(max(item.load_factor for item in current) - min(item.load_factor for item in current))
|
|
182
|
+
/ max(abs(reference), 1.0)
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
return groups
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def solve_eigenvalue_buckling(
|
|
190
|
+
model: "FEModel",
|
|
191
|
+
element_states: Optional[Any] = None,
|
|
192
|
+
num_modes: int = 3,
|
|
193
|
+
eigen_tolerance: float = 1.0e-8,
|
|
194
|
+
dense_size_limit: int = 200,
|
|
195
|
+
shift_load_factor: Optional[float] = None,
|
|
196
|
+
load_factor_range: Optional[Tuple[Optional[float], Optional[float]]] = None,
|
|
197
|
+
search_factor: int = 4,
|
|
198
|
+
repeated_tolerance: float = 1.0e-3,
|
|
199
|
+
allow_dense_fallback: bool = False,
|
|
200
|
+
allow_free_mechanisms: bool = False,
|
|
201
|
+
factorization_cache: Optional[FactorizationCache] = None,
|
|
202
|
+
) -> BucklingResult:
|
|
203
|
+
"""Solve ``K phi = lambda KG phi`` for positive buckling factors.
|
|
204
|
+
|
|
205
|
+
``element_states`` is passed to
|
|
206
|
+
:func:`anysolver.matrix_assembly.assemble_geometric_stiffness_matrix`.
|
|
207
|
+
Beam elements accept axial reference compression and shell elements accept
|
|
208
|
+
membrane resultant prestress (compression positive, or tension-positive
|
|
209
|
+
``membrane_forces``).
|
|
210
|
+
"""
|
|
211
|
+
if num_modes <= 0:
|
|
212
|
+
raise ValueError("num_modes must be positive")
|
|
213
|
+
|
|
214
|
+
# Make the solve independent of whether a caller happened to run another
|
|
215
|
+
# constrained analysis first. Rigid-mode filtering reads the active DOF
|
|
216
|
+
# constraints, so boundary conditions must be applied here explicitly.
|
|
217
|
+
model.apply_boundary_conditions()
|
|
218
|
+
K, stiffness_info = assemble_stiffness_matrix(model)
|
|
219
|
+
KG, geometric_info = assemble_geometric_stiffness_matrix(model, element_states)
|
|
220
|
+
zero_load = np.zeros(model.mesh.dof_manager.total_dofs, dtype=float)
|
|
221
|
+
|
|
222
|
+
K_red, _, T, _, independent_dofs, constraint_info = build_constraint_transformation(K, zero_load, model)
|
|
223
|
+
KG_red = (T.T @ KG @ T).tocsr()
|
|
224
|
+
|
|
225
|
+
assembly_info = {
|
|
226
|
+
"stiffness": stiffness_info,
|
|
227
|
+
"geometric_stiffness": geometric_info,
|
|
228
|
+
"total_dofs": model.mesh.dof_manager.total_dofs,
|
|
229
|
+
"reduced_dofs": int(K_red.shape[0]),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
settings = {
|
|
233
|
+
"num_modes": num_modes,
|
|
234
|
+
"dense_size_limit": dense_size_limit,
|
|
235
|
+
"eigen_tolerance": eigen_tolerance,
|
|
236
|
+
"shift_load_factor": shift_load_factor,
|
|
237
|
+
"load_factor_range": None if load_factor_range is None else list(load_factor_range),
|
|
238
|
+
"search_factor": search_factor,
|
|
239
|
+
"repeated_tolerance": repeated_tolerance,
|
|
240
|
+
"allow_dense_fallback": allow_dense_fallback,
|
|
241
|
+
"allow_free_mechanisms": allow_free_mechanisms,
|
|
242
|
+
"factorization_cache": None if factorization_cache is None else factorization_cache.name,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if K_red.shape[0] == 0:
|
|
246
|
+
diagnostics = {"status": "empty_reduced_system"}
|
|
247
|
+
result_case = make_result_case(
|
|
248
|
+
name="linear_buckling",
|
|
249
|
+
analysis_type="linear_buckling",
|
|
250
|
+
assembly_info=assembly_info,
|
|
251
|
+
solver_info={"convergence_info": diagnostics},
|
|
252
|
+
recovery={"modes": num_modes},
|
|
253
|
+
settings=settings,
|
|
254
|
+
metadata={"prestress_state_source": "none" if element_states is None else type(element_states).__name__},
|
|
255
|
+
).to_dict()
|
|
256
|
+
return BucklingResult([], num_modes, "empty_reduced_system", constraint_info, assembly_info, result_case, diagnostics)
|
|
257
|
+
if KG_red.nnz == 0:
|
|
258
|
+
diagnostics = {"status": "zero_geometric_stiffness"}
|
|
259
|
+
result_case = make_result_case(
|
|
260
|
+
name="linear_buckling",
|
|
261
|
+
analysis_type="linear_buckling",
|
|
262
|
+
assembly_info=assembly_info,
|
|
263
|
+
solver_info={"convergence_info": diagnostics},
|
|
264
|
+
recovery={"modes": num_modes},
|
|
265
|
+
settings=settings,
|
|
266
|
+
metadata={"prestress_state_source": "none" if element_states is None else type(element_states).__name__},
|
|
267
|
+
).to_dict()
|
|
268
|
+
return BucklingResult([], num_modes, "zero_geometric_stiffness", constraint_info, assembly_info, result_case, diagnostics)
|
|
269
|
+
|
|
270
|
+
Q_rigid, nullspace_info = build_reduced_rigid_body_modes(model, independent_dofs, int(K.shape[0]))
|
|
271
|
+
assembly_info["nullspace"] = nullspace_info
|
|
272
|
+
|
|
273
|
+
# Work with the inverted symmetric pencil KG phi = mu K phi where K is
|
|
274
|
+
# positive definite after constraint elimination. The largest mu
|
|
275
|
+
# correspond to the smallest positive load factors lambda = 1/mu, and the
|
|
276
|
+
# symmetric formulation lets both the Lanczos and the dense solver work on
|
|
277
|
+
# well-posed problems (KG itself may be indefinite or singular).
|
|
278
|
+
n_red = int(K_red.shape[0])
|
|
279
|
+
K_sym = (0.5 * (K_red + K_red.T)).tocsr()
|
|
280
|
+
KG_sym = (0.5 * (KG_red + KG_red.T)).tocsr()
|
|
281
|
+
k = min(max(num_modes * max(int(search_factor), 1), num_modes + 2), n_red - 1)
|
|
282
|
+
|
|
283
|
+
eigenvectors = None
|
|
284
|
+
solver_kind = "not_started"
|
|
285
|
+
sparse_error = None
|
|
286
|
+
shift_invert_diagnostics: Dict[str, Any] = {"shift_invert": False}
|
|
287
|
+
if n_red > dense_size_limit and 1 <= k < n_red:
|
|
288
|
+
try:
|
|
289
|
+
if shift_load_factor is None:
|
|
290
|
+
_, eigenvectors = sparse_linalg.eigsh(KG_sym.tocsc(), k=k, M=K_sym.tocsc(), which="LA")
|
|
291
|
+
else:
|
|
292
|
+
sigma = 1.0 / float(shift_load_factor)
|
|
293
|
+
shift_matrix = (KG_sym - sigma * K_sym).tocsc()
|
|
294
|
+
cache = factorization_cache or FactorizationCache(name="buckling_shift_invert", max_entries=2)
|
|
295
|
+
operator, handle = cached_inverse_operator(
|
|
296
|
+
shift_matrix,
|
|
297
|
+
MatrixClass.SYMMETRIC_INDEFINITE,
|
|
298
|
+
cache=cache,
|
|
299
|
+
)
|
|
300
|
+
_, eigenvectors = sparse_linalg.eigsh(
|
|
301
|
+
KG_sym.tocsc(),
|
|
302
|
+
k=k,
|
|
303
|
+
M=K_sym.tocsc(),
|
|
304
|
+
sigma=sigma,
|
|
305
|
+
which="LM",
|
|
306
|
+
OPinv=operator,
|
|
307
|
+
)
|
|
308
|
+
shift_invert_diagnostics = {
|
|
309
|
+
"shift_invert": True,
|
|
310
|
+
"shift_inverse_sigma": float(sigma),
|
|
311
|
+
"shift_factorization": handle.diagnostics(),
|
|
312
|
+
"factorization_cache": cache.diagnostics(),
|
|
313
|
+
}
|
|
314
|
+
solver_kind = "sparse_scipy_eigsh_inverted_pencil"
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
sparse_error = str(exc)
|
|
317
|
+
eigenvectors = None
|
|
318
|
+
if eigenvectors is None and (n_red <= dense_size_limit or allow_dense_fallback):
|
|
319
|
+
K_dense = _as_symmetric_dense(K_red)
|
|
320
|
+
KG_dense = _as_symmetric_dense(KG_red)
|
|
321
|
+
try:
|
|
322
|
+
_, eigenvectors = linalg.eigh(KG_dense, K_dense)
|
|
323
|
+
solver_kind = "dense_scipy_eigh_inverted_pencil"
|
|
324
|
+
except linalg.LinAlgError:
|
|
325
|
+
# Singular K (e.g. unconstrained model): fall back to the general
|
|
326
|
+
# nonsymmetric pencil and let the Rayleigh filtering sort it out.
|
|
327
|
+
_, eigenvectors = linalg.eig(K_dense, KG_dense)
|
|
328
|
+
solver_kind = "dense_scipy_eig_general_pencil"
|
|
329
|
+
if eigenvectors is None:
|
|
330
|
+
diagnostics = {
|
|
331
|
+
"status": "failed",
|
|
332
|
+
"solver": solver_kind,
|
|
333
|
+
"sparse_error": sparse_error,
|
|
334
|
+
"reason": "sparse eigensolve failed and dense fallback is disabled for this reduced size",
|
|
335
|
+
}
|
|
336
|
+
result_case = make_result_case(
|
|
337
|
+
name="linear_buckling",
|
|
338
|
+
analysis_type="linear_buckling",
|
|
339
|
+
assembly_info=assembly_info,
|
|
340
|
+
solver_info={"convergence_info": diagnostics},
|
|
341
|
+
recovery={"modes": num_modes},
|
|
342
|
+
settings=settings,
|
|
343
|
+
metadata={"prestress_state_source": "none" if element_states is None else type(element_states).__name__},
|
|
344
|
+
).to_dict()
|
|
345
|
+
return BucklingResult([], num_modes, "failed", constraint_info, assembly_info, result_case, diagnostics)
|
|
346
|
+
|
|
347
|
+
candidates: List[tuple[float, np.ndarray, float, float, float, float]] = []
|
|
348
|
+
rejected: List[Dict[str, Any]] = []
|
|
349
|
+
for i in range(eigenvectors.shape[1]):
|
|
350
|
+
reduced_mode = np.asarray(np.real(eigenvectors[:, i]), dtype=float)
|
|
351
|
+
mode_norm = float(np.linalg.norm(reduced_mode))
|
|
352
|
+
if not np.isfinite(mode_norm) or mode_norm <= 0.0:
|
|
353
|
+
rejected.append({"root_index": int(i), "reason": "invalid_or_zero_norm"})
|
|
354
|
+
continue
|
|
355
|
+
reduced_mode = reduced_mode / mode_norm
|
|
356
|
+
rigid_body_correlation = float(np.max(np.abs(Q_rigid.T @ reduced_mode))) if Q_rigid.shape[1] else 0.0
|
|
357
|
+
if rigid_body_correlation > 0.90 and not allow_free_mechanisms:
|
|
358
|
+
rejected.append(
|
|
359
|
+
{
|
|
360
|
+
"root_index": int(i),
|
|
361
|
+
"reason": "rigid_body_mode",
|
|
362
|
+
"rigid_body_correlation": rigid_body_correlation,
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
continue
|
|
366
|
+
modal_geometric = float(reduced_mode @ (KG_sym @ reduced_mode))
|
|
367
|
+
modal_stiffness = float(reduced_mode @ (K_sym @ reduced_mode))
|
|
368
|
+
if modal_geometric <= eigen_tolerance or modal_stiffness <= 0.0:
|
|
369
|
+
rejected.append(
|
|
370
|
+
{
|
|
371
|
+
"root_index": int(i),
|
|
372
|
+
"reason": "nonpositive_modal_terms",
|
|
373
|
+
"modal_stiffness": float(modal_stiffness),
|
|
374
|
+
"modal_geometric_stiffness": float(modal_geometric),
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
continue
|
|
378
|
+
rayleigh_value = modal_stiffness / modal_geometric
|
|
379
|
+
if rayleigh_value <= eigen_tolerance or not np.isfinite(rayleigh_value):
|
|
380
|
+
rejected.append({"root_index": int(i), "reason": "invalid_load_factor", "load_factor": float(rayleigh_value)})
|
|
381
|
+
continue
|
|
382
|
+
if not _factor_in_range(rayleigh_value, load_factor_range):
|
|
383
|
+
rejected.append({"root_index": int(i), "reason": "outside_load_factor_range", "load_factor": float(rayleigh_value)})
|
|
384
|
+
continue
|
|
385
|
+
residual_norm = _mode_residual(K_sym, KG_sym, reduced_mode, rayleigh_value)
|
|
386
|
+
candidates.append((rayleigh_value, reduced_mode, modal_stiffness, modal_geometric, residual_norm, rigid_body_correlation))
|
|
387
|
+
|
|
388
|
+
candidates.sort(key=lambda item: _sort_key(item[0], shift_load_factor))
|
|
389
|
+
modes: List[BucklingMode] = []
|
|
390
|
+
for mode_number, (value, reduced_mode, _modal_stiffness, _modal_geometric, _residual_norm, rigid_body_correlation) in enumerate(
|
|
391
|
+
candidates[:num_modes],
|
|
392
|
+
start=1,
|
|
393
|
+
):
|
|
394
|
+
full_mode = np.asarray(T @ reduced_mode, dtype=float).reshape(-1)
|
|
395
|
+
full_mode, reduced_mode = _normalize_mode(full_mode, reduced_mode)
|
|
396
|
+
modal_stiffness = float(reduced_mode @ (K_sym @ reduced_mode))
|
|
397
|
+
modal_geometric = float(reduced_mode @ (KG_sym @ reduced_mode))
|
|
398
|
+
residual_norm = _mode_residual(K_sym, KG_sym, reduced_mode, value)
|
|
399
|
+
validity = "ok" if residual_norm <= max(1.0e-6, 100.0 * eigen_tolerance) else "high_residual"
|
|
400
|
+
modes.append(
|
|
401
|
+
BucklingMode(
|
|
402
|
+
mode_number=mode_number,
|
|
403
|
+
load_factor=float(value),
|
|
404
|
+
eigenvalue=float(value),
|
|
405
|
+
mode_shape=full_mode,
|
|
406
|
+
reduced_mode_shape=reduced_mode,
|
|
407
|
+
modal_stiffness=modal_stiffness,
|
|
408
|
+
modal_geometric_stiffness=modal_geometric,
|
|
409
|
+
residual_norm=residual_norm,
|
|
410
|
+
rigid_body_correlation=rigid_body_correlation,
|
|
411
|
+
validity_status=validity,
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
repeated_groups = _assign_repeated_groups(modes, repeated_tolerance)
|
|
416
|
+
status = "ok" if modes else "no_positive_modes"
|
|
417
|
+
diagnostics = {
|
|
418
|
+
"status": status,
|
|
419
|
+
"solver": solver_kind,
|
|
420
|
+
**shift_invert_diagnostics,
|
|
421
|
+
"sparse_error": sparse_error,
|
|
422
|
+
"nullspace_rank": int(Q_rigid.shape[1]),
|
|
423
|
+
"nullspace_info": nullspace_info,
|
|
424
|
+
"free_mechanism_handling": "retained" if allow_free_mechanisms else "rigid_body_roots_filtered",
|
|
425
|
+
"num_roots_considered": int(eigenvectors.shape[1]),
|
|
426
|
+
"num_rejected_roots": int(len(rejected)),
|
|
427
|
+
"rejected_roots": rejected[:50],
|
|
428
|
+
"max_residual_norm": max((mode.residual_norm for mode in modes), default=0.0),
|
|
429
|
+
"repeated_mode_groups": repeated_groups,
|
|
430
|
+
"num_repeated_mode_groups": int(len(repeated_groups)),
|
|
431
|
+
"sorting": "nearest_shift" if shift_load_factor is not None else "ascending_load_factor",
|
|
432
|
+
}
|
|
433
|
+
result_case = make_result_case(
|
|
434
|
+
name="linear_buckling",
|
|
435
|
+
analysis_type="linear_buckling",
|
|
436
|
+
assembly_info=assembly_info,
|
|
437
|
+
solver_info={"convergence_info": diagnostics},
|
|
438
|
+
recovery={"modes": num_modes, "num_modes_returned": len(modes)},
|
|
439
|
+
settings=settings,
|
|
440
|
+
metadata={"prestress_state_source": "none" if element_states is None else type(element_states).__name__},
|
|
441
|
+
).to_dict()
|
|
442
|
+
return BucklingResult(modes, num_modes, status, constraint_info, assembly_info, result_case, diagnostics)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Sparse buckling validity smoke metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .boundary import BoundaryCondition
|
|
12
|
+
from .buckling import solve_eigenvalue_buckling
|
|
13
|
+
from .elements import BeamElement
|
|
14
|
+
from .fe_core import FEModel
|
|
15
|
+
|
|
16
|
+
DEFAULT_BUCKLING_VALIDITY_PATH = Path("reports/buckling_validity/buckling_validity_report.json")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _column_model(num_elements: int = 12, symmetric: bool = False) -> Tuple[FEModel, float, Dict[str, float]]:
|
|
20
|
+
length = 4.0
|
|
21
|
+
model = FEModel("buckling_validity_column")
|
|
22
|
+
model.add_material("steel", 210.0e9, 0.3)
|
|
23
|
+
for i in range(num_elements + 1):
|
|
24
|
+
model.add_node(i + 1, length * i / num_elements, 0.0, 0.0)
|
|
25
|
+
inertia_y = 4.0e-6 if symmetric else 3.0e-6
|
|
26
|
+
inertia_z = 4.0e-6 if symmetric else 5.0e-6
|
|
27
|
+
section = {"area": 0.02, "Iy": inertia_y, "Iz": inertia_z, "J": 2.0e-6}
|
|
28
|
+
for element_id in range(1, num_elements + 1):
|
|
29
|
+
model.add_element(element_id, BeamElement(element_id, [element_id, element_id + 1], "steel", section))
|
|
30
|
+
all_nodes = list(range(1, num_elements + 2))
|
|
31
|
+
end_nodes = [1, num_elements + 1]
|
|
32
|
+
if symmetric:
|
|
33
|
+
model.add_boundary_condition(BoundaryCondition("suppress_axial_torsion", all_nodes, {"ux": 0.0, "rx": 0.0}))
|
|
34
|
+
model.add_boundary_condition(BoundaryCondition("pinned_lateral_ends", end_nodes, {"uy": 0.0, "uz": 0.0}))
|
|
35
|
+
else:
|
|
36
|
+
model.add_boundary_condition(
|
|
37
|
+
BoundaryCondition("suppress_unrelated_dofs", all_nodes, {"ux": 0.0, "uz": 0.0, "rx": 0.0, "ry": 0.0})
|
|
38
|
+
)
|
|
39
|
+
model.add_boundary_condition(BoundaryCondition("pinned_lateral_ends", end_nodes, {"uy": 0.0}))
|
|
40
|
+
return model, length, section
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def generate_buckling_validity_report() -> Dict[str, Any]:
|
|
44
|
+
model, length, section = _column_model()
|
|
45
|
+
states = {element_id: {"axial_compression": 1.0} for element_id in model.mesh.elements}
|
|
46
|
+
result = solve_eigenvalue_buckling(model, states, num_modes=3, dense_size_limit=2)
|
|
47
|
+
expected = np.pi**2 * model.get_material("steel").elastic_modulus * section["Iz"] / length**2
|
|
48
|
+
|
|
49
|
+
higher = solve_eigenvalue_buckling(
|
|
50
|
+
model,
|
|
51
|
+
states,
|
|
52
|
+
num_modes=1,
|
|
53
|
+
load_factor_range=(3.0 * float(result.critical_load_factor), 5.0 * float(result.critical_load_factor)),
|
|
54
|
+
dense_size_limit=2,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
symmetric, _, _ = _column_model(symmetric=True)
|
|
58
|
+
symmetric_states = {element_id: {"axial_compression": 1.0} for element_id in symmetric.mesh.elements}
|
|
59
|
+
repeated = solve_eigenvalue_buckling(symmetric, symmetric_states, num_modes=4, dense_size_limit=2, repeated_tolerance=1.0e-2)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"status": "passed" if result.solver_status == "ok" and repeated.diagnostics["num_repeated_mode_groups"] >= 1 else "failed",
|
|
63
|
+
"euler_column": {
|
|
64
|
+
"critical_load_factor": result.critical_load_factor,
|
|
65
|
+
"expected_euler_load": float(expected),
|
|
66
|
+
"relative_error": abs(float(result.critical_load_factor) - float(expected)) / float(expected),
|
|
67
|
+
"diagnostics": result.diagnostics,
|
|
68
|
+
},
|
|
69
|
+
"higher_mode_range": {
|
|
70
|
+
"critical_load_factor": higher.critical_load_factor,
|
|
71
|
+
"ratio_to_first": float(higher.critical_load_factor) / float(result.critical_load_factor),
|
|
72
|
+
"diagnostics": higher.diagnostics,
|
|
73
|
+
},
|
|
74
|
+
"repeated_modes": {
|
|
75
|
+
"load_factors": [mode.load_factor for mode in repeated.modes],
|
|
76
|
+
"groups": repeated.diagnostics["repeated_mode_groups"],
|
|
77
|
+
"diagnostics": repeated.diagnostics,
|
|
78
|
+
},
|
|
79
|
+
"known_limitations": [
|
|
80
|
+
"Shifted sparse buckling now exposes an explicit cached shift-invert factorization; unshifted eigsh still uses SciPy's internal operator policy.",
|
|
81
|
+
"Repeated modes are grouped by load-factor proximity; deterministic basis projection is later modal/buckling work.",
|
|
82
|
+
"External plate, shell and cylinder buckling references remain Phase 14 validation work.",
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def write_buckling_validity_report(path: Path = DEFAULT_BUCKLING_VALIDITY_PATH) -> Dict[str, Any]:
|
|
88
|
+
report = generate_buckling_validity_report()
|
|
89
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
91
|
+
return report
|