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.
Files changed (70) hide show
  1. anysolver/__init__.py +631 -0
  2. anysolver/anystructure_fem_mode.py +942 -0
  3. anysolver/arc_length.py +758 -0
  4. anysolver/assembly.py +950 -0
  5. anysolver/baselines.py +303 -0
  6. anysolver/beam_shell_verification.py +4276 -0
  7. anysolver/beam_validity.py +110 -0
  8. anysolver/benchmarks.py +495 -0
  9. anysolver/boundary.py +476 -0
  10. anysolver/buckling.py +442 -0
  11. anysolver/buckling_validity.py +91 -0
  12. anysolver/capacity_workflow.py +350 -0
  13. anysolver/cases.py +173 -0
  14. anysolver/composite_strip_verification.py +297 -0
  15. anysolver/contact.py +3461 -0
  16. anysolver/corotational.py +371 -0
  17. anysolver/cylinder_benchmarks.py +364 -0
  18. anysolver/dynamics.py +723 -0
  19. anysolver/element_qualification.py +432 -0
  20. anysolver/elements.py +2724 -0
  21. anysolver/external_references.py +369 -0
  22. anysolver/fe_core.py +327 -0
  23. anysolver/fracture.py +551 -0
  24. anysolver/imperfections.py +390 -0
  25. anysolver/jit_compiler.py +108 -0
  26. anysolver/kernel_warmup.py +180 -0
  27. anysolver/linalg.py +760 -0
  28. anysolver/mass_properties.py +179 -0
  29. anysolver/material_curves.py +231 -0
  30. anysolver/matrix_assembly.py +558 -0
  31. anysolver/mesh_gen.py +1065 -0
  32. anysolver/mesh_load_bc_verification.py +609 -0
  33. anysolver/modal.py +282 -0
  34. anysolver/nonlinear.py +300 -0
  35. anysolver/nonlinear_performance.py +920 -0
  36. anysolver/nonlinear_performance_batch_b.py +592 -0
  37. anysolver/nonlinear_performance_batch_c.py +506 -0
  38. anysolver/nonlinear_performance_bootstrap.py +120 -0
  39. anysolver/nonlinear_reduced_assembly.py +760 -0
  40. anysolver/nonlinear_static.py +1518 -0
  41. anysolver/plasticity.py +419 -0
  42. anysolver/plasticity_qualification.py +314 -0
  43. anysolver/production_readiness.py +304 -0
  44. anysolver/quality_control.py +1497 -0
  45. anysolver/recovery.py +505 -0
  46. anysolver/recovery_policy.py +205 -0
  47. anysolver/reference_cases.py +425 -0
  48. anysolver/results.py +503 -0
  49. anysolver/runtime.py +9030 -0
  50. anysolver/s4_validity.py +326 -0
  51. anysolver/sesam_fem/__init__.py +55 -0
  52. anysolver/sesam_fem/__main__.py +80 -0
  53. anysolver/sesam_fem/diagnostics.py +65 -0
  54. anysolver/sesam_fem/document.py +814 -0
  55. anysolver/sesam_fem/exporter.py +84 -0
  56. anysolver/sesam_fem/importer.py +365 -0
  57. anysolver/sesam_fem/records.py +257 -0
  58. anysolver/sesam_fem/schema.py +107 -0
  59. anysolver/sesam_fem/sif_importer.py +397 -0
  60. anysolver/sesam_fem/validation.py +62 -0
  61. anysolver/shell_benchmarks.py +367 -0
  62. anysolver/test_cases.py +610 -0
  63. anysolver/validation.py +416 -0
  64. anysolver/vectorized_nonlinear.py +334 -0
  65. anysolver/vectorized_stiffness.py +571 -0
  66. anysolver-0.1.0.dist-info/METADATA +165 -0
  67. anysolver-0.1.0.dist-info/RECORD +70 -0
  68. anysolver-0.1.0.dist-info/WHEEL +5 -0
  69. anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
  70. 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