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,908 @@
1
+ """Runtime orchestration for TNFR dynamics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import sys
7
+ from copy import deepcopy
8
+ from collections import deque
9
+ from collections.abc import Iterable, Mapping, MutableMapping
10
+ from numbers import Real
11
+ from typing import Any, cast
12
+
13
+ from ..alias import get_attr
14
+ from ..utils import CallbackEvent, callback_manager
15
+ from ..constants import get_graph_param, get_param
16
+ from ..glyph_history import ensure_history
17
+ from ..metrics.sense_index import compute_Si
18
+ from ..operators import apply_remesh_if_globally_stable
19
+ from ..telemetry import publish_graph_cache_metrics
20
+ from ..types import HistoryState, NodeId, TNFRGraph
21
+ from ..utils import normalize_optional_int
22
+ from ..validation import apply_canonical_clamps
23
+ from . import adaptation, coordination, integrators, selectors
24
+ from .aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_THETA, ALIAS_VF
25
+
26
+ try: # pragma: no cover - optional NumPy dependency
27
+ import numpy as np
28
+ except ImportError: # pragma: no cover - optional dependency missing
29
+ np = None # type: ignore[assignment]
30
+
31
+ try: # pragma: no cover - optional math extras
32
+ from ..mathematics.dynamics import MathematicalDynamicsEngine
33
+ from ..mathematics.projection import BasicStateProjector
34
+ from ..mathematics.runtime import (
35
+ coherence as runtime_coherence,
36
+ frequency_positive as runtime_frequency_positive,
37
+ normalized as runtime_normalized,
38
+ )
39
+ except Exception: # pragma: no cover - fallback when extras not available
40
+ MathematicalDynamicsEngine = None # type: ignore[assignment]
41
+ BasicStateProjector = None # type: ignore[assignment]
42
+ runtime_coherence = None # type: ignore[assignment]
43
+ runtime_frequency_positive = None # type: ignore[assignment]
44
+ runtime_normalized = None # type: ignore[assignment]
45
+ from .dnfr import default_compute_delta_nfr
46
+ from .sampling import update_node_sample as _update_node_sample
47
+
48
+ __all__ = (
49
+ "ALIAS_VF",
50
+ "ALIAS_DNFR",
51
+ "ALIAS_EPI",
52
+ "ALIAS_SI",
53
+ "_normalize_job_overrides",
54
+ "_resolve_jobs_override",
55
+ "_prepare_dnfr",
56
+ "_update_nodes",
57
+ "_update_epi_hist",
58
+ "_maybe_remesh",
59
+ "_run_validators",
60
+ "_run_before_callbacks",
61
+ "_run_after_callbacks",
62
+ "step",
63
+ "run",
64
+ )
65
+
66
+
67
+ def _normalize_job_overrides(
68
+ job_overrides: Mapping[str, Any] | None,
69
+ ) -> dict[str, Any]:
70
+ """Canonicalise job override keys for ΔNFR, νf and phase orchestration.
71
+
72
+ Parameters
73
+ ----------
74
+ job_overrides : Mapping[str, Any] | None
75
+ User-provided mapping whose keys may use legacy ``*_N_JOBS`` forms or
76
+ mixed casing. The values tune the parallel workloads that update ΔNFR,
77
+ νf adaptation and global phase coordination.
78
+
79
+ Returns
80
+ -------
81
+ dict[str, Any]
82
+ A dictionary where keys are upper-cased without the ``_N_JOBS`` suffix,
83
+ ready for downstream lookup in the runtime schedulers.
84
+
85
+ Notes
86
+ -----
87
+ ``None`` keys are silently skipped to preserve resiliency when
88
+ orchestrating ΔNFR workers.
89
+
90
+ Examples
91
+ --------
92
+ >>> _normalize_job_overrides({"dnfr_n_jobs": 2, "vf_adapt": 4})
93
+ {'DNFR': 2, 'VF_ADAPT': 4}
94
+ >>> _normalize_job_overrides(None)
95
+ {}
96
+ """
97
+ if not job_overrides:
98
+ return {}
99
+
100
+ normalized: dict[str, Any] = {}
101
+ for key, value in job_overrides.items():
102
+ if key is None:
103
+ continue
104
+ key_str = str(key).upper()
105
+ if key_str.endswith("_N_JOBS"):
106
+ key_str = key_str[: -len("_N_JOBS")]
107
+ normalized[key_str] = value
108
+ return normalized
109
+
110
+
111
+ def _resolve_jobs_override(
112
+ overrides: Mapping[str, Any],
113
+ key: str,
114
+ graph_value: Any,
115
+ *,
116
+ allow_non_positive: bool,
117
+ ) -> int | None:
118
+ """Resolve job overrides prioritising user hints over graph defaults.
119
+
120
+ Parameters
121
+ ----------
122
+ overrides : Mapping[str, Any]
123
+ Normalised overrides produced by :func:`_normalize_job_overrides` that
124
+ steer the ΔNFR computation, νf adaptation or phase coupling workers.
125
+ key : str
126
+ Logical subsystem key such as ``"DNFR"`` or ``"VF_ADAPT"``.
127
+ graph_value : Any
128
+ Baseline job count stored in the graph configuration.
129
+ allow_non_positive : bool
130
+ Propagated policy describing whether zero or negative values are valid
131
+ for the subsystem.
132
+
133
+ Returns
134
+ -------
135
+ int | None
136
+ Final job count that each scheduler will use, or ``None`` when no
137
+ explicit override or valid fallback exists.
138
+
139
+ Notes
140
+ -----
141
+ Preference resolution is pure and returns ``None`` instead of raising when
142
+ overrides cannot be coerced into valid integers.
143
+
144
+ Examples
145
+ --------
146
+ >>> overrides = _normalize_job_overrides({"phase": 0})
147
+ >>> _resolve_jobs_override(overrides, "phase", 2, allow_non_positive=True)
148
+ 0
149
+ >>> _resolve_jobs_override({}, "vf_adapt", 4, allow_non_positive=False)
150
+ 4
151
+ """
152
+ norm_key = key.upper()
153
+ if overrides and norm_key in overrides:
154
+ return normalize_optional_int(
155
+ overrides.get(norm_key),
156
+ allow_non_positive=allow_non_positive,
157
+ strict=False,
158
+ sentinels=None,
159
+ )
160
+
161
+ return normalize_optional_int(
162
+ graph_value,
163
+ allow_non_positive=allow_non_positive,
164
+ strict=False,
165
+ sentinels=None,
166
+ )
167
+
168
+
169
+ _INTEGRATOR_CACHE_KEY = "_integrator_cache"
170
+
171
+
172
+ def _call_integrator_factory(factory: Any, G: TNFRGraph) -> Any:
173
+ """Invoke an integrator factory respecting optional graph injection."""
174
+
175
+ try:
176
+ signature = inspect.signature(factory)
177
+ except (TypeError, ValueError):
178
+ return factory()
179
+
180
+ params = list(signature.parameters.values())
181
+ required = [
182
+ p
183
+ for p in params
184
+ if p.default is inspect._empty
185
+ and p.kind
186
+ in (
187
+ inspect.Parameter.POSITIONAL_ONLY,
188
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
189
+ inspect.Parameter.KEYWORD_ONLY,
190
+ )
191
+ ]
192
+
193
+ if any(p.kind is inspect.Parameter.KEYWORD_ONLY for p in required):
194
+ raise TypeError(
195
+ "Integrator factory cannot require keyword-only arguments",
196
+ )
197
+
198
+ positional_required = [
199
+ p
200
+ for p in required
201
+ if p.kind
202
+ in (
203
+ inspect.Parameter.POSITIONAL_ONLY,
204
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
205
+ )
206
+ ]
207
+ if len(positional_required) > 1:
208
+ raise TypeError(
209
+ "Integrator factory must accept at most one positional argument",
210
+ )
211
+
212
+ if positional_required:
213
+ return factory(G)
214
+
215
+ positional = [
216
+ p
217
+ for p in params
218
+ if p.kind
219
+ in (
220
+ inspect.Parameter.POSITIONAL_ONLY,
221
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
222
+ )
223
+ ]
224
+ if len(positional) == 1:
225
+ return factory(G)
226
+ elif len(positional) > 1:
227
+ raise TypeError(
228
+ "Integrator factory must accept at most one positional argument",
229
+ )
230
+
231
+ # Check for any remaining required positional or keyword-only arguments
232
+ remaining_required = [
233
+ p
234
+ for p in params
235
+ if p.default is inspect._empty
236
+ and p.kind
237
+ in (
238
+ inspect.Parameter.POSITIONAL_ONLY,
239
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
240
+ inspect.Parameter.KEYWORD_ONLY,
241
+ )
242
+ ]
243
+ if remaining_required:
244
+ raise TypeError(
245
+ f"Integrator factory requires arguments: {', '.join(p.name for p in remaining_required)}"
246
+ )
247
+ return factory()
248
+
249
+
250
+ def _resolve_integrator_instance(G: TNFRGraph) -> integrators.AbstractIntegrator:
251
+ """Return an integrator instance configured on ``G`` or a default."""
252
+
253
+ cache_entry = G.graph.get(_INTEGRATOR_CACHE_KEY)
254
+ candidate = G.graph.get("integrator")
255
+ if (
256
+ isinstance(cache_entry, tuple)
257
+ and len(cache_entry) == 2
258
+ and cache_entry[0] is candidate
259
+ and isinstance(cache_entry[1], integrators.AbstractIntegrator)
260
+ ):
261
+ return cache_entry[1]
262
+
263
+ if isinstance(candidate, integrators.AbstractIntegrator):
264
+ instance = candidate
265
+ elif inspect.isclass(candidate) and issubclass(
266
+ candidate, integrators.AbstractIntegrator
267
+ ):
268
+ instance = candidate()
269
+ elif callable(candidate):
270
+ instance = cast(
271
+ integrators.AbstractIntegrator,
272
+ _call_integrator_factory(candidate, G),
273
+ )
274
+ elif candidate is None:
275
+ instance = integrators.DefaultIntegrator()
276
+ else:
277
+ raise TypeError(
278
+ "Graph integrator must be an AbstractIntegrator, subclass or callable",
279
+ )
280
+
281
+ if not isinstance(instance, integrators.AbstractIntegrator):
282
+ raise TypeError(
283
+ "Configured integrator must implement AbstractIntegrator.integrate",
284
+ )
285
+
286
+ G.graph[_INTEGRATOR_CACHE_KEY] = (candidate, instance)
287
+ return instance
288
+
289
+
290
+ def _run_before_callbacks(
291
+ G: TNFRGraph,
292
+ *,
293
+ step_idx: int,
294
+ dt: float | None,
295
+ use_Si: bool,
296
+ apply_glyphs: bool,
297
+ ) -> None:
298
+ """Notify ``BEFORE_STEP`` observers with execution context."""
299
+
300
+ callback_manager.invoke_callbacks(
301
+ G,
302
+ CallbackEvent.BEFORE_STEP.value,
303
+ {
304
+ "step": step_idx,
305
+ "dt": dt,
306
+ "use_Si": use_Si,
307
+ "apply_glyphs": apply_glyphs,
308
+ },
309
+ )
310
+
311
+
312
+ def _prepare_dnfr(
313
+ G: TNFRGraph,
314
+ *,
315
+ use_Si: bool,
316
+ job_overrides: Mapping[str, Any] | None = None,
317
+ ) -> None:
318
+ """Recompute ΔNFR (and optionally Si) ahead of an integration step."""
319
+
320
+ compute_dnfr_cb = G.graph.get("compute_delta_nfr", default_compute_delta_nfr)
321
+ overrides = job_overrides or {}
322
+ n_jobs = _resolve_jobs_override(
323
+ overrides,
324
+ "DNFR",
325
+ G.graph.get("DNFR_N_JOBS"),
326
+ allow_non_positive=False,
327
+ )
328
+
329
+ supports_n_jobs = False
330
+ try:
331
+ signature = inspect.signature(compute_dnfr_cb)
332
+ except (TypeError, ValueError):
333
+ signature = None
334
+ if signature is not None:
335
+ params = signature.parameters
336
+ if "n_jobs" in params:
337
+ kind = params["n_jobs"].kind
338
+ supports_n_jobs = kind in (
339
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
340
+ inspect.Parameter.KEYWORD_ONLY,
341
+ )
342
+ elif any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()):
343
+ supports_n_jobs = True
344
+
345
+ if supports_n_jobs:
346
+ compute_dnfr_cb(G, n_jobs=n_jobs)
347
+ else:
348
+ try:
349
+ compute_dnfr_cb(G, n_jobs=n_jobs)
350
+ except TypeError as exc:
351
+ if "n_jobs" in str(exc):
352
+ compute_dnfr_cb(G)
353
+ else:
354
+ raise
355
+ G.graph.pop("_sel_norms", None)
356
+ if use_Si:
357
+ si_jobs = _resolve_jobs_override(
358
+ overrides,
359
+ "SI",
360
+ G.graph.get("SI_N_JOBS"),
361
+ allow_non_positive=False,
362
+ )
363
+ dynamics_module = sys.modules.get("tnfr.dynamics")
364
+ compute_si_fn = (
365
+ getattr(dynamics_module, "compute_Si", None)
366
+ if dynamics_module is not None
367
+ else None
368
+ )
369
+ if compute_si_fn is None:
370
+ compute_si_fn = compute_Si
371
+ compute_si_fn(G, inplace=True, n_jobs=si_jobs)
372
+
373
+
374
+ def _update_nodes(
375
+ G: TNFRGraph,
376
+ *,
377
+ dt: float | None,
378
+ use_Si: bool,
379
+ apply_glyphs: bool,
380
+ step_idx: int,
381
+ hist: HistoryState,
382
+ job_overrides: Mapping[str, Any] | None = None,
383
+ ) -> None:
384
+ """Apply glyphs, integrate ΔNFR and refresh derived nodal state."""
385
+
386
+ _update_node_sample(G, step=step_idx)
387
+ overrides = job_overrides or {}
388
+ _prepare_dnfr(G, use_Si=use_Si, job_overrides=overrides)
389
+ selector = selectors._apply_selector(G)
390
+ if apply_glyphs:
391
+ selectors._apply_glyphs(G, selector, hist)
392
+ _dt = get_graph_param(G, "DT") if dt is None else float(dt)
393
+ method = get_graph_param(G, "INTEGRATOR_METHOD", str)
394
+ n_jobs = _resolve_jobs_override(
395
+ overrides,
396
+ "INTEGRATOR",
397
+ G.graph.get("INTEGRATOR_N_JOBS"),
398
+ allow_non_positive=True,
399
+ )
400
+ integrator = _resolve_integrator_instance(G)
401
+ integrator.integrate(
402
+ G,
403
+ dt=_dt,
404
+ t=None,
405
+ method=cast(str | None, method),
406
+ n_jobs=n_jobs,
407
+ )
408
+ for n, nd in G.nodes(data=True):
409
+ apply_canonical_clamps(cast(MutableMapping[str, Any], nd), G, cast(NodeId, n))
410
+ phase_jobs = _resolve_jobs_override(
411
+ overrides,
412
+ "PHASE",
413
+ G.graph.get("PHASE_N_JOBS"),
414
+ allow_non_positive=True,
415
+ )
416
+ coordination.coordinate_global_local_phase(G, None, None, n_jobs=phase_jobs)
417
+ vf_jobs = _resolve_jobs_override(
418
+ overrides,
419
+ "VF_ADAPT",
420
+ G.graph.get("VF_ADAPT_N_JOBS"),
421
+ allow_non_positive=False,
422
+ )
423
+ adaptation.adapt_vf_by_coherence(G, n_jobs=vf_jobs)
424
+
425
+
426
+ def _update_epi_hist(G: TNFRGraph) -> None:
427
+ """Maintain the rolling EPI history used by remeshing heuristics."""
428
+
429
+ tau_g = int(get_param(G, "REMESH_TAU_GLOBAL"))
430
+ tau_l = int(get_param(G, "REMESH_TAU_LOCAL"))
431
+ tau = max(tau_g, tau_l)
432
+ maxlen = max(2 * tau + 5, 64)
433
+ epi_hist = G.graph.get("_epi_hist")
434
+ if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
435
+ epi_hist = deque(list(epi_hist or [])[-maxlen:], maxlen=maxlen)
436
+ G.graph["_epi_hist"] = epi_hist
437
+ epi_hist.append({n: get_attr(nd, ALIAS_EPI, 0.0) for n, nd in G.nodes(data=True)})
438
+
439
+
440
+ def _maybe_remesh(G: TNFRGraph) -> None:
441
+ """Trigger remeshing when stability thresholds are satisfied."""
442
+
443
+ apply_remesh_if_globally_stable(G)
444
+
445
+
446
+ def _run_validators(G: TNFRGraph) -> None:
447
+ """Execute registered validators ensuring canonical invariants hold."""
448
+
449
+ from ..validation import run_validators
450
+
451
+ run_validators(G)
452
+
453
+
454
+ def _run_after_callbacks(G, *, step_idx: int) -> None:
455
+ """Notify ``AFTER_STEP`` observers with the latest structural metrics."""
456
+
457
+ h = ensure_history(G)
458
+ ctx = {"step": step_idx}
459
+ metric_pairs = [
460
+ ("C", "C_steps"),
461
+ ("stable_frac", "stable_frac"),
462
+ ("phase_sync", "phase_sync"),
463
+ ("glyph_disr", "glyph_load_disr"),
464
+ ("Si_mean", "Si_mean"),
465
+ ]
466
+ for dst, src in metric_pairs:
467
+ values = h.get(src)
468
+ if values:
469
+ ctx[dst] = values[-1]
470
+ callback_manager.invoke_callbacks(G, CallbackEvent.AFTER_STEP.value, ctx)
471
+
472
+ telemetry = G.graph.get("telemetry")
473
+ if isinstance(telemetry, MutableMapping):
474
+ history = telemetry.get("nu_f")
475
+ history_key = "nu_f_history"
476
+ if isinstance(history, list) and history_key not in telemetry:
477
+ telemetry[history_key] = history
478
+ payload = telemetry.get("nu_f_snapshot")
479
+ if isinstance(payload, Mapping):
480
+ bridge_raw = telemetry.get("nu_f_bridge")
481
+ try:
482
+ bridge = float(bridge_raw) if bridge_raw is not None else None
483
+ except (TypeError, ValueError):
484
+ bridge = None
485
+ nu_f_summary = {
486
+ "total_reorganisations": payload.get("total_reorganisations"),
487
+ "total_duration": payload.get("total_duration"),
488
+ "rate_hz_str": payload.get("rate_hz_str"),
489
+ "rate_hz": payload.get("rate_hz"),
490
+ "variance_hz_str": payload.get("variance_hz_str"),
491
+ "variance_hz": payload.get("variance_hz"),
492
+ "confidence_level": payload.get("confidence_level"),
493
+ "ci_hz_str": {
494
+ "lower": payload.get("ci_lower_hz_str"),
495
+ "upper": payload.get("ci_upper_hz_str"),
496
+ },
497
+ "ci_hz": {
498
+ "lower": payload.get("ci_lower_hz"),
499
+ "upper": payload.get("ci_upper_hz"),
500
+ },
501
+ "bridge": bridge,
502
+ }
503
+ telemetry["nu_f"] = nu_f_summary
504
+ math_summary = telemetry.get("math_engine")
505
+ if isinstance(math_summary, MutableMapping):
506
+ math_summary["nu_f"] = dict(nu_f_summary)
507
+
508
+
509
+ def _get_math_engine_config(G: TNFRGraph) -> MutableMapping[str, Any] | None:
510
+ """Return the mutable math-engine configuration stored on ``G``."""
511
+
512
+ cfg_raw = G.graph.get("MATH_ENGINE")
513
+ if not isinstance(cfg_raw, Mapping) or not cfg_raw.get("enabled", False):
514
+ return None
515
+ if isinstance(cfg_raw, MutableMapping):
516
+ return cfg_raw
517
+ cfg_mutable: MutableMapping[str, Any] = dict(cfg_raw)
518
+ G.graph["MATH_ENGINE"] = cfg_mutable
519
+ return cfg_mutable
520
+
521
+
522
+ def _initialise_math_state(
523
+ G: TNFRGraph,
524
+ cfg: MutableMapping[str, Any],
525
+ *,
526
+ hilbert_space: Any,
527
+ projector: BasicStateProjector,
528
+ ) -> np.ndarray | None:
529
+ """Project graph nodes into the Hilbert space to seed the math engine."""
530
+
531
+ dimension = getattr(hilbert_space, "dimension", None)
532
+ if dimension is None:
533
+ raise AttributeError("Hilbert space configuration is missing 'dimension'.")
534
+
535
+ vectors: list[np.ndarray] = []
536
+ for _, nd in G.nodes(data=True):
537
+ epi = float(get_attr(nd, ALIAS_EPI, 0.0) or 0.0)
538
+ nu_f = float(get_attr(nd, ALIAS_VF, 0.0) or 0.0)
539
+ theta_val = float(get_attr(nd, ALIAS_THETA, 0.0) or 0.0)
540
+ try:
541
+ vector = projector(epi=epi, nu_f=nu_f, theta=theta_val, dim=int(dimension))
542
+ except ValueError:
543
+ continue
544
+ vectors.append(np.asarray(vector, dtype=np.complex128))
545
+
546
+ if not vectors:
547
+ return None
548
+
549
+ stacked = np.vstack(vectors)
550
+ averaged = np.mean(stacked, axis=0)
551
+ atol = float(getattr(projector, "atol", 1e-9))
552
+ norm = float(getattr(hilbert_space, "norm")(averaged))
553
+ if np.isclose(norm, 0.0, atol=atol):
554
+ averaged = vectors[0]
555
+ norm = float(getattr(hilbert_space, "norm")(averaged))
556
+ if np.isclose(norm, 0.0, atol=atol):
557
+ return None
558
+ normalised = averaged / norm
559
+ cfg.setdefault("_state_origin", "projected")
560
+ return normalised
561
+
562
+
563
+ def _advance_math_engine(
564
+ G: TNFRGraph,
565
+ *,
566
+ dt: float,
567
+ step_idx: int,
568
+ hist: HistoryState,
569
+ ) -> None:
570
+ """Advance the optional math engine and record spectral telemetry."""
571
+
572
+ cfg = _get_math_engine_config(G)
573
+ if cfg is None:
574
+ return
575
+
576
+ if (
577
+ np is None
578
+ or MathematicalDynamicsEngine is None
579
+ or runtime_normalized is None
580
+ or runtime_coherence is None
581
+ ):
582
+ raise RuntimeError(
583
+ "Mathematical dynamics require NumPy and tnfr.mathematics extras to be installed."
584
+ )
585
+
586
+ hilbert_space = cfg.get("hilbert_space")
587
+ coherence_operator = cfg.get("coherence_operator")
588
+ coherence_threshold = cfg.get("coherence_threshold")
589
+ if (
590
+ hilbert_space is None
591
+ or coherence_operator is None
592
+ or coherence_threshold is None
593
+ ):
594
+ raise ValueError(
595
+ "MATH_ENGINE requires 'hilbert_space', 'coherence_operator' and "
596
+ "'coherence_threshold' entries."
597
+ )
598
+
599
+ if BasicStateProjector is None: # pragma: no cover - guarded by import above
600
+ raise RuntimeError(
601
+ "Mathematical dynamics require the BasicStateProjector helper."
602
+ )
603
+
604
+ projector = cfg.get("state_projector")
605
+ if not isinstance(projector, BasicStateProjector):
606
+ projector = BasicStateProjector()
607
+ cfg["state_projector"] = projector
608
+
609
+ engine = cfg.get("dynamics_engine")
610
+ if not isinstance(engine, MathematicalDynamicsEngine):
611
+ generator = cfg.get("generator_matrix")
612
+ if generator is None:
613
+ raise ValueError(
614
+ "MATH_ENGINE requires either a 'dynamics_engine' instance or a "
615
+ "'generator_matrix'."
616
+ )
617
+ generator_matrix = np.asarray(generator, dtype=np.complex128)
618
+ engine = MathematicalDynamicsEngine(
619
+ generator_matrix, hilbert_space=hilbert_space
620
+ )
621
+ cfg["dynamics_engine"] = engine
622
+
623
+ state_vector = cfg.get("_state_vector")
624
+ if state_vector is None:
625
+ state_vector = _initialise_math_state(
626
+ G,
627
+ cfg,
628
+ hilbert_space=hilbert_space,
629
+ projector=projector,
630
+ )
631
+ if state_vector is None:
632
+ return
633
+ else:
634
+ state_vector = np.asarray(state_vector, dtype=np.complex128)
635
+ dimension = getattr(hilbert_space, "dimension", state_vector.shape[0])
636
+ if state_vector.shape != (int(dimension),):
637
+ state_vector = _initialise_math_state(
638
+ G,
639
+ cfg,
640
+ hilbert_space=hilbert_space,
641
+ projector=projector,
642
+ )
643
+ if state_vector is None:
644
+ return
645
+
646
+ advanced = engine.step(state_vector, dt=float(dt), normalize=True)
647
+ cfg["_state_vector"] = advanced
648
+
649
+ atol = float(cfg.get("atol", getattr(engine, "atol", 1e-9)))
650
+ label = f"step[{step_idx}]"
651
+
652
+ normalized_passed, norm_value = runtime_normalized(
653
+ advanced,
654
+ hilbert_space,
655
+ atol=atol,
656
+ label=label,
657
+ )
658
+ coherence_passed, coherence_value = runtime_coherence(
659
+ advanced,
660
+ coherence_operator,
661
+ float(coherence_threshold),
662
+ normalise=False,
663
+ atol=atol,
664
+ label=label,
665
+ )
666
+
667
+ frequency_operator = cfg.get("frequency_operator")
668
+ frequency_summary: dict[str, Any] | None = None
669
+ if frequency_operator is not None:
670
+ if runtime_frequency_positive is None: # pragma: no cover - guarded above
671
+ raise RuntimeError(
672
+ "Frequency positivity checks require tnfr.mathematics extras."
673
+ )
674
+ freq_raw = runtime_frequency_positive(
675
+ advanced,
676
+ frequency_operator,
677
+ normalise=False,
678
+ enforce=True,
679
+ atol=atol,
680
+ label=label,
681
+ )
682
+ frequency_summary = {
683
+ "passed": bool(freq_raw.get("passed", False)),
684
+ "value": float(freq_raw.get("value", 0.0)),
685
+ "projection_passed": bool(freq_raw.get("projection_passed", False)),
686
+ "spectrum_psd": bool(freq_raw.get("spectrum_psd", False)),
687
+ "enforced": bool(freq_raw.get("enforce", True)),
688
+ }
689
+ if "spectrum_min" in freq_raw:
690
+ frequency_summary["spectrum_min"] = float(freq_raw.get("spectrum_min", 0.0))
691
+
692
+ summary = {
693
+ "step": step_idx,
694
+ "normalized": bool(normalized_passed),
695
+ "norm": float(norm_value),
696
+ "coherence": {
697
+ "passed": bool(coherence_passed),
698
+ "value": float(coherence_value),
699
+ "threshold": float(coherence_threshold),
700
+ },
701
+ "frequency": frequency_summary,
702
+ }
703
+
704
+ hist.setdefault("math_engine_summary", []).append(summary)
705
+ hist.setdefault("math_engine_norm", []).append(summary["norm"])
706
+ hist.setdefault("math_engine_normalized", []).append(summary["normalized"])
707
+ hist.setdefault("math_engine_coherence", []).append(summary["coherence"]["value"])
708
+ hist.setdefault("math_engine_coherence_passed", []).append(
709
+ summary["coherence"]["passed"]
710
+ )
711
+
712
+ if frequency_summary is None:
713
+ hist.setdefault("math_engine_frequency", []).append(None)
714
+ hist.setdefault("math_engine_frequency_passed", []).append(None)
715
+ hist.setdefault("math_engine_frequency_projection_passed", []).append(None)
716
+ else:
717
+ hist.setdefault("math_engine_frequency", []).append(frequency_summary["value"])
718
+ hist.setdefault("math_engine_frequency_passed", []).append(
719
+ frequency_summary["passed"]
720
+ )
721
+ hist.setdefault("math_engine_frequency_projection_passed", []).append(
722
+ frequency_summary["projection_passed"]
723
+ )
724
+
725
+ cfg["last_summary"] = summary
726
+ telemetry = G.graph.setdefault("telemetry", {})
727
+ telemetry["math_engine"] = deepcopy(summary)
728
+
729
+
730
+ def step(
731
+ G: TNFRGraph,
732
+ *,
733
+ dt: float | None = None,
734
+ use_Si: bool = True,
735
+ apply_glyphs: bool = True,
736
+ n_jobs: Mapping[str, Any] | None = None,
737
+ ) -> None:
738
+ """Advance the runtime one ΔNFR step updating νf, phase and glyphs.
739
+
740
+ Parameters
741
+ ----------
742
+ G : TNFRGraph
743
+ Graph whose nodes store EPI, νf and phase metadata. The graph must
744
+ expose a ΔNFR hook under ``G.graph['compute_delta_nfr']`` and optional
745
+ selector or callback registrations.
746
+ dt : float | None, optional
747
+ Time increment injected into the integrator. ``None`` falls back to the
748
+ ``DT`` attribute stored in ``G.graph`` which keeps ΔNFR integration
749
+ aligned with the nodal equation.
750
+ use_Si : bool, default True
751
+ When ``True`` the Sense Index (Si) is recomputed to modulate ΔNFR and
752
+ νf adaptation heuristics.
753
+ apply_glyphs : bool, default True
754
+ Enables canonical glyph selection so that phase and coherence glyphs
755
+ continue to modulate ΔNFR.
756
+ n_jobs : Mapping[str, Any] | None, optional
757
+ Optional overrides that tune the parallel workers used for ΔNFR, phase
758
+ coordination and νf adaptation. The mapping is processed by
759
+ :func:`_normalize_job_overrides`.
760
+
761
+ Returns
762
+ -------
763
+ None
764
+ Mutates ``G`` in place by recomputing ΔNFR, νf and phase metrics.
765
+
766
+ Notes
767
+ -----
768
+ Registered callbacks execute within :func:`step` and any exceptions they
769
+ raise propagate according to the callback manager configuration.
770
+
771
+ Examples
772
+ --------
773
+ Register a hook that records phase synchrony while using the parametric
774
+ selector to choose glyphs before advancing one runtime step.
775
+
776
+ >>> from tnfr.utils import CallbackEvent, callback_manager
777
+ >>> from tnfr.dynamics import selectors
778
+ >>> from tnfr.dynamics.runtime import ALIAS_VF
779
+ >>> from tnfr.structural import create_nfr
780
+ >>> G, node = create_nfr("seed", epi=0.2, vf=1.5)
781
+ >>> callback_manager.register_callback(
782
+ ... G,
783
+ ... CallbackEvent.AFTER_STEP,
784
+ ... lambda graph, ctx: graph.graph.setdefault("phase_log", []).append(ctx.get("phase_sync")),
785
+ ... )
786
+ >>> G.graph["glyph_selector"] = selectors.ParametricGlyphSelector()
787
+ >>> step(G, dt=0.05, n_jobs={"dnfr_n_jobs": 1})
788
+ >>> ALIAS_VF in G.nodes[node]
789
+ True
790
+ """
791
+ job_overrides = _normalize_job_overrides(n_jobs)
792
+ hist = ensure_history(G)
793
+ step_idx = len(hist.setdefault("C_steps", []))
794
+ _run_before_callbacks(
795
+ G, step_idx=step_idx, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs
796
+ )
797
+ _update_nodes(
798
+ G,
799
+ dt=dt,
800
+ use_Si=use_Si,
801
+ apply_glyphs=apply_glyphs,
802
+ step_idx=step_idx,
803
+ hist=hist,
804
+ job_overrides=job_overrides,
805
+ )
806
+ resolved_dt = get_graph_param(G, "DT") if dt is None else float(dt)
807
+ _advance_math_engine(
808
+ G,
809
+ dt=resolved_dt,
810
+ step_idx=step_idx,
811
+ hist=hist,
812
+ )
813
+ _update_epi_hist(G)
814
+ _maybe_remesh(G)
815
+ _run_validators(G)
816
+ _run_after_callbacks(G, step_idx=step_idx)
817
+ publish_graph_cache_metrics(G)
818
+
819
+
820
+ def run(
821
+ G: TNFRGraph,
822
+ steps: int,
823
+ *,
824
+ dt: float | None = None,
825
+ use_Si: bool = True,
826
+ apply_glyphs: bool = True,
827
+ n_jobs: Mapping[str, Any] | None = None,
828
+ ) -> None:
829
+ """Iterate :func:`step` to evolve ΔNFR, νf and phase trajectories.
830
+
831
+ Parameters
832
+ ----------
833
+ G : TNFRGraph
834
+ Graph that stores the coherent structures. Callbacks and selectors
835
+ configured on ``G.graph`` orchestrate glyph application and telemetry.
836
+ steps : int
837
+ Number of times :func:`step` is invoked. Each iteration integrates ΔNFR
838
+ and νf according to ``dt`` and the configured selector.
839
+ dt : float | None, optional
840
+ Time increment for each step. ``None`` uses the graph's default ``DT``.
841
+ use_Si : bool, default True
842
+ Recompute the Sense Index during each iteration to keep ΔNFR feedback
843
+ loops tied to νf adjustments.
844
+ apply_glyphs : bool, default True
845
+ Enables glyph selection and application per step.
846
+ n_jobs : Mapping[str, Any] | None, optional
847
+ Shared overrides forwarded to each :func:`step` call.
848
+
849
+ Returns
850
+ -------
851
+ None
852
+ The graph ``G`` is updated in place.
853
+
854
+ Raises
855
+ ------
856
+ ValueError
857
+ Raised when ``steps`` is negative because the runtime cannot evolve a
858
+ negative number of ΔNFR updates.
859
+
860
+ Examples
861
+ --------
862
+ Install a before-step callback and use the default glyph selector while
863
+ running two iterations that synchronise phase and νf.
864
+
865
+ >>> from tnfr.utils import CallbackEvent, callback_manager
866
+ >>> from tnfr.dynamics import selectors
867
+ >>> from tnfr.structural import create_nfr
868
+ >>> G, node = create_nfr("seed", epi=0.3, vf=1.2)
869
+ >>> callback_manager.register_callback(
870
+ ... G,
871
+ ... CallbackEvent.BEFORE_STEP,
872
+ ... lambda graph, ctx: graph.graph.setdefault("dt_trace", []).append(ctx["dt"]),
873
+ ... )
874
+ >>> G.graph["glyph_selector"] = selectors.default_glyph_selector
875
+ >>> run(G, 2, dt=0.1)
876
+ >>> len(G.graph["dt_trace"])
877
+ 2
878
+ """
879
+ steps_int = int(steps)
880
+ if steps_int < 0:
881
+ raise ValueError("'steps' must be non-negative")
882
+ stop_cfg = get_graph_param(G, "STOP_EARLY", dict)
883
+ stop_enabled = False
884
+ if stop_cfg and stop_cfg.get("enabled", False):
885
+ w = max(1, int(stop_cfg.get("window", 25)))
886
+ frac = float(stop_cfg.get("fraction", 0.90))
887
+ stop_enabled = True
888
+ job_overrides = _normalize_job_overrides(n_jobs)
889
+ for _ in range(steps_int):
890
+ step(
891
+ G,
892
+ dt=dt,
893
+ use_Si=use_Si,
894
+ apply_glyphs=apply_glyphs,
895
+ n_jobs=job_overrides,
896
+ )
897
+ if stop_enabled:
898
+ history = ensure_history(G)
899
+ raw_series = dict.get(history, "stable_frac", [])
900
+ if not isinstance(raw_series, Iterable):
901
+ series = []
902
+ elif isinstance(raw_series, list):
903
+ series = raw_series
904
+ else:
905
+ series = list(raw_series)
906
+ numeric_series = [v for v in series if isinstance(v, Real)]
907
+ if len(numeric_series) >= w and all(v >= frac for v in numeric_series[-w:]):
908
+ break