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,39 +1,84 @@
1
- """Network operators."""
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
+ """
2
8
 
3
9
  from __future__ import annotations
4
- from typing import Any, TYPE_CHECKING, Callable
5
- import math
10
+
6
11
  import heapq
12
+ import math
13
+ from collections.abc import Callable, Iterator
7
14
  from itertools import islice
8
- from statistics import fmean, StatisticsError
15
+ from statistics import StatisticsError, fmean
16
+ from typing import TYPE_CHECKING, Any
9
17
 
10
- from ..alias import get_attr
11
- from ..constants import DEFAULTS, get_aliases, get_param
18
+ from tnfr import glyph_history
12
19
 
13
- from ..helpers.numeric import angle_diff
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
14
24
  from ..metrics.trig import neighbor_phase_mean
15
- from ..import_utils import get_nodonx
16
25
  from ..rng import make_rng
17
- from tnfr import glyph_history
18
- from ..types import Glyph
19
-
26
+ from ..types import EPIValue, Glyph, NodeId, TNFRGraph
27
+ from ..utils import get_nodenx
28
+ from . import definitions as _definitions
20
29
  from .jitter import (
21
30
  JitterCache,
22
31
  JitterCacheManager,
23
32
  get_jitter_manager,
24
- reset_jitter_manager,
25
33
  random_jitter,
34
+ reset_jitter_manager,
26
35
  )
36
+ from .registry import OPERATORS, discover_operators, get_operator_class
27
37
  from .remesh import (
28
38
  apply_network_remesh,
29
- apply_topological_remesh,
30
39
  apply_remesh_if_globally_stable,
40
+ apply_topological_remesh,
31
41
  )
32
42
 
33
- if TYPE_CHECKING: # pragma: no cover - type checking only
34
- from ..node import NodoProtocol
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)
35
61
 
36
- ALIAS_EPI = get_aliases("EPI")
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
+ )
37
82
 
38
83
  __all__ = [
39
84
  "JitterCache",
@@ -46,29 +91,153 @@ __all__ = [
46
91
  "GLYPH_OPERATIONS",
47
92
  "apply_glyph_obj",
48
93
  "apply_glyph",
94
+ "apply_glyph_with_grammar",
49
95
  "apply_network_remesh",
50
96
  "apply_topological_remesh",
51
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",
52
112
  ]
53
113
 
54
-
55
- def get_glyph_factors(node: NodoProtocol) -> dict[str, Any]:
56
- """Return glyph factors for ``node`` with defaults."""
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
+ """
57
150
  return node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"].copy())
58
151
 
59
152
 
60
- def get_factor(gf: dict[str, Any], key: str, default: float) -> float:
61
- """Return ``gf[key]`` as ``float`` with ``default`` fallback."""
62
- return float(gf.get(key, default))
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
63
197
 
64
198
 
65
199
  # -------------------------
66
- # Glyphs (operadores locales)
200
+ # Glyphs (local operators)
67
201
  # -------------------------
68
202
 
69
203
 
70
- def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
71
- """Return neighbour list and their mean ``EPI`` without mutating ``node``."""
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
+ """
72
241
 
73
242
  epi = node.EPI
74
243
  neigh = list(node.neighbors())
@@ -98,12 +267,11 @@ def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
98
267
  return [], epi
99
268
  epi_bar = total / count if count else float(epi)
100
269
  if needs_conversion:
101
- NodoNX = get_nodonx()
102
- if NodoNX is None:
103
- raise ImportError("NodoNX is unavailable")
270
+ NodeNX = get_nodenx()
271
+ if NodeNX is None:
272
+ raise ImportError("NodeNX is unavailable")
104
273
  neigh = [
105
- v if hasattr(v, "EPI") else NodoNX.from_graph(node.G, v)
106
- for v in neigh
274
+ v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v) for v in neigh
107
275
  ]
108
276
  else:
109
277
  try:
@@ -115,9 +283,37 @@ def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
115
283
 
116
284
 
117
285
  def _determine_dominant(
118
- neigh: list[NodoProtocol], default_kind: str
286
+ neigh: list[NodeProtocol], default_kind: str
119
287
  ) -> tuple[str, float]:
120
- """Return dominant ``epi_kind`` among ``neigh`` and its absolute ``EPI``."""
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
+ """
121
317
  best_kind: str | None = None
122
318
  best_abs = 0.0
123
319
  for v in neigh:
@@ -131,13 +327,48 @@ def _determine_dominant(
131
327
 
132
328
 
133
329
  def _mix_epi_with_neighbors(
134
- node: NodoProtocol, mix: float, default_glyph: Glyph | str
330
+ node: NodeProtocol, mix: float, default_glyph: Glyph | str
135
331
  ) -> tuple[float, str]:
136
- """Mix ``EPI`` of ``node`` with the mean of its neighbours."""
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
+ """
137
370
  default_kind = (
138
- default_glyph.value
139
- if isinstance(default_glyph, Glyph)
140
- else str(default_glyph)
371
+ default_glyph.value if isinstance(default_glyph, Glyph) else str(default_glyph)
141
372
  )
142
373
  epi = node.EPI
143
374
  neigh, epi_bar = get_neighbor_epi(node)
@@ -148,7 +379,7 @@ def _mix_epi_with_neighbors(
148
379
 
149
380
  dominant, best_abs = _determine_dominant(neigh, default_kind)
150
381
  new_epi = (1 - mix) * epi + mix * epi_bar
151
- node.EPI = new_epi
382
+ _set_epi_with_boundary_check(node, new_epi)
152
383
  final = dominant if best_abs > abs(new_epi) else node.epi_kind
153
384
  if not final:
154
385
  final = default_kind
@@ -156,22 +387,122 @@ def _mix_epi_with_neighbors(
156
387
  return epi_bar, final
157
388
 
158
389
 
159
- def _op_AL(node: NodoProtocol, gf: dict[str, Any]) -> None: # AL — Emisión
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
+ """
160
415
  f = get_factor(gf, "AL_boost", 0.05)
161
- node.EPI = node.EPI + f
162
-
163
-
164
- def _op_EN(node: NodoProtocol, gf: dict[str, Any]) -> None: # EN — Recepción
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
+ """
165
449
  mix = get_factor(gf, "EN_mix", 0.25)
166
450
  _mix_epi_with_neighbors(node, mix, Glyph.EN)
167
451
 
168
452
 
169
- def _op_IL(node: NodoProtocol, gf: dict[str, Any]) -> None: # IL — Coherencia
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
+ """
170
477
  factor = get_factor(gf, "IL_dnfr_factor", 0.7)
171
478
  node.dnfr = factor * getattr(node, "dnfr", 0.0)
172
479
 
173
480
 
174
- def _op_OZ(node: NodoProtocol, gf: dict[str, Any]) -> None: # OZ — Disonancia
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
+ """
175
506
  factor = get_factor(gf, "OZ_dnfr_factor", 1.3)
176
507
  dnfr = getattr(node, "dnfr", 0.0)
177
508
  if bool(node.graph.get("OZ_NOISE_MODE", False)):
@@ -184,31 +515,29 @@ def _op_OZ(node: NodoProtocol, gf: dict[str, Any]) -> None: # OZ — Disonancia
184
515
  node.dnfr = factor * dnfr if abs(dnfr) > 1e-9 else 0.1
185
516
 
186
517
 
187
- def _um_candidate_iter(node: NodoProtocol):
518
+ def _um_candidate_iter(node: NodeProtocol) -> Iterator[NodeProtocol]:
188
519
  sample_ids = node.graph.get("_node_sample")
189
520
  if sample_ids is not None and hasattr(node, "G"):
190
- NodoNX = get_nodonx()
191
- if NodoNX is None:
192
- raise ImportError("NodoNX is unavailable")
193
- base = (NodoNX.from_graph(node.G, j) for j in sample_ids)
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)
194
525
  else:
195
526
  base = node.all_nodes()
196
527
  for j in base:
197
- same = (j is node) or (
198
- getattr(node, "n", None) == getattr(j, "n", None)
199
- )
528
+ same = (j is node) or (getattr(node, "n", None) == getattr(j, "n", None))
200
529
  if same or node.has_edge(j):
201
530
  continue
202
531
  yield j
203
532
 
204
533
 
205
534
  def _um_select_candidates(
206
- node: NodoProtocol,
207
- candidates,
535
+ node: NodeProtocol,
536
+ candidates: Iterator[NodeProtocol],
208
537
  limit: int,
209
538
  mode: str,
210
539
  th: float,
211
- ):
540
+ ) -> list[NodeProtocol]:
212
541
  """Select a subset of ``candidates`` for UM coupling."""
213
542
  rng = make_rng(int(node.graph.get("RANDOM_SEED", 0)), node.offset(), node.G)
214
543
 
@@ -232,14 +561,202 @@ def _um_select_candidates(
232
561
  return reservoir
233
562
 
234
563
 
235
- def _op_UM(node: NodoProtocol, gf: dict[str, Any]) -> None: # UM — Coupling
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
+ """
236
670
  k = get_factor(gf, "UM_theta_push", 0.25)
237
- th = node.theta
238
- thL = neighbor_phase_mean(node)
239
- d = angle_diff(thL, th)
240
- node.theta = th + k * d
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]
241
691
 
242
- if bool(node.graph.get("UM_FUNCTIONAL_LINKS", False)):
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)):
243
760
  thr = float(
244
761
  node.graph.get(
245
762
  "UM_COMPAT_THRESHOLD",
@@ -252,69 +769,733 @@ def _op_UM(node: NodoProtocol, gf: dict[str, Any]) -> None: # UM — Coupling
252
769
  limit = int(node.graph.get("UM_CANDIDATE_COUNT", 0))
253
770
  mode = str(node.graph.get("UM_CANDIDATE_MODE", "sample")).lower()
254
771
  candidates = _um_select_candidates(
255
- node, _um_candidate_iter(node), limit, mode, th
772
+ node, _um_candidate_iter(node), limit, mode, th_i
256
773
  )
257
774
 
258
775
  for j in candidates:
259
- th_j = j.theta
260
- dphi = abs(angle_diff(th_j, th)) / math.pi
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
+
261
781
  epi_j = j.EPI
262
782
  si_j = j.Si
263
- epi_sim = 1.0 - abs(epi_i - epi_j) / (
264
- abs(epi_i) + abs(epi_j) + 1e-9
265
- )
783
+ epi_sim = 1.0 - abs(epi_i - epi_j) / (abs(epi_i) + abs(epi_j) + 1e-9)
266
784
  si_sim = 1.0 - abs(si_i - si_j)
267
- compat = (1 - dphi) * 0.5 + 0.25 * epi_sim + 0.25 * si_sim
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
268
787
  if compat >= thr:
269
788
  node.add_edge(j, compat)
270
789
 
271
790
 
272
- def _op_RA(node: NodoProtocol, gf: dict[str, Any]) -> None: # RA — Resonancia
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
273
858
  diff = get_factor(gf, "RA_epi_diff", 0.15)
274
- _mix_epi_with_neighbors(node, diff, Glyph.RA)
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
275
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
276
882
 
277
- def _op_SHA(node: NodoProtocol, gf: dict[str, Any]) -> None: # SHA — Silencio
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
+ """
278
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)
279
1024
  node.vf = factor * node.vf
280
1025
 
281
1026
 
282
- factor_val = 1.15
1027
+ factor_val = 1.05 # Conservative scale prevents EPI overflow near boundaries
283
1028
  factor_nul = 0.85
284
1029
  _SCALE_FACTORS = {Glyph.VAL: factor_val, Glyph.NUL: factor_nul}
285
1030
 
286
1031
 
287
- def _op_scale(node: NodoProtocol, factor: float) -> None:
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
+ """
288
1219
  node.vf *= factor
289
1220
 
290
1221
 
291
- def _make_scale_op(glyph: Glyph):
292
- def _op(node: NodoProtocol, gf: dict[str, Any]) -> None:
1222
+ def _make_scale_op(glyph: Glyph) -> GlyphOperation:
1223
+ def _op(node: NodeProtocol, gf: GlyphFactors) -> None:
293
1224
  key = "VAL_scale" if glyph is Glyph.VAL else "NUL_scale"
294
1225
  default = _SCALE_FACTORS[glyph]
295
1226
  factor = get_factor(gf, key, default)
1227
+
1228
+ # Always scale νf (existing behavior)
296
1229
  _op_scale(node, factor)
297
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
+ )
298
1350
  return _op
299
1351
 
300
1352
 
301
- def _op_THOL(
302
- node: NodoProtocol, gf: dict[str, Any]
303
- ) -> None: # THOL — Autoorganización
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
+ """
304
1377
  a = get_factor(gf, "THOL_accel", 0.10)
305
1378
  node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
306
1379
 
307
1380
 
308
- def _op_ZHIR(
309
- node: NodoProtocol, gf: dict[str, Any]
310
- ) -> None: # ZHIR — Mutación
311
- shift = get_factor(gf, "ZHIR_theta_shift", math.pi / 2)
312
- node.theta = node.theta + shift
313
-
314
-
315
- def _op_NAV(
316
- node: NodoProtocol, gf: dict[str, Any]
317
- ) -> None: # NAV Transición
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
+ """
318
1499
  dnfr = node.dnfr
319
1500
  vf = node.vf
320
1501
  eta = get_factor(gf, "NAV_eta", 0.5)
@@ -334,14 +1515,37 @@ def _op_NAV(
334
1515
 
335
1516
 
336
1517
  def _op_REMESH(
337
- node: NodoProtocol, gf: dict[str, Any] | None = None
338
- ) -> None: # REMESH — aviso
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
+ """
339
1543
  step_idx = glyph_history.current_step_idx(node)
340
1544
  last_warn = node.graph.get("_remesh_warn_step", None)
341
1545
  if last_warn != step_idx:
342
1546
  msg = (
343
- "REMESH es a escala de red. Usa apply_remesh_if_globally_"
344
- "stable(G) o apply_network_remesh(G)."
1547
+ "REMESH operates at network scale. Use apply_remesh_if_globally_"
1548
+ "stable(G) or apply_network_remesh(G)."
345
1549
  )
346
1550
  hist = glyph_history.ensure_history(node)
347
1551
  glyph_history.append_metric(
@@ -357,7 +1561,7 @@ def _op_REMESH(
357
1561
  # Dispatcher
358
1562
  # -------------------------
359
1563
 
360
- GLYPH_OPERATIONS: dict[Glyph, Callable[["NodoProtocol", dict[str, Any]], None]] = {
1564
+ GLYPH_OPERATIONS: dict[Glyph, GlyphOperation] = {
361
1565
  Glyph.AL: _op_AL,
362
1566
  Glyph.EN: _op_EN,
363
1567
  Glyph.IL: _op_IL,
@@ -375,13 +1579,25 @@ GLYPH_OPERATIONS: dict[Glyph, Callable[["NodoProtocol", dict[str, Any]], None]]
375
1579
 
376
1580
 
377
1581
  def apply_glyph_obj(
378
- node: NodoProtocol, glyph: Glyph | str, *, window: int | None = None
1582
+ node: NodeProtocol, glyph: Glyph | str, *, window: int | None = None
379
1583
  ) -> None:
380
- """Apply ``glyph`` to an object satisfying :class:`NodoProtocol`."""
1584
+ """Apply ``glyph`` to an object satisfying :class:`NodeProtocol`."""
381
1585
 
1586
+ from .grammar import function_name_to_glyph
1587
+ from ..validation.input_validation import ValidationError, validate_glyph
1588
+
1589
+ # Validate glyph parameter
382
1590
  try:
383
- g = glyph if isinstance(glyph, Glyph) else Glyph(str(glyph))
384
- except ValueError:
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:
385
1601
  step_idx = glyph_history.current_step_idx(node)
386
1602
  hist = glyph_history.ensure_history(node)
387
1603
  glyph_history.append_metric(
@@ -392,15 +1608,38 @@ def apply_glyph_obj(
392
1608
  {
393
1609
  "step": step_idx,
394
1610
  "node": getattr(node, "n", None),
395
- "msg": f"glyph desconocido: {glyph}",
1611
+ "msg": f"invalid glyph: {e}",
396
1612
  },
397
1613
  ),
398
1614
  )
399
- raise ValueError(f"glyph desconocido: {glyph}")
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}")
400
1639
 
401
1640
  op = GLYPH_OPERATIONS.get(g)
402
1641
  if op is None:
403
- raise ValueError(f"glyph sin operador: {g}")
1642
+ raise ValueError(f"glyph has no registered operator: {g}")
404
1643
  if window is None:
405
1644
  window = int(get_param(node, "GLYPH_HYSTERESIS_WINDOW"))
406
1645
  gf = get_glyph_factors(node)
@@ -410,11 +1649,24 @@ def apply_glyph_obj(
410
1649
 
411
1650
 
412
1651
  def apply_glyph(
413
- G, n, glyph: Glyph | str, *, window: int | None = None
1652
+ G: TNFRGraph, n: NodeId, glyph: Glyph | str, *, window: int | None = None
414
1653
  ) -> None:
415
1654
  """Adapter to operate on ``networkx`` graphs."""
416
- NodoNX = get_nodonx()
417
- if NodoNX is None:
418
- raise ImportError("NodoNX is unavailable")
419
- node = NodoNX(G, n)
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)
420
1672
  apply_glyph_obj(node, glyph, window=window)