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,506 @@
1
+ """Batch C runtime integration for direct reduced-coordinate assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import sys
7
+ import threading
8
+ import time
9
+ from dataclasses import dataclass, field
10
+ from types import ModuleType
11
+ from typing import Any, Dict, List, Mapping, Optional, Tuple
12
+
13
+ import numpy as np
14
+ from scipy import sparse
15
+
16
+ from . import nonlinear_performance_batch_b as _batch_b
17
+ from . import nonlinear_reduced_assembly as _reduced
18
+ from .jit_compiler import JIT_ENABLED
19
+ from .nonlinear_reduced_assembly import (
20
+ ReducedAssemblyPlan,
21
+ ReducedAssemblyPlanLimit,
22
+ _identity_transformation,
23
+ _maximum_map_bytes,
24
+ assemble_reduced_system,
25
+ build_reduced_assembly_plan,
26
+ )
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class _ReducedVectorPayload:
31
+ values: np.ndarray
32
+ token: object
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class _ReducedMatrixPayload:
37
+ matrix: sparse.csr_matrix
38
+ token: object
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class _ReducedLeftProduct:
43
+ matrix: sparse.csr_matrix
44
+ token: object
45
+
46
+ def __matmul__(self, other: Any):
47
+ if (
48
+ isinstance(other, _DirectReductionTransform)
49
+ and other._batch_c_token is self.token
50
+ ):
51
+ return self.matrix
52
+ return NotImplemented
53
+
54
+
55
+ class _DirectReductionTranspose(sparse.csc_matrix):
56
+ """Sparse transpose that recognizes already-reduced assembly payloads."""
57
+
58
+ def __init__(
59
+ self,
60
+ arg1: Any,
61
+ *,
62
+ token: Optional[object] = None,
63
+ **kwargs: Any,
64
+ ) -> None:
65
+ super().__init__(arg1, **kwargs)
66
+ self._batch_c_token = token
67
+
68
+ def __matmul__(self, other: Any):
69
+ if (
70
+ isinstance(other, _ReducedVectorPayload)
71
+ and other.token is self._batch_c_token
72
+ ):
73
+ return other.values
74
+ if (
75
+ isinstance(other, _ReducedMatrixPayload)
76
+ and other.token is self._batch_c_token
77
+ ):
78
+ return _ReducedLeftProduct(other.matrix, self._batch_c_token)
79
+ return sparse.csc_matrix(self).__matmul__(other)
80
+
81
+
82
+ class _DirectReductionTransform(sparse.csr_matrix):
83
+ """CSR transformation retaining a token for direct-reduction interception."""
84
+
85
+ def __init__(
86
+ self,
87
+ arg1: Any,
88
+ *,
89
+ token: Optional[object] = None,
90
+ **kwargs: Any,
91
+ ) -> None:
92
+ super().__init__(arg1, **kwargs)
93
+ self._batch_c_token = token
94
+
95
+ def transpose(self, axes=None, copy: bool = False):
96
+ base = sparse.csr_matrix(self).transpose(axes=axes, copy=copy)
97
+ return _DirectReductionTranspose(base, token=self._batch_c_token)
98
+
99
+
100
+ @dataclass
101
+ class _SolveContext:
102
+ requested_model: Any
103
+ token: object = field(default_factory=object)
104
+ bound_model: Any = None
105
+ transformation: Optional[_DirectReductionTransform] = None
106
+ reduced_plan: Optional[ReducedAssemblyPlan] = None
107
+ fallback_reason: Optional[str] = None
108
+
109
+
110
+ _CONTEXT = threading.local()
111
+ _STATUS_LOCK = threading.RLock()
112
+ _STATUS: Dict[str, Any] = {
113
+ "installed": False,
114
+ "contexts_entered": 0,
115
+ "reduced_plan_builds": 0,
116
+ "reduced_assemblies": 0,
117
+ "full_coordinate_fallbacks": 0,
118
+ "last_fallback_reason": None,
119
+ "last_plan": None,
120
+ }
121
+ _PATCHES: List[Tuple[ModuleType, str, Any, Any]] = []
122
+ _INSTALLED = False
123
+ _BASE_ASSEMBLER = None
124
+ _ORIGINAL_STATIC_SOLVER = None
125
+ _ORIGINAL_ARC_SOLVER = None
126
+ _ORIGINAL_CONSTRAINT_BUILDER = None
127
+ _ORIGINAL_LOCAL_EVALUATOR = _reduced._evaluate_local_responses
128
+
129
+
130
+ def _context_stack() -> List[_SolveContext]:
131
+ stack = getattr(_CONTEXT, "stack", None)
132
+ if stack is None:
133
+ stack = []
134
+ _CONTEXT.stack = stack
135
+ return stack
136
+
137
+
138
+ def _active_context(model: Any) -> Optional[_SolveContext]:
139
+ for context in reversed(_context_stack()):
140
+ if context.bound_model is model:
141
+ return context
142
+ return None
143
+
144
+
145
+ def _run_with_context(original, *args: Any, **kwargs: Any):
146
+ model = args[0] if args else kwargs.get("model")
147
+ context = _SolveContext(requested_model=model)
148
+ stack = _context_stack()
149
+ stack.append(context)
150
+ with _STATUS_LOCK:
151
+ _STATUS["contexts_entered"] += 1
152
+ try:
153
+ return original(*args, **kwargs)
154
+ finally:
155
+ if stack and stack[-1] is context:
156
+ stack.pop()
157
+ else:
158
+ try:
159
+ stack.remove(context)
160
+ except ValueError:
161
+ pass
162
+
163
+
164
+ def _make_solver_wrapper(original):
165
+ @functools.wraps(original)
166
+ def wrapper(*args: Any, **kwargs: Any):
167
+ return _run_with_context(original, *args, **kwargs)
168
+
169
+ wrapper._batch_c_original = original
170
+ return wrapper
171
+
172
+
173
+ def _constraint_builder_wrapper(K, F, model):
174
+ result = _ORIGINAL_CONSTRAINT_BUILDER(K, F, model)
175
+ K_red, F_red, transformation, u0, independent_dofs, info = result
176
+ stack = _context_stack()
177
+ if not stack:
178
+ return result
179
+ context = stack[-1]
180
+ if context.bound_model is None:
181
+ context.bound_model = model
182
+ if context.bound_model is not model or _identity_transformation(transformation):
183
+ return result
184
+ wrapped = _DirectReductionTransform(transformation, token=context.token)
185
+ context.transformation = wrapped
186
+ return K_red, F_red, wrapped, u0, independent_dofs, info
187
+
188
+
189
+ def _batch_c_evaluate_local_responses(
190
+ nonlinear_plan: Any,
191
+ displacements: np.ndarray,
192
+ committed_states: Mapping[int, Any],
193
+ tangent: bool,
194
+ ):
195
+ """Fill local buffers while retaining Batch B's elastic shell fast path."""
196
+
197
+ start = time.perf_counter()
198
+ nonlinear_plan.force_values.fill(0.0)
199
+ if tangent:
200
+ nonlinear_plan.tangent_values.fill(0.0)
201
+ trial_states: Dict[int, Any] = {}
202
+ displacement_array = np.asarray(displacements, dtype=float)
203
+
204
+ for batch in nonlinear_plan.shell_batches:
205
+ if getattr(batch, "_batch_b_elastic", False):
206
+ kernel_start = time.perf_counter()
207
+ _batch_b._elastic_shell_batch_into_buffers(
208
+ displacement_array,
209
+ batch.dof_mappings,
210
+ batch.T0,
211
+ batch.B_m,
212
+ batch.B_b,
213
+ batch.B_d,
214
+ batch.Gw,
215
+ batch.detw,
216
+ batch.B_s,
217
+ batch.detw_shear,
218
+ batch._batch_b_membrane_matrix,
219
+ batch._batch_b_bending_matrix,
220
+ batch._batch_b_shear_matrix,
221
+ float(batch._batch_b_drilling_stiffness),
222
+ batch.force_positions,
223
+ batch.tangent_positions,
224
+ nonlinear_plan.force_values,
225
+ nonlinear_plan.tangent_values,
226
+ batch.u_work,
227
+ bool(tangent),
228
+ )
229
+ nonlinear_plan.timings.shell_kernel_seconds += (
230
+ time.perf_counter() - kernel_start
231
+ )
232
+ elastic_states = batch._batch_b_elastic_state_mapping
233
+ use_cached_mapping = True
234
+ for element_id in batch.element_ids:
235
+ existing = committed_states.get(int(element_id))
236
+ if (
237
+ isinstance(existing, dict)
238
+ and existing is not elastic_states[int(element_id)]
239
+ ):
240
+ use_cached_mapping = False
241
+ break
242
+ if use_cached_mapping:
243
+ trial_states.update(elastic_states)
244
+ else:
245
+ for element_id in batch.element_ids:
246
+ element_key = int(element_id)
247
+ existing = committed_states.get(element_key)
248
+ trial_states[element_key] = (
249
+ existing
250
+ if isinstance(existing, dict)
251
+ else elastic_states[element_key]
252
+ )
253
+ continue
254
+
255
+ force_batch, tangent_batch, batch_states, kernel_seconds = batch.evaluate(
256
+ displacement_array,
257
+ committed_states,
258
+ tangent,
259
+ )
260
+ nonlinear_plan.timings.shell_kernel_seconds += kernel_seconds
261
+ nonlinear_plan.force_values[batch.force_positions.reshape(-1)] = np.asarray(
262
+ force_batch,
263
+ dtype=float,
264
+ ).reshape(-1)
265
+ if tangent and tangent_batch is not None:
266
+ nonlinear_plan.tangent_values[
267
+ batch.tangent_positions.reshape(-1)
268
+ ] = np.asarray(tangent_batch, dtype=float).reshape(-1)
269
+ trial_states.update(batch_states)
270
+
271
+ non_shell_start = time.perf_counter()
272
+ model = nonlinear_plan.model
273
+ mesh = model.mesh
274
+ for record in nonlinear_plan.non_shell_elements:
275
+ material = model.get_material(record.element.material_name)
276
+ element_displacement = displacement_array[record.dof_mapping]
277
+ force_element, tangent_element, trial_state = (
278
+ record.element.compute_nonlinear_response(
279
+ mesh,
280
+ material,
281
+ element_displacement,
282
+ committed_states.get(record.element_id),
283
+ nonlinear_plan.num_layers,
284
+ tangent,
285
+ )
286
+ )
287
+ nonlinear_plan.force_values[record.force_positions] = np.asarray(
288
+ force_element,
289
+ dtype=float,
290
+ ).reshape(-1)
291
+ if tangent and tangent_element is not None:
292
+ nonlinear_plan.tangent_values[record.tangent_positions] = np.asarray(
293
+ tangent_element,
294
+ dtype=float,
295
+ ).reshape(-1)
296
+ if trial_state is not None:
297
+ trial_states[record.element_id] = trial_state
298
+ nonlinear_plan.timings.non_shell_seconds += (
299
+ time.perf_counter() - non_shell_start
300
+ )
301
+ return trial_states, time.perf_counter() - start
302
+
303
+
304
+ def _batch_c_assemble_nonlinear_system(
305
+ model,
306
+ displacements: np.ndarray,
307
+ committed_states: Dict[int, Any],
308
+ num_layers: int,
309
+ tangent: bool = True,
310
+ deleted_element_ids=None,
311
+ residual_stiffness_fraction: float = 1.0,
312
+ **extra,
313
+ ):
314
+ deleted_tuple = tuple(deleted_element_ids or ())
315
+ kinematics = str(extra.pop("kinematics", "von_karman"))
316
+ if deleted_tuple or extra or kinematics != "von_karman":
317
+ # The reduced-assembly plan encodes the default von Karman element
318
+ # response; erosion, per-element stiffness scales and corotational
319
+ # kinematics take the full base assembler.
320
+ return _BASE_ASSEMBLER(
321
+ model,
322
+ displacements,
323
+ committed_states,
324
+ num_layers,
325
+ tangent=tangent,
326
+ deleted_element_ids=deleted_tuple,
327
+ residual_stiffness_fraction=float(residual_stiffness_fraction),
328
+ kinematics=kinematics,
329
+ **extra,
330
+ )
331
+ context = _active_context(model)
332
+ if (
333
+ context is None
334
+ or context.transformation is None
335
+ or context.fallback_reason is not None
336
+ ):
337
+ return _BASE_ASSEMBLER(
338
+ model,
339
+ displacements,
340
+ committed_states,
341
+ num_layers,
342
+ tangent=tangent,
343
+ deleted_element_ids=deleted_tuple,
344
+ residual_stiffness_fraction=float(residual_stiffness_fraction),
345
+ )
346
+
347
+ from .nonlinear_performance_bootstrap import get_nonlinear_assembly_plan
348
+
349
+ nonlinear_plan = get_nonlinear_assembly_plan(model, int(num_layers))
350
+ if (
351
+ context.reduced_plan is None
352
+ or context.reduced_plan.source_plan is not nonlinear_plan
353
+ ):
354
+ try:
355
+ context.reduced_plan = build_reduced_assembly_plan(
356
+ nonlinear_plan,
357
+ context.transformation,
358
+ )
359
+ with _STATUS_LOCK:
360
+ _STATUS["reduced_plan_builds"] += 1
361
+ _STATUS["last_plan"] = context.reduced_plan.diagnostics()
362
+ except ReducedAssemblyPlanLimit as exc:
363
+ context.fallback_reason = str(exc)
364
+ with _STATUS_LOCK:
365
+ _STATUS["full_coordinate_fallbacks"] += 1
366
+ _STATUS["last_fallback_reason"] = context.fallback_reason
367
+ return _BASE_ASSEMBLER(
368
+ model,
369
+ displacements,
370
+ committed_states,
371
+ num_layers,
372
+ tangent=tangent,
373
+ deleted_element_ids=deleted_tuple,
374
+ residual_stiffness_fraction=float(residual_stiffness_fraction),
375
+ )
376
+
377
+ force_reduced, tangent_reduced, trial_states = assemble_reduced_system(
378
+ nonlinear_plan,
379
+ context.reduced_plan,
380
+ displacements,
381
+ committed_states,
382
+ tangent=tangent,
383
+ )
384
+ with _STATUS_LOCK:
385
+ _STATUS["reduced_assemblies"] += 1
386
+ _STATUS["last_plan"] = context.reduced_plan.diagnostics()
387
+ force_payload = _ReducedVectorPayload(force_reduced, context.token)
388
+ tangent_payload = (
389
+ _ReducedMatrixPayload(tangent_reduced, context.token)
390
+ if tangent_reduced is not None
391
+ else None
392
+ )
393
+ return force_payload, tangent_payload, trial_states
394
+
395
+
396
+ def _record_patch(module: ModuleType, name: str, replacement: Any) -> None:
397
+ current = getattr(module, name, None)
398
+ if current is replacement:
399
+ return
400
+ _PATCHES.append((module, name, current, replacement))
401
+ setattr(module, name, replacement)
402
+
403
+
404
+ def _replace_function_references(original: Any, replacement: Any) -> None:
405
+ for module in list(sys.modules.values()):
406
+ if not isinstance(module, ModuleType):
407
+ continue
408
+ module_name = getattr(module, "__name__", "")
409
+ if not module_name.startswith("anysolver") or module_name == __name__:
410
+ continue
411
+ for name, value in list(vars(module).items()):
412
+ if value is original:
413
+ _record_patch(module, name, replacement)
414
+
415
+
416
+ def install_batch_c_optimizations() -> bool:
417
+ """Install direct reduced-coordinate assembly for Numba-enabled runs."""
418
+
419
+ global _INSTALLED, _BASE_ASSEMBLER, _ORIGINAL_STATIC_SOLVER
420
+ global _ORIGINAL_ARC_SOLVER, _ORIGINAL_CONSTRAINT_BUILDER
421
+ if _INSTALLED:
422
+ return True
423
+ if not JIT_ENABLED:
424
+ return False
425
+
426
+ from . import arc_length
427
+ from . import assembly
428
+ from . import nonlinear_static
429
+
430
+ _BASE_ASSEMBLER = nonlinear_static._assemble_nonlinear_system
431
+ _ORIGINAL_STATIC_SOLVER = nonlinear_static.solve_static_nonlinear
432
+ _ORIGINAL_ARC_SOLVER = arc_length.solve_static_arc_length
433
+ _ORIGINAL_CONSTRAINT_BUILDER = assembly.build_constraint_transformation
434
+
435
+ static_wrapper = _make_solver_wrapper(_ORIGINAL_STATIC_SOLVER)
436
+ arc_wrapper = _make_solver_wrapper(_ORIGINAL_ARC_SOLVER)
437
+ _replace_function_references(_ORIGINAL_STATIC_SOLVER, static_wrapper)
438
+ _replace_function_references(_ORIGINAL_ARC_SOLVER, arc_wrapper)
439
+ _replace_function_references(
440
+ _ORIGINAL_CONSTRAINT_BUILDER,
441
+ _constraint_builder_wrapper,
442
+ )
443
+
444
+ for module in list(sys.modules.values()):
445
+ if not isinstance(module, ModuleType):
446
+ continue
447
+ module_name = getattr(module, "__name__", "")
448
+ if not module_name.startswith("anysolver") or module_name == __name__:
449
+ continue
450
+ if hasattr(module, "_assemble_nonlinear_system"):
451
+ _record_patch(
452
+ module,
453
+ "_assemble_nonlinear_system",
454
+ _batch_c_assemble_nonlinear_system,
455
+ )
456
+
457
+ _reduced._evaluate_local_responses = _batch_c_evaluate_local_responses
458
+ _INSTALLED = True
459
+ with _STATUS_LOCK:
460
+ _STATUS["installed"] = True
461
+ return True
462
+
463
+
464
+ def uninstall_batch_c_optimizations() -> None:
465
+ """Restore references changed by :func:`install_batch_c_optimizations`."""
466
+
467
+ global _INSTALLED
468
+ if not _INSTALLED:
469
+ return
470
+ _reduced._evaluate_local_responses = _ORIGINAL_LOCAL_EVALUATOR
471
+ for module, name, original, replacement in reversed(_PATCHES):
472
+ if getattr(module, name, None) is replacement:
473
+ setattr(module, name, original)
474
+ _PATCHES.clear()
475
+ _context_stack().clear()
476
+ _INSTALLED = False
477
+ with _STATUS_LOCK:
478
+ _STATUS["installed"] = False
479
+
480
+
481
+ def reset_batch_c_counters() -> None:
482
+ with _STATUS_LOCK:
483
+ installed = bool(_STATUS["installed"])
484
+ _STATUS.update(
485
+ {
486
+ "installed": installed,
487
+ "contexts_entered": 0,
488
+ "reduced_plan_builds": 0,
489
+ "reduced_assemblies": 0,
490
+ "full_coordinate_fallbacks": 0,
491
+ "last_fallback_reason": None,
492
+ "last_plan": None,
493
+ }
494
+ )
495
+
496
+
497
+ def batch_c_status() -> Dict[str, Any]:
498
+ with _STATUS_LOCK:
499
+ result = dict(_STATUS)
500
+ result["eligible"] = bool(JIT_ENABLED)
501
+ result["map_limit_mb"] = float(_maximum_map_bytes() / (1024.0**2))
502
+ result["active_context_depth"] = len(_context_stack())
503
+ result["batch_b_local_kernel_retained"] = bool(
504
+ _reduced._evaluate_local_responses is _batch_c_evaluate_local_responses
505
+ )
506
+ return result
@@ -0,0 +1,120 @@
1
+ """Bootstrap nonlinear performance batches without requiring hashable FEModel objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import weakref
7
+ from typing import Any, Dict, Optional
8
+
9
+ from . import linalg as _linalg
10
+ from . import nonlinear_performance as _performance
11
+ from . import nonlinear_performance_batch_b as _batch_b
12
+ from . import nonlinear_performance_batch_c as _batch_c
13
+ from .jit_compiler import JIT_DISABLED_REASON, JIT_ENABLED
14
+
15
+ # Keep one size-aware backend policy in every environment. AutoSparseSolverBackend
16
+ # already falls back to SuperLU when PyPardiso is unavailable, while also
17
+ # recording the same provenance metadata used when PyPardiso is installed.
18
+ if not isinstance(_linalg.DEFAULT_BACKEND, _linalg.AutoSparseSolverBackend):
19
+ _linalg.DEFAULT_BACKEND = _linalg.AutoSparseSolverBackend()
20
+
21
+ _CACHE_LOCK = threading.RLock()
22
+ # FEModel is a mutable dataclass and therefore intentionally unhashable. Cache
23
+ # by object identity and keep a weak reference so entries disappear naturally.
24
+ _PLAN_CACHE: Dict[int, tuple[weakref.ReferenceType[Any], Dict[int, Any]]] = {}
25
+
26
+
27
+ def _purge_dead(model_id: int) -> None:
28
+ with _CACHE_LOCK:
29
+ _PLAN_CACHE.pop(int(model_id), None)
30
+
31
+
32
+ def get_nonlinear_assembly_plan(model: Any, num_layers: int):
33
+ model_id = id(model)
34
+ with _CACHE_LOCK:
35
+ entry = _PLAN_CACHE.get(model_id)
36
+ if entry is None or entry[0]() is not model:
37
+ model_ref = weakref.ref(model, lambda _ref, key=model_id: _purge_dead(key))
38
+ plans: Dict[int, Any] = {}
39
+ _PLAN_CACHE[model_id] = (model_ref, plans)
40
+ else:
41
+ plans = entry[1]
42
+ plan = plans.get(int(num_layers))
43
+ if plan is None or not plan.is_valid(model, int(num_layers)):
44
+ plan = _performance.NonlinearAssemblyPlan.build(model, int(num_layers))
45
+ plans[int(num_layers)] = plan
46
+ return plan
47
+
48
+
49
+ def clear_nonlinear_assembly_cache(model: Optional[Any] = None) -> None:
50
+ with _CACHE_LOCK:
51
+ if model is None:
52
+ _PLAN_CACHE.clear()
53
+ else:
54
+ _PLAN_CACHE.pop(id(model), None)
55
+
56
+
57
+ def nonlinear_assembly_diagnostics(model: Optional[Any] = None) -> Dict[str, Any]:
58
+ with _CACHE_LOCK:
59
+ if model is not None:
60
+ entry = _PLAN_CACHE.get(id(model))
61
+ if entry is None or entry[0]() is not model:
62
+ return {}
63
+ return {str(layers): plan.diagnostics() for layers, plan in entry[1].items()}
64
+ result: Dict[str, Any] = {}
65
+ for model_id, (model_ref, plans) in list(_PLAN_CACHE.items()):
66
+ cached_model = model_ref()
67
+ if cached_model is None:
68
+ continue
69
+ result[str(model_id)] = {
70
+ "model_name": getattr(cached_model, "name", None),
71
+ "plans": {str(layers): plan.diagnostics() for layers, plan in plans.items()},
72
+ }
73
+ return result
74
+
75
+
76
+ _BASE_INSTALL = _performance.install_nonlinear_performance_optimizations
77
+ _BASE_UNINSTALL = _performance.uninstall_nonlinear_performance_optimizations
78
+
79
+
80
+ def install_nonlinear_performance_optimizations() -> bool:
81
+ active = bool(_BASE_INSTALL())
82
+ if active and JIT_ENABLED:
83
+ _batch_b.install_batch_b_optimizations()
84
+ _batch_c.install_batch_c_optimizations()
85
+ return active
86
+
87
+
88
+ def uninstall_nonlinear_performance_optimizations() -> None:
89
+ _batch_c.uninstall_batch_c_optimizations()
90
+ _batch_b.uninstall_batch_b_optimizations()
91
+ _BASE_UNINSTALL()
92
+
93
+
94
+ def nonlinear_performance_status() -> Dict[str, Any]:
95
+ batch_b = _batch_b.batch_b_status()
96
+ batch_b["eligible"] = bool(JIT_ENABLED)
97
+ batch_b["disabled_reason"] = None if JIT_ENABLED else JIT_DISABLED_REASON
98
+ batch_c = _batch_c.batch_c_status()
99
+ batch_c["eligible"] = bool(JIT_ENABLED)
100
+ batch_c["disabled_reason"] = None if JIT_ENABLED else JIT_DISABLED_REASON
101
+ return {
102
+ "installed": bool(_performance._INSTALLED),
103
+ "batch_b": batch_b,
104
+ "batch_c": batch_c,
105
+ "cached_models": len(_PLAN_CACHE),
106
+ "diagnostics": nonlinear_assembly_diagnostics(),
107
+ }
108
+
109
+
110
+ # Replace the initial WeakKeyDictionary helpers before installation. Functions
111
+ # in nonlinear_performance resolve these names dynamically, so the optimized
112
+ # assembler immediately uses the identity/weak-reference cache above.
113
+ _performance.get_nonlinear_assembly_plan = get_nonlinear_assembly_plan
114
+ _performance.clear_nonlinear_assembly_cache = clear_nonlinear_assembly_cache
115
+ _performance.nonlinear_assembly_diagnostics = nonlinear_assembly_diagnostics
116
+ _performance.nonlinear_performance_status = nonlinear_performance_status
117
+ _performance.install_nonlinear_performance_optimizations = install_nonlinear_performance_optimizations
118
+ _performance.uninstall_nonlinear_performance_optimizations = uninstall_nonlinear_performance_optimizations
119
+
120
+ NonlinearAssemblyPlan = _performance.NonlinearAssemblyPlan