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,571 @@
1
+ """Vectorized JIT-compiled shell stiffness matrix formulation.
2
+
3
+ This module provides JIT-compiled routines to compute the global stiffness matrices
4
+ of a batch of ShellElements of the same configuration.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Tuple
10
+ import numpy as np
11
+ from .jit_compiler import njit, prange
12
+
13
+ _SMALL = 1.0e-12
14
+
15
+ @njit(cache=True)
16
+ def _cross3(a: np.ndarray, b: np.ndarray) -> np.ndarray:
17
+ return np.array([
18
+ a[1]*b[2] - a[2]*b[1],
19
+ a[2]*b[0] - a[0]*b[2],
20
+ a[0]*b[1] - a[1]*b[0]
21
+ ])
22
+
23
+ @njit(cache=True)
24
+ def _normalize(vector: np.ndarray) -> Tuple[np.ndarray, float]:
25
+ norm = float(np.sqrt(np.dot(vector, vector)))
26
+ if norm < _SMALL:
27
+ return np.zeros(3, dtype=float), norm
28
+ return vector / norm, norm
29
+
30
+ @njit(cache=True)
31
+ def _inv2(matrix: np.ndarray) -> Tuple[np.ndarray, float]:
32
+ det = matrix[0, 0] * matrix[1, 1] - matrix[0, 1] * matrix[1, 0]
33
+ if abs(det) < _SMALL:
34
+ raise ValueError("singular 2x2 matrix")
35
+ inv = np.array([
36
+ [matrix[1, 1], -matrix[0, 1]],
37
+ [-matrix[1, 0], matrix[0, 0]]
38
+ ]) / det
39
+ return inv, float(det)
40
+
41
+ @njit(cache=True)
42
+ def _compute_4node_shape_functions(xi: float, eta: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
43
+ N = np.array([
44
+ 0.25 * (1.0 - xi) * (1.0 - eta),
45
+ 0.25 * (1.0 + xi) * (1.0 - eta),
46
+ 0.25 * (1.0 + xi) * (1.0 + eta),
47
+ 0.25 * (1.0 - xi) * (1.0 + eta)
48
+ ])
49
+ dN_dxi = np.array([
50
+ -0.25 * (1.0 - eta),
51
+ 0.25 * (1.0 - eta),
52
+ 0.25 * (1.0 + eta),
53
+ -0.25 * (1.0 + eta)
54
+ ])
55
+ dN_deta = np.array([
56
+ -0.25 * (1.0 - xi),
57
+ -0.25 * (1.0 + xi),
58
+ 0.25 * (1.0 + xi),
59
+ 0.25 * (1.0 - xi)
60
+ ])
61
+ return N, dN_dxi, dN_deta
62
+
63
+ @njit(cache=True)
64
+ def _compute_8node_shape_functions(xi: float, eta: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
65
+ N = np.zeros(8)
66
+ N[0] = -0.25 * (1.0 - xi) * (1.0 - eta) * (1.0 + xi + eta)
67
+ N[1] = -0.25 * (1.0 + xi) * (1.0 - eta) * (1.0 - xi + eta)
68
+ N[2] = -0.25 * (1.0 + xi) * (1.0 + eta) * (1.0 - xi - eta)
69
+ N[3] = -0.25 * (1.0 - xi) * (1.0 + eta) * (1.0 + xi - eta)
70
+ N[4] = 0.5 * (1.0 - xi**2) * (1.0 - eta)
71
+ N[5] = 0.5 * (1.0 + xi) * (1.0 - eta**2)
72
+ N[6] = 0.5 * (1.0 - xi**2) * (1.0 + eta)
73
+ N[7] = 0.5 * (1.0 - xi) * (1.0 - eta**2)
74
+
75
+ dN_dxi = np.zeros(8)
76
+ dN_dxi[0] = 0.25 * (1.0 - eta) * (1.0 + xi + eta) - 0.25 * (1.0 - xi) * (1.0 - eta)
77
+ dN_dxi[1] = -0.25 * (1.0 - eta) * (1.0 - xi + eta) + 0.25 * (1.0 + xi) * (1.0 - eta)
78
+ dN_dxi[2] = -0.25 * (1.0 + eta) * (1.0 - xi - eta) + 0.25 * (1.0 + xi) * (1.0 + eta)
79
+ dN_dxi[3] = 0.25 * (1.0 + eta) * (1.0 + xi - eta) - 0.25 * (1.0 - xi) * (1.0 + eta)
80
+ dN_dxi[4] = -xi * (1.0 - eta)
81
+ dN_dxi[5] = 0.5 * (1.0 - eta**2)
82
+ dN_dxi[6] = -xi * (1.0 + eta)
83
+ dN_dxi[7] = -0.5 * (1.0 - eta**2)
84
+
85
+ dN_deta = np.zeros(8)
86
+ dN_deta[0] = 0.25 * (1.0 - xi) * (1.0 + xi + eta) - 0.25 * (1.0 - xi) * (1.0 - eta)
87
+ dN_deta[1] = 0.25 * (1.0 + xi) * (1.0 - xi + eta) - 0.25 * (1.0 + xi) * (1.0 - eta)
88
+ dN_deta[2] = -0.25 * (1.0 + xi) * (1.0 - xi - eta) + 0.25 * (1.0 + xi) * (1.0 + eta)
89
+ dN_deta[3] = -0.25 * (1.0 - xi) * (1.0 + xi - eta) + 0.25 * (1.0 - xi) * (1.0 + eta)
90
+ dN_deta[4] = -0.5 * (1.0 - xi**2)
91
+ dN_deta[5] = -eta * (1.0 + xi)
92
+ dN_deta[6] = 0.5 * (1.0 - xi**2)
93
+ dN_deta[7] = -eta * (1.0 - xi)
94
+ return N, dN_dxi, dN_deta
95
+
96
+ @njit(cache=True)
97
+ def _fallback_edge_direction_jit(coords: np.ndarray, normal: np.ndarray) -> np.ndarray:
98
+ num_nodes = coords.shape[0]
99
+ if num_nodes >= 2:
100
+ edge = coords[1] - coords[0]
101
+ proj = edge - np.dot(edge, normal) * normal
102
+ e1, n = _normalize(proj)
103
+ if n > _SMALL:
104
+ return e1
105
+ if num_nodes >= 4:
106
+ edge = coords[2] - coords[3]
107
+ proj = edge - np.dot(edge, normal) * normal
108
+ e1, n = _normalize(proj)
109
+ if n > _SMALL:
110
+ return e1
111
+ edge = coords[3] - coords[0]
112
+ proj = edge - np.dot(edge, normal) * normal
113
+ e1, n = _normalize(proj)
114
+ if n > _SMALL:
115
+ return e1
116
+ edge = coords[2] - coords[1]
117
+ proj = edge - np.dot(edge, normal) * normal
118
+ e1, n = _normalize(proj)
119
+ if n > _SMALL:
120
+ return e1
121
+ raise ValueError("Shell element has no valid in-plane direction")
122
+
123
+ @njit(cache=True)
124
+ def _local_frame_and_derivatives_jit(
125
+ coords: np.ndarray,
126
+ dN_dxi: np.ndarray,
127
+ dN_deta: np.ndarray,
128
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float]:
129
+ J0 = np.zeros(3)
130
+ J1 = np.zeros(3)
131
+ for i in range(coords.shape[0]):
132
+ J0 += coords[i] * dN_dxi[i]
133
+ J1 += coords[i] * dN_deta[i]
134
+
135
+ e3, det_j = _normalize(_cross3(J0, J1))
136
+ if det_j < _SMALL:
137
+ raise ValueError("Shell element has a near-zero surface Jacobian")
138
+
139
+ e1_raw = J0 - np.dot(J0, e3) * e3
140
+ e1, e1_norm = _normalize(e1_raw)
141
+ if e1_norm < _SMALL:
142
+ e1 = _fallback_edge_direction_jit(coords, e3)
143
+
144
+ e2, e2_norm = _normalize(_cross3(e3, e1))
145
+ if e2_norm < _SMALL:
146
+ raise ValueError("Shell element has an invalid local y direction")
147
+
148
+ e1, _ = _normalize(_cross3(e2, e3))
149
+ R = np.zeros((3, 3))
150
+ R[:, 0] = e1
151
+ R[:, 1] = e2
152
+ R[:, 2] = e3
153
+
154
+ J_local = np.array([
155
+ [np.dot(J0, e1), np.dot(J0, e2)],
156
+ [np.dot(J1, e1), np.dot(J1, e2)]
157
+ ])
158
+ inv_j_local, _ = _inv2(J_local)
159
+
160
+ dN_dx = inv_j_local[0, 0] * dN_dxi + inv_j_local[0, 1] * dN_deta
161
+ dN_dy = inv_j_local[1, 0] * dN_dxi + inv_j_local[1, 1] * dN_deta
162
+ return R, dN_dx, dN_dy, det_j
163
+
164
+ @njit(cache=True)
165
+ def _build_shell_b_matrices_jit(
166
+ N: np.ndarray,
167
+ dN_dx: np.ndarray,
168
+ dN_dy: np.ndarray,
169
+ total_dofs: int,
170
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
171
+ B_m = np.zeros((3, total_dofs))
172
+ B_b = np.zeros((3, total_dofs))
173
+ B_s = np.zeros((2, total_dofs))
174
+
175
+ B_m[0, 0::6] = dN_dx
176
+ B_m[1, 1::6] = dN_dy
177
+ B_m[2, 0::6] = dN_dy
178
+ B_m[2, 1::6] = dN_dx
179
+
180
+ B_b[0, 4::6] = dN_dx
181
+ B_b[1, 3::6] = -dN_dy
182
+ B_b[2, 4::6] = dN_dy
183
+ B_b[2, 3::6] = -dN_dx
184
+
185
+ B_s[0, 2::6] = dN_dx
186
+ B_s[0, 4::6] = N
187
+ B_s[1, 2::6] = dN_dy
188
+ B_s[1, 3::6] = -N
189
+ return B_m, B_b, B_s
190
+
191
+ @njit(cache=True)
192
+ def _build_drilling_b_matrix_jit(
193
+ N: np.ndarray,
194
+ dN_dx: np.ndarray,
195
+ dN_dy: np.ndarray,
196
+ total_dofs: int,
197
+ ) -> np.ndarray:
198
+ B_d = np.zeros((1, total_dofs))
199
+ B_d[0, 0::6] = 0.5 * dN_dy
200
+ B_d[0, 1::6] = -0.5 * dN_dx
201
+ B_d[0, 5::6] = N
202
+ return B_d
203
+
204
+ @njit(cache=True)
205
+ def _mitc4_shear_samples_jit(
206
+ coords: np.ndarray,
207
+ R: np.ndarray,
208
+ total_dofs: int,
209
+ ) -> Tuple[np.ndarray, np.ndarray]:
210
+ planar = np.zeros((4, 2))
211
+ for i in range(4):
212
+ planar[i, 0] = coords[i, 0] * R[0, 0] + coords[i, 1] * R[1, 0] + coords[i, 2] * R[2, 0]
213
+ planar[i, 1] = coords[i, 0] * R[0, 1] + coords[i, 1] * R[1, 1] + coords[i, 2] * R[2, 1]
214
+
215
+ pts = np.array([
216
+ [0.0, -1.0], # A
217
+ [1.0, 0.0], # B
218
+ [0.0, 1.0], # C
219
+ [-1.0, 0.0] # D
220
+ ])
221
+
222
+ samples_arr = np.zeros((4, 2, total_dofs))
223
+
224
+ for pt_idx in range(4):
225
+ xi = pts[pt_idx, 0]
226
+ eta = pts[pt_idx, 1]
227
+ N, dN_dxi, dN_deta = _compute_4node_shape_functions(xi, eta)
228
+
229
+ x_xi = 0.0
230
+ y_xi = 0.0
231
+ x_eta = 0.0
232
+ y_eta = 0.0
233
+ for i in range(4):
234
+ x_xi += dN_dxi[i] * planar[i, 0]
235
+ y_xi += dN_dxi[i] * planar[i, 1]
236
+ x_eta += dN_deta[i] * planar[i, 0]
237
+ y_eta += dN_deta[i] * planar[i, 1]
238
+
239
+ row_xi = np.zeros(total_dofs)
240
+ row_eta = np.zeros(total_dofs)
241
+
242
+ row_xi[2::6] = dN_dxi
243
+ row_xi[3::6] = -N * y_xi
244
+ row_xi[4::6] = N * x_xi
245
+
246
+ row_eta[2::6] = dN_deta
247
+ row_eta[3::6] = -N * y_eta
248
+ row_eta[4::6] = N * x_eta
249
+
250
+ samples_arr[pt_idx, 0] = row_xi
251
+ samples_arr[pt_idx, 1] = row_eta
252
+
253
+ return planar, samples_arr
254
+
255
+ @njit(cache=True)
256
+ def _mitc4_shear_b_matrix_jit(
257
+ planar: np.ndarray,
258
+ samples_arr: np.ndarray,
259
+ xi: float,
260
+ eta: float,
261
+ total_dofs: int,
262
+ ) -> Tuple[np.ndarray, float]:
263
+ _, dN_dxi, dN_deta = _compute_4node_shape_functions(xi, eta)
264
+ J2 = np.zeros((2, 2))
265
+ for i in range(4):
266
+ J2[0, 0] += dN_dxi[i] * planar[i, 0]
267
+ J2[0, 1] += dN_dxi[i] * planar[i, 1]
268
+ J2[1, 0] += dN_deta[i] * planar[i, 0]
269
+ J2[1, 1] += dN_deta[i] * planar[i, 1]
270
+
271
+ inv_j2, det_j = _inv2(J2)
272
+
273
+ B_covariant = np.zeros((2, total_dofs))
274
+ for i in range(total_dofs):
275
+ B_covariant[0, i] = 0.5 * (1.0 - eta) * samples_arr[0, 0, i] + 0.5 * (1.0 + eta) * samples_arr[2, 0, i]
276
+ B_covariant[1, i] = 0.5 * (1.0 - xi) * samples_arr[3, 1, i] + 0.5 * (1.0 + xi) * samples_arr[1, 1, i]
277
+
278
+ res = np.zeros((2, total_dofs))
279
+ for r in range(2):
280
+ for c in range(total_dofs):
281
+ val = 0.0
282
+ for k in range(2):
283
+ val += inv_j2[r, k] * B_covariant[k, c]
284
+ res[r, c] = val
285
+
286
+ return res, det_j
287
+
288
+ @njit(cache=True, parallel=True)
289
+ def compute_shell_stiffness_matrices_jit(
290
+ coords_all: np.ndarray, # (N, num_nodes, 3)
291
+ is_4node: bool,
292
+ thickness: float,
293
+ drilling_stabilization: float,
294
+ E: float,
295
+ nu: float,
296
+ G: float,
297
+ gauss_points: np.ndarray, # (num_gp, 2)
298
+ gauss_weights: np.ndarray, # (num_gp,)
299
+ shear_points: np.ndarray, # (num_shear_gp, 2)
300
+ shear_weights: np.ndarray, # (num_shear_gp,)
301
+ ) -> np.ndarray: # (N, total_dofs, total_dofs)
302
+ N_elem = coords_all.shape[0]
303
+ num_nodes = 4 if is_4node else 8
304
+ total_dofs = num_nodes * 6
305
+ n_blocks = 2 * num_nodes
306
+
307
+ shell_plane = np.zeros((3, 3))
308
+ shell_plane[0, 0] = 1.0
309
+ shell_plane[0, 1] = nu
310
+ shell_plane[1, 0] = nu
311
+ shell_plane[1, 1] = 1.0
312
+ shell_plane[2, 2] = (1.0 - nu) / 2.0
313
+
314
+ h = thickness
315
+ D_membrane = E * h / (1.0 - nu**2) * shell_plane
316
+ D_bending = E * h**3 / (12.0 * (1.0 - nu**2)) * shell_plane
317
+
318
+ D_shear = np.zeros((2, 2))
319
+ D_shear[0, 0] = G * (5.0 / 6.0) * h
320
+ D_shear[1, 1] = G * (5.0 / 6.0) * h
321
+
322
+ drilling_stiffness = G * h * drilling_stabilization
323
+
324
+ K_all = np.zeros((N_elem, total_dofs, total_dofs))
325
+
326
+ for e in prange(N_elem):
327
+ coords = coords_all[e]
328
+ K = np.zeros((total_dofs, total_dofs))
329
+
330
+ num_gp = gauss_points.shape[0]
331
+ for gp_idx in range(num_gp):
332
+ xi = gauss_points[gp_idx, 0]
333
+ eta = gauss_points[gp_idx, 1]
334
+ weight = gauss_weights[gp_idx]
335
+
336
+ if is_4node:
337
+ N, dN_dxi, dN_deta = _compute_4node_shape_functions(xi, eta)
338
+ else:
339
+ N, dN_dxi, dN_deta = _compute_8node_shape_functions(xi, eta)
340
+
341
+ R, dN_dx, dN_dy, det_j = _local_frame_and_derivatives_jit(coords, dN_dxi, dN_deta)
342
+ B_m, B_b, _ = _build_shell_b_matrices_jit(N, dN_dx, dN_dy, total_dofs)
343
+ B_d = _build_drilling_b_matrix_jit(N, dN_dx, dN_dy, total_dofs)
344
+
345
+ scale = det_j * weight
346
+
347
+ D_B_m = np.zeros((3, total_dofs))
348
+ D_B_b = np.zeros((3, total_dofs))
349
+ for r in range(3):
350
+ for c in range(total_dofs):
351
+ val_m = 0.0
352
+ val_b = 0.0
353
+ for k in range(3):
354
+ val_m += D_membrane[r, k] * B_m[k, c]
355
+ val_b += D_bending[r, k] * B_b[k, c]
356
+ D_B_m[r, c] = val_m
357
+ D_B_b[r, c] = val_b
358
+
359
+ K_local = np.zeros((total_dofs, total_dofs))
360
+ for r in range(total_dofs):
361
+ for c in range(total_dofs):
362
+ val = 0.0
363
+ for k in range(3):
364
+ val += B_m[k, r] * D_B_m[k, c] + B_b[k, r] * D_B_b[k, c]
365
+ K_local[r, c] = val * scale
366
+ K_local[r, c] += B_d[0, r] * drilling_stiffness * B_d[0, c] * scale
367
+
368
+ # Global transformation block-by-block
369
+ for I in range(n_blocks):
370
+ for J in range(n_blocks):
371
+ block = np.zeros((3, 3))
372
+ for r in range(3):
373
+ for c in range(3):
374
+ block[r, c] = K_local[3*I + r, 3*J + c]
375
+
376
+ R_block_temp = np.zeros((3, 3))
377
+ for r in range(3):
378
+ for c in range(3):
379
+ val = 0.0
380
+ for k in range(3):
381
+ val += R[r, k] * block[k, c]
382
+ R_block_temp[r, c] = val
383
+ R_block = np.zeros((3, 3))
384
+ for r in range(3):
385
+ for c in range(3):
386
+ val = 0.0
387
+ for k in range(3):
388
+ val += R_block_temp[r, k] * R[c, k]
389
+ R_block[r, c] = val
390
+
391
+ for r in range(3):
392
+ for c in range(3):
393
+ K[3*I + r, 3*J + c] += R_block[r, c]
394
+
395
+ # Transverse shear
396
+ if is_4node:
397
+ N_c, dN_dxi_c, dN_deta_c = _compute_4node_shape_functions(0.0, 0.0)
398
+ R_center, _, _, _ = _local_frame_and_derivatives_jit(coords, dN_dxi_c, dN_deta_c)
399
+
400
+ planar, samples_arr = _mitc4_shear_samples_jit(coords, R_center, total_dofs)
401
+
402
+ gps_shear = np.array([
403
+ [-1.0, -1.0],
404
+ [1.0, -1.0],
405
+ [-1.0, 1.0],
406
+ [1.0, 1.0]
407
+ ]) / np.sqrt(3.0)
408
+ w_shear = np.array([1.0, 1.0, 1.0, 1.0])
409
+
410
+ for sh_idx in range(4):
411
+ xi = gps_shear[sh_idx, 0]
412
+ eta = gps_shear[sh_idx, 1]
413
+ weight = w_shear[sh_idx]
414
+
415
+ B_s, det_j = _mitc4_shear_b_matrix_jit(planar, samples_arr, xi, eta, total_dofs)
416
+ scale = det_j * weight
417
+
418
+ D_B_s = np.zeros((2, total_dofs))
419
+ for r in range(2):
420
+ for c in range(total_dofs):
421
+ val = 0.0
422
+ for k in range(2):
423
+ val += D_shear[r, k] * B_s[k, c]
424
+ D_B_s[r, c] = val
425
+
426
+ K_local = np.zeros((total_dofs, total_dofs))
427
+ for r in range(total_dofs):
428
+ for c in range(total_dofs):
429
+ val = 0.0
430
+ for k in range(2):
431
+ val += B_s[k, r] * D_B_s[k, c]
432
+ K_local[r, c] = val * scale
433
+
434
+ for I in range(n_blocks):
435
+ for J in range(n_blocks):
436
+ block = np.zeros((3, 3))
437
+ for r in range(3):
438
+ for c in range(3):
439
+ block[r, c] = K_local[3*I + r, 3*J + c]
440
+ R_block_temp = np.zeros((3, 3))
441
+ for r in range(3):
442
+ for c in range(3):
443
+ val = 0.0
444
+ for k in range(3):
445
+ val += R_center[r, k] * block[k, c]
446
+ R_block_temp[r, c] = val
447
+ R_block = np.zeros((3, 3))
448
+ for r in range(3):
449
+ for c in range(3):
450
+ val = 0.0
451
+ for k in range(3):
452
+ val += R_block_temp[r, k] * R_center[c, k]
453
+ R_block[r, c] = val
454
+ for r in range(3):
455
+ for c in range(3):
456
+ K[3*I + r, 3*J + c] += R_block[r, c]
457
+
458
+ else:
459
+ num_shear_gp = shear_points.shape[0]
460
+ for sh_idx in range(num_shear_gp):
461
+ xi = shear_points[sh_idx, 0]
462
+ eta = shear_points[sh_idx, 1]
463
+ weight = shear_weights[sh_idx]
464
+
465
+ N, dN_dxi, dN_deta = _compute_8node_shape_functions(xi, eta)
466
+ R, dN_dx, dN_dy, det_j = _local_frame_and_derivatives_jit(coords, dN_dxi, dN_deta)
467
+
468
+ _, _, B_s = _build_shell_b_matrices_jit(N, dN_dx, dN_dy, total_dofs)
469
+ scale = det_j * weight
470
+
471
+ D_B_s = np.zeros((2, total_dofs))
472
+ for r in range(2):
473
+ for c in range(total_dofs):
474
+ val = 0.0
475
+ for k in range(2):
476
+ val += D_shear[r, k] * B_s[k, c]
477
+ D_B_s[r, c] = val
478
+
479
+ K_local = np.zeros((total_dofs, total_dofs))
480
+ for r in range(total_dofs):
481
+ for c in range(total_dofs):
482
+ val = 0.0
483
+ for k in range(2):
484
+ val += B_s[k, r] * D_B_s[k, c]
485
+ K_local[r, c] = val * scale
486
+
487
+ for I in range(n_blocks):
488
+ for J in range(n_blocks):
489
+ block = np.zeros((3, 3))
490
+ for r in range(3):
491
+ for c in range(3):
492
+ block[r, c] = K_local[3*I + r, 3*J + c]
493
+ R_block_temp = np.zeros((3, 3))
494
+ for r in range(3):
495
+ for c in range(3):
496
+ val = 0.0
497
+ for k in range(3):
498
+ val += R[r, k] * block[k, c]
499
+ R_block_temp[r, c] = val
500
+ R_block = np.zeros((3, 3))
501
+ for r in range(3):
502
+ for c in range(3):
503
+ val = 0.0
504
+ for k in range(3):
505
+ val += R_block_temp[r, k] * R[c, k]
506
+ R_block[r, c] = val
507
+ for r in range(3):
508
+ for c in range(3):
509
+ K[3*I + r, 3*J + c] += R_block[r, c]
510
+
511
+ K_all[e] = K
512
+
513
+ return K_all
514
+
515
+
516
+ @njit(cache=True, parallel=True)
517
+ def compute_shell_mass_matrices_jit(
518
+ coords_all: np.ndarray, # (N, num_nodes, 3)
519
+ is_4node: bool,
520
+ thickness: float,
521
+ rho: float,
522
+ gauss_points: np.ndarray, # (num_gp, 2)
523
+ gauss_weights: np.ndarray, # (num_gp,)
524
+ ) -> np.ndarray: # (N, total_dofs, total_dofs)
525
+ """Batched consistent shell mass matrices.
526
+
527
+ The per-integration-point mass blocks are isotropic per node pair
528
+ (``m_ij * I3`` for translations and rotations separately), so the local
529
+ shell frame rotation cancels exactly (``R.T (m I3) R == m I3``) and the
530
+ local-to-global DOF transform used by the scalar element path can be
531
+ skipped without changing the result.
532
+ """
533
+ N_elem = coords_all.shape[0]
534
+ num_nodes = 4 if is_4node else 8
535
+ total_dofs = num_nodes * 6
536
+
537
+ trans_factor = rho * thickness
538
+ rot_factor = rho * thickness**3 / 12.0
539
+
540
+ M_all = np.zeros((N_elem, total_dofs, total_dofs))
541
+
542
+ for e in prange(N_elem):
543
+ coords = coords_all[e]
544
+ M = np.zeros((total_dofs, total_dofs))
545
+
546
+ num_gp = gauss_points.shape[0]
547
+ for gp_idx in range(num_gp):
548
+ xi = gauss_points[gp_idx, 0]
549
+ eta = gauss_points[gp_idx, 1]
550
+ weight = gauss_weights[gp_idx]
551
+
552
+ if is_4node:
553
+ N, dN_dxi, dN_deta = _compute_4node_shape_functions(xi, eta)
554
+ else:
555
+ N, dN_dxi, dN_deta = _compute_8node_shape_functions(xi, eta)
556
+
557
+ _R, _dN_dx, _dN_dy, det_j = _local_frame_and_derivatives_jit(coords, dN_dxi, dN_deta)
558
+ scale = det_j * weight
559
+
560
+ for i in range(num_nodes):
561
+ for j in range(num_nodes):
562
+ outer_n = N[i] * N[j] * scale
563
+ trans = trans_factor * outer_n
564
+ rot = rot_factor * outer_n
565
+ for d in range(3):
566
+ M[6 * i + d, 6 * j + d] += trans
567
+ M[6 * i + 3 + d, 6 * j + 3 + d] += rot
568
+
569
+ M_all[e] = M
570
+
571
+ return M_all