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,760 @@
1
+ """Constraint-aware direct reduced-coordinate nonlinear assembly core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Mapping, Optional, Tuple
9
+
10
+ import numpy as np
11
+ from scipy import sparse
12
+
13
+ from .jit_compiler import njit
14
+
15
+
16
+ @njit(cache=True)
17
+ def _scatter_reduced_values(
18
+ local_values: np.ndarray,
19
+ unit_positions: np.ndarray,
20
+ weighted_sources: np.ndarray,
21
+ weighted_positions: np.ndarray,
22
+ weighted_coefficients: np.ndarray,
23
+ output: np.ndarray,
24
+ ) -> None:
25
+ """Scatter local values into a retained reduced-coordinate output buffer."""
26
+
27
+ output.fill(0.0)
28
+ for source in range(unit_positions.size):
29
+ position = int(unit_positions[source])
30
+ if position >= 0:
31
+ output[position] += local_values[source]
32
+ for index in range(weighted_sources.size):
33
+ output[int(weighted_positions[index])] += (
34
+ local_values[int(weighted_sources[index])] * weighted_coefficients[index]
35
+ )
36
+
37
+
38
+ @dataclass
39
+ class ReducedAssemblyTimings:
40
+ builds: int = 0
41
+ assemblies: int = 0
42
+ residual_only_assemblies: int = 0
43
+ local_response_seconds: float = 0.0
44
+ reduced_force_scatter_seconds: float = 0.0
45
+ reduced_tangent_scatter_seconds: float = 0.0
46
+ total_seconds: float = 0.0
47
+
48
+ def to_dict(self) -> Dict[str, Any]:
49
+ return {
50
+ "builds": int(self.builds),
51
+ "assemblies": int(self.assemblies),
52
+ "residual_only_assemblies": int(self.residual_only_assemblies),
53
+ "local_response_seconds": float(self.local_response_seconds),
54
+ "reduced_force_scatter_seconds": float(self.reduced_force_scatter_seconds),
55
+ "reduced_tangent_scatter_seconds": float(self.reduced_tangent_scatter_seconds),
56
+ "total_seconds": float(self.total_seconds),
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class ReducedAssemblyPlan:
62
+ """Precomputed direct scatter from element-local values to reduced CSR data."""
63
+
64
+ transformation: sparse.csr_matrix
65
+ total_dofs: int
66
+ reduced_dofs: int
67
+ force_unit_positions: np.ndarray
68
+ force_weighted_sources: np.ndarray
69
+ force_weighted_positions: np.ndarray
70
+ force_weighted_coefficients: np.ndarray
71
+ tangent_unit_positions: np.ndarray
72
+ tangent_weighted_sources: np.ndarray
73
+ tangent_weighted_positions: np.ndarray
74
+ tangent_weighted_coefficients: np.ndarray
75
+ csr_indptr: np.ndarray
76
+ csr_indices: np.ndarray
77
+ force_buffer: np.ndarray
78
+ tangent_buffer: np.ndarray
79
+ tangent_matrix: sparse.csr_matrix
80
+ setup_seconds: float
81
+ force_contributions: int
82
+ tangent_contributions: int
83
+ tangent_expansion_ratio: float
84
+ estimated_map_bytes: int
85
+ mapping_kind: str
86
+ source_plan: Any = field(repr=False)
87
+ timings: ReducedAssemblyTimings = field(default_factory=ReducedAssemblyTimings)
88
+
89
+ @property
90
+ def nnz(self) -> int:
91
+ return int(self.csr_indices.size)
92
+
93
+ def diagnostics(self) -> Dict[str, Any]:
94
+ return {
95
+ "total_dofs": int(self.total_dofs),
96
+ "reduced_dofs": int(self.reduced_dofs),
97
+ "reduction_ratio": float(self.reduced_dofs / max(self.total_dofs, 1)),
98
+ "reduced_tangent_nnz": self.nnz,
99
+ "force_contributions": int(self.force_contributions),
100
+ "tangent_contributions": int(self.tangent_contributions),
101
+ "tangent_expansion_ratio": float(self.tangent_expansion_ratio),
102
+ "estimated_map_bytes": int(self.estimated_map_bytes),
103
+ "mapping_kind": str(self.mapping_kind),
104
+ "setup_seconds": float(self.setup_seconds),
105
+ "timings": self.timings.to_dict(),
106
+ }
107
+
108
+
109
+ class ReducedAssemblyPlanLimit(RuntimeError):
110
+ """Raised when a direct reduced scatter map exceeds the configured memory cap."""
111
+
112
+
113
+ def _maximum_map_bytes() -> int:
114
+ raw = os.environ.get("FE_SOLVER_BATCH_C_MAX_MAP_MB", "512").strip()
115
+ try:
116
+ megabytes = max(float(raw), 1.0)
117
+ except ValueError:
118
+ megabytes = 512.0
119
+ return int(megabytes * 1024.0 * 1024.0)
120
+
121
+
122
+ def _identity_transformation(transformation: sparse.spmatrix) -> bool:
123
+ T = sparse.csr_matrix(transformation, dtype=float)
124
+ if T.shape[0] != T.shape[1] or T.nnz != T.shape[0]:
125
+ return False
126
+ T.sum_duplicates()
127
+ T.sort_indices()
128
+ expected = np.arange(T.shape[0], dtype=T.indices.dtype)
129
+ return bool(
130
+ np.array_equal(T.indptr, np.arange(T.shape[0] + 1))
131
+ and np.array_equal(T.indices, expected)
132
+ and np.all(T.data == 1.0)
133
+ )
134
+
135
+
136
+ def _preflight_reduced_map_bytes(
137
+ nonlinear_plan: Any,
138
+ transformation: sparse.csr_matrix,
139
+ ) -> Tuple[int, int]:
140
+ """Estimate retained map memory before allocating expanded MPC arrays."""
141
+
142
+ force_dofs = np.asarray(nonlinear_plan.force_dofs_flat, dtype=np.intp)
143
+ row_nnz = np.diff(transformation.indptr)
144
+ force_counts = row_nnz[force_dofs]
145
+ force_unit = np.zeros(force_counts.size, dtype=bool)
146
+ force_simple = np.flatnonzero(force_counts == 1)
147
+ if force_simple.size:
148
+ pointers = transformation.indptr[force_dofs[force_simple]]
149
+ force_unit[force_simple] = transformation.data[pointers] == 1.0
150
+ force_contributions = int(np.sum(force_counts, dtype=np.int64))
151
+ force_weighted = force_contributions - int(np.count_nonzero(force_unit))
152
+
153
+ source_positions = np.asarray(nonlinear_plan.tangent_scatter, dtype=np.intp)
154
+ source_rows = np.searchsorted(
155
+ np.asarray(nonlinear_plan.csr_indptr, dtype=np.intp),
156
+ source_positions,
157
+ side="right",
158
+ ) - 1
159
+ source_columns = nonlinear_plan.csr_indices[source_positions]
160
+ tangent_counts = (
161
+ row_nnz[source_rows].astype(np.int64)
162
+ * row_nnz[source_columns].astype(np.int64)
163
+ )
164
+ tangent_contributions = int(np.sum(tangent_counts, dtype=np.int64))
165
+ tangent_unit = np.zeros(tangent_counts.size, dtype=bool)
166
+ simple_sources = np.flatnonzero(tangent_counts == 1)
167
+ if simple_sources.size:
168
+ row_ptr = transformation.indptr[source_rows[simple_sources]]
169
+ column_ptr = transformation.indptr[source_columns[simple_sources]]
170
+ tangent_unit[simple_sources] = (
171
+ transformation.data[row_ptr] * transformation.data[column_ptr]
172
+ ) == 1.0
173
+ tangent_weighted = tangent_contributions - int(np.count_nonzero(tangent_unit))
174
+
175
+ max_row_nnz = int(np.max(row_nnz)) if row_nnz.size else 0
176
+ estimated_reduced_nnz = min(
177
+ tangent_contributions,
178
+ int(nonlinear_plan.nnz) * max(max_row_nnz * max_row_nnz, 1),
179
+ )
180
+ estimated = int(
181
+ 4 * int(nonlinear_plan.force_values.size)
182
+ + 16 * force_weighted
183
+ + 4 * int(nonlinear_plan.tangent_values.size)
184
+ + 16 * tangent_weighted
185
+ + 16 * estimated_reduced_nnz
186
+ + 8 * int(transformation.shape[1])
187
+ + 8 * (int(transformation.shape[1]) + 1)
188
+ )
189
+ return estimated, tangent_contributions
190
+
191
+
192
+ def _append_force_mapping(
193
+ transformation: sparse.csr_matrix,
194
+ full_dofs: np.ndarray,
195
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
196
+ row_counts = np.diff(transformation.indptr)[full_dofs]
197
+ unit_sources: List[np.ndarray] = []
198
+ unit_keys: List[np.ndarray] = []
199
+ weighted_sources: List[np.ndarray] = []
200
+ weighted_keys: List[np.ndarray] = []
201
+ weighted_coefficients: List[np.ndarray] = []
202
+
203
+ simple_sources = np.flatnonzero(row_counts == 1)
204
+ if simple_sources.size:
205
+ pointers = transformation.indptr[full_dofs[simple_sources]]
206
+ columns = transformation.indices[pointers]
207
+ coefficients = transformation.data[pointers]
208
+ unit_mask = coefficients == 1.0
209
+ if np.any(unit_mask):
210
+ unit_sources.append(simple_sources[unit_mask].astype(np.int32, copy=False))
211
+ unit_keys.append(columns[unit_mask].astype(np.int64, copy=False))
212
+ if np.any(~unit_mask):
213
+ weighted_sources.append(
214
+ simple_sources[~unit_mask].astype(np.int32, copy=False)
215
+ )
216
+ weighted_keys.append(columns[~unit_mask].astype(np.int64, copy=False))
217
+ weighted_coefficients.append(
218
+ coefficients[~unit_mask].astype(float, copy=False)
219
+ )
220
+
221
+ complex_sources = np.flatnonzero(row_counts > 1)
222
+ for source in complex_sources:
223
+ full_dof = int(full_dofs[source])
224
+ start = int(transformation.indptr[full_dof])
225
+ stop = int(transformation.indptr[full_dof + 1])
226
+ columns = transformation.indices[start:stop]
227
+ coefficients = transformation.data[start:stop]
228
+ weighted_sources.append(np.full(columns.size, int(source), dtype=np.int32))
229
+ weighted_keys.append(columns.astype(np.int64, copy=False))
230
+ weighted_coefficients.append(coefficients.astype(float, copy=False))
231
+
232
+ return (
233
+ np.concatenate(unit_sources) if unit_sources else np.empty(0, dtype=np.int32),
234
+ np.concatenate(unit_keys) if unit_keys else np.empty(0, dtype=np.int64),
235
+ np.concatenate(weighted_sources)
236
+ if weighted_sources
237
+ else np.empty(0, dtype=np.int32),
238
+ np.concatenate(weighted_keys)
239
+ if weighted_keys
240
+ else np.empty(0, dtype=np.int64),
241
+ np.concatenate(weighted_coefficients)
242
+ if weighted_coefficients
243
+ else np.empty(0, dtype=float),
244
+ )
245
+
246
+
247
+ def _append_tangent_mapping(
248
+ nonlinear_plan: Any,
249
+ transformation: sparse.csr_matrix,
250
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, int]:
251
+ source_positions = np.asarray(nonlinear_plan.tangent_scatter, dtype=np.intp)
252
+ source_rows = np.searchsorted(
253
+ np.asarray(nonlinear_plan.csr_indptr, dtype=np.intp),
254
+ source_positions,
255
+ side="right",
256
+ ) - 1
257
+ source_columns = nonlinear_plan.csr_indices[source_positions]
258
+ row_counts = np.diff(transformation.indptr)[source_rows]
259
+ column_counts = np.diff(transformation.indptr)[source_columns]
260
+ expansion_counts = row_counts.astype(np.int64) * column_counts.astype(np.int64)
261
+ total_contributions = int(np.sum(expansion_counts, dtype=np.int64))
262
+
263
+ unit_sources: List[np.ndarray] = []
264
+ unit_keys: List[np.ndarray] = []
265
+ weighted_sources: List[np.ndarray] = []
266
+ weighted_keys: List[np.ndarray] = []
267
+ weighted_coefficients: List[np.ndarray] = []
268
+ n_reduced = int(transformation.shape[1])
269
+
270
+ simple = (row_counts == 1) & (column_counts == 1)
271
+ if np.any(simple):
272
+ sources = np.flatnonzero(simple)
273
+ row_ptr = transformation.indptr[source_rows[sources]]
274
+ column_ptr = transformation.indptr[source_columns[sources]]
275
+ reduced_rows = transformation.indices[row_ptr]
276
+ reduced_columns = transformation.indices[column_ptr]
277
+ coefficients = transformation.data[row_ptr] * transformation.data[column_ptr]
278
+ keys = (
279
+ reduced_rows.astype(np.int64) * np.int64(n_reduced)
280
+ + reduced_columns.astype(np.int64)
281
+ )
282
+ unit_mask = coefficients == 1.0
283
+ if np.any(unit_mask):
284
+ unit_sources.append(sources[unit_mask].astype(np.int32, copy=False))
285
+ unit_keys.append(keys[unit_mask])
286
+ if np.any(~unit_mask):
287
+ weighted_sources.append(sources[~unit_mask].astype(np.int32, copy=False))
288
+ weighted_keys.append(keys[~unit_mask])
289
+ weighted_coefficients.append(
290
+ coefficients[~unit_mask].astype(float, copy=False)
291
+ )
292
+
293
+ complex_sources = np.flatnonzero((expansion_counts > 0) & ~simple)
294
+ for source in complex_sources:
295
+ row = int(source_rows[source])
296
+ column = int(source_columns[source])
297
+ row_start = int(transformation.indptr[row])
298
+ row_stop = int(transformation.indptr[row + 1])
299
+ column_start = int(transformation.indptr[column])
300
+ column_stop = int(transformation.indptr[column + 1])
301
+ reduced_rows = transformation.indices[row_start:row_stop]
302
+ reduced_columns = transformation.indices[column_start:column_stop]
303
+ row_coefficients = transformation.data[row_start:row_stop]
304
+ column_coefficients = transformation.data[column_start:column_stop]
305
+ count = int(reduced_rows.size * reduced_columns.size)
306
+ keys = (
307
+ np.repeat(reduced_rows, reduced_columns.size).astype(np.int64)
308
+ * np.int64(n_reduced)
309
+ + np.tile(reduced_columns, reduced_rows.size).astype(np.int64)
310
+ )
311
+ coefficients = np.repeat(row_coefficients, reduced_columns.size) * np.tile(
312
+ column_coefficients, reduced_rows.size
313
+ )
314
+ weighted_sources.append(np.full(count, int(source), dtype=np.int32))
315
+ weighted_keys.append(keys)
316
+ weighted_coefficients.append(coefficients.astype(float, copy=False))
317
+
318
+ return (
319
+ np.concatenate(unit_sources) if unit_sources else np.empty(0, dtype=np.int32),
320
+ np.concatenate(unit_keys) if unit_keys else np.empty(0, dtype=np.int64),
321
+ np.concatenate(weighted_sources)
322
+ if weighted_sources
323
+ else np.empty(0, dtype=np.int32),
324
+ np.concatenate(weighted_keys)
325
+ if weighted_keys
326
+ else np.empty(0, dtype=np.int64),
327
+ np.concatenate(weighted_coefficients)
328
+ if weighted_coefficients
329
+ else np.empty(0, dtype=float),
330
+ total_contributions,
331
+ )
332
+
333
+
334
+ def _csr_pattern_from_keys(
335
+ unique_keys: np.ndarray,
336
+ reduced_dofs: int,
337
+ ) -> Tuple[np.ndarray, np.ndarray]:
338
+ if unique_keys.size == 0:
339
+ return (
340
+ np.zeros(reduced_dofs + 1, dtype=np.intp),
341
+ np.empty(0, dtype=np.intp),
342
+ )
343
+ reduced_rows = unique_keys // np.int64(reduced_dofs)
344
+ reduced_columns = unique_keys % np.int64(reduced_dofs)
345
+ row_counts = np.bincount(reduced_rows.astype(np.intp), minlength=reduced_dofs)
346
+ csr_indptr = np.empty(reduced_dofs + 1, dtype=np.intp)
347
+ csr_indptr[0] = 0
348
+ np.cumsum(row_counts, out=csr_indptr[1:])
349
+ return csr_indptr, reduced_columns.astype(np.intp, copy=False)
350
+
351
+
352
+ def _finalize_reduced_plan(
353
+ *,
354
+ nonlinear_plan: Any,
355
+ transformation: sparse.csr_matrix,
356
+ force_unit_positions: np.ndarray,
357
+ force_weighted_sources: np.ndarray,
358
+ force_weighted_positions: np.ndarray,
359
+ force_weighted_coefficients: np.ndarray,
360
+ tangent_unit_positions: np.ndarray,
361
+ tangent_weighted_sources: np.ndarray,
362
+ tangent_weighted_positions: np.ndarray,
363
+ tangent_weighted_coefficients: np.ndarray,
364
+ csr_indptr: np.ndarray,
365
+ csr_indices: np.ndarray,
366
+ force_contributions: int,
367
+ tangent_contributions: int,
368
+ mapping_kind: str,
369
+ setup_start: float,
370
+ ) -> ReducedAssemblyPlan:
371
+ force_buffer = np.zeros(transformation.shape[1], dtype=float)
372
+ tangent_buffer = np.zeros(csr_indices.size, dtype=float)
373
+ tangent_matrix = sparse.csr_matrix(
374
+ (tangent_buffer, csr_indices, csr_indptr),
375
+ shape=(transformation.shape[1], transformation.shape[1]),
376
+ copy=False,
377
+ )
378
+ tangent_buffer = tangent_matrix.data
379
+ estimated_map_bytes = int(
380
+ force_unit_positions.nbytes
381
+ + force_weighted_sources.nbytes
382
+ + force_weighted_positions.nbytes
383
+ + force_weighted_coefficients.nbytes
384
+ + tangent_unit_positions.nbytes
385
+ + tangent_weighted_sources.nbytes
386
+ + tangent_weighted_positions.nbytes
387
+ + tangent_weighted_coefficients.nbytes
388
+ + csr_indptr.nbytes
389
+ + csr_indices.nbytes
390
+ + force_buffer.nbytes
391
+ + tangent_buffer.nbytes
392
+ )
393
+ if estimated_map_bytes > _maximum_map_bytes():
394
+ raise ReducedAssemblyPlanLimit(
395
+ "direct reduced scatter map would require "
396
+ f"{estimated_map_bytes / (1024.0 ** 2):.1f} MiB; "
397
+ "increase FE_SOLVER_BATCH_C_MAX_MAP_MB or retain full-coordinate reduction"
398
+ )
399
+
400
+ plan = ReducedAssemblyPlan(
401
+ transformation=transformation,
402
+ total_dofs=int(transformation.shape[0]),
403
+ reduced_dofs=int(transformation.shape[1]),
404
+ force_unit_positions=force_unit_positions.astype(np.int32, copy=False),
405
+ force_weighted_sources=force_weighted_sources.astype(np.int32, copy=False),
406
+ force_weighted_positions=force_weighted_positions.astype(np.int32, copy=False),
407
+ force_weighted_coefficients=force_weighted_coefficients.astype(float, copy=False),
408
+ tangent_unit_positions=tangent_unit_positions.astype(np.int32, copy=False),
409
+ tangent_weighted_sources=tangent_weighted_sources.astype(np.int32, copy=False),
410
+ tangent_weighted_positions=tangent_weighted_positions.astype(np.int32, copy=False),
411
+ tangent_weighted_coefficients=tangent_weighted_coefficients.astype(float, copy=False),
412
+ csr_indptr=csr_indptr,
413
+ csr_indices=csr_indices,
414
+ force_buffer=force_buffer,
415
+ tangent_buffer=tangent_buffer,
416
+ tangent_matrix=tangent_matrix,
417
+ setup_seconds=float(time.perf_counter() - setup_start),
418
+ force_contributions=int(force_contributions),
419
+ tangent_contributions=int(tangent_contributions),
420
+ tangent_expansion_ratio=float(
421
+ tangent_contributions / max(int(nonlinear_plan.tangent_values.size), 1)
422
+ ),
423
+ estimated_map_bytes=estimated_map_bytes,
424
+ mapping_kind=mapping_kind,
425
+ source_plan=nonlinear_plan,
426
+ )
427
+ plan.timings.builds = 1
428
+ return plan
429
+
430
+
431
+ def _build_selector_reduced_plan(
432
+ nonlinear_plan: Any,
433
+ transformation: sparse.csr_matrix,
434
+ setup_start: float,
435
+ ) -> ReducedAssemblyPlan:
436
+ """Fast setup for fixed-support/selector transformations."""
437
+
438
+ reduced_dofs = int(transformation.shape[1])
439
+ row_counts = np.diff(transformation.indptr)
440
+ full_to_reduced = np.full(transformation.shape[0], -1, dtype=np.int32)
441
+ active_rows = np.flatnonzero(row_counts == 1)
442
+ if active_rows.size:
443
+ pointers = transformation.indptr[active_rows]
444
+ full_to_reduced[active_rows] = transformation.indices[pointers].astype(
445
+ np.int32, copy=False
446
+ )
447
+
448
+ force_unit_positions = full_to_reduced[
449
+ np.asarray(nonlinear_plan.force_dofs_flat, dtype=np.intp)
450
+ ].copy()
451
+
452
+ source_positions = np.asarray(nonlinear_plan.tangent_scatter, dtype=np.intp)
453
+ source_rows = np.searchsorted(
454
+ np.asarray(nonlinear_plan.csr_indptr, dtype=np.intp),
455
+ source_positions,
456
+ side="right",
457
+ ) - 1
458
+ source_columns = np.asarray(nonlinear_plan.csr_indices, dtype=np.intp)[
459
+ source_positions
460
+ ]
461
+ reduced_rows = full_to_reduced[source_rows]
462
+ reduced_columns = full_to_reduced[source_columns]
463
+ valid = (reduced_rows >= 0) & (reduced_columns >= 0)
464
+
465
+ tangent_unit_positions = np.full(
466
+ nonlinear_plan.tangent_values.size, -1, dtype=np.int32
467
+ )
468
+ if np.any(valid):
469
+ keys = (
470
+ reduced_rows[valid].astype(np.int64) * np.int64(reduced_dofs)
471
+ + reduced_columns[valid].astype(np.int64)
472
+ )
473
+ unique_keys, inverse = np.unique(keys, return_inverse=True)
474
+ if unique_keys.size > np.iinfo(np.int32).max:
475
+ raise ReducedAssemblyPlanLimit(
476
+ "reduced tangent pattern exceeds the supported 32-bit scatter index range"
477
+ )
478
+ tangent_unit_positions[valid] = inverse.astype(np.int32, copy=False)
479
+ csr_indptr, csr_indices = _csr_pattern_from_keys(unique_keys, reduced_dofs)
480
+ else:
481
+ csr_indptr, csr_indices = _csr_pattern_from_keys(
482
+ np.empty(0, dtype=np.int64), reduced_dofs
483
+ )
484
+
485
+ empty_i32 = np.empty(0, dtype=np.int32)
486
+ empty_float = np.empty(0, dtype=float)
487
+ return _finalize_reduced_plan(
488
+ nonlinear_plan=nonlinear_plan,
489
+ transformation=transformation,
490
+ force_unit_positions=force_unit_positions,
491
+ force_weighted_sources=empty_i32,
492
+ force_weighted_positions=empty_i32,
493
+ force_weighted_coefficients=empty_float,
494
+ tangent_unit_positions=tangent_unit_positions,
495
+ tangent_weighted_sources=empty_i32,
496
+ tangent_weighted_positions=empty_i32,
497
+ tangent_weighted_coefficients=empty_float,
498
+ csr_indptr=csr_indptr,
499
+ csr_indices=csr_indices,
500
+ force_contributions=int(np.count_nonzero(force_unit_positions >= 0)),
501
+ tangent_contributions=int(np.count_nonzero(valid)),
502
+ mapping_kind="selector",
503
+ setup_start=setup_start,
504
+ )
505
+
506
+
507
+ def _build_weighted_reduced_plan(
508
+ nonlinear_plan: Any,
509
+ transformation: sparse.csr_matrix,
510
+ setup_start: float,
511
+ preflight_tangent_contributions: int,
512
+ ) -> ReducedAssemblyPlan:
513
+ (
514
+ force_unit_sources,
515
+ force_unit_keys,
516
+ force_weighted_sources,
517
+ force_weighted_keys,
518
+ force_weighted_coefficients,
519
+ ) = _append_force_mapping(
520
+ transformation,
521
+ np.asarray(nonlinear_plan.force_dofs_flat, dtype=np.intp),
522
+ )
523
+ (
524
+ tangent_unit_sources,
525
+ tangent_unit_keys,
526
+ tangent_weighted_sources,
527
+ tangent_weighted_keys,
528
+ tangent_weighted_coefficients,
529
+ tangent_contributions,
530
+ ) = _append_tangent_mapping(nonlinear_plan, transformation)
531
+ if tangent_contributions != preflight_tangent_contributions:
532
+ raise RuntimeError(
533
+ "reduced tangent contribution count changed during plan construction"
534
+ )
535
+
536
+ all_tangent_keys = np.concatenate((tangent_unit_keys, tangent_weighted_keys))
537
+ if all_tangent_keys.size:
538
+ unique_keys, inverse = np.unique(all_tangent_keys, return_inverse=True)
539
+ if unique_keys.size > np.iinfo(np.int32).max:
540
+ raise ReducedAssemblyPlanLimit(
541
+ "reduced tangent pattern exceeds the supported 32-bit scatter index range"
542
+ )
543
+ split = tangent_unit_keys.size
544
+ tangent_unit_reduced_positions = inverse[:split]
545
+ tangent_weighted_positions = inverse[split:]
546
+ csr_indptr, csr_indices = _csr_pattern_from_keys(
547
+ unique_keys, int(transformation.shape[1])
548
+ )
549
+ else:
550
+ tangent_unit_reduced_positions = np.empty(0, dtype=np.intp)
551
+ tangent_weighted_positions = np.empty(0, dtype=np.intp)
552
+ csr_indptr, csr_indices = _csr_pattern_from_keys(
553
+ np.empty(0, dtype=np.int64), int(transformation.shape[1])
554
+ )
555
+
556
+ tangent_unit_positions = np.full(
557
+ nonlinear_plan.tangent_values.size, -1, dtype=np.int32
558
+ )
559
+ if tangent_unit_sources.size:
560
+ tangent_unit_positions[tangent_unit_sources] = (
561
+ tangent_unit_reduced_positions.astype(np.int32, copy=False)
562
+ )
563
+
564
+ force_unit_positions = np.full(
565
+ nonlinear_plan.force_values.size, -1, dtype=np.int32
566
+ )
567
+ if force_unit_sources.size:
568
+ force_unit_positions[force_unit_sources] = force_unit_keys.astype(
569
+ np.int32, copy=False
570
+ )
571
+
572
+ return _finalize_reduced_plan(
573
+ nonlinear_plan=nonlinear_plan,
574
+ transformation=transformation,
575
+ force_unit_positions=force_unit_positions,
576
+ force_weighted_sources=force_weighted_sources,
577
+ force_weighted_positions=force_weighted_keys,
578
+ force_weighted_coefficients=force_weighted_coefficients,
579
+ tangent_unit_positions=tangent_unit_positions,
580
+ tangent_weighted_sources=tangent_weighted_sources,
581
+ tangent_weighted_positions=tangent_weighted_positions,
582
+ tangent_weighted_coefficients=tangent_weighted_coefficients,
583
+ csr_indptr=csr_indptr,
584
+ csr_indices=csr_indices,
585
+ force_contributions=int(force_unit_sources.size + force_weighted_sources.size),
586
+ tangent_contributions=int(tangent_contributions),
587
+ mapping_kind="weighted_mpc",
588
+ setup_start=setup_start,
589
+ )
590
+
591
+
592
+ def build_reduced_assembly_plan(
593
+ nonlinear_plan: Any,
594
+ transformation: sparse.spmatrix,
595
+ ) -> ReducedAssemblyPlan:
596
+ """Build a retained direct scatter map for one constraint transformation."""
597
+
598
+ start = time.perf_counter()
599
+ T = sparse.csr_matrix(transformation, dtype=float, copy=True)
600
+ T.sum_duplicates()
601
+ T.eliminate_zeros()
602
+ T.sort_indices()
603
+ if T.shape[0] != nonlinear_plan.total_dofs:
604
+ raise ValueError(
605
+ f"Constraint transformation has {T.shape[0]} rows; "
606
+ f"expected {nonlinear_plan.total_dofs}"
607
+ )
608
+ if T.shape[1] > np.iinfo(np.int32).max:
609
+ raise ReducedAssemblyPlanLimit(
610
+ "reduced system exceeds the supported 32-bit scatter index range"
611
+ )
612
+
613
+ estimated_preflight_bytes, preflight_tangent_contributions = (
614
+ _preflight_reduced_map_bytes(nonlinear_plan, T)
615
+ )
616
+ if estimated_preflight_bytes > _maximum_map_bytes():
617
+ raise ReducedAssemblyPlanLimit(
618
+ "direct reduced scatter map is estimated to require "
619
+ f"{estimated_preflight_bytes / (1024.0 ** 2):.1f} MiB; "
620
+ "increase FE_SOLVER_BATCH_C_MAX_MAP_MB or retain full-coordinate reduction"
621
+ )
622
+
623
+ row_counts = np.diff(T.indptr)
624
+ selector = bool(
625
+ (row_counts.size == 0 or int(np.max(row_counts)) <= 1)
626
+ and np.all(T.data == 1.0)
627
+ )
628
+ if selector:
629
+ return _build_selector_reduced_plan(nonlinear_plan, T, start)
630
+ return _build_weighted_reduced_plan(
631
+ nonlinear_plan,
632
+ T,
633
+ start,
634
+ preflight_tangent_contributions,
635
+ )
636
+
637
+
638
+ def _evaluate_local_responses(
639
+ nonlinear_plan: Any,
640
+ displacements: np.ndarray,
641
+ committed_states: Mapping[int, Any],
642
+ tangent: bool,
643
+ ) -> Tuple[Dict[int, Any], float]:
644
+ """Fill the persistent element-local buffers without global sparse assembly."""
645
+
646
+ start = time.perf_counter()
647
+ nonlinear_plan.force_values.fill(0.0)
648
+ if tangent:
649
+ nonlinear_plan.tangent_values.fill(0.0)
650
+ trial_states: Dict[int, Any] = {}
651
+
652
+ for batch in nonlinear_plan.shell_batches:
653
+ force_batch, tangent_batch, batch_states, kernel_seconds = batch.evaluate(
654
+ displacements,
655
+ committed_states,
656
+ tangent,
657
+ )
658
+ nonlinear_plan.timings.shell_kernel_seconds += kernel_seconds
659
+ nonlinear_plan.force_values[batch.force_positions.reshape(-1)] = np.asarray(
660
+ force_batch, dtype=float
661
+ ).reshape(-1)
662
+ if tangent and tangent_batch is not None:
663
+ nonlinear_plan.tangent_values[
664
+ batch.tangent_positions.reshape(-1)
665
+ ] = np.asarray(tangent_batch, dtype=float).reshape(-1)
666
+ trial_states.update(batch_states)
667
+
668
+ non_shell_start = time.perf_counter()
669
+ model = nonlinear_plan.model
670
+ mesh = model.mesh
671
+ for record in nonlinear_plan.non_shell_elements:
672
+ material = model.get_material(record.element.material_name)
673
+ element_displacement = np.asarray(displacements, dtype=float)[
674
+ record.dof_mapping
675
+ ]
676
+ force_element, tangent_element, trial_state = (
677
+ record.element.compute_nonlinear_response(
678
+ mesh,
679
+ material,
680
+ element_displacement,
681
+ committed_states.get(record.element_id),
682
+ nonlinear_plan.num_layers,
683
+ tangent,
684
+ )
685
+ )
686
+ nonlinear_plan.force_values[record.force_positions] = np.asarray(
687
+ force_element, dtype=float
688
+ ).reshape(-1)
689
+ if tangent and tangent_element is not None:
690
+ nonlinear_plan.tangent_values[record.tangent_positions] = np.asarray(
691
+ tangent_element, dtype=float
692
+ ).reshape(-1)
693
+ if trial_state is not None:
694
+ trial_states[record.element_id] = trial_state
695
+ nonlinear_plan.timings.non_shell_seconds += time.perf_counter() - non_shell_start
696
+ return trial_states, time.perf_counter() - start
697
+
698
+
699
+ def assemble_reduced_system(
700
+ nonlinear_plan: Any,
701
+ reduced_plan: ReducedAssemblyPlan,
702
+ displacements: np.ndarray,
703
+ committed_states: Mapping[int, Any],
704
+ tangent: bool = True,
705
+ ) -> Tuple[np.ndarray, Optional[sparse.csr_matrix], Dict[int, Any]]:
706
+ """Assemble reduced internal force and tangent directly from local buffers."""
707
+
708
+ with nonlinear_plan._lock:
709
+ start_total = time.perf_counter()
710
+ nonlinear_plan.timings.calls += 1
711
+ reduced_plan.timings.assemblies += 1
712
+ if tangent:
713
+ nonlinear_plan.timings.tangent_calls += 1
714
+ else:
715
+ nonlinear_plan.timings.residual_only_calls += 1
716
+ reduced_plan.timings.residual_only_assemblies += 1
717
+
718
+ trial_states, local_seconds = _evaluate_local_responses(
719
+ nonlinear_plan,
720
+ displacements,
721
+ committed_states,
722
+ tangent,
723
+ )
724
+ reduced_plan.timings.local_response_seconds += local_seconds
725
+
726
+ scatter_start = time.perf_counter()
727
+ _scatter_reduced_values(
728
+ nonlinear_plan.force_values,
729
+ reduced_plan.force_unit_positions,
730
+ reduced_plan.force_weighted_sources,
731
+ reduced_plan.force_weighted_positions,
732
+ reduced_plan.force_weighted_coefficients,
733
+ reduced_plan.force_buffer,
734
+ )
735
+ force_seconds = time.perf_counter() - scatter_start
736
+ reduced_plan.timings.reduced_force_scatter_seconds += force_seconds
737
+ nonlinear_plan.timings.force_scatter_seconds += force_seconds
738
+
739
+ tangent_matrix: Optional[sparse.csr_matrix]
740
+ if tangent:
741
+ scatter_start = time.perf_counter()
742
+ _scatter_reduced_values(
743
+ nonlinear_plan.tangent_values,
744
+ reduced_plan.tangent_unit_positions,
745
+ reduced_plan.tangent_weighted_sources,
746
+ reduced_plan.tangent_weighted_positions,
747
+ reduced_plan.tangent_weighted_coefficients,
748
+ reduced_plan.tangent_buffer,
749
+ )
750
+ tangent_seconds = time.perf_counter() - scatter_start
751
+ reduced_plan.timings.reduced_tangent_scatter_seconds += tangent_seconds
752
+ nonlinear_plan.timings.tangent_scatter_seconds += tangent_seconds
753
+ tangent_matrix = reduced_plan.tangent_matrix
754
+ else:
755
+ tangent_matrix = None
756
+
757
+ elapsed = time.perf_counter() - start_total
758
+ reduced_plan.timings.total_seconds += elapsed
759
+ nonlinear_plan.timings.total_seconds += elapsed
760
+ return reduced_plan.force_buffer, tangent_matrix, trial_states