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,1672 @@
1
+ """Network operators.
2
+
3
+ Operator helpers interact with TNFR graphs adhering to
4
+ :class:`tnfr.types.GraphLike`, relying on ``nodes``/``neighbors`` views,
5
+ ``number_of_nodes`` and the graph-level ``.graph`` metadata when applying
6
+ structural transformations.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import heapq
12
+ import math
13
+ from collections.abc import Callable, Iterator
14
+ from itertools import islice
15
+ from statistics import StatisticsError, fmean
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from tnfr import glyph_history
19
+
20
+ from ..alias import get_attr
21
+ from ..constants import DEFAULTS, get_param
22
+ from ..constants.aliases import ALIAS_EPI, ALIAS_VF
23
+ from ..utils import angle_diff
24
+ from ..metrics.trig import neighbor_phase_mean
25
+ from ..rng import make_rng
26
+ from ..types import EPIValue, Glyph, NodeId, TNFRGraph
27
+ from ..utils import get_nodenx
28
+ from . import definitions as _definitions
29
+ from .jitter import (
30
+ JitterCache,
31
+ JitterCacheManager,
32
+ get_jitter_manager,
33
+ random_jitter,
34
+ reset_jitter_manager,
35
+ )
36
+ from .registry import OPERATORS, discover_operators, get_operator_class
37
+ from .remesh import (
38
+ apply_network_remesh,
39
+ apply_remesh_if_globally_stable,
40
+ apply_topological_remesh,
41
+ )
42
+
43
+ _remesh_doc = (
44
+ "Trigger a remesh once the stability window is satisfied.\n\n"
45
+ "Parameters\n----------\n"
46
+ "stable_step_window : int | None\n"
47
+ " Number of consecutive stable steps required before remeshing.\n"
48
+ " Only the English keyword 'stable_step_window' is supported."
49
+ )
50
+ if apply_remesh_if_globally_stable.__doc__:
51
+ apply_remesh_if_globally_stable.__doc__ += "\n\n" + _remesh_doc
52
+ else:
53
+ apply_remesh_if_globally_stable.__doc__ = _remesh_doc
54
+
55
+ discover_operators()
56
+
57
+ _DEFINITION_EXPORTS = {
58
+ name: getattr(_definitions, name) for name in getattr(_definitions, "__all__", ())
59
+ }
60
+ globals().update(_DEFINITION_EXPORTS)
61
+
62
+ if TYPE_CHECKING: # pragma: no cover - type checking only
63
+ from ..node import NodeProtocol
64
+
65
+ GlyphFactors = dict[str, Any]
66
+ GlyphOperation = Callable[["NodeProtocol", GlyphFactors], None]
67
+
68
+ from .grammar import apply_glyph_with_grammar # noqa: E402
69
+ from .health_analyzer import SequenceHealthAnalyzer, SequenceHealthMetrics # noqa: E402
70
+ from .hamiltonian import (
71
+ InternalHamiltonian,
72
+ build_H_coherence,
73
+ build_H_frequency,
74
+ build_H_coupling,
75
+ ) # noqa: E402
76
+ from .pattern_detection import ( # noqa: E402
77
+ PatternMatch,
78
+ UnifiedPatternDetector,
79
+ detect_pattern,
80
+ analyze_sequence,
81
+ )
82
+
83
+ __all__ = [
84
+ "JitterCache",
85
+ "JitterCacheManager",
86
+ "get_jitter_manager",
87
+ "reset_jitter_manager",
88
+ "random_jitter",
89
+ "get_neighbor_epi",
90
+ "get_glyph_factors",
91
+ "GLYPH_OPERATIONS",
92
+ "apply_glyph_obj",
93
+ "apply_glyph",
94
+ "apply_glyph_with_grammar",
95
+ "apply_network_remesh",
96
+ "apply_topological_remesh",
97
+ "apply_remesh_if_globally_stable",
98
+ "OPERATORS",
99
+ "discover_operators",
100
+ "get_operator_class",
101
+ "SequenceHealthMetrics",
102
+ "SequenceHealthAnalyzer",
103
+ "InternalHamiltonian",
104
+ "build_H_coherence",
105
+ "build_H_frequency",
106
+ "build_H_coupling",
107
+ # Pattern detection (unified module)
108
+ "PatternMatch",
109
+ "UnifiedPatternDetector",
110
+ "detect_pattern",
111
+ "analyze_sequence",
112
+ ]
113
+
114
+ __all__.extend(_DEFINITION_EXPORTS.keys())
115
+
116
+
117
+ def get_glyph_factors(node: NodeProtocol) -> GlyphFactors:
118
+ """Fetch glyph tuning factors for a node.
119
+
120
+ The glyph factors expose per-operator coefficients that modulate how an
121
+ operator reorganizes a node's Primary Information Structure (EPI),
122
+ structural frequency (νf), internal reorganization differential (ΔNFR), and
123
+ phase. Missing factors fall back to the canonical defaults stored at the
124
+ graph level.
125
+
126
+ Parameters
127
+ ----------
128
+ node : NodeProtocol
129
+ TNFR node providing a ``graph`` mapping where glyph factors may be
130
+ cached under ``"GLYPH_FACTORS"``.
131
+
132
+ Returns
133
+ -------
134
+ GlyphFactors
135
+ Mapping with operator-specific coefficients merged with the canonical
136
+ defaults. Mutating the returned mapping does not affect the graph.
137
+
138
+ Examples
139
+ --------
140
+ >>> class MockNode:
141
+ ... def __init__(self):
142
+ ... self.graph = {"GLYPH_FACTORS": {"AL_boost": 0.2}}
143
+ >>> node = MockNode()
144
+ >>> factors = get_glyph_factors(node)
145
+ >>> factors["AL_boost"]
146
+ 0.2
147
+ >>> factors["EN_mix"] # Fallback to the default reception mix
148
+ 0.25
149
+ """
150
+ return node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"].copy())
151
+
152
+
153
+ def get_factor(gf: GlyphFactors, key: str, default: float) -> float:
154
+ """Return a glyph factor as ``float`` with a default fallback.
155
+
156
+ Parameters
157
+ ----------
158
+ gf : GlyphFactors
159
+ Mapping of glyph names to numeric factors.
160
+ key : str
161
+ Factor identifier to look up.
162
+ default : float
163
+ Value used when ``key`` is absent. This typically corresponds to the
164
+ canonical operator tuning and protects structural invariants.
165
+
166
+ Returns
167
+ -------
168
+ float
169
+ The resolved factor converted to ``float``.
170
+
171
+ Notes
172
+ -----
173
+ This function performs defensive validation to ensure numeric safety.
174
+ Invalid values (non-numeric, nan, inf) are silently replaced with the
175
+ default to prevent operator failures. For strict validation, use
176
+ ``validate_glyph_factors`` before passing factors to operators.
177
+
178
+ Examples
179
+ --------
180
+ >>> get_factor({"AL_boost": 0.3}, "AL_boost", 0.05)
181
+ 0.3
182
+ >>> get_factor({}, "IL_dnfr_factor", 0.7)
183
+ 0.7
184
+ """
185
+ value = gf.get(key, default)
186
+ # Defensive validation: ensure the value is numeric and finite
187
+ # Use default for invalid values to prevent operator failures
188
+ if not isinstance(value, (int, float, str)):
189
+ return default
190
+ try:
191
+ value = float(value)
192
+ except (ValueError, TypeError):
193
+ return default
194
+ if not math.isfinite(value):
195
+ return default
196
+ return value
197
+
198
+
199
+ # -------------------------
200
+ # Glyphs (local operators)
201
+ # -------------------------
202
+
203
+
204
+ def get_neighbor_epi(node: NodeProtocol) -> tuple[list[NodeProtocol], EPIValue]:
205
+ """Collect neighbour nodes and their mean EPI.
206
+
207
+ The neighbour EPI is used by reception-like glyphs (e.g., EN, RA) to
208
+ harmonise the node's EPI with the surrounding field without mutating νf,
209
+ ΔNFR, or phase. When a neighbour lacks a direct ``EPI`` attribute the
210
+ function resolves it from NetworkX metadata using known aliases.
211
+
212
+ Parameters
213
+ ----------
214
+ node : NodeProtocol
215
+ Node whose neighbours participate in the averaging.
216
+
217
+ Returns
218
+ -------
219
+ list of NodeProtocol
220
+ Concrete neighbour objects that expose TNFR attributes.
221
+ EPIValue
222
+ Arithmetic mean of the neighbouring EPIs. Equals the node EPI when no
223
+ valid neighbours are found, allowing glyphs to preserve the node state.
224
+
225
+ Examples
226
+ --------
227
+ >>> class MockNode:
228
+ ... def __init__(self, epi, neighbors):
229
+ ... self.EPI = epi
230
+ ... self._neighbors = neighbors
231
+ ... self.graph = {}
232
+ ... def neighbors(self):
233
+ ... return self._neighbors
234
+ >>> neigh_a = MockNode(1.0, [])
235
+ >>> neigh_b = MockNode(2.0, [])
236
+ >>> node = MockNode(0.5, [neigh_a, neigh_b])
237
+ >>> neighbors, epi_bar = get_neighbor_epi(node)
238
+ >>> len(neighbors), round(epi_bar, 2)
239
+ (2, 1.5)
240
+ """
241
+
242
+ epi = node.EPI
243
+ neigh = list(node.neighbors())
244
+ if not neigh:
245
+ return [], epi
246
+
247
+ if hasattr(node, "G"):
248
+ G = node.G
249
+ total = 0.0
250
+ count = 0
251
+ has_valid_neighbor = False
252
+ needs_conversion = False
253
+ for v in neigh:
254
+ if hasattr(v, "EPI"):
255
+ total += float(v.EPI)
256
+ has_valid_neighbor = True
257
+ else:
258
+ attr = get_attr(G.nodes[v], ALIAS_EPI, None)
259
+ if attr is not None:
260
+ total += float(attr)
261
+ has_valid_neighbor = True
262
+ else:
263
+ total += float(epi)
264
+ needs_conversion = True
265
+ count += 1
266
+ if not has_valid_neighbor:
267
+ return [], epi
268
+ epi_bar = total / count if count else float(epi)
269
+ if needs_conversion:
270
+ NodeNX = get_nodenx()
271
+ if NodeNX is None:
272
+ raise ImportError("NodeNX is unavailable")
273
+ neigh = [
274
+ v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v) for v in neigh
275
+ ]
276
+ else:
277
+ try:
278
+ epi_bar = fmean(v.EPI for v in neigh)
279
+ except StatisticsError:
280
+ epi_bar = epi
281
+
282
+ return neigh, epi_bar
283
+
284
+
285
+ def _determine_dominant(
286
+ neigh: list[NodeProtocol], default_kind: str
287
+ ) -> tuple[str, float]:
288
+ """Resolve the dominant ``epi_kind`` across neighbours.
289
+
290
+ The dominant kind guides glyphs that synchronise EPI, ensuring that
291
+ reshaping a node's EPI also maintains a coherent semantic label for the
292
+ structural phase space.
293
+
294
+ Parameters
295
+ ----------
296
+ neigh : list of NodeProtocol
297
+ Neighbouring nodes providing EPI magnitude and semantic kind.
298
+ default_kind : str
299
+ Fallback label when no neighbour exposes an ``epi_kind``.
300
+
301
+ Returns
302
+ -------
303
+ tuple of (str, float)
304
+ The dominant ``epi_kind`` together with the maximum absolute EPI. The
305
+ amplitude assists downstream logic when choosing between the node's own
306
+ label and the neighbour-driven kind.
307
+
308
+ Examples
309
+ --------
310
+ >>> class Mock:
311
+ ... def __init__(self, epi, kind):
312
+ ... self.EPI = epi
313
+ ... self.epi_kind = kind
314
+ >>> _determine_dominant([Mock(0.2, "seed"), Mock(-1.0, "pulse")], "seed")
315
+ ('pulse', 1.0)
316
+ """
317
+ best_kind: str | None = None
318
+ best_abs = 0.0
319
+ for v in neigh:
320
+ abs_v = abs(v.EPI)
321
+ if abs_v > best_abs:
322
+ best_abs = abs_v
323
+ best_kind = v.epi_kind
324
+ if not best_kind:
325
+ return default_kind, 0.0
326
+ return best_kind, best_abs
327
+
328
+
329
+ def _mix_epi_with_neighbors(
330
+ node: NodeProtocol, mix: float, default_glyph: Glyph | str
331
+ ) -> tuple[float, str]:
332
+ """Blend node EPI with the neighbour field and update its semantic label.
333
+
334
+ The routine is shared by reception-like glyphs. It interpolates between the
335
+ node EPI and the neighbour mean while selecting a dominant ``epi_kind``.
336
+ ΔNFR, νf, and phase remain untouched; the function focuses on reconciling
337
+ form.
338
+
339
+ Parameters
340
+ ----------
341
+ node : NodeProtocol
342
+ Node that exposes ``EPI`` and ``epi_kind`` attributes.
343
+ mix : float
344
+ Interpolation weight for the neighbour mean. ``mix = 0`` preserves the
345
+ current EPI, while ``mix = 1`` adopts the average neighbour field.
346
+ default_glyph : Glyph or str
347
+ Glyph driving the mix. Its value informs the fallback ``epi_kind``.
348
+
349
+ Returns
350
+ -------
351
+ tuple of (float, str)
352
+ The neighbour mean EPI and the resolved ``epi_kind`` after mixing.
353
+
354
+ Examples
355
+ --------
356
+ >>> class MockNode:
357
+ ... def __init__(self, epi, kind, neighbors):
358
+ ... self.EPI = epi
359
+ ... self.epi_kind = kind
360
+ ... self.graph = {}
361
+ ... self._neighbors = neighbors
362
+ ... def neighbors(self):
363
+ ... return self._neighbors
364
+ >>> neigh = [MockNode(0.8, "wave", []), MockNode(1.2, "wave", [])]
365
+ >>> node = MockNode(0.0, "seed", neigh)
366
+ >>> _, kind = _mix_epi_with_neighbors(node, 0.5, Glyph.EN)
367
+ >>> round(node.EPI, 2), kind
368
+ (0.5, 'wave')
369
+ """
370
+ default_kind = (
371
+ default_glyph.value if isinstance(default_glyph, Glyph) else str(default_glyph)
372
+ )
373
+ epi = node.EPI
374
+ neigh, epi_bar = get_neighbor_epi(node)
375
+
376
+ if not neigh:
377
+ node.epi_kind = default_kind
378
+ return epi, default_kind
379
+
380
+ dominant, best_abs = _determine_dominant(neigh, default_kind)
381
+ new_epi = (1 - mix) * epi + mix * epi_bar
382
+ _set_epi_with_boundary_check(node, new_epi)
383
+ final = dominant if best_abs > abs(new_epi) else node.epi_kind
384
+ if not final:
385
+ final = default_kind
386
+ node.epi_kind = final
387
+ return epi_bar, final
388
+
389
+
390
+ def _op_AL(node: NodeProtocol, gf: GlyphFactors) -> None: # AL — Emission
391
+ """Amplify the node EPI via the Emission glyph.
392
+
393
+ Emission injects additional coherence into the node by boosting its EPI
394
+ without touching νf, ΔNFR, or phase. The boost amplitude is controlled by
395
+ ``AL_boost``.
396
+
397
+ Parameters
398
+ ----------
399
+ node : NodeProtocol
400
+ Node whose EPI is increased.
401
+ gf : GlyphFactors
402
+ Factor mapping used to resolve ``AL_boost``.
403
+
404
+ Examples
405
+ --------
406
+ >>> class MockNode:
407
+ ... def __init__(self, epi):
408
+ ... self.EPI = epi
409
+ ... self.graph = {}
410
+ >>> node = MockNode(0.8)
411
+ >>> _op_AL(node, {"AL_boost": 0.2})
412
+ >>> node.EPI <= 1.0 # Bounded by structural_clip
413
+ True
414
+ """
415
+ f = get_factor(gf, "AL_boost", 0.05)
416
+ new_epi = node.EPI + f
417
+ _set_epi_with_boundary_check(node, new_epi)
418
+
419
+
420
+ def _op_EN(node: NodeProtocol, gf: GlyphFactors) -> None: # EN — Reception
421
+ """Mix the node EPI with the neighbour field via Reception.
422
+
423
+ Reception reorganizes the node's EPI towards the neighbourhood mean while
424
+ choosing a coherent ``epi_kind``. νf, ΔNFR, and phase remain unchanged.
425
+
426
+ Parameters
427
+ ----------
428
+ node : NodeProtocol
429
+ Node whose EPI is being reconciled.
430
+ gf : GlyphFactors
431
+ Source of the ``EN_mix`` blending coefficient.
432
+
433
+ Examples
434
+ --------
435
+ >>> class MockNode:
436
+ ... def __init__(self, epi, neighbors):
437
+ ... self.EPI = epi
438
+ ... self.epi_kind = "seed"
439
+ ... self.graph = {}
440
+ ... self._neighbors = neighbors
441
+ ... def neighbors(self):
442
+ ... return self._neighbors
443
+ >>> neigh = [MockNode(1.0, []), MockNode(0.0, [])]
444
+ >>> node = MockNode(0.4, neigh)
445
+ >>> _op_EN(node, {"EN_mix": 0.5})
446
+ >>> round(node.EPI, 2)
447
+ 0.7
448
+ """
449
+ mix = get_factor(gf, "EN_mix", 0.25)
450
+ _mix_epi_with_neighbors(node, mix, Glyph.EN)
451
+
452
+
453
+ def _op_IL(node: NodeProtocol, gf: GlyphFactors) -> None: # IL — Coherence
454
+ """Dampen ΔNFR magnitudes through the Coherence glyph.
455
+
456
+ Coherence contracts the internal reorganization differential (ΔNFR) while
457
+ leaving EPI, νf, and phase untouched. The contraction preserves the sign of
458
+ ΔNFR, increasing structural stability.
459
+
460
+ Parameters
461
+ ----------
462
+ node : NodeProtocol
463
+ Node whose ΔNFR is being scaled.
464
+ gf : GlyphFactors
465
+ Provides ``IL_dnfr_factor`` controlling the contraction strength.
466
+
467
+ Examples
468
+ --------
469
+ >>> class MockNode:
470
+ ... def __init__(self, dnfr):
471
+ ... self.dnfr = dnfr
472
+ >>> node = MockNode(0.5)
473
+ >>> _op_IL(node, {"IL_dnfr_factor": 0.2})
474
+ >>> node.dnfr
475
+ 0.1
476
+ """
477
+ factor = get_factor(gf, "IL_dnfr_factor", 0.7)
478
+ node.dnfr = factor * getattr(node, "dnfr", 0.0)
479
+
480
+
481
+ def _op_OZ(node: NodeProtocol, gf: GlyphFactors) -> None: # OZ — Dissonance
482
+ """Excite ΔNFR through the Dissonance glyph.
483
+
484
+ Dissonance amplifies ΔNFR or injects jitter, testing the node's stability.
485
+ EPI, νf, and phase remain unaffected while ΔNFR grows to trigger potential
486
+ bifurcations.
487
+
488
+ Parameters
489
+ ----------
490
+ node : NodeProtocol
491
+ Node whose ΔNFR is being stressed.
492
+ gf : GlyphFactors
493
+ Supplies ``OZ_dnfr_factor`` and optional noise parameters.
494
+
495
+ Examples
496
+ --------
497
+ >>> class MockNode:
498
+ ... def __init__(self, dnfr):
499
+ ... self.dnfr = dnfr
500
+ ... self.graph = {}
501
+ >>> node = MockNode(0.2)
502
+ >>> _op_OZ(node, {"OZ_dnfr_factor": 2.0})
503
+ >>> node.dnfr
504
+ 0.4
505
+ """
506
+ factor = get_factor(gf, "OZ_dnfr_factor", 1.3)
507
+ dnfr = getattr(node, "dnfr", 0.0)
508
+ if bool(node.graph.get("OZ_NOISE_MODE", False)):
509
+ sigma = float(node.graph.get("OZ_SIGMA", 0.1))
510
+ if sigma <= 0:
511
+ node.dnfr = dnfr
512
+ return
513
+ node.dnfr = dnfr + random_jitter(node, sigma)
514
+ else:
515
+ node.dnfr = factor * dnfr if abs(dnfr) > 1e-9 else 0.1
516
+
517
+
518
+ def _um_candidate_iter(node: NodeProtocol) -> Iterator[NodeProtocol]:
519
+ sample_ids = node.graph.get("_node_sample")
520
+ if sample_ids is not None and hasattr(node, "G"):
521
+ NodeNX = get_nodenx()
522
+ if NodeNX is None:
523
+ raise ImportError("NodeNX is unavailable")
524
+ base = (NodeNX.from_graph(node.G, j) for j in sample_ids)
525
+ else:
526
+ base = node.all_nodes()
527
+ for j in base:
528
+ same = (j is node) or (getattr(node, "n", None) == getattr(j, "n", None))
529
+ if same or node.has_edge(j):
530
+ continue
531
+ yield j
532
+
533
+
534
+ def _um_select_candidates(
535
+ node: NodeProtocol,
536
+ candidates: Iterator[NodeProtocol],
537
+ limit: int,
538
+ mode: str,
539
+ th: float,
540
+ ) -> list[NodeProtocol]:
541
+ """Select a subset of ``candidates`` for UM coupling."""
542
+ rng = make_rng(int(node.graph.get("RANDOM_SEED", 0)), node.offset(), node.G)
543
+
544
+ if limit <= 0:
545
+ return list(candidates)
546
+
547
+ if mode == "proximity":
548
+ return heapq.nsmallest(
549
+ limit, candidates, key=lambda j: abs(angle_diff(j.theta, th))
550
+ )
551
+
552
+ reservoir = list(islice(candidates, limit))
553
+ for i, cand in enumerate(candidates, start=limit):
554
+ j = rng.randint(0, i)
555
+ if j < limit:
556
+ reservoir[j] = cand
557
+
558
+ if mode == "sample":
559
+ rng.shuffle(reservoir)
560
+
561
+ return reservoir
562
+
563
+
564
+ def compute_consensus_phase(phases: list[float]) -> float:
565
+ """Compute circular mean (consensus phase) from a list of phase angles.
566
+
567
+ This function calculates the consensus phase using the circular mean
568
+ formula: arctan2(mean(sin), mean(cos)). This ensures proper handling
569
+ of phase wrapping at ±π boundaries.
570
+
571
+ Parameters
572
+ ----------
573
+ phases : list[float]
574
+ List of phase angles in radians.
575
+
576
+ Returns
577
+ -------
578
+ float
579
+ Consensus phase angle in radians, in the range [-π, π).
580
+
581
+ Notes
582
+ -----
583
+ The consensus phase represents the central tendency of a set of angular
584
+ values, accounting for the circular nature of phase space. This is
585
+ critical for bidirectional phase synchronization in the UM operator.
586
+
587
+ Examples
588
+ --------
589
+ >>> import math
590
+ >>> phases = [0.0, math.pi/2, math.pi]
591
+ >>> result = compute_consensus_phase(phases)
592
+ >>> -math.pi <= result < math.pi
593
+ True
594
+ """
595
+ if not phases:
596
+ return 0.0
597
+
598
+ cos_sum = sum(math.cos(ph) for ph in phases)
599
+ sin_sum = sum(math.sin(ph) for ph in phases)
600
+ return math.atan2(sin_sum, cos_sum)
601
+
602
+
603
+ def _op_UM(node: NodeProtocol, gf: GlyphFactors) -> None: # UM — Coupling
604
+ """Align node phase and frequency with neighbours and optionally create links.
605
+
606
+ Coupling shifts the node phase ``theta`` towards the neighbour mean while
607
+ respecting νf and EPI. When bidirectional mode is enabled (default), both
608
+ the node and its neighbors synchronize their phases mutually. Additionally,
609
+ structural frequency (νf) synchronization causes coupled nodes to converge
610
+ their reorganization rates. Coupling also reduces ΔNFR through mutual
611
+ stabilization, decreasing reorganization pressure proportional to phase
612
+ alignment strength. When functional links are enabled it may add edges
613
+ based on combined phase, EPI, and sense-index similarity.
614
+
615
+ Parameters
616
+ ----------
617
+ node : NodeProtocol
618
+ Node whose phase and frequency are being synchronised.
619
+ gf : GlyphFactors
620
+ Provides ``UM_theta_push``, ``UM_vf_sync``, ``UM_dnfr_reduction`` and
621
+ optional selection parameters.
622
+
623
+ Notes
624
+ -----
625
+ Bidirectional synchronization (UM_BIDIRECTIONAL=True, default) implements
626
+ the canonical TNFR requirement φᵢ(t) ≈ φⱼ(t) by mutually adjusting phases
627
+ of both the node and its neighbors towards a consensus phase. This ensures
628
+ true coupling as defined in the theory.
629
+
630
+ Structural frequency synchronization (UM_SYNC_VF=True, default) implements
631
+ the TNFR requirement that coupling synchronizes not only phases but also
632
+ structural frequencies (νf). This enables coupled nodes to converge their
633
+ reorganization rates, which is essential for sustained resonance and coherent
634
+ network evolution as described by the nodal equation: ∂EPI/∂t = νf · ΔNFR(t).
635
+
636
+ ΔNFR stabilization (UM_STABILIZE_DNFR=True, default) implements the canonical
637
+ effect where coupling reduces reorganization pressure through mutual stabilization.
638
+ The reduction is proportional to phase alignment: well-coupled nodes (high phase
639
+ alignment) experience stronger ΔNFR reduction, promoting structural coherence.
640
+
641
+ Legacy unidirectional mode (UM_BIDIRECTIONAL=False) only adjusts the node's
642
+ phase towards its neighbors, preserving backward compatibility.
643
+
644
+ Examples
645
+ --------
646
+ >>> import math
647
+ >>> class MockNode:
648
+ ... def __init__(self, theta, neighbors):
649
+ ... self.theta = theta
650
+ ... self.EPI = 1.0
651
+ ... self.Si = 0.5
652
+ ... self.graph = {}
653
+ ... self._neighbors = neighbors
654
+ ... def neighbors(self):
655
+ ... return self._neighbors
656
+ ... def offset(self):
657
+ ... return 0
658
+ ... def all_nodes(self):
659
+ ... return []
660
+ ... def has_edge(self, _):
661
+ ... return False
662
+ ... def add_edge(self, *_):
663
+ ... raise AssertionError("not used in example")
664
+ >>> neighbor = MockNode(math.pi / 2, [])
665
+ >>> node = MockNode(0.0, [neighbor])
666
+ >>> _op_UM(node, {"UM_theta_push": 0.5})
667
+ >>> round(node.theta, 2)
668
+ 0.79
669
+ """
670
+ k = get_factor(gf, "UM_theta_push", 0.25)
671
+ k_vf = get_factor(gf, "UM_vf_sync", 0.10)
672
+ th_i = node.theta
673
+
674
+ # Check if bidirectional synchronization is enabled (default: True)
675
+ bidirectional = bool(node.graph.get("UM_BIDIRECTIONAL", True))
676
+
677
+ if bidirectional:
678
+ # Bidirectional mode: mutually synchronize node and neighbors
679
+ neighbor_ids = list(node.neighbors())
680
+ if neighbor_ids:
681
+ # Get NodeNX wrapper for accessing neighbor attributes
682
+ NodeNX = get_nodenx()
683
+ if NodeNX is None or not hasattr(node, "G"):
684
+ # Fallback to unidirectional if NodeNX unavailable
685
+ thL = neighbor_phase_mean(node)
686
+ d = angle_diff(thL, th_i)
687
+ node.theta = th_i + k * d
688
+ else:
689
+ # Wrap neighbor IDs to access theta attribute
690
+ neighbors = [NodeNX.from_graph(node.G, nid) for nid in neighbor_ids]
691
+
692
+ # Collect all phases (node + neighbors)
693
+ phases = [th_i] + [n.theta for n in neighbors]
694
+ target_phase = compute_consensus_phase(phases)
695
+
696
+ # Adjust node phase towards consensus
697
+ node.theta = th_i + k * angle_diff(target_phase, th_i)
698
+
699
+ # Adjust neighbor phases towards consensus
700
+ for neighbor in neighbors:
701
+ th_j = neighbor.theta
702
+ neighbor.theta = th_j + k * angle_diff(target_phase, th_j)
703
+ else:
704
+ # Legacy unidirectional mode: only adjust node towards neighbors
705
+ thL = neighbor_phase_mean(node)
706
+ d = angle_diff(thL, th_i)
707
+ node.theta = th_i + k * d
708
+
709
+ # Structural frequency (νf) synchronization
710
+ # According to TNFR theory, coupling synchronizes both phase and frequency
711
+ sync_vf = bool(node.graph.get("UM_SYNC_VF", True))
712
+ if sync_vf:
713
+ neighbor_ids = list(node.neighbors())
714
+ if neighbor_ids and hasattr(node, "G"):
715
+ # Canonical access to vf through alias system
716
+ vf_i = node.vf
717
+ vf_neighbors = [
718
+ get_attr(node.G.nodes[nid], ALIAS_VF, 0.0) for nid in neighbor_ids
719
+ ]
720
+
721
+ if vf_neighbors:
722
+ vf_mean = sum(vf_neighbors) / len(vf_neighbors)
723
+
724
+ # Gradual convergence towards mean (similar to phase sync)
725
+ node.vf = vf_i + k_vf * (vf_mean - vf_i)
726
+
727
+ # ΔNFR reduction by mutual stabilization
728
+ # Coupling produces a stabilizing effect that reduces reorganization pressure
729
+ stabilize_dnfr = bool(node.graph.get("UM_STABILIZE_DNFR", True))
730
+
731
+ if stabilize_dnfr:
732
+ k_dnfr = get_factor(gf, "UM_dnfr_reduction", 0.15)
733
+
734
+ # Calculate compatibility with neighbors based on phase alignment
735
+ neighbor_ids = list(node.neighbors())
736
+ if neighbor_ids:
737
+ # Get NodeNX wrapper for accessing neighbor attributes
738
+ NodeNX = get_nodenx()
739
+ if NodeNX is not None and hasattr(node, "G"):
740
+ neighbors = [NodeNX.from_graph(node.G, nid) for nid in neighbor_ids]
741
+
742
+ # Compute phase alignments with each neighbor
743
+ phase_alignments = []
744
+ # Compute phase alignment using canonical formula
745
+ from ..metrics.phase_compatibility import compute_phase_coupling_strength
746
+
747
+ for neighbor in neighbors:
748
+ alignment = compute_phase_coupling_strength(node.theta, neighbor.theta)
749
+ phase_alignments.append(alignment)
750
+
751
+ # Mean alignment represents coupling strength
752
+ mean_alignment = sum(phase_alignments) / len(phase_alignments)
753
+
754
+ # Reduce ΔNFR proportionally to coupling strength
755
+ # reduction_factor < 1.0 when well-coupled (high alignment)
756
+ reduction_factor = 1.0 - (k_dnfr * mean_alignment)
757
+ node.dnfr = node.dnfr * reduction_factor
758
+
759
+ if bool(node.graph.get("UM_FUNCTIONAL_LINKS", True)):
760
+ thr = float(
761
+ node.graph.get(
762
+ "UM_COMPAT_THRESHOLD",
763
+ DEFAULTS.get("UM_COMPAT_THRESHOLD", 0.75),
764
+ )
765
+ )
766
+ epi_i = node.EPI
767
+ si_i = node.Si
768
+
769
+ limit = int(node.graph.get("UM_CANDIDATE_COUNT", 0))
770
+ mode = str(node.graph.get("UM_CANDIDATE_MODE", "sample")).lower()
771
+ candidates = _um_select_candidates(
772
+ node, _um_candidate_iter(node), limit, mode, th_i
773
+ )
774
+
775
+ for j in candidates:
776
+ # Use canonical phase coupling strength formula
777
+ from ..metrics.phase_compatibility import compute_phase_coupling_strength
778
+
779
+ phase_coupling = compute_phase_coupling_strength(th_i, j.theta)
780
+
781
+ epi_j = j.EPI
782
+ si_j = j.Si
783
+ epi_sim = 1.0 - abs(epi_i - epi_j) / (abs(epi_i) + abs(epi_j) + 1e-9)
784
+ si_sim = 1.0 - abs(si_i - si_j)
785
+ # Compatibility combines phase coupling (50%), EPI similarity (25%), Si similarity (25%)
786
+ compat = phase_coupling * 0.5 + 0.25 * epi_sim + 0.25 * si_sim
787
+ if compat >= thr:
788
+ node.add_edge(j, compat)
789
+
790
+
791
+ def _op_RA(node: NodeProtocol, gf: GlyphFactors) -> None: # RA — Resonance
792
+ """Propagate coherence through resonance with νf amplification.
793
+
794
+ Resonance (RA) propagates EPI along existing couplings while amplifying
795
+ the structural frequency (νf) to reflect network coherence propagation.
796
+ According to TNFR theory, RA creates "resonant cascades" where coherence
797
+ amplifies across the network, increasing collective νf and global C(t).
798
+
799
+ **Canonical Effects (always active):**
800
+
801
+ - **EPI Propagation**: Diffuses EPI to neighbors (identity-preserving)
802
+ - **νf Amplification**: Increases structural frequency when propagating coherence
803
+ - **Phase Alignment**: Strengthens phase synchrony across propagation path
804
+ - **Network C(t)**: Contributes to global coherence increase
805
+ - **Identity Preservation**: Maintains structural identity during propagation
806
+
807
+ Parameters
808
+ ----------
809
+ node : NodeProtocol
810
+ Node harmonising with its neighbourhood.
811
+ gf : GlyphFactors
812
+ Provides ``RA_epi_diff`` (mixing coefficient, default 0.15),
813
+ ``RA_vf_amplification`` (νf boost factor, default 0.05), and
814
+ ``RA_phase_coupling`` (phase alignment factor, default 0.10).
815
+
816
+ Notes
817
+ -----
818
+ **νf Amplification (Canonical)**: When neighbors have coherence (|epi_bar| > 1e-9),
819
+ node.vf is multiplied by (1.0 + RA_vf_amplification). This reflects
820
+ the canonical TNFR property that resonance amplifies collective νf.
821
+ This is NOT optional - it is a fundamental property of resonance per TNFR theory.
822
+
823
+ **Phase Alignment Strengthening (Canonical)**: RA strengthens phase alignment
824
+ with neighbors by applying a small phase correction toward the network mean.
825
+ This ensures that "Phase alignment: Strengthens across propagation path" as
826
+ stated in the theoretical foundations. Uses existing phase utility functions
827
+ to avoid code duplication.
828
+
829
+ **Network Coherence Tracking (Optional)**: If ``TRACK_NETWORK_COHERENCE`` is enabled,
830
+ global C(t) is measured before/after RA application to quantify network-level
831
+ coherence increase.
832
+
833
+ **Identity Preservation (Canonical)**: EPI structure (kind and sign) are preserved
834
+ during propagation to ensure structural identity is maintained as required by theory.
835
+
836
+ Examples
837
+ --------
838
+ >>> class MockNode:
839
+ ... def __init__(self, epi, neighbors):
840
+ ... self.EPI = epi
841
+ ... self.epi_kind = "seed"
842
+ ... self.vf = 1.0
843
+ ... self.theta = 0.0
844
+ ... self.graph = {}
845
+ ... self._neighbors = neighbors
846
+ ... def neighbors(self):
847
+ ... return self._neighbors
848
+ >>> neighbor = MockNode(1.0, [])
849
+ >>> neighbor.theta = 0.1
850
+ >>> node = MockNode(0.2, [neighbor])
851
+ >>> _op_RA(node, {"RA_epi_diff": 0.25, "RA_vf_amplification": 0.05})
852
+ >>> round(node.EPI, 2)
853
+ 0.4
854
+ >>> node.vf # Amplified due to neighbor coherence (canonical effect)
855
+ 1.05
856
+ """
857
+ # Get configuration factors
858
+ diff = get_factor(gf, "RA_epi_diff", 0.15)
859
+ vf_boost = get_factor(gf, "RA_vf_amplification", 0.05)
860
+ phase_coupling = get_factor(
861
+ gf, "RA_phase_coupling", 0.10
862
+ ) # Canonical phase strengthening
863
+
864
+ # Track network C(t) before RA if enabled (optional telemetry)
865
+ track_coherence = bool(node.graph.get("TRACK_NETWORK_COHERENCE", False))
866
+ c_before = None
867
+ if track_coherence and hasattr(node, "G"):
868
+ try:
869
+ from ..metrics.coherence import compute_network_coherence
870
+
871
+ c_before = compute_network_coherence(node.G)
872
+ if "_ra_c_tracking" not in node.graph:
873
+ node.graph["_ra_c_tracking"] = []
874
+ except ImportError:
875
+ pass # Metrics module not available
876
+
877
+ # Capture state before for metrics
878
+ vf_before = node.vf
879
+ epi_before = node.EPI
880
+ kind_before = node.epi_kind
881
+ theta_before = node.theta if hasattr(node, "theta") else None
882
+
883
+ # EPI diffusion (existing behavior)
884
+ neigh, epi_bar = get_neighbor_epi(node)
885
+ epi_bar_result, kind_result = _mix_epi_with_neighbors(node, diff, Glyph.RA)
886
+
887
+ # CANONICAL EFFECT 1: νf amplification through resonance
888
+ # This is always active - it's a fundamental property of resonance per TNFR theory
889
+ # Only amplify if neighbors have coherence to propagate
890
+ if abs(epi_bar_result) > 1e-9 and len(neigh) > 0:
891
+ node.vf *= 1.0 + vf_boost
892
+
893
+ # CANONICAL EFFECT 2: Phase alignment strengthening
894
+ # Per theory: "Phase alignment: Strengthens across propagation path"
895
+ # Uses existing phase locking logic from IL operator (avoid duplication)
896
+ phase_strengthened = False
897
+ if len(neigh) > 0 and hasattr(node, "theta") and hasattr(node, "G"):
898
+ try:
899
+ # Use existing phase locking utility from IL operator
900
+ from ..alias import get_attr
901
+ from ..constants.aliases import ALIAS_THETA
902
+ import cmath
903
+ import math
904
+
905
+ # Get neighbor phases using existing utilities
906
+ neighbor_phases = []
907
+ for n in neigh:
908
+ try:
909
+ theta_n = float(get_attr(n, ALIAS_THETA, 0.0))
910
+ neighbor_phases.append(theta_n)
911
+ except (KeyError, ValueError, TypeError):
912
+ continue
913
+
914
+ if neighbor_phases:
915
+ # Circular mean using the same method as in phase_coherence.py
916
+ complex_phases = [cmath.exp(1j * theta) for theta in neighbor_phases]
917
+ mean_real = sum(z.real for z in complex_phases) / len(complex_phases)
918
+ mean_imag = sum(z.imag for z in complex_phases) / len(complex_phases)
919
+ mean_complex = complex(mean_real, mean_imag)
920
+ mean_phase = cmath.phase(mean_complex)
921
+
922
+ # Ensure positive phase [0, 2π]
923
+ if mean_phase < 0:
924
+ mean_phase += 2 * math.pi
925
+
926
+ # Calculate phase difference (shortest arc)
927
+ delta_theta = mean_phase - node.theta
928
+ if delta_theta > math.pi:
929
+ delta_theta -= 2 * math.pi
930
+ elif delta_theta < -math.pi:
931
+ delta_theta += 2 * math.pi
932
+
933
+ # Apply phase strengthening (move toward network mean)
934
+ # Same approach as IL operator phase locking
935
+ node.theta = node.theta + phase_coupling * delta_theta
936
+
937
+ # Normalize to [0, 2π]
938
+ node.theta = node.theta % (2 * math.pi)
939
+ phase_strengthened = True
940
+ except (AttributeError, ImportError):
941
+ pass # Phase alignment not possible in this context
942
+
943
+ # Track identity preservation (canonical validation)
944
+ identity_preserved = (
945
+ kind_result == kind_before or kind_result == Glyph.RA.value
946
+ ) and (
947
+ float(epi_before) * float(node.EPI) >= 0
948
+ ) # Sign preserved
949
+
950
+ # Collect propagation metrics if enabled (optional telemetry)
951
+ collect_metrics = bool(node.graph.get("COLLECT_RA_METRICS", False))
952
+ if collect_metrics:
953
+ metrics = {
954
+ "operator": "RA",
955
+ "epi_propagated": epi_bar_result,
956
+ "vf_amplification": node.vf / vf_before if vf_before > 0 else 1.0,
957
+ "neighbors_influenced": len(neigh),
958
+ "identity_preserved": identity_preserved,
959
+ "epi_before": epi_before,
960
+ "epi_after": float(node.EPI),
961
+ "vf_before": vf_before,
962
+ "vf_after": node.vf,
963
+ "phase_before": theta_before,
964
+ "phase_after": node.theta if hasattr(node, "theta") else None,
965
+ "phase_alignment_strengthened": phase_strengthened,
966
+ }
967
+ if "ra_metrics" not in node.graph:
968
+ node.graph["ra_metrics"] = []
969
+ node.graph["ra_metrics"].append(metrics)
970
+
971
+ # Track network C(t) after RA if enabled (optional telemetry)
972
+ if track_coherence and c_before is not None and hasattr(node, "G"):
973
+ try:
974
+ from ..metrics.coherence import compute_network_coherence
975
+
976
+ c_after = compute_network_coherence(node.G)
977
+ node.graph["_ra_c_tracking"].append(
978
+ {
979
+ "node": getattr(node, "n", None),
980
+ "c_before": c_before,
981
+ "c_after": c_after,
982
+ "c_delta": c_after - c_before,
983
+ }
984
+ )
985
+ except ImportError:
986
+ pass
987
+
988
+
989
+ def _op_SHA(node: NodeProtocol, gf: GlyphFactors) -> None: # SHA — Silence
990
+ """Reduce νf while preserving EPI, ΔNFR, and phase.
991
+
992
+ Silence decelerates a node by scaling νf (structural frequency) towards
993
+ stillness. EPI, ΔNFR, and phase remain unchanged, signalling a temporary
994
+ suspension of structural evolution.
995
+
996
+ **TNFR Canonical Behavior:**
997
+
998
+ According to the nodal equation ∂EPI/∂t = νf · ΔNFR(t), reducing νf → νf_min ≈ 0
999
+ causes structural evolution to freeze (∂EPI/∂t → 0) regardless of ΔNFR magnitude.
1000
+ This implements **structural silence** - a state where the node's form (EPI) is
1001
+ preserved intact despite external pressures, enabling memory consolidation and
1002
+ protective latency.
1003
+
1004
+ Parameters
1005
+ ----------
1006
+ node : NodeProtocol
1007
+ Node whose νf is being attenuated.
1008
+ gf : GlyphFactors
1009
+ Provides ``SHA_vf_factor`` to scale νf (default 0.85 for gradual reduction).
1010
+
1011
+ Examples
1012
+ --------
1013
+ >>> class MockNode:
1014
+ ... def __init__(self, vf):
1015
+ ... self.vf = vf
1016
+ >>> node = MockNode(1.0)
1017
+ >>> _op_SHA(node, {"SHA_vf_factor": 0.5})
1018
+ >>> node.vf
1019
+ 0.5
1020
+ """
1021
+ factor = get_factor(gf, "SHA_vf_factor", 0.85)
1022
+ # Canonical SHA effect: reduce structural frequency toward zero
1023
+ # This implements: νf → νf_min ≈ 0 ⇒ ∂EPI/∂t → 0 (structural preservation)
1024
+ node.vf = factor * node.vf
1025
+
1026
+
1027
+ factor_val = 1.05 # Conservative scale prevents EPI overflow near boundaries
1028
+ factor_nul = 0.85
1029
+ _SCALE_FACTORS = {Glyph.VAL: factor_val, Glyph.NUL: factor_nul}
1030
+
1031
+
1032
+ def _set_epi_with_boundary_check(
1033
+ node: NodeProtocol, new_epi: float, *, apply_clip: bool = True
1034
+ ) -> None:
1035
+ """Canonical EPI assignment with structural boundary preservation.
1036
+
1037
+ This is the unified function all operators should use when modifying EPI
1038
+ to ensure structural boundaries are respected. Provides single point of
1039
+ enforcement for TNFR canonical invariant: EPI ∈ [EPI_MIN, EPI_MAX].
1040
+
1041
+ Parameters
1042
+ ----------
1043
+ node : NodeProtocol
1044
+ Node whose EPI is being updated
1045
+ new_epi : float
1046
+ New EPI value to assign
1047
+ apply_clip : bool, default True
1048
+ If True, applies structural_clip to enforce boundaries.
1049
+ If False, assigns value directly (use only when boundaries
1050
+ are known to be satisfied, e.g., from edge-aware pre-computation).
1051
+
1052
+ Notes
1053
+ -----
1054
+ TNFR Principle: This function embodies the canonical invariant that EPI
1055
+ must remain within structural boundaries. All operator EPI modifications
1056
+ should flow through this function to maintain coherence.
1057
+
1058
+ The function uses the graph-level configuration for EPI_MIN, EPI_MAX,
1059
+ and CLIP_MODE to ensure consistent boundary enforcement across all operators.
1060
+
1061
+ Examples
1062
+ --------
1063
+ >>> class MockNode:
1064
+ ... def __init__(self, epi):
1065
+ ... self.EPI = epi
1066
+ ... self.graph = {"EPI_MAX": 1.0, "EPI_MIN": -1.0}
1067
+ >>> node = MockNode(0.5)
1068
+ >>> _set_epi_with_boundary_check(node, 1.2) # Will be clipped to 1.0
1069
+ >>> float(node.EPI)
1070
+ 1.0
1071
+ """
1072
+ from ..dynamics.structural_clip import structural_clip
1073
+
1074
+ if not apply_clip:
1075
+ node.EPI = new_epi
1076
+ return
1077
+
1078
+ # Ensure new_epi is float (in case it's a BEPI or other structure)
1079
+ new_epi_float = float(new_epi)
1080
+
1081
+ # Get boundary configuration from graph (with defensive fallback)
1082
+ graph_attrs = getattr(node, "graph", {})
1083
+ epi_min = float(graph_attrs.get("EPI_MIN", DEFAULTS.get("EPI_MIN", -1.0)))
1084
+ epi_max = float(graph_attrs.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0)))
1085
+ clip_mode_str = str(graph_attrs.get("CLIP_MODE", "hard"))
1086
+
1087
+ # Validate clip mode
1088
+ if clip_mode_str not in ("hard", "soft"):
1089
+ clip_mode_str = "hard"
1090
+
1091
+ # Apply structural boundary preservation
1092
+ clipped_epi = structural_clip(
1093
+ new_epi_float,
1094
+ lo=epi_min,
1095
+ hi=epi_max,
1096
+ mode=clip_mode_str, # type: ignore[arg-type]
1097
+ record_stats=False,
1098
+ )
1099
+
1100
+ node.EPI = clipped_epi
1101
+
1102
+
1103
+ def _compute_val_edge_aware_scale(
1104
+ epi_current: float, scale: float, epi_max: float, epsilon: float
1105
+ ) -> float:
1106
+ """Compute edge-aware scale factor for VAL (Expansion) operator.
1107
+
1108
+ Adapts the expansion scale to prevent EPI overflow beyond EPI_MAX.
1109
+ When EPI is near the upper boundary, the effective scale is reduced
1110
+ to ensure EPI * scale_eff <= EPI_MAX.
1111
+
1112
+ Parameters
1113
+ ----------
1114
+ epi_current : float
1115
+ Current EPI value
1116
+ scale : float
1117
+ Desired expansion scale factor (e.g., VAL_scale = 1.05)
1118
+ epi_max : float
1119
+ Upper EPI boundary (typically 1.0)
1120
+ epsilon : float
1121
+ Small value to prevent division by zero (e.g., 1e-12)
1122
+
1123
+ Returns
1124
+ -------
1125
+ float
1126
+ Effective scale factor, adapted to respect EPI_MAX boundary
1127
+
1128
+ Notes
1129
+ -----
1130
+ TNFR Principle: This implements "resonance to the edge" - expansion
1131
+ scales adaptively to explore volume while respecting structural envelope.
1132
+ The adaptation is a dynamic compatibility check, not a fixed constant.
1133
+
1134
+ Examples
1135
+ --------
1136
+ >>> # Normal case: EPI far from boundary
1137
+ >>> _compute_val_edge_aware_scale(0.5, 1.05, 1.0, 1e-12)
1138
+ 1.05
1139
+
1140
+ >>> # Edge case: EPI near boundary, scale adapts
1141
+ >>> scale = _compute_val_edge_aware_scale(0.96, 1.05, 1.0, 1e-12)
1142
+ >>> abs(scale - 1.0417) < 0.001 # Roughly 1.0/0.96
1143
+ True
1144
+ """
1145
+ abs_epi = abs(epi_current)
1146
+ if abs_epi < epsilon:
1147
+ # EPI near zero, full scale can be applied safely
1148
+ return scale
1149
+
1150
+ # Compute maximum safe scale that keeps EPI within bounds
1151
+ max_safe_scale = epi_max / abs_epi
1152
+
1153
+ # Return the minimum of desired scale and safe scale
1154
+ return min(scale, max_safe_scale)
1155
+
1156
+
1157
+ def _compute_nul_edge_aware_scale(
1158
+ epi_current: float, scale: float, epi_min: float, epsilon: float
1159
+ ) -> float:
1160
+ """Compute edge-aware scale factor for NUL (Contraction) operator.
1161
+
1162
+ Adapts the contraction scale to prevent EPI underflow below EPI_MIN.
1163
+
1164
+ Parameters
1165
+ ----------
1166
+ epi_current : float
1167
+ Current EPI value
1168
+ scale : float
1169
+ Desired contraction scale factor (e.g., NUL_scale = 0.85)
1170
+ epi_min : float
1171
+ Lower EPI boundary (typically -1.0)
1172
+ epsilon : float
1173
+ Small value to prevent division by zero (e.g., 1e-12)
1174
+
1175
+ Returns
1176
+ -------
1177
+ float
1178
+ Effective scale factor, adapted to respect EPI_MIN boundary
1179
+
1180
+ Notes
1181
+ -----
1182
+ TNFR Principle: Contraction concentrates structure toward core while
1183
+ maintaining coherence.
1184
+
1185
+ For typical NUL_scale < 1.0, contraction naturally moves EPI toward zero
1186
+ (the center), which is always safe regardless of whether EPI is positive
1187
+ or negative. Edge-awareness is only needed if scale could somehow push
1188
+ EPI beyond boundaries.
1189
+
1190
+ In practice, with NUL_scale = 0.85 < 1.0:
1191
+ - Positive EPI contracts toward zero: safe
1192
+ - Negative EPI contracts toward zero: safe
1193
+
1194
+ Edge-awareness is provided for completeness and future extensibility.
1195
+
1196
+ Examples
1197
+ --------
1198
+ >>> # Normal contraction (always safe with scale < 1.0)
1199
+ >>> _compute_nul_edge_aware_scale(0.5, 0.85, -1.0, 1e-12)
1200
+ 0.85
1201
+ >>> _compute_nul_edge_aware_scale(-0.5, 0.85, -1.0, 1e-12)
1202
+ 0.85
1203
+ """
1204
+ # With NUL_scale < 1.0, contraction moves toward zero (always safe)
1205
+ # No adaptation needed in typical case
1206
+ return scale
1207
+
1208
+
1209
+ def _op_scale(node: NodeProtocol, factor: float) -> None:
1210
+ """Scale νf with the provided factor.
1211
+
1212
+ Parameters
1213
+ ----------
1214
+ node : NodeProtocol
1215
+ Node whose νf is being updated.
1216
+ factor : float
1217
+ Multiplicative change applied to νf.
1218
+ """
1219
+ node.vf *= factor
1220
+
1221
+
1222
+ def _make_scale_op(glyph: Glyph) -> GlyphOperation:
1223
+ def _op(node: NodeProtocol, gf: GlyphFactors) -> None:
1224
+ key = "VAL_scale" if glyph is Glyph.VAL else "NUL_scale"
1225
+ default = _SCALE_FACTORS[glyph]
1226
+ factor = get_factor(gf, key, default)
1227
+
1228
+ # Always scale νf (existing behavior)
1229
+ _op_scale(node, factor)
1230
+
1231
+ # NUL canonical ΔNFR densification (implements structural pressure concentration)
1232
+ if glyph is Glyph.NUL:
1233
+ # Volume reduction: V' = V · scale_factor (where scale_factor < 1.0)
1234
+ # Density increase: ρ_ΔNFR = ΔNFR / V' = ΔNFR / (V · scale_factor)
1235
+ # Result: ΔNFR' = ΔNFR · densification_factor
1236
+ #
1237
+ # Physics: When volume contracts by factor λ < 1, structural pressure
1238
+ # concentrates by factor 1/λ > 1. For NUL_scale = 0.85, densification ≈ 1.176
1239
+ #
1240
+ # Default densification_factor from config (typically 1.3-1.5) provides
1241
+ # additional canonical amplification beyond geometric 1/λ to account for
1242
+ # nonlinear structural effects at smaller scales.
1243
+ densification_key = "NUL_densification_factor"
1244
+ densification_default = 1.35 # Canonical default: moderate amplification
1245
+ densification_factor = get_factor(gf, densification_key, densification_default)
1246
+
1247
+ # Apply densification to ΔNFR (use lowercase dnfr for NodeProtocol)
1248
+ current_dnfr = node.dnfr
1249
+ node.dnfr = current_dnfr * densification_factor
1250
+
1251
+ # Record densification telemetry for traceability
1252
+ telemetry = node.graph.setdefault("nul_densification_log", [])
1253
+ telemetry.append(
1254
+ {
1255
+ "dnfr_before": current_dnfr,
1256
+ "dnfr_after": float(node.dnfr),
1257
+ "densification_factor": densification_factor,
1258
+ "contraction_scale": factor,
1259
+ }
1260
+ )
1261
+
1262
+ # Edge-aware EPI scaling (new behavior) if enabled
1263
+ edge_aware_enabled = bool(
1264
+ node.graph.get(
1265
+ "EDGE_AWARE_ENABLED", DEFAULTS.get("EDGE_AWARE_ENABLED", True)
1266
+ )
1267
+ )
1268
+
1269
+ if edge_aware_enabled:
1270
+ epsilon = float(
1271
+ node.graph.get(
1272
+ "EDGE_AWARE_EPSILON", DEFAULTS.get("EDGE_AWARE_EPSILON", 1e-12)
1273
+ )
1274
+ )
1275
+ epi_min = float(node.graph.get("EPI_MIN", DEFAULTS.get("EPI_MIN", -1.0)))
1276
+ epi_max = float(node.graph.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0)))
1277
+
1278
+ epi_current = node.EPI
1279
+
1280
+ # Compute edge-aware scale factor
1281
+ if glyph is Glyph.VAL:
1282
+ scale_eff = _compute_val_edge_aware_scale(
1283
+ epi_current, factor, epi_max, epsilon
1284
+ )
1285
+ else: # Glyph.NUL
1286
+ scale_eff = _compute_nul_edge_aware_scale(
1287
+ epi_current, factor, epi_min, epsilon
1288
+ )
1289
+
1290
+ # Apply edge-aware EPI scaling with boundary check
1291
+ # Edge-aware already computed safe scale, but use unified function
1292
+ # for consistency (with apply_clip=True as safety net)
1293
+ new_epi = epi_current * scale_eff
1294
+ _set_epi_with_boundary_check(node, new_epi, apply_clip=True)
1295
+
1296
+ # Record telemetry if scale was adapted
1297
+ if abs(scale_eff - factor) > epsilon:
1298
+ telemetry = node.graph.setdefault("edge_aware_interventions", [])
1299
+ telemetry.append(
1300
+ {
1301
+ "glyph": glyph.name if hasattr(glyph, "name") else str(glyph),
1302
+ "epi_before": epi_current,
1303
+ "epi_after": float(
1304
+ node.EPI
1305
+ ), # Get actual value after boundary check
1306
+ "scale_requested": factor,
1307
+ "scale_effective": scale_eff,
1308
+ "adapted": True,
1309
+ }
1310
+ )
1311
+
1312
+ _op.__doc__ = """{} glyph scales νf and EPI with edge-aware adaptation.
1313
+
1314
+ VAL (expansion) increases νf and EPI, whereas NUL (contraction) decreases them.
1315
+ Edge-aware scaling adapts the scale factor near EPI boundaries to prevent
1316
+ overflow/underflow, maintaining structural coherence within [-1.0, 1.0].
1317
+
1318
+ When EDGE_AWARE_ENABLED is True (default), the effective scale is computed as:
1319
+ - VAL: scale_eff = min(VAL_scale, EPI_MAX / |EPI_current|)
1320
+ - NUL: scale_eff = min(NUL_scale, |EPI_MIN| / |EPI_current|) for negative EPI
1321
+
1322
+ This implements TNFR principle: "resonance to the edge" without breaking
1323
+ the structural envelope. Telemetry records adaptation events.
1324
+
1325
+ Parameters
1326
+ ----------
1327
+ node : NodeProtocol
1328
+ Node whose νf and EPI are updated.
1329
+ gf : GlyphFactors
1330
+ Provides the respective scale factor (``VAL_scale`` or
1331
+ ``NUL_scale``).
1332
+
1333
+ Examples
1334
+ --------
1335
+ >>> class MockNode:
1336
+ ... def __init__(self, vf, epi):
1337
+ ... self.vf = vf
1338
+ ... self.EPI = epi
1339
+ ... self.graph = {{"EDGE_AWARE_ENABLED": True, "EPI_MAX": 1.0}}
1340
+ >>> node = MockNode(1.0, 0.96)
1341
+ >>> op = _make_scale_op(Glyph.VAL)
1342
+ >>> op(node, {{"VAL_scale": 1.05}})
1343
+ >>> node.vf # νf scaled normally
1344
+ 1.05
1345
+ >>> node.EPI <= 1.0 # EPI kept within bounds
1346
+ True
1347
+ """.format(
1348
+ glyph.name
1349
+ )
1350
+ return _op
1351
+
1352
+
1353
+ def _op_THOL(node: NodeProtocol, gf: GlyphFactors) -> None: # THOL — Self-organization
1354
+ """Inject curvature from ``d2EPI`` into ΔNFR to trigger self-organization.
1355
+
1356
+ The glyph keeps EPI, νf, and phase fixed while increasing ΔNFR according to
1357
+ the second derivative of EPI, accelerating structural rearrangement.
1358
+
1359
+ Parameters
1360
+ ----------
1361
+ node : NodeProtocol
1362
+ Node contributing ``d2EPI`` to ΔNFR.
1363
+ gf : GlyphFactors
1364
+ Source of the ``THOL_accel`` multiplier.
1365
+
1366
+ Examples
1367
+ --------
1368
+ >>> class MockNode:
1369
+ ... def __init__(self, dnfr, curvature):
1370
+ ... self.dnfr = dnfr
1371
+ ... self.d2EPI = curvature
1372
+ >>> node = MockNode(0.1, 0.5)
1373
+ >>> _op_THOL(node, {"THOL_accel": 0.2})
1374
+ >>> node.dnfr
1375
+ 0.2
1376
+ """
1377
+ a = get_factor(gf, "THOL_accel", 0.10)
1378
+ node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
1379
+
1380
+
1381
+ def _op_ZHIR(node: NodeProtocol, gf: GlyphFactors) -> None: # ZHIR — Mutation
1382
+ """Apply canonical phase transformation θ → θ' based on structural dynamics.
1383
+
1384
+ ZHIR (Mutation) implements the canonical TNFR phase transformation that depends on
1385
+ the node's reorganization state (ΔNFR). Unlike a fixed rotation, the transformation
1386
+ magnitude and direction are determined by structural pressure, implementing the
1387
+ physics: θ → θ' when ΔEPI/Δt > ξ (AGENTS.md §11, TNFR.pdf §2.2.11).
1388
+
1389
+ **Canonical Behavior**:
1390
+ - Direction: Based on ΔNFR sign (positive → forward phase, negative → backward)
1391
+ - Magnitude: Proportional to theta_shift_factor and |ΔNFR|
1392
+ - Regime detection: Identifies quadrant crossings (π/2 boundaries)
1393
+ - Deterministic: Same seed produces same transformation
1394
+
1395
+ The transformation preserves structural identity (epi_kind) while shifting the
1396
+ operational regime, enabling adaptation without losing coherence.
1397
+
1398
+ Parameters
1399
+ ----------
1400
+ node : NodeProtocol
1401
+ Node whose phase is transformed based on its structural state.
1402
+ gf : GlyphFactors
1403
+ Supplies ``ZHIR_theta_shift_factor`` (default: 0.3) controlling transformation
1404
+ magnitude. Can override with explicit ``ZHIR_theta_shift`` for fixed rotation.
1405
+
1406
+ Examples
1407
+ --------
1408
+ >>> import math
1409
+ >>> class MockNode:
1410
+ ... def __init__(self, theta, dnfr):
1411
+ ... self.theta = theta
1412
+ ... self.dnfr = dnfr
1413
+ ... self.graph = {}
1414
+ >>> # Positive ΔNFR → forward phase shift
1415
+ >>> node = MockNode(0.0, 0.5)
1416
+ >>> _op_ZHIR(node, {"ZHIR_theta_shift_factor": 0.3})
1417
+ >>> 0.2 < node.theta < 0.3 # ~π/4 * 0.3 ≈ 0.24
1418
+ True
1419
+ >>> # Negative ΔNFR → backward phase shift
1420
+ >>> node2 = MockNode(math.pi, -0.5)
1421
+ >>> _op_ZHIR(node2, {"ZHIR_theta_shift_factor": 0.3})
1422
+ >>> 2.9 < node2.theta < 3.0 # π - 0.24 ≈ 2.90
1423
+ True
1424
+ >>> # Fixed shift overrides dynamic behavior
1425
+ >>> node3 = MockNode(0.0, 0.5)
1426
+ >>> _op_ZHIR(node3, {"ZHIR_theta_shift": math.pi / 2})
1427
+ >>> round(node3.theta, 2)
1428
+ 1.57
1429
+ """
1430
+ # Check for explicit fixed shift (backward compatibility)
1431
+ if "ZHIR_theta_shift" in gf:
1432
+ shift = get_factor(gf, "ZHIR_theta_shift", math.pi / 2)
1433
+ node.theta = node.theta + shift
1434
+ # Store telemetry for fixed shift mode
1435
+ storage = node._glyph_storage()
1436
+ storage["_zhir_theta_shift"] = shift
1437
+ storage["_zhir_fixed_mode"] = True
1438
+ return
1439
+
1440
+ # Canonical transformation: θ → θ' based on ΔNFR
1441
+ theta_before = node.theta
1442
+ dnfr = node.dnfr
1443
+
1444
+ # Transformation magnitude controlled by factor
1445
+ theta_shift_factor = get_factor(gf, "ZHIR_theta_shift_factor", 0.3)
1446
+
1447
+ # Direction based on ΔNFR sign (coherent with structural pressure)
1448
+ # Magnitude based on |ΔNFR| (stronger pressure → larger shift)
1449
+ # Base shift is π/4, scaled by factor and ΔNFR
1450
+ base_shift = math.pi / 4
1451
+ shift = theta_shift_factor * math.copysign(1.0, dnfr) * base_shift
1452
+
1453
+ # Apply transformation with phase wrapping [0, 2π)
1454
+ theta_new = (theta_before + shift) % (2 * math.pi)
1455
+ node.theta = theta_new
1456
+
1457
+ # Detect regime change (crossing quadrant boundaries)
1458
+ regime_before = int(theta_before // (math.pi / 2))
1459
+ regime_after = int(theta_new // (math.pi / 2))
1460
+ regime_changed = regime_before != regime_after
1461
+
1462
+ # Store telemetry for metrics collection
1463
+ storage = node._glyph_storage()
1464
+ storage["_zhir_theta_shift"] = shift
1465
+ storage["_zhir_theta_before"] = theta_before
1466
+ storage["_zhir_theta_after"] = theta_new
1467
+ storage["_zhir_regime_changed"] = regime_changed
1468
+ storage["_zhir_regime_before"] = regime_before
1469
+ storage["_zhir_regime_after"] = regime_after
1470
+ storage["_zhir_fixed_mode"] = False
1471
+
1472
+
1473
+ def _op_NAV(node: NodeProtocol, gf: GlyphFactors) -> None: # NAV — Transition
1474
+ """Rebalance ΔNFR towards νf while permitting jitter.
1475
+
1476
+ Transition pulls ΔNFR towards a νf-aligned target, optionally adding jitter
1477
+ to explore nearby states. EPI and phase remain untouched; νf may be used as
1478
+ a reference but is not directly changed.
1479
+
1480
+ Parameters
1481
+ ----------
1482
+ node : NodeProtocol
1483
+ Node whose ΔNFR is redirected.
1484
+ gf : GlyphFactors
1485
+ Supplies ``NAV_eta`` and ``NAV_jitter`` tuning parameters.
1486
+
1487
+ Examples
1488
+ --------
1489
+ >>> class MockNode:
1490
+ ... def __init__(self, dnfr, vf):
1491
+ ... self.dnfr = dnfr
1492
+ ... self.vf = vf
1493
+ ... self.graph = {"NAV_RANDOM": False}
1494
+ >>> node = MockNode(-0.6, 0.4)
1495
+ >>> _op_NAV(node, {"NAV_eta": 0.5, "NAV_jitter": 0.0})
1496
+ >>> round(node.dnfr, 2)
1497
+ -0.1
1498
+ """
1499
+ dnfr = node.dnfr
1500
+ vf = node.vf
1501
+ eta = get_factor(gf, "NAV_eta", 0.5)
1502
+ strict = bool(node.graph.get("NAV_STRICT", False))
1503
+ if strict:
1504
+ base = vf
1505
+ else:
1506
+ sign = 1.0 if dnfr >= 0 else -1.0
1507
+ target = sign * vf
1508
+ base = (1.0 - eta) * dnfr + eta * target
1509
+ j = get_factor(gf, "NAV_jitter", 0.05)
1510
+ if bool(node.graph.get("NAV_RANDOM", True)):
1511
+ jitter = random_jitter(node, j)
1512
+ else:
1513
+ jitter = j * (1 if base >= 0 else -1)
1514
+ node.dnfr = base + jitter
1515
+
1516
+
1517
+ def _op_REMESH(
1518
+ node: NodeProtocol, gf: GlyphFactors | None = None
1519
+ ) -> None: # REMESH — advisory
1520
+ """Record an advisory requesting network-scale remeshing.
1521
+
1522
+ REMESH does not change node-level EPI, νf, ΔNFR, or phase. Instead it
1523
+ annotates the glyph history so orchestrators can trigger global remesh
1524
+ procedures once the stability conditions are met.
1525
+
1526
+ Parameters
1527
+ ----------
1528
+ node : NodeProtocol
1529
+ Node whose history records the advisory.
1530
+ gf : GlyphFactors, optional
1531
+ Unused but accepted for API symmetry.
1532
+
1533
+ Examples
1534
+ --------
1535
+ >>> class MockNode:
1536
+ ... def __init__(self):
1537
+ ... self.graph = {}
1538
+ >>> node = MockNode()
1539
+ >>> _op_REMESH(node)
1540
+ >>> "_remesh_warn_step" in node.graph
1541
+ True
1542
+ """
1543
+ step_idx = glyph_history.current_step_idx(node)
1544
+ last_warn = node.graph.get("_remesh_warn_step", None)
1545
+ if last_warn != step_idx:
1546
+ msg = (
1547
+ "REMESH operates at network scale. Use apply_remesh_if_globally_"
1548
+ "stable(G) or apply_network_remesh(G)."
1549
+ )
1550
+ hist = glyph_history.ensure_history(node)
1551
+ glyph_history.append_metric(
1552
+ hist,
1553
+ "events",
1554
+ ("warn", {"step": step_idx, "node": None, "msg": msg}),
1555
+ )
1556
+ node.graph["_remesh_warn_step"] = step_idx
1557
+ return
1558
+
1559
+
1560
+ # -------------------------
1561
+ # Dispatcher
1562
+ # -------------------------
1563
+
1564
+ GLYPH_OPERATIONS: dict[Glyph, GlyphOperation] = {
1565
+ Glyph.AL: _op_AL,
1566
+ Glyph.EN: _op_EN,
1567
+ Glyph.IL: _op_IL,
1568
+ Glyph.OZ: _op_OZ,
1569
+ Glyph.UM: _op_UM,
1570
+ Glyph.RA: _op_RA,
1571
+ Glyph.SHA: _op_SHA,
1572
+ Glyph.VAL: _make_scale_op(Glyph.VAL),
1573
+ Glyph.NUL: _make_scale_op(Glyph.NUL),
1574
+ Glyph.THOL: _op_THOL,
1575
+ Glyph.ZHIR: _op_ZHIR,
1576
+ Glyph.NAV: _op_NAV,
1577
+ Glyph.REMESH: _op_REMESH,
1578
+ }
1579
+
1580
+
1581
+ def apply_glyph_obj(
1582
+ node: NodeProtocol, glyph: Glyph | str, *, window: int | None = None
1583
+ ) -> None:
1584
+ """Apply ``glyph`` to an object satisfying :class:`NodeProtocol`."""
1585
+
1586
+ from .grammar import function_name_to_glyph
1587
+ from ..validation.input_validation import ValidationError, validate_glyph
1588
+
1589
+ # Validate glyph parameter
1590
+ try:
1591
+ if not isinstance(glyph, Glyph):
1592
+ validated_glyph = validate_glyph(glyph)
1593
+ glyph = (
1594
+ validated_glyph.value
1595
+ if isinstance(validated_glyph, Glyph)
1596
+ else str(glyph)
1597
+ )
1598
+ else:
1599
+ glyph = glyph.value
1600
+ except ValidationError as e:
1601
+ step_idx = glyph_history.current_step_idx(node)
1602
+ hist = glyph_history.ensure_history(node)
1603
+ glyph_history.append_metric(
1604
+ hist,
1605
+ "events",
1606
+ (
1607
+ "warn",
1608
+ {
1609
+ "step": step_idx,
1610
+ "node": getattr(node, "n", None),
1611
+ "msg": f"invalid glyph: {e}",
1612
+ },
1613
+ ),
1614
+ )
1615
+ raise ValueError(f"invalid glyph: {e}") from e
1616
+
1617
+ # Try direct glyph code first
1618
+ try:
1619
+ g = Glyph(str(glyph))
1620
+ except ValueError:
1621
+ # Try structural function name mapping
1622
+ g = function_name_to_glyph(glyph)
1623
+ if g is None:
1624
+ step_idx = glyph_history.current_step_idx(node)
1625
+ hist = glyph_history.ensure_history(node)
1626
+ glyph_history.append_metric(
1627
+ hist,
1628
+ "events",
1629
+ (
1630
+ "warn",
1631
+ {
1632
+ "step": step_idx,
1633
+ "node": getattr(node, "n", None),
1634
+ "msg": f"unknown glyph: {glyph}",
1635
+ },
1636
+ ),
1637
+ )
1638
+ raise ValueError(f"unknown glyph: {glyph}")
1639
+
1640
+ op = GLYPH_OPERATIONS.get(g)
1641
+ if op is None:
1642
+ raise ValueError(f"glyph has no registered operator: {g}")
1643
+ if window is None:
1644
+ window = int(get_param(node, "GLYPH_HYSTERESIS_WINDOW"))
1645
+ gf = get_glyph_factors(node)
1646
+ op(node, gf)
1647
+ glyph_history.push_glyph(node._glyph_storage(), g.value, window)
1648
+ node.epi_kind = g.value
1649
+
1650
+
1651
+ def apply_glyph(
1652
+ G: TNFRGraph, n: NodeId, glyph: Glyph | str, *, window: int | None = None
1653
+ ) -> None:
1654
+ """Adapter to operate on ``networkx`` graphs."""
1655
+ from ..validation.input_validation import (
1656
+ ValidationError,
1657
+ validate_node_id,
1658
+ validate_tnfr_graph,
1659
+ )
1660
+
1661
+ # Validate graph and node parameters
1662
+ try:
1663
+ validate_tnfr_graph(G)
1664
+ validate_node_id(n)
1665
+ except ValidationError as e:
1666
+ raise ValueError(f"Invalid parameters for apply_glyph: {e}") from e
1667
+
1668
+ NodeNX = get_nodenx()
1669
+ if NodeNX is None:
1670
+ raise ImportError("NodeNX is unavailable")
1671
+ node = NodeNX(G, n)
1672
+ apply_glyph_obj(node, glyph, window=window)