tnfr 3.0.3__py3-none-any.whl → 8.5.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.

Potentially problematic release.


This version of tnfr might be problematic. Click here for more details.

Files changed (360) hide show
  1. tnfr/__init__.py +375 -56
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +723 -0
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +171 -0
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +110 -0
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +489 -0
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +914 -0
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +51 -0
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/config/constants.py +104 -0
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/config/defaults_core.py +212 -0
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +92 -0
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +33 -0
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +104 -0
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +238 -0
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +3034 -0
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +661 -0
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +36 -0
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +223 -0
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +262 -0
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +354 -0
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +377 -0
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +218 -0
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +203 -0
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +10 -0
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.py +37 -0
  139. tnfr/locking.pyi +7 -0
  140. tnfr/mathematics/__init__.py +79 -0
  141. tnfr/mathematics/backend.py +453 -0
  142. tnfr/mathematics/backend.pyi +99 -0
  143. tnfr/mathematics/dynamics.py +408 -0
  144. tnfr/mathematics/dynamics.pyi +90 -0
  145. tnfr/mathematics/epi.py +391 -0
  146. tnfr/mathematics/epi.pyi +65 -0
  147. tnfr/mathematics/generators.py +242 -0
  148. tnfr/mathematics/generators.pyi +29 -0
  149. tnfr/mathematics/metrics.py +119 -0
  150. tnfr/mathematics/metrics.pyi +16 -0
  151. tnfr/mathematics/operators.py +239 -0
  152. tnfr/mathematics/operators.pyi +59 -0
  153. tnfr/mathematics/operators_factory.py +124 -0
  154. tnfr/mathematics/operators_factory.pyi +11 -0
  155. tnfr/mathematics/projection.py +87 -0
  156. tnfr/mathematics/projection.pyi +33 -0
  157. tnfr/mathematics/runtime.py +182 -0
  158. tnfr/mathematics/runtime.pyi +64 -0
  159. tnfr/mathematics/spaces.py +256 -0
  160. tnfr/mathematics/spaces.pyi +83 -0
  161. tnfr/mathematics/transforms.py +305 -0
  162. tnfr/mathematics/transforms.pyi +62 -0
  163. tnfr/metrics/__init__.py +79 -0
  164. tnfr/metrics/__init__.pyi +20 -0
  165. tnfr/metrics/buffer_cache.py +163 -0
  166. tnfr/metrics/buffer_cache.pyi +24 -0
  167. tnfr/metrics/cache_utils.py +214 -0
  168. tnfr/metrics/coherence.py +2009 -0
  169. tnfr/metrics/coherence.pyi +129 -0
  170. tnfr/metrics/common.py +158 -0
  171. tnfr/metrics/common.pyi +35 -0
  172. tnfr/metrics/core.py +316 -0
  173. tnfr/metrics/core.pyi +13 -0
  174. tnfr/metrics/diagnosis.py +833 -0
  175. tnfr/metrics/diagnosis.pyi +86 -0
  176. tnfr/metrics/emergence.py +245 -0
  177. tnfr/metrics/export.py +179 -0
  178. tnfr/metrics/export.pyi +7 -0
  179. tnfr/metrics/glyph_timing.py +379 -0
  180. tnfr/metrics/glyph_timing.pyi +81 -0
  181. tnfr/metrics/learning_metrics.py +280 -0
  182. tnfr/metrics/learning_metrics.pyi +21 -0
  183. tnfr/metrics/phase_coherence.py +351 -0
  184. tnfr/metrics/phase_compatibility.py +349 -0
  185. tnfr/metrics/reporting.py +183 -0
  186. tnfr/metrics/reporting.pyi +25 -0
  187. tnfr/metrics/sense_index.py +1203 -0
  188. tnfr/metrics/sense_index.pyi +9 -0
  189. tnfr/metrics/trig.py +373 -0
  190. tnfr/metrics/trig.pyi +13 -0
  191. tnfr/metrics/trig_cache.py +233 -0
  192. tnfr/metrics/trig_cache.pyi +10 -0
  193. tnfr/multiscale/__init__.py +32 -0
  194. tnfr/multiscale/hierarchical.py +517 -0
  195. tnfr/node.py +763 -0
  196. tnfr/node.pyi +139 -0
  197. tnfr/observers.py +255 -130
  198. tnfr/observers.pyi +31 -0
  199. tnfr/ontosim.py +144 -137
  200. tnfr/ontosim.pyi +28 -0
  201. tnfr/operators/__init__.py +1672 -0
  202. tnfr/operators/__init__.pyi +31 -0
  203. tnfr/operators/algebra.py +277 -0
  204. tnfr/operators/canonical_patterns.py +420 -0
  205. tnfr/operators/cascade.py +267 -0
  206. tnfr/operators/cycle_detection.py +358 -0
  207. tnfr/operators/definitions.py +4108 -0
  208. tnfr/operators/definitions.pyi +78 -0
  209. tnfr/operators/grammar.py +1164 -0
  210. tnfr/operators/grammar.pyi +140 -0
  211. tnfr/operators/hamiltonian.py +710 -0
  212. tnfr/operators/health_analyzer.py +809 -0
  213. tnfr/operators/jitter.py +272 -0
  214. tnfr/operators/jitter.pyi +11 -0
  215. tnfr/operators/lifecycle.py +314 -0
  216. tnfr/operators/metabolism.py +618 -0
  217. tnfr/operators/metrics.py +2138 -0
  218. tnfr/operators/network_analysis/__init__.py +27 -0
  219. tnfr/operators/network_analysis/source_detection.py +186 -0
  220. tnfr/operators/nodal_equation.py +395 -0
  221. tnfr/operators/pattern_detection.py +660 -0
  222. tnfr/operators/patterns.py +669 -0
  223. tnfr/operators/postconditions/__init__.py +38 -0
  224. tnfr/operators/postconditions/mutation.py +236 -0
  225. tnfr/operators/preconditions/__init__.py +1226 -0
  226. tnfr/operators/preconditions/coherence.py +305 -0
  227. tnfr/operators/preconditions/dissonance.py +236 -0
  228. tnfr/operators/preconditions/emission.py +128 -0
  229. tnfr/operators/preconditions/mutation.py +580 -0
  230. tnfr/operators/preconditions/reception.py +125 -0
  231. tnfr/operators/preconditions/resonance.py +364 -0
  232. tnfr/operators/registry.py +74 -0
  233. tnfr/operators/registry.pyi +9 -0
  234. tnfr/operators/remesh.py +1809 -0
  235. tnfr/operators/remesh.pyi +26 -0
  236. tnfr/operators/structural_units.py +268 -0
  237. tnfr/operators/unified_grammar.py +105 -0
  238. tnfr/parallel/__init__.py +54 -0
  239. tnfr/parallel/auto_scaler.py +234 -0
  240. tnfr/parallel/distributed.py +384 -0
  241. tnfr/parallel/engine.py +238 -0
  242. tnfr/parallel/gpu_engine.py +420 -0
  243. tnfr/parallel/monitoring.py +248 -0
  244. tnfr/parallel/partitioner.py +459 -0
  245. tnfr/py.typed +0 -0
  246. tnfr/recipes/__init__.py +22 -0
  247. tnfr/recipes/cookbook.py +743 -0
  248. tnfr/rng.py +178 -0
  249. tnfr/rng.pyi +26 -0
  250. tnfr/schemas/__init__.py +8 -0
  251. tnfr/schemas/grammar.json +94 -0
  252. tnfr/sdk/__init__.py +107 -0
  253. tnfr/sdk/__init__.pyi +19 -0
  254. tnfr/sdk/adaptive_system.py +173 -0
  255. tnfr/sdk/adaptive_system.pyi +21 -0
  256. tnfr/sdk/builders.py +370 -0
  257. tnfr/sdk/builders.pyi +51 -0
  258. tnfr/sdk/fluent.py +1121 -0
  259. tnfr/sdk/fluent.pyi +74 -0
  260. tnfr/sdk/templates.py +342 -0
  261. tnfr/sdk/templates.pyi +41 -0
  262. tnfr/sdk/utils.py +341 -0
  263. tnfr/secure_config.py +46 -0
  264. tnfr/security/__init__.py +70 -0
  265. tnfr/security/database.py +514 -0
  266. tnfr/security/subprocess.py +503 -0
  267. tnfr/security/validation.py +290 -0
  268. tnfr/selector.py +247 -0
  269. tnfr/selector.pyi +19 -0
  270. tnfr/sense.py +378 -0
  271. tnfr/sense.pyi +23 -0
  272. tnfr/services/__init__.py +17 -0
  273. tnfr/services/orchestrator.py +325 -0
  274. tnfr/sparse/__init__.py +39 -0
  275. tnfr/sparse/representations.py +492 -0
  276. tnfr/structural.py +705 -0
  277. tnfr/structural.pyi +83 -0
  278. tnfr/telemetry/__init__.py +35 -0
  279. tnfr/telemetry/cache_metrics.py +226 -0
  280. tnfr/telemetry/cache_metrics.pyi +64 -0
  281. tnfr/telemetry/nu_f.py +422 -0
  282. tnfr/telemetry/nu_f.pyi +108 -0
  283. tnfr/telemetry/verbosity.py +36 -0
  284. tnfr/telemetry/verbosity.pyi +15 -0
  285. tnfr/tokens.py +58 -0
  286. tnfr/tokens.pyi +36 -0
  287. tnfr/tools/__init__.py +20 -0
  288. tnfr/tools/domain_templates.py +478 -0
  289. tnfr/tools/sequence_generator.py +846 -0
  290. tnfr/topology/__init__.py +13 -0
  291. tnfr/topology/asymmetry.py +151 -0
  292. tnfr/trace.py +543 -0
  293. tnfr/trace.pyi +42 -0
  294. tnfr/tutorials/__init__.py +38 -0
  295. tnfr/tutorials/autonomous_evolution.py +285 -0
  296. tnfr/tutorials/interactive.py +1576 -0
  297. tnfr/tutorials/structural_metabolism.py +238 -0
  298. tnfr/types.py +775 -0
  299. tnfr/types.pyi +357 -0
  300. tnfr/units.py +68 -0
  301. tnfr/units.pyi +13 -0
  302. tnfr/utils/__init__.py +282 -0
  303. tnfr/utils/__init__.pyi +215 -0
  304. tnfr/utils/cache.py +4223 -0
  305. tnfr/utils/cache.pyi +470 -0
  306. tnfr/utils/callbacks.py +375 -0
  307. tnfr/utils/callbacks.pyi +49 -0
  308. tnfr/utils/chunks.py +108 -0
  309. tnfr/utils/chunks.pyi +22 -0
  310. tnfr/utils/data.py +428 -0
  311. tnfr/utils/data.pyi +74 -0
  312. tnfr/utils/graph.py +85 -0
  313. tnfr/utils/graph.pyi +10 -0
  314. tnfr/utils/init.py +821 -0
  315. tnfr/utils/init.pyi +80 -0
  316. tnfr/utils/io.py +559 -0
  317. tnfr/utils/io.pyi +66 -0
  318. tnfr/utils/numeric.py +114 -0
  319. tnfr/utils/numeric.pyi +21 -0
  320. tnfr/validation/__init__.py +257 -0
  321. tnfr/validation/__init__.pyi +85 -0
  322. tnfr/validation/compatibility.py +460 -0
  323. tnfr/validation/compatibility.pyi +6 -0
  324. tnfr/validation/config.py +73 -0
  325. tnfr/validation/graph.py +139 -0
  326. tnfr/validation/graph.pyi +18 -0
  327. tnfr/validation/input_validation.py +755 -0
  328. tnfr/validation/invariants.py +712 -0
  329. tnfr/validation/rules.py +253 -0
  330. tnfr/validation/rules.pyi +44 -0
  331. tnfr/validation/runtime.py +279 -0
  332. tnfr/validation/runtime.pyi +28 -0
  333. tnfr/validation/sequence_validator.py +162 -0
  334. tnfr/validation/soft_filters.py +170 -0
  335. tnfr/validation/soft_filters.pyi +32 -0
  336. tnfr/validation/spectral.py +164 -0
  337. tnfr/validation/spectral.pyi +42 -0
  338. tnfr/validation/validator.py +1266 -0
  339. tnfr/validation/window.py +39 -0
  340. tnfr/validation/window.pyi +1 -0
  341. tnfr/visualization/__init__.py +98 -0
  342. tnfr/visualization/cascade_viz.py +256 -0
  343. tnfr/visualization/hierarchy.py +284 -0
  344. tnfr/visualization/sequence_plotter.py +784 -0
  345. tnfr/viz/__init__.py +60 -0
  346. tnfr/viz/matplotlib.py +278 -0
  347. tnfr/viz/matplotlib.pyi +35 -0
  348. tnfr-8.5.0.dist-info/METADATA +573 -0
  349. tnfr-8.5.0.dist-info/RECORD +353 -0
  350. tnfr-8.5.0.dist-info/entry_points.txt +3 -0
  351. tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
  352. tnfr/constants.py +0 -183
  353. tnfr/dynamics.py +0 -543
  354. tnfr/helpers.py +0 -198
  355. tnfr/main.py +0 -37
  356. tnfr/operators.py +0 -296
  357. tnfr-3.0.3.dist-info/METADATA +0 -35
  358. tnfr-3.0.3.dist-info/RECORD +0 -13
  359. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  360. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,375 @@
1
+ """Callback registration and invocation helpers.
2
+
3
+ This module is thread-safe: all mutations of the callback registry stored in a
4
+ graph's ``G.graph`` are serialised using a process-wide lock obtained via
5
+ ``locking.get_lock("callbacks")``. Callback functions themselves execute
6
+ outside of the lock and must therefore be independently thread-safe if they
7
+ modify shared state.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ import traceback
14
+ from collections import defaultdict, deque
15
+ from collections.abc import Callable, Iterable, Mapping
16
+ from enum import Enum
17
+ from typing import Any, NamedTuple
18
+
19
+ import networkx as nx
20
+
21
+ from ..constants import DEFAULTS
22
+ from ..locking import get_lock
23
+ from .init import get_logger
24
+ from .data import is_non_string_sequence
25
+ from ..types import CallbackError
26
+
27
+
28
+ class CallbackSpec(NamedTuple):
29
+ """Specification for a registered callback."""
30
+
31
+ name: str | None
32
+ func: Callable[..., Any]
33
+
34
+
35
+ __all__ = (
36
+ "CallbackEvent",
37
+ "CallbackManager",
38
+ "callback_manager",
39
+ "CallbackError",
40
+ "CallbackSpec",
41
+ )
42
+
43
+ logger = get_logger(__name__)
44
+
45
+
46
+ class CallbackEvent(str, Enum):
47
+ """Supported callback events."""
48
+
49
+ BEFORE_STEP = "before_step"
50
+ AFTER_STEP = "after_step"
51
+ ON_REMESH = "on_remesh"
52
+ CACHE_METRICS = "cache_metrics"
53
+
54
+
55
+ class CallbackManager:
56
+ """Centralised registry and error tracking for callbacks."""
57
+
58
+ def __init__(self) -> None:
59
+ self._lock = get_lock("callbacks")
60
+ self._error_limit_lock = threading.Lock()
61
+ self._error_limit = 100
62
+ self._error_limit_cache = self._error_limit
63
+
64
+ # ------------------------------------------------------------------
65
+ # Error limit management
66
+ # ------------------------------------------------------------------
67
+ def get_callback_error_limit(self) -> int:
68
+ """Return the current callback error retention limit."""
69
+ with self._error_limit_lock:
70
+ return self._error_limit
71
+
72
+ def set_callback_error_limit(self, limit: int) -> int:
73
+ """Set the maximum number of callback errors retained."""
74
+ if limit < 1:
75
+ raise ValueError("limit must be positive")
76
+ with self._error_limit_lock:
77
+ previous = self._error_limit
78
+ self._error_limit = int(limit)
79
+ self._error_limit_cache = self._error_limit
80
+ return previous
81
+
82
+ # ------------------------------------------------------------------
83
+ # Registry helpers
84
+ # ------------------------------------------------------------------
85
+ def _record_callback_error(
86
+ self,
87
+ G: "nx.Graph",
88
+ event: str,
89
+ ctx: dict[str, Any],
90
+ spec: CallbackSpec,
91
+ err: Exception,
92
+ ) -> None:
93
+ """Log and store a callback error for later inspection."""
94
+
95
+ logger.exception("callback %r failed for %s: %s", spec.name, event, err)
96
+ limit = self._error_limit_cache
97
+ err_list = G.graph.setdefault(
98
+ "_callback_errors", deque[CallbackError](maxlen=limit)
99
+ )
100
+ if err_list.maxlen != limit:
101
+ err_list = deque[CallbackError](err_list, maxlen=limit)
102
+ G.graph["_callback_errors"] = err_list
103
+ error: CallbackError = {
104
+ "event": event,
105
+ "step": ctx.get("step"),
106
+ "error": repr(err),
107
+ "traceback": traceback.format_exc(),
108
+ "fn": _func_id(spec.func),
109
+ "name": spec.name,
110
+ }
111
+ err_list.append(error)
112
+
113
+ def _ensure_callbacks_nolock(self, G: "nx.Graph") -> CallbackRegistry:
114
+ cbs = G.graph.setdefault("callbacks", defaultdict(dict))
115
+ dirty: set[str] = set(G.graph.pop("_callbacks_dirty", ()))
116
+ return _validate_registry(G, cbs, dirty)
117
+
118
+ def _ensure_callbacks(self, G: "nx.Graph") -> CallbackRegistry:
119
+ with self._lock:
120
+ return self._ensure_callbacks_nolock(G)
121
+
122
+ def register_callback(
123
+ self,
124
+ G: "nx.Graph",
125
+ event: CallbackEvent | str,
126
+ func: Callback,
127
+ *,
128
+ name: str | None = None,
129
+ ) -> Callback:
130
+ """Register ``func`` as callback for ``event``."""
131
+
132
+ event = _normalize_event(event)
133
+ _ensure_known_event(event)
134
+ if not callable(func):
135
+ raise TypeError("func must be callable")
136
+ with self._lock:
137
+ cbs = self._ensure_callbacks_nolock(G)
138
+
139
+ cb_name = name or getattr(func, "__name__", None)
140
+ spec = CallbackSpec(cb_name, func)
141
+ existing_map = cbs[event]
142
+ strict = bool(G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"]))
143
+ key = _reconcile_callback(event, existing_map, spec, strict)
144
+
145
+ existing_map[key] = spec
146
+ dirty = G.graph.setdefault("_callbacks_dirty", set())
147
+ dirty.add(event)
148
+ return func
149
+
150
+ def invoke_callbacks(
151
+ self,
152
+ G: "nx.Graph",
153
+ event: CallbackEvent | str,
154
+ ctx: dict[str, Any] | None = None,
155
+ ) -> None:
156
+ """Invoke all callbacks registered for ``event`` with context ``ctx``."""
157
+
158
+ event = _normalize_event(event)
159
+ with self._lock:
160
+ cbs = dict(self._ensure_callbacks_nolock(G).get(event, {}))
161
+ strict = bool(G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"]))
162
+ if ctx is None:
163
+ ctx = {}
164
+ for spec in cbs.values():
165
+ try:
166
+ spec.func(G, ctx)
167
+ except (
168
+ RuntimeError,
169
+ ValueError,
170
+ TypeError,
171
+ ) as e:
172
+ with self._lock:
173
+ self._record_callback_error(G, event, ctx, spec, e)
174
+ if strict:
175
+ raise
176
+ except nx.NetworkXError as err:
177
+ with self._lock:
178
+ self._record_callback_error(G, event, ctx, spec, err)
179
+ logger.exception(
180
+ "callback %r raised NetworkXError for %s with ctx=%r",
181
+ spec.name,
182
+ event,
183
+ ctx,
184
+ )
185
+ raise
186
+
187
+
188
+ Callback = Callable[["nx.Graph", dict[str, Any]], None]
189
+ CallbackRegistry = dict[str, dict[str, "CallbackSpec"]]
190
+
191
+
192
+ def _func_id(fn: Callable[..., Any]) -> str:
193
+ """Return a deterministic identifier for ``fn``.
194
+
195
+ Combines the function's module and qualified name to avoid the
196
+ nondeterminism of ``repr(fn)`` which includes the memory address.
197
+ """
198
+ module = getattr(fn, "__module__", fn.__class__.__module__)
199
+ qualname = getattr(
200
+ fn,
201
+ "__qualname__",
202
+ getattr(fn, "__name__", fn.__class__.__qualname__),
203
+ )
204
+ return f"{module}.{qualname}"
205
+
206
+
207
+ def _validate_registry(G: "nx.Graph", cbs: Any, dirty: set[str]) -> CallbackRegistry:
208
+ """Validate and normalise the callback registry.
209
+
210
+ ``cbs`` is coerced to a ``defaultdict(dict)`` and any events listed in
211
+ ``dirty`` are rebuilt using :func:`_normalize_callbacks`. Unknown events are
212
+ removed. The cleaned registry is stored back on the graph and returned.
213
+ """
214
+
215
+ if not isinstance(cbs, Mapping):
216
+ logger.warning(
217
+ "Invalid callbacks registry on graph; resetting to empty",
218
+ )
219
+ cbs = defaultdict(dict)
220
+ elif not isinstance(cbs, defaultdict) or cbs.default_factory is not dict:
221
+ cbs = defaultdict(
222
+ dict,
223
+ {
224
+ event: _normalize_callbacks(entries)
225
+ for event, entries in dict(cbs).items()
226
+ if _is_known_event(event)
227
+ },
228
+ )
229
+ else:
230
+ for event in dirty:
231
+ if _is_known_event(event):
232
+ cbs[event] = _normalize_callbacks(cbs.get(event))
233
+ else:
234
+ cbs.pop(event, None)
235
+
236
+ G.graph["callbacks"] = cbs
237
+ return cbs
238
+
239
+
240
+ def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]:
241
+ """Return ``entries`` normalised into a callback mapping."""
242
+ if isinstance(entries, Mapping):
243
+ entries_iter = entries.values()
244
+ elif isinstance(entries, Iterable) and not isinstance(
245
+ entries, (str, bytes, bytearray)
246
+ ):
247
+ entries_iter = entries
248
+ else:
249
+ return {}
250
+
251
+ new_map: dict[str, CallbackSpec] = {}
252
+ for entry in entries_iter:
253
+ spec = _normalize_callback_entry(entry)
254
+ if spec is None:
255
+ continue
256
+ key = spec.name or _func_id(spec.func)
257
+ new_map[key] = spec
258
+ return new_map
259
+
260
+
261
+ def _normalize_event(event: CallbackEvent | str) -> str:
262
+ """Return ``event`` as a string."""
263
+ return event.value if isinstance(event, CallbackEvent) else str(event)
264
+
265
+
266
+ def _is_known_event(event: str) -> bool:
267
+ """Return ``True`` when ``event`` matches a declared :class:`CallbackEvent`."""
268
+
269
+ try:
270
+ CallbackEvent(event)
271
+ except ValueError:
272
+ return False
273
+ else:
274
+ return True
275
+
276
+
277
+ def _ensure_known_event(event: str) -> None:
278
+ """Raise :class:`ValueError` when ``event`` is not a known callback."""
279
+
280
+ try:
281
+ CallbackEvent(event)
282
+ except ValueError as exc: # pragma: no cover - defensive branch
283
+ raise ValueError(f"Unknown event: {event}") from exc
284
+
285
+
286
+ def _normalize_callback_entry(entry: Any) -> "CallbackSpec | None":
287
+ """Normalize a callback specification.
288
+
289
+ Supported formats
290
+ -----------------
291
+ * :class:`CallbackSpec` instances (returned unchanged).
292
+ * Sequences ``(name: str, func: Callable)`` such as lists, tuples or other
293
+ iterables.
294
+ * Bare callables ``func`` whose name is taken from ``func.__name__``.
295
+
296
+ ``None`` is returned when ``entry`` does not match any of the accepted
297
+ formats. The original ``entry`` is never mutated. Sequence inputs are
298
+ converted to ``tuple`` before validation to support generators; the
299
+ materialization consumes the iterable and failure results in ``None``.
300
+ """
301
+
302
+ if isinstance(entry, CallbackSpec):
303
+ return entry
304
+ elif is_non_string_sequence(entry):
305
+ try:
306
+ entry = tuple(entry)
307
+ except TypeError:
308
+ return None
309
+ if len(entry) != 2:
310
+ return None
311
+ name, fn = entry
312
+ if not isinstance(name, str) or not callable(fn):
313
+ return None
314
+ return CallbackSpec(name, fn)
315
+ elif callable(entry):
316
+ name = getattr(entry, "__name__", None)
317
+ return CallbackSpec(name, entry)
318
+ else:
319
+ return None
320
+
321
+
322
+ def _reconcile_callback(
323
+ event: str,
324
+ existing_map: dict[str, CallbackSpec],
325
+ spec: CallbackSpec,
326
+ strict: bool,
327
+ ) -> str:
328
+ """Reconcile ``spec`` with ``existing_map``.
329
+
330
+ Ensures that callbacks remain unique by explicit name or function identity.
331
+ When a name collision occurs with a different function, ``strict`` controls
332
+ whether a :class:`ValueError` is raised or a warning is logged.
333
+
334
+ Parameters
335
+ ----------
336
+ event:
337
+ Event under which ``spec`` will be registered. Only used for messages.
338
+ existing_map:
339
+ Current mapping of callbacks for ``event``.
340
+ spec:
341
+ Callback specification being registered.
342
+ strict:
343
+ Whether to raise on name collisions instead of logging a warning.
344
+
345
+ Returns
346
+ -------
347
+ str
348
+ Key under which ``spec`` should be stored in ``existing_map``.
349
+ """
350
+
351
+ key = spec.name or _func_id(spec.func)
352
+
353
+ if spec.name is not None:
354
+ existing_spec = existing_map.get(key)
355
+ if existing_spec is not None and existing_spec.func is not spec.func:
356
+ msg = f"Callback {spec.name!r} already registered for {event}"
357
+ if strict:
358
+ raise ValueError(msg)
359
+ logger.warning(msg)
360
+
361
+ # Remove existing entries under the same key and any other using the same
362
+ # function identity to avoid duplicates.
363
+ existing_map.pop(key, None)
364
+ fn_key = next((k for k, s in existing_map.items() if s.func is spec.func), None)
365
+ if fn_key is not None:
366
+ existing_map.pop(fn_key, None)
367
+
368
+ return key
369
+
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # Default manager instance and convenience wrappers
373
+ # ---------------------------------------------------------------------------
374
+
375
+ callback_manager = CallbackManager()
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import networkx as nx
4
+ from ..types import CallbackError as CallbackError
5
+ from _typeshed import Incomplete
6
+ from collections.abc import Callable
7
+ from enum import Enum
8
+ from typing import Any, NamedTuple
9
+
10
+ __all__ = [
11
+ "CallbackEvent",
12
+ "CallbackManager",
13
+ "callback_manager",
14
+ "CallbackError",
15
+ "CallbackSpec",
16
+ ]
17
+
18
+ Callback = Callable[[nx.Graph, dict[str, Any]], None]
19
+
20
+ class CallbackSpec(NamedTuple):
21
+ name: str | None
22
+ func: Callable[..., Any]
23
+
24
+ class CallbackEvent(str, Enum):
25
+ BEFORE_STEP = "before_step"
26
+ AFTER_STEP = "after_step"
27
+ ON_REMESH = "on_remesh"
28
+ CACHE_METRICS = "cache_metrics"
29
+
30
+ class CallbackManager:
31
+ def __init__(self) -> None: ...
32
+ def get_callback_error_limit(self) -> int: ...
33
+ def set_callback_error_limit(self, limit: int) -> int: ...
34
+ def register_callback(
35
+ self,
36
+ G: nx.Graph,
37
+ event: CallbackEvent | str,
38
+ func: Callback,
39
+ *,
40
+ name: str | None = None,
41
+ ) -> Callback: ...
42
+ def invoke_callbacks(
43
+ self, G: nx.Graph, event: CallbackEvent | str, ctx: dict[str, Any] | None = None
44
+ ) -> None: ...
45
+
46
+ callback_manager: Incomplete
47
+
48
+ def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]: ...
49
+ def _normalize_callback_entry(entry: Any) -> CallbackSpec | None: ...
tnfr/utils/chunks.py ADDED
@@ -0,0 +1,108 @@
1
+ """Chunk sizing heuristics for batching structural computations.
2
+
3
+ The helpers in this module determine how large each processing block should be
4
+ when splitting work across workers or vectorised loops. They take into account
5
+ the number of items involved, approximate memory pressure, and available CPU
6
+ parallelism so the caller can balance throughput with deterministic behaviour.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ import os
13
+ from typing import Final
14
+
15
+ DEFAULT_APPROX_BYTES_PER_ITEM: Final[int] = 64
16
+ DEFAULT_CHUNK_CLAMP: Final[int] | None = 131_072
17
+
18
+
19
+ def _estimate_available_memory() -> int | None:
20
+ """Best-effort estimation of free memory available to the process."""
21
+
22
+ try:
23
+ page_size = os.sysconf("SC_PAGE_SIZE")
24
+ avail_pages = os.sysconf("SC_AVPHYS_PAGES")
25
+ except (
26
+ AttributeError,
27
+ ValueError,
28
+ OSError,
29
+ ): # pragma: no cover - platform specific
30
+ return None
31
+ if page_size <= 0 or avail_pages <= 0:
32
+ return None
33
+ return int(page_size) * int(avail_pages)
34
+
35
+
36
+ def auto_chunk_size(
37
+ total_items: int,
38
+ *,
39
+ minimum: int = 1,
40
+ approx_bytes_per_item: int = DEFAULT_APPROX_BYTES_PER_ITEM,
41
+ clamp_to: int | None = DEFAULT_CHUNK_CLAMP,
42
+ ) -> int:
43
+ """Infer a safe chunk length when the caller does not specify one."""
44
+
45
+ if total_items <= 0:
46
+ return 0
47
+
48
+ minimum = max(1, minimum)
49
+ approx_bytes_per_item = max(1, approx_bytes_per_item)
50
+
51
+ available_memory = _estimate_available_memory()
52
+ if available_memory is not None and available_memory > 0:
53
+ safe_bytes = max(approx_bytes_per_item * minimum, available_memory // 8)
54
+ mem_bound = max(minimum, min(total_items, safe_bytes // approx_bytes_per_item))
55
+ else:
56
+ mem_bound = total_items
57
+
58
+ if clamp_to is not None:
59
+ mem_bound = min(mem_bound, clamp_to)
60
+
61
+ cpu_count = os.cpu_count() or 1
62
+ target_chunks = max(1, cpu_count * 4)
63
+ cpu_chunk = max(minimum, math.ceil(total_items / target_chunks))
64
+ baseline = max(minimum, min(total_items, 1024))
65
+ target = max(cpu_chunk, baseline)
66
+
67
+ chunk = min(mem_bound, target)
68
+ chunk = max(minimum, min(total_items, chunk))
69
+ return chunk
70
+
71
+
72
+ def resolve_chunk_size(
73
+ chunk_size: int | None,
74
+ total_items: int,
75
+ *,
76
+ minimum: int = 1,
77
+ approx_bytes_per_item: int = DEFAULT_APPROX_BYTES_PER_ITEM,
78
+ clamp_to: int | None = DEFAULT_CHUNK_CLAMP,
79
+ ) -> int:
80
+ """Return a sanitised chunk size honouring automatic fallbacks."""
81
+
82
+ if total_items <= 0:
83
+ return 0
84
+
85
+ resolved: int | None
86
+ if chunk_size is None:
87
+ resolved = None
88
+ else:
89
+ try:
90
+ resolved = int(chunk_size)
91
+ except (TypeError, ValueError):
92
+ resolved = None
93
+ else:
94
+ if resolved <= 0:
95
+ resolved = None
96
+
97
+ if resolved is None:
98
+ resolved = auto_chunk_size(
99
+ total_items,
100
+ minimum=minimum,
101
+ approx_bytes_per_item=approx_bytes_per_item,
102
+ clamp_to=clamp_to,
103
+ )
104
+
105
+ return max(minimum, min(total_items, resolved))
106
+
107
+
108
+ __all__ = ["auto_chunk_size", "resolve_chunk_size"]
tnfr/utils/chunks.pyi ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Final
4
+
5
+ DEFAULT_APPROX_BYTES_PER_ITEM: Final[int]
6
+ DEFAULT_CHUNK_CLAMP: Final[int | None]
7
+
8
+ def auto_chunk_size(
9
+ total_items: int,
10
+ *,
11
+ minimum: int = ...,
12
+ approx_bytes_per_item: int = ...,
13
+ clamp_to: int | None = ...,
14
+ ) -> int: ...
15
+ def resolve_chunk_size(
16
+ chunk_size: int | None,
17
+ total_items: int,
18
+ *,
19
+ minimum: int = ...,
20
+ approx_bytes_per_item: int = ...,
21
+ clamp_to: int | None = ...,
22
+ ) -> int: ...