tnfr 4.5.2__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 (365) hide show
  1. tnfr/__init__.py +334 -50
  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 +214 -37
  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 +149 -556
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +51 -16
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +344 -32
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +676 -50
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +18 -3
  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/{constants_glyphs.py → config/constants.py} +26 -20
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/{constants/core.py → config/defaults_core.py} +59 -6
  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 +51 -133
  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 +3 -1
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +9 -15
  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 +213 -633
  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 +2699 -398
  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 +496 -102
  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 +10 -5
  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 +77 -55
  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 +29 -50
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +66 -53
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +144 -57
  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 +70 -30
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +22 -16
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +5 -241
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.pyi +7 -0
  139. tnfr/mathematics/__init__.py +79 -0
  140. tnfr/mathematics/backend.py +453 -0
  141. tnfr/mathematics/backend.pyi +99 -0
  142. tnfr/mathematics/dynamics.py +408 -0
  143. tnfr/mathematics/dynamics.pyi +90 -0
  144. tnfr/mathematics/epi.py +391 -0
  145. tnfr/mathematics/epi.pyi +65 -0
  146. tnfr/mathematics/generators.py +242 -0
  147. tnfr/mathematics/generators.pyi +29 -0
  148. tnfr/mathematics/metrics.py +119 -0
  149. tnfr/mathematics/metrics.pyi +16 -0
  150. tnfr/mathematics/operators.py +239 -0
  151. tnfr/mathematics/operators.pyi +59 -0
  152. tnfr/mathematics/operators_factory.py +124 -0
  153. tnfr/mathematics/operators_factory.pyi +11 -0
  154. tnfr/mathematics/projection.py +87 -0
  155. tnfr/mathematics/projection.pyi +33 -0
  156. tnfr/mathematics/runtime.py +182 -0
  157. tnfr/mathematics/runtime.pyi +64 -0
  158. tnfr/mathematics/spaces.py +256 -0
  159. tnfr/mathematics/spaces.pyi +83 -0
  160. tnfr/mathematics/transforms.py +305 -0
  161. tnfr/mathematics/transforms.pyi +62 -0
  162. tnfr/metrics/__init__.py +47 -9
  163. tnfr/metrics/__init__.pyi +20 -0
  164. tnfr/metrics/buffer_cache.py +163 -0
  165. tnfr/metrics/buffer_cache.pyi +24 -0
  166. tnfr/metrics/cache_utils.py +214 -0
  167. tnfr/metrics/coherence.py +1510 -330
  168. tnfr/metrics/coherence.pyi +129 -0
  169. tnfr/metrics/common.py +23 -16
  170. tnfr/metrics/common.pyi +35 -0
  171. tnfr/metrics/core.py +251 -36
  172. tnfr/metrics/core.pyi +13 -0
  173. tnfr/metrics/diagnosis.py +709 -110
  174. tnfr/metrics/diagnosis.pyi +86 -0
  175. tnfr/metrics/emergence.py +245 -0
  176. tnfr/metrics/export.py +60 -18
  177. tnfr/metrics/export.pyi +7 -0
  178. tnfr/metrics/glyph_timing.py +233 -43
  179. tnfr/metrics/glyph_timing.pyi +81 -0
  180. tnfr/metrics/learning_metrics.py +280 -0
  181. tnfr/metrics/learning_metrics.pyi +21 -0
  182. tnfr/metrics/phase_coherence.py +351 -0
  183. tnfr/metrics/phase_compatibility.py +349 -0
  184. tnfr/metrics/reporting.py +63 -28
  185. tnfr/metrics/reporting.pyi +25 -0
  186. tnfr/metrics/sense_index.py +1126 -43
  187. tnfr/metrics/sense_index.pyi +9 -0
  188. tnfr/metrics/trig.py +215 -23
  189. tnfr/metrics/trig.pyi +13 -0
  190. tnfr/metrics/trig_cache.py +148 -24
  191. tnfr/metrics/trig_cache.pyi +10 -0
  192. tnfr/multiscale/__init__.py +32 -0
  193. tnfr/multiscale/hierarchical.py +517 -0
  194. tnfr/node.py +646 -140
  195. tnfr/node.pyi +139 -0
  196. tnfr/observers.py +160 -45
  197. tnfr/observers.pyi +31 -0
  198. tnfr/ontosim.py +23 -19
  199. tnfr/ontosim.pyi +28 -0
  200. tnfr/operators/__init__.py +1358 -106
  201. tnfr/operators/__init__.pyi +31 -0
  202. tnfr/operators/algebra.py +277 -0
  203. tnfr/operators/canonical_patterns.py +420 -0
  204. tnfr/operators/cascade.py +267 -0
  205. tnfr/operators/cycle_detection.py +358 -0
  206. tnfr/operators/definitions.py +4108 -0
  207. tnfr/operators/definitions.pyi +78 -0
  208. tnfr/operators/grammar.py +1164 -0
  209. tnfr/operators/grammar.pyi +140 -0
  210. tnfr/operators/hamiltonian.py +710 -0
  211. tnfr/operators/health_analyzer.py +809 -0
  212. tnfr/operators/jitter.py +107 -38
  213. tnfr/operators/jitter.pyi +11 -0
  214. tnfr/operators/lifecycle.py +314 -0
  215. tnfr/operators/metabolism.py +618 -0
  216. tnfr/operators/metrics.py +2138 -0
  217. tnfr/operators/network_analysis/__init__.py +27 -0
  218. tnfr/operators/network_analysis/source_detection.py +186 -0
  219. tnfr/operators/nodal_equation.py +395 -0
  220. tnfr/operators/pattern_detection.py +660 -0
  221. tnfr/operators/patterns.py +669 -0
  222. tnfr/operators/postconditions/__init__.py +38 -0
  223. tnfr/operators/postconditions/mutation.py +236 -0
  224. tnfr/operators/preconditions/__init__.py +1226 -0
  225. tnfr/operators/preconditions/coherence.py +305 -0
  226. tnfr/operators/preconditions/dissonance.py +236 -0
  227. tnfr/operators/preconditions/emission.py +128 -0
  228. tnfr/operators/preconditions/mutation.py +580 -0
  229. tnfr/operators/preconditions/reception.py +125 -0
  230. tnfr/operators/preconditions/resonance.py +364 -0
  231. tnfr/operators/registry.py +74 -0
  232. tnfr/operators/registry.pyi +9 -0
  233. tnfr/operators/remesh.py +1415 -91
  234. tnfr/operators/remesh.pyi +26 -0
  235. tnfr/operators/structural_units.py +268 -0
  236. tnfr/operators/unified_grammar.py +105 -0
  237. tnfr/parallel/__init__.py +54 -0
  238. tnfr/parallel/auto_scaler.py +234 -0
  239. tnfr/parallel/distributed.py +384 -0
  240. tnfr/parallel/engine.py +238 -0
  241. tnfr/parallel/gpu_engine.py +420 -0
  242. tnfr/parallel/monitoring.py +248 -0
  243. tnfr/parallel/partitioner.py +459 -0
  244. tnfr/py.typed +0 -0
  245. tnfr/recipes/__init__.py +22 -0
  246. tnfr/recipes/cookbook.py +743 -0
  247. tnfr/rng.py +75 -151
  248. tnfr/rng.pyi +26 -0
  249. tnfr/schemas/__init__.py +8 -0
  250. tnfr/schemas/grammar.json +94 -0
  251. tnfr/sdk/__init__.py +107 -0
  252. tnfr/sdk/__init__.pyi +19 -0
  253. tnfr/sdk/adaptive_system.py +173 -0
  254. tnfr/sdk/adaptive_system.pyi +21 -0
  255. tnfr/sdk/builders.py +370 -0
  256. tnfr/sdk/builders.pyi +51 -0
  257. tnfr/sdk/fluent.py +1121 -0
  258. tnfr/sdk/fluent.pyi +74 -0
  259. tnfr/sdk/templates.py +342 -0
  260. tnfr/sdk/templates.pyi +41 -0
  261. tnfr/sdk/utils.py +341 -0
  262. tnfr/secure_config.py +46 -0
  263. tnfr/security/__init__.py +70 -0
  264. tnfr/security/database.py +514 -0
  265. tnfr/security/subprocess.py +503 -0
  266. tnfr/security/validation.py +290 -0
  267. tnfr/selector.py +59 -22
  268. tnfr/selector.pyi +19 -0
  269. tnfr/sense.py +92 -67
  270. tnfr/sense.pyi +23 -0
  271. tnfr/services/__init__.py +17 -0
  272. tnfr/services/orchestrator.py +325 -0
  273. tnfr/sparse/__init__.py +39 -0
  274. tnfr/sparse/representations.py +492 -0
  275. tnfr/structural.py +639 -263
  276. tnfr/structural.pyi +83 -0
  277. tnfr/telemetry/__init__.py +35 -0
  278. tnfr/telemetry/cache_metrics.py +226 -0
  279. tnfr/telemetry/cache_metrics.pyi +64 -0
  280. tnfr/telemetry/nu_f.py +422 -0
  281. tnfr/telemetry/nu_f.pyi +108 -0
  282. tnfr/telemetry/verbosity.py +36 -0
  283. tnfr/telemetry/verbosity.pyi +15 -0
  284. tnfr/tokens.py +2 -4
  285. tnfr/tokens.pyi +36 -0
  286. tnfr/tools/__init__.py +20 -0
  287. tnfr/tools/domain_templates.py +478 -0
  288. tnfr/tools/sequence_generator.py +846 -0
  289. tnfr/topology/__init__.py +13 -0
  290. tnfr/topology/asymmetry.py +151 -0
  291. tnfr/trace.py +300 -126
  292. tnfr/trace.pyi +42 -0
  293. tnfr/tutorials/__init__.py +38 -0
  294. tnfr/tutorials/autonomous_evolution.py +285 -0
  295. tnfr/tutorials/interactive.py +1576 -0
  296. tnfr/tutorials/structural_metabolism.py +238 -0
  297. tnfr/types.py +743 -12
  298. tnfr/types.pyi +357 -0
  299. tnfr/units.py +68 -0
  300. tnfr/units.pyi +13 -0
  301. tnfr/utils/__init__.py +282 -0
  302. tnfr/utils/__init__.pyi +215 -0
  303. tnfr/utils/cache.py +4223 -0
  304. tnfr/utils/cache.pyi +470 -0
  305. tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
  306. tnfr/utils/callbacks.pyi +49 -0
  307. tnfr/utils/chunks.py +108 -0
  308. tnfr/utils/chunks.pyi +22 -0
  309. tnfr/utils/data.py +428 -0
  310. tnfr/utils/data.pyi +74 -0
  311. tnfr/utils/graph.py +85 -0
  312. tnfr/utils/graph.pyi +10 -0
  313. tnfr/utils/init.py +821 -0
  314. tnfr/utils/init.pyi +80 -0
  315. tnfr/utils/io.py +559 -0
  316. tnfr/utils/io.pyi +66 -0
  317. tnfr/{helpers → utils}/numeric.py +51 -24
  318. tnfr/utils/numeric.pyi +21 -0
  319. tnfr/validation/__init__.py +257 -0
  320. tnfr/validation/__init__.pyi +85 -0
  321. tnfr/validation/compatibility.py +460 -0
  322. tnfr/validation/compatibility.pyi +6 -0
  323. tnfr/validation/config.py +73 -0
  324. tnfr/validation/graph.py +139 -0
  325. tnfr/validation/graph.pyi +18 -0
  326. tnfr/validation/input_validation.py +755 -0
  327. tnfr/validation/invariants.py +712 -0
  328. tnfr/validation/rules.py +253 -0
  329. tnfr/validation/rules.pyi +44 -0
  330. tnfr/validation/runtime.py +279 -0
  331. tnfr/validation/runtime.pyi +28 -0
  332. tnfr/validation/sequence_validator.py +162 -0
  333. tnfr/validation/soft_filters.py +170 -0
  334. tnfr/validation/soft_filters.pyi +32 -0
  335. tnfr/validation/spectral.py +164 -0
  336. tnfr/validation/spectral.pyi +42 -0
  337. tnfr/validation/validator.py +1266 -0
  338. tnfr/validation/window.py +39 -0
  339. tnfr/validation/window.pyi +1 -0
  340. tnfr/visualization/__init__.py +98 -0
  341. tnfr/visualization/cascade_viz.py +256 -0
  342. tnfr/visualization/hierarchy.py +284 -0
  343. tnfr/visualization/sequence_plotter.py +784 -0
  344. tnfr/viz/__init__.py +60 -0
  345. tnfr/viz/matplotlib.py +278 -0
  346. tnfr/viz/matplotlib.pyi +35 -0
  347. tnfr-8.5.0.dist-info/METADATA +573 -0
  348. tnfr-8.5.0.dist-info/RECORD +353 -0
  349. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
  350. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
  351. tnfr/collections_utils.py +0 -300
  352. tnfr/config.py +0 -32
  353. tnfr/grammar.py +0 -344
  354. tnfr/graph_utils.py +0 -84
  355. tnfr/helpers/__init__.py +0 -71
  356. tnfr/import_utils.py +0 -228
  357. tnfr/json_utils.py +0 -162
  358. tnfr/logging_utils.py +0 -116
  359. tnfr/presets.py +0 -60
  360. tnfr/validators.py +0 -84
  361. tnfr/value_utils.py +0 -59
  362. tnfr-4.5.2.dist-info/METADATA +0 -379
  363. tnfr-4.5.2.dist-info/RECORD +0 -67
  364. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  365. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
@@ -1,42 +1,189 @@
1
- from __future__ import annotations
1
+ """Canonical ΔNFR integrators driving TNFR runtime evolution.
2
2
 
3
- import math
4
- from collections.abc import Mapping
5
- from typing import Any, Literal
3
+ This module implements numerical integration of the canonical TNFR nodal equation:
4
+
5
+ ∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
6
+
7
+ The extended equation includes:
8
+ - Base term: νf · ΔNFR(t) - canonical structural evolution
9
+ - Network term: Γi(R) - optional Kuramoto coupling
6
10
 
7
- import networkx as nx # type: ignore[import-untyped]
11
+ Integration respects TNFR invariants:
12
+ - Structural units (Hz_str for νf)
13
+ - Operator closure (valid ΔNFR semantics)
14
+ - Phase coherence (network synchronization)
15
+ - Reproducibility (deterministic with seeds)
8
16
 
9
- from ..constants import (
10
- DEFAULTS,
11
- get_aliases,
17
+ The canonical base term is computed explicitly in _collect_nodal_increments()
18
+ at line 321 and 342 as: base = vf * dnfr, implementing ∂EPI/∂t = νf·ΔNFR(t).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import math
24
+ from abc import ABC, abstractmethod
25
+ from collections.abc import Iterable, Mapping
26
+ from concurrent.futures import ProcessPoolExecutor
27
+ from multiprocessing import get_context
28
+ from typing import Any, Literal, cast
29
+
30
+ import networkx as nx
31
+
32
+ from .._compat import TypeAlias
33
+ from ..alias import collect_attr, get_attr, get_attr_str, set_attr, set_attr_str
34
+ from ..constants import DEFAULTS
35
+ from ..constants.aliases import (
36
+ ALIAS_D2EPI,
37
+ ALIAS_DEPI,
38
+ ALIAS_DNFR,
39
+ ALIAS_EPI,
40
+ ALIAS_EPI_KIND,
41
+ ALIAS_VF,
12
42
  )
13
43
  from ..gamma import _get_gamma_spec, eval_gamma
14
- from ..alias import get_attr, get_attr_str, set_attr, set_attr_str
15
-
16
- ALIAS_VF = get_aliases("VF")
17
- ALIAS_DNFR = get_aliases("DNFR")
18
- ALIAS_DEPI = get_aliases("DEPI")
19
- ALIAS_EPI = get_aliases("EPI")
20
- ALIAS_EPI_KIND = get_aliases("EPI_KIND")
21
- ALIAS_D2EPI = get_aliases("D2EPI")
44
+ from ..types import NodeId, TNFRGraph
45
+ from ..utils import get_numpy, resolve_chunk_size
46
+ from .canonical import compute_canonical_nodal_derivative
47
+ from .structural_clip import structural_clip
22
48
 
23
49
  __all__ = (
50
+ "AbstractIntegrator",
51
+ "DefaultIntegrator",
24
52
  "prepare_integration_params",
25
53
  "update_epi_via_nodal_equation",
26
54
  )
27
55
 
56
+ GammaMap: TypeAlias = dict[NodeId, float]
57
+ """Γ evaluation cache keyed by node identifier."""
58
+
59
+ NodeIncrements: TypeAlias = dict[NodeId, tuple[float, ...]]
60
+ """Mapping of nodes to staged integration increments."""
61
+
62
+ NodalUpdate: TypeAlias = dict[NodeId, tuple[float, float, float]]
63
+ """Mapping of nodes to ``(EPI, dEPI/dt, ∂²EPI/∂t²)`` tuples."""
64
+
65
+ IntegratorMethod: TypeAlias = Literal["euler", "rk4"]
66
+ """Supported explicit integration schemes for nodal updates."""
67
+
68
+ _PARALLEL_GRAPH: TNFRGraph | None = None
69
+
70
+
71
+ def _gamma_worker_init(graph: TNFRGraph) -> None:
72
+ """Initialise process-local graph reference for Γ evaluation."""
73
+
74
+ global _PARALLEL_GRAPH
75
+ _PARALLEL_GRAPH = graph
76
+
77
+
78
+ def _gamma_worker(task: tuple[list[NodeId], float]) -> list[tuple[NodeId, float]]:
79
+ """Evaluate Γ for ``task`` chunk using process-local graph."""
80
+
81
+ chunk, t = task
82
+ if _PARALLEL_GRAPH is None:
83
+ raise RuntimeError("Parallel Γ worker initialised without graph reference")
84
+ return [(node, float(eval_gamma(_PARALLEL_GRAPH, node, t))) for node in chunk]
85
+
86
+
87
+ def _normalise_jobs(n_jobs: int | None, total: int) -> int | None:
88
+ """Return an effective worker count respecting serial fallbacks."""
89
+
90
+ if n_jobs is None:
91
+ return None
92
+ try:
93
+ workers = int(n_jobs)
94
+ except (TypeError, ValueError):
95
+ return None
96
+ if workers <= 1 or total <= 1:
97
+ return None
98
+ return max(1, min(workers, total))
99
+
100
+
101
+ def _chunk_nodes(nodes: list[NodeId], chunk_size: int) -> Iterable[list[NodeId]]:
102
+ """Yield deterministic chunks from ``nodes`` respecting insertion order."""
103
+
104
+ for idx in range(0, len(nodes), chunk_size):
105
+ yield nodes[idx : idx + chunk_size]
106
+
107
+
108
+ def _apply_increment_chunk(
109
+ chunk: list[tuple[NodeId, float, float, tuple[float, ...]]],
110
+ dt_step: float,
111
+ method: str,
112
+ ) -> list[tuple[NodeId, tuple[float, float, float]]]:
113
+ """Compute updated states for ``chunk`` using scalar arithmetic."""
114
+
115
+ results: list[tuple[NodeId, tuple[float, float, float]]] = []
116
+ dt_nonzero = dt_step != 0
117
+
118
+ for node, epi_i, dEPI_prev, ks in chunk:
119
+ if method == "rk4":
120
+ k1, k2, k3, k4 = ks
121
+ epi = epi_i + (dt_step / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
122
+ dEPI_dt = k4
123
+ else:
124
+ (k1,) = ks
125
+ epi = epi_i + dt_step * k1
126
+ dEPI_dt = k1
127
+ d2epi = (dEPI_dt - dEPI_prev) / dt_step if dt_nonzero else 0.0
128
+ results.append((node, (float(epi), float(dEPI_dt), float(d2epi))))
129
+
130
+ return results
131
+
132
+
133
+ def _evaluate_gamma_map(
134
+ G: TNFRGraph,
135
+ nodes: list[NodeId],
136
+ t: float,
137
+ *,
138
+ n_jobs: int | None = None,
139
+ ) -> GammaMap:
140
+ """Return Γ evaluations for ``nodes`` at time ``t`` respecting parallelism."""
141
+
142
+ workers = _normalise_jobs(n_jobs, len(nodes))
143
+ if workers is None:
144
+ return {n: float(eval_gamma(G, n, t)) for n in nodes}
145
+
146
+ approx_chunk = math.ceil(len(nodes) / (workers * 4)) if workers > 0 else None
147
+ chunk_size = resolve_chunk_size(
148
+ approx_chunk,
149
+ len(nodes),
150
+ minimum=1,
151
+ )
152
+ mp_ctx = get_context("spawn")
153
+ tasks = ((chunk, t) for chunk in _chunk_nodes(nodes, chunk_size))
154
+
155
+ results: GammaMap = {}
156
+ with ProcessPoolExecutor(
157
+ max_workers=workers,
158
+ mp_context=mp_ctx,
159
+ initializer=_gamma_worker_init,
160
+ initargs=(G,),
161
+ ) as executor:
162
+ futures = [executor.submit(_gamma_worker, task) for task in tasks]
163
+ for fut in futures:
164
+ for node, value in fut.result():
165
+ results[node] = value
166
+ return results
167
+
28
168
 
29
169
  def prepare_integration_params(
30
- G,
170
+ G: TNFRGraph,
31
171
  dt: float | None = None,
32
172
  t: float | None = None,
33
173
  method: Literal["euler", "rk4"] | None = None,
34
- ):
174
+ ) -> tuple[float, int, float, Literal["euler", "rk4"]]:
35
175
  """Validate and normalise ``dt``, ``t`` and ``method`` for integration.
36
176
 
37
- Returns ``(dt_step, steps, t0, method)`` where ``dt_step`` is the
38
- effective step, ``steps`` the number of substeps and ``t0`` the prepared
39
- initial time.
177
+ The function raises :class:`TypeError` when ``dt`` cannot be coerced to a
178
+ number, :class:`ValueError` if ``dt`` is negative, and another
179
+ :class:`ValueError` when an unsupported method is requested. When ``dt``
180
+ exceeds a positive ``DT_MIN`` stored on ``G`` the span is deterministically
181
+ subdivided into integer steps so that the resulting ``dt_step`` never falls
182
+ below that minimum threshold.
183
+
184
+ Returns ``(dt_step, steps, t0, method)`` where ``dt_step`` is the effective
185
+ step, ``steps`` the number of substeps and ``t0`` the prepared initial
186
+ time.
40
187
  """
41
188
  if dt is None:
42
189
  dt = float(G.graph.get("DT", DEFAULTS["DT"]))
@@ -52,99 +199,229 @@ def prepare_integration_params(
52
199
  else:
53
200
  t = float(t)
54
201
 
55
- method = (
202
+ method_value = (
56
203
  method
57
- or G.graph.get(
58
- "INTEGRATOR_METHOD", DEFAULTS.get("INTEGRATOR_METHOD", "euler")
59
- )
204
+ or G.graph.get("INTEGRATOR_METHOD", DEFAULTS.get("INTEGRATOR_METHOD", "euler"))
60
205
  ).lower()
61
- if method not in ("euler", "rk4"):
206
+ if method_value not in ("euler", "rk4"):
62
207
  raise ValueError("method must be 'euler' or 'rk4'")
63
208
 
64
209
  dt_min = float(G.graph.get("DT_MIN", DEFAULTS.get("DT_MIN", 0.0)))
210
+ steps = 1
65
211
  if dt_min > 0 and dt > dt_min:
66
- steps = int(math.ceil(dt / dt_min))
67
- else:
68
- steps = 1
69
- # ``steps`` is guaranteed to be ≥1 at this point
70
- dt_step = dt / steps
212
+ ratio = dt / dt_min
213
+ steps = max(1, int(math.floor(ratio + 1e-12)))
214
+ if dt / steps < dt_min:
215
+ steps = int(math.ceil(ratio))
216
+ dt_step = dt / steps if steps else 0.0
71
217
 
72
- return dt_step, steps, t, method
218
+ return dt_step, steps, t, cast(Literal["euler", "rk4"], method_value)
73
219
 
74
220
 
75
221
  def _apply_increments(
76
- G: Any,
222
+ G: TNFRGraph,
77
223
  dt_step: float,
78
- increments: dict[Any, tuple[float, ...]],
224
+ increments: NodeIncrements,
79
225
  *,
80
226
  method: str,
81
- ) -> dict[Any, tuple[float, float, float]]:
227
+ n_jobs: int | None = None,
228
+ ) -> NodalUpdate:
82
229
  """Combine precomputed increments to update node states."""
83
230
 
84
- new_states: dict[Any, tuple[float, float, float]] = {}
85
- for n, nd in G.nodes(data=True):
86
- vf, dnfr, dEPI_dt_prev, epi_i = _node_state(nd)
87
- ks = increments[n]
231
+ nodes: list[NodeId] = list(G.nodes)
232
+ if not nodes:
233
+ return {}
234
+
235
+ np = get_numpy()
236
+
237
+ epi_initial: list[float] = []
238
+ dEPI_prev: list[float] = []
239
+ ordered_increments: list[tuple[float, ...]] = []
240
+
241
+ for node in nodes:
242
+ nd = G.nodes[node]
243
+ _, _, dEPI_dt_prev, epi_i = _node_state(nd)
244
+ epi_initial.append(float(epi_i))
245
+ dEPI_prev.append(float(dEPI_dt_prev))
246
+ ordered_increments.append(increments[node])
247
+
248
+ if np is not None:
249
+ epi_arr = np.asarray(epi_initial, dtype=float)
250
+ dEPI_prev_arr = np.asarray(dEPI_prev, dtype=float)
251
+ k_arr = np.asarray(ordered_increments, dtype=float)
252
+
88
253
  if method == "rk4":
89
- k1, k2, k3, k4 = ks
90
- # RK4: EPIₙ₊₁ = EPIᵢ + Δt/6·(k1 + 2k2 + 2k3 + k4)
91
- epi = epi_i + (dt_step / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
254
+ if k_arr.ndim != 2 or k_arr.shape[1] != 4:
255
+ raise ValueError("rk4 increments require four staged values")
256
+ dt_factor = dt_step / 6.0
257
+ k1 = k_arr[:, 0]
258
+ k2 = k_arr[:, 1]
259
+ k3 = k_arr[:, 2]
260
+ k4 = k_arr[:, 3]
261
+ epi = epi_arr + dt_factor * (k1 + 2 * k2 + 2 * k3 + k4)
92
262
  dEPI_dt = k4
93
263
  else:
94
- (k1,) = ks
95
- # Euler: EPIₙ₊₁ = EPIᵢ + Δt·k1 where k1 = νf·ΔNFR + Γ
96
- epi = epi_i + dt_step * k1
264
+ if k_arr.ndim == 1:
265
+ k1 = k_arr
266
+ else:
267
+ k1 = k_arr[:, 0]
268
+ epi = epi_arr + dt_step * k1
97
269
  dEPI_dt = k1
98
- d2epi = (dEPI_dt - dEPI_dt_prev) / dt_step if dt_step != 0 else 0.0
99
- new_states[n] = (epi, dEPI_dt, d2epi)
100
- return new_states
270
+
271
+ if dt_step != 0:
272
+ d2epi = (dEPI_dt - dEPI_prev_arr) / dt_step
273
+ else:
274
+ d2epi = np.zeros_like(dEPI_dt)
275
+
276
+ results: NodalUpdate = {}
277
+ for idx, node in enumerate(nodes):
278
+ results[node] = (
279
+ float(epi[idx]),
280
+ float(dEPI_dt[idx]),
281
+ float(d2epi[idx]),
282
+ )
283
+ return results
284
+
285
+ payload: list[tuple[NodeId, float, float, tuple[float, ...]]] = list(
286
+ zip(nodes, epi_initial, dEPI_prev, ordered_increments)
287
+ )
288
+
289
+ workers = _normalise_jobs(n_jobs, len(nodes))
290
+ if workers is None:
291
+ return dict(_apply_increment_chunk(payload, dt_step, method))
292
+
293
+ approx_chunk = math.ceil(len(nodes) / (workers * 4)) if workers > 0 else None
294
+ chunk_size = resolve_chunk_size(
295
+ approx_chunk,
296
+ len(nodes),
297
+ minimum=1,
298
+ )
299
+ mp_ctx = get_context("spawn")
300
+
301
+ results: NodalUpdate = {}
302
+ with ProcessPoolExecutor(max_workers=workers, mp_context=mp_ctx) as executor:
303
+ futures = [
304
+ executor.submit(
305
+ _apply_increment_chunk,
306
+ chunk,
307
+ dt_step,
308
+ method,
309
+ )
310
+ for chunk in _chunk_nodes(payload, chunk_size)
311
+ ]
312
+ for fut in futures:
313
+ for node, value in fut.result():
314
+ results[node] = value
315
+
316
+ return {node: results[node] for node in nodes}
101
317
 
102
318
 
103
319
  def _collect_nodal_increments(
104
- G: Any,
105
- gamma_maps: tuple[dict[Any, float], ...],
320
+ G: TNFRGraph,
321
+ gamma_maps: tuple[GammaMap, ...],
106
322
  *,
107
323
  method: str,
108
- ) -> dict[Any, tuple[float, ...]]:
324
+ ) -> NodeIncrements:
109
325
  """Combine node base state with staged Γ contributions.
110
326
 
111
- ``gamma_maps`` must contain one entry for Euler integration and four for
112
- RK4. The helper merges the structural frequency/ΔNFR base contribution
113
- with the supplied Γ evaluations.
327
+ Implements the canonical TNFR nodal equation in two parts:
328
+
329
+ 1. **Base term** (canonical equation):
330
+ base = vf * dnfr → ∂EPI/∂t = νf · ΔNFR(t)
331
+
332
+ This is the fundamental TNFR equation where:
333
+ - vf (νf): structural frequency in Hz_str
334
+ - dnfr (ΔNFR): nodal gradient (reorganization operator)
335
+ - base: instantaneous rate of EPI evolution
336
+
337
+ 2. **Network coupling term**:
338
+ Γi(R) from gamma_maps - optional Kuramoto order parameter
339
+
340
+ The full extended equation is: ∂EPI/∂t = νf·ΔNFR(t) + Γi(R)
341
+
342
+ Args:
343
+ G: TNFR graph with node attributes vf and dnfr
344
+ gamma_maps: Staged Γ evaluations (1 for Euler, 4 for RK4)
345
+ method: Integration method ('euler' or 'rk4')
346
+
347
+ Returns:
348
+ Mapping of nodes to staged integration increments
349
+
350
+ Notes:
351
+ - Line 321 implements the canonical nodal equation explicitly
352
+ - Units: vf in Hz_str, dnfr dimensionless, base in Hz_str
353
+ - Preserves TNFR operator closure and structural semantics
114
354
  """
115
355
 
116
- increments: dict[Any, tuple[float, ...]] = {}
117
- for n, nd in G.nodes(data=True):
356
+ nodes: list[NodeId] = list(G.nodes())
357
+ if not nodes:
358
+ return {}
359
+
360
+ if method == "rk4":
361
+ expected_maps = 4
362
+ elif method == "euler":
363
+ expected_maps = 1
364
+ else:
365
+ raise ValueError("method must be 'euler' or 'rk4'")
366
+
367
+ if len(gamma_maps) != expected_maps:
368
+ raise ValueError(f"{method} integration requires {expected_maps} gamma maps")
369
+
370
+ np = get_numpy()
371
+ if np is not None:
372
+ vf = collect_attr(G, nodes, ALIAS_VF, 0.0, np=np)
373
+ dnfr = collect_attr(G, nodes, ALIAS_DNFR, 0.0, np=np)
374
+ # CANONICAL TNFR EQUATION: ∂EPI/∂t = νf · ΔNFR(t)
375
+ # This implements the fundamental nodal equation explicitly
376
+ base = vf * dnfr
377
+
378
+ gamma_arrays = [
379
+ np.fromiter((gm.get(n, 0.0) for n in nodes), float, count=len(nodes))
380
+ for gm in gamma_maps
381
+ ]
382
+ if gamma_arrays:
383
+ gamma_stack = np.stack(gamma_arrays, axis=1)
384
+ combined = base[:, None] + gamma_stack
385
+ else:
386
+ combined = base[:, None]
387
+
388
+ return {
389
+ node: tuple(float(value) for value in combined[idx])
390
+ for idx, node in enumerate(nodes)
391
+ }
392
+
393
+ increments: NodeIncrements = {}
394
+ for node in nodes:
395
+ nd = G.nodes[node]
118
396
  vf, dnfr, *_ = _node_state(nd)
397
+ # CANONICAL TNFR EQUATION: ∂EPI/∂t = νf · ΔNFR(t)
398
+ # Scalar implementation of the fundamental nodal equation
119
399
  base = vf * dnfr
120
- gammas = [gm.get(n, 0.0) for gm in gamma_maps]
400
+ gammas = [gm.get(node, 0.0) for gm in gamma_maps]
121
401
 
122
402
  if method == "rk4":
123
- if len(gammas) != 4:
124
- raise ValueError("rk4 integration requires four gamma maps")
125
403
  k1, k2, k3, k4 = gammas
126
- increments[n] = (
404
+ increments[node] = (
127
405
  base + k1,
128
406
  base + k2,
129
407
  base + k3,
130
408
  base + k4,
131
409
  )
132
410
  else:
133
- if len(gammas) != 1:
134
- raise ValueError("euler integration requires one gamma map")
135
411
  (k1,) = gammas
136
- increments[n] = (base + k1,)
412
+ increments[node] = (base + k1,)
137
413
 
138
414
  return increments
139
415
 
140
416
 
141
417
  def _build_gamma_increments(
142
- G: Any,
418
+ G: TNFRGraph,
143
419
  dt_step: float,
144
420
  t_local: float,
145
421
  *,
146
422
  method: str,
147
- ) -> dict[Any, tuple[float, ...]]:
423
+ n_jobs: int | None = None,
424
+ ) -> NodeIncrements:
148
425
  """Evaluate Γ contributions and merge them with ``νf·ΔNFR`` base terms."""
149
426
 
150
427
  if method == "rk4":
@@ -163,50 +440,170 @@ def _build_gamma_increments(
163
440
  gamma_type = str(gamma_spec.get("type", "")).lower()
164
441
 
165
442
  if gamma_type == "none":
166
- gamma_maps = tuple({} for _ in range(gamma_count))
443
+ gamma_maps: tuple[GammaMap, ...] = tuple(
444
+ cast(GammaMap, {}) for _ in range(gamma_count)
445
+ )
446
+ return _collect_nodal_increments(G, gamma_maps, method=method)
447
+
448
+ nodes: list[NodeId] = list(G.nodes)
449
+ if not nodes:
450
+ gamma_maps = tuple(cast(GammaMap, {}) for _ in range(gamma_count))
167
451
  return _collect_nodal_increments(G, gamma_maps, method=method)
168
452
 
169
453
  if method == "rk4":
170
454
  t_mid = t_local + dt_step / 2.0
171
455
  t_end = t_local + dt_step
172
- g1_map = {n: eval_gamma(G, n, t_local) for n in G.nodes}
173
- g_mid_map = {n: eval_gamma(G, n, t_mid) for n in G.nodes}
174
- g4_map = {n: eval_gamma(G, n, t_end) for n in G.nodes}
456
+ g1_map = _evaluate_gamma_map(G, nodes, t_local, n_jobs=n_jobs)
457
+ g_mid_map = _evaluate_gamma_map(G, nodes, t_mid, n_jobs=n_jobs)
458
+ g4_map = _evaluate_gamma_map(G, nodes, t_end, n_jobs=n_jobs)
175
459
  gamma_maps = (g1_map, g_mid_map, g_mid_map, g4_map)
176
460
  else: # method == "euler"
177
- gamma_maps = ({n: eval_gamma(G, n, t_local) for n in G.nodes},)
461
+ gamma_maps = (_evaluate_gamma_map(G, nodes, t_local, n_jobs=n_jobs),)
178
462
 
179
463
  return _collect_nodal_increments(G, gamma_maps, method=method)
180
464
 
181
465
 
182
- def _integrate_euler(G, dt_step: float, t_local: float):
466
+ def _integrate_euler(
467
+ G: TNFRGraph,
468
+ dt_step: float,
469
+ t_local: float,
470
+ *,
471
+ n_jobs: int | None = None,
472
+ ) -> NodalUpdate:
183
473
  """One explicit Euler integration step."""
184
474
  increments = _build_gamma_increments(
185
475
  G,
186
476
  dt_step,
187
477
  t_local,
188
478
  method="euler",
479
+ n_jobs=n_jobs,
480
+ )
481
+ return _apply_increments(
482
+ G,
483
+ dt_step,
484
+ increments,
485
+ method="euler",
486
+ n_jobs=n_jobs,
189
487
  )
190
- return _apply_increments(G, dt_step, increments, method="euler")
191
488
 
192
489
 
193
- def _integrate_rk4(G, dt_step: float, t_local: float):
490
+ def _integrate_rk4(
491
+ G: TNFRGraph,
492
+ dt_step: float,
493
+ t_local: float,
494
+ *,
495
+ n_jobs: int | None = None,
496
+ ) -> NodalUpdate:
194
497
  """One Runge–Kutta order-4 integration step."""
195
498
  increments = _build_gamma_increments(
196
499
  G,
197
500
  dt_step,
198
501
  t_local,
199
502
  method="rk4",
503
+ n_jobs=n_jobs,
504
+ )
505
+ return _apply_increments(
506
+ G,
507
+ dt_step,
508
+ increments,
509
+ method="rk4",
510
+ n_jobs=n_jobs,
200
511
  )
201
- return _apply_increments(G, dt_step, increments, method="rk4")
512
+
513
+
514
+ class AbstractIntegrator(ABC):
515
+ """Abstract base class encapsulating nodal equation integration."""
516
+
517
+ @abstractmethod
518
+ def integrate(
519
+ self,
520
+ graph: TNFRGraph,
521
+ *,
522
+ dt: float | None,
523
+ t: float | None,
524
+ method: str | None,
525
+ n_jobs: int | None,
526
+ ) -> None:
527
+ """Advance ``graph`` coherence states according to the nodal equation."""
528
+
529
+
530
+ class DefaultIntegrator(AbstractIntegrator):
531
+ """Explicit integrator combining Euler and RK4 step implementations."""
532
+
533
+ def integrate(
534
+ self,
535
+ graph: TNFRGraph,
536
+ *,
537
+ dt: float | None,
538
+ t: float | None,
539
+ method: str | None,
540
+ n_jobs: int | None,
541
+ ) -> None:
542
+ """Integrate the nodal equation updating EPI, ΔEPI and Δ²EPI."""
543
+
544
+ if not isinstance(
545
+ graph, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
546
+ ):
547
+ raise TypeError("G must be a networkx graph instance")
548
+
549
+ dt_step, steps, t0, resolved_method = prepare_integration_params(
550
+ graph, dt, t, cast(IntegratorMethod | None, method)
551
+ )
552
+
553
+ t_local = t0
554
+ for _ in range(steps):
555
+ if resolved_method == "rk4":
556
+ updates: NodalUpdate = _integrate_rk4(
557
+ graph, dt_step, t_local, n_jobs=n_jobs
558
+ )
559
+ else:
560
+ updates = _integrate_euler(graph, dt_step, t_local, n_jobs=n_jobs)
561
+
562
+ for n, (epi, dEPI_dt, d2epi) in updates.items():
563
+ nd = graph.nodes[n]
564
+ epi_kind = get_attr_str(nd, ALIAS_EPI_KIND, "")
565
+
566
+ # Apply structural boundary preservation
567
+ epi_min = float(
568
+ graph.graph.get("EPI_MIN", DEFAULTS.get("EPI_MIN", -1.0))
569
+ )
570
+ epi_max = float(
571
+ graph.graph.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0))
572
+ )
573
+ clip_mode_str = str(graph.graph.get("CLIP_MODE", "hard"))
574
+ # Validate clip mode and cast to proper type
575
+ if clip_mode_str not in ("hard", "soft"):
576
+ clip_mode_str = "hard"
577
+ clip_mode: Literal["hard", "soft"] = clip_mode_str # type: ignore[assignment]
578
+ clip_k = float(graph.graph.get("CLIP_SOFT_K", 3.0))
579
+
580
+ epi_clipped = structural_clip(
581
+ epi,
582
+ lo=epi_min,
583
+ hi=epi_max,
584
+ mode=clip_mode,
585
+ k=clip_k,
586
+ record_stats=False,
587
+ )
588
+
589
+ set_attr(nd, ALIAS_EPI, epi_clipped)
590
+ if epi_kind:
591
+ set_attr_str(nd, ALIAS_EPI_KIND, epi_kind)
592
+ set_attr(nd, ALIAS_DEPI, dEPI_dt)
593
+ set_attr(nd, ALIAS_D2EPI, d2epi)
594
+
595
+ t_local += dt_step
596
+
597
+ graph.graph["_t"] = t_local
202
598
 
203
599
 
204
600
  def update_epi_via_nodal_equation(
205
- G,
601
+ G: TNFRGraph,
206
602
  *,
207
603
  dt: float | None = None,
208
604
  t: float | None = None,
209
605
  method: Literal["euler", "rk4"] | None = None,
606
+ n_jobs: int | None = None,
210
607
  ) -> None:
211
608
  """TNFR nodal equation.
212
609
 
@@ -224,40 +621,37 @@ def update_epi_via_nodal_equation(
224
621
  TNFR references: nodal equation (manual), νf/ΔNFR/EPI glossary, Γ operator.
225
622
  Side effects: caches dEPI and updates EPI via explicit integration.
226
623
  """
227
- if not isinstance(
228
- G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
229
- ):
230
- raise TypeError("G must be a networkx graph instance")
624
+ DefaultIntegrator().integrate(
625
+ G,
626
+ dt=dt,
627
+ t=t,
628
+ method=method,
629
+ n_jobs=n_jobs,
630
+ )
231
631
 
232
- dt_step, steps, t0, method = prepare_integration_params(G, dt, t, method)
233
632
 
234
- t_local = t0
235
- for _ in range(steps):
236
- if method == "rk4":
237
- updates = _integrate_rk4(G, dt_step, t_local)
238
- else:
239
- updates = _integrate_euler(G, dt_step, t_local)
240
-
241
- for n, (epi, dEPI_dt, d2epi) in updates.items():
242
- nd = G.nodes[n]
243
- epi_kind = get_attr_str(nd, ALIAS_EPI_KIND, "")
244
- set_attr(nd, ALIAS_EPI, epi)
245
- if epi_kind:
246
- set_attr_str(nd, ALIAS_EPI_KIND, epi_kind)
247
- set_attr(nd, ALIAS_DEPI, dEPI_dt)
248
- set_attr(nd, ALIAS_D2EPI, d2epi)
633
+ def _node_state(nd: dict[str, Any]) -> tuple[float, float, float, float]:
634
+ """Return common node state attributes for canonical equation evaluation.
249
635
 
250
- t_local += dt_step
636
+ Extracts the fundamental TNFR variables from node data:
637
+ - νf (vf): Structural frequency in Hz_str
638
+ - ΔNFR (dnfr): Nodal gradient (reorganization operator)
639
+ - dEPI/dt (previous): Last computed EPI derivative
640
+ - EPI (current): Current Primary Information Structure
251
641
 
252
- G.graph["_t"] = t_local
642
+ These variables are used in the canonical nodal equation:
643
+ ∂EPI/∂t = νf · ΔNFR(t)
253
644
 
645
+ Args:
646
+ nd: Node data dictionary containing TNFR attributes
254
647
 
255
- def _node_state(nd: dict[str, Any]) -> tuple[float, float, float, float]:
256
- """Return common node state attributes.
648
+ Returns:
649
+ Tuple of (vf, dnfr, dEPI_dt_prev, epi_i) with 0.0 defaults
257
650
 
258
- Extracts ``νf``, ``ΔNFR``, previous ``dEPI/dt`` and current ``EPI``
259
- using alias helpers, providing ``0.0`` defaults when attributes are
260
- missing.
651
+ Notes:
652
+ - vf alias maps to VF, frequency, or structural_frequency
653
+ - dnfr alias maps to DNFR, delta_nfr, or reorganization_gradient
654
+ - All values are coerced to float for numerical stability
261
655
  """
262
656
 
263
657
  vf = get_attr(nd, ALIAS_VF, 0.0)