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/arc_length.py
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
"""Bounded Crisfield-style arc-length continuation for nonlinear static analysis.
|
|
2
|
+
|
|
3
|
+
This module follows one proportional reference load through the first limit
|
|
4
|
+
point. It reuses the production nonlinear element response, constraint
|
|
5
|
+
transformation and committed material-state machinery from
|
|
6
|
+
:mod:`anysolver.nonlinear_static`.
|
|
7
|
+
|
|
8
|
+
Scope is deliberately limited to the ANYsolver capacity workflow:
|
|
9
|
+
|
|
10
|
+
* one proportional load pattern plus an optional constant preload,
|
|
11
|
+
* constrained models (no nonlinear free-free nullspace solve),
|
|
12
|
+
* geometric and material nonlinearity already supported by the elements,
|
|
13
|
+
* continuation only far enough beyond the peak to confirm the descending
|
|
14
|
+
branch.
|
|
15
|
+
|
|
16
|
+
The equilibrium equations are
|
|
17
|
+
|
|
18
|
+
R(q, lambda) = F_constant + lambda F_reference - F_internal(q) = 0
|
|
19
|
+
|
|
20
|
+
with the spherical constraint
|
|
21
|
+
|
|
22
|
+
dq.T W dq + alpha**2 dlambda**2 = ds**2.
|
|
23
|
+
|
|
24
|
+
Newton corrections use block elimination. The tangent is factorized once per
|
|
25
|
+
iteration and solved for two right-hand sides instead of assembling a generally
|
|
26
|
+
nonsymmetric bordered matrix.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import copy
|
|
32
|
+
import time
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
from scipy import sparse
|
|
38
|
+
from scipy.sparse import linalg as sparse_linalg
|
|
39
|
+
|
|
40
|
+
from .assembly import build_constraint_transformation
|
|
41
|
+
from .cases import make_result_case
|
|
42
|
+
from .linalg import MatrixClass, factorize
|
|
43
|
+
from .matrix_assembly import assemble_load_vector, assemble_stiffness_matrix
|
|
44
|
+
from .nonlinear_static import (
|
|
45
|
+
_assemble_nonlinear_system,
|
|
46
|
+
_copy_initial_states,
|
|
47
|
+
_max_plastic_strain,
|
|
48
|
+
_nonlinear_state_summary,
|
|
49
|
+
solve_static_nonlinear,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from .boundary import LoadCase
|
|
54
|
+
from .fe_core import FEModel
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_SMALL = 1.0e-14
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class ArcLengthControl:
|
|
62
|
+
"""Controls for bounded spherical arc-length continuation.
|
|
63
|
+
|
|
64
|
+
``initial_load_increment`` is used only to construct the first arc radius
|
|
65
|
+
from the initial tangent direction. Thereafter the radius is adapted in
|
|
66
|
+
path space, not forced to produce a particular load increment.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
initial_load_increment: float = 0.05
|
|
70
|
+
minimum_load_increment: float = 5.0e-4
|
|
71
|
+
maximum_load_increment: float = 0.20
|
|
72
|
+
load_scaling: Optional[float] = None
|
|
73
|
+
rotation_length_scale: Optional[float] = None
|
|
74
|
+
target_iterations: int = 5
|
|
75
|
+
growth_factor: float = 1.25
|
|
76
|
+
cutback_factor: float = 0.5
|
|
77
|
+
max_steps: int = 100
|
|
78
|
+
max_retries_per_step: int = 8
|
|
79
|
+
stop_after_peak_steps: int = 4
|
|
80
|
+
peak_drop_tolerance: float = 1.0e-3
|
|
81
|
+
maximum_absolute_load_factor: Optional[float] = None
|
|
82
|
+
preload_steps: int = 10
|
|
83
|
+
# Post-buckling continuation controls. When ``post_peak_load_fraction``
|
|
84
|
+
# is set the trace continues past the limit point and stops automatically
|
|
85
|
+
# once the load factor has fallen to that fraction of the recorded peak
|
|
86
|
+
# (set ``stop_after_peak_steps`` high to allow the descending branch).
|
|
87
|
+
# ``max_translation`` is an absolute displacement guard in metres on the
|
|
88
|
+
# largest nodal translation, protecting against runaway post-peak paths.
|
|
89
|
+
post_peak_load_fraction: Optional[float] = None
|
|
90
|
+
max_translation: Optional[float] = None
|
|
91
|
+
|
|
92
|
+
def __post_init__(self) -> None:
|
|
93
|
+
if self.initial_load_increment <= 0.0:
|
|
94
|
+
raise ValueError("initial_load_increment must be positive")
|
|
95
|
+
if self.minimum_load_increment <= 0.0:
|
|
96
|
+
raise ValueError("minimum_load_increment must be positive")
|
|
97
|
+
if self.maximum_load_increment < self.initial_load_increment:
|
|
98
|
+
raise ValueError("maximum_load_increment must be at least initial_load_increment")
|
|
99
|
+
if self.minimum_load_increment > self.initial_load_increment:
|
|
100
|
+
raise ValueError("minimum_load_increment must not exceed initial_load_increment")
|
|
101
|
+
if self.load_scaling is not None and self.load_scaling <= 0.0:
|
|
102
|
+
raise ValueError("load_scaling must be positive when supplied")
|
|
103
|
+
if self.rotation_length_scale is not None and self.rotation_length_scale <= 0.0:
|
|
104
|
+
raise ValueError("rotation_length_scale must be positive when supplied")
|
|
105
|
+
if self.target_iterations <= 0:
|
|
106
|
+
raise ValueError("target_iterations must be positive")
|
|
107
|
+
if self.growth_factor < 1.0:
|
|
108
|
+
raise ValueError("growth_factor must be at least 1.0")
|
|
109
|
+
if not (0.0 < self.cutback_factor < 1.0):
|
|
110
|
+
raise ValueError("cutback_factor must be between 0 and 1")
|
|
111
|
+
if self.max_steps <= 0 or self.max_retries_per_step <= 0:
|
|
112
|
+
raise ValueError("max_steps and max_retries_per_step must be positive")
|
|
113
|
+
if self.stop_after_peak_steps <= 0:
|
|
114
|
+
raise ValueError("stop_after_peak_steps must be positive")
|
|
115
|
+
if self.peak_drop_tolerance < 0.0:
|
|
116
|
+
raise ValueError("peak_drop_tolerance must be non-negative")
|
|
117
|
+
if self.maximum_absolute_load_factor is not None and self.maximum_absolute_load_factor <= 0.0:
|
|
118
|
+
raise ValueError("maximum_absolute_load_factor must be positive when supplied")
|
|
119
|
+
if self.preload_steps <= 0:
|
|
120
|
+
raise ValueError("preload_steps must be positive")
|
|
121
|
+
if self.post_peak_load_fraction is not None and not (0.0 < self.post_peak_load_fraction < 1.0):
|
|
122
|
+
raise ValueError("post_peak_load_fraction must be in (0, 1) when supplied")
|
|
123
|
+
if self.max_translation is not None and self.max_translation <= 0.0:
|
|
124
|
+
raise ValueError("max_translation must be positive when supplied")
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
127
|
+
return {
|
|
128
|
+
"initial_load_increment": float(self.initial_load_increment),
|
|
129
|
+
"minimum_load_increment": float(self.minimum_load_increment),
|
|
130
|
+
"maximum_load_increment": float(self.maximum_load_increment),
|
|
131
|
+
"load_scaling": self.load_scaling,
|
|
132
|
+
"rotation_length_scale": self.rotation_length_scale,
|
|
133
|
+
"target_iterations": int(self.target_iterations),
|
|
134
|
+
"growth_factor": float(self.growth_factor),
|
|
135
|
+
"cutback_factor": float(self.cutback_factor),
|
|
136
|
+
"max_steps": int(self.max_steps),
|
|
137
|
+
"max_retries_per_step": int(self.max_retries_per_step),
|
|
138
|
+
"stop_after_peak_steps": int(self.stop_after_peak_steps),
|
|
139
|
+
"peak_drop_tolerance": float(self.peak_drop_tolerance),
|
|
140
|
+
"maximum_absolute_load_factor": self.maximum_absolute_load_factor,
|
|
141
|
+
"preload_steps": int(self.preload_steps),
|
|
142
|
+
"post_peak_load_fraction": self.post_peak_load_fraction,
|
|
143
|
+
"max_translation": self.max_translation,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class ArcLengthStep:
|
|
149
|
+
"""One converged point on the equilibrium path."""
|
|
150
|
+
|
|
151
|
+
step_index: int
|
|
152
|
+
load_factor: float
|
|
153
|
+
iterations: int
|
|
154
|
+
retries: int
|
|
155
|
+
arc_radius: float
|
|
156
|
+
residual_norm: float
|
|
157
|
+
arc_residual: float
|
|
158
|
+
displacement_norm: float
|
|
159
|
+
load_increment: float
|
|
160
|
+
path_increment_norm: float
|
|
161
|
+
max_equivalent_plastic_strain: float
|
|
162
|
+
is_peak: bool = False
|
|
163
|
+
|
|
164
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
165
|
+
return {
|
|
166
|
+
"step_index": int(self.step_index),
|
|
167
|
+
"load_factor": float(self.load_factor),
|
|
168
|
+
"iterations": int(self.iterations),
|
|
169
|
+
"retries": int(self.retries),
|
|
170
|
+
"arc_radius": float(self.arc_radius),
|
|
171
|
+
"residual_norm": float(self.residual_norm),
|
|
172
|
+
"arc_residual": float(self.arc_residual),
|
|
173
|
+
"displacement_norm": float(self.displacement_norm),
|
|
174
|
+
"load_increment": float(self.load_increment),
|
|
175
|
+
"path_increment_norm": float(self.path_increment_norm),
|
|
176
|
+
"max_equivalent_plastic_strain": float(self.max_equivalent_plastic_strain),
|
|
177
|
+
"is_peak": bool(self.is_peak),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class ArcLengthResult:
|
|
183
|
+
"""Result from bounded arc-length continuation."""
|
|
184
|
+
|
|
185
|
+
steps: List[ArcLengthStep]
|
|
186
|
+
status: str
|
|
187
|
+
displacements: np.ndarray
|
|
188
|
+
load_factor: float
|
|
189
|
+
peak_load_factor: float
|
|
190
|
+
peak_step_index: Optional[int]
|
|
191
|
+
element_states: Dict[int, Any] = field(default_factory=dict)
|
|
192
|
+
info: Dict[str, Any] = field(default_factory=dict)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def converged(self) -> bool:
|
|
196
|
+
return self.status in {
|
|
197
|
+
"peak_confirmed",
|
|
198
|
+
"maximum_steps_reached",
|
|
199
|
+
"load_factor_limit_reached",
|
|
200
|
+
"post_buckling_traced",
|
|
201
|
+
"displacement_limit_reached",
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def capacity_estimate(self) -> float:
|
|
206
|
+
return float(self.peak_load_factor)
|
|
207
|
+
|
|
208
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
209
|
+
return {
|
|
210
|
+
"status": self.status,
|
|
211
|
+
"converged": self.converged,
|
|
212
|
+
"load_factor": float(self.load_factor),
|
|
213
|
+
"peak_load_factor": float(self.peak_load_factor),
|
|
214
|
+
"peak_step_index": self.peak_step_index,
|
|
215
|
+
"capacity_estimate": self.capacity_estimate,
|
|
216
|
+
"info": self.info,
|
|
217
|
+
"steps": [step.to_dict() for step in self.steps],
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _characteristic_length(model: "FEModel") -> float:
|
|
222
|
+
coords = np.asarray(model.mesh.get_node_coordinates(), dtype=float)
|
|
223
|
+
if coords.size == 0:
|
|
224
|
+
return 1.0
|
|
225
|
+
spans = np.ptp(coords, axis=0)
|
|
226
|
+
value = float(np.max(spans))
|
|
227
|
+
return value if value > _SMALL else 1.0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _reduced_metric(model: "FEModel", T: sparse.spmatrix, rotation_length_scale: float) -> sparse.csr_matrix:
|
|
231
|
+
"""Project a translation-equivalent full-DOF metric to reduced coordinates."""
|
|
232
|
+
total_dofs = model.mesh.dof_manager.total_dofs
|
|
233
|
+
weights = np.ones(total_dofs, dtype=float)
|
|
234
|
+
rotation_weight = float(rotation_length_scale) ** 2
|
|
235
|
+
for dof in range(total_dofs):
|
|
236
|
+
_node_id, local_index, _name = model.mesh.dof_manager.get_dof_info(dof)
|
|
237
|
+
if local_index >= 3:
|
|
238
|
+
weights[dof] = rotation_weight
|
|
239
|
+
W_full = sparse.diags(weights, format="csr")
|
|
240
|
+
return (T.T @ W_full @ T).tocsr()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _metric_dot(W: sparse.spmatrix, left: np.ndarray, right: np.ndarray) -> float:
|
|
244
|
+
return float(np.asarray(left, dtype=float) @ np.asarray(W @ right, dtype=float))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _metric_norm(W: sparse.spmatrix, vector: np.ndarray) -> float:
|
|
248
|
+
return float(np.sqrt(max(_metric_dot(W, vector, vector), 0.0)))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _factorized_solve(matrix: sparse.spmatrix, rhs: np.ndarray, signature: str) -> np.ndarray:
|
|
252
|
+
handle = factorize(matrix, MatrixClass.SYMMETRIC_INDEFINITE, signature=signature)
|
|
253
|
+
solution = np.asarray(handle.solve(np.asarray(rhs, dtype=float)), dtype=float).reshape(-1)
|
|
254
|
+
if np.any(~np.isfinite(solution)):
|
|
255
|
+
raise np.linalg.LinAlgError("non-finite tangent solution")
|
|
256
|
+
return solution
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _recover_reduced_coordinates(T: sparse.spmatrix, u0: np.ndarray, displacements: np.ndarray) -> np.ndarray:
|
|
260
|
+
rhs = np.asarray(displacements, dtype=float).reshape(-1) - np.asarray(u0, dtype=float).reshape(-1)
|
|
261
|
+
result = sparse_linalg.lsqr(T, rhs, atol=1.0e-12, btol=1.0e-12)
|
|
262
|
+
q = np.asarray(result[0], dtype=float).reshape(-1)
|
|
263
|
+
mismatch = np.asarray(T @ q + u0 - displacements, dtype=float).reshape(-1)
|
|
264
|
+
scale = max(float(np.linalg.norm(displacements)), 1.0)
|
|
265
|
+
if float(np.linalg.norm(mismatch)) > 1.0e-8 * scale:
|
|
266
|
+
raise RuntimeError("could not recover reduced coordinates from preloaded displacement state")
|
|
267
|
+
return q
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _copy_model_with_imperfection(model: "FEModel", imperfection: Optional[Any]) -> "FEModel":
|
|
271
|
+
if imperfection is None:
|
|
272
|
+
return model
|
|
273
|
+
from .imperfections import apply_imperfection
|
|
274
|
+
|
|
275
|
+
return apply_imperfection(model, imperfection, copy_model=True)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _max_nodal_translation(model: "FEModel", displacements: np.ndarray) -> float:
|
|
279
|
+
"""Largest nodal translation magnitude in the displacement vector."""
|
|
280
|
+
peak = 0.0
|
|
281
|
+
size = int(displacements.size)
|
|
282
|
+
for node in model.mesh.nodes.values():
|
|
283
|
+
dofs = np.asarray(node.dofs[:3], dtype=np.intp)
|
|
284
|
+
if dofs.size == 0 or int(dofs.max()) >= size:
|
|
285
|
+
continue
|
|
286
|
+
value = float(np.linalg.norm(displacements[dofs]))
|
|
287
|
+
if value > peak:
|
|
288
|
+
peak = value
|
|
289
|
+
return peak
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def solve_static_arc_length(
|
|
293
|
+
model: "FEModel",
|
|
294
|
+
load_case: "LoadCase",
|
|
295
|
+
*,
|
|
296
|
+
constant_load_case: Optional["LoadCase"] = None,
|
|
297
|
+
control: Optional[ArcLengthControl] = None,
|
|
298
|
+
max_iterations: int = 25,
|
|
299
|
+
tolerance: float = 1.0e-6,
|
|
300
|
+
arc_tolerance: float = 1.0e-6,
|
|
301
|
+
num_layers: int = 5,
|
|
302
|
+
imperfection: Optional[Any] = None,
|
|
303
|
+
initial_element_states: Optional[Mapping[int, Any]] = None,
|
|
304
|
+
progress_callback: Optional[Any] = None,
|
|
305
|
+
) -> ArcLengthResult:
|
|
306
|
+
"""Trace the first nonlinear limit point with spherical arc-length control.
|
|
307
|
+
|
|
308
|
+
The optional ``constant_load_case`` is first brought to equilibrium using
|
|
309
|
+
the existing adaptive force-control solver. Arc-length continuation then
|
|
310
|
+
scales only ``load_case``. Material states are committed only after a full
|
|
311
|
+
equilibrium-plus-constraint convergence.
|
|
312
|
+
"""
|
|
313
|
+
if load_case is None:
|
|
314
|
+
raise ValueError("load_case is required for arc-length continuation")
|
|
315
|
+
if max_iterations <= 0:
|
|
316
|
+
raise ValueError("max_iterations must be positive")
|
|
317
|
+
if tolerance <= 0.0 or arc_tolerance <= 0.0:
|
|
318
|
+
raise ValueError("tolerances must be positive")
|
|
319
|
+
if num_layers <= 0:
|
|
320
|
+
raise ValueError("num_layers must be positive")
|
|
321
|
+
|
|
322
|
+
settings = control or ArcLengthControl()
|
|
323
|
+
start_time = time.time()
|
|
324
|
+
working_model = _copy_model_with_imperfection(model, imperfection)
|
|
325
|
+
working_model.apply_boundary_conditions()
|
|
326
|
+
|
|
327
|
+
K0, stiffness_info = assemble_stiffness_matrix(working_model)
|
|
328
|
+
F_prop, load_info = assemble_load_vector(working_model, load_case)
|
|
329
|
+
if constant_load_case is None:
|
|
330
|
+
F_const = np.zeros_like(F_prop)
|
|
331
|
+
constant_load_info = None
|
|
332
|
+
else:
|
|
333
|
+
F_const, constant_load_info = assemble_load_vector(working_model, constant_load_case)
|
|
334
|
+
|
|
335
|
+
_, _, T, u0, _, constraint_info = build_constraint_transformation(K0, F_prop, working_model)
|
|
336
|
+
n_red = int(T.shape[1])
|
|
337
|
+
assembly_info = {
|
|
338
|
+
"stiffness": stiffness_info,
|
|
339
|
+
"load": load_info,
|
|
340
|
+
"constant_load": constant_load_info,
|
|
341
|
+
"constraint_info": constraint_info,
|
|
342
|
+
"total_dofs": int(working_model.mesh.dof_manager.total_dofs),
|
|
343
|
+
"reduced_dofs": n_red,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
info: Dict[str, Any] = {
|
|
347
|
+
**assembly_info,
|
|
348
|
+
"control": settings.to_dict(),
|
|
349
|
+
"num_layers": int(num_layers),
|
|
350
|
+
"formulation": "crisfield_spherical_block_elimination",
|
|
351
|
+
}
|
|
352
|
+
if imperfection is not None:
|
|
353
|
+
info["imperfection"] = getattr(working_model, "imperfection_metadata", [])
|
|
354
|
+
|
|
355
|
+
if n_red == 0:
|
|
356
|
+
info["failure_reason"] = "empty_reduced_system"
|
|
357
|
+
return ArcLengthResult([], "empty_reduced_system", u0.copy(), 0.0, 0.0, None, {}, info)
|
|
358
|
+
|
|
359
|
+
F_prop_red = np.asarray(T.T @ F_prop, dtype=float).reshape(-1)
|
|
360
|
+
F_const_red = np.asarray(T.T @ F_const, dtype=float).reshape(-1)
|
|
361
|
+
if float(np.linalg.norm(F_prop_red)) <= _SMALL:
|
|
362
|
+
info["failure_reason"] = "zero_reduced_reference_load"
|
|
363
|
+
return ArcLengthResult([], "zero_reference_load", u0.copy(), 0.0, 0.0, None, {}, info)
|
|
364
|
+
|
|
365
|
+
committed_states: Dict[int, Any] = _copy_initial_states(initial_element_states)
|
|
366
|
+
q = np.zeros(n_red, dtype=float)
|
|
367
|
+
lam = 0.0
|
|
368
|
+
preload_info = None
|
|
369
|
+
|
|
370
|
+
if constant_load_case is not None and float(np.linalg.norm(F_const_red)) > _SMALL:
|
|
371
|
+
preload = solve_static_nonlinear(
|
|
372
|
+
working_model,
|
|
373
|
+
load_case=constant_load_case,
|
|
374
|
+
max_load_factor=1.0,
|
|
375
|
+
num_steps=settings.preload_steps,
|
|
376
|
+
max_iterations=max_iterations,
|
|
377
|
+
tolerance=tolerance,
|
|
378
|
+
num_layers=num_layers,
|
|
379
|
+
initial_element_states=committed_states,
|
|
380
|
+
)
|
|
381
|
+
preload_info = preload.to_dict()
|
|
382
|
+
if preload.status != "completed":
|
|
383
|
+
info["preload"] = preload_info
|
|
384
|
+
info["failure_reason"] = "constant_preload_not_converged"
|
|
385
|
+
return ArcLengthResult(
|
|
386
|
+
[],
|
|
387
|
+
"preload_failed",
|
|
388
|
+
preload.displacements,
|
|
389
|
+
0.0,
|
|
390
|
+
0.0,
|
|
391
|
+
None,
|
|
392
|
+
preload.element_states,
|
|
393
|
+
info,
|
|
394
|
+
)
|
|
395
|
+
q = _recover_reduced_coordinates(T, u0, preload.displacements)
|
|
396
|
+
committed_states = copy.deepcopy(preload.element_states)
|
|
397
|
+
|
|
398
|
+
rotation_scale = settings.rotation_length_scale or _characteristic_length(working_model)
|
|
399
|
+
W = _reduced_metric(working_model, T, rotation_scale)
|
|
400
|
+
|
|
401
|
+
# Establish the first tangent direction and derive fixed path-space radius
|
|
402
|
+
# limits from the user-facing load-increment settings.
|
|
403
|
+
u = np.asarray(T @ q + u0, dtype=float).reshape(-1)
|
|
404
|
+
F_int, K_T, _trial_states = _assemble_nonlinear_system(
|
|
405
|
+
working_model, u, committed_states, num_layers, tangent=True
|
|
406
|
+
)
|
|
407
|
+
residual0 = F_const_red + lam * F_prop_red - np.asarray(T.T @ F_int, dtype=float).reshape(-1)
|
|
408
|
+
reference0 = max(float(np.linalg.norm(F_const_red + F_prop_red)), 1.0)
|
|
409
|
+
if float(np.linalg.norm(residual0)) > 10.0 * tolerance * reference0:
|
|
410
|
+
info["failure_reason"] = "initial_state_not_in_equilibrium"
|
|
411
|
+
info["initial_residual_norm"] = float(np.linalg.norm(residual0))
|
|
412
|
+
return ArcLengthResult([], "initial_equilibrium_failed", u, lam, lam, None, committed_states, info)
|
|
413
|
+
|
|
414
|
+
K_red = (T.T @ K_T @ T).tocsr()
|
|
415
|
+
try:
|
|
416
|
+
tangent_direction = _factorized_solve(K_red, F_prop_red, "arc_length.initial_tangent")
|
|
417
|
+
except Exception as exc:
|
|
418
|
+
info["failure_reason"] = "initial_tangent_factorization_failed"
|
|
419
|
+
info["factorization_error"] = str(exc)
|
|
420
|
+
return ArcLengthResult([], "initial_tangent_failed", u, lam, lam, None, committed_states, info)
|
|
421
|
+
|
|
422
|
+
tangent_norm = _metric_norm(W, tangent_direction)
|
|
423
|
+
load_scaling = float(settings.load_scaling) if settings.load_scaling is not None else max(tangent_norm, 1.0e-12)
|
|
424
|
+
predictor_norm = float(np.sqrt(tangent_norm * tangent_norm + load_scaling * load_scaling))
|
|
425
|
+
radius = settings.initial_load_increment * predictor_norm
|
|
426
|
+
min_radius = radius * settings.minimum_load_increment / settings.initial_load_increment
|
|
427
|
+
max_radius = radius * settings.maximum_load_increment / settings.initial_load_increment
|
|
428
|
+
|
|
429
|
+
steps: List[ArcLengthStep] = []
|
|
430
|
+
previous_dq: Optional[np.ndarray] = None
|
|
431
|
+
previous_dlambda: Optional[float] = None
|
|
432
|
+
peak_load_factor = float(lam)
|
|
433
|
+
peak_step_index: Optional[int] = None
|
|
434
|
+
max_translation = 0.0
|
|
435
|
+
descending_steps = 0
|
|
436
|
+
status = "maximum_steps_reached"
|
|
437
|
+
failure_reason: Optional[str] = None
|
|
438
|
+
total_iterations = 0
|
|
439
|
+
total_retries = 0
|
|
440
|
+
adaptation_history: List[Dict[str, Any]] = []
|
|
441
|
+
|
|
442
|
+
for step_index in range(1, settings.max_steps + 1):
|
|
443
|
+
q_base = q.copy()
|
|
444
|
+
lambda_base = float(lam)
|
|
445
|
+
states_base = copy.deepcopy(committed_states)
|
|
446
|
+
accepted = False
|
|
447
|
+
step_failure = "unknown"
|
|
448
|
+
|
|
449
|
+
for retry in range(settings.max_retries_per_step + 1):
|
|
450
|
+
total_retries += int(retry > 0)
|
|
451
|
+
u_base = np.asarray(T @ q_base + u0, dtype=float).reshape(-1)
|
|
452
|
+
F_base, K_base, _ = _assemble_nonlinear_system(
|
|
453
|
+
working_model, u_base, states_base, num_layers, tangent=True
|
|
454
|
+
)
|
|
455
|
+
K_base_red = (T.T @ K_base @ T).tocsr()
|
|
456
|
+
try:
|
|
457
|
+
load_direction = _factorized_solve(
|
|
458
|
+
K_base_red,
|
|
459
|
+
F_prop_red,
|
|
460
|
+
f"arc_length.predictor:{step_index}:{retry}",
|
|
461
|
+
)
|
|
462
|
+
except Exception:
|
|
463
|
+
step_failure = "singular_predictor_tangent"
|
|
464
|
+
radius *= settings.cutback_factor
|
|
465
|
+
if radius < min_radius:
|
|
466
|
+
break
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
sign = 1.0
|
|
470
|
+
if previous_dq is not None and previous_dlambda is not None:
|
|
471
|
+
orientation = _metric_dot(W, previous_dq, load_direction) + (
|
|
472
|
+
load_scaling * load_scaling * previous_dlambda
|
|
473
|
+
)
|
|
474
|
+
sign = 1.0 if orientation >= 0.0 else -1.0
|
|
475
|
+
|
|
476
|
+
direction_norm = float(
|
|
477
|
+
np.sqrt(
|
|
478
|
+
max(_metric_dot(W, load_direction, load_direction), 0.0)
|
|
479
|
+
+ load_scaling * load_scaling
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
if direction_norm <= _SMALL:
|
|
483
|
+
step_failure = "zero_predictor_direction"
|
|
484
|
+
break
|
|
485
|
+
|
|
486
|
+
dlambda_total = sign * radius / direction_norm
|
|
487
|
+
dq_total = dlambda_total * load_direction
|
|
488
|
+
q_trial = q_base + dq_total
|
|
489
|
+
lambda_trial = lambda_base + dlambda_total
|
|
490
|
+
residual_norm = float("inf")
|
|
491
|
+
arc_residual = float("inf")
|
|
492
|
+
trial_states = states_base
|
|
493
|
+
|
|
494
|
+
for iteration in range(1, max_iterations + 1):
|
|
495
|
+
total_iterations += 1
|
|
496
|
+
u_trial = np.asarray(T @ q_trial + u0, dtype=float).reshape(-1)
|
|
497
|
+
F_internal, K_trial, states_candidate = _assemble_nonlinear_system(
|
|
498
|
+
working_model, u_trial, states_base, num_layers, tangent=True
|
|
499
|
+
)
|
|
500
|
+
residual = (
|
|
501
|
+
F_const_red
|
|
502
|
+
+ lambda_trial * F_prop_red
|
|
503
|
+
- np.asarray(T.T @ F_internal, dtype=float).reshape(-1)
|
|
504
|
+
)
|
|
505
|
+
residual_norm = float(np.linalg.norm(residual))
|
|
506
|
+
arc_residual = float(
|
|
507
|
+
_metric_dot(W, dq_total, dq_total)
|
|
508
|
+
+ (load_scaling * dlambda_total) ** 2
|
|
509
|
+
- radius * radius
|
|
510
|
+
)
|
|
511
|
+
force_reference = max(
|
|
512
|
+
float(np.linalg.norm(F_const_red + lambda_trial * F_prop_red)),
|
|
513
|
+
float(np.linalg.norm(F_prop_red)),
|
|
514
|
+
1.0,
|
|
515
|
+
)
|
|
516
|
+
arc_reference = max(radius * radius, 1.0e-24)
|
|
517
|
+
|
|
518
|
+
if (
|
|
519
|
+
residual_norm <= tolerance * force_reference
|
|
520
|
+
and abs(arc_residual) <= arc_tolerance * arc_reference
|
|
521
|
+
):
|
|
522
|
+
trial_states = states_candidate
|
|
523
|
+
accepted = True
|
|
524
|
+
break
|
|
525
|
+
|
|
526
|
+
K_trial_red = (T.T @ K_trial @ T).tocsr()
|
|
527
|
+
try:
|
|
528
|
+
handle = factorize(
|
|
529
|
+
K_trial_red,
|
|
530
|
+
MatrixClass.SYMMETRIC_INDEFINITE,
|
|
531
|
+
signature=f"arc_length.corrector:{step_index}:{retry}:{iteration}",
|
|
532
|
+
)
|
|
533
|
+
correction_at_fixed_load = np.asarray(handle.solve(residual), dtype=float).reshape(-1)
|
|
534
|
+
correction_per_load = np.asarray(handle.solve(F_prop_red), dtype=float).reshape(-1)
|
|
535
|
+
except Exception:
|
|
536
|
+
step_failure = "singular_corrector_tangent"
|
|
537
|
+
break
|
|
538
|
+
if (
|
|
539
|
+
np.any(~np.isfinite(correction_at_fixed_load))
|
|
540
|
+
or np.any(~np.isfinite(correction_per_load))
|
|
541
|
+
):
|
|
542
|
+
step_failure = "nonfinite_corrector_solution"
|
|
543
|
+
break
|
|
544
|
+
|
|
545
|
+
denominator = 2.0 * (
|
|
546
|
+
_metric_dot(W, dq_total, correction_per_load)
|
|
547
|
+
+ load_scaling * load_scaling * dlambda_total
|
|
548
|
+
)
|
|
549
|
+
denominator_scale = max(
|
|
550
|
+
2.0 * radius * max(_metric_norm(W, correction_per_load), load_scaling),
|
|
551
|
+
1.0,
|
|
552
|
+
)
|
|
553
|
+
if abs(denominator) <= 1.0e-14 * denominator_scale:
|
|
554
|
+
step_failure = "singular_arc_constraint_linearization"
|
|
555
|
+
break
|
|
556
|
+
|
|
557
|
+
numerator = -arc_residual - 2.0 * _metric_dot(
|
|
558
|
+
W, dq_total, correction_at_fixed_load
|
|
559
|
+
)
|
|
560
|
+
dlambda_correction = numerator / denominator
|
|
561
|
+
dq_correction = correction_at_fixed_load + correction_per_load * dlambda_correction
|
|
562
|
+
if (
|
|
563
|
+
np.any(~np.isfinite(dq_correction))
|
|
564
|
+
or not np.isfinite(dlambda_correction)
|
|
565
|
+
):
|
|
566
|
+
step_failure = "nonfinite_arc_correction"
|
|
567
|
+
break
|
|
568
|
+
|
|
569
|
+
q_trial += dq_correction
|
|
570
|
+
lambda_trial += float(dlambda_correction)
|
|
571
|
+
dq_total = q_trial - q_base
|
|
572
|
+
dlambda_total = lambda_trial - lambda_base
|
|
573
|
+
else:
|
|
574
|
+
step_failure = "maximum_iterations_reached"
|
|
575
|
+
|
|
576
|
+
if accepted:
|
|
577
|
+
q = q_trial
|
|
578
|
+
lam = float(lambda_trial)
|
|
579
|
+
committed_states = trial_states
|
|
580
|
+
u = np.asarray(T @ q + u0, dtype=float).reshape(-1)
|
|
581
|
+
path_increment_norm = float(
|
|
582
|
+
np.sqrt(
|
|
583
|
+
max(_metric_dot(W, dq_total, dq_total), 0.0)
|
|
584
|
+
+ (load_scaling * dlambda_total) ** 2
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
is_new_peak = lam > peak_load_factor
|
|
588
|
+
if is_new_peak:
|
|
589
|
+
peak_load_factor = float(lam)
|
|
590
|
+
peak_step_index = step_index
|
|
591
|
+
descending_steps = 0
|
|
592
|
+
for old_step in steps:
|
|
593
|
+
old_step.is_peak = False
|
|
594
|
+
else:
|
|
595
|
+
required_drop = settings.peak_drop_tolerance * max(abs(peak_load_factor), 1.0)
|
|
596
|
+
if peak_step_index is not None and lam < peak_load_factor - required_drop:
|
|
597
|
+
descending_steps += 1
|
|
598
|
+
else:
|
|
599
|
+
descending_steps = 0
|
|
600
|
+
|
|
601
|
+
step = ArcLengthStep(
|
|
602
|
+
step_index=step_index,
|
|
603
|
+
load_factor=float(lam),
|
|
604
|
+
iterations=iteration,
|
|
605
|
+
retries=retry,
|
|
606
|
+
arc_radius=float(radius),
|
|
607
|
+
residual_norm=float(residual_norm),
|
|
608
|
+
arc_residual=float(arc_residual),
|
|
609
|
+
displacement_norm=float(np.linalg.norm(u)),
|
|
610
|
+
load_increment=float(dlambda_total),
|
|
611
|
+
path_increment_norm=path_increment_norm,
|
|
612
|
+
max_equivalent_plastic_strain=_max_plastic_strain(committed_states),
|
|
613
|
+
is_peak=is_new_peak,
|
|
614
|
+
)
|
|
615
|
+
steps.append(step)
|
|
616
|
+
previous_dq = dq_total.copy()
|
|
617
|
+
previous_dlambda = float(dlambda_total)
|
|
618
|
+
max_translation = _max_nodal_translation(working_model, u)
|
|
619
|
+
if progress_callback is not None:
|
|
620
|
+
try:
|
|
621
|
+
progress_callback(
|
|
622
|
+
{
|
|
623
|
+
"type": "nonlinear_static_step",
|
|
624
|
+
"control": "arc length",
|
|
625
|
+
"step_index": int(step_index),
|
|
626
|
+
"load_factor": float(lam),
|
|
627
|
+
"peak_load_factor": float(peak_load_factor),
|
|
628
|
+
"displacement_norm": float(np.linalg.norm(u)),
|
|
629
|
+
"max_translation": float(max_translation),
|
|
630
|
+
"iterations": int(iteration),
|
|
631
|
+
"max_equivalent_plastic_strain": float(step.max_equivalent_plastic_strain),
|
|
632
|
+
}
|
|
633
|
+
)
|
|
634
|
+
except Exception:
|
|
635
|
+
pass
|
|
636
|
+
|
|
637
|
+
old_radius = radius
|
|
638
|
+
if iteration <= max(settings.target_iterations // 2, 1):
|
|
639
|
+
radius = min(radius * settings.growth_factor, max_radius)
|
|
640
|
+
action = "grow"
|
|
641
|
+
elif iteration > settings.target_iterations:
|
|
642
|
+
radius = max(
|
|
643
|
+
radius * max(settings.cutback_factor, np.sqrt(settings.target_iterations / iteration)),
|
|
644
|
+
min_radius,
|
|
645
|
+
)
|
|
646
|
+
action = "shrink_after_slow_convergence"
|
|
647
|
+
else:
|
|
648
|
+
action = "keep"
|
|
649
|
+
adaptation_history.append(
|
|
650
|
+
{
|
|
651
|
+
"step_index": step_index,
|
|
652
|
+
"iterations": int(iteration),
|
|
653
|
+
"retries": int(retry),
|
|
654
|
+
"accepted_radius": float(old_radius),
|
|
655
|
+
"next_radius": float(radius),
|
|
656
|
+
"action": action,
|
|
657
|
+
}
|
|
658
|
+
)
|
|
659
|
+
break
|
|
660
|
+
|
|
661
|
+
radius *= settings.cutback_factor
|
|
662
|
+
adaptation_history.append(
|
|
663
|
+
{
|
|
664
|
+
"step_index": step_index,
|
|
665
|
+
"retry": int(retry),
|
|
666
|
+
"accepted": False,
|
|
667
|
+
"next_radius": float(radius),
|
|
668
|
+
"action": "cutback_after_nonconvergence",
|
|
669
|
+
"failure_reason": step_failure,
|
|
670
|
+
}
|
|
671
|
+
)
|
|
672
|
+
if radius < min_radius:
|
|
673
|
+
break
|
|
674
|
+
|
|
675
|
+
if not accepted:
|
|
676
|
+
q = q_base
|
|
677
|
+
lam = lambda_base
|
|
678
|
+
committed_states = states_base
|
|
679
|
+
status = "stopped_at_limit" if steps else "diverged"
|
|
680
|
+
failure_reason = step_failure if radius >= min_radius else "minimum_arc_radius_reached"
|
|
681
|
+
break
|
|
682
|
+
|
|
683
|
+
if (
|
|
684
|
+
settings.post_peak_load_fraction is not None
|
|
685
|
+
and peak_step_index is not None
|
|
686
|
+
and step_index > peak_step_index
|
|
687
|
+
and lam <= settings.post_peak_load_fraction * peak_load_factor
|
|
688
|
+
):
|
|
689
|
+
# Automatic post-buckling stop: the descending branch has shed
|
|
690
|
+
# the requested fraction of the peak load, so the post-buckling
|
|
691
|
+
# response is traced and further continuation adds no insight.
|
|
692
|
+
status = "post_buckling_traced"
|
|
693
|
+
break
|
|
694
|
+
if settings.max_translation is not None and max_translation > settings.max_translation:
|
|
695
|
+
status = "displacement_limit_reached"
|
|
696
|
+
break
|
|
697
|
+
if descending_steps >= settings.stop_after_peak_steps:
|
|
698
|
+
status = "peak_confirmed"
|
|
699
|
+
break
|
|
700
|
+
if (
|
|
701
|
+
settings.maximum_absolute_load_factor is not None
|
|
702
|
+
and abs(lam) >= settings.maximum_absolute_load_factor
|
|
703
|
+
):
|
|
704
|
+
status = "load_factor_limit_reached"
|
|
705
|
+
break
|
|
706
|
+
else:
|
|
707
|
+
status = "maximum_steps_reached"
|
|
708
|
+
|
|
709
|
+
u_final = np.asarray(T @ q + u0, dtype=float).reshape(-1)
|
|
710
|
+
info["failure_reason"] = failure_reason
|
|
711
|
+
info["last_converged_load_factor"] = float(lam)
|
|
712
|
+
info["peak_load_factor"] = float(peak_load_factor)
|
|
713
|
+
info["peak_step_index"] = peak_step_index
|
|
714
|
+
info["descending_steps_after_peak"] = int(descending_steps)
|
|
715
|
+
info["final_max_translation"] = float(max_translation)
|
|
716
|
+
info["load_scaling"] = float(load_scaling)
|
|
717
|
+
info["rotation_length_scale"] = float(rotation_scale)
|
|
718
|
+
info["initial_arc_radius"] = float(settings.initial_load_increment * predictor_norm)
|
|
719
|
+
info["minimum_arc_radius"] = float(min_radius)
|
|
720
|
+
info["maximum_arc_radius"] = float(max_radius)
|
|
721
|
+
info["adaptation_history"] = adaptation_history
|
|
722
|
+
info["strain_summary"] = _nonlinear_state_summary(committed_states)
|
|
723
|
+
info["preload"] = preload_info
|
|
724
|
+
info["total_newton_iterations"] = int(total_iterations)
|
|
725
|
+
info["total_retries"] = int(total_retries)
|
|
726
|
+
info["solve_time"] = float(time.time() - start_time)
|
|
727
|
+
info["result_case"] = make_result_case(
|
|
728
|
+
name="nonlinear_static_arc_length",
|
|
729
|
+
analysis_type="nonlinear_static",
|
|
730
|
+
load_cases=(load_case,) if constant_load_case is None else (constant_load_case, load_case),
|
|
731
|
+
assembly_info=assembly_info,
|
|
732
|
+
solver_info={"convergence_info": {"status": status, "failure_reason": failure_reason}},
|
|
733
|
+
recovery={
|
|
734
|
+
"displacements": True,
|
|
735
|
+
"element_states": True,
|
|
736
|
+
"force_displacement_history": True,
|
|
737
|
+
"arc_length_history": True,
|
|
738
|
+
},
|
|
739
|
+
settings={
|
|
740
|
+
"control": "arc_length",
|
|
741
|
+
"arc_length": settings.to_dict(),
|
|
742
|
+
"max_iterations": int(max_iterations),
|
|
743
|
+
"tolerance": float(tolerance),
|
|
744
|
+
"arc_tolerance": float(arc_tolerance),
|
|
745
|
+
"num_layers": int(num_layers),
|
|
746
|
+
},
|
|
747
|
+
).to_dict()
|
|
748
|
+
|
|
749
|
+
return ArcLengthResult(
|
|
750
|
+
steps=steps,
|
|
751
|
+
status=status,
|
|
752
|
+
displacements=u_final,
|
|
753
|
+
load_factor=float(lam),
|
|
754
|
+
peak_load_factor=float(peak_load_factor),
|
|
755
|
+
peak_step_index=peak_step_index,
|
|
756
|
+
element_states=committed_states,
|
|
757
|
+
info=info,
|
|
758
|
+
)
|