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,592 @@
1
+ """Batch B optimizations for persistent nonlinear shell assembly.
2
+
3
+ This module completes the nonlinear-assembly redesign for ordinary Python runs
4
+ with Numba available. Elastic shell groups use a dedicated in-place kernel that
5
+ writes element force and tangent entries directly into the persistent assembly
6
+ plan buffers. The plastic shell path and all element formulations remain
7
+ unchanged.
8
+
9
+ The elastic path avoids, per Newton evaluation:
10
+
11
+ * plastic-strain and hardening-state work arrays,
12
+ * through-thickness layer-strain arrays,
13
+ * broadcast constitutive tensor batches,
14
+ * per-batch force/tangent result arrays, and
15
+ * Python reconstruction of new elastic state arrays.
16
+
17
+ The kernel relies on the shell transformation's existing 3x3 block-diagonal
18
+ layout (translations and rotations for every node). Each local 3x3 tangent
19
+ block is transformed in place, so no second dense element tangent batch is
20
+ required.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import time
26
+ from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
27
+
28
+ import numpy as np
29
+ from scipy import sparse
30
+
31
+ from .jit_compiler import njit, prange
32
+ from .elements import plane_stress_elastic_matrix
33
+ from . import nonlinear_performance as _performance
34
+
35
+
36
+ @njit(cache=True, parallel=True)
37
+ def _elastic_shell_batch_into_buffers(
38
+ displacements: np.ndarray,
39
+ dof_mappings: np.ndarray,
40
+ T0_batch: np.ndarray,
41
+ B_m_batch: np.ndarray,
42
+ B_b_batch: np.ndarray,
43
+ B_d_batch: np.ndarray,
44
+ Gw_batch: np.ndarray,
45
+ detw_batch: np.ndarray,
46
+ B_s_batch: np.ndarray,
47
+ detw_shear_batch: np.ndarray,
48
+ membrane_matrix: np.ndarray,
49
+ bending_matrix: np.ndarray,
50
+ shear_matrix: np.ndarray,
51
+ drilling_stiffness: float,
52
+ force_positions: np.ndarray,
53
+ tangent_positions: np.ndarray,
54
+ force_values: np.ndarray,
55
+ tangent_values: np.ndarray,
56
+ u_local_work: np.ndarray,
57
+ tangent: bool,
58
+ ) -> None:
59
+ """Evaluate elastic geometrically nonlinear shells into persistent buffers."""
60
+ n_elem = dof_mappings.shape[0]
61
+ n_dof = dof_mappings.shape[1]
62
+ n_gp = detw_batch.shape[1]
63
+ n_shear = detw_shear_batch.shape[1]
64
+
65
+ for element_index in prange(n_elem):
66
+ T0 = T0_batch[element_index]
67
+ u_local = u_local_work[element_index]
68
+
69
+ # Gather global element DOFs and transform to the reference local frame.
70
+ for local_row in range(n_dof):
71
+ value = 0.0
72
+ for local_col in range(n_dof):
73
+ value += (
74
+ T0[local_row, local_col]
75
+ * displacements[dof_mappings[element_index, local_col]]
76
+ )
77
+ u_local[local_row] = value
78
+
79
+ # The persistent plan clears the flat buffers before this kernel. Work
80
+ # directly in the element's assigned slices.
81
+ force_pos = force_positions[element_index]
82
+ tangent_pos = tangent_positions[element_index]
83
+
84
+ B_eff = np.empty((3, n_dof), dtype=np.float64)
85
+ membrane_times_B = np.empty((3, n_dof), dtype=np.float64)
86
+ bending_times_B = np.empty((3, n_dof), dtype=np.float64)
87
+
88
+ for gp_index in range(n_gp):
89
+ B_m = B_m_batch[element_index, gp_index]
90
+ B_b = B_b_batch[element_index, gp_index]
91
+ B_d = B_d_batch[element_index, gp_index]
92
+ Gw = Gw_batch[element_index, gp_index]
93
+ detw = detw_batch[element_index, gp_index]
94
+
95
+ theta_0 = 0.0
96
+ theta_1 = 0.0
97
+ membrane_strain_0 = 0.0
98
+ membrane_strain_1 = 0.0
99
+ membrane_strain_2 = 0.0
100
+ curvature_0 = 0.0
101
+ curvature_1 = 0.0
102
+ curvature_2 = 0.0
103
+ drilling_rotation = 0.0
104
+
105
+ for dof_index in range(n_dof):
106
+ displacement = u_local[dof_index]
107
+ theta_0 += Gw[0, dof_index] * displacement
108
+ theta_1 += Gw[1, dof_index] * displacement
109
+ membrane_strain_0 += B_m[0, dof_index] * displacement
110
+ membrane_strain_1 += B_m[1, dof_index] * displacement
111
+ membrane_strain_2 += B_m[2, dof_index] * displacement
112
+ curvature_0 += B_b[0, dof_index] * displacement
113
+ curvature_1 += B_b[1, dof_index] * displacement
114
+ curvature_2 += B_b[2, dof_index] * displacement
115
+ drilling_rotation += B_d[0, dof_index] * displacement
116
+
117
+ membrane_strain_0 += 0.5 * theta_0 * theta_0
118
+ membrane_strain_1 += 0.5 * theta_1 * theta_1
119
+ membrane_strain_2 += theta_0 * theta_1
120
+
121
+ N_0 = (
122
+ membrane_matrix[0, 0] * membrane_strain_0
123
+ + membrane_matrix[0, 1] * membrane_strain_1
124
+ + membrane_matrix[0, 2] * membrane_strain_2
125
+ )
126
+ N_1 = (
127
+ membrane_matrix[1, 0] * membrane_strain_0
128
+ + membrane_matrix[1, 1] * membrane_strain_1
129
+ + membrane_matrix[1, 2] * membrane_strain_2
130
+ )
131
+ N_2 = (
132
+ membrane_matrix[2, 0] * membrane_strain_0
133
+ + membrane_matrix[2, 1] * membrane_strain_1
134
+ + membrane_matrix[2, 2] * membrane_strain_2
135
+ )
136
+ M_0 = (
137
+ bending_matrix[0, 0] * curvature_0
138
+ + bending_matrix[0, 1] * curvature_1
139
+ + bending_matrix[0, 2] * curvature_2
140
+ )
141
+ M_1 = (
142
+ bending_matrix[1, 0] * curvature_0
143
+ + bending_matrix[1, 1] * curvature_1
144
+ + bending_matrix[1, 2] * curvature_2
145
+ )
146
+ M_2 = (
147
+ bending_matrix[2, 0] * curvature_0
148
+ + bending_matrix[2, 1] * curvature_1
149
+ + bending_matrix[2, 2] * curvature_2
150
+ )
151
+
152
+ for dof_index in range(n_dof):
153
+ B_eff[0, dof_index] = B_m[0, dof_index] + theta_0 * Gw[0, dof_index]
154
+ B_eff[1, dof_index] = B_m[1, dof_index] + theta_1 * Gw[1, dof_index]
155
+ B_eff[2, dof_index] = (
156
+ B_m[2, dof_index]
157
+ + theta_0 * Gw[1, dof_index]
158
+ + theta_1 * Gw[0, dof_index]
159
+ )
160
+
161
+ force_values[force_pos[dof_index]] += (
162
+ B_eff[0, dof_index] * N_0
163
+ + B_eff[1, dof_index] * N_1
164
+ + B_eff[2, dof_index] * N_2
165
+ + B_b[0, dof_index] * M_0
166
+ + B_b[1, dof_index] * M_1
167
+ + B_b[2, dof_index] * M_2
168
+ + B_d[0, dof_index] * drilling_stiffness * drilling_rotation
169
+ ) * detw
170
+
171
+ if not tangent:
172
+ continue
173
+
174
+ for row in range(3):
175
+ for dof_index in range(n_dof):
176
+ membrane_value = 0.0
177
+ bending_value = 0.0
178
+ for constitutive_index in range(3):
179
+ membrane_value += (
180
+ membrane_matrix[row, constitutive_index]
181
+ * B_eff[constitutive_index, dof_index]
182
+ )
183
+ bending_value += (
184
+ bending_matrix[row, constitutive_index]
185
+ * B_b[constitutive_index, dof_index]
186
+ )
187
+ membrane_times_B[row, dof_index] = membrane_value
188
+ bending_times_B[row, dof_index] = bending_value
189
+
190
+ for local_row in range(n_dof):
191
+ gw0_row = Gw[0, local_row]
192
+ gw1_row = Gw[1, local_row]
193
+ bd_row = B_d[0, local_row]
194
+ for local_col in range(n_dof):
195
+ material_value = 0.0
196
+ bending_value = 0.0
197
+ for constitutive_index in range(3):
198
+ material_value += (
199
+ B_eff[constitutive_index, local_row]
200
+ * membrane_times_B[constitutive_index, local_col]
201
+ )
202
+ bending_value += (
203
+ B_b[constitutive_index, local_row]
204
+ * bending_times_B[constitutive_index, local_col]
205
+ )
206
+ geometric_value = (
207
+ gw0_row * (N_0 * Gw[0, local_col] + N_2 * Gw[1, local_col])
208
+ + gw1_row * (N_2 * Gw[0, local_col] + N_1 * Gw[1, local_col])
209
+ )
210
+ drilling_value = bd_row * drilling_stiffness * B_d[0, local_col]
211
+ tangent_values[
212
+ tangent_pos[local_row * n_dof + local_col]
213
+ ] += (
214
+ material_value
215
+ + bending_value
216
+ + geometric_value
217
+ + drilling_value
218
+ ) * detw
219
+
220
+ for shear_index in range(n_shear):
221
+ B_s = B_s_batch[element_index, shear_index]
222
+ detw_shear = detw_shear_batch[element_index, shear_index]
223
+ gamma_0 = 0.0
224
+ gamma_1 = 0.0
225
+ for dof_index in range(n_dof):
226
+ gamma_0 += B_s[0, dof_index] * u_local[dof_index]
227
+ gamma_1 += B_s[1, dof_index] * u_local[dof_index]
228
+
229
+ shear_resultant_0 = (
230
+ shear_matrix[0, 0] * gamma_0 + shear_matrix[0, 1] * gamma_1
231
+ )
232
+ shear_resultant_1 = (
233
+ shear_matrix[1, 0] * gamma_0 + shear_matrix[1, 1] * gamma_1
234
+ )
235
+ for dof_index in range(n_dof):
236
+ force_values[force_pos[dof_index]] += (
237
+ B_s[0, dof_index] * shear_resultant_0
238
+ + B_s[1, dof_index] * shear_resultant_1
239
+ ) * detw_shear
240
+
241
+ if tangent:
242
+ for local_row in range(n_dof):
243
+ for local_col in range(n_dof):
244
+ shear_value = (
245
+ B_s[0, local_row]
246
+ * (
247
+ shear_matrix[0, 0] * B_s[0, local_col]
248
+ + shear_matrix[0, 1] * B_s[1, local_col]
249
+ )
250
+ + B_s[1, local_row]
251
+ * (
252
+ shear_matrix[1, 0] * B_s[0, local_col]
253
+ + shear_matrix[1, 1] * B_s[1, local_col]
254
+ )
255
+ )
256
+ tangent_values[
257
+ tangent_pos[local_row * n_dof + local_col]
258
+ ] += shear_value * detw_shear
259
+
260
+ # Transform force blocks from local to global in place. T0 maps global
261
+ # displacements to local displacements, hence forces use T0.T.
262
+ for block_start in range(0, n_dof, 3):
263
+ local_0 = force_values[force_pos[block_start]]
264
+ local_1 = force_values[force_pos[block_start + 1]]
265
+ local_2 = force_values[force_pos[block_start + 2]]
266
+ force_values[force_pos[block_start]] = (
267
+ T0[block_start, block_start] * local_0
268
+ + T0[block_start + 1, block_start] * local_1
269
+ + T0[block_start + 2, block_start] * local_2
270
+ )
271
+ force_values[force_pos[block_start + 1]] = (
272
+ T0[block_start, block_start + 1] * local_0
273
+ + T0[block_start + 1, block_start + 1] * local_1
274
+ + T0[block_start + 2, block_start + 1] * local_2
275
+ )
276
+ force_values[force_pos[block_start + 2]] = (
277
+ T0[block_start, block_start + 2] * local_0
278
+ + T0[block_start + 1, block_start + 2] * local_1
279
+ + T0[block_start + 2, block_start + 2] * local_2
280
+ )
281
+
282
+ if not tangent:
283
+ continue
284
+
285
+ # T0 is block diagonal with 3x3 translation/rotation blocks. Each
286
+ # K_global block depends only on the corresponding K_local block, so it
287
+ # can be transformed and overwritten safely without a second matrix.
288
+ local_block = np.empty((3, 3), dtype=np.float64)
289
+ intermediate = np.empty((3, 3), dtype=np.float64)
290
+ global_block = np.empty((3, 3), dtype=np.float64)
291
+ for row_block in range(0, n_dof, 3):
292
+ for col_block in range(0, n_dof, 3):
293
+ for row in range(3):
294
+ for col in range(3):
295
+ local_block[row, col] = tangent_values[
296
+ tangent_pos[
297
+ (row_block + row) * n_dof + col_block + col
298
+ ]
299
+ ]
300
+
301
+ for row in range(3):
302
+ for col in range(3):
303
+ value = 0.0
304
+ for inner in range(3):
305
+ value += (
306
+ local_block[row, inner]
307
+ * T0[col_block + inner, col_block + col]
308
+ )
309
+ intermediate[row, col] = value
310
+
311
+ for row in range(3):
312
+ for col in range(3):
313
+ value = 0.0
314
+ for inner in range(3):
315
+ value += (
316
+ T0[row_block + inner, row_block + row]
317
+ * intermediate[inner, col]
318
+ )
319
+ global_block[row, col] = value
320
+
321
+ for row in range(3):
322
+ for col in range(3):
323
+ tangent_values[
324
+ tangent_pos[
325
+ (row_block + row) * n_dof + col_block + col
326
+ ]
327
+ ] = global_block[row, col]
328
+
329
+
330
+ _ORIGINAL_BATCH_BUILD = _performance._ShellBatchPlan.build.__func__
331
+ _ORIGINAL_PLAN_ASSEMBLE = _performance.NonlinearAssemblyPlan.assemble
332
+ _ORIGINAL_PLAN_DIAGNOSTICS = _performance.NonlinearAssemblyPlan.diagnostics
333
+ _INSTALLED = False
334
+
335
+
336
+ def _batch_b_shell_build(
337
+ cls,
338
+ model,
339
+ key,
340
+ items,
341
+ num_layers,
342
+ ):
343
+ batch = _ORIGINAL_BATCH_BUILD(cls, model, key, items, num_layers)
344
+ if batch.has_plasticity:
345
+ batch._batch_b_elastic = False
346
+ return batch
347
+
348
+ elastic_matrix = plane_stress_elastic_matrix(
349
+ float(batch.material.elastic_modulus),
350
+ float(batch.material.poisson_ratio),
351
+ )
352
+ batch._batch_b_elastic = True
353
+ batch._batch_b_membrane_matrix = batch.thickness * elastic_matrix
354
+ batch._batch_b_bending_matrix = batch.thickness**3 / 12.0 * elastic_matrix
355
+ batch._batch_b_shear_matrix = (
356
+ float(batch.material.shear_modulus)
357
+ * (5.0 / 6.0)
358
+ * batch.thickness
359
+ * np.eye(2, dtype=float)
360
+ )
361
+ batch._batch_b_drilling_stiffness = (
362
+ float(batch.material.shear_modulus)
363
+ * batch.thickness
364
+ * batch.drilling_stabilization
365
+ )
366
+
367
+ # Elastic groups do not require mutable constitutive history. Release the
368
+ # per-element plastic work arrays allocated by the compatibility builder.
369
+ batch.plastic_work = np.empty((0, 0, 3), dtype=float)
370
+ batch.alpha_work = np.empty((0, 0), dtype=float)
371
+
372
+ points_per_element = int(batch.n_gp * batch.num_layers)
373
+ shared_plastic = np.zeros((points_per_element, 3), dtype=float)
374
+ shared_alpha = np.zeros(points_per_element, dtype=float)
375
+ shared_plastic.setflags(write=False)
376
+ shared_alpha.setflags(write=False)
377
+ batch.elastic_states = tuple(
378
+ {
379
+ "plastic_strain": shared_plastic,
380
+ "alpha": shared_alpha,
381
+ }
382
+ for _ in batch.elements
383
+ )
384
+ batch._batch_b_elastic_state_mapping = {
385
+ int(element_id): state
386
+ for element_id, state in zip(batch.element_ids, batch.elastic_states)
387
+ }
388
+ return batch
389
+
390
+
391
+ def _batch_b_plan_assemble(
392
+ self,
393
+ displacements: np.ndarray,
394
+ committed_states: Mapping[int, Any],
395
+ tangent: bool = True,
396
+ deleted_element_ids: Sequence[int] = (),
397
+ residual_stiffness_fraction: float = 1.0,
398
+ ) -> Tuple[np.ndarray, Optional[sparse.csr_matrix], Dict[int, Any]]:
399
+ """Assemble with direct-buffer elastic batches and legacy plastic batches."""
400
+ if tuple(deleted_element_ids or ()):
401
+ return _ORIGINAL_PLAN_ASSEMBLE(
402
+ self,
403
+ displacements,
404
+ committed_states,
405
+ tangent=tangent,
406
+ deleted_element_ids=tuple(deleted_element_ids),
407
+ residual_stiffness_fraction=float(residual_stiffness_fraction),
408
+ )
409
+ with self._lock:
410
+ start_total = time.perf_counter()
411
+ self.timings.calls += 1
412
+ if tangent:
413
+ self.timings.tangent_calls += 1
414
+ else:
415
+ self.timings.residual_only_calls += 1
416
+
417
+ self.force_values.fill(0.0)
418
+ if tangent:
419
+ self.tangent_values.fill(0.0)
420
+ trial_states: Dict[int, Any] = {}
421
+
422
+ for batch in self.shell_batches:
423
+ if getattr(batch, "_batch_b_elastic", False):
424
+ kernel_start = time.perf_counter()
425
+ _elastic_shell_batch_into_buffers(
426
+ np.asarray(displacements, dtype=float),
427
+ batch.dof_mappings,
428
+ batch.T0,
429
+ batch.B_m,
430
+ batch.B_b,
431
+ batch.B_d,
432
+ batch.Gw,
433
+ batch.detw,
434
+ batch.B_s,
435
+ batch.detw_shear,
436
+ batch._batch_b_membrane_matrix,
437
+ batch._batch_b_bending_matrix,
438
+ batch._batch_b_shear_matrix,
439
+ float(batch._batch_b_drilling_stiffness),
440
+ batch.force_positions,
441
+ batch.tangent_positions,
442
+ self.force_values,
443
+ self.tangent_values,
444
+ batch.u_work,
445
+ bool(tangent),
446
+ )
447
+ self.timings.shell_kernel_seconds += time.perf_counter() - kernel_start
448
+ elastic_states = batch._batch_b_elastic_state_mapping
449
+ # Preserve explicitly supplied/previous elastic states without
450
+ # allocating a new mapping in the normal case.
451
+ use_cached_mapping = True
452
+ for element_id in batch.element_ids:
453
+ existing = committed_states.get(int(element_id))
454
+ if isinstance(existing, dict) and existing is not elastic_states[int(element_id)]:
455
+ use_cached_mapping = False
456
+ break
457
+ if use_cached_mapping:
458
+ trial_states.update(elastic_states)
459
+ else:
460
+ for element_id in batch.element_ids:
461
+ element_key = int(element_id)
462
+ existing = committed_states.get(element_key)
463
+ trial_states[element_key] = (
464
+ existing if isinstance(existing, dict) else elastic_states[element_key]
465
+ )
466
+ else:
467
+ F_batch, K_batch, batch_states, kernel_seconds = batch.evaluate(
468
+ displacements,
469
+ committed_states,
470
+ tangent,
471
+ )
472
+ self.timings.shell_kernel_seconds += kernel_seconds
473
+ self.force_values[batch.force_positions.reshape(-1)] = np.asarray(
474
+ F_batch, dtype=float
475
+ ).reshape(-1)
476
+ if tangent and K_batch is not None:
477
+ self.tangent_values[batch.tangent_positions.reshape(-1)] = np.asarray(
478
+ K_batch, dtype=float
479
+ ).reshape(-1)
480
+ trial_states.update(batch_states)
481
+
482
+ non_shell_start = time.perf_counter()
483
+ model = self.model
484
+ mesh = model.mesh
485
+ for record in self.non_shell_elements:
486
+ material = model.get_material(record.element.material_name)
487
+ u_element = np.asarray(displacements, dtype=float)[record.dof_mapping]
488
+ f_element, k_element, trial_state = record.element.compute_nonlinear_response(
489
+ mesh,
490
+ material,
491
+ u_element,
492
+ committed_states.get(record.element_id),
493
+ self.num_layers,
494
+ tangent,
495
+ )
496
+ self.force_values[record.force_positions] = np.asarray(
497
+ f_element, dtype=float
498
+ ).reshape(-1)
499
+ if tangent and k_element is not None:
500
+ self.tangent_values[record.tangent_positions] = np.asarray(
501
+ k_element, dtype=float
502
+ ).reshape(-1)
503
+ if trial_state is not None:
504
+ trial_states[record.element_id] = trial_state
505
+ self.timings.non_shell_seconds += time.perf_counter() - non_shell_start
506
+
507
+ scatter_start = time.perf_counter()
508
+ force = _performance._scatter_sum(
509
+ self.force_values,
510
+ self.force_dofs_flat,
511
+ self.total_dofs,
512
+ )
513
+ self.timings.force_scatter_seconds += time.perf_counter() - scatter_start
514
+
515
+ tangent_matrix: Optional[sparse.csr_matrix]
516
+ if tangent:
517
+ scatter_start = time.perf_counter()
518
+ csr_data = _performance._scatter_sum(
519
+ self.tangent_values,
520
+ self.tangent_scatter,
521
+ self.nnz,
522
+ )
523
+ tangent_matrix = sparse.csr_matrix(
524
+ (csr_data, self.csr_indices, self.csr_indptr),
525
+ shape=(self.total_dofs, self.total_dofs),
526
+ copy=False,
527
+ )
528
+ self.timings.tangent_scatter_seconds += time.perf_counter() - scatter_start
529
+ else:
530
+ tangent_matrix = None
531
+
532
+ self.timings.total_seconds += time.perf_counter() - start_total
533
+ return force, tangent_matrix, trial_states
534
+
535
+
536
+ def _batch_b_plan_diagnostics(self) -> Dict[str, Any]:
537
+ diagnostics = _ORIGINAL_PLAN_DIAGNOSTICS(self)
538
+ elastic_batches = [
539
+ batch
540
+ for batch in self.shell_batches
541
+ if getattr(batch, "_batch_b_elastic", False)
542
+ ]
543
+ diagnostics.update(
544
+ {
545
+ "batch_b_installed": True,
546
+ "elastic_fast_path_batch_count": len(elastic_batches),
547
+ "elastic_fast_path_element_count": int(
548
+ sum(batch.element_ids.size for batch in elastic_batches)
549
+ ),
550
+ "plastic_batch_count": int(len(self.shell_batches) - len(elastic_batches)),
551
+ "elastic_constitutive_state_bytes": int(
552
+ sum(
553
+ batch.elastic_states[0]["plastic_strain"].nbytes
554
+ + batch.elastic_states[0]["alpha"].nbytes
555
+ for batch in elastic_batches
556
+ if batch.elastic_states
557
+ )
558
+ ),
559
+ }
560
+ )
561
+ return diagnostics
562
+
563
+
564
+ def install_batch_b_optimizations() -> bool:
565
+ global _INSTALLED
566
+ if _INSTALLED:
567
+ return True
568
+ _performance._ShellBatchPlan.build = classmethod(_batch_b_shell_build)
569
+ _performance.NonlinearAssemblyPlan.assemble = _batch_b_plan_assemble
570
+ _performance.NonlinearAssemblyPlan.diagnostics = _batch_b_plan_diagnostics
571
+ _performance.clear_nonlinear_assembly_cache()
572
+ _INSTALLED = True
573
+ return True
574
+
575
+
576
+ def uninstall_batch_b_optimizations() -> None:
577
+ global _INSTALLED
578
+ if not _INSTALLED:
579
+ return
580
+ _performance._ShellBatchPlan.build = classmethod(_ORIGINAL_BATCH_BUILD)
581
+ _performance.NonlinearAssemblyPlan.assemble = _ORIGINAL_PLAN_ASSEMBLE
582
+ _performance.NonlinearAssemblyPlan.diagnostics = _ORIGINAL_PLAN_DIAGNOSTICS
583
+ _performance.clear_nonlinear_assembly_cache()
584
+ _INSTALLED = False
585
+
586
+
587
+ def batch_b_status() -> Dict[str, Any]:
588
+ return {
589
+ "installed": bool(_INSTALLED),
590
+ "description": "in-place elastic shell assembly into persistent buffers",
591
+ "parallel_kernel": True,
592
+ }