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
@@ -0,0 +1,2138 @@
1
+ """Operator-specific metrics collection for TNFR structural operators.
2
+
3
+ Each operator produces characteristic metrics that reflect its structural
4
+ effects on nodes. This module provides metric collectors for telemetry
5
+ and analysis.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ from ..types import NodeId, TNFRGraph
14
+
15
+ from ..alias import get_attr, get_attr_str
16
+ from ..constants.aliases import (
17
+ ALIAS_EPI,
18
+ ALIAS_VF,
19
+ ALIAS_DNFR,
20
+ ALIAS_THETA,
21
+ ALIAS_D2EPI,
22
+ ALIAS_EMISSION_TIMESTAMP,
23
+ )
24
+
25
+ __all__ = [
26
+ "emission_metrics",
27
+ "reception_metrics",
28
+ "coherence_metrics",
29
+ "dissonance_metrics",
30
+ "coupling_metrics",
31
+ "resonance_metrics",
32
+ "silence_metrics",
33
+ "expansion_metrics",
34
+ "contraction_metrics",
35
+ "self_organization_metrics",
36
+ "mutation_metrics",
37
+ "transition_metrics",
38
+ "recursivity_metrics",
39
+ ]
40
+
41
+
42
+ def _get_node_attr(
43
+ G: TNFRGraph, node: NodeId, aliases: tuple[str, ...], default: float = 0.0
44
+ ) -> float:
45
+ """Get node attribute using alias fallback."""
46
+ return float(get_attr(G.nodes[node], aliases, default))
47
+
48
+
49
+ def emission_metrics(
50
+ G: TNFRGraph, node: NodeId, epi_before: float, vf_before: float
51
+ ) -> dict[str, Any]:
52
+ """AL - Emission metrics with structural fidelity indicators.
53
+
54
+ Collects emission-specific metrics that reflect canonical AL effects:
55
+ - EPI: Increments (form activation)
56
+ - vf: Activates/increases (Hz_str)
57
+ - DELTA_NFR: Initializes positive reorganization
58
+ - theta: Influences phase alignment
59
+
60
+ Parameters
61
+ ----------
62
+ G : TNFRGraph
63
+ Graph containing the node
64
+ node : NodeId
65
+ Node to collect metrics from
66
+ epi_before : float
67
+ EPI value before operator application
68
+ vf_before : float
69
+ νf value before operator application
70
+
71
+ Returns
72
+ -------
73
+ dict
74
+ Emission-specific metrics including:
75
+ - Core deltas (delta_epi, delta_vf, dnfr_initialized, theta_current)
76
+ - AL-specific quality indicators:
77
+ - emission_quality: "valid" if both EPI and νf increased, else "weak"
78
+ - activation_from_latency: True if node was latent (EPI < 0.3)
79
+ - form_emergence_magnitude: Absolute EPI increment
80
+ - frequency_activation: True if νf increased
81
+ - reorganization_positive: True if ΔNFR > 0
82
+ - Traceability markers:
83
+ - emission_timestamp: ISO UTC timestamp of activation
84
+ - irreversibility_marker: True if node was activated
85
+ """
86
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
87
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
88
+ dnfr = _get_node_attr(G, node, ALIAS_DNFR)
89
+ theta = _get_node_attr(G, node, ALIAS_THETA)
90
+
91
+ # Fetch emission timestamp using alias system
92
+ emission_timestamp = None
93
+ try:
94
+ emission_timestamp = get_attr_str(
95
+ G.nodes[node], ALIAS_EMISSION_TIMESTAMP, default=None
96
+ )
97
+ except (AttributeError, KeyError, ImportError):
98
+ # Fallback if alias system unavailable or node lacks timestamp
99
+ emission_timestamp = G.nodes[node].get("emission_timestamp")
100
+
101
+ # Compute deltas
102
+ delta_epi = epi_after - epi_before
103
+ delta_vf = vf_after - vf_before
104
+
105
+ # AL-specific quality indicators
106
+ emission_quality = "valid" if (delta_epi > 0 and delta_vf > 0) else "weak"
107
+ activation_from_latency = epi_before < 0.3 # Latency threshold
108
+ frequency_activation = delta_vf > 0
109
+ reorganization_positive = dnfr > 0
110
+
111
+ # Irreversibility marker
112
+ irreversibility_marker = G.nodes[node].get("_emission_activated", False)
113
+
114
+ return {
115
+ "operator": "Emission",
116
+ "glyph": "AL",
117
+ # Core metrics (existing)
118
+ "delta_epi": delta_epi,
119
+ "delta_vf": delta_vf,
120
+ "dnfr_initialized": dnfr,
121
+ "theta_current": theta,
122
+ # Legacy compatibility
123
+ "epi_final": epi_after,
124
+ "vf_final": vf_after,
125
+ "dnfr_final": dnfr,
126
+ "activation_strength": delta_epi,
127
+ "is_activated": epi_after > 0.5,
128
+ # AL-specific (NEW)
129
+ "emission_quality": emission_quality,
130
+ "activation_from_latency": activation_from_latency,
131
+ "form_emergence_magnitude": delta_epi,
132
+ "frequency_activation": frequency_activation,
133
+ "reorganization_positive": reorganization_positive,
134
+ # Traceability (NEW)
135
+ "emission_timestamp": emission_timestamp,
136
+ "irreversibility_marker": irreversibility_marker,
137
+ }
138
+
139
+
140
+ def reception_metrics(G: TNFRGraph, node: NodeId, epi_before: float) -> dict[str, Any]:
141
+ """EN - Reception metrics: EPI integration, source tracking, integration efficiency.
142
+
143
+ Extended metrics for Reception (EN) operator that track emission sources,
144
+ phase compatibility, and integration efficiency as specified in TNFR.pdf
145
+ §2.2.1 (EN - Structural reception).
146
+
147
+ Parameters
148
+ ----------
149
+ G : TNFRGraph
150
+ Graph containing the node
151
+ node : NodeId
152
+ Node to collect metrics from
153
+ epi_before : float
154
+ EPI value before operator application
155
+
156
+ Returns
157
+ -------
158
+ dict
159
+ Reception-specific metrics including:
160
+ - Core metrics: delta_epi, epi_final, dnfr_after
161
+ - Legacy metrics: neighbor_count, neighbor_epi_mean, integration_strength
162
+ - EN-specific (NEW):
163
+ - num_sources: Number of detected emission sources
164
+ - integration_efficiency: Ratio of integrated to available coherence
165
+ - most_compatible_source: Most phase-compatible source node
166
+ - phase_compatibility_avg: Average phase compatibility with sources
167
+ - coherence_received: Total coherence integrated (delta_epi)
168
+ - stabilization_effective: Whether ΔNFR reduced below threshold
169
+ """
170
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
171
+ dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
172
+
173
+ # Legacy neighbor metrics (backward compatibility)
174
+ neighbors = list(G.neighbors(node))
175
+ neighbor_count = len(neighbors)
176
+
177
+ # Calculate mean neighbor EPI
178
+ neighbor_epi_sum = 0.0
179
+ for n in neighbors:
180
+ neighbor_epi_sum += _get_node_attr(G, n, ALIAS_EPI)
181
+ neighbor_epi_mean = neighbor_epi_sum / neighbor_count if neighbor_count > 0 else 0.0
182
+
183
+ # Compute delta EPI (coherence received)
184
+ delta_epi = epi_after - epi_before
185
+
186
+ # EN-specific: Source tracking and integration efficiency
187
+ sources = G.nodes[node].get("_reception_sources", [])
188
+ num_sources = len(sources)
189
+
190
+ # Calculate total available coherence from sources
191
+ total_available_coherence = sum(strength for _, _, strength in sources)
192
+
193
+ # Integration efficiency: ratio of integrated to available coherence
194
+ # Only meaningful if coherence was actually available
195
+ integration_efficiency = (
196
+ delta_epi / total_available_coherence if total_available_coherence > 0 else 0.0
197
+ )
198
+
199
+ # Most compatible source (first in sorted list)
200
+ most_compatible_source = sources[0][0] if sources else None
201
+
202
+ # Average phase compatibility across all sources
203
+ phase_compatibility_avg = (
204
+ sum(compat for _, compat, _ in sources) / num_sources
205
+ if num_sources > 0
206
+ else 0.0
207
+ )
208
+
209
+ # Stabilization effectiveness (ΔNFR reduced?)
210
+ stabilization_effective = dnfr_after < 0.1
211
+
212
+ return {
213
+ "operator": "Reception",
214
+ "glyph": "EN",
215
+ # Core metrics
216
+ "delta_epi": delta_epi,
217
+ "epi_final": epi_after,
218
+ "dnfr_after": dnfr_after,
219
+ # Legacy metrics (backward compatibility)
220
+ "neighbor_count": neighbor_count,
221
+ "neighbor_epi_mean": neighbor_epi_mean,
222
+ "integration_strength": abs(delta_epi),
223
+ # EN-specific (NEW)
224
+ "num_sources": num_sources,
225
+ "integration_efficiency": integration_efficiency,
226
+ "most_compatible_source": most_compatible_source,
227
+ "phase_compatibility_avg": phase_compatibility_avg,
228
+ "coherence_received": delta_epi,
229
+ "stabilization_effective": stabilization_effective,
230
+ }
231
+
232
+
233
+ def coherence_metrics(G: TNFRGraph, node: NodeId, dnfr_before: float) -> dict[str, Any]:
234
+ """IL - Coherence metrics: ΔC(t), stability gain, ΔNFR reduction, phase alignment.
235
+
236
+ Extended to include ΔNFR reduction percentage, C(t) coherence metrics,
237
+ phase alignment quality, and telemetry from the explicit reduction mechanism
238
+ implemented in the Coherence operator.
239
+
240
+ Parameters
241
+ ----------
242
+ G : TNFRGraph
243
+ Graph containing the node
244
+ node : NodeId
245
+ Node to collect metrics from
246
+ dnfr_before : float
247
+ ΔNFR value before operator application
248
+
249
+ Returns
250
+ -------
251
+ dict
252
+ Coherence-specific metrics including:
253
+ - dnfr_before: ΔNFR value before operator
254
+ - dnfr_after: ΔNFR value after operator
255
+ - dnfr_reduction: Absolute reduction (before - after)
256
+ - dnfr_reduction_pct: Percentage reduction relative to before
257
+ - stability_gain: Improvement in stability (reduction of |ΔNFR|)
258
+ - is_stabilized: Whether node reached stable state (|ΔNFR| < 0.1)
259
+ - C_global: Global network coherence (current)
260
+ - C_local: Local neighborhood coherence (current)
261
+ - phase_alignment: Local phase alignment quality (Kuramoto order parameter)
262
+ - phase_coherence_quality: Alias for phase_alignment (for clarity)
263
+ - stabilization_quality: Combined metric (C_local * (1.0 - dnfr_after))
264
+ - epi_final, vf_final: Final structural state
265
+ """
266
+ # Import here to avoid circular import
267
+ from ..metrics.coherence import compute_global_coherence, compute_local_coherence
268
+ from ..metrics.phase_coherence import compute_phase_alignment
269
+
270
+ dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
271
+ epi = _get_node_attr(G, node, ALIAS_EPI)
272
+ vf = _get_node_attr(G, node, ALIAS_VF)
273
+
274
+ # Compute reduction metrics
275
+ dnfr_reduction = dnfr_before - dnfr_after
276
+ dnfr_reduction_pct = (
277
+ (dnfr_reduction / dnfr_before * 100.0) if dnfr_before > 0 else 0.0
278
+ )
279
+
280
+ # Compute coherence metrics
281
+ C_global = compute_global_coherence(G)
282
+ C_local = compute_local_coherence(G, node)
283
+
284
+ # Compute phase alignment (Kuramoto order parameter)
285
+ phase_alignment = compute_phase_alignment(G, node)
286
+
287
+ return {
288
+ "operator": "Coherence",
289
+ "glyph": "IL",
290
+ "dnfr_before": dnfr_before,
291
+ "dnfr_after": dnfr_after,
292
+ "dnfr_reduction": dnfr_reduction,
293
+ "dnfr_reduction_pct": dnfr_reduction_pct,
294
+ "dnfr_final": dnfr_after,
295
+ "stability_gain": abs(dnfr_before) - abs(dnfr_after),
296
+ "C_global": C_global,
297
+ "C_local": C_local,
298
+ "phase_alignment": phase_alignment,
299
+ "phase_coherence_quality": phase_alignment, # Alias for clarity
300
+ "stabilization_quality": C_local * (1.0 - dnfr_after), # Combined metric
301
+ "epi_final": epi,
302
+ "vf_final": vf,
303
+ "is_stabilized": abs(dnfr_after) < 0.1, # Configurable threshold
304
+ }
305
+
306
+
307
+ def dissonance_metrics(
308
+ G: TNFRGraph, node: NodeId, dnfr_before: float, theta_before: float
309
+ ) -> dict[str, Any]:
310
+ """OZ - Comprehensive dissonance and bifurcation metrics.
311
+
312
+ Collects extended metrics for the Dissonance (OZ) operator, including
313
+ quantitative bifurcation analysis, topological disruption measures, and
314
+ viable path identification. This aligns with TNFR canonical theory (§2.3.3)
315
+ that OZ introduces **topological dissonance**, not just numerical instability.
316
+
317
+ Parameters
318
+ ----------
319
+ G : TNFRGraph
320
+ Graph containing the node
321
+ node : NodeId
322
+ Node to collect metrics from
323
+ dnfr_before : float
324
+ ΔNFR value before operator application
325
+ theta_before : float
326
+ Phase value before operator application
327
+
328
+ Returns
329
+ -------
330
+ dict
331
+ Comprehensive dissonance metrics with keys:
332
+
333
+ **Quantitative dynamics:**
334
+
335
+ - dnfr_increase: Magnitude of introduced instability
336
+ - dnfr_final: Post-OZ ΔNFR value
337
+ - theta_shift: Phase exploration degree
338
+ - theta_final: Post-OZ phase value
339
+ - d2epi: Structural acceleration (bifurcation indicator)
340
+
341
+ **Bifurcation analysis:**
342
+
343
+ - bifurcation_score: Quantitative potential [0,1]
344
+ - bifurcation_active: Boolean threshold indicator (score > 0.5)
345
+ - viable_paths: List of viable operator glyph values
346
+ - viable_path_count: Number of viable paths
347
+ - mutation_readiness: Boolean indicator for ZHIR viability
348
+
349
+ **Topological effects:**
350
+
351
+ - topological_asymmetry_delta: Change in structural asymmetry
352
+ - symmetry_disrupted: Boolean (|delta| > 0.1)
353
+
354
+ **Network impact:**
355
+
356
+ - neighbor_count: Total neighbors
357
+ - impacted_neighbors: Count with |ΔNFR| > 0.1
358
+ - network_impact_radius: Ratio of impacted neighbors
359
+
360
+ **Recovery guidance:**
361
+
362
+ - recovery_estimate_IL: Estimated IL applications needed
363
+ - dissonance_level: |ΔNFR| magnitude
364
+ - critical_dissonance: Boolean (|ΔNFR| > 0.8)
365
+
366
+ Notes
367
+ -----
368
+ **Enhanced metrics vs original:**
369
+
370
+ The original implementation (lines 326-342) provided:
371
+ - Basic ΔNFR change
372
+ - Boolean bifurcation_risk
373
+ - Simple d2epi reading
374
+
375
+ This enhanced version adds:
376
+ - Quantitative bifurcation_score [0,1]
377
+ - Viable path identification
378
+ - Topological asymmetry measurement
379
+ - Network impact analysis
380
+ - Recovery estimation
381
+
382
+ **Topological asymmetry:**
383
+
384
+ Measures structural disruption in the node's ego-network using degree
385
+ and clustering heterogeneity. This captures the canonical effect that
386
+ OZ introduces **topological disruption**, not just numerical change.
387
+
388
+ **Viable paths:**
389
+
390
+ Identifies which operators can structurally resolve the dissonance:
391
+ - IL (Coherence): Always viable (universal resolution)
392
+ - ZHIR (Mutation): If νf > 0.8 (controlled transformation)
393
+ - NUL (Contraction): If EPI < 0.5 (safe collapse window)
394
+ - THOL (Self-organization): If degree >= 2 (network support)
395
+
396
+ Examples
397
+ --------
398
+ >>> from tnfr.structural import create_nfr
399
+ >>> from tnfr.operators.definitions import Dissonance, Coherence
400
+ >>>
401
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.2)
402
+ >>> # Add neighbors for network analysis
403
+ >>> for i in range(3):
404
+ ... G.add_node(f"n{i}")
405
+ ... G.add_edge(node, f"n{i}")
406
+ >>>
407
+ >>> # Enable metrics collection
408
+ >>> G.graph['COLLECT_OPERATOR_METRICS'] = True
409
+ >>>
410
+ >>> # Apply Coherence to stabilize, then Dissonance to disrupt
411
+ >>> Coherence()(G, node)
412
+ >>> Dissonance()(G, node)
413
+ >>>
414
+ >>> # Retrieve enhanced metrics
415
+ >>> metrics = G.graph['operator_metrics'][-1]
416
+ >>> print(f"Bifurcation score: {metrics['bifurcation_score']:.2f}")
417
+ >>> print(f"Viable paths: {metrics['viable_paths']}")
418
+ >>> print(f"Network impact: {metrics['network_impact_radius']:.1%}")
419
+ >>> print(f"Recovery estimate: {metrics['recovery_estimate_IL']} IL")
420
+
421
+ See Also
422
+ --------
423
+ tnfr.dynamics.bifurcation.compute_bifurcation_score : Bifurcation scoring
424
+ tnfr.topology.asymmetry.compute_topological_asymmetry : Asymmetry measurement
425
+ tnfr.dynamics.bifurcation.get_bifurcation_paths : Viable path identification
426
+ """
427
+ from ..dynamics.bifurcation import compute_bifurcation_score, get_bifurcation_paths
428
+ from ..topology.asymmetry import compute_topological_asymmetry
429
+ from .nodal_equation import compute_d2epi_dt2
430
+
431
+ # Get post-OZ node state
432
+ dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
433
+ theta_after = _get_node_attr(G, node, ALIAS_THETA)
434
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
435
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
436
+
437
+ # 1. Compute d2epi actively during OZ
438
+ d2epi = compute_d2epi_dt2(G, node)
439
+
440
+ # 2. Quantitative bifurcation score (not just boolean)
441
+ bifurcation_threshold = float(G.graph.get("OZ_BIFURCATION_THRESHOLD", 0.5))
442
+ bifurcation_score = compute_bifurcation_score(
443
+ d2epi=d2epi,
444
+ dnfr=dnfr_after,
445
+ vf=vf_after,
446
+ epi=epi_after,
447
+ tau=bifurcation_threshold,
448
+ )
449
+
450
+ # 3. Topological asymmetry introduced by OZ
451
+ # Note: We measure asymmetry after OZ. In a full implementation, we'd also
452
+ # capture before state, but for metrics collection we focus on post-state.
453
+ # The delta is captured conceptually (OZ introduces disruption).
454
+ asymmetry_after = compute_topological_asymmetry(G, node)
455
+
456
+ # For now, we'll estimate delta based on the assumption that OZ increases asymmetry
457
+ # In a future enhancement, this could be computed by storing asymmetry_before
458
+ asymmetry_delta = asymmetry_after # Simplified: assume OZ caused current asymmetry
459
+
460
+ # 4. Analyze viable post-OZ paths
461
+ # Set bifurcation_ready flag if score exceeds threshold
462
+ if bifurcation_score > 0.5:
463
+ G.nodes[node]["_bifurcation_ready"] = True
464
+
465
+ viable_paths = get_bifurcation_paths(G, node)
466
+
467
+ # 5. Network impact (neighbors affected by dissonance)
468
+ neighbors = list(G.neighbors(node))
469
+ impacted_neighbors = 0
470
+
471
+ if neighbors:
472
+ # Count neighbors with significant |ΔNFR|
473
+ impact_threshold = 0.1
474
+ for n in neighbors:
475
+ neighbor_dnfr = abs(_get_node_attr(G, n, ALIAS_DNFR))
476
+ if neighbor_dnfr > impact_threshold:
477
+ impacted_neighbors += 1
478
+
479
+ # 6. Recovery estimate (how many IL needed to resolve)
480
+ # Assumes ~15% ΔNFR reduction per IL application
481
+ il_reduction_rate = 0.15
482
+ recovery_estimate = (
483
+ int(abs(dnfr_after) / il_reduction_rate) + 1 if dnfr_after != 0 else 1
484
+ )
485
+
486
+ # 7. Propagation analysis (if propagation occurred)
487
+ propagation_data = {}
488
+ propagation_events = G.graph.get("_oz_propagation_events", [])
489
+ if propagation_events:
490
+ latest_event = propagation_events[-1]
491
+ if latest_event["source"] == node:
492
+ propagation_data = {
493
+ "propagation_occurred": True,
494
+ "affected_neighbors": latest_event["affected_count"],
495
+ "propagation_magnitude": latest_event["magnitude"],
496
+ "affected_nodes": latest_event["affected_nodes"],
497
+ }
498
+ else:
499
+ propagation_data = {"propagation_occurred": False}
500
+ else:
501
+ propagation_data = {"propagation_occurred": False}
502
+
503
+ # 8. Compute network dissonance field (if propagation module available)
504
+ field_data = {}
505
+ try:
506
+ from ..dynamics.propagation import compute_network_dissonance_field
507
+
508
+ field = compute_network_dissonance_field(G, node, radius=2)
509
+ field_data = {
510
+ "dissonance_field_radius": len(field),
511
+ "max_field_strength": max(field.values()) if field else 0.0,
512
+ "mean_field_strength": sum(field.values()) / len(field) if field else 0.0,
513
+ }
514
+ except (ImportError, Exception):
515
+ # Gracefully handle if propagation module not available
516
+ field_data = {
517
+ "dissonance_field_radius": 0,
518
+ "max_field_strength": 0.0,
519
+ "mean_field_strength": 0.0,
520
+ }
521
+
522
+ return {
523
+ "operator": "Dissonance",
524
+ "glyph": "OZ",
525
+ # Quantitative dynamics
526
+ "dnfr_increase": dnfr_after - dnfr_before,
527
+ "dnfr_final": dnfr_after,
528
+ "theta_shift": abs(theta_after - theta_before),
529
+ "theta_final": theta_after,
530
+ "d2epi": d2epi,
531
+ # Bifurcation analysis
532
+ "bifurcation_score": bifurcation_score,
533
+ "bifurcation_active": bifurcation_score > 0.5,
534
+ "viable_paths": [str(g.value) for g in viable_paths],
535
+ "viable_path_count": len(viable_paths),
536
+ "mutation_readiness": any(g.value == "ZHIR" for g in viable_paths),
537
+ # Topological effects
538
+ "topological_asymmetry_delta": asymmetry_delta,
539
+ "symmetry_disrupted": abs(asymmetry_delta) > 0.1,
540
+ # Network impact
541
+ "neighbor_count": len(neighbors),
542
+ "impacted_neighbors": impacted_neighbors,
543
+ "network_impact_radius": (
544
+ impacted_neighbors / len(neighbors) if neighbors else 0.0
545
+ ),
546
+ # Recovery guidance
547
+ "recovery_estimate_IL": recovery_estimate,
548
+ "dissonance_level": abs(dnfr_after),
549
+ "critical_dissonance": abs(dnfr_after) > 0.8,
550
+ # Network propagation
551
+ **propagation_data,
552
+ **field_data,
553
+ }
554
+
555
+
556
+ def coupling_metrics(
557
+ G: TNFRGraph,
558
+ node: NodeId,
559
+ theta_before: float,
560
+ dnfr_before: float = None,
561
+ vf_before: float = None,
562
+ edges_before: int = None,
563
+ epi_before: float = None,
564
+ ) -> dict[str, Any]:
565
+ """UM - Coupling metrics: phase alignment, link formation, synchrony, ΔNFR reduction.
566
+
567
+ Extended metrics for Coupling (UM) operator that track structural changes,
568
+ network formation, and synchronization effectiveness.
569
+
570
+ Parameters
571
+ ----------
572
+ G : TNFRGraph
573
+ Graph containing the node
574
+ node : NodeId
575
+ Node to collect metrics from
576
+ theta_before : float
577
+ Phase value before operator application
578
+ dnfr_before : float, optional
579
+ ΔNFR value before operator application (for reduction tracking)
580
+ vf_before : float, optional
581
+ Structural frequency (νf) before operator application
582
+ edges_before : int, optional
583
+ Number of edges before operator application
584
+ epi_before : float, optional
585
+ EPI value before operator application (for invariance verification)
586
+
587
+ Returns
588
+ -------
589
+ dict
590
+ Coupling-specific metrics including:
591
+
592
+ **Phase metrics:**
593
+
594
+ - theta_shift: Absolute phase change
595
+ - theta_final: Post-coupling phase
596
+ - mean_neighbor_phase: Average phase of neighbors
597
+ - phase_alignment: Alignment with neighbors [0,1]
598
+ - phase_dispersion: Standard deviation of phases in local cluster
599
+ - is_synchronized: Boolean indicating strong synchronization (alignment > 0.8)
600
+
601
+ **Frequency metrics:**
602
+
603
+ - delta_vf: Change in structural frequency (νf)
604
+ - vf_final: Post-coupling structural frequency
605
+
606
+ **Reorganization metrics:**
607
+
608
+ - delta_dnfr: Change in ΔNFR
609
+ - dnfr_stabilization: Reduction of reorganization pressure (positive if stabilized)
610
+ - dnfr_final: Post-coupling ΔNFR
611
+ - dnfr_reduction: Absolute reduction (before - after)
612
+ - dnfr_reduction_pct: Percentage reduction
613
+
614
+ **EPI Invariance metrics:**
615
+
616
+ - epi_before: EPI value before coupling
617
+ - epi_after: EPI value after coupling
618
+ - epi_drift: Absolute difference between before and after
619
+ - epi_preserved: Boolean indicating EPI invariance (drift < 1e-9)
620
+
621
+ **Network metrics:**
622
+
623
+ - neighbor_count: Number of neighbors after coupling
624
+ - new_edges_count: Number of edges added
625
+ - total_edges: Total edges after coupling
626
+ - coupling_strength_total: Sum of coupling weights on edges
627
+ - local_coherence: Kuramoto order parameter of local subgraph
628
+
629
+ Notes
630
+ -----
631
+ The extended metrics align with TNFR canonical theory (§2.2.2) that UM creates
632
+ structural links through phase synchronization (φᵢ(t) ≈ φⱼ(t)). The metrics
633
+ capture both the synchronization quality and the network structural changes
634
+ resulting from coupling.
635
+
636
+ **EPI Invariance**: UM MUST preserve EPI identity. The epi_preserved metric
637
+ validates this fundamental invariant. If epi_preserved is False, it indicates
638
+ a violation of TNFR canonical requirements.
639
+
640
+ See Also
641
+ --------
642
+ operators.definitions.Coupling : UM operator implementation
643
+ metrics.phase_coherence.compute_phase_alignment : Phase alignment computation
644
+ """
645
+ import math
646
+ import statistics
647
+
648
+ theta_after = _get_node_attr(G, node, ALIAS_THETA)
649
+ dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
650
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
651
+ neighbors = list(G.neighbors(node))
652
+ neighbor_count = len(neighbors)
653
+
654
+ # Calculate phase coherence with neighbors
655
+ if neighbor_count > 0:
656
+ phase_sum = sum(_get_node_attr(G, n, ALIAS_THETA) for n in neighbors)
657
+ mean_neighbor_phase = phase_sum / neighbor_count
658
+ phase_alignment = 1.0 - abs(theta_after - mean_neighbor_phase) / math.pi
659
+ else:
660
+ mean_neighbor_phase = theta_after
661
+ phase_alignment = 0.0
662
+
663
+ # Base metrics (always present)
664
+ metrics = {
665
+ "operator": "Coupling",
666
+ "glyph": "UM",
667
+ "theta_shift": abs(theta_after - theta_before),
668
+ "theta_final": theta_after,
669
+ "neighbor_count": neighbor_count,
670
+ "mean_neighbor_phase": mean_neighbor_phase,
671
+ "phase_alignment": max(0.0, phase_alignment),
672
+ }
673
+
674
+ # Structural frequency metrics (if vf_before provided)
675
+ if vf_before is not None:
676
+ delta_vf = vf_after - vf_before
677
+ metrics.update(
678
+ {
679
+ "delta_vf": delta_vf,
680
+ "vf_final": vf_after,
681
+ }
682
+ )
683
+
684
+ # ΔNFR reduction metrics (if dnfr_before provided)
685
+ if dnfr_before is not None:
686
+ dnfr_reduction = dnfr_before - dnfr_after
687
+ dnfr_reduction_pct = (dnfr_reduction / (abs(dnfr_before) + 1e-9)) * 100.0
688
+ dnfr_stabilization = dnfr_before - dnfr_after # Positive if stabilized
689
+ metrics.update(
690
+ {
691
+ "dnfr_before": dnfr_before,
692
+ "dnfr_after": dnfr_after,
693
+ "delta_dnfr": dnfr_after - dnfr_before,
694
+ "dnfr_reduction": dnfr_reduction,
695
+ "dnfr_reduction_pct": dnfr_reduction_pct,
696
+ "dnfr_stabilization": dnfr_stabilization,
697
+ "dnfr_final": dnfr_after,
698
+ }
699
+ )
700
+
701
+ # EPI invariance verification (if epi_before provided)
702
+ # CRITICAL: UM MUST preserve EPI identity per TNFR canonical theory
703
+ if epi_before is not None:
704
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
705
+ epi_drift = abs(epi_after - epi_before)
706
+ metrics.update(
707
+ {
708
+ "epi_before": epi_before,
709
+ "epi_after": epi_after,
710
+ "epi_drift": epi_drift,
711
+ "epi_preserved": epi_drift < 1e-9, # Should ALWAYS be True
712
+ }
713
+ )
714
+
715
+ # Edge/network formation metrics (if edges_before provided)
716
+ edges_after = G.degree(node)
717
+ if edges_before is not None:
718
+ new_edges_count = edges_after - edges_before
719
+ metrics.update(
720
+ {
721
+ "new_edges_count": new_edges_count,
722
+ "total_edges": edges_after,
723
+ }
724
+ )
725
+ else:
726
+ # Still provide total_edges even without edges_before
727
+ metrics["total_edges"] = edges_after
728
+
729
+ # Coupling strength (sum of edge weights)
730
+ coupling_strength_total = 0.0
731
+ for neighbor in neighbors:
732
+ edge_data = G.get_edge_data(node, neighbor)
733
+ if edge_data and isinstance(edge_data, dict):
734
+ coupling_strength_total += edge_data.get("coupling", 0.0)
735
+ metrics["coupling_strength_total"] = coupling_strength_total
736
+
737
+ # Phase dispersion (standard deviation of local phases)
738
+ if neighbor_count > 1:
739
+ phases = [theta_after] + [_get_node_attr(G, n, ALIAS_THETA) for n in neighbors]
740
+ phase_std = statistics.stdev(phases)
741
+ metrics["phase_dispersion"] = phase_std
742
+ else:
743
+ metrics["phase_dispersion"] = 0.0
744
+
745
+ # Local coherence (Kuramoto order parameter of subgraph)
746
+ if neighbor_count > 0:
747
+ from ..metrics.phase_coherence import compute_phase_alignment
748
+
749
+ local_coherence = compute_phase_alignment(G, node, radius=1)
750
+ metrics["local_coherence"] = local_coherence
751
+ else:
752
+ metrics["local_coherence"] = 0.0
753
+
754
+ # Synchronization indicator
755
+ metrics["is_synchronized"] = phase_alignment > 0.8
756
+
757
+ return metrics
758
+
759
+
760
+ def resonance_metrics(
761
+ G: TNFRGraph,
762
+ node: NodeId,
763
+ epi_before: float,
764
+ vf_before: float | None = None,
765
+ ) -> dict[str, Any]:
766
+ """RA - Resonance metrics: EPI propagation, νf amplification, phase strengthening.
767
+
768
+ Canonical TNFR resonance metrics include:
769
+ - EPI propagation effectiveness
770
+ - νf amplification (structural frequency increase)
771
+ - Phase alignment strengthening
772
+ - Identity preservation validation
773
+ - Network coherence contribution
774
+
775
+ Parameters
776
+ ----------
777
+ G : TNFRGraph
778
+ Graph containing the node
779
+ node : NodeId
780
+ Node to collect metrics from
781
+ epi_before : float
782
+ EPI value before operator application
783
+ vf_before : float | None
784
+ νf value before operator application (for amplification tracking)
785
+
786
+ Returns
787
+ -------
788
+ dict
789
+ Resonance-specific metrics including:
790
+ - EPI propagation metrics
791
+ - νf amplification ratio (canonical effect)
792
+ - Phase alignment quality
793
+ - Identity preservation status
794
+ - Network coherence contribution
795
+ """
796
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
797
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
798
+ neighbors = list(G.neighbors(node))
799
+ neighbor_count = len(neighbors)
800
+
801
+ # Calculate resonance strength based on neighbor coupling
802
+ if neighbor_count > 0:
803
+ neighbor_epi_sum = sum(_get_node_attr(G, n, ALIAS_EPI) for n in neighbors)
804
+ neighbor_epi_mean = neighbor_epi_sum / neighbor_count
805
+ resonance_strength = abs(epi_after - epi_before) * neighbor_count
806
+
807
+ # Canonical νf amplification tracking
808
+ if vf_before is not None and vf_before > 0:
809
+ vf_amplification = vf_after / vf_before
810
+ else:
811
+ vf_amplification = 1.0
812
+
813
+ # Phase alignment quality (measure coherence with neighbors)
814
+ from ..metrics.phase_coherence import compute_phase_alignment
815
+
816
+ phase_alignment = compute_phase_alignment(G, node)
817
+ else:
818
+ neighbor_epi_mean = 0.0
819
+ resonance_strength = 0.0
820
+ vf_amplification = 1.0
821
+ phase_alignment = 0.0
822
+
823
+ # Identity preservation check (sign should be preserved)
824
+ identity_preserved = epi_before * epi_after >= 0
825
+
826
+ return {
827
+ "operator": "Resonance",
828
+ "glyph": "RA",
829
+ "delta_epi": epi_after - epi_before,
830
+ "epi_final": epi_after,
831
+ "epi_before": epi_before,
832
+ "neighbor_count": neighbor_count,
833
+ "neighbor_epi_mean": neighbor_epi_mean,
834
+ "resonance_strength": resonance_strength,
835
+ "propagation_successful": neighbor_count > 0
836
+ and abs(epi_after - neighbor_epi_mean) < 0.5,
837
+ # Canonical TNFR effects
838
+ "vf_amplification": vf_amplification, # Canonical: νf increases through resonance
839
+ "vf_before": vf_before if vf_before is not None else vf_after,
840
+ "vf_after": vf_after,
841
+ "phase_alignment": phase_alignment, # Canonical: phase strengthens
842
+ "identity_preserved": identity_preserved, # Canonical: EPI identity maintained
843
+ }
844
+
845
+
846
+ def _compute_epi_variance(G: TNFRGraph, node: NodeId) -> float:
847
+ """Compute EPI variance during silence period.
848
+
849
+ Measures the standard deviation of EPI values recorded during silence,
850
+ validating effective preservation (variance ≈ 0).
851
+
852
+ Parameters
853
+ ----------
854
+ G : TNFRGraph
855
+ Graph containing the node
856
+ node : NodeId
857
+ Node to compute variance for
858
+
859
+ Returns
860
+ -------
861
+ float
862
+ Standard deviation of EPI during silence period
863
+ """
864
+ import numpy as np
865
+
866
+ epi_history = G.nodes[node].get("epi_history_during_silence", [])
867
+ if len(epi_history) < 2:
868
+ return 0.0
869
+ return float(np.std(epi_history))
870
+
871
+
872
+ def _compute_preservation_integrity(preserved_epi: float, epi_after: float) -> float:
873
+ """Compute preservation integrity ratio.
874
+
875
+ Measures structural preservation quality as:
876
+ integrity = 1 - |EPI_after - EPI_preserved| / EPI_preserved
877
+
878
+ Interpretation:
879
+ - integrity = 1.0: Perfect preservation
880
+ - integrity < 0.95: Significant degradation
881
+ - integrity < 0.8: Preservation failure
882
+
883
+ Parameters
884
+ ----------
885
+ preserved_epi : float
886
+ EPI value that was preserved at silence start
887
+ epi_after : float
888
+ Current EPI value
889
+
890
+ Returns
891
+ -------
892
+ float
893
+ Preservation integrity in [0, 1]
894
+ """
895
+ if preserved_epi == 0:
896
+ return 1.0 if epi_after == 0 else 0.0
897
+
898
+ integrity = 1.0 - abs(epi_after - preserved_epi) / abs(preserved_epi)
899
+ return max(0.0, integrity)
900
+
901
+
902
+ def _compute_reactivation_readiness(G: TNFRGraph, node: NodeId) -> float:
903
+ """Compute readiness score for reactivation from silence.
904
+
905
+ Evaluates if the node can reactivate effectively based on:
906
+ - νf residual (must be recoverable)
907
+ - EPI preserved (must be coherent)
908
+ - Silence duration (not excessive)
909
+ - Network connectivity (active neighbors)
910
+
911
+ Score in [0, 1]:
912
+ - 1.0: Fully ready to reactivate
913
+ - 0.5-0.8: Moderate readiness
914
+ - < 0.3: Risky reactivation
915
+
916
+ Parameters
917
+ ----------
918
+ G : TNFRGraph
919
+ Graph containing the node
920
+ node : NodeId
921
+ Node to compute readiness for
922
+
923
+ Returns
924
+ -------
925
+ float
926
+ Reactivation readiness score in [0, 1]
927
+ """
928
+ vf = _get_node_attr(G, node, ALIAS_VF)
929
+ epi = _get_node_attr(G, node, ALIAS_EPI)
930
+ duration = G.nodes[node].get("silence_duration", 0.0)
931
+
932
+ # Count active neighbors
933
+ active_neighbors = 0
934
+ if G.has_node(node):
935
+ for n in G.neighbors(node):
936
+ if _get_node_attr(G, n, ALIAS_VF) > 0.1:
937
+ active_neighbors += 1
938
+
939
+ # Scoring components
940
+ vf_score = min(vf / 0.5, 1.0) # νf recoverable
941
+ epi_score = min(epi / 0.3, 1.0) # EPI coherent
942
+ duration_score = 1.0 / (1.0 + duration * 0.1) # Penalize long silence
943
+ network_score = min(active_neighbors / 3.0, 1.0) # Network support
944
+
945
+ return (vf_score + epi_score + duration_score + network_score) / 4.0
946
+
947
+
948
+ def _estimate_time_to_collapse(G: TNFRGraph, node: NodeId) -> float:
949
+ """Estimate time until nodal collapse during silence.
950
+
951
+ Estimates how long silence can be maintained before structural collapse
952
+ based on observed drift rate or default degradation model.
953
+
954
+ Model:
955
+ t_collapse ≈ EPI_preserved / |DRIFT_RATE|
956
+
957
+ Parameters
958
+ ----------
959
+ G : TNFRGraph
960
+ Graph containing the node
961
+ node : NodeId
962
+ Node to estimate collapse time for
963
+
964
+ Returns
965
+ -------
966
+ float
967
+ Estimated time steps until collapse (inf if no degradation)
968
+ """
969
+ preserved_epi = G.nodes[node].get("preserved_epi", 0.0)
970
+ drift_rate = G.nodes[node].get("epi_drift_rate", 0.0)
971
+
972
+ if abs(drift_rate) < 1e-10:
973
+ # No observed degradation - return large value
974
+ return float("inf")
975
+
976
+ if preserved_epi <= 0:
977
+ # Already at or below collapse threshold
978
+ return 0.0
979
+
980
+ # Estimate time until EPI reaches zero
981
+ return abs(preserved_epi / drift_rate)
982
+
983
+
984
+ def silence_metrics(
985
+ G: TNFRGraph, node: NodeId, vf_before: float, epi_before: float
986
+ ) -> dict[str, Any]:
987
+ """SHA - Silence metrics: νf reduction, EPI preservation, duration tracking.
988
+
989
+ Extended metrics for deep analysis of structural preservation effectiveness.
990
+ Collects silence-specific metrics that reflect canonical SHA effects including
991
+ latency state management as specified in TNFR.pdf §2.3.10.
992
+
993
+ Parameters
994
+ ----------
995
+ G : TNFRGraph
996
+ Graph containing the node
997
+ node : NodeId
998
+ Node to collect metrics from
999
+ vf_before : float
1000
+ νf value before operator application
1001
+ epi_before : float
1002
+ EPI value before operator application
1003
+
1004
+ Returns
1005
+ -------
1006
+ dict
1007
+ Silence-specific metrics including:
1008
+
1009
+ **Core metrics (existing):**
1010
+
1011
+ - operator: "Silence"
1012
+ - glyph: "SHA"
1013
+ - vf_reduction: Absolute reduction in νf
1014
+ - vf_final: Post-silence νf value
1015
+ - epi_preservation: Absolute EPI change (should be ≈ 0)
1016
+ - epi_final: Post-silence EPI value
1017
+ - is_silent: Boolean indicating silent state (νf < 0.1)
1018
+
1019
+ **Latency state tracking:**
1020
+
1021
+ - latent: Boolean latency flag
1022
+ - silence_duration: Time in silence state (steps or structural time)
1023
+
1024
+ **Extended metrics (NEW):**
1025
+
1026
+ - epi_variance: Standard deviation of EPI during silence
1027
+ - preservation_integrity: Quality metric [0, 1] for preservation
1028
+ - reactivation_readiness: Readiness score [0, 1] for reactivation
1029
+ - time_to_collapse: Estimated time until nodal collapse
1030
+
1031
+ Notes
1032
+ -----
1033
+ Extended metrics enable:
1034
+ - Detection of excessive silence (collapse risk)
1035
+ - Validation of preservation quality
1036
+ - Analysis of consolidation patterns (memory, learning)
1037
+ - Strategic pause effectiveness (biomedical, cognitive, social domains)
1038
+
1039
+ See Also
1040
+ --------
1041
+ _compute_epi_variance : EPI variance computation
1042
+ _compute_preservation_integrity : Preservation quality metric
1043
+ _compute_reactivation_readiness : Reactivation readiness score
1044
+ _estimate_time_to_collapse : Collapse time estimation
1045
+ """
1046
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
1047
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
1048
+ preserved_epi = G.nodes[node].get("preserved_epi")
1049
+
1050
+ # Core metrics (existing)
1051
+ core = {
1052
+ "operator": "Silence",
1053
+ "glyph": "SHA",
1054
+ "vf_reduction": vf_before - vf_after,
1055
+ "vf_final": vf_after,
1056
+ "epi_preservation": abs(epi_after - epi_before),
1057
+ "epi_final": epi_after,
1058
+ "is_silent": vf_after < 0.1,
1059
+ }
1060
+
1061
+ # Latency state tracking metrics
1062
+ core["latent"] = G.nodes[node].get("latent", False)
1063
+ core["silence_duration"] = G.nodes[node].get("silence_duration", 0.0)
1064
+
1065
+ # Extended metrics (new)
1066
+ extended = {
1067
+ "epi_variance": _compute_epi_variance(G, node),
1068
+ "preservation_integrity": (
1069
+ _compute_preservation_integrity(preserved_epi, epi_after)
1070
+ if preserved_epi is not None
1071
+ else 1.0 - abs(epi_after - epi_before)
1072
+ ),
1073
+ "reactivation_readiness": _compute_reactivation_readiness(G, node),
1074
+ "time_to_collapse": _estimate_time_to_collapse(G, node),
1075
+ }
1076
+
1077
+ return {**core, **extended}
1078
+
1079
+
1080
+ def expansion_metrics(
1081
+ G: TNFRGraph, node: NodeId, vf_before: float, epi_before: float
1082
+ ) -> dict[str, Any]:
1083
+ """VAL - Enhanced expansion metrics with structural indicators (Issue #2724).
1084
+
1085
+ Captures comprehensive metrics reflecting canonical VAL effects:
1086
+ - Basic growth metrics (Δνf, ΔEPI)
1087
+ - Bifurcation risk (∂²EPI/∂t²)
1088
+ - Coherence preservation (local C(t))
1089
+ - Fractality indicators (growth ratios)
1090
+ - Network impact (phase coherence with neighbors)
1091
+ - Structural stability (ΔNFR bounds)
1092
+
1093
+ Parameters
1094
+ ----------
1095
+ G : TNFRGraph
1096
+ Graph containing the node
1097
+ node : NodeId
1098
+ Node to collect metrics from
1099
+ vf_before : float
1100
+ νf value before operator application
1101
+ epi_before : float
1102
+ EPI value before operator application
1103
+
1104
+ Returns
1105
+ -------
1106
+ dict
1107
+ Comprehensive expansion metrics including:
1108
+
1109
+ **Core Metrics (existing)**:
1110
+ - operator, glyph: Identification
1111
+ - vf_increase, vf_final: Frequency changes
1112
+ - delta_epi, epi_final: EPI changes
1113
+ - expansion_factor: Relative νf increase
1114
+
1115
+ **Structural Stability (NEW)**:
1116
+ - dnfr_final: Final reorganization gradient
1117
+ - dnfr_positive: True if ΔNFR > 0 (required for expansion)
1118
+ - dnfr_stable: True if 0 < ΔNFR < 1.0 (bounded growth)
1119
+
1120
+ **Bifurcation Risk (ENHANCED)**:
1121
+ - d2epi: EPI acceleration (∂²EPI/∂t²)
1122
+ - bifurcation_risk: True when |∂²EPI/∂t²| > threshold
1123
+ - bifurcation_magnitude: Ratio of d2epi to threshold
1124
+ - bifurcation_threshold: Configurable threshold value
1125
+
1126
+ **Coherence Preservation (ENHANCED)**:
1127
+ - coherence_local: Local coherence measurement [0,1]
1128
+ - coherence_preserved: True when C_local > threshold
1129
+
1130
+ **Fractality Indicators (ENHANCED)**:
1131
+ - epi_growth_rate: Relative EPI growth
1132
+ - vf_growth_rate: Relative νf growth
1133
+ - growth_ratio: vf_growth_rate / epi_growth_rate
1134
+ - fractal_preserved: True when ratio in valid range [0.5, 2.0]
1135
+
1136
+ **Network Impact (NEW)**:
1137
+ - neighbor_count: Number of neighbors
1138
+ - phase_coherence_neighbors: Phase alignment with neighbors [0,1]
1139
+ - network_coupled: True if neighbors exist and phase_coherence > 0.5
1140
+ - theta_final: Final phase value
1141
+
1142
+ **Overall Health (NEW)**:
1143
+ - expansion_healthy: Combined indicator of all health metrics
1144
+
1145
+ Notes
1146
+ -----
1147
+ Key indicators:
1148
+ - bifurcation_risk: True when |∂²EPI/∂t²| > threshold
1149
+ - fractal_preserved: True when growth rates maintain scaling relationship
1150
+ - coherence_preserved: True when local C(t) remains above threshold
1151
+ - dnfr_positive: True when ΔNFR > 0 (required for expansion)
1152
+
1153
+ Thresholds are configurable via graph metadata:
1154
+ - VAL_BIFURCATION_THRESHOLD (default: 0.3)
1155
+ - VAL_MIN_COHERENCE (default: 0.5)
1156
+ - VAL_FRACTAL_RATIO_MIN (default: 0.5)
1157
+ - VAL_FRACTAL_RATIO_MAX (default: 2.0)
1158
+
1159
+ Examples
1160
+ --------
1161
+ >>> from tnfr.structural import create_nfr, run_sequence
1162
+ >>> from tnfr.operators.definitions import Expansion
1163
+ >>>
1164
+ >>> G, node = create_nfr("test", epi=0.4, vf=1.0)
1165
+ >>> G.graph["COLLECT_OPERATOR_METRICS"] = True
1166
+ >>> run_sequence(G, node, [Expansion()])
1167
+ >>>
1168
+ >>> metrics = G.graph["operator_metrics"][-1]
1169
+ >>> if metrics["bifurcation_risk"]:
1170
+ ... print(f"WARNING: Bifurcation risk! d2epi={metrics['d2epi']:.3f}")
1171
+ >>> if not metrics["coherence_preserved"]:
1172
+ ... print(f"WARNING: Coherence degraded! C={metrics['coherence_local']:.3f}")
1173
+
1174
+ See Also
1175
+ --------
1176
+ Expansion : VAL operator that produces these metrics
1177
+ validate_expansion : Preconditions ensuring valid expansion
1178
+ """
1179
+ import math
1180
+
1181
+ # Basic state
1182
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
1183
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
1184
+ dnfr = _get_node_attr(G, node, ALIAS_DNFR)
1185
+ d2epi = _get_node_attr(G, node, ALIAS_D2EPI)
1186
+ theta = _get_node_attr(G, node, ALIAS_THETA)
1187
+
1188
+ # Network context
1189
+ neighbors = list(G.neighbors(node))
1190
+ neighbor_count = len(neighbors)
1191
+
1192
+ # Thresholds (configurable)
1193
+ bifurcation_threshold = float(G.graph.get("VAL_BIFURCATION_THRESHOLD", 0.3))
1194
+ coherence_threshold = float(G.graph.get("VAL_MIN_COHERENCE", 0.5))
1195
+ fractal_ratio_min = float(G.graph.get("VAL_FRACTAL_RATIO_MIN", 0.5))
1196
+ fractal_ratio_max = float(G.graph.get("VAL_FRACTAL_RATIO_MAX", 2.0))
1197
+
1198
+ # Growth deltas
1199
+ delta_epi = epi_after - epi_before
1200
+ delta_vf = vf_after - vf_before
1201
+
1202
+ # Growth rates (relative to initial values)
1203
+ epi_growth_rate = (delta_epi / epi_before) if epi_before > 1e-9 else 0.0
1204
+ vf_growth_rate = (delta_vf / vf_before) if vf_before > 1e-9 else 0.0
1205
+ growth_ratio = (
1206
+ vf_growth_rate / epi_growth_rate if abs(epi_growth_rate) > 1e-9 else 0.0
1207
+ )
1208
+
1209
+ # Coherence preservation
1210
+ try:
1211
+ from ..metrics.coherence import compute_local_coherence
1212
+
1213
+ c_local = compute_local_coherence(G, node)
1214
+ except (ImportError, AttributeError):
1215
+ # Fallback if coherence module not available
1216
+ c_local = 0.0
1217
+
1218
+ # Phase coherence with neighbors
1219
+ if neighbor_count > 0:
1220
+ neighbor_theta_sum = sum(_get_node_attr(G, n, ALIAS_THETA) for n in neighbors)
1221
+ mean_neighbor_theta = neighbor_theta_sum / neighbor_count
1222
+ phase_diff = abs(theta - mean_neighbor_theta)
1223
+ # Normalize to [0, 1], 1 = perfect alignment
1224
+ phase_coherence_neighbors = 1.0 - min(phase_diff, math.pi) / math.pi
1225
+ else:
1226
+ phase_coherence_neighbors = 0.0
1227
+
1228
+ # Bifurcation magnitude (ratio to threshold)
1229
+ bifurcation_magnitude = (
1230
+ abs(d2epi) / bifurcation_threshold if bifurcation_threshold > 0 else 0.0
1231
+ )
1232
+
1233
+ # Boolean indicators
1234
+ bifurcation_risk = abs(d2epi) > bifurcation_threshold
1235
+ coherence_preserved = c_local > coherence_threshold
1236
+ dnfr_positive = dnfr > 0
1237
+ dnfr_stable = 0 < dnfr < 1.0
1238
+ fractal_preserved = (
1239
+ fractal_ratio_min < growth_ratio < fractal_ratio_max
1240
+ if abs(epi_growth_rate) > 1e-9
1241
+ else True
1242
+ )
1243
+ network_coupled = neighbor_count > 0 and phase_coherence_neighbors > 0.5
1244
+
1245
+ # Overall health indicator
1246
+ expansion_healthy = (
1247
+ dnfr_positive
1248
+ and not bifurcation_risk
1249
+ and coherence_preserved
1250
+ and fractal_preserved
1251
+ )
1252
+
1253
+ return {
1254
+ # Core identification
1255
+ "operator": "Expansion",
1256
+ "glyph": "VAL",
1257
+ # Existing basic metrics
1258
+ "vf_increase": delta_vf,
1259
+ "vf_final": vf_after,
1260
+ "delta_epi": delta_epi,
1261
+ "epi_final": epi_after,
1262
+ "expansion_factor": vf_after / vf_before if vf_before > 1e-9 else 1.0,
1263
+ # NEW: Structural stability
1264
+ "dnfr_final": dnfr,
1265
+ "dnfr_positive": dnfr_positive,
1266
+ "dnfr_stable": dnfr_stable,
1267
+ # NEW: Bifurcation risk (enhanced)
1268
+ "d2epi": d2epi,
1269
+ "bifurcation_risk": bifurcation_risk,
1270
+ "bifurcation_magnitude": bifurcation_magnitude,
1271
+ "bifurcation_threshold": bifurcation_threshold,
1272
+ # NEW: Coherence preservation
1273
+ "coherence_local": c_local,
1274
+ "coherence_preserved": coherence_preserved,
1275
+ # NEW: Fractality indicators
1276
+ "epi_growth_rate": epi_growth_rate,
1277
+ "vf_growth_rate": vf_growth_rate,
1278
+ "growth_ratio": growth_ratio,
1279
+ "fractal_preserved": fractal_preserved,
1280
+ # NEW: Network impact
1281
+ "neighbor_count": neighbor_count,
1282
+ "phase_coherence_neighbors": max(0.0, phase_coherence_neighbors),
1283
+ "network_coupled": network_coupled,
1284
+ "theta_final": theta,
1285
+ # NEW: Overall health
1286
+ "expansion_healthy": expansion_healthy,
1287
+ # Metadata
1288
+ "metrics_version": "3.0_canonical",
1289
+ }
1290
+
1291
+
1292
+ def contraction_metrics(
1293
+ G: TNFRGraph, node: NodeId, vf_before: float, epi_before: float
1294
+ ) -> dict[str, Any]:
1295
+ """NUL - Contraction metrics: νf decrease, core concentration, ΔNFR densification.
1296
+
1297
+ Collects comprehensive contraction metrics including structural density dynamics
1298
+ that validate canonical NUL behavior and enable early warning for over-compression.
1299
+
1300
+ Parameters
1301
+ ----------
1302
+ G : TNFRGraph
1303
+ Graph containing the node
1304
+ node : NodeId
1305
+ Node to collect metrics from
1306
+ vf_before : float
1307
+ νf value before operator application
1308
+ epi_before : float
1309
+ EPI value before operator application
1310
+
1311
+ Returns
1312
+ -------
1313
+ dict
1314
+ Contraction-specific metrics including:
1315
+
1316
+ **Basic metrics:**
1317
+
1318
+ - operator: "Contraction"
1319
+ - glyph: "NUL"
1320
+ - vf_decrease: Absolute reduction in νf
1321
+ - vf_final: Post-contraction νf
1322
+ - delta_epi: EPI change
1323
+ - epi_final: Post-contraction EPI
1324
+ - dnfr_final: Post-contraction ΔNFR
1325
+ - contraction_factor: Ratio of vf_after / vf_before
1326
+
1327
+ **Densification metrics (if available):**
1328
+
1329
+ - densification_factor: ΔNFR amplification factor (typically 1.35)
1330
+ - dnfr_densified: Boolean indicating densification occurred
1331
+ - dnfr_before: ΔNFR value before contraction
1332
+ - dnfr_increase: Absolute ΔNFR change (dnfr_after - dnfr_before)
1333
+
1334
+ **Structural density metrics (NEW):**
1335
+
1336
+ - density_before: |ΔNFR| / max(EPI, ε) before contraction
1337
+ - density_after: |ΔNFR| / max(EPI, ε) after contraction
1338
+ - densification_ratio: density_after / density_before
1339
+ - is_critical_density: Warning flag (density > threshold)
1340
+
1341
+ Notes
1342
+ -----
1343
+ **Structural Density**: Defined as ρ = |ΔNFR| / max(EPI, ε) where ε = 1e-9.
1344
+ This captures the concentration of reorganization pressure per unit structure.
1345
+
1346
+ **Critical Density**: When density exceeds CRITICAL_DENSITY_THRESHOLD (default: 5.0),
1347
+ it indicates over-compression risk where the node may become unstable.
1348
+
1349
+ **Densification Ratio**: Quantifies how much density increased during contraction.
1350
+ Canonical NUL should produce densification_ratio ≈ densification_factor / contraction_factor.
1351
+
1352
+ See Also
1353
+ --------
1354
+ Contraction : NUL operator implementation
1355
+ validate_contraction : Preconditions for safe contraction
1356
+ """
1357
+ # Small epsilon for numerical stability
1358
+ EPSILON = 1e-9
1359
+
1360
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
1361
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
1362
+ dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
1363
+
1364
+ # Extract densification telemetry if available
1365
+ densification_log = G.graph.get("nul_densification_log", [])
1366
+ densification_factor = None
1367
+ dnfr_before = None
1368
+ if densification_log:
1369
+ # Get the most recent densification entry for this node
1370
+ last_entry = densification_log[-1]
1371
+ densification_factor = last_entry.get("densification_factor")
1372
+ dnfr_before = last_entry.get("dnfr_before")
1373
+
1374
+ # Calculate structural density before and after
1375
+ # Density = |ΔNFR| / max(EPI, ε)
1376
+ density_before = abs(dnfr_before) / max(abs(epi_before), EPSILON) if dnfr_before is not None else 0.0
1377
+ density_after = abs(dnfr_after) / max(abs(epi_after), EPSILON)
1378
+
1379
+ # Calculate densification ratio (how much density increased)
1380
+ densification_ratio = density_after / density_before if density_before > EPSILON else float('inf')
1381
+
1382
+ # Get critical density threshold from graph config or use default
1383
+ critical_density_threshold = float(G.graph.get("CRITICAL_DENSITY_THRESHOLD", 5.0))
1384
+ is_critical_density = density_after > critical_density_threshold
1385
+
1386
+ metrics = {
1387
+ "operator": "Contraction",
1388
+ "glyph": "NUL",
1389
+ "vf_decrease": vf_before - vf_after,
1390
+ "vf_final": vf_after,
1391
+ "delta_epi": epi_after - epi_before,
1392
+ "epi_final": epi_after,
1393
+ "dnfr_final": dnfr_after,
1394
+ "contraction_factor": vf_after / vf_before if vf_before > 0 else 1.0,
1395
+ }
1396
+
1397
+ # Add densification metrics if available
1398
+ if densification_factor is not None:
1399
+ metrics["densification_factor"] = densification_factor
1400
+ metrics["dnfr_densified"] = True
1401
+ if dnfr_before is not None:
1402
+ metrics["dnfr_before"] = dnfr_before
1403
+ metrics["dnfr_increase"] = dnfr_after - dnfr_before if dnfr_before else 0.0
1404
+
1405
+ # Add NEW structural density metrics
1406
+ metrics["density_before"] = density_before
1407
+ metrics["density_after"] = density_after
1408
+ metrics["densification_ratio"] = densification_ratio
1409
+ metrics["is_critical_density"] = is_critical_density
1410
+
1411
+ return metrics
1412
+
1413
+
1414
+ def self_organization_metrics(
1415
+ G: TNFRGraph, node: NodeId, epi_before: float, vf_before: float
1416
+ ) -> dict[str, Any]:
1417
+ """THOL - Enhanced metrics with cascade dynamics and collective coherence.
1418
+
1419
+ Collects comprehensive THOL metrics including bifurcation, cascade propagation,
1420
+ collective coherence of sub-EPIs, and metabolic activity indicators.
1421
+
1422
+ Parameters
1423
+ ----------
1424
+ G : TNFRGraph
1425
+ Graph containing the node
1426
+ node : NodeId
1427
+ Node to collect metrics from
1428
+ epi_before : float
1429
+ EPI value before operator application
1430
+ vf_before : float
1431
+ νf value before operator application
1432
+
1433
+ Returns
1434
+ -------
1435
+ dict
1436
+ Self-organization-specific metrics including:
1437
+
1438
+ **Base operator metrics:**
1439
+
1440
+ - operator: "Self-organization"
1441
+ - glyph: "THOL"
1442
+ - delta_epi: Change in EPI
1443
+ - delta_vf: Change in νf
1444
+ - epi_final: Final EPI value
1445
+ - vf_final: Final νf value
1446
+ - d2epi: Structural acceleration
1447
+ - dnfr_final: Final ΔNFR
1448
+
1449
+ **Bifurcation metrics:**
1450
+
1451
+ - bifurcation_occurred: Boolean indicator
1452
+ - nested_epi_count: Number of sub-EPIs created
1453
+ - d2epi_magnitude: Absolute acceleration
1454
+
1455
+ **Cascade dynamics (NEW):**
1456
+
1457
+ - cascade_depth: Maximum hierarchical bifurcation depth
1458
+ - propagation_radius: Total unique nodes affected
1459
+ - cascade_detected: Boolean cascade indicator
1460
+ - affected_node_count: Nodes reached by cascade
1461
+ - total_propagations: Total propagation events
1462
+
1463
+ **Collective coherence (NEW):**
1464
+
1465
+ - subepi_coherence: Coherence of sub-EPI ensemble [0,1]
1466
+ - metabolic_activity_index: Network context usage [0,1]
1467
+
1468
+ **Network emergence indicator (NEW):**
1469
+
1470
+ - network_emergence: Combined indicator (cascade + high coherence)
1471
+
1472
+ Notes
1473
+ -----
1474
+ TNFR Principle: Complete traceability of self-organization dynamics.
1475
+ These metrics enable reconstruction of entire cascade evolution,
1476
+ validation of controlled emergence, and identification of collective
1477
+ network phenomena.
1478
+
1479
+ See Also
1480
+ --------
1481
+ operators.metabolism.compute_cascade_depth : Cascade depth computation
1482
+ operators.metabolism.compute_subepi_collective_coherence : Coherence metric
1483
+ operators.metabolism.compute_metabolic_activity_index : Metabolic tracking
1484
+ operators.cascade.detect_cascade : Cascade detection
1485
+ """
1486
+ from .cascade import detect_cascade
1487
+ from .metabolism import (
1488
+ compute_cascade_depth,
1489
+ compute_propagation_radius,
1490
+ compute_subepi_collective_coherence,
1491
+ compute_metabolic_activity_index,
1492
+ )
1493
+
1494
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
1495
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
1496
+ d2epi = _get_node_attr(G, node, ALIAS_D2EPI)
1497
+ dnfr = _get_node_attr(G, node, ALIAS_DNFR)
1498
+
1499
+ # Track nested EPI count from node attribute or graph (backward compatibility)
1500
+ nested_epi_count = len(G.nodes[node].get("sub_epis", []))
1501
+ if nested_epi_count == 0:
1502
+ # Fallback to old location for backward compatibility
1503
+ nested_epi_count = len(G.graph.get("sub_epi", []))
1504
+
1505
+ # Cascade and propagation analysis
1506
+ cascade_analysis = detect_cascade(G)
1507
+
1508
+ # NEW: Enhanced cascade and emergence metrics
1509
+ cascade_depth = compute_cascade_depth(G, node)
1510
+ propagation_radius = compute_propagation_radius(G)
1511
+ subepi_coherence = compute_subepi_collective_coherence(G, node)
1512
+ metabolic_activity = compute_metabolic_activity_index(G, node)
1513
+
1514
+ return {
1515
+ # Base operator metrics
1516
+ "operator": "Self-organization",
1517
+ "glyph": "THOL",
1518
+ "delta_epi": epi_after - epi_before,
1519
+ "delta_vf": vf_after - vf_before,
1520
+ "epi_final": epi_after,
1521
+ "vf_final": vf_after,
1522
+ "d2epi": d2epi,
1523
+ "dnfr_final": dnfr,
1524
+ # Bifurcation metrics
1525
+ "bifurcation_occurred": nested_epi_count > 0,
1526
+ "nested_epi_count": nested_epi_count,
1527
+ "d2epi_magnitude": abs(d2epi),
1528
+ # NEW: Cascade dynamics
1529
+ "cascade_depth": cascade_depth,
1530
+ "propagation_radius": propagation_radius,
1531
+ "cascade_detected": cascade_analysis["is_cascade"],
1532
+ "affected_node_count": len(cascade_analysis["affected_nodes"]),
1533
+ "total_propagations": cascade_analysis["total_propagations"],
1534
+ # NEW: Collective coherence
1535
+ "subepi_coherence": subepi_coherence,
1536
+ "metabolic_activity_index": metabolic_activity,
1537
+ # NEW: Network emergence indicator
1538
+ "network_emergence": (
1539
+ cascade_analysis["is_cascade"] and subepi_coherence > 0.5
1540
+ ),
1541
+ }
1542
+
1543
+
1544
+ def mutation_metrics(
1545
+ G: TNFRGraph,
1546
+ node: NodeId,
1547
+ theta_before: float,
1548
+ epi_before: float,
1549
+ vf_before: float | None = None,
1550
+ dnfr_before: float | None = None,
1551
+ ) -> dict[str, Any]:
1552
+ """ZHIR - Comprehensive mutation metrics with canonical structural indicators.
1553
+
1554
+ Collects extended metrics reflecting canonical ZHIR effects:
1555
+ - Threshold verification (∂EPI/∂t > ξ)
1556
+ - Phase transformation quality (θ → θ')
1557
+ - Bifurcation potential (∂²EPI/∂t² > τ)
1558
+ - Structural identity preservation
1559
+ - Network impact and propagation
1560
+ - Destabilizer context (R4 Extended)
1561
+ - Grammar validation status
1562
+
1563
+ Parameters
1564
+ ----------
1565
+ G : TNFRGraph
1566
+ Graph containing the node
1567
+ node : NodeId
1568
+ Node to collect metrics from
1569
+ theta_before : float
1570
+ Phase value before operator application
1571
+ epi_before : float
1572
+ EPI value before operator application
1573
+ vf_before : float, optional
1574
+ νf before mutation (for frequency shift tracking)
1575
+ dnfr_before : float, optional
1576
+ ΔNFR before mutation (for pressure tracking)
1577
+
1578
+ Returns
1579
+ -------
1580
+ dict
1581
+ Comprehensive mutation metrics organized by category:
1582
+
1583
+ **Core metrics (existing):**
1584
+
1585
+ - operator, glyph: Identification
1586
+ - theta_shift, theta_final: Phase changes
1587
+ - delta_epi, epi_final: EPI changes
1588
+ - phase_change: Boolean indicator
1589
+
1590
+ **Threshold verification (ENHANCED):**
1591
+
1592
+ - depi_dt: Structural velocity (∂EPI/∂t)
1593
+ - threshold_xi: Configured threshold
1594
+ - threshold_met: Boolean (∂EPI/∂t > ξ)
1595
+ - threshold_ratio: depi_dt / ξ
1596
+ - threshold_exceeded_by: max(0, depi_dt - ξ)
1597
+
1598
+ **Phase transformation (ENHANCED):**
1599
+
1600
+ - theta_regime_before: Initial phase regime [0-3]
1601
+ - theta_regime_after: Final phase regime [0-3]
1602
+ - regime_changed: Boolean regime transition
1603
+ - theta_shift_direction: +1 (forward) or -1 (backward)
1604
+ - phase_transformation_magnitude: Normalized shift [0, 1]
1605
+
1606
+ **Bifurcation analysis (NEW):**
1607
+
1608
+ - d2epi: Structural acceleration
1609
+ - bifurcation_threshold_tau: Configured τ
1610
+ - bifurcation_potential: Boolean (∂²EPI/∂t² > τ)
1611
+ - bifurcation_score: Quantitative potential [0, 1]
1612
+ - bifurcation_triggered: Boolean (event recorded)
1613
+ - bifurcation_event_count: Number of bifurcation events
1614
+
1615
+ **Structural preservation (NEW):**
1616
+
1617
+ - epi_kind_before: Identity before mutation
1618
+ - epi_kind_after: Identity after mutation
1619
+ - identity_preserved: Boolean (must be True)
1620
+ - delta_vf: Change in structural frequency
1621
+ - vf_final: Final νf
1622
+ - delta_dnfr: Change in reorganization pressure
1623
+ - dnfr_final: Final ΔNFR
1624
+
1625
+ **Network impact (NEW):**
1626
+
1627
+ - neighbor_count: Number of neighbors
1628
+ - impacted_neighbors: Count with phase shift detected
1629
+ - network_impact_radius: Ratio of impacted neighbors
1630
+ - phase_coherence_neighbors: Phase alignment after mutation
1631
+
1632
+ **Destabilizer context (NEW - R4 Extended):**
1633
+
1634
+ - destabilizer_type: "strong"/"moderate"/"weak"/None
1635
+ - destabilizer_operator: Glyph that enabled mutation
1636
+ - destabilizer_distance: Operators since destabilizer
1637
+ - recent_history: Last 4 operators
1638
+
1639
+ **Grammar validation (NEW):**
1640
+
1641
+ - grammar_u4b_satisfied: Boolean (IL precedence + destabilizer)
1642
+ - il_precedence_found: Boolean (IL in history)
1643
+ - destabilizer_recent: Boolean (within window)
1644
+
1645
+ Examples
1646
+ --------
1647
+ >>> from tnfr.structural import create_nfr, run_sequence
1648
+ >>> from tnfr.operators.definitions import Coherence, Dissonance, Mutation
1649
+ >>>
1650
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.2)
1651
+ >>> G.graph["COLLECT_OPERATOR_METRICS"] = True
1652
+ >>>
1653
+ >>> # Apply canonical sequence (IL → OZ → ZHIR)
1654
+ >>> run_sequence(G, node, [Coherence(), Dissonance(), Mutation()])
1655
+ >>>
1656
+ >>> # Retrieve comprehensive metrics
1657
+ >>> metrics = G.graph["operator_metrics"][-1]
1658
+ >>> print(f"Threshold met: {metrics['threshold_met']}")
1659
+ >>> print(f"Bifurcation score: {metrics['bifurcation_score']:.2f}")
1660
+ >>> print(f"Identity preserved: {metrics['identity_preserved']}")
1661
+ >>> print(f"Grammar satisfied: {metrics['grammar_u4b_satisfied']}")
1662
+
1663
+ See Also
1664
+ --------
1665
+ operators.definitions.Mutation : ZHIR operator implementation
1666
+ dynamics.bifurcation.compute_bifurcation_score : Bifurcation scoring
1667
+ operators.preconditions.validate_mutation : Precondition validation with context tracking
1668
+ """
1669
+ import math
1670
+
1671
+ # === GET POST-MUTATION STATE ===
1672
+ theta_after = _get_node_attr(G, node, ALIAS_THETA)
1673
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
1674
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
1675
+ dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
1676
+ d2epi = _get_node_attr(G, node, ALIAS_D2EPI, 0.0)
1677
+
1678
+ # === THRESHOLD VERIFICATION ===
1679
+ # Compute ∂EPI/∂t from history
1680
+ epi_history = G.nodes[node].get("epi_history") or G.nodes[node].get(
1681
+ "_epi_history", []
1682
+ )
1683
+ if len(epi_history) >= 2:
1684
+ depi_dt = abs(epi_history[-1] - epi_history[-2])
1685
+ else:
1686
+ depi_dt = 0.0
1687
+
1688
+ xi = float(G.graph.get("ZHIR_THRESHOLD_XI", 0.1))
1689
+ threshold_met = depi_dt >= xi
1690
+ threshold_ratio = depi_dt / xi if xi > 0 else 0.0
1691
+
1692
+ # === PHASE TRANSFORMATION ===
1693
+ # Extract transformation telemetry from glyph storage
1694
+ theta_shift_stored = G.nodes[node].get("_zhir_theta_shift", None)
1695
+ regime_changed = G.nodes[node].get("_zhir_regime_changed", False)
1696
+ regime_before_stored = G.nodes[node].get("_zhir_regime_before", None)
1697
+ regime_after_stored = G.nodes[node].get("_zhir_regime_after", None)
1698
+ fixed_mode = G.nodes[node].get("_zhir_fixed_mode", False)
1699
+
1700
+ # Compute theta shift
1701
+ theta_shift = theta_after - theta_before
1702
+ theta_shift_magnitude = abs(theta_shift)
1703
+
1704
+ # Compute regimes if not stored
1705
+ regime_before = (
1706
+ regime_before_stored
1707
+ if regime_before_stored is not None
1708
+ else int(theta_before // (math.pi / 2))
1709
+ )
1710
+ regime_after = (
1711
+ regime_after_stored
1712
+ if regime_after_stored is not None
1713
+ else int(theta_after // (math.pi / 2))
1714
+ )
1715
+
1716
+ # Normalized phase transformation magnitude [0, 1]
1717
+ phase_transformation_magnitude = min(theta_shift_magnitude / math.pi, 1.0)
1718
+
1719
+ # === BIFURCATION ANALYSIS ===
1720
+ tau = float(
1721
+ G.graph.get(
1722
+ "BIFURCATION_THRESHOLD_TAU", G.graph.get("ZHIR_BIFURCATION_THRESHOLD", 0.5)
1723
+ )
1724
+ )
1725
+ bifurcation_potential = d2epi > tau
1726
+
1727
+ # Compute bifurcation score using canonical formula
1728
+ from ..dynamics.bifurcation import compute_bifurcation_score
1729
+
1730
+ bifurcation_score = compute_bifurcation_score(
1731
+ d2epi=d2epi, dnfr=dnfr_after, vf=vf_after, epi=epi_after, tau=tau
1732
+ )
1733
+
1734
+ # Check if bifurcation was triggered (event recorded)
1735
+ bifurcation_events = G.graph.get("zhir_bifurcation_events", [])
1736
+ bifurcation_triggered = len(bifurcation_events) > 0
1737
+ bifurcation_event_count = len(bifurcation_events)
1738
+
1739
+ # === STRUCTURAL PRESERVATION ===
1740
+ epi_kind_before = G.nodes[node].get("_epi_kind_before")
1741
+ epi_kind_after = G.nodes[node].get("epi_kind")
1742
+ identity_preserved = (
1743
+ epi_kind_before == epi_kind_after if epi_kind_before is not None else True
1744
+ )
1745
+
1746
+ # Track frequency and pressure changes
1747
+ delta_vf = vf_after - vf_before if vf_before is not None else 0.0
1748
+ delta_dnfr = dnfr_after - dnfr_before if dnfr_before is not None else 0.0
1749
+
1750
+ # === NETWORK IMPACT ===
1751
+ neighbors = list(G.neighbors(node))
1752
+ neighbor_count = len(neighbors)
1753
+
1754
+ # Count neighbors that experienced phase shifts
1755
+ # This is a simplified heuristic - we check if neighbors have recent phase changes
1756
+ impacted_neighbors = 0
1757
+ phase_impact_threshold = 0.1
1758
+
1759
+ if neighbor_count > 0:
1760
+ # Check neighbors for phase alignment/disruption
1761
+ for n in neighbors:
1762
+ neighbor_theta = _get_node_attr(G, n, ALIAS_THETA)
1763
+ # Simplified: check if neighbor is in similar phase regime after mutation
1764
+ phase_diff = abs(neighbor_theta - theta_after)
1765
+ # If phase diff is large, neighbor might be impacted
1766
+ if phase_diff > phase_impact_threshold:
1767
+ # Check if neighbor has changed recently (has history)
1768
+ neighbor_theta_history = G.nodes[n].get("theta_history", [])
1769
+ if len(neighbor_theta_history) >= 2:
1770
+ neighbor_change = abs(
1771
+ neighbor_theta_history[-1] - neighbor_theta_history[-2]
1772
+ )
1773
+ if neighbor_change > 0.05: # Neighbor experienced change
1774
+ impacted_neighbors += 1
1775
+
1776
+ # Phase coherence with neighbors after mutation
1777
+ from ..metrics.phase_coherence import compute_phase_alignment
1778
+
1779
+ phase_coherence = compute_phase_alignment(G, node, radius=1)
1780
+ else:
1781
+ phase_coherence = 0.0
1782
+
1783
+ # === DESTABILIZER CONTEXT (R4 Extended) ===
1784
+ mutation_context = G.nodes[node].get("_mutation_context", {})
1785
+ destabilizer_type = mutation_context.get("destabilizer_type")
1786
+ destabilizer_operator = mutation_context.get("destabilizer_operator")
1787
+ destabilizer_distance = mutation_context.get("destabilizer_distance")
1788
+ recent_history = mutation_context.get("recent_history", [])
1789
+
1790
+ # === GRAMMAR VALIDATION (U4b) ===
1791
+ # Check if U4b satisfied (IL precedence + recent destabilizer)
1792
+ glyph_history = G.nodes[node].get("glyph_history", [])
1793
+
1794
+ # Look for IL in history
1795
+ il_precedence_found = any("IL" in str(g) for g in glyph_history)
1796
+
1797
+ # Check if destabilizer is recent (within ~3 operators)
1798
+ destabilizer_recent = (
1799
+ destabilizer_distance is not None and destabilizer_distance <= 3
1800
+ )
1801
+
1802
+ grammar_u4b_satisfied = il_precedence_found and destabilizer_recent
1803
+
1804
+ # === RETURN COMPREHENSIVE METRICS ===
1805
+ return {
1806
+ # === CORE (existing) ===
1807
+ "operator": "Mutation",
1808
+ "glyph": "ZHIR",
1809
+ "theta_shift": theta_shift_magnitude,
1810
+ "theta_shift_signed": (
1811
+ theta_shift_stored if theta_shift_stored is not None else theta_shift
1812
+ ),
1813
+ "theta_before": theta_before,
1814
+ "theta_after": theta_after,
1815
+ "theta_final": theta_after,
1816
+ "phase_change": theta_shift_magnitude > 0.5, # Configurable threshold
1817
+ "transformation_mode": "fixed" if fixed_mode else "canonical",
1818
+ # === THRESHOLD VERIFICATION (ENHANCED) ===
1819
+ "depi_dt": depi_dt,
1820
+ "threshold_xi": xi,
1821
+ "threshold_met": threshold_met,
1822
+ "threshold_ratio": threshold_ratio,
1823
+ "threshold_exceeded_by": max(0.0, depi_dt - xi),
1824
+ "threshold_warning": G.nodes[node].get("_zhir_threshold_warning", False),
1825
+ "threshold_validated": G.nodes[node].get("_zhir_threshold_met", False),
1826
+ "threshold_unknown": G.nodes[node].get("_zhir_threshold_unknown", False),
1827
+ # === PHASE TRANSFORMATION (ENHANCED) ===
1828
+ "theta_regime_before": regime_before,
1829
+ "theta_regime_after": regime_after,
1830
+ "regime_changed": regime_changed or (regime_before != regime_after),
1831
+ "theta_regime_change": regime_changed or (regime_before != regime_after), # Backwards compat
1832
+ "regime_before": regime_before, # Backwards compat
1833
+ "regime_after": regime_after, # Backwards compat
1834
+ "theta_shift_direction": math.copysign(1.0, theta_shift),
1835
+ "phase_transformation_magnitude": phase_transformation_magnitude,
1836
+ # === BIFURCATION ANALYSIS (NEW) ===
1837
+ "d2epi": d2epi,
1838
+ "bifurcation_threshold_tau": tau,
1839
+ "bifurcation_potential": bifurcation_potential,
1840
+ "bifurcation_score": bifurcation_score,
1841
+ "bifurcation_triggered": bifurcation_triggered,
1842
+ "bifurcation_event_count": bifurcation_event_count,
1843
+ # === EPI METRICS ===
1844
+ "delta_epi": epi_after - epi_before,
1845
+ "epi_before": epi_before,
1846
+ "epi_after": epi_after,
1847
+ "epi_final": epi_after,
1848
+ # === STRUCTURAL PRESERVATION (NEW) ===
1849
+ "epi_kind_before": epi_kind_before,
1850
+ "epi_kind_after": epi_kind_after,
1851
+ "identity_preserved": identity_preserved,
1852
+ "delta_vf": delta_vf,
1853
+ "vf_before": vf_before if vf_before is not None else vf_after,
1854
+ "vf_final": vf_after,
1855
+ "delta_dnfr": delta_dnfr,
1856
+ "dnfr_before": dnfr_before if dnfr_before is not None else dnfr_after,
1857
+ "dnfr_final": dnfr_after,
1858
+ # === NETWORK IMPACT (NEW) ===
1859
+ "neighbor_count": neighbor_count,
1860
+ "impacted_neighbors": impacted_neighbors,
1861
+ "network_impact_radius": (
1862
+ impacted_neighbors / neighbor_count if neighbor_count > 0 else 0.0
1863
+ ),
1864
+ "phase_coherence_neighbors": phase_coherence,
1865
+ # === DESTABILIZER CONTEXT (NEW - R4 Extended) ===
1866
+ "destabilizer_type": destabilizer_type,
1867
+ "destabilizer_operator": destabilizer_operator,
1868
+ "destabilizer_distance": destabilizer_distance,
1869
+ "recent_history": recent_history,
1870
+ # === GRAMMAR VALIDATION (NEW) ===
1871
+ "grammar_u4b_satisfied": grammar_u4b_satisfied,
1872
+ "il_precedence_found": il_precedence_found,
1873
+ "destabilizer_recent": destabilizer_recent,
1874
+ # === METADATA ===
1875
+ "metrics_version": "2.0_canonical",
1876
+ }
1877
+
1878
+
1879
+ def transition_metrics(
1880
+ G: TNFRGraph,
1881
+ node: NodeId,
1882
+ dnfr_before: float,
1883
+ vf_before: float,
1884
+ theta_before: float,
1885
+ epi_before: float | None = None,
1886
+ ) -> dict[str, Any]:
1887
+ """NAV - Transition metrics: regime classification, phase shift, frequency scaling.
1888
+
1889
+ Collects comprehensive transition metrics including regime origin/destination,
1890
+ phase shift magnitude (properly wrapped), transition type classification, and
1891
+ structural preservation ratios as specified in TNFR.pdf Table 2.3.
1892
+
1893
+ Parameters
1894
+ ----------
1895
+ G : TNFRGraph
1896
+ Graph containing the node
1897
+ node : NodeId
1898
+ Node to collect metrics from
1899
+ dnfr_before : float
1900
+ ΔNFR value before operator application
1901
+ vf_before : float
1902
+ νf value before operator application
1903
+ theta_before : float
1904
+ Phase value before operator application
1905
+ epi_before : float, optional
1906
+ EPI value before operator application (for preservation tracking)
1907
+
1908
+ Returns
1909
+ -------
1910
+ dict
1911
+ Transition-specific metrics including:
1912
+
1913
+ **Core metrics (existing)**:
1914
+
1915
+ - operator: "Transition"
1916
+ - glyph: "NAV"
1917
+ - delta_theta: Signed phase change
1918
+ - delta_vf: Change in νf
1919
+ - delta_dnfr: Change in ΔNFR
1920
+ - dnfr_final: Final ΔNFR value
1921
+ - vf_final: Final νf value
1922
+ - theta_final: Final phase value
1923
+ - transition_complete: Boolean (|ΔNFR| < |νf|)
1924
+
1925
+ **Regime classification (NEW)**:
1926
+
1927
+ - regime_origin: "latent" | "active" | "resonant"
1928
+ - regime_destination: "latent" | "active" | "resonant"
1929
+ - transition_type: "reactivation" | "phase_shift" | "regime_change"
1930
+
1931
+ **Phase metrics (NEW)**:
1932
+
1933
+ - phase_shift_magnitude: Absolute phase change (radians, 0-π)
1934
+ - phase_shift_signed: Signed phase change (radians, wrapped to [-π, π])
1935
+
1936
+ **Structural scaling (NEW)**:
1937
+
1938
+ - vf_scaling_factor: vf_after / vf_before
1939
+ - dnfr_damping_ratio: dnfr_after / dnfr_before
1940
+ - epi_preservation: epi_after / epi_before (if epi_before provided)
1941
+
1942
+ **Latency tracking (NEW)**:
1943
+
1944
+ - latency_duration: Time in silence (seconds) if transitioning from SHA
1945
+
1946
+ Notes
1947
+ -----
1948
+ **Regime Classification**:
1949
+
1950
+ - **Latent**: latent flag set OR νf < 0.05
1951
+ - **Active**: Default operational state
1952
+ - **Resonant**: EPI > 0.5 AND νf > 0.8
1953
+
1954
+ **Transition Type**:
1955
+
1956
+ - **reactivation**: From latent state (SHA → NAV flow)
1957
+ - **phase_shift**: Significant phase change (|Δθ| > 0.3 rad)
1958
+ - **regime_change**: Regime switch without significant phase shift
1959
+
1960
+ **Phase Shift Wrapping**:
1961
+
1962
+ Phase shifts are properly wrapped to [-π, π] range to handle 0-2π boundary
1963
+ crossings correctly, ensuring accurate phase change measurement.
1964
+
1965
+ Examples
1966
+ --------
1967
+ >>> from tnfr.structural import create_nfr, run_sequence
1968
+ >>> from tnfr.operators.definitions import Silence, Transition
1969
+ >>>
1970
+ >>> # Example: SHA → NAV reactivation
1971
+ >>> G, node = create_nfr("test", epi=0.5, vf=0.8)
1972
+ >>> G.graph["COLLECT_OPERATOR_METRICS"] = True
1973
+ >>> run_sequence(G, node, [Silence(), Transition()])
1974
+ >>>
1975
+ >>> metrics = G.graph["operator_metrics"][-1]
1976
+ >>> assert metrics["operator"] == "Transition"
1977
+ >>> assert metrics["transition_type"] == "reactivation"
1978
+ >>> assert metrics["regime_origin"] == "latent"
1979
+ >>> assert metrics["latency_duration"] is not None
1980
+
1981
+ See Also
1982
+ --------
1983
+ operators.definitions.Transition : NAV operator implementation
1984
+ operators.definitions.Transition._detect_regime : Regime detection logic
1985
+ """
1986
+ import math
1987
+
1988
+ # Get current state (after transformation)
1989
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
1990
+ dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
1991
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
1992
+ theta_after = _get_node_attr(G, node, ALIAS_THETA)
1993
+
1994
+ # === REGIME CLASSIFICATION ===
1995
+ # Get regime origin from node attribute (stored by Transition operator before super().__call__)
1996
+ regime_origin = G.nodes[node].get("_regime_before", None)
1997
+ if regime_origin is None:
1998
+ # Fallback: detect regime from before state
1999
+ regime_origin = _detect_regime_from_state(
2000
+ epi_before or epi_after, vf_before, False # Cannot access latent flag from before
2001
+ )
2002
+
2003
+ # Detect destination regime
2004
+ regime_destination = _detect_regime_from_state(
2005
+ epi_after, vf_after, G.nodes[node].get("latent", False)
2006
+ )
2007
+
2008
+ # === TRANSITION TYPE CLASSIFICATION ===
2009
+ # Calculate phase shift (properly wrapped)
2010
+ phase_shift_raw = theta_after - theta_before
2011
+ if phase_shift_raw > math.pi:
2012
+ phase_shift_raw -= 2 * math.pi
2013
+ elif phase_shift_raw < -math.pi:
2014
+ phase_shift_raw += 2 * math.pi
2015
+
2016
+ # Classify transition type
2017
+ if regime_origin == "latent":
2018
+ transition_type = "reactivation"
2019
+ elif abs(phase_shift_raw) > 0.3:
2020
+ transition_type = "phase_shift"
2021
+ else:
2022
+ transition_type = "regime_change"
2023
+
2024
+ # === STRUCTURAL SCALING FACTORS ===
2025
+ vf_scaling = vf_after / vf_before if vf_before > 0 else 1.0
2026
+ dnfr_damping = dnfr_after / dnfr_before if abs(dnfr_before) > 1e-9 else 1.0
2027
+
2028
+ # === EPI PRESERVATION ===
2029
+ epi_preservation = None
2030
+ if epi_before is not None and epi_before > 0:
2031
+ epi_preservation = epi_after / epi_before
2032
+
2033
+ # === LATENCY DURATION ===
2034
+ # Get from node if transitioning from silence
2035
+ latency_duration = G.nodes[node].get("silence_duration", None)
2036
+
2037
+ return {
2038
+ # === CORE (existing, preserved) ===
2039
+ "operator": "Transition",
2040
+ "glyph": "NAV",
2041
+ "delta_theta": phase_shift_raw,
2042
+ "delta_vf": vf_after - vf_before,
2043
+ "delta_dnfr": dnfr_after - dnfr_before,
2044
+ "dnfr_final": dnfr_after,
2045
+ "vf_final": vf_after,
2046
+ "theta_final": theta_after,
2047
+ "transition_complete": abs(dnfr_after) < abs(vf_after),
2048
+ # Legacy compatibility
2049
+ "dnfr_change": abs(dnfr_after - dnfr_before),
2050
+ "vf_change": abs(vf_after - vf_before),
2051
+ "theta_shift": abs(phase_shift_raw),
2052
+ # === REGIME CLASSIFICATION (NEW) ===
2053
+ "regime_origin": regime_origin,
2054
+ "regime_destination": regime_destination,
2055
+ "transition_type": transition_type,
2056
+ # === PHASE METRICS (NEW) ===
2057
+ "phase_shift_magnitude": abs(phase_shift_raw),
2058
+ "phase_shift_signed": phase_shift_raw,
2059
+ # === STRUCTURAL SCALING (NEW) ===
2060
+ "vf_scaling_factor": vf_scaling,
2061
+ "dnfr_damping_ratio": dnfr_damping,
2062
+ "epi_preservation": epi_preservation,
2063
+ # === LATENCY TRACKING (NEW) ===
2064
+ "latency_duration": latency_duration,
2065
+ }
2066
+
2067
+
2068
+ def _detect_regime_from_state(epi: float, vf: float, latent: bool) -> str:
2069
+ """Detect structural regime from node state.
2070
+
2071
+ Helper function for transition_metrics to classify regime without
2072
+ accessing the Transition operator directly.
2073
+
2074
+ Parameters
2075
+ ----------
2076
+ epi : float
2077
+ EPI value
2078
+ vf : float
2079
+ νf value
2080
+ latent : bool
2081
+ Latent flag
2082
+
2083
+ Returns
2084
+ -------
2085
+ str
2086
+ Regime classification: "latent", "active", or "resonant"
2087
+
2088
+ Notes
2089
+ -----
2090
+ Matches logic in Transition._detect_regime (definitions.py).
2091
+ """
2092
+ if latent or vf < 0.05:
2093
+ return "latent"
2094
+ elif epi > 0.5 and vf > 0.8:
2095
+ return "resonant"
2096
+ else:
2097
+ return "active"
2098
+
2099
+
2100
+ def recursivity_metrics(
2101
+ G: TNFRGraph, node: NodeId, epi_before: float, vf_before: float
2102
+ ) -> dict[str, Any]:
2103
+ """REMESH - Recursivity metrics: fractal propagation, multi-scale coherence.
2104
+
2105
+ Parameters
2106
+ ----------
2107
+ G : TNFRGraph
2108
+ Graph containing the node
2109
+ node : NodeId
2110
+ Node to collect metrics from
2111
+ epi_before : float
2112
+ EPI value before operator application
2113
+ vf_before : float
2114
+ νf value before operator application
2115
+
2116
+ Returns
2117
+ -------
2118
+ dict
2119
+ Recursivity-specific metrics including fractal pattern indicators
2120
+ """
2121
+ epi_after = _get_node_attr(G, node, ALIAS_EPI)
2122
+ vf_after = _get_node_attr(G, node, ALIAS_VF)
2123
+
2124
+ # Track echo traces if graph maintains them
2125
+ echo_traces = G.graph.get("echo_trace", [])
2126
+ echo_count = len(echo_traces)
2127
+
2128
+ return {
2129
+ "operator": "Recursivity",
2130
+ "glyph": "REMESH",
2131
+ "delta_epi": epi_after - epi_before,
2132
+ "delta_vf": vf_after - vf_before,
2133
+ "epi_final": epi_after,
2134
+ "vf_final": vf_after,
2135
+ "echo_count": echo_count,
2136
+ "fractal_depth": echo_count,
2137
+ "multi_scale_active": echo_count > 0,
2138
+ }