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
anysolver/recovery.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""Selective result recovery and resource-policy helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .fe_core import FEModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_DOF_COMPONENTS = ("ux", "uy", "uz", "rx", "ry", "rz")
|
|
17
|
+
_HISTORY_MODES = {"full", "selected", "envelope"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _optional_int_tuple(values: Optional[Sequence[int]]) -> Optional[Tuple[int, ...]]:
|
|
21
|
+
if values is None:
|
|
22
|
+
return None
|
|
23
|
+
return tuple(int(value) for value in values)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _optional_str_tuple(values: Optional[Sequence[str]]) -> Optional[Tuple[str, ...]]:
|
|
27
|
+
if values is None:
|
|
28
|
+
return None
|
|
29
|
+
return tuple(str(value) for value in values)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class RecoveryConfig:
|
|
34
|
+
"""Requested result-recovery scope.
|
|
35
|
+
|
|
36
|
+
``None`` for node or element ids means recover all available items, matching
|
|
37
|
+
the legacy result behavior. Components filter stress/result dictionaries by
|
|
38
|
+
key; displacement arrays remain six-DOF node vectors.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
node_ids: Optional[Sequence[int]] = None
|
|
42
|
+
element_ids: Optional[Sequence[int]] = None
|
|
43
|
+
components: Optional[Sequence[str]] = None
|
|
44
|
+
include_displacements: bool = True
|
|
45
|
+
include_stresses: bool = True
|
|
46
|
+
include_reactions: bool = True
|
|
47
|
+
history_mode: str = "full"
|
|
48
|
+
store_full_histories: bool = True
|
|
49
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if self.history_mode not in _HISTORY_MODES:
|
|
53
|
+
raise ValueError(f"history_mode must be one of {sorted(_HISTORY_MODES)}")
|
|
54
|
+
|
|
55
|
+
def selected_node_ids(self, model: "FEModel") -> Tuple[int, ...]:
|
|
56
|
+
if self.node_ids is None:
|
|
57
|
+
return tuple(int(node_id) for node_id in model.mesh.nodes)
|
|
58
|
+
missing = [int(node_id) for node_id in self.node_ids if int(node_id) not in model.mesh.nodes]
|
|
59
|
+
if missing:
|
|
60
|
+
raise ValueError(f"Requested recovery node ids not found: {missing}")
|
|
61
|
+
return _optional_int_tuple(self.node_ids) or ()
|
|
62
|
+
|
|
63
|
+
def selected_element_ids(self, model: "FEModel") -> Tuple[int, ...]:
|
|
64
|
+
if self.element_ids is None:
|
|
65
|
+
return tuple(int(element_id) for element_id in model.mesh.elements)
|
|
66
|
+
missing = [int(element_id) for element_id in self.element_ids if int(element_id) not in model.mesh.elements]
|
|
67
|
+
if missing:
|
|
68
|
+
raise ValueError(f"Requested recovery element ids not found: {missing}")
|
|
69
|
+
return _optional_int_tuple(self.element_ids) or ()
|
|
70
|
+
|
|
71
|
+
def selected_components(self) -> Optional[Tuple[str, ...]]:
|
|
72
|
+
return _optional_str_tuple(self.components)
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"node_ids": None if self.node_ids is None else list(_optional_int_tuple(self.node_ids) or ()),
|
|
77
|
+
"element_ids": None if self.element_ids is None else list(_optional_int_tuple(self.element_ids) or ()),
|
|
78
|
+
"components": None if self.components is None else list(_optional_str_tuple(self.components) or ()),
|
|
79
|
+
"include_displacements": bool(self.include_displacements),
|
|
80
|
+
"include_stresses": bool(self.include_stresses),
|
|
81
|
+
"include_reactions": bool(self.include_reactions),
|
|
82
|
+
"history_mode": self.history_mode,
|
|
83
|
+
"store_full_histories": bool(self.store_full_histories),
|
|
84
|
+
"metadata": dict(self.metadata),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class ResourceConfig:
|
|
90
|
+
"""Bounded resource policy for solver phases.
|
|
91
|
+
|
|
92
|
+
This batch records requested limits and deterministic behavior. It does not
|
|
93
|
+
force parallel execution; later measured-parallelism work can consume the
|
|
94
|
+
same contract.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
solver_threads: Optional[int] = None
|
|
98
|
+
assembly_threads: Optional[int] = None
|
|
99
|
+
recovery_threads: Optional[int] = None
|
|
100
|
+
process_workers: Optional[int] = None
|
|
101
|
+
deterministic: bool = True
|
|
102
|
+
memory_limit_bytes: Optional[int] = None
|
|
103
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
104
|
+
|
|
105
|
+
def __post_init__(self) -> None:
|
|
106
|
+
for name in ("solver_threads", "assembly_threads", "recovery_threads", "process_workers"):
|
|
107
|
+
value = getattr(self, name)
|
|
108
|
+
if value is not None and int(value) <= 0:
|
|
109
|
+
raise ValueError(f"{name} must be positive when provided")
|
|
110
|
+
if self.memory_limit_bytes is not None and int(self.memory_limit_bytes) <= 0:
|
|
111
|
+
raise ValueError("memory_limit_bytes must be positive when provided")
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
114
|
+
return {
|
|
115
|
+
"solver_threads": None if self.solver_threads is None else int(self.solver_threads),
|
|
116
|
+
"assembly_threads": None if self.assembly_threads is None else int(self.assembly_threads),
|
|
117
|
+
"recovery_threads": None if self.recovery_threads is None else int(self.recovery_threads),
|
|
118
|
+
"process_workers": None if self.process_workers is None else int(self.process_workers),
|
|
119
|
+
"deterministic": bool(self.deterministic),
|
|
120
|
+
"memory_limit_bytes": None if self.memory_limit_bytes is None else int(self.memory_limit_bytes),
|
|
121
|
+
"metadata": dict(self.metadata),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ResourcePolicyError(ValueError):
|
|
126
|
+
"""Raised when a requested resource policy cannot be satisfied."""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
message: str,
|
|
131
|
+
*,
|
|
132
|
+
context: str,
|
|
133
|
+
memory_estimate: Optional["MemoryEstimate"] = None,
|
|
134
|
+
resource_config: Optional[ResourceConfig] = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
super().__init__(message)
|
|
137
|
+
self.context = context
|
|
138
|
+
self.memory_estimate = memory_estimate
|
|
139
|
+
self.resource_config = resource_config
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
142
|
+
return {
|
|
143
|
+
"context": self.context,
|
|
144
|
+
"message": str(self),
|
|
145
|
+
"memory_estimate": None if self.memory_estimate is None else self.memory_estimate.to_dict(),
|
|
146
|
+
"resource_config": None if self.resource_config is None else self.resource_config.to_dict(),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class MemoryEstimate:
|
|
152
|
+
"""Conservative byte estimates for common FE storage blocks."""
|
|
153
|
+
|
|
154
|
+
total_dofs: int
|
|
155
|
+
num_nodes: int
|
|
156
|
+
num_elements: int
|
|
157
|
+
matrix_nnz_estimate: int
|
|
158
|
+
csr_bytes_estimate: int
|
|
159
|
+
rhs_bytes_estimate: int
|
|
160
|
+
history_bytes_estimate: int
|
|
161
|
+
eigenvector_bytes_estimate: int
|
|
162
|
+
nonlinear_state_bytes_estimate: int
|
|
163
|
+
total_bytes_estimate: int
|
|
164
|
+
notes: Tuple[str, ...] = ()
|
|
165
|
+
|
|
166
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
167
|
+
return {
|
|
168
|
+
"total_dofs": int(self.total_dofs),
|
|
169
|
+
"num_nodes": int(self.num_nodes),
|
|
170
|
+
"num_elements": int(self.num_elements),
|
|
171
|
+
"matrix_nnz_estimate": int(self.matrix_nnz_estimate),
|
|
172
|
+
"csr_bytes_estimate": int(self.csr_bytes_estimate),
|
|
173
|
+
"rhs_bytes_estimate": int(self.rhs_bytes_estimate),
|
|
174
|
+
"history_bytes_estimate": int(self.history_bytes_estimate),
|
|
175
|
+
"eigenvector_bytes_estimate": int(self.eigenvector_bytes_estimate),
|
|
176
|
+
"nonlinear_state_bytes_estimate": int(self.nonlinear_state_bytes_estimate),
|
|
177
|
+
"total_bytes_estimate": int(self.total_bytes_estimate),
|
|
178
|
+
"notes": list(self.notes),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class RecoveryExecutionReport:
|
|
184
|
+
"""Execution diagnostics for a recovery phase."""
|
|
185
|
+
|
|
186
|
+
phase: str
|
|
187
|
+
item_count: int
|
|
188
|
+
requested_workers: int
|
|
189
|
+
used_workers: int
|
|
190
|
+
backend: str
|
|
191
|
+
deterministic: bool
|
|
192
|
+
elapsed_seconds: float
|
|
193
|
+
reason: str = ""
|
|
194
|
+
|
|
195
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
196
|
+
return {
|
|
197
|
+
"phase": self.phase,
|
|
198
|
+
"item_count": int(self.item_count),
|
|
199
|
+
"requested_workers": int(self.requested_workers),
|
|
200
|
+
"used_workers": int(self.used_workers),
|
|
201
|
+
"backend": self.backend,
|
|
202
|
+
"deterministic": bool(self.deterministic),
|
|
203
|
+
"elapsed_seconds": float(self.elapsed_seconds),
|
|
204
|
+
"reason": self.reason,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def default_recovery_config(config: Optional[RecoveryConfig] = None) -> RecoveryConfig:
|
|
209
|
+
"""Return a full-recovery config when none is supplied."""
|
|
210
|
+
|
|
211
|
+
return config if config is not None else RecoveryConfig()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _estimate_matrix_nnz(model: "FEModel") -> int:
|
|
215
|
+
"""Assembled-matrix nonzero count, cached on the mesh topology revision.
|
|
216
|
+
|
|
217
|
+
Only element connectivity determines the sparsity, so the (topological)
|
|
218
|
+
count is cached and reused across the preflight and recovery memory checks
|
|
219
|
+
of a solve, and across repeated solves on the same mesh. The union of
|
|
220
|
+
per-element DOF-pair blocks is computed vectorized rather than with a
|
|
221
|
+
Python set of tuples.
|
|
222
|
+
"""
|
|
223
|
+
mesh = model.mesh
|
|
224
|
+
signature = mesh.revision_signature()
|
|
225
|
+
cached = getattr(mesh, "_matrix_nnz_cache", None)
|
|
226
|
+
if cached is not None and cached[0] == signature:
|
|
227
|
+
return cached[1]
|
|
228
|
+
total_dofs = int(mesh.dof_manager.total_dofs)
|
|
229
|
+
encoded_blocks = []
|
|
230
|
+
for element in mesh.elements.values():
|
|
231
|
+
try:
|
|
232
|
+
mapping = np.asarray(element.get_dof_mapping(mesh), dtype=np.int64).reshape(-1)
|
|
233
|
+
except Exception:
|
|
234
|
+
continue
|
|
235
|
+
if mapping.size == 0:
|
|
236
|
+
continue
|
|
237
|
+
rows = np.repeat(mapping, mapping.size)
|
|
238
|
+
cols = np.tile(mapping, mapping.size)
|
|
239
|
+
encoded_blocks.append(rows * total_dofs + cols)
|
|
240
|
+
if encoded_blocks:
|
|
241
|
+
matrix_nnz = int(np.unique(np.concatenate(encoded_blocks)).size)
|
|
242
|
+
else:
|
|
243
|
+
matrix_nnz = 0
|
|
244
|
+
mesh._matrix_nnz_cache = (signature, matrix_nnz)
|
|
245
|
+
return matrix_nnz
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def estimate_model_memory(
|
|
249
|
+
model: "FEModel",
|
|
250
|
+
*,
|
|
251
|
+
num_rhs: int = 1,
|
|
252
|
+
num_modes: int = 0,
|
|
253
|
+
transient_saved_steps: int = 0,
|
|
254
|
+
store_full_history: bool = True,
|
|
255
|
+
recovery_config: Optional[RecoveryConfig] = None,
|
|
256
|
+
nonlinear_state: bool = False,
|
|
257
|
+
) -> MemoryEstimate:
|
|
258
|
+
"""Estimate matrix/result storage for a model and recovery request."""
|
|
259
|
+
|
|
260
|
+
total_dofs = int(model.mesh.dof_manager.total_dofs)
|
|
261
|
+
num_nodes = int(len(model.mesh.nodes))
|
|
262
|
+
num_elements = int(len(model.mesh.elements))
|
|
263
|
+
notes = []
|
|
264
|
+
matrix_nnz = _estimate_matrix_nnz(model)
|
|
265
|
+
if matrix_nnz == 0 and total_dofs:
|
|
266
|
+
matrix_nnz = total_dofs
|
|
267
|
+
notes.append("matrix sparsity estimated as diagonal because no element mapping was available")
|
|
268
|
+
|
|
269
|
+
csr_bytes = int(matrix_nnz * (8 + 4) + (total_dofs + 1) * 4)
|
|
270
|
+
rhs_bytes = int(max(int(num_rhs), 0) * total_dofs * 8)
|
|
271
|
+
|
|
272
|
+
recovery = default_recovery_config(recovery_config)
|
|
273
|
+
if transient_saved_steps > 0:
|
|
274
|
+
if recovery.history_mode == "envelope":
|
|
275
|
+
selected_history_dofs = 6 * len(recovery.selected_node_ids(model)) if recovery.node_ids is not None and recovery.include_displacements else 0
|
|
276
|
+
history_bytes = int(total_dofs * 8 * 3 + max(int(transient_saved_steps), 0) * selected_history_dofs * 8)
|
|
277
|
+
elif store_full_history and recovery.store_full_histories:
|
|
278
|
+
history_dofs = total_dofs
|
|
279
|
+
history_bytes = int(max(int(transient_saved_steps), 0) * max(history_dofs, 0) * 8 * 3)
|
|
280
|
+
else:
|
|
281
|
+
history_dofs = 6 * len(recovery.selected_node_ids(model)) if recovery.include_displacements else 0
|
|
282
|
+
history_bytes = int(max(int(transient_saved_steps), 0) * max(history_dofs, 0) * 8 * 3)
|
|
283
|
+
else:
|
|
284
|
+
history_bytes = 0
|
|
285
|
+
|
|
286
|
+
eigenvector_bytes = int(max(int(num_modes), 0) * total_dofs * 8)
|
|
287
|
+
if nonlinear_state:
|
|
288
|
+
nonlinear_bytes = int(max(num_elements, 0) * 8 * 64)
|
|
289
|
+
else:
|
|
290
|
+
nonlinear_bytes = 0
|
|
291
|
+
total = int(csr_bytes + rhs_bytes + history_bytes + eigenvector_bytes + nonlinear_bytes)
|
|
292
|
+
return MemoryEstimate(
|
|
293
|
+
total_dofs=total_dofs,
|
|
294
|
+
num_nodes=num_nodes,
|
|
295
|
+
num_elements=num_elements,
|
|
296
|
+
matrix_nnz_estimate=matrix_nnz,
|
|
297
|
+
csr_bytes_estimate=csr_bytes,
|
|
298
|
+
rhs_bytes_estimate=rhs_bytes,
|
|
299
|
+
history_bytes_estimate=history_bytes,
|
|
300
|
+
eigenvector_bytes_estimate=eigenvector_bytes,
|
|
301
|
+
nonlinear_state_bytes_estimate=nonlinear_bytes,
|
|
302
|
+
total_bytes_estimate=total,
|
|
303
|
+
notes=tuple(notes),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def enforce_memory_limit(
|
|
308
|
+
memory_estimate: MemoryEstimate,
|
|
309
|
+
resource_config: Optional[ResourceConfig],
|
|
310
|
+
*,
|
|
311
|
+
context: str,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Raise when an estimate exceeds ``ResourceConfig.memory_limit_bytes``."""
|
|
314
|
+
|
|
315
|
+
if resource_config is None or resource_config.memory_limit_bytes is None:
|
|
316
|
+
return
|
|
317
|
+
limit = int(resource_config.memory_limit_bytes)
|
|
318
|
+
estimated = int(memory_estimate.total_bytes_estimate)
|
|
319
|
+
if estimated > limit:
|
|
320
|
+
raise ResourcePolicyError(
|
|
321
|
+
f"{context} estimated memory {estimated} bytes exceeds configured limit {limit} bytes",
|
|
322
|
+
context=context,
|
|
323
|
+
memory_estimate=memory_estimate,
|
|
324
|
+
resource_config=resource_config,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def select_node_displacements(
|
|
329
|
+
model: "FEModel",
|
|
330
|
+
displacements: np.ndarray,
|
|
331
|
+
recovery_config: Optional[RecoveryConfig] = None,
|
|
332
|
+
) -> Dict[int, np.ndarray]:
|
|
333
|
+
"""Extract selected nodal displacement vectors."""
|
|
334
|
+
|
|
335
|
+
recovery = default_recovery_config(recovery_config)
|
|
336
|
+
if not recovery.include_displacements:
|
|
337
|
+
return {}
|
|
338
|
+
vector = np.asarray(displacements, dtype=float)
|
|
339
|
+
selected: Dict[int, np.ndarray] = {}
|
|
340
|
+
for node_id in recovery.selected_node_ids(model):
|
|
341
|
+
node = model.mesh.nodes[int(node_id)]
|
|
342
|
+
selected[int(node_id)] = vector[np.asarray(node.dofs, dtype=np.intp)]
|
|
343
|
+
return selected
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _filter_components(values: Mapping[str, Any], components: Optional[Tuple[str, ...]]) -> Dict[str, Any]:
|
|
347
|
+
if components is None:
|
|
348
|
+
return dict(values)
|
|
349
|
+
wanted = set(components)
|
|
350
|
+
return {key: value for key, value in values.items() if str(key) in wanted}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _recovery_worker_count(resource_config: Optional[ResourceConfig], item_count: int) -> Tuple[int, int, str]:
|
|
354
|
+
requested = 1 if resource_config is None or resource_config.recovery_threads is None else int(resource_config.recovery_threads)
|
|
355
|
+
if item_count <= 1:
|
|
356
|
+
return requested, 1, "serial: item count <= 1"
|
|
357
|
+
if requested <= 1:
|
|
358
|
+
return requested, 1, "serial: recovery_threads not requested"
|
|
359
|
+
return requested, min(requested, int(item_count)), "thread_pool"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _ordered_element_ids(model: "FEModel", selected: Sequence[int]) -> Tuple[int, ...]:
|
|
363
|
+
wanted = {int(element_id) for element_id in selected}
|
|
364
|
+
return tuple(int(element_id) for element_id in model.mesh.elements if int(element_id) in wanted)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _compute_one_element_stress(
|
|
368
|
+
model: "FEModel",
|
|
369
|
+
displacements: np.ndarray,
|
|
370
|
+
element_id: int,
|
|
371
|
+
*,
|
|
372
|
+
return_global: bool,
|
|
373
|
+
) -> Optional[Tuple[int, Dict[str, np.ndarray]]]:
|
|
374
|
+
element = model.mesh.elements[int(element_id)]
|
|
375
|
+
material = model.get_material(element.material_name)
|
|
376
|
+
dof_mapping = np.asarray(element.get_dof_mapping(model.mesh), dtype=np.intp)
|
|
377
|
+
if dof_mapping.size == 0 or int(dof_mapping.max()) >= displacements.size:
|
|
378
|
+
return None
|
|
379
|
+
try:
|
|
380
|
+
return int(element_id), element.compute_stresses(
|
|
381
|
+
model.mesh,
|
|
382
|
+
displacements[dof_mapping],
|
|
383
|
+
material,
|
|
384
|
+
return_global=return_global,
|
|
385
|
+
)
|
|
386
|
+
except (IndexError, ValueError):
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def recover_element_stresses_with_report(
|
|
391
|
+
model: "FEModel",
|
|
392
|
+
displacements: np.ndarray,
|
|
393
|
+
recovery_config: Optional[RecoveryConfig] = None,
|
|
394
|
+
*,
|
|
395
|
+
return_global: bool = False,
|
|
396
|
+
resource_config: Optional[ResourceConfig] = None,
|
|
397
|
+
) -> Tuple[Dict[int, Dict[str, np.ndarray]], RecoveryExecutionReport]:
|
|
398
|
+
"""Recover element stresses and return bounded execution diagnostics."""
|
|
399
|
+
|
|
400
|
+
recovery = default_recovery_config(recovery_config)
|
|
401
|
+
if not recovery.include_stresses:
|
|
402
|
+
report = RecoveryExecutionReport(
|
|
403
|
+
phase="element_stress_recovery",
|
|
404
|
+
item_count=0,
|
|
405
|
+
requested_workers=1 if resource_config is None or resource_config.recovery_threads is None else int(resource_config.recovery_threads),
|
|
406
|
+
used_workers=0,
|
|
407
|
+
backend="disabled",
|
|
408
|
+
deterministic=True if resource_config is None else bool(resource_config.deterministic),
|
|
409
|
+
elapsed_seconds=0.0,
|
|
410
|
+
reason="stress recovery disabled",
|
|
411
|
+
)
|
|
412
|
+
return {}, report
|
|
413
|
+
|
|
414
|
+
selected_ids = _ordered_element_ids(model, recovery.selected_element_ids(model))
|
|
415
|
+
displacements = np.asarray(displacements, dtype=float)
|
|
416
|
+
requested, used_workers, reason = _recovery_worker_count(resource_config, len(selected_ids))
|
|
417
|
+
deterministic = True if resource_config is None else bool(resource_config.deterministic)
|
|
418
|
+
backend = "serial" if used_workers <= 1 else "thread_pool"
|
|
419
|
+
start = time.perf_counter()
|
|
420
|
+
stresses: Dict[int, Dict[str, np.ndarray]] = {}
|
|
421
|
+
if used_workers <= 1:
|
|
422
|
+
for element_id in selected_ids:
|
|
423
|
+
item = _compute_one_element_stress(model, displacements, element_id, return_global=return_global)
|
|
424
|
+
if item is not None:
|
|
425
|
+
stresses[item[0]] = item[1]
|
|
426
|
+
else:
|
|
427
|
+
with ThreadPoolExecutor(max_workers=used_workers) as executor:
|
|
428
|
+
futures = [
|
|
429
|
+
executor.submit(_compute_one_element_stress, model, displacements, element_id, return_global=return_global)
|
|
430
|
+
for element_id in selected_ids
|
|
431
|
+
]
|
|
432
|
+
results = [future.result() for future in futures]
|
|
433
|
+
for item in results:
|
|
434
|
+
if item is not None:
|
|
435
|
+
stresses[item[0]] = item[1]
|
|
436
|
+
|
|
437
|
+
components = recovery.selected_components()
|
|
438
|
+
if components is not None:
|
|
439
|
+
stresses = {int(element_id): _filter_components(values, components) for element_id, values in stresses.items()}
|
|
440
|
+
elapsed = time.perf_counter() - start
|
|
441
|
+
report = RecoveryExecutionReport(
|
|
442
|
+
phase="element_stress_recovery",
|
|
443
|
+
item_count=len(selected_ids),
|
|
444
|
+
requested_workers=requested,
|
|
445
|
+
used_workers=used_workers,
|
|
446
|
+
backend=backend,
|
|
447
|
+
deterministic=deterministic,
|
|
448
|
+
elapsed_seconds=float(elapsed),
|
|
449
|
+
reason=reason,
|
|
450
|
+
)
|
|
451
|
+
return stresses, report
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def recover_element_stresses(
|
|
455
|
+
model: "FEModel",
|
|
456
|
+
displacements: np.ndarray,
|
|
457
|
+
recovery_config: Optional[RecoveryConfig] = None,
|
|
458
|
+
*,
|
|
459
|
+
return_global: bool = False,
|
|
460
|
+
resource_config: Optional[ResourceConfig] = None,
|
|
461
|
+
) -> Dict[int, Dict[str, np.ndarray]]:
|
|
462
|
+
"""Recover selected element stresses with optional component filtering."""
|
|
463
|
+
|
|
464
|
+
stresses, _report = recover_element_stresses_with_report(
|
|
465
|
+
model,
|
|
466
|
+
displacements,
|
|
467
|
+
recovery_config,
|
|
468
|
+
return_global=return_global,
|
|
469
|
+
resource_config=resource_config,
|
|
470
|
+
)
|
|
471
|
+
return stresses
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def filter_reactions(
|
|
475
|
+
reactions: Mapping[int, np.ndarray],
|
|
476
|
+
recovery_config: Optional[RecoveryConfig] = None,
|
|
477
|
+
model: Optional["FEModel"] = None,
|
|
478
|
+
) -> Dict[int, np.ndarray]:
|
|
479
|
+
"""Filter reaction dictionary by requested node ids."""
|
|
480
|
+
|
|
481
|
+
recovery = default_recovery_config(recovery_config)
|
|
482
|
+
if not recovery.include_reactions:
|
|
483
|
+
return {}
|
|
484
|
+
if recovery.node_ids is None:
|
|
485
|
+
return {int(node_id): np.asarray(values, dtype=float) for node_id, values in reactions.items()}
|
|
486
|
+
if model is not None:
|
|
487
|
+
selected = set(recovery.selected_node_ids(model))
|
|
488
|
+
else:
|
|
489
|
+
selected = set(_optional_int_tuple(recovery.node_ids) or ())
|
|
490
|
+
return {int(node_id): np.asarray(values, dtype=float) for node_id, values in reactions.items() if int(node_id) in selected}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def recovery_metadata(
|
|
494
|
+
recovery_config: Optional[RecoveryConfig] = None,
|
|
495
|
+
resource_config: Optional[ResourceConfig] = None,
|
|
496
|
+
memory_estimate: Optional[MemoryEstimate] = None,
|
|
497
|
+
) -> Dict[str, Any]:
|
|
498
|
+
"""Serialize recovery/resource policy metadata for provenance records."""
|
|
499
|
+
|
|
500
|
+
payload: Dict[str, Any] = {"recovery": default_recovery_config(recovery_config).to_dict()}
|
|
501
|
+
if resource_config is not None:
|
|
502
|
+
payload["resources"] = resource_config.to_dict()
|
|
503
|
+
if memory_estimate is not None:
|
|
504
|
+
payload["memory_estimate"] = memory_estimate.to_dict()
|
|
505
|
+
return payload
|