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,371 @@
|
|
|
1
|
+
"""Element-independent corotational kinematics for large rigid rotations.
|
|
2
|
+
|
|
3
|
+
This module implements the corotational (CR) formulation for the nonlinear
|
|
4
|
+
static solver. Each element's rigid-body rotation is extracted from the
|
|
5
|
+
current deformed geometry, the nodal displacements are pulled back to the
|
|
6
|
+
reference configuration (removing the rigid motion), the element's own
|
|
7
|
+
nonlinear local response (layered J2 shells, fiber beams, local von Karman
|
|
8
|
+
coupling) acts on the small deformational part, and the resulting forces are
|
|
9
|
+
rotated forward to the current configuration:
|
|
10
|
+
|
|
11
|
+
u_d,i (translation) = R_rig^T (x_i - x_c) - (X_i - X_c)
|
|
12
|
+
u_d,i (rotation) = rotvec(R_rig^T exp(skew(theta_i)))
|
|
13
|
+
f_global = E f_local(u_d), E = blockdiag(R_rig, R_rig, ...)
|
|
14
|
+
K_tangent = E k_local E^T for shells (empirically stable), plus
|
|
15
|
+
frame-sensitivity geometric terms for beams
|
|
16
|
+
|
|
17
|
+
Scope and validity:
|
|
18
|
+
|
|
19
|
+
- small strains and small *deformational* rotations per element; rigid
|
|
20
|
+
rotations of any magnitude (verified by rigid-rotation invariance);
|
|
21
|
+
- shells extract the rigid rotation from the element midsurface frame at the
|
|
22
|
+
element center; beams from axis alignment plus mean axial twist;
|
|
23
|
+
- the deformational displacements are routed through the elements' own
|
|
24
|
+
nonlinear local responses, so layered shell J2 plasticity, beam fiber
|
|
25
|
+
plasticity and the local von Karman coupling are active in the corotated
|
|
26
|
+
frame (plastic state is objective under rigid rotation); fracture/erosion
|
|
27
|
+
remains unsupported in corotational mode;
|
|
28
|
+
- the tangent omits the rotational geometric stiffness, so Newton convergence
|
|
29
|
+
is linear rather than quadratic near strongly rotating states — use more,
|
|
30
|
+
smaller increments;
|
|
31
|
+
- the pull-back subtracts order-one nodal coordinates, so the internal force
|
|
32
|
+
carries an intrinsic roundoff floor of roughly ``eps * ||K_e|| * L`` per
|
|
33
|
+
element (~1e-7 N for steel at metre scale). Use residual tolerances of
|
|
34
|
+
1e-5..1e-6 relative and realistic load magnitudes; demanding convergence
|
|
35
|
+
below the floor stalls the increment adaptation;
|
|
36
|
+
- eccentric beam-shell couplings use linear MPC constraints whose eccentricity
|
|
37
|
+
vectors do not rotate; coupled regions should not undergo large rotations.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from .fe_core import FEModel
|
|
48
|
+
|
|
49
|
+
_SMALL = 1.0e-12
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _skew(vector: np.ndarray) -> np.ndarray:
|
|
53
|
+
x, y, z = float(vector[0]), float(vector[1]), float(vector[2])
|
|
54
|
+
return np.array([[0.0, -z, y], [z, 0.0, -x], [-y, x, 0.0]], dtype=float)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def rotation_matrix_from_vector(vector: np.ndarray) -> np.ndarray:
|
|
58
|
+
"""Rodrigues exponential map: rotation vector -> rotation matrix."""
|
|
59
|
+
vector = np.asarray(vector, dtype=float).reshape(3)
|
|
60
|
+
angle = float(np.linalg.norm(vector))
|
|
61
|
+
if angle < _SMALL:
|
|
62
|
+
return np.eye(3) + _skew(vector)
|
|
63
|
+
axis = vector / angle
|
|
64
|
+
K = _skew(axis)
|
|
65
|
+
return np.eye(3) + np.sin(angle) * K + (1.0 - np.cos(angle)) * (K @ K)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def rotation_vector_from_matrix(matrix: np.ndarray) -> np.ndarray:
|
|
69
|
+
"""Logarithmic map: rotation matrix -> rotation vector (robust near 0 and pi)."""
|
|
70
|
+
R = np.asarray(matrix, dtype=float).reshape(3, 3)
|
|
71
|
+
trace = float(np.trace(R))
|
|
72
|
+
cos_angle = min(max(0.5 * (trace - 1.0), -1.0), 1.0)
|
|
73
|
+
angle = float(np.arccos(cos_angle))
|
|
74
|
+
if angle < 1.0e-8:
|
|
75
|
+
return np.array([R[2, 1] - R[1, 2], R[0, 2] - R[2, 0], R[1, 0] - R[0, 1]], dtype=float) * 0.5
|
|
76
|
+
if angle > np.pi - 1.0e-6:
|
|
77
|
+
# near pi: extract axis from the symmetric part
|
|
78
|
+
A = 0.5 * (R + np.eye(3))
|
|
79
|
+
axis = np.sqrt(np.maximum(np.diag(A), 0.0))
|
|
80
|
+
# fix signs from off-diagonal terms using the largest component
|
|
81
|
+
k = int(np.argmax(axis))
|
|
82
|
+
if axis[k] > _SMALL:
|
|
83
|
+
for j in range(3):
|
|
84
|
+
if j != k:
|
|
85
|
+
axis[j] = A[j, k] / axis[k]
|
|
86
|
+
norm = float(np.linalg.norm(axis))
|
|
87
|
+
axis = axis / norm if norm > _SMALL else np.array([1.0, 0.0, 0.0])
|
|
88
|
+
return angle * axis
|
|
89
|
+
factor = angle / (2.0 * np.sin(angle))
|
|
90
|
+
return factor * np.array([R[2, 1] - R[1, 2], R[0, 2] - R[2, 0], R[1, 0] - R[0, 1]], dtype=float)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _minimal_rotation(from_direction: np.ndarray, to_direction: np.ndarray) -> np.ndarray:
|
|
94
|
+
"""Smallest rotation mapping one unit vector onto another."""
|
|
95
|
+
a = np.asarray(from_direction, dtype=float).reshape(3)
|
|
96
|
+
b = np.asarray(to_direction, dtype=float).reshape(3)
|
|
97
|
+
cross = np.cross(a, b)
|
|
98
|
+
dot = float(a @ b)
|
|
99
|
+
if dot < -1.0 + 1.0e-12:
|
|
100
|
+
# antiparallel: rotate pi about any axis orthogonal to a
|
|
101
|
+
seed = np.array([1.0, 0.0, 0.0]) if abs(a[0]) < 0.9 else np.array([0.0, 1.0, 0.0])
|
|
102
|
+
axis = np.cross(a, seed)
|
|
103
|
+
axis = axis / max(float(np.linalg.norm(axis)), _SMALL)
|
|
104
|
+
return rotation_matrix_from_vector(np.pi * axis)
|
|
105
|
+
K = _skew(cross)
|
|
106
|
+
return np.eye(3) + K + (K @ K) / (1.0 + dot)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _shell_center_frame(element: Any, coords: np.ndarray) -> np.ndarray:
|
|
110
|
+
"""Element midsurface frame (columns = local axes) at the element center."""
|
|
111
|
+
if element.num_nodes in (3, 6):
|
|
112
|
+
xi, eta = 1.0 / 3.0, 1.0 / 3.0
|
|
113
|
+
else:
|
|
114
|
+
xi, eta = 0.0, 0.0
|
|
115
|
+
_N, dN_dxi, dN_deta = element.compute_shape_functions(xi, eta)
|
|
116
|
+
R, _dN_dx, _dN_dy, _det_j = element._local_frame_and_derivatives(coords, dN_dxi, dN_deta)
|
|
117
|
+
return np.asarray(R, dtype=float)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _beam_rigid_rotation(reference: np.ndarray, deformed: np.ndarray, node_rotations: np.ndarray) -> np.ndarray:
|
|
121
|
+
"""Beam rigid rotation: axis alignment composed with mean axial twist."""
|
|
122
|
+
t_ref = reference[-1] - reference[0]
|
|
123
|
+
t_def = deformed[-1] - deformed[0]
|
|
124
|
+
t_ref = t_ref / max(float(np.linalg.norm(t_ref)), _SMALL)
|
|
125
|
+
t_def = t_def / max(float(np.linalg.norm(t_def)), _SMALL)
|
|
126
|
+
R_align = _minimal_rotation(t_ref, t_def)
|
|
127
|
+
twists = []
|
|
128
|
+
for theta in node_rotations:
|
|
129
|
+
R_node = rotation_matrix_from_vector(theta)
|
|
130
|
+
residual = rotation_vector_from_matrix(R_align.T @ R_node)
|
|
131
|
+
twists.append(float(residual @ t_ref))
|
|
132
|
+
mean_twist = float(np.mean(twists)) if twists else 0.0
|
|
133
|
+
return R_align @ rotation_matrix_from_vector(mean_twist * t_ref)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _element_category(element: Any) -> Optional[str]:
|
|
137
|
+
from .elements import BeamElement, ShellElement
|
|
138
|
+
|
|
139
|
+
if isinstance(element, ShellElement):
|
|
140
|
+
return "shell"
|
|
141
|
+
if isinstance(element, BeamElement):
|
|
142
|
+
return "beam"
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _CorotationalReference:
|
|
147
|
+
"""Reference geometry, frame and linear stiffness per element."""
|
|
148
|
+
|
|
149
|
+
__slots__ = ("stiffness", "coordinates", "centroid", "frame", "category")
|
|
150
|
+
|
|
151
|
+
def __init__(self, element: Any, model: "FEModel"):
|
|
152
|
+
material = model.get_material(element.material_name)
|
|
153
|
+
self.stiffness = np.asarray(element.compute_stiffness_matrix(model.mesh, material), dtype=float).copy()
|
|
154
|
+
self.coordinates = np.asarray(element.get_node_coordinates(model.mesh), dtype=float).copy()
|
|
155
|
+
self.centroid = self.coordinates.mean(axis=0)
|
|
156
|
+
self.category = _element_category(element)
|
|
157
|
+
if self.category == "shell":
|
|
158
|
+
self.frame = _shell_center_frame(element, self.coordinates)
|
|
159
|
+
else:
|
|
160
|
+
self.frame = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _corotational_cache(model: "FEModel") -> Dict[int, _CorotationalReference]:
|
|
164
|
+
mesh = model.mesh
|
|
165
|
+
signature = mesh.revision_signature()
|
|
166
|
+
cached = getattr(mesh, "_corotational_cache", None)
|
|
167
|
+
if cached is not None and cached[0] == signature:
|
|
168
|
+
return cached[1]
|
|
169
|
+
cache: Dict[int, _CorotationalReference] = {}
|
|
170
|
+
for element_id, element in mesh.elements.items():
|
|
171
|
+
if _element_category(element) is not None:
|
|
172
|
+
cache[int(element_id)] = _CorotationalReference(element, model)
|
|
173
|
+
mesh._corotational_cache = (signature, cache)
|
|
174
|
+
return cache
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def corotational_element_response(
|
|
178
|
+
model: "FEModel",
|
|
179
|
+
element_id: int,
|
|
180
|
+
element: Any,
|
|
181
|
+
u_element: np.ndarray,
|
|
182
|
+
tangent: bool,
|
|
183
|
+
committed_state: Optional[Any] = None,
|
|
184
|
+
num_layers: int = 5,
|
|
185
|
+
) -> Tuple[Optional[np.ndarray], Optional[np.ndarray], Optional[Any]]:
|
|
186
|
+
"""Corotational internal force, tangent and trial state for one element.
|
|
187
|
+
|
|
188
|
+
The deformational displacement ``u_d`` is a small-deformation field on the
|
|
189
|
+
reference geometry, so it is routed through the element's own nonlinear
|
|
190
|
+
local response — layered J2 plasticity for shells and fiber sections for
|
|
191
|
+
beams — and the resulting force/tangent are rotated forward with the rigid
|
|
192
|
+
frame. Plastic state therefore lives in the corotated frame and is
|
|
193
|
+
objective under arbitrary rigid rotation.
|
|
194
|
+
|
|
195
|
+
Returns ``(None, None, None)`` for element types outside the corotational
|
|
196
|
+
scope so the caller can fall back to the element's own response.
|
|
197
|
+
"""
|
|
198
|
+
reference = _corotational_cache(model).get(int(element_id))
|
|
199
|
+
if reference is None or reference.category is None:
|
|
200
|
+
return None, None, None
|
|
201
|
+
num_nodes = int(element.num_nodes)
|
|
202
|
+
u = np.asarray(u_element, dtype=float).reshape(num_nodes, 6)
|
|
203
|
+
translations = u[:, :3]
|
|
204
|
+
rotations = u[:, 3:]
|
|
205
|
+
deformed = reference.coordinates + translations
|
|
206
|
+
|
|
207
|
+
if reference.category == "shell":
|
|
208
|
+
R_ref = reference.frame
|
|
209
|
+
R_def = None
|
|
210
|
+
if reference.category == "shell":
|
|
211
|
+
R_def = _shell_center_frame(element, deformed)
|
|
212
|
+
R_rig = R_def @ reference.frame.T
|
|
213
|
+
else:
|
|
214
|
+
R_rig = _beam_rigid_rotation(reference.coordinates, deformed, rotations)
|
|
215
|
+
|
|
216
|
+
centroid_def = deformed.mean(axis=0)
|
|
217
|
+
u_d = np.zeros((num_nodes, 6), dtype=float)
|
|
218
|
+
for node in range(num_nodes):
|
|
219
|
+
u_d[node, :3] = R_rig.T @ (deformed[node] - centroid_def) - (reference.coordinates[node] - reference.centroid)
|
|
220
|
+
R_node = rotation_matrix_from_vector(rotations[node])
|
|
221
|
+
u_d[node, 3:] = rotation_vector_from_matrix(R_rig.T @ R_node)
|
|
222
|
+
|
|
223
|
+
material = model.get_material(element.material_name)
|
|
224
|
+
f_ref, k_ref, trial_state = element.compute_nonlinear_response(
|
|
225
|
+
model.mesh, material, u_d.reshape(-1), committed_state, int(num_layers), tangent
|
|
226
|
+
)
|
|
227
|
+
f_ref = np.asarray(f_ref, dtype=float).reshape(-1)
|
|
228
|
+
E = np.zeros((num_nodes * 6, num_nodes * 6), dtype=float)
|
|
229
|
+
for node in range(num_nodes):
|
|
230
|
+
E[node * 6 : node * 6 + 3, node * 6 : node * 6 + 3] = R_rig
|
|
231
|
+
E[node * 6 + 3 : node * 6 + 6, node * 6 + 3 : node * 6 + 6] = R_rig
|
|
232
|
+
f_global = E @ f_ref
|
|
233
|
+
if not tangent:
|
|
234
|
+
return f_global, None, trial_state
|
|
235
|
+
k_ref = np.asarray(k_ref, dtype=float)
|
|
236
|
+
# The rotated local tangent is the empirically stable choice for both
|
|
237
|
+
# element families: adding the frame-sensitivity geometric terms (see
|
|
238
|
+
# _consistent_corotational_tangent, kept for future nonsymmetric-tangent
|
|
239
|
+
# work) makes the symmetrized Newton map repulsive near equilibrium for
|
|
240
|
+
# bending-dominated shells and for plastically softened beams, while
|
|
241
|
+
# E k E^T converges in a handful of iterations without line search.
|
|
242
|
+
k_global = E @ k_ref @ E.T
|
|
243
|
+
return f_global, k_global, trial_state
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _rotation_right_jacobian(vector: np.ndarray) -> np.ndarray:
|
|
247
|
+
"""Right Jacobian of the exponential map: d exp(theta + d) ~ exp(theta) skew(Jr d)."""
|
|
248
|
+
vector = np.asarray(vector, dtype=float).reshape(3)
|
|
249
|
+
angle = float(np.linalg.norm(vector))
|
|
250
|
+
K = _skew(vector)
|
|
251
|
+
if angle < 1.0e-6:
|
|
252
|
+
return np.eye(3) - 0.5 * K + (K @ K) / 6.0
|
|
253
|
+
return (
|
|
254
|
+
np.eye(3)
|
|
255
|
+
- (1.0 - np.cos(angle)) / angle**2 * K
|
|
256
|
+
+ (angle - np.sin(angle)) / angle**3 * (K @ K)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _rigid_rotation_for_state(
|
|
261
|
+
reference: "_CorotationalReference", element: Any, deformed: np.ndarray, rotations: np.ndarray
|
|
262
|
+
) -> np.ndarray:
|
|
263
|
+
if reference.category == "shell":
|
|
264
|
+
return _shell_center_frame(element, deformed) @ reference.frame.T
|
|
265
|
+
return _beam_rigid_rotation(reference.coordinates, deformed, rotations)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _rotation_sensitivity(
|
|
269
|
+
reference: "_CorotationalReference",
|
|
270
|
+
element: Any,
|
|
271
|
+
deformed: np.ndarray,
|
|
272
|
+
rotations: np.ndarray,
|
|
273
|
+
R_rig: np.ndarray,
|
|
274
|
+
) -> np.ndarray:
|
|
275
|
+
"""G = d(omega)/du (3 x 6n): rigid-rotation sensitivity by central differences.
|
|
276
|
+
|
|
277
|
+
``omega`` is the left-increment rotation vector of the extracted rigid
|
|
278
|
+
rotation, ``R_rig(u + du) ~ exp(skew(G du)) R_rig(u)``. Shell frames
|
|
279
|
+
depend only on the nodal translations; beam frames additionally pick up
|
|
280
|
+
twist from the nodal rotations.
|
|
281
|
+
"""
|
|
282
|
+
num_nodes = deformed.shape[0]
|
|
283
|
+
G = np.zeros((3, num_nodes * 6), dtype=float)
|
|
284
|
+
spread = float(np.max(np.linalg.norm(reference.coordinates - reference.centroid, axis=1)))
|
|
285
|
+
step_translation = 1.0e-6 * max(spread, 1.0e-3)
|
|
286
|
+
step_rotation = 1.0e-6
|
|
287
|
+
include_rotations = reference.category == "beam"
|
|
288
|
+
Rt = R_rig.T
|
|
289
|
+
for node in range(num_nodes):
|
|
290
|
+
for axis in range(3):
|
|
291
|
+
perturbed = deformed.copy()
|
|
292
|
+
perturbed[node, axis] += step_translation
|
|
293
|
+
R_plus = _rigid_rotation_for_state(reference, element, perturbed, rotations)
|
|
294
|
+
perturbed[node, axis] -= 2.0 * step_translation
|
|
295
|
+
R_minus = _rigid_rotation_for_state(reference, element, perturbed, rotations)
|
|
296
|
+
delta = rotation_vector_from_matrix(R_plus @ Rt) - rotation_vector_from_matrix(R_minus @ Rt)
|
|
297
|
+
G[:, node * 6 + axis] = delta / (2.0 * step_translation)
|
|
298
|
+
if include_rotations:
|
|
299
|
+
for axis in range(3):
|
|
300
|
+
perturbed_rotations = rotations.copy()
|
|
301
|
+
perturbed_rotations[node, axis] += step_rotation
|
|
302
|
+
R_plus = _rigid_rotation_for_state(reference, element, deformed, perturbed_rotations)
|
|
303
|
+
perturbed_rotations[node, axis] -= 2.0 * step_rotation
|
|
304
|
+
R_minus = _rigid_rotation_for_state(reference, element, deformed, perturbed_rotations)
|
|
305
|
+
delta = rotation_vector_from_matrix(R_plus @ Rt) - rotation_vector_from_matrix(R_minus @ Rt)
|
|
306
|
+
G[:, node * 6 + 3 + axis] = delta / (2.0 * step_rotation)
|
|
307
|
+
return G
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _consistent_corotational_tangent(
|
|
311
|
+
reference: "_CorotationalReference",
|
|
312
|
+
element: Any,
|
|
313
|
+
deformed: np.ndarray,
|
|
314
|
+
rotations: np.ndarray,
|
|
315
|
+
R_rig: np.ndarray,
|
|
316
|
+
E: np.ndarray,
|
|
317
|
+
f_global: np.ndarray,
|
|
318
|
+
k_ref: np.ndarray,
|
|
319
|
+
) -> np.ndarray:
|
|
320
|
+
"""Consistent (symmetrized) corotational tangent.
|
|
321
|
+
|
|
322
|
+
Chain rule of ``f = E(omega) K_ref u_d(u, omega)`` through the three
|
|
323
|
+
dependency paths:
|
|
324
|
+
|
|
325
|
+
- ``D`` — pull-back derivative at fixed frame: centered translations and
|
|
326
|
+
right-Jacobian-corrected nodal rotation increments;
|
|
327
|
+
- ``U`` — pull-back sensitivity to the rigid rotation;
|
|
328
|
+
- ``S`` — rotation of the internal force with the frame.
|
|
329
|
+
|
|
330
|
+
``K = E K_ref D + (S + E K_ref U) G`` with ``G`` the frame sensitivity
|
|
331
|
+
from :func:`_rotation_sensitivity`. The result is symmetrized for the
|
|
332
|
+
solver's symmetric factorization; the skew part vanishes at equilibrium.
|
|
333
|
+
"""
|
|
334
|
+
num_nodes = deformed.shape[0]
|
|
335
|
+
n_dofs = num_nodes * 6
|
|
336
|
+
Rt = R_rig.T
|
|
337
|
+
centroid_def = deformed.mean(axis=0)
|
|
338
|
+
|
|
339
|
+
D = np.zeros((n_dofs, n_dofs), dtype=float)
|
|
340
|
+
average = 1.0 / num_nodes
|
|
341
|
+
for i in range(num_nodes):
|
|
342
|
+
for j in range(num_nodes):
|
|
343
|
+
weight = (1.0 if i == j else 0.0) - average
|
|
344
|
+
D[i * 6 : i * 6 + 3, j * 6 : j * 6 + 3] = weight * Rt
|
|
345
|
+
D[i * 6 + 3 : i * 6 + 6, i * 6 + 3 : i * 6 + 6] = Rt @ _rotation_right_jacobian(rotations[i])
|
|
346
|
+
|
|
347
|
+
U = np.zeros((n_dofs, 3), dtype=float)
|
|
348
|
+
for i in range(num_nodes):
|
|
349
|
+
U[i * 6 : i * 6 + 3, :] = Rt @ _skew(deformed[i] - centroid_def)
|
|
350
|
+
U[i * 6 + 3 : i * 6 + 6, :] = -Rt
|
|
351
|
+
|
|
352
|
+
S = np.zeros((n_dofs, 3), dtype=float)
|
|
353
|
+
for i in range(num_nodes):
|
|
354
|
+
S[i * 6 : i * 6 + 3, :] = -_skew(f_global[i * 6 : i * 6 + 3])
|
|
355
|
+
S[i * 6 + 3 : i * 6 + 6, :] = -_skew(f_global[i * 6 + 3 : i * 6 + 6])
|
|
356
|
+
|
|
357
|
+
G = _rotation_sensitivity(reference, element, deformed, rotations, R_rig)
|
|
358
|
+
EK = E @ k_ref
|
|
359
|
+
tangent = EK @ D + (S + EK @ U) @ G
|
|
360
|
+
return 0.5 * (tangent + tangent.T)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def validate_corotational_scope(model: "FEModel") -> None:
|
|
364
|
+
"""Validate corotational applicability.
|
|
365
|
+
|
|
366
|
+
Since Phase 4 the corotational path routes the deformational displacements
|
|
367
|
+
through the elements' own nonlinear local responses, so layered shell J2
|
|
368
|
+
plasticity and beam fiber plasticity are supported. The function is kept
|
|
369
|
+
as the hook for future scope checks.
|
|
370
|
+
"""
|
|
371
|
+
return None
|