ANYsolver 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anysolver/__init__.py +631 -0
- anysolver/anystructure_fem_mode.py +942 -0
- anysolver/arc_length.py +758 -0
- anysolver/assembly.py +950 -0
- anysolver/baselines.py +303 -0
- anysolver/beam_shell_verification.py +4276 -0
- anysolver/beam_validity.py +110 -0
- anysolver/benchmarks.py +495 -0
- anysolver/boundary.py +476 -0
- anysolver/buckling.py +442 -0
- anysolver/buckling_validity.py +91 -0
- anysolver/capacity_workflow.py +350 -0
- anysolver/cases.py +173 -0
- anysolver/composite_strip_verification.py +297 -0
- anysolver/contact.py +3461 -0
- anysolver/corotational.py +371 -0
- anysolver/cylinder_benchmarks.py +364 -0
- anysolver/dynamics.py +723 -0
- anysolver/element_qualification.py +432 -0
- anysolver/elements.py +2724 -0
- anysolver/external_references.py +369 -0
- anysolver/fe_core.py +327 -0
- anysolver/fracture.py +551 -0
- anysolver/imperfections.py +390 -0
- anysolver/jit_compiler.py +108 -0
- anysolver/kernel_warmup.py +180 -0
- anysolver/linalg.py +760 -0
- anysolver/mass_properties.py +179 -0
- anysolver/material_curves.py +231 -0
- anysolver/matrix_assembly.py +558 -0
- anysolver/mesh_gen.py +1065 -0
- anysolver/mesh_load_bc_verification.py +609 -0
- anysolver/modal.py +282 -0
- anysolver/nonlinear.py +300 -0
- anysolver/nonlinear_performance.py +920 -0
- anysolver/nonlinear_performance_batch_b.py +592 -0
- anysolver/nonlinear_performance_batch_c.py +506 -0
- anysolver/nonlinear_performance_bootstrap.py +120 -0
- anysolver/nonlinear_reduced_assembly.py +760 -0
- anysolver/nonlinear_static.py +1518 -0
- anysolver/plasticity.py +419 -0
- anysolver/plasticity_qualification.py +314 -0
- anysolver/production_readiness.py +304 -0
- anysolver/quality_control.py +1497 -0
- anysolver/recovery.py +505 -0
- anysolver/recovery_policy.py +205 -0
- anysolver/reference_cases.py +425 -0
- anysolver/results.py +503 -0
- anysolver/runtime.py +9030 -0
- anysolver/s4_validity.py +326 -0
- anysolver/sesam_fem/__init__.py +55 -0
- anysolver/sesam_fem/__main__.py +80 -0
- anysolver/sesam_fem/diagnostics.py +65 -0
- anysolver/sesam_fem/document.py +814 -0
- anysolver/sesam_fem/exporter.py +84 -0
- anysolver/sesam_fem/importer.py +365 -0
- anysolver/sesam_fem/records.py +257 -0
- anysolver/sesam_fem/schema.py +107 -0
- anysolver/sesam_fem/sif_importer.py +397 -0
- anysolver/sesam_fem/validation.py +62 -0
- anysolver/shell_benchmarks.py +367 -0
- anysolver/test_cases.py +610 -0
- anysolver/validation.py +416 -0
- anysolver/vectorized_nonlinear.py +334 -0
- anysolver/vectorized_stiffness.py +571 -0
- anysolver-0.1.0.dist-info/METADATA +165 -0
- anysolver-0.1.0.dist-info/RECORD +70 -0
- anysolver-0.1.0.dist-info/WHEEL +5 -0
- anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
- anysolver-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""Explicit stiffness, mass and load assembly APIs.
|
|
2
|
+
|
|
3
|
+
This module is the step-3 public assembly interface. It keeps K, M and F
|
|
4
|
+
assembly separate so modal, buckling and nonlinear solvers can choose exactly
|
|
5
|
+
which matrices they need without side effects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Mapping, Optional, Sequence, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from scipy import sparse
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .boundary import LoadCase
|
|
20
|
+
from .fe_core import FEModel
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AssemblyError(ValueError):
|
|
24
|
+
"""Raised when an element returns an invalid matrix or load contribution."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _base_info(model: "FEModel", matrix_type: str) -> Dict[str, Any]:
|
|
28
|
+
mesh = model.mesh
|
|
29
|
+
return {
|
|
30
|
+
"matrix_type": matrix_type,
|
|
31
|
+
"num_elements": 0,
|
|
32
|
+
"num_nodes": mesh.num_nodes,
|
|
33
|
+
"total_dofs": mesh.dof_manager.total_dofs,
|
|
34
|
+
"assembly_time": 0.0,
|
|
35
|
+
"element_times": {},
|
|
36
|
+
"skipped_elements": [],
|
|
37
|
+
"diagnostics": {},
|
|
38
|
+
"revision_signature": getattr(mesh, "revision_signature", lambda: {})(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _check_element_matrix_shape(element_id: int, matrix_name: str, matrix: np.ndarray, expected_size: int) -> np.ndarray:
|
|
43
|
+
matrix = np.asarray(matrix, dtype=float)
|
|
44
|
+
expected_shape = (expected_size, expected_size)
|
|
45
|
+
if matrix.shape != expected_shape:
|
|
46
|
+
raise AssemblyError(
|
|
47
|
+
f"Element {element_id} returned {matrix_name} with shape {matrix.shape}; "
|
|
48
|
+
f"expected {expected_shape}."
|
|
49
|
+
)
|
|
50
|
+
if not np.all(np.isfinite(matrix)):
|
|
51
|
+
raise AssemblyError(f"Element {element_id} returned non-finite values in {matrix_name}.")
|
|
52
|
+
return matrix
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _relative_symmetry_error(matrix: sparse.spmatrix | np.ndarray) -> float:
|
|
56
|
+
if sparse.issparse(matrix):
|
|
57
|
+
diff = matrix - matrix.T
|
|
58
|
+
numerator = float(sparse.linalg.norm(diff))
|
|
59
|
+
denominator = max(float(sparse.linalg.norm(matrix)), 1.0)
|
|
60
|
+
return numerator / denominator
|
|
61
|
+
dense = np.asarray(matrix, dtype=float)
|
|
62
|
+
return float(np.linalg.norm(dense - dense.T) / max(np.linalg.norm(dense), 1.0))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _topology_signature(mesh: Any, matrix_type: str) -> str:
|
|
66
|
+
revisions = getattr(mesh, "revision_signature", lambda: {})()
|
|
67
|
+
cache_key = (
|
|
68
|
+
str(matrix_type),
|
|
69
|
+
int(revisions.get("topology", 0)),
|
|
70
|
+
int(revisions.get("mpc", 0)),
|
|
71
|
+
)
|
|
72
|
+
cache = getattr(mesh, "_topology_signature_cache", None)
|
|
73
|
+
if cache is None:
|
|
74
|
+
cache = {}
|
|
75
|
+
mesh._topology_signature_cache = cache
|
|
76
|
+
cached = cache.get(cache_key)
|
|
77
|
+
if cached is not None:
|
|
78
|
+
return str(cached)
|
|
79
|
+
|
|
80
|
+
payload = {
|
|
81
|
+
"matrix_type": matrix_type,
|
|
82
|
+
"topology_revision": revisions.get("topology", 0),
|
|
83
|
+
"mpc_revision": revisions.get("mpc", 0),
|
|
84
|
+
"elements": [
|
|
85
|
+
{
|
|
86
|
+
"id": int(elem_id),
|
|
87
|
+
"class": element.__class__.__name__,
|
|
88
|
+
"node_ids": [int(node_id) for node_id in getattr(element, "node_ids", [])],
|
|
89
|
+
"dofs": [int(dof) for dof in element.get_dof_mapping(mesh)],
|
|
90
|
+
}
|
|
91
|
+
for elem_id, element in mesh.elements.items()
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
text = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
95
|
+
signature = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
96
|
+
cache[cache_key] = signature
|
|
97
|
+
return signature
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _scatter_element_matrix(
|
|
101
|
+
element_matrix: np.ndarray,
|
|
102
|
+
dof_mapping: np.ndarray,
|
|
103
|
+
rows: list,
|
|
104
|
+
cols: list,
|
|
105
|
+
data: list,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Append element matrix entries to COO triplet buffers (vectorized)."""
|
|
108
|
+
n_local = dof_mapping.size
|
|
109
|
+
values = element_matrix.ravel()
|
|
110
|
+
mask = values != 0.0
|
|
111
|
+
if not np.any(mask):
|
|
112
|
+
return
|
|
113
|
+
rows.append(np.repeat(dof_mapping, n_local)[mask])
|
|
114
|
+
cols.append(np.tile(dof_mapping, n_local)[mask])
|
|
115
|
+
data.append(values[mask])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _triplets_to_csr(rows: list, cols: list, data: list, total_dofs: int) -> sparse.csr_matrix:
|
|
119
|
+
"""Build a CSR matrix from COO triplet buffers; duplicates are summed."""
|
|
120
|
+
if not data:
|
|
121
|
+
return sparse.csr_matrix((total_dofs, total_dofs), dtype=float)
|
|
122
|
+
coo = sparse.coo_matrix(
|
|
123
|
+
(np.concatenate(data), (np.concatenate(rows), np.concatenate(cols))),
|
|
124
|
+
shape=(total_dofs, total_dofs),
|
|
125
|
+
dtype=float,
|
|
126
|
+
)
|
|
127
|
+
return coo.tocsr()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _get_cached_sparsity_pattern(mesh: "FEMesh", matrix_type: str) -> Tuple[np.ndarray, np.ndarray]:
|
|
131
|
+
"""Retrieve or build the cached row and column indices for global matrix COO assembly."""
|
|
132
|
+
if not hasattr(mesh, "_sparsity_cache"):
|
|
133
|
+
mesh._sparsity_cache = {}
|
|
134
|
+
|
|
135
|
+
signature = _topology_signature(mesh, matrix_type)
|
|
136
|
+
|
|
137
|
+
if matrix_type in mesh._sparsity_cache:
|
|
138
|
+
cached = mesh._sparsity_cache[matrix_type]
|
|
139
|
+
if cached.get("signature") == signature:
|
|
140
|
+
return cached["rows"], cached["cols"]
|
|
141
|
+
|
|
142
|
+
rows_list = []
|
|
143
|
+
cols_list = []
|
|
144
|
+
for _, element in mesh.elements.items():
|
|
145
|
+
dof_mapping = np.asarray(element.get_dof_mapping(mesh), dtype=np.intp)
|
|
146
|
+
if dof_mapping.size == 0:
|
|
147
|
+
continue
|
|
148
|
+
n_local = dof_mapping.size
|
|
149
|
+
rows_list.append(np.repeat(dof_mapping, n_local))
|
|
150
|
+
cols_list.append(np.tile(dof_mapping, n_local))
|
|
151
|
+
|
|
152
|
+
rows_concat = np.concatenate(rows_list) if rows_list else np.empty(0, dtype=np.intp)
|
|
153
|
+
cols_concat = np.concatenate(cols_list) if cols_list else np.empty(0, dtype=np.intp)
|
|
154
|
+
|
|
155
|
+
mesh._sparsity_cache[matrix_type] = {
|
|
156
|
+
"rows": rows_concat,
|
|
157
|
+
"cols": cols_concat,
|
|
158
|
+
"signature": signature,
|
|
159
|
+
}
|
|
160
|
+
return rows_concat, cols_concat
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _assemble_element_matrix(
|
|
164
|
+
model: "FEModel",
|
|
165
|
+
matrix_type: str,
|
|
166
|
+
element_matrix_getter: Callable[[Any, Any, Any], np.ndarray],
|
|
167
|
+
) -> Tuple[sparse.csr_matrix, Dict[str, Any]]:
|
|
168
|
+
mesh = model.mesh
|
|
169
|
+
total_dofs = mesh.dof_manager.total_dofs
|
|
170
|
+
info = _base_info(model, matrix_type)
|
|
171
|
+
start_time = time.time()
|
|
172
|
+
|
|
173
|
+
# Precompute shell matrices in a JIT-compiled batch for stiffness and mass assembly
|
|
174
|
+
precomputed = {}
|
|
175
|
+
vectorized_shell_groups = []
|
|
176
|
+
if matrix_type in {"stiffness", "mass"}:
|
|
177
|
+
from .elements import ShellElement
|
|
178
|
+
from .jit_compiler import JIT_ENABLED, JIT_DISABLED_REASON, jit_diagnostics
|
|
179
|
+
from .vectorized_stiffness import compute_shell_mass_matrices_jit, compute_shell_stiffness_matrices_jit
|
|
180
|
+
|
|
181
|
+
groups = {}
|
|
182
|
+
for elem_id, element in mesh.elements.items():
|
|
183
|
+
if (
|
|
184
|
+
isinstance(element, ShellElement)
|
|
185
|
+
and getattr(element, "_is_quadrilateral", False)
|
|
186
|
+
and not (getattr(element, "_is_8node", False) and bool(getattr(element, "reduced_integration", False)))
|
|
187
|
+
):
|
|
188
|
+
key = (
|
|
189
|
+
element.num_nodes,
|
|
190
|
+
element.thickness,
|
|
191
|
+
element.drilling_stabilization,
|
|
192
|
+
element.reduced_integration,
|
|
193
|
+
element.hourglass_stabilization,
|
|
194
|
+
element.material_name,
|
|
195
|
+
)
|
|
196
|
+
if key not in groups:
|
|
197
|
+
groups[key] = []
|
|
198
|
+
groups[key].append((elem_id, element))
|
|
199
|
+
|
|
200
|
+
for key, elem_list in groups.items():
|
|
201
|
+
num_nodes, thickness, drilling_stabilization, _reduced_integration, _hourglass_stabilization, material_name = key
|
|
202
|
+
material = model.get_material(material_name)
|
|
203
|
+
E = float(material.elastic_modulus)
|
|
204
|
+
nu = float(material.poisson_ratio)
|
|
205
|
+
G = float(material.shear_modulus)
|
|
206
|
+
|
|
207
|
+
n_elem = len(elem_list)
|
|
208
|
+
coords_all = np.zeros((n_elem, num_nodes, 3))
|
|
209
|
+
for idx, (elem_id, element) in enumerate(elem_list):
|
|
210
|
+
coords_all[idx] = element.get_node_coordinates(mesh)
|
|
211
|
+
|
|
212
|
+
first_element = elem_list[0][1]
|
|
213
|
+
is_4node = first_element._is_4node
|
|
214
|
+
gauss_points = first_element.gauss_points
|
|
215
|
+
gauss_weights = first_element.gauss_weights
|
|
216
|
+
|
|
217
|
+
if matrix_type == "mass":
|
|
218
|
+
kernel_name = "compute_shell_mass_matrices_jit"
|
|
219
|
+
batched = compute_shell_mass_matrices_jit(
|
|
220
|
+
coords_all,
|
|
221
|
+
is_4node,
|
|
222
|
+
thickness,
|
|
223
|
+
float(material.density),
|
|
224
|
+
gauss_points,
|
|
225
|
+
gauss_weights,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
kernel_name = "compute_shell_stiffness_matrices_jit"
|
|
229
|
+
if is_4node:
|
|
230
|
+
shear_points = np.empty((0, 2))
|
|
231
|
+
shear_weights = np.empty(0)
|
|
232
|
+
else:
|
|
233
|
+
shear_points = first_element.shear_gauss_points
|
|
234
|
+
shear_weights = first_element.shear_gauss_weights
|
|
235
|
+
batched = compute_shell_stiffness_matrices_jit(
|
|
236
|
+
coords_all,
|
|
237
|
+
is_4node,
|
|
238
|
+
thickness,
|
|
239
|
+
drilling_stabilization,
|
|
240
|
+
E,
|
|
241
|
+
nu,
|
|
242
|
+
G,
|
|
243
|
+
gauss_points,
|
|
244
|
+
gauss_weights,
|
|
245
|
+
shear_points,
|
|
246
|
+
shear_weights,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
for idx, (elem_id, element) in enumerate(elem_list):
|
|
250
|
+
precomputed[elem_id] = batched[idx]
|
|
251
|
+
jit_info = jit_diagnostics()
|
|
252
|
+
vectorized_shell_groups.append(
|
|
253
|
+
{
|
|
254
|
+
"shell_order": "S4" if is_4node else "Q8",
|
|
255
|
+
"num_elements": int(n_elem),
|
|
256
|
+
"num_nodes": int(num_nodes),
|
|
257
|
+
"material": str(material_name),
|
|
258
|
+
"thickness": float(thickness),
|
|
259
|
+
"jit_enabled": bool(JIT_ENABLED),
|
|
260
|
+
"jit_disabled_reason": JIT_DISABLED_REASON,
|
|
261
|
+
"kernel": kernel_name,
|
|
262
|
+
"parallel_kernel": True,
|
|
263
|
+
"parallel_threads": jit_info.get("num_threads"),
|
|
264
|
+
"backend": jit_info.get("backend"),
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Retrieve or build cached sparsity pattern
|
|
269
|
+
rows_concat, cols_concat = _get_cached_sparsity_pattern(mesh, matrix_type)
|
|
270
|
+
|
|
271
|
+
data_list = []
|
|
272
|
+
for elem_id, element in mesh.elements.items():
|
|
273
|
+
elem_start = time.time()
|
|
274
|
+
material = model.get_material(element.material_name)
|
|
275
|
+
dof_mapping = np.asarray(element.get_dof_mapping(mesh), dtype=np.intp)
|
|
276
|
+
if dof_mapping.size == 0:
|
|
277
|
+
info["skipped_elements"].append(int(elem_id))
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
if elem_id in precomputed:
|
|
281
|
+
element_matrix = precomputed[elem_id]
|
|
282
|
+
else:
|
|
283
|
+
element_matrix = element_matrix_getter(element, mesh, material)
|
|
284
|
+
|
|
285
|
+
element_matrix = _check_element_matrix_shape(
|
|
286
|
+
int(elem_id),
|
|
287
|
+
matrix_type,
|
|
288
|
+
element_matrix,
|
|
289
|
+
int(dof_mapping.size),
|
|
290
|
+
)
|
|
291
|
+
if matrix_type in {"stiffness", "mass", "geometric_stiffness"}:
|
|
292
|
+
local_symmetry = _relative_symmetry_error(element_matrix)
|
|
293
|
+
if local_symmetry > 1.0e-8:
|
|
294
|
+
raise AssemblyError(
|
|
295
|
+
f"Element {elem_id} returned nonsymmetric {matrix_type}; "
|
|
296
|
+
f"relative symmetry error {local_symmetry:.3e}."
|
|
297
|
+
)
|
|
298
|
+
data_list.append(np.asarray(element_matrix, dtype=float).ravel())
|
|
299
|
+
|
|
300
|
+
info["element_times"][int(elem_id)] = time.time() - elem_start
|
|
301
|
+
info["num_elements"] += 1
|
|
302
|
+
|
|
303
|
+
if not data_list:
|
|
304
|
+
matrix = sparse.csr_matrix((total_dofs, total_dofs), dtype=float)
|
|
305
|
+
info["diagnostics"]["assembled_symmetry_error"] = 0.0
|
|
306
|
+
info["sparsity_signature"] = _topology_signature(mesh, matrix_type)
|
|
307
|
+
info["assembly_time"] = time.time() - start_time
|
|
308
|
+
return matrix, info
|
|
309
|
+
|
|
310
|
+
data_concat = np.concatenate(data_list)
|
|
311
|
+
coo = sparse.coo_matrix(
|
|
312
|
+
(data_concat, (rows_concat, cols_concat)),
|
|
313
|
+
shape=(total_dofs, total_dofs),
|
|
314
|
+
dtype=float,
|
|
315
|
+
)
|
|
316
|
+
matrix = coo.tocsr()
|
|
317
|
+
info["diagnostics"]["assembled_symmetry_error"] = _relative_symmetry_error(matrix)
|
|
318
|
+
if matrix_type in {"stiffness", "mass"}:
|
|
319
|
+
info["diagnostics"]["vectorized_shell_groups"] = vectorized_shell_groups
|
|
320
|
+
info["diagnostics"]["vectorized_shell_element_count"] = int(len(precomputed))
|
|
321
|
+
info["diagnostics"]["scalar_shell_element_count"] = int(info["num_elements"] - len(precomputed))
|
|
322
|
+
info["sparsity_signature"] = _topology_signature(mesh, matrix_type)
|
|
323
|
+
info["assembly_time"] = time.time() - start_time
|
|
324
|
+
return matrix, info
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def assemble_stiffness_matrix(model: "FEModel") -> Tuple[sparse.csr_matrix, Dict[str, Any]]:
|
|
328
|
+
"""Assemble the global stiffness matrix K only."""
|
|
329
|
+
return _assemble_element_matrix(
|
|
330
|
+
model,
|
|
331
|
+
"stiffness",
|
|
332
|
+
lambda element, mesh, material: element.compute_stiffness_matrix(mesh, material),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def assemble_mass_matrix(model: "FEModel") -> Tuple[sparse.csr_matrix, Dict[str, Any]]:
|
|
337
|
+
"""Assemble the global mass matrix M only, including any added point masses."""
|
|
338
|
+
matrix, info = _assemble_element_matrix(
|
|
339
|
+
model,
|
|
340
|
+
"mass",
|
|
341
|
+
lambda element, mesh, material: element.compute_mass_matrix(mesh, material),
|
|
342
|
+
)
|
|
343
|
+
matrix = _add_point_masses_to_matrix(model, matrix)
|
|
344
|
+
info["diagnostics"]["point_mass_count"] = int(len(getattr(model.mesh, "point_masses", {}) or {}))
|
|
345
|
+
return matrix, info
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _add_point_masses_to_matrix(model: "FEModel", matrix: sparse.csr_matrix) -> sparse.csr_matrix:
|
|
349
|
+
"""Add lumped point masses to the translational-DOF diagonal of ``matrix``."""
|
|
350
|
+
point_masses = getattr(model.mesh, "point_masses", None)
|
|
351
|
+
if not point_masses:
|
|
352
|
+
return matrix
|
|
353
|
+
total_dofs = model.mesh.dof_manager.total_dofs
|
|
354
|
+
diagonal = np.zeros(total_dofs, dtype=float)
|
|
355
|
+
for node_id, mass in point_masses.items():
|
|
356
|
+
node = model.mesh.get_node(int(node_id))
|
|
357
|
+
if node is None or float(mass) == 0.0:
|
|
358
|
+
continue
|
|
359
|
+
for axis in range(3):
|
|
360
|
+
diagonal[node.dofs[axis]] += float(mass)
|
|
361
|
+
if not diagonal.any():
|
|
362
|
+
return matrix
|
|
363
|
+
return (matrix + sparse.diags(diagonal, 0, shape=(total_dofs, total_dofs), format="csr")).tocsr()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _get_element_state(element_states: Optional[Any], element_id: int, element: Any) -> Any:
|
|
367
|
+
if element_states is None:
|
|
368
|
+
return None
|
|
369
|
+
if callable(element_states):
|
|
370
|
+
try:
|
|
371
|
+
return element_states(element_id, element)
|
|
372
|
+
except TypeError:
|
|
373
|
+
return element_states(element_id)
|
|
374
|
+
if isinstance(element_states, Mapping):
|
|
375
|
+
if element_id in element_states:
|
|
376
|
+
return element_states[element_id]
|
|
377
|
+
element_id_text = str(element_id)
|
|
378
|
+
if element_id_text in element_states:
|
|
379
|
+
return element_states[element_id_text]
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def assemble_geometric_stiffness_matrix(
|
|
384
|
+
model: "FEModel",
|
|
385
|
+
element_states: Optional[Any] = None,
|
|
386
|
+
) -> Tuple[sparse.csr_matrix, Dict[str, Any]]:
|
|
387
|
+
"""Assemble the global geometric stiffness matrix KG only.
|
|
388
|
+
|
|
389
|
+
``element_states`` supplies the reference stress/resultant state for each
|
|
390
|
+
element. The current beam-column implementation accepts a numeric value or
|
|
391
|
+
a mapping with ``axial_compression`` positive in compression.
|
|
392
|
+
"""
|
|
393
|
+
mesh = model.mesh
|
|
394
|
+
total_dofs = mesh.dof_manager.total_dofs
|
|
395
|
+
info = _base_info(model, "geometric_stiffness")
|
|
396
|
+
start_time = time.time()
|
|
397
|
+
|
|
398
|
+
# Retrieve or build cached sparsity pattern
|
|
399
|
+
rows_concat, cols_concat = _get_cached_sparsity_pattern(mesh, "geometric_stiffness")
|
|
400
|
+
|
|
401
|
+
data_list = []
|
|
402
|
+
for elem_id, element in mesh.elements.items():
|
|
403
|
+
elem_start = time.time()
|
|
404
|
+
material = model.get_material(element.material_name)
|
|
405
|
+
dof_mapping = np.asarray(element.get_dof_mapping(mesh), dtype=np.intp)
|
|
406
|
+
if dof_mapping.size == 0:
|
|
407
|
+
info["skipped_elements"].append(int(elem_id))
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
state = _get_element_state(element_states, int(elem_id), element)
|
|
411
|
+
getter = getattr(element, "compute_geometric_stiffness_matrix", None)
|
|
412
|
+
if getter is None:
|
|
413
|
+
element_matrix = np.zeros((dof_mapping.size, dof_mapping.size), dtype=float)
|
|
414
|
+
else:
|
|
415
|
+
element_matrix = getter(mesh, material, state)
|
|
416
|
+
element_matrix = _check_element_matrix_shape(
|
|
417
|
+
int(elem_id),
|
|
418
|
+
"geometric_stiffness",
|
|
419
|
+
element_matrix,
|
|
420
|
+
int(dof_mapping.size),
|
|
421
|
+
)
|
|
422
|
+
data_list.append(np.asarray(element_matrix, dtype=float).ravel())
|
|
423
|
+
|
|
424
|
+
info["element_times"][int(elem_id)] = time.time() - elem_start
|
|
425
|
+
info["num_elements"] += 1
|
|
426
|
+
|
|
427
|
+
info["state_source"] = "none" if element_states is None else type(element_states).__name__
|
|
428
|
+
|
|
429
|
+
if not data_list:
|
|
430
|
+
matrix = sparse.csr_matrix((total_dofs, total_dofs), dtype=float)
|
|
431
|
+
info["diagnostics"]["assembled_symmetry_error"] = 0.0
|
|
432
|
+
info["sparsity_signature"] = _topology_signature(mesh, "geometric_stiffness")
|
|
433
|
+
info["assembly_time"] = time.time() - start_time
|
|
434
|
+
return matrix, info
|
|
435
|
+
|
|
436
|
+
data_concat = np.concatenate(data_list)
|
|
437
|
+
coo = sparse.coo_matrix(
|
|
438
|
+
(data_concat, (rows_concat, cols_concat)),
|
|
439
|
+
shape=(total_dofs, total_dofs),
|
|
440
|
+
dtype=float,
|
|
441
|
+
)
|
|
442
|
+
matrix = coo.tocsr()
|
|
443
|
+
info["diagnostics"]["assembled_symmetry_error"] = _relative_symmetry_error(matrix)
|
|
444
|
+
info["sparsity_signature"] = _topology_signature(mesh, "geometric_stiffness")
|
|
445
|
+
info["assembly_time"] = time.time() - start_time
|
|
446
|
+
return matrix, info
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def assemble_load_vector(model: "FEModel", load_case: Optional["LoadCase"] = None) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
450
|
+
"""Assemble the global external load vector F only."""
|
|
451
|
+
total_dofs = model.mesh.dof_manager.total_dofs
|
|
452
|
+
start_time = time.time()
|
|
453
|
+
if load_case is None:
|
|
454
|
+
load_vector = np.zeros(total_dofs, dtype=float)
|
|
455
|
+
load_name = None
|
|
456
|
+
else:
|
|
457
|
+
load_vector = load_case.get_load_vector(model.mesh, model.mesh.dof_manager, model.get_material)
|
|
458
|
+
load_vector = np.asarray(load_vector, dtype=float).reshape(-1)
|
|
459
|
+
load_name = load_case.name
|
|
460
|
+
|
|
461
|
+
if load_vector.shape != (total_dofs,):
|
|
462
|
+
raise AssemblyError(f"Load vector shape {load_vector.shape} does not match total DOFs {(total_dofs,)}.")
|
|
463
|
+
if not np.all(np.isfinite(load_vector)):
|
|
464
|
+
raise AssemblyError(f"Load case {load_name!r} produced non-finite load vector values.")
|
|
465
|
+
|
|
466
|
+
return load_vector, {
|
|
467
|
+
"vector_type": "load",
|
|
468
|
+
"load_case": load_name,
|
|
469
|
+
"num_nodes": model.mesh.num_nodes,
|
|
470
|
+
"total_dofs": total_dofs,
|
|
471
|
+
"assembly_time": time.time() - start_time,
|
|
472
|
+
"load_norm": float(np.linalg.norm(load_vector)),
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def assemble_load_matrix(
|
|
477
|
+
model: "FEModel",
|
|
478
|
+
load_cases: Sequence[Optional["LoadCase"]],
|
|
479
|
+
) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
480
|
+
"""Assemble a dense load matrix with one column per load case."""
|
|
481
|
+
start = time.time()
|
|
482
|
+
vectors = []
|
|
483
|
+
infos = []
|
|
484
|
+
names = []
|
|
485
|
+
for load_case in load_cases:
|
|
486
|
+
vector, info = assemble_load_vector(model, load_case)
|
|
487
|
+
vectors.append(vector)
|
|
488
|
+
infos.append(info)
|
|
489
|
+
names.append(None if load_case is None else load_case.name)
|
|
490
|
+
total_dofs = model.mesh.dof_manager.total_dofs
|
|
491
|
+
matrix = np.column_stack(vectors) if vectors else np.zeros((total_dofs, 0), dtype=float)
|
|
492
|
+
return matrix, {
|
|
493
|
+
"vector_type": "load_matrix",
|
|
494
|
+
"load_cases": names,
|
|
495
|
+
"num_load_cases": len(names),
|
|
496
|
+
"total_dofs": total_dofs,
|
|
497
|
+
"assembly_time": time.time() - start,
|
|
498
|
+
"columns": infos,
|
|
499
|
+
"load_norms": [float(np.linalg.norm(matrix[:, idx])) for idx in range(matrix.shape[1])],
|
|
500
|
+
"revision_signature": getattr(model.mesh, "revision_signature", lambda: {})(),
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def assemble_damping_matrix(
|
|
505
|
+
model: "FEModel",
|
|
506
|
+
rayleigh_alpha: float = 0.0,
|
|
507
|
+
rayleigh_beta: float = 0.0,
|
|
508
|
+
) -> Tuple[sparse.csr_matrix, Dict[str, Any]]:
|
|
509
|
+
"""Assemble Rayleigh damping C = alpha M + beta K."""
|
|
510
|
+
start = time.time()
|
|
511
|
+
M, mass_info = assemble_mass_matrix(model)
|
|
512
|
+
K, stiffness_info = assemble_stiffness_matrix(model)
|
|
513
|
+
C = (float(rayleigh_alpha) * M + float(rayleigh_beta) * K).tocsr()
|
|
514
|
+
return C, {
|
|
515
|
+
"matrix_type": "damping",
|
|
516
|
+
"rayleigh_alpha": float(rayleigh_alpha),
|
|
517
|
+
"rayleigh_beta": float(rayleigh_beta),
|
|
518
|
+
"mass": mass_info,
|
|
519
|
+
"stiffness": stiffness_info,
|
|
520
|
+
"assembly_time": time.time() - start,
|
|
521
|
+
"diagnostics": {"assembled_symmetry_error": _relative_symmetry_error(C)},
|
|
522
|
+
"revision_signature": getattr(model.mesh, "revision_signature", lambda: {})(),
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def assemble_system(
|
|
527
|
+
model: "FEModel",
|
|
528
|
+
load_case: Optional["LoadCase"] = None,
|
|
529
|
+
include_mass: bool = False,
|
|
530
|
+
) -> Tuple[sparse.csr_matrix, np.ndarray, Dict[str, Any]]:
|
|
531
|
+
"""Compatibility wrapper returning K, F and assembly metadata.
|
|
532
|
+
|
|
533
|
+
The mass matrix is assembled separately and returned in info["mass_matrix"]
|
|
534
|
+
only when include_mass is true. It is never added to stiffness.
|
|
535
|
+
"""
|
|
536
|
+
start_time = time.time()
|
|
537
|
+
K, stiffness_info = assemble_stiffness_matrix(model)
|
|
538
|
+
F, load_info = assemble_load_vector(model, load_case)
|
|
539
|
+
|
|
540
|
+
info: Dict[str, Any] = {
|
|
541
|
+
"num_elements": stiffness_info["num_elements"],
|
|
542
|
+
"num_nodes": model.mesh.num_nodes,
|
|
543
|
+
"total_dofs": model.mesh.dof_manager.total_dofs,
|
|
544
|
+
"includes_mass_matrix": bool(include_mass),
|
|
545
|
+
"assembly_time": 0.0,
|
|
546
|
+
"stiffness": stiffness_info,
|
|
547
|
+
"load": load_info,
|
|
548
|
+
# Backwards-compatible keys used by older diagnostics/tests.
|
|
549
|
+
"element_times": stiffness_info.get("element_times", {}),
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if include_mass:
|
|
553
|
+
M, mass_info = assemble_mass_matrix(model)
|
|
554
|
+
info["mass_matrix"] = M
|
|
555
|
+
info["mass"] = mass_info
|
|
556
|
+
|
|
557
|
+
info["assembly_time"] = time.time() - start_time
|
|
558
|
+
return K, F, info
|